mirror of
https://github.com/n08i40k/schedule-parser-rusted.git
synced 2025-12-06 09:47:50 +03:00
0.4.0
Авторизация через токен вк Слияние schedule_parser с проектом Перенос схемы запросов/ответов в файлы эндпоинтов Переход с библиотеки jwt на jsonwebtokens
This commit is contained in:
@@ -2,9 +2,9 @@ use crate::xls_downloader::basic_impl::BasicXlsDownloader;
|
||||
use actix_web::web;
|
||||
use chrono::{DateTime, Utc};
|
||||
use diesel::{Connection, PgConnection};
|
||||
use schedule_parser::schema::ParseResult;
|
||||
use std::env;
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
use crate::parser::schema::ParseResult;
|
||||
|
||||
pub struct Schedule {
|
||||
pub etag: String,
|
||||
|
||||
@@ -2,13 +2,13 @@ pub mod users {
|
||||
use crate::database::models::User;
|
||||
use crate::database::schema::users::dsl::users;
|
||||
use crate::database::schema::users::dsl::*;
|
||||
use diesel::{insert_into, ExpressionMethods, QueryResult};
|
||||
use diesel::{ExpressionMethods, QueryResult, insert_into};
|
||||
use diesel::{PgConnection, SelectableHelper};
|
||||
use diesel::{QueryDsl, RunQueryDsl};
|
||||
use std::ops::DerefMut;
|
||||
use std::sync::Mutex;
|
||||
|
||||
pub fn get(connection: &Mutex<PgConnection>, _id: String) -> QueryResult<User> {
|
||||
pub fn get(connection: &Mutex<PgConnection>, _id: &String) -> QueryResult<User> {
|
||||
let mut lock = connection.lock().unwrap();
|
||||
let con = lock.deref_mut();
|
||||
|
||||
@@ -20,7 +20,7 @@ pub mod users {
|
||||
|
||||
pub fn get_by_username(
|
||||
connection: &Mutex<PgConnection>,
|
||||
_username: String,
|
||||
_username: &String,
|
||||
) -> QueryResult<User> {
|
||||
let mut lock = connection.lock().unwrap();
|
||||
let con = lock.deref_mut();
|
||||
@@ -30,8 +30,21 @@ pub mod users {
|
||||
.select(User::as_select())
|
||||
.first(con)
|
||||
}
|
||||
|
||||
pub fn get_by_vk_id(
|
||||
connection: &Mutex<PgConnection>,
|
||||
_vk_id: i32,
|
||||
) -> QueryResult<User> {
|
||||
let mut lock = connection.lock().unwrap();
|
||||
let con = lock.deref_mut();
|
||||
|
||||
pub fn contains_by_username(connection: &Mutex<PgConnection>, _username: String) -> bool {
|
||||
users
|
||||
.filter(vk_id.eq(_vk_id))
|
||||
.select(User::as_select())
|
||||
.first(con)
|
||||
}
|
||||
|
||||
pub fn contains_by_username(connection: &Mutex<PgConnection>, _username: &String) -> bool {
|
||||
let mut lock = connection.lock().unwrap();
|
||||
let con = lock.deref_mut();
|
||||
|
||||
@@ -45,7 +58,21 @@ pub mod users {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_by_username(connection: &Mutex<PgConnection>, _username: String) -> bool {
|
||||
pub fn contains_by_vk_id(connection: &Mutex<PgConnection>, _vk_id: i32) -> bool {
|
||||
let mut lock = connection.lock().unwrap();
|
||||
let con = lock.deref_mut();
|
||||
|
||||
match users
|
||||
.filter(vk_id.eq(_vk_id))
|
||||
.count()
|
||||
.get_result::<i64>(con)
|
||||
{
|
||||
Ok(count) => count > 0,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_by_username(connection: &Mutex<PgConnection>, _username: &String) -> bool {
|
||||
let mut lock = connection.lock().unwrap();
|
||||
let con = lock.deref_mut();
|
||||
|
||||
@@ -61,11 +88,14 @@ pub mod users {
|
||||
|
||||
insert_into(users).values(user).execute(con)
|
||||
}
|
||||
|
||||
|
||||
pub fn insert_or_ignore(connection: &Mutex<PgConnection>, user: &User) -> QueryResult<usize> {
|
||||
let mut lock = connection.lock().unwrap();
|
||||
let con = lock.deref_mut();
|
||||
|
||||
insert_into(users).values(user).on_conflict_do_nothing().execute(con)
|
||||
insert_into(users)
|
||||
.values(user)
|
||||
.on_conflict_do_nothing()
|
||||
.execute(con)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,4 +23,4 @@ pub struct User {
|
||||
pub group: String,
|
||||
pub role: UserRole,
|
||||
pub version: String,
|
||||
}
|
||||
}
|
||||
1
src/lib.rs
Normal file
1
src/lib.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod parser;
|
||||
14
src/main.rs
14
src/main.rs
@@ -1,5 +1,6 @@
|
||||
use crate::app_state::{AppState, app_state};
|
||||
use crate::routes::auth::sign_in::sign_in;
|
||||
use crate::routes::auth::sign_in::{sign_in_default, sign_in_vk};
|
||||
use crate::routes::auth::sign_up::{sign_up_default, sign_up_vk};
|
||||
use actix_web::{App, HttpServer, web};
|
||||
use dotenvy::dotenv;
|
||||
|
||||
@@ -7,19 +8,24 @@ mod app_state;
|
||||
mod database;
|
||||
mod routes;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_env;
|
||||
|
||||
mod utility;
|
||||
mod xls_downloader;
|
||||
|
||||
mod parser;
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() {
|
||||
dotenv().ok();
|
||||
|
||||
HttpServer::new(move || {
|
||||
let schedule_scope = web::scope("/auth").service(sign_in);
|
||||
let api_scope = web::scope("/api/v1").service(schedule_scope);
|
||||
let auth_scope = web::scope("/auth")
|
||||
.service(sign_in_default)
|
||||
.service(sign_in_vk)
|
||||
.service(sign_up_default)
|
||||
.service(sign_up_vk);
|
||||
let api_scope = web::scope("/api/v1").service(auth_scope);
|
||||
|
||||
App::new().app_data(move || app_state()).service(api_scope)
|
||||
})
|
||||
|
||||
709
src/parser/mod.rs
Normal file
709
src/parser/mod.rs
Normal file
@@ -0,0 +1,709 @@
|
||||
use crate::parser::LessonParseResult::{Lessons, Street};
|
||||
use crate::parser::schema::LessonType::Break;
|
||||
use crate::parser::schema::{
|
||||
Day, Lesson, LessonSubGroup, LessonTime, LessonType, ParseResult, ScheduleEntry,
|
||||
};
|
||||
use calamine::{Reader, Xls, open_workbook_from_rs};
|
||||
use chrono::{Duration, NaiveDateTime};
|
||||
use fuzzy_matcher::FuzzyMatcher;
|
||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
pub mod schema;
|
||||
|
||||
struct InternalId {
|
||||
/**
|
||||
* Индекс строки
|
||||
*/
|
||||
row: u32,
|
||||
|
||||
/**
|
||||
* Индекс столбца
|
||||
*/
|
||||
column: u32,
|
||||
|
||||
/**
|
||||
* Текст в ячейке
|
||||
*/
|
||||
name: String,
|
||||
}
|
||||
|
||||
struct InternalTime {
|
||||
/**
|
||||
* Временной отрезок проведения пары
|
||||
*/
|
||||
time_range: LessonTime,
|
||||
|
||||
/**
|
||||
* Тип пары
|
||||
*/
|
||||
lesson_type: LessonType,
|
||||
|
||||
/**
|
||||
* Индекс пары
|
||||
*/
|
||||
default_index: Option<u32>,
|
||||
|
||||
/**
|
||||
* Рамка ячейки
|
||||
*/
|
||||
xls_range: ((u32, u32), (u32, u32)),
|
||||
}
|
||||
|
||||
type WorkSheet = calamine::Range<calamine::Data>;
|
||||
|
||||
fn get_string_from_cell(worksheet: &WorkSheet, row: u32, col: u32) -> Option<String> {
|
||||
let cell_data = if let Some(data) = worksheet.get((row as usize, col as usize)) {
|
||||
data.to_string()
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if cell_data.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
static NL_RE: LazyLock<Regex, fn() -> Regex> =
|
||||
LazyLock::new(|| Regex::new(r"[\n\r]+").unwrap());
|
||||
static SP_RE: LazyLock<Regex, fn() -> Regex> = LazyLock::new(|| Regex::new(r"\s+").unwrap());
|
||||
|
||||
let trimmed_data = SP_RE
|
||||
.replace_all(&NL_RE.replace_all(&cell_data, " "), " ")
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
if trimmed_data.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed_data)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_merge_from_start(worksheet: &WorkSheet, row: u32, column: u32) -> ((u32, u32), (u32, u32)) {
|
||||
let worksheet_end = worksheet.end().unwrap();
|
||||
|
||||
let row_end: u32 = {
|
||||
let mut r: u32 = 0;
|
||||
|
||||
for _r in (row + 1)..worksheet_end.0 {
|
||||
r = _r;
|
||||
|
||||
if let Some(_) = worksheet.get((_r as usize, column as usize)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
r
|
||||
};
|
||||
|
||||
let column_end: u32 = {
|
||||
let mut c: u32 = 0;
|
||||
|
||||
for _c in (column + 1)..worksheet_end.1 {
|
||||
c = _c;
|
||||
|
||||
if let Some(_) = worksheet.get((row as usize, _c as usize)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
c
|
||||
};
|
||||
|
||||
((row, column), (row_end, column_end))
|
||||
}
|
||||
|
||||
fn parse_skeleton(worksheet: &WorkSheet) -> (Vec<InternalId>, Vec<InternalId>) {
|
||||
let range = &worksheet;
|
||||
|
||||
let mut is_parsed = false;
|
||||
|
||||
let mut groups: Vec<InternalId> = Vec::new();
|
||||
let mut days: Vec<InternalId> = Vec::new();
|
||||
|
||||
let start = range.start().expect("Could not find start");
|
||||
let end = range.end().expect("Could not find end");
|
||||
|
||||
let mut row = start.0;
|
||||
while row < end.0 {
|
||||
row += 1;
|
||||
|
||||
let day_name_opt = get_string_from_cell(&worksheet, row, 0);
|
||||
if day_name_opt.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let day_name = day_name_opt.unwrap();
|
||||
|
||||
if !is_parsed {
|
||||
is_parsed = true;
|
||||
|
||||
row -= 1;
|
||||
|
||||
for column in (start.1 + 2)..=end.1 {
|
||||
let group_name = get_string_from_cell(&worksheet, row, column);
|
||||
if group_name.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
groups.push(InternalId {
|
||||
row,
|
||||
column,
|
||||
name: group_name.unwrap(),
|
||||
});
|
||||
}
|
||||
|
||||
row += 1;
|
||||
}
|
||||
|
||||
days.push(InternalId {
|
||||
row,
|
||||
column: 0,
|
||||
name: day_name.clone(),
|
||||
});
|
||||
|
||||
if days.len() > 2 && day_name.starts_with("Суббота") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
(days, groups)
|
||||
}
|
||||
|
||||
enum LessonParseResult {
|
||||
Lessons(Vec<Lesson>),
|
||||
Street(String),
|
||||
}
|
||||
|
||||
trait StringInnerSlice {
|
||||
fn inner_slice(&self, from: usize, to: usize) -> Self;
|
||||
}
|
||||
|
||||
impl StringInnerSlice for String {
|
||||
fn inner_slice(&self, from: usize, to: usize) -> Self {
|
||||
self.chars()
|
||||
.take(from)
|
||||
.chain(self.chars().skip(to))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn guess_lesson_type(name: &String) -> Option<(String, LessonType)> {
|
||||
let map: HashMap<String, LessonType> = HashMap::from([
|
||||
("(консультация)".to_string(), LessonType::Consultation),
|
||||
(
|
||||
"самостоятельная работа".to_string(),
|
||||
LessonType::IndependentWork,
|
||||
),
|
||||
("зачет".to_string(), LessonType::Exam),
|
||||
("зачет с оценкой".to_string(), LessonType::ExamWithGrade),
|
||||
("экзамен".to_string(), LessonType::ExamDefault),
|
||||
]);
|
||||
|
||||
let matcher = SkimMatcherV2::default();
|
||||
let name_lower = name.to_lowercase();
|
||||
|
||||
type SearchResult<'a> = (&'a LessonType, i64, Vec<usize>);
|
||||
|
||||
let mut search_results: Vec<SearchResult> = map
|
||||
.iter()
|
||||
.map(|entry| -> SearchResult {
|
||||
if let Some((score, indices)) = matcher.fuzzy_indices(&*name_lower, entry.0) {
|
||||
return (entry.1, score, indices);
|
||||
}
|
||||
|
||||
(entry.1, 0, Vec::new())
|
||||
})
|
||||
.collect();
|
||||
search_results.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
|
||||
let guessed_type = search_results.first().unwrap();
|
||||
|
||||
if guessed_type.1 > 80 {
|
||||
Some((
|
||||
name.inner_slice(guessed_type.2[0], guessed_type.2[guessed_type.2.len() - 1]),
|
||||
guessed_type.0.clone(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_lesson(
|
||||
worksheet: &WorkSheet,
|
||||
day: &mut Day,
|
||||
day_times: &Vec<InternalTime>,
|
||||
time: &InternalTime,
|
||||
column: u32,
|
||||
) -> LessonParseResult {
|
||||
let row = time.xls_range.0.0;
|
||||
|
||||
let (name, lesson_type) = {
|
||||
let raw_name_opt = get_string_from_cell(&worksheet, row, column);
|
||||
if raw_name_opt.is_none() {
|
||||
return Lessons(Vec::new());
|
||||
}
|
||||
|
||||
let raw_name = raw_name_opt.unwrap();
|
||||
|
||||
static OTHER_STREET_RE: LazyLock<Regex, fn() -> Regex> =
|
||||
LazyLock::new(|| Regex::new(r"^[А-Я][а-я]+,?\s?[0-9]+$").unwrap());
|
||||
|
||||
if OTHER_STREET_RE.is_match(&raw_name) {
|
||||
return Street(raw_name);
|
||||
}
|
||||
|
||||
if let Some(guess) = guess_lesson_type(&raw_name) {
|
||||
guess
|
||||
} else {
|
||||
(raw_name, time.lesson_type.clone())
|
||||
}
|
||||
};
|
||||
|
||||
let (default_range, lesson_time): (Option<[u8; 2]>, LessonTime) = {
|
||||
// check if multi-lesson
|
||||
let cell_range = get_merge_from_start(worksheet, row, column);
|
||||
|
||||
let end_time_arr = day_times
|
||||
.iter()
|
||||
.filter(|time| time.xls_range.1.0 == cell_range.1.0)
|
||||
.collect::<Vec<&InternalTime>>();
|
||||
|
||||
let end_time = end_time_arr.first().expect("Unable to find lesson time!");
|
||||
|
||||
let range: Option<[u8; 2]> = if time.default_index != None {
|
||||
let default = time.default_index.unwrap() as u8;
|
||||
Some([default, end_time.default_index.unwrap() as u8])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let time = LessonTime {
|
||||
start: time.time_range.start,
|
||||
end: end_time.time_range.end,
|
||||
};
|
||||
|
||||
(range, time)
|
||||
};
|
||||
|
||||
let (name, mut subgroups) = parse_name_and_subgroups(&name);
|
||||
|
||||
{
|
||||
let cabinets: Vec<String> = parse_cabinets(worksheet, row, column + 1);
|
||||
|
||||
// Если количество кабинетов равно 1, назначаем этот кабинет всем подгруппам
|
||||
if cabinets.len() == 1 {
|
||||
for subgroup in &mut subgroups {
|
||||
subgroup.cabinet = Some(cabinets.get(0).or(Some(&String::new())).unwrap().clone())
|
||||
}
|
||||
}
|
||||
// Если количество кабинетов совпадает с количеством подгрупп, назначаем кабинеты по порядку
|
||||
else if cabinets.len() == subgroups.len() {
|
||||
for subgroup in &mut subgroups {
|
||||
subgroup.cabinet = Some(
|
||||
cabinets
|
||||
.get((subgroup.number - 1) as usize)
|
||||
.unwrap()
|
||||
.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Если количество кабинетов больше количества подгрупп, делаем ещё одну подгруппу.
|
||||
else if cabinets.len() > subgroups.len() {
|
||||
for index in 0..subgroups.len() {
|
||||
subgroups[index].cabinet = Some(cabinets[index].clone());
|
||||
}
|
||||
|
||||
while cabinets.len() > subgroups.len() {
|
||||
subgroups.push(LessonSubGroup {
|
||||
number: (subgroups.len() + 1) as u8,
|
||||
cabinet: Some(cabinets[subgroups.len()].clone()),
|
||||
teacher: "Ошибка в расписании".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
// Если кабинетов нет, но есть подгруппы, назначаем им значение "??"
|
||||
else {
|
||||
for subgroup in &mut subgroups {
|
||||
subgroup.cabinet = Some("??".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
cabinets
|
||||
};
|
||||
|
||||
let lesson = Lesson {
|
||||
lesson_type,
|
||||
default_range,
|
||||
name: Some(name),
|
||||
time: lesson_time,
|
||||
subgroups: Some(subgroups),
|
||||
group: None,
|
||||
};
|
||||
|
||||
let prev_lesson = if day.lessons.len() == 0 {
|
||||
return Lessons(Vec::from([lesson]));
|
||||
} else {
|
||||
&day.lessons[day.lessons.len() - 1]
|
||||
};
|
||||
|
||||
Lessons(Vec::from([
|
||||
Lesson {
|
||||
lesson_type: Break,
|
||||
default_range: None,
|
||||
name: None,
|
||||
time: LessonTime {
|
||||
start: prev_lesson.time.end,
|
||||
end: lesson.time.start,
|
||||
},
|
||||
subgroups: Some(Vec::new()),
|
||||
group: None,
|
||||
},
|
||||
lesson,
|
||||
]))
|
||||
}
|
||||
|
||||
fn parse_cabinets(worksheet: &WorkSheet, row: u32, column: u32) -> Vec<String> {
|
||||
let mut cabinets: Vec<String> = Vec::new();
|
||||
|
||||
if let Some(raw) = get_string_from_cell(&worksheet, row, column) {
|
||||
let clean = raw.replace("\n", " ");
|
||||
let parts: Vec<&str> = clean.split(" ").collect();
|
||||
|
||||
for part in parts {
|
||||
let clean_part = part.to_string().trim().to_string();
|
||||
|
||||
cabinets.push(clean_part);
|
||||
}
|
||||
}
|
||||
|
||||
cabinets
|
||||
}
|
||||
|
||||
fn parse_name_and_subgroups(name: &String) -> (String, Vec<LessonSubGroup>) {
|
||||
static LESSON_RE: LazyLock<Regex, fn() -> Regex> =
|
||||
LazyLock::new(|| Regex::new(r"(?:[А-Я][а-я]+[А-Я]{2}(?:\([0-9][а-я]+\))?)+$").unwrap());
|
||||
static TEACHER_RE: LazyLock<Regex, fn() -> Regex> =
|
||||
LazyLock::new(|| Regex::new(r"([А-Я][а-я]+)([А-Я])([А-Я])(?:\(([0-9])[а-я]+\))?").unwrap());
|
||||
static CLEAN_RE: LazyLock<Regex, fn() -> Regex> =
|
||||
LazyLock::new(|| Regex::new(r"[\s.,]+").unwrap());
|
||||
static NAME_CLEAN_RE: LazyLock<Regex, fn() -> Regex> =
|
||||
LazyLock::new(|| Regex::new(r"\.\s+$").unwrap());
|
||||
|
||||
let (teachers, lesson_name) = {
|
||||
let clean_name = CLEAN_RE.replace_all(&name, "").to_string();
|
||||
|
||||
if let Some(captures) = LESSON_RE.captures(&clean_name) {
|
||||
let capture = captures.get(0).unwrap();
|
||||
let capture_str = capture.as_str().to_string();
|
||||
let capture_name: String = capture_str.chars().take(5).collect();
|
||||
|
||||
(
|
||||
NAME_CLEAN_RE.replace(&capture_str, "").to_string(),
|
||||
name[0..name.find(&*capture_name).unwrap()].to_string(),
|
||||
)
|
||||
} else {
|
||||
return (NAME_CLEAN_RE.replace(&name, "").to_string(), Vec::new());
|
||||
}
|
||||
};
|
||||
|
||||
let mut subgroups: Vec<LessonSubGroup> = Vec::new();
|
||||
|
||||
let teacher_it = TEACHER_RE.captures_iter(&teachers);
|
||||
|
||||
for captures in teacher_it {
|
||||
subgroups.push(LessonSubGroup {
|
||||
number: if let Some(capture) = captures.get(4) {
|
||||
capture
|
||||
.as_str()
|
||||
.to_string()
|
||||
.parse::<u8>()
|
||||
.expect("Unable to read subgroup index!")
|
||||
} else {
|
||||
0
|
||||
},
|
||||
cabinet: None,
|
||||
teacher: format!(
|
||||
"{} {}.{}.",
|
||||
captures.get(1).unwrap().as_str().to_string(),
|
||||
captures.get(2).unwrap().as_str().to_string(),
|
||||
captures.get(3).unwrap().as_str().to_string()
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
// фикс, если у кого-то отсутствует индекс подгруппы
|
||||
|
||||
if subgroups.len() == 1 {
|
||||
let index = subgroups[0].number;
|
||||
|
||||
if index == 0 {
|
||||
subgroups[0].number = 1u8;
|
||||
} else {
|
||||
subgroups.push(LessonSubGroup {
|
||||
number: if index == 1 { 2 } else { 1 },
|
||||
cabinet: None,
|
||||
teacher: "Только у другой".to_string(),
|
||||
});
|
||||
}
|
||||
} else if subgroups.len() == 2 {
|
||||
// если индексы отсутствуют у обоих, ставим поочерёдно
|
||||
if subgroups[0].number == 0 && subgroups[1].number == 0 {
|
||||
subgroups[0].number = 1;
|
||||
subgroups[1].number = 2;
|
||||
}
|
||||
// если индекс отсутствует у первого, ставим 2, если у второго индекс 1 и наоборот
|
||||
else if subgroups[0].number == 0 {
|
||||
subgroups[0].number = if subgroups[1].number == 1 { 2 } else { 1 };
|
||||
}
|
||||
// если индекс отсутствует у второго, ставим 2, если у первого индекс 1 и наоборот
|
||||
else if subgroups[1].number == 0 {
|
||||
subgroups[1].number = if subgroups[0].number == 1 { 2 } else { 1 };
|
||||
}
|
||||
}
|
||||
|
||||
if subgroups.len() == 2 && subgroups[0].number == 2 && subgroups[1].number == 1 {
|
||||
subgroups.reverse()
|
||||
}
|
||||
|
||||
(lesson_name, subgroups)
|
||||
}
|
||||
|
||||
fn convert_groups_to_teachers(
|
||||
groups: &HashMap<String, ScheduleEntry>,
|
||||
) -> HashMap<String, ScheduleEntry> {
|
||||
let mut teachers: HashMap<String, ScheduleEntry> = HashMap::new();
|
||||
|
||||
let empty_days: Vec<Day> = groups
|
||||
.values()
|
||||
.next()
|
||||
.unwrap()
|
||||
.days
|
||||
.iter()
|
||||
.map(|day| Day {
|
||||
name: day.name.clone(),
|
||||
street: day.street.clone(),
|
||||
date: day.date.clone(),
|
||||
lessons: vec![],
|
||||
})
|
||||
.collect();
|
||||
|
||||
for group in groups.values() {
|
||||
for (index, day) in group.days.iter().enumerate() {
|
||||
for group_lesson in &day.lessons {
|
||||
if group_lesson.lesson_type == Break {
|
||||
continue;
|
||||
}
|
||||
|
||||
if group_lesson.subgroups.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let subgroups = group_lesson.subgroups.as_ref().unwrap();
|
||||
|
||||
for subgroup in subgroups {
|
||||
if subgroup.teacher == "Ошибка в расписании" {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !teachers.contains_key(&subgroup.teacher) {
|
||||
teachers.insert(
|
||||
subgroup.teacher.clone(),
|
||||
ScheduleEntry {
|
||||
name: subgroup.teacher.clone(),
|
||||
days: empty_days.to_vec(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let teacher_day = teachers
|
||||
.get_mut(&subgroup.teacher)
|
||||
.unwrap()
|
||||
.days
|
||||
.get_mut(index)
|
||||
.unwrap();
|
||||
|
||||
teacher_day.lessons.push({
|
||||
let mut lesson = group_lesson.clone();
|
||||
lesson.group = Some(group.name.clone());
|
||||
|
||||
lesson
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
teachers
|
||||
}
|
||||
|
||||
pub fn parse_xls(buffer: &Vec<u8>) -> ParseResult {
|
||||
let cursor = Cursor::new(&buffer);
|
||||
let mut workbook: Xls<_> = open_workbook_from_rs(cursor).expect("Can't open workbook");
|
||||
|
||||
let worksheet: WorkSheet = workbook
|
||||
.worksheets()
|
||||
.first()
|
||||
.expect("No worksheet found")
|
||||
.1
|
||||
.to_owned();
|
||||
|
||||
let (days_markup, groups_markup) = parse_skeleton(&worksheet);
|
||||
|
||||
let mut groups: HashMap<String, ScheduleEntry> = HashMap::new();
|
||||
let mut days_times: Vec<Vec<InternalTime>> = Vec::new();
|
||||
|
||||
let saturday_end_row = worksheet.end().unwrap().0;
|
||||
|
||||
for group_markup in groups_markup {
|
||||
let mut group = ScheduleEntry {
|
||||
name: group_markup.name,
|
||||
days: Vec::new(),
|
||||
};
|
||||
|
||||
for day_index in 0..(&days_markup).len() {
|
||||
let day_markup = &days_markup[day_index];
|
||||
|
||||
let mut day = {
|
||||
let space_index = day_markup.name.find(' ').unwrap();
|
||||
|
||||
let name = day_markup.name[..space_index].to_string();
|
||||
|
||||
let date_raw = day_markup.name[space_index + 1..].to_string();
|
||||
let date_add = format!("{} 00:00:00", date_raw);
|
||||
|
||||
let date = NaiveDateTime::parse_from_str(&*date_add, "%d.%m.%Y %H:%M:%S");
|
||||
|
||||
Day {
|
||||
name,
|
||||
street: None,
|
||||
date: date.unwrap().and_utc(),
|
||||
lessons: Vec::new(),
|
||||
}
|
||||
};
|
||||
|
||||
let lesson_time_column = days_markup[0].column + 1;
|
||||
|
||||
let row_distance = if day_index != days_markup.len() - 1 {
|
||||
days_markup[day_index + 1].row
|
||||
} else {
|
||||
saturday_end_row
|
||||
} - day_markup.row;
|
||||
|
||||
if days_times.len() != 6 {
|
||||
let mut day_times: Vec<InternalTime> = Vec::new();
|
||||
|
||||
for row in day_markup.row..(day_markup.row + row_distance) {
|
||||
// time
|
||||
let time_opt = get_string_from_cell(&worksheet, row, lesson_time_column);
|
||||
if time_opt.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let time = time_opt.unwrap();
|
||||
|
||||
// type
|
||||
let lesson_type = if time.contains("пара") {
|
||||
LessonType::Default
|
||||
} else {
|
||||
LessonType::Additional
|
||||
};
|
||||
|
||||
// lesson index
|
||||
let default_index = if lesson_type == LessonType::Default {
|
||||
Some(
|
||||
time.chars()
|
||||
.next()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.parse::<u32>()
|
||||
.unwrap(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// time
|
||||
let time_range = {
|
||||
static TIME_RE: LazyLock<Regex, fn() -> Regex> =
|
||||
LazyLock::new(|| Regex::new(r"(\d+\.\d+)-(\d+\.\d+)").unwrap());
|
||||
|
||||
let parse_res = TIME_RE
|
||||
.captures(&time)
|
||||
.expect("Unable to obtain lesson start and end!");
|
||||
|
||||
let start_match = parse_res.get(1).unwrap().as_str();
|
||||
let start_parts: Vec<&str> = start_match.split(".").collect();
|
||||
|
||||
let end_match = parse_res.get(2).unwrap().as_str();
|
||||
let end_parts: Vec<&str> = end_match.split(".").collect();
|
||||
|
||||
LessonTime {
|
||||
start: day.date.clone()
|
||||
+ Duration::hours(start_parts[0].parse().unwrap())
|
||||
+ Duration::minutes(start_parts[1].parse().unwrap()),
|
||||
end: day.date.clone()
|
||||
+ Duration::hours(end_parts[0].parse().unwrap())
|
||||
+ Duration::minutes(end_parts[1].parse().unwrap()),
|
||||
}
|
||||
};
|
||||
|
||||
day_times.push(InternalTime {
|
||||
time_range,
|
||||
lesson_type,
|
||||
default_index,
|
||||
xls_range: get_merge_from_start(&worksheet, row, lesson_time_column),
|
||||
});
|
||||
}
|
||||
|
||||
days_times.push(day_times);
|
||||
}
|
||||
|
||||
let day_times = &days_times[day_index];
|
||||
|
||||
for time in day_times {
|
||||
match &mut parse_lesson(
|
||||
&worksheet,
|
||||
&mut day,
|
||||
&day_times,
|
||||
&time,
|
||||
group_markup.column,
|
||||
) {
|
||||
Lessons(l) => day.lessons.append(l),
|
||||
Street(s) => day.street = Some(s.to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
group.days.push(day);
|
||||
}
|
||||
|
||||
groups.insert(group.name.clone(), group);
|
||||
}
|
||||
|
||||
ParseResult {
|
||||
teachers: convert_groups_to_teachers(&groups),
|
||||
groups,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
pub fn test_result() -> ParseResult {
|
||||
let buffer: Vec<u8> = include_bytes!("../../schedule.xls").to_vec();
|
||||
|
||||
parse_xls(&buffer)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read() {
|
||||
let result = test_result();
|
||||
|
||||
assert_ne!(result.groups.len(), 0);
|
||||
assert_ne!(result.teachers.len(), 0);
|
||||
}
|
||||
}
|
||||
127
src/parser/schema.rs
Normal file
127
src/parser/schema.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct LessonTime {
|
||||
pub start: DateTime<Utc>,
|
||||
pub end: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, Clone)]
|
||||
#[repr(u8)]
|
||||
pub enum LessonType {
|
||||
Default = 0, // Обычная
|
||||
Additional, // Допы
|
||||
Break, // Перемена
|
||||
Consultation, // Консультация
|
||||
IndependentWork, // Самостоятельная работа
|
||||
Exam, // Зачёт
|
||||
ExamWithGrade, // Зачет с оценкой
|
||||
ExamDefault, // Экзамен
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct LessonSubGroup {
|
||||
/**
|
||||
* Номер подгруппы.
|
||||
*/
|
||||
pub number: u8,
|
||||
|
||||
/**
|
||||
* Кабинет, если присутствует.
|
||||
*/
|
||||
pub cabinet: Option<String>,
|
||||
|
||||
/**
|
||||
* Фио преподавателя.
|
||||
*/
|
||||
pub teacher: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Lesson {
|
||||
/**
|
||||
* Тип занятия.
|
||||
*/
|
||||
#[serde(rename = "type")]
|
||||
pub lesson_type: LessonType,
|
||||
|
||||
/**
|
||||
* Индексы пар, если присутствуют.
|
||||
*/
|
||||
#[serde(rename = "defaultRange")]
|
||||
pub default_range: Option<[u8; 2]>,
|
||||
|
||||
/**
|
||||
* Название занятия.
|
||||
*/
|
||||
pub name: Option<String>,
|
||||
|
||||
/**
|
||||
* Начало и конец занятия.
|
||||
*/
|
||||
pub time: LessonTime,
|
||||
|
||||
/**
|
||||
* Подгруппы.
|
||||
*/
|
||||
#[serde(rename = "subGroups")]
|
||||
pub subgroups: Option<Vec<LessonSubGroup>>,
|
||||
|
||||
/**
|
||||
* Группа, если это расписание для преподавателей.
|
||||
*/
|
||||
pub group: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Day {
|
||||
/**
|
||||
* День недели.
|
||||
*/
|
||||
pub name: String,
|
||||
|
||||
/**
|
||||
* Адрес другого корпуса.
|
||||
*/
|
||||
pub street: Option<String>,
|
||||
|
||||
/**
|
||||
* Дата.
|
||||
*/
|
||||
pub date: DateTime<Utc>,
|
||||
|
||||
/**
|
||||
* Список пар в этот день.
|
||||
*/
|
||||
pub lessons: Vec<Lesson>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ScheduleEntry {
|
||||
/**
|
||||
* Название группы или ФИО преподавателя.
|
||||
*/
|
||||
pub name: String,
|
||||
|
||||
/**
|
||||
* Список из шести дней.
|
||||
*/
|
||||
pub days: Vec<Day>,
|
||||
}
|
||||
|
||||
pub struct ParseResult {
|
||||
/**
|
||||
* Список групп.
|
||||
* Ключом является название группы.
|
||||
*/
|
||||
pub groups: HashMap<String, ScheduleEntry>,
|
||||
|
||||
/**
|
||||
* Список преподавателей.
|
||||
* Ключом является ФИО преподавателя.
|
||||
*/
|
||||
pub teachers: HashMap<String, ScheduleEntry>,
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
mod schema;
|
||||
pub mod sign_in;
|
||||
pub mod sign_up;
|
||||
mod shared;
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
pub mod sign_in {
|
||||
use crate::database::models::User;
|
||||
use crate::routes::schema::shared::{ErrorToHttpCode, IResponse};
|
||||
use crate::routes::schema::user;
|
||||
use actix_web::http::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Request {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
pub type Response = IResponse<user::ResponseOk, ResponseErr>;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ResponseErr {
|
||||
code: ErrorCode,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum ErrorCode {
|
||||
IncorrectCredentials,
|
||||
InvalidVkAccessToken,
|
||||
}
|
||||
|
||||
pub trait ResponseExt {
|
||||
fn ok(user: &User) -> Self;
|
||||
fn err(code: ErrorCode) -> Response;
|
||||
}
|
||||
|
||||
impl ResponseExt for Response {
|
||||
fn ok(user: &User) -> Self {
|
||||
IResponse(Ok(user::ResponseOk::from_user(&user)))
|
||||
}
|
||||
|
||||
fn err(code: ErrorCode) -> Response {
|
||||
IResponse(Err(ResponseErr { code }))
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorToHttpCode for ResponseErr {
|
||||
fn to_http_status_code(&self) -> StatusCode {
|
||||
StatusCode::NOT_ACCEPTABLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod sign_up {
|
||||
use crate::database::models::{User, UserRole};
|
||||
use crate::routes::schema::shared::{ErrorToHttpCode, IResponse};
|
||||
use crate::routes::schema::user;
|
||||
use actix_web::http::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Request {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub group: String,
|
||||
pub role: UserRole,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
pub type Response = IResponse<user::ResponseOk, ResponseErr>;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ResponseOk {
|
||||
id: String,
|
||||
access_token: String,
|
||||
group: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ResponseErr {
|
||||
code: ErrorCode,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum ErrorCode {
|
||||
DisallowedRole,
|
||||
InvalidGroupName,
|
||||
UsernameAlreadyExists,
|
||||
}
|
||||
|
||||
pub trait ResponseExt {
|
||||
fn ok(user: &User) -> Self;
|
||||
fn err(code: ErrorCode) -> Self;
|
||||
}
|
||||
|
||||
impl ResponseExt for Response {
|
||||
fn ok(user: &User) -> Self {
|
||||
IResponse(Ok(user::ResponseOk::from_user(&user)))
|
||||
}
|
||||
|
||||
fn err(code: ErrorCode) -> Response {
|
||||
Self(Err(ResponseErr { code }))
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorToHttpCode for ResponseErr {
|
||||
fn to_http_status_code(&self) -> StatusCode {
|
||||
StatusCode::NOT_ACCEPTABLE
|
||||
}
|
||||
}
|
||||
}
|
||||
96
src/routes/auth/shared.rs
Normal file
96
src/routes/auth/shared.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use crate::utility::jwt::DEFAULT_ALGORITHM;
|
||||
use jsonwebtoken::errors::ErrorKind;
|
||||
use jsonwebtoken::{decode, DecodingKey, Validation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct TokenData {
|
||||
iis: String,
|
||||
sub: i32,
|
||||
app: i32,
|
||||
exp: i32,
|
||||
iat: i32,
|
||||
jti: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Claims {
|
||||
sub: String,
|
||||
iis: String,
|
||||
jti: i32,
|
||||
app: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Error {
|
||||
JwtError(ErrorKind),
|
||||
InvalidSignature,
|
||||
InvalidToken,
|
||||
Expired,
|
||||
UnknownIssuer(String),
|
||||
UnknownType(i32),
|
||||
UnknownClientId(i32),
|
||||
}
|
||||
|
||||
//noinspection SpellCheckingInspection
|
||||
const VK_PUBLIC_KEY: &str = concat!(
|
||||
"-----BEGIN PUBLIC KEY-----\n",
|
||||
"MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvsvJlhFX9Ju/pvCz1frB\n",
|
||||
"DgJs592VjdwQuRAmnlJAItyHkoiDIOEocPzgcUBTbDf1plDcTyO2RCkUt0pz0WK6\n",
|
||||
"6HNhpJyIfARjaWHeUlv4TpuHXAJJsBKklkU2gf1cjID+40sWWYjtq5dAkXnSJUVA\n",
|
||||
"UR+sq0lJ7GmTdJtAr8hzESqGEcSP15PTs7VUdHZ1nkC2XgkuR8KmKAUb388ji1Q4\n",
|
||||
"n02rJNOPQgd9r0ac4N2v/yTAFPXumO78N25bpcuWf5vcL9e8THk/U2zt7wf+aAWL\n",
|
||||
"748e0pREqNluTBJNZfmhC79Xx6GHtwqHyyduiqfPmejmiujNM/rqnA4e30Tg86Yn\n",
|
||||
"cNZ6vLJyF72Eva1wXchukH/aLispbY+EqNPxxn4zzCWaLKHG87gaCxpVv9Tm0jSD\n",
|
||||
"2es22NjrUbtb+2pAGnXbyDp2eGUqw0RrTQFZqt/VcmmSCE45FlcZMT28otrwG1ZB\n",
|
||||
"kZAb5Js3wLEch3ZfYL8sjhyNRPBmJBrAvzrd8qa3rdUjkC9sKyjGAaHu2MNmFl1Y\n",
|
||||
"JFQ3J54tGpkGgJjD7Kz3w0K6OiPDlVCNQN5sqXm24fCw85Pbi8SJiaLTp/CImrs1\n",
|
||||
"Z3nHW5q8hljA7OGmqfOP0nZS/5zW9GHPyepsI1rW6CympYLJ15WeNzePxYS5KEX9\n",
|
||||
"EncmkSD9b45ge95hJeJZteUCAwEAAQ==\n",
|
||||
"-----END PUBLIC KEY-----"
|
||||
);
|
||||
|
||||
static VK_ID_CLIENT_ID: LazyLock<i32> = LazyLock::new(|| {
|
||||
env::var("VK_ID_CLIENT_ID")
|
||||
.expect("VK_ID_CLIENT_ID must be set")
|
||||
.parse::<i32>()
|
||||
.expect("VK_ID_CLIENT_ID must be i32")
|
||||
});
|
||||
|
||||
pub fn parse_vk_id(token_str: &String) -> Result<i32, Error> {
|
||||
let dkey = DecodingKey::from_rsa_pem(VK_PUBLIC_KEY.as_bytes()).unwrap();
|
||||
|
||||
match decode::<Claims>(&token_str, &dkey, &Validation::new(DEFAULT_ALGORITHM)) {
|
||||
Ok(token_data) => {
|
||||
let claims = token_data.claims;
|
||||
|
||||
if claims.iis != "VK" {
|
||||
Err(Error::UnknownIssuer(claims.iis))
|
||||
} else if claims.jti != 21 {
|
||||
Err(Error::UnknownType(claims.jti))
|
||||
} else if claims.app != *VK_ID_CLIENT_ID {
|
||||
Err(Error::UnknownClientId(claims.app))
|
||||
} else {
|
||||
match claims.sub.parse::<i32>() {
|
||||
Ok(sub) => Ok(sub),
|
||||
Err(_) => Err(Error::InvalidToken),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => Err(match err.into_kind() {
|
||||
ErrorKind::InvalidToken => Error::InvalidToken,
|
||||
ErrorKind::InvalidSignature => Error::InvalidSignature,
|
||||
ErrorKind::InvalidAlgorithmName => Error::InvalidToken,
|
||||
ErrorKind::MissingRequiredClaim(_) => Error::InvalidToken,
|
||||
ErrorKind::ExpiredSignature => Error::Expired,
|
||||
ErrorKind::InvalidAlgorithm => Error::InvalidToken,
|
||||
ErrorKind::MissingAlgorithm => Error::InvalidToken,
|
||||
ErrorKind::Base64(_) => Error::InvalidToken,
|
||||
ErrorKind::Json(_) => Error::InvalidToken,
|
||||
ErrorKind::Utf8(_) => Error::InvalidToken,
|
||||
kind => Error::JwtError(kind),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -1,47 +1,138 @@
|
||||
use self::schema::*;
|
||||
use crate::database::driver;
|
||||
use crate::database::models::User;
|
||||
use crate::routes::auth::schema;
|
||||
use crate::{AppState, utility};
|
||||
use crate::routes::auth::shared::parse_vk_id;
|
||||
use crate::routes::auth::sign_in::schema::ErrorCode;
|
||||
use crate::routes::auth::sign_in::schema::SignInData::{Default, Vk};
|
||||
use crate::{utility, AppState};
|
||||
use actix_web::{post, web};
|
||||
use diesel::SaveChangesDsl;
|
||||
use std::ops::DerefMut;
|
||||
use web::Json;
|
||||
|
||||
#[post("/sign-in")]
|
||||
pub async fn sign_in(
|
||||
data: Json<schema::sign_in::Request>,
|
||||
app_state: web::Data<AppState>,
|
||||
) -> schema::sign_in::Response {
|
||||
use schema::sign_in::*;
|
||||
async fn sign_in(data: SignInData, app_state: &web::Data<AppState>) -> Response {
|
||||
let user = match &data {
|
||||
Default(data) => driver::users::get_by_username(&app_state.database, &data.username),
|
||||
Vk(id) => driver::users::get_by_vk_id(&app_state.database, *id),
|
||||
};
|
||||
|
||||
match driver::users::get_by_username(&app_state.database, data.username.clone()) {
|
||||
Ok(mut user) => match bcrypt::verify(&data.password, &user.password) {
|
||||
Ok(true) => {
|
||||
let mut lock = app_state.connection();
|
||||
let conn = lock.deref_mut();
|
||||
|
||||
user.access_token = utility::jwt::encode(&user.id);
|
||||
|
||||
user.save_changes::<User>(conn)
|
||||
.expect("Failed to update user");
|
||||
|
||||
Response::ok(&user)
|
||||
match user {
|
||||
Ok(mut user) => {
|
||||
if let Default(data) = data {
|
||||
match bcrypt::verify(&data.password, &user.password) {
|
||||
Ok(result) => {
|
||||
if !result {
|
||||
return Response::err(ErrorCode::IncorrectCredentials);
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
return Response::err(ErrorCode::IncorrectCredentials);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(false) | Err(_) => Response::err(ErrorCode::IncorrectCredentials),
|
||||
},
|
||||
|
||||
let mut lock = app_state.connection();
|
||||
let conn = lock.deref_mut();
|
||||
|
||||
user.access_token = utility::jwt::encode(&user.id);
|
||||
|
||||
user.save_changes::<User>(conn)
|
||||
.expect("Failed to update user");
|
||||
|
||||
Response::ok(&user)
|
||||
}
|
||||
|
||||
Err(_) => Response::err(ErrorCode::IncorrectCredentials),
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/sign-in")]
|
||||
pub async fn sign_in_default(data: Json<Request>, app_state: web::Data<AppState>) -> Response {
|
||||
sign_in(Default(data.into_inner()), &app_state).await
|
||||
}
|
||||
|
||||
#[post("/sign-in-vk")]
|
||||
pub async fn sign_in_vk(data_json: Json<vk::Request>, app_state: web::Data<AppState>) -> Response {
|
||||
let data = data_json.into_inner();
|
||||
|
||||
match parse_vk_id(&data.access_token) {
|
||||
Ok(id) => sign_in(Vk(id), &app_state).await,
|
||||
Err(_) => Response::err(ErrorCode::InvalidVkAccessToken),
|
||||
}
|
||||
}
|
||||
|
||||
mod schema {
|
||||
use crate::database::models::User;
|
||||
use crate::routes::schema::{user, ErrorToHttpCode, IResponse};
|
||||
use actix_web::http::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Request {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
pub mod vk {
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Request {
|
||||
pub access_token: String,
|
||||
}
|
||||
}
|
||||
|
||||
pub type Response = IResponse<user::ResponseOk, ResponseErr>;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ResponseErr {
|
||||
code: ErrorCode,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum ErrorCode {
|
||||
IncorrectCredentials,
|
||||
InvalidVkAccessToken,
|
||||
}
|
||||
|
||||
pub trait ResponseExt {
|
||||
fn ok(user: &User) -> Self;
|
||||
fn err(code: ErrorCode) -> Response;
|
||||
}
|
||||
|
||||
impl ResponseExt for Response {
|
||||
fn ok(user: &User) -> Self {
|
||||
IResponse(Ok(user::ResponseOk::from_user(&user)))
|
||||
}
|
||||
|
||||
fn err(code: ErrorCode) -> Response {
|
||||
IResponse(Err(ResponseErr { code }))
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorToHttpCode for ResponseErr {
|
||||
fn to_http_status_code(&self) -> StatusCode {
|
||||
StatusCode::NOT_ACCEPTABLE
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal
|
||||
|
||||
pub enum SignInData {
|
||||
Default(Request),
|
||||
Vk(i32),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app_state::app_state;
|
||||
use super::schema::*;
|
||||
use crate::database::driver;
|
||||
use crate::database::models::{User, UserRole};
|
||||
use crate::routes::auth::schema;
|
||||
use crate::routes::auth::sign_in::sign_in;
|
||||
use crate::test_env::tests::{static_app_state, test_app, test_env};
|
||||
use crate::routes::auth::sign_in::sign_in_default;
|
||||
use crate::test_env::tests::{static_app_state, test_app, test_app_state, test_env};
|
||||
use crate::utility;
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::dev::ServiceResponse;
|
||||
@@ -50,8 +141,8 @@ mod tests {
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fmt::Write;
|
||||
|
||||
async fn sign_in_client(data: schema::sign_in::Request) -> ServiceResponse {
|
||||
let app = test_app(app_state(), sign_in).await;
|
||||
async fn sign_in_client(data: Request) -> ServiceResponse {
|
||||
let app = test_app(test_app_state(), sign_in_default).await;
|
||||
|
||||
let req = test::TestRequest::with_uri("/sign-in")
|
||||
.method(Method::POST)
|
||||
@@ -100,7 +191,7 @@ mod tests {
|
||||
async fn sign_in_ok() {
|
||||
prepare("test::sign_in_ok".to_string());
|
||||
|
||||
let resp = sign_in_client(schema::sign_in::Request {
|
||||
let resp = sign_in_client(Request {
|
||||
username: "test::sign_in_ok".to_string(),
|
||||
password: "example".to_string(),
|
||||
})
|
||||
@@ -113,7 +204,7 @@ mod tests {
|
||||
async fn sign_in_err() {
|
||||
prepare("test::sign_in_err".to_string());
|
||||
|
||||
let invalid_username = sign_in_client(schema::sign_in::Request {
|
||||
let invalid_username = sign_in_client(Request {
|
||||
username: "test::sign_in_err::username".to_string(),
|
||||
password: "example".to_string(),
|
||||
})
|
||||
@@ -121,7 +212,7 @@ mod tests {
|
||||
|
||||
assert_eq!(invalid_username.status(), StatusCode::NOT_ACCEPTABLE);
|
||||
|
||||
let invalid_password = sign_in_client(schema::sign_in::Request {
|
||||
let invalid_password = sign_in_client(Request {
|
||||
username: "test::sign_in_err".to_string(),
|
||||
password: "bad_password".to_string(),
|
||||
})
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
use self::schema::*;
|
||||
use crate::AppState;
|
||||
use crate::database::driver;
|
||||
use crate::database::models::{User, UserRole};
|
||||
use crate::routes::auth::schema;
|
||||
use crate::{utility, AppState};
|
||||
use crate::database::models::UserRole;
|
||||
use crate::routes::auth::shared::{Error, parse_vk_id};
|
||||
use actix_web::{post, web};
|
||||
use objectid::ObjectId;
|
||||
use rand::{Rng, rng};
|
||||
use web::Json;
|
||||
|
||||
#[post("/sign-up")]
|
||||
pub async fn sign_up(
|
||||
data: Json<schema::sign_up::Request>,
|
||||
app_state: web::Data<AppState>,
|
||||
) -> schema::sign_up::Response {
|
||||
use schema::sign_up::*;
|
||||
|
||||
async fn sign_up(data: SignUpData, app_state: &web::Data<AppState>) -> Response {
|
||||
// If user selected forbidden role.
|
||||
if data.role == UserRole::Admin {
|
||||
return Response::err(ErrorCode::DisallowedRole);
|
||||
}
|
||||
|
||||
// If specified group doesn't exist in schedule.
|
||||
let schedule_opt = app_state.schedule.lock().unwrap();
|
||||
|
||||
if let Some(schedule) = &*schedule_opt {
|
||||
@@ -25,37 +22,193 @@ pub async fn sign_up(
|
||||
}
|
||||
}
|
||||
|
||||
if driver::users::contains_by_username(&app_state.database, data.username.clone()) {
|
||||
// If user with specified username already exists.
|
||||
if driver::users::contains_by_username(&app_state.database, &data.username) {
|
||||
return Response::err(ErrorCode::UsernameAlreadyExists);
|
||||
}
|
||||
|
||||
let id = ObjectId::new().unwrap().to_string();
|
||||
let access_token = utility::jwt::encode(&id);
|
||||
|
||||
let user = User {
|
||||
id,
|
||||
username: data.username.clone(),
|
||||
password: bcrypt::hash(data.password.as_str(), bcrypt::DEFAULT_COST).unwrap(),
|
||||
vk_id: None,
|
||||
access_token,
|
||||
group: data.group.clone(),
|
||||
role: data.role.clone(),
|
||||
version: data.version.clone(),
|
||||
};
|
||||
// If user with specified VKID already exists.
|
||||
if let Some(id) = data.vk_id {
|
||||
if driver::users::contains_by_vk_id(&app_state.database, id) {
|
||||
return Response::err(ErrorCode::VkAlreadyExists);
|
||||
}
|
||||
}
|
||||
|
||||
let user = data.to_user();
|
||||
driver::users::insert(&app_state.database, &user).unwrap();
|
||||
|
||||
Response::ok(&user)
|
||||
}
|
||||
|
||||
#[post("/sign-up")]
|
||||
pub async fn sign_up_default(data_json: Json<Request>, app_state: web::Data<AppState>) -> Response {
|
||||
let data = data_json.into_inner();
|
||||
|
||||
sign_up(
|
||||
SignUpData {
|
||||
username: data.username,
|
||||
password: data.password,
|
||||
vk_id: None,
|
||||
group: data.group,
|
||||
role: data.role,
|
||||
version: data.version,
|
||||
},
|
||||
&app_state,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[post("/sign-up-vk")]
|
||||
pub async fn sign_up_vk(data_json: Json<vk::Request>, app_state: web::Data<AppState>) -> Response {
|
||||
let data = data_json.into_inner();
|
||||
|
||||
match parse_vk_id(&data.access_token) {
|
||||
Ok(id) => {
|
||||
sign_up(
|
||||
SignUpData {
|
||||
username: data.username,
|
||||
password: rng()
|
||||
.sample_iter(&rand::distr::Alphanumeric)
|
||||
.take(16)
|
||||
.map(char::from)
|
||||
.collect(),
|
||||
vk_id: Some(id),
|
||||
group: data.group,
|
||||
role: data.role,
|
||||
version: data.version,
|
||||
},
|
||||
&app_state,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Err(err) => {
|
||||
if err != Error::Expired {
|
||||
eprintln!("Failed to parse vk id token!");
|
||||
eprintln!("{:?}", err);
|
||||
}
|
||||
|
||||
Response::err(ErrorCode::InvalidVkAccessToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod schema {
|
||||
use crate::database::models::{User, UserRole};
|
||||
use crate::routes::schema::{ErrorToHttpCode, IResponse, user};
|
||||
use crate::utility;
|
||||
use actix_web::http::StatusCode;
|
||||
use objectid::ObjectId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// WEB
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Request {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub group: String,
|
||||
pub role: UserRole,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
pub mod vk {
|
||||
use crate::database::models::UserRole;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Request {
|
||||
pub access_token: String,
|
||||
pub username: String,
|
||||
pub group: String,
|
||||
pub role: UserRole,
|
||||
pub version: String,
|
||||
}
|
||||
}
|
||||
|
||||
pub type Response = IResponse<user::ResponseOk, ResponseErr>;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ResponseOk {
|
||||
id: String,
|
||||
access_token: String,
|
||||
group: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ResponseErr {
|
||||
code: ErrorCode,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum ErrorCode {
|
||||
DisallowedRole,
|
||||
InvalidGroupName,
|
||||
UsernameAlreadyExists,
|
||||
InvalidVkAccessToken,
|
||||
VkAlreadyExists,
|
||||
}
|
||||
|
||||
pub trait ResponseExt {
|
||||
fn ok(user: &User) -> Self;
|
||||
fn err(code: ErrorCode) -> Self;
|
||||
}
|
||||
|
||||
impl ResponseExt for Response {
|
||||
fn ok(user: &User) -> Self {
|
||||
IResponse(Ok(user::ResponseOk::from_user(&user)))
|
||||
}
|
||||
|
||||
fn err(code: ErrorCode) -> Response {
|
||||
Self(Err(ResponseErr { code }))
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorToHttpCode for ResponseErr {
|
||||
fn to_http_status_code(&self) -> StatusCode {
|
||||
StatusCode::NOT_ACCEPTABLE
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal
|
||||
|
||||
pub struct SignUpData {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub vk_id: Option<i32>,
|
||||
pub group: String,
|
||||
pub role: UserRole,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
impl SignUpData {
|
||||
pub fn to_user(self) -> User {
|
||||
let id = ObjectId::new().unwrap().to_string();
|
||||
let access_token = utility::jwt::encode(&id);
|
||||
|
||||
User {
|
||||
id,
|
||||
username: self.username,
|
||||
password: bcrypt::hash(self.password, bcrypt::DEFAULT_COST).unwrap(),
|
||||
vk_id: self.vk_id,
|
||||
access_token,
|
||||
group: self.group,
|
||||
role: self.role,
|
||||
version: self.version,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app_state::app_state;
|
||||
use crate::database::driver;
|
||||
use crate::database::models::UserRole;
|
||||
use crate::routes::auth::schema;
|
||||
use crate::routes::auth::sign_up::sign_up;
|
||||
use crate::test_env::tests::{static_app_state, test_app, test_env};
|
||||
use crate::routes::auth::sign_up::schema::Request;
|
||||
use crate::routes::auth::sign_up::sign_up_default;
|
||||
use crate::test_env::tests::{static_app_state, test_app, test_app_state, test_env};
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::dev::ServiceResponse;
|
||||
use actix_web::http::Method;
|
||||
@@ -68,11 +221,11 @@ mod tests {
|
||||
}
|
||||
|
||||
async fn sign_up_client(data: SignUpPartial) -> ServiceResponse {
|
||||
let app = test_app(app_state(), sign_up).await;
|
||||
let app = test_app(test_app_state(), sign_up_default).await;
|
||||
|
||||
let req = test::TestRequest::with_uri("/sign-up")
|
||||
.method(Method::POST)
|
||||
.set_json(schema::sign_up::Request {
|
||||
.set_json(Request {
|
||||
username: data.username.clone(),
|
||||
password: "example".to_string(),
|
||||
group: data.group.clone(),
|
||||
@@ -91,7 +244,7 @@ mod tests {
|
||||
test_env();
|
||||
|
||||
let app_state = static_app_state();
|
||||
driver::users::delete_by_username(&app_state.database, "test::sign_up_valid".to_string());
|
||||
driver::users::delete_by_username(&app_state.database, &"test::sign_up_valid".to_string());
|
||||
|
||||
// test
|
||||
|
||||
@@ -114,7 +267,7 @@ mod tests {
|
||||
let app_state = static_app_state();
|
||||
driver::users::delete_by_username(
|
||||
&app_state.database,
|
||||
"test::sign_up_multiple".to_string(),
|
||||
&"test::sign_up_multiple".to_string(),
|
||||
);
|
||||
|
||||
let create = sign_up_client(SignUpPartial {
|
||||
@@ -150,4 +303,19 @@ mod tests {
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::NOT_ACCEPTABLE);
|
||||
}
|
||||
|
||||
#[actix_web::test]
|
||||
async fn sign_up_invalid_group() {
|
||||
test_env();
|
||||
|
||||
// test
|
||||
let resp = sign_up_client(SignUpPartial {
|
||||
username: "test::sign_up_invalid_group".to_string(),
|
||||
group: "invalid_group".to_string(),
|
||||
role: UserRole::Student,
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::NOT_ACCEPTABLE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,37 @@
|
||||
pub mod shared {
|
||||
use actix_web::body::EitherBody;
|
||||
use actix_web::error::JsonPayloadError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{HttpRequest, HttpResponse, Responder};
|
||||
use serde::Serialize;
|
||||
use actix_web::body::EitherBody;
|
||||
use actix_web::error::JsonPayloadError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{HttpRequest, HttpResponse, Responder};
|
||||
use serde::Serialize;
|
||||
|
||||
pub struct IResponse<T: Serialize, E: Serialize>(pub Result<T, E>);
|
||||
pub struct IResponse<T: Serialize, E: Serialize>(pub Result<T, E>);
|
||||
|
||||
pub trait ErrorToHttpCode {
|
||||
fn to_http_status_code(&self) -> StatusCode;
|
||||
}
|
||||
pub trait ErrorToHttpCode {
|
||||
fn to_http_status_code(&self) -> StatusCode;
|
||||
}
|
||||
|
||||
impl<T: Serialize, E: Serialize + ErrorToHttpCode> Responder for IResponse<T, E> {
|
||||
type Body = EitherBody<String>;
|
||||
impl<T: Serialize, E: Serialize + ErrorToHttpCode> Responder for IResponse<T, E> {
|
||||
type Body = EitherBody<String>;
|
||||
|
||||
fn respond_to(self, _: &HttpRequest) -> HttpResponse<Self::Body> {
|
||||
match serde_json::to_string(&self.0) {
|
||||
Ok(body) => {
|
||||
let code = match &self.0 {
|
||||
Ok(_) => StatusCode::OK,
|
||||
Err(e) => e.to_http_status_code(),
|
||||
};
|
||||
fn respond_to(self, _: &HttpRequest) -> HttpResponse<Self::Body> {
|
||||
match serde_json::to_string(&self.0) {
|
||||
Ok(body) => {
|
||||
let code = match &self.0 {
|
||||
Ok(_) => StatusCode::OK,
|
||||
Err(e) => e.to_http_status_code(),
|
||||
};
|
||||
|
||||
match HttpResponse::build(code)
|
||||
.content_type(mime::APPLICATION_JSON)
|
||||
.message_body(body)
|
||||
{
|
||||
Ok(res) => res.map_into_left_body(),
|
||||
Err(err) => HttpResponse::from_error(err).map_into_right_body(),
|
||||
}
|
||||
match HttpResponse::build(code)
|
||||
.content_type(mime::APPLICATION_JSON)
|
||||
.message_body(body)
|
||||
{
|
||||
Ok(res) => res.map_into_left_body(),
|
||||
Err(err) => HttpResponse::from_error(err).map_into_right_body(),
|
||||
}
|
||||
}
|
||||
|
||||
Err(err) => {
|
||||
HttpResponse::from_error(JsonPayloadError::Serialize(err)).map_into_right_body()
|
||||
}
|
||||
Err(err) => {
|
||||
HttpResponse::from_error(JsonPayloadError::Serialize(err)).map_into_right_body()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use crate::app_state::{AppState, app_state};
|
||||
use crate::app_state::{app_state, AppState, Schedule};
|
||||
use actix_web::dev::{HttpServiceFactory, Service, ServiceResponse};
|
||||
use actix_web::{App, test, web};
|
||||
use actix_web::{test, web, App};
|
||||
use std::sync::LazyLock;
|
||||
use crate::parser::tests::test_result;
|
||||
|
||||
pub fn test_env() {
|
||||
dotenvy::from_path(".env.test").expect("Failed to load test environment file");
|
||||
@@ -19,8 +20,22 @@ pub(crate) mod tests {
|
||||
test::init_service(App::new().app_data(app_state).service(factory)).await
|
||||
}
|
||||
|
||||
pub fn test_app_state() -> web::Data<AppState> {
|
||||
let state = app_state();
|
||||
let mut schedule_lock = state.schedule.lock().unwrap();
|
||||
|
||||
*schedule_lock = Some(Schedule {
|
||||
etag: "".to_string(),
|
||||
updated_at: Default::default(),
|
||||
parsed_at: Default::default(),
|
||||
data: test_result(),
|
||||
});
|
||||
|
||||
state.clone()
|
||||
}
|
||||
|
||||
pub fn static_app_state() -> web::Data<AppState> {
|
||||
static STATE: LazyLock<web::Data<AppState>> = LazyLock::new(|| app_state());
|
||||
static STATE: LazyLock<web::Data<AppState>> = LazyLock::new(|| test_app_state());
|
||||
|
||||
STATE.clone()
|
||||
}
|
||||
|
||||
@@ -1,88 +1,91 @@
|
||||
use chrono::DateTime;
|
||||
use chrono::Duration;
|
||||
use chrono::TimeZone;
|
||||
use chrono::Utc;
|
||||
use hmac::{Hmac, Mac};
|
||||
use jwt::{SignWithKey, Token, VerifyWithKey};
|
||||
use sha2::Sha256;
|
||||
use std::collections::BTreeMap;
|
||||
use jsonwebtoken::errors::ErrorKind;
|
||||
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::DisplayFromStr;
|
||||
use serde_with::serde_as;
|
||||
use std::env;
|
||||
use std::mem::discriminant;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
static JWT_SECRET: LazyLock<Hmac<Sha256>> = LazyLock::new(|| {
|
||||
static DECODING_KEY: LazyLock<DecodingKey> = LazyLock::new(|| {
|
||||
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
|
||||
|
||||
Hmac::new_from_slice(secret.as_bytes()).expect("Hmac::new_from_slice failed")
|
||||
DecodingKey::from_secret(secret.as_bytes())
|
||||
});
|
||||
|
||||
static ENCODING_KEY: LazyLock<EncodingKey> = LazyLock::new(|| {
|
||||
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
|
||||
|
||||
EncodingKey::from_secret(secret.as_bytes())
|
||||
});
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum VerifyError {
|
||||
JwtError(jwt::Error),
|
||||
pub enum Error {
|
||||
InvalidSignature,
|
||||
InvalidToken,
|
||||
InvalidToken(ErrorKind),
|
||||
Expired,
|
||||
}
|
||||
|
||||
impl PartialEq for VerifyError {
|
||||
impl PartialEq for Error {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
discriminant(self) == discriminant(other)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_and_decode(token: &String) -> Result<String, VerifyError> {
|
||||
let jwt = &*JWT_SECRET;
|
||||
#[serde_as]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Claims {
|
||||
id: String,
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
iat: u64,
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
exp: u64,
|
||||
}
|
||||
|
||||
let result: Result<BTreeMap<String, String>, jwt::Error> = token.verify_with_key(jwt);
|
||||
pub(crate) const DEFAULT_ALGORITHM: Algorithm = Algorithm::HS256;
|
||||
|
||||
pub fn verify_and_decode(token: &String) -> Result<String, Error> {
|
||||
let mut validation = Validation::new(DEFAULT_ALGORITHM);
|
||||
|
||||
validation.required_spec_claims.remove("exp");
|
||||
validation.validate_exp = false;
|
||||
|
||||
let result = decode::<Claims>(&token, &*DECODING_KEY, &validation);
|
||||
|
||||
match result {
|
||||
Ok(claims) => {
|
||||
let exp = claims.get("exp").unwrap();
|
||||
let exp_date = DateTime::from_timestamp(exp.parse::<i64>().unwrap(), 0)
|
||||
.expect("Failed to parse expiration time");
|
||||
|
||||
if Utc::now() > exp_date {
|
||||
return Err(VerifyError::Expired);
|
||||
Ok(token_data) => {
|
||||
if token_data.claims.exp < Utc::now().timestamp().unsigned_abs() {
|
||||
Err(Error::Expired)
|
||||
} else {
|
||||
Ok(token_data.claims.id)
|
||||
}
|
||||
|
||||
Ok(claims.get("id").cloned().unwrap())
|
||||
}
|
||||
Err(err) => Err(match err {
|
||||
jwt::Error::InvalidSignature | jwt::Error::RustCryptoMac(_) => {
|
||||
VerifyError::InvalidSignature
|
||||
}
|
||||
jwt::Error::Format | jwt::Error::Base64(_) | jwt::Error::NoClaimsComponent => {
|
||||
VerifyError::InvalidToken
|
||||
}
|
||||
|
||||
_ => VerifyError::JwtError(err),
|
||||
Err(err) => Err(match err.into_kind() {
|
||||
ErrorKind::InvalidSignature => Error::InvalidSignature,
|
||||
ErrorKind::ExpiredSignature => Error::Expired,
|
||||
kind => Error::InvalidToken(kind),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode(id: &String) -> String {
|
||||
let header = jwt::Header {
|
||||
type_: Some(jwt::header::HeaderType::JsonWebToken),
|
||||
let header = Header {
|
||||
typ: Some(String::from("JWT")),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut claims = BTreeMap::new();
|
||||
|
||||
let iat = Utc::now();
|
||||
let exp = iat + Duration::days(365 * 4);
|
||||
|
||||
let iat_str = iat.timestamp().to_string();
|
||||
let exp_str = exp.timestamp().to_string();
|
||||
let claims = Claims {
|
||||
id: id.clone(),
|
||||
iat: iat.timestamp().unsigned_abs(),
|
||||
exp: exp.timestamp().unsigned_abs(),
|
||||
};
|
||||
|
||||
claims.insert("id", id.as_str());
|
||||
claims.insert("iat", iat_str.as_str());
|
||||
claims.insert("exp", exp_str.as_str());
|
||||
|
||||
Token::new(header, claims)
|
||||
.sign_with_key(&*JWT_SECRET)
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.to_string()
|
||||
jsonwebtoken::encode(&header, &claims, &*ENCODING_KEY).unwrap()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -105,7 +108,10 @@ mod tests {
|
||||
let result = verify_and_decode(&token);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.err().unwrap(), VerifyError::InvalidToken);
|
||||
assert_eq!(
|
||||
result.err().unwrap(),
|
||||
Error::InvalidToken(ErrorKind::InvalidToken)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -116,7 +122,7 @@ mod tests {
|
||||
let result = verify_and_decode(&token);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.err().unwrap(), VerifyError::InvalidSignature);
|
||||
assert_eq!(result.err().unwrap(), Error::InvalidSignature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -127,7 +133,7 @@ mod tests {
|
||||
let result = verify_and_decode(&token);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.err().unwrap(), VerifyError::Expired);
|
||||
assert_eq!(result.err().unwrap(), Error::Expired);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user