Авторизация через токен вк

Слияние schedule_parser с проектом

Перенос схемы запросов/ответов в файлы эндпоинтов

Переход с библиотеки jwt на jsonwebtokens
This commit is contained in:
2025-03-25 02:05:27 +04:00
parent 0316f58592
commit ab1cbd795e
22 changed files with 794 additions and 369 deletions

View File

@@ -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,

View File

@@ -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)
}
}

View File

@@ -23,4 +23,4 @@ pub struct User {
pub group: String,
pub role: UserRole,
pub version: String,
}
}

1
src/lib.rs Normal file
View File

@@ -0,0 +1 @@
pub mod parser;

View File

@@ -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
View 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
View 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>,
}

View File

@@ -1,3 +1,3 @@
mod schema;
pub mod sign_in;
pub mod sign_up;
mod shared;

View File

@@ -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
View 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),
}),
}
}

View File

@@ -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(),
})

View File

@@ -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);
}
}

View File

@@ -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()
}
}
}

View File

@@ -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()
}

View File

@@ -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]