From a95494d3be9788c07f06b3587991b164b04f4897 Mon Sep 17 00:00:00 2001 From: N08I40K Date: Sun, 23 Mar 2025 06:11:13 +0400 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B8=20=D1=82=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D1=8B=20=D1=8D=D0=BD=D0=B4=D0=BF=D0=BE=D0=B8=D0=BD=D1=82=D0=BE?= =?UTF-8?q?=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 190 ++++++++++++++++++++++++-- Cargo.toml | 18 ++- lib/schedule_parser/src/lib/lib.rs | 38 +++--- lib/schedule_parser/src/lib/schema.rs | 70 ++++++---- src/app_state.rs | 39 ++++++ src/database/driver.rs | 41 +++++- src/database/models.rs | 6 +- src/main.rs | 47 +------ src/routes/auth/mod.rs | 3 +- src/routes/auth/schema.rs | 151 +++++++++++++------- src/routes/auth/sign_in.rs | 119 ++++++++++++++-- src/routes/auth/sign_up.rs | 153 +++++++++++++++++++++ src/routes/mod.rs | 3 +- src/routes/schema.rs | 69 ++++++++++ src/test_env.rs | 27 ++++ src/utility/jwt.rs | 24 ++-- 16 files changed, 822 insertions(+), 176 deletions(-) create mode 100644 src/app_state.rs create mode 100644 src/routes/auth/sign_up.rs create mode 100644 src/routes/schema.rs create mode 100644 src/test_env.rs diff --git a/Cargo.lock b/Cargo.lock index 95dbadf..e0b3b06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,7 +49,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rand", + "rand 0.9.0", "sha1", "smallvec", "tokio", @@ -168,7 +168,7 @@ dependencies = [ "serde_urlencoded", "smallvec", "socket2", - "time", + "time 0.3.40", "tracing", "url", ] @@ -333,7 +333,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" dependencies = [ - "byteorder", + "byteorder 1.5.0", "cipher", ] @@ -364,6 +364,12 @@ version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +[[package]] +name = "byteorder" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b2aa490a8f546381308d68fc79e6bd753cd3ad839f7a7172897f1feedfa175" + [[package]] name = "byteorder" version = "1.5.0" @@ -391,7 +397,7 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138646b9af2c5d7f1804ea4bf93afc597737d2bd4f7341d67c48b03316976eb1" dependencies = [ - "byteorder", + "byteorder 1.5.0", "codepage", "encoding_rs", "log", @@ -516,7 +522,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ "percent-encoding", - "time", + "time 0.3.40", "version_check", ] @@ -714,7 +720,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "470eb10efc8646313634c99bb1593f402a6434cbd86e266770c6e39219adb86a" dependencies = [ "bitflags", - "byteorder", + "byteorder 1.5.0", "diesel_derives", "itoa", "pq-sys", @@ -878,6 +884,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "futures-channel" version = "0.3.31" @@ -926,6 +938,12 @@ dependencies = [ "thread_local", ] +[[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + [[package]] name = "generic-array" version = "0.14.7" @@ -1040,6 +1058,16 @@ dependencies = [ "digest", ] +[[package]] +name = "hostname" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21ceb46a83a85e824ef93669c8b390009623863b5c195d1ba747292c0c72f94e" +dependencies = [ + "libc", + "winutil", +] + [[package]] name = "http" version = "0.2.12" @@ -1434,6 +1462,12 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" +[[package]] +name = "lazy_static" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" + [[package]] name = "libc" version = "0.2.171" @@ -1565,6 +1599,22 @@ dependencies = [ "memchr", ] +[[package]] +name = "objectid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a605e0778d73324c897e7e4bd5903f64639884a99d1bad55bccfd986260063" +dependencies = [ + "byteorder 0.3.13", + "hostname", + "lazy_static", + "libc", + "quick-error", + "rand 0.3.23", + "rust-crypto", + "rustc-serialize", +] + [[package]] name = "once_cell" version = "1.21.1" @@ -1730,6 +1780,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quick-xml" version = "0.31.0" @@ -1755,6 +1811,29 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[package]] +name = "rand" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" +dependencies = [ + "libc", + "rand 0.4.6", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + [[package]] name = "rand" version = "0.9.0" @@ -1762,7 +1841,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ "rand_chacha", - "rand_core", + "rand_core 0.9.3", "zerocopy", ] @@ -1773,9 +1852,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.3", ] +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + [[package]] name = "rand_core" version = "0.9.3" @@ -1805,6 +1899,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "redox_syscall" version = "0.5.10" @@ -1907,12 +2010,31 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rust-crypto" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a" +dependencies = [ + "gcc", + "libc", + "rand 0.3.23", + "rustc-serialize", + "time 0.1.45", +] + [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-serialize" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe834bc780604f4674073badbad26d7219cadfb4a2275802db12cbae17498401" + [[package]] name = "rustix" version = "1.0.3" @@ -1999,6 +2121,7 @@ dependencies = [ name = "schedule-parser-rusted" version = "0.3.0" dependencies = [ + "actix-http", "actix-web", "bcrypt", "chrono", @@ -2007,9 +2130,12 @@ dependencies = [ "dotenvy", "hmac", "jwt", + "mime", + "objectid", "reqwest", "schedule_parser", "serde", + "serde_json", "sha2", "tokio", ] @@ -2292,6 +2418,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "time" version = "0.3.40" @@ -2548,6 +2685,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2644,6 +2787,22 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.9" @@ -2653,6 +2812,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.52.0" @@ -2843,6 +3008,15 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +[[package]] +name = "winutil" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7daf138b6b14196e3830a588acf1e86966c694d3e8fb026fb105b8b5dca07e6e" +dependencies = [ + "winapi", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/Cargo.toml b/Cargo.toml index 0629a9b..dc6567f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,16 +8,20 @@ edition = "2024" publish = false [dependencies] +actix-http = "3.10.0" +actix-web = "4.10.2" bcrypt = "0.17.0" -jwt = "0.16.0" -hmac = "0.12.1" -sha2 = "0.10.8" +chrono = "0.4.40" diesel = { version = "2.2.8", features = ["postgres"] } diesel-derive-enum = { git = "https://github.com/Havunen/diesel-derive-enum.git", features = ["postgres"] } dotenvy = "0.15.7" -serde = { version = "1.0.219", features = ["derive"] } -schedule_parser = { path = "./lib/schedule_parser" } -chrono = "0.4.40" +hmac = "0.12.1" +jwt = "0.16.0" +mime = "0.3.17" +objectid = "0.2.0" reqwest = "0.12.15" +schedule_parser = { path = "./lib/schedule_parser" } +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +sha2 = "0.10.8" tokio = { version = "1.44.1", features = ["macros", "rt-multi-thread"] } -actix-web = "4.10.2" diff --git a/lib/schedule_parser/src/lib/lib.rs b/lib/schedule_parser/src/lib/lib.rs index c30bb7b..d5897d2 100644 --- a/lib/schedule_parser/src/lib/lib.rs +++ b/lib/schedule_parser/src/lib/lib.rs @@ -1,10 +1,12 @@ -use crate::schema::LessonType::Break; -use crate::schema::{Day, Lesson, LessonSubGroup, LessonTime, LessonType, ScheduleEntity}; use crate::LessonParseResult::{Lessons, Street}; -use calamine::{open_workbook_from_rs, Reader, Xls}; +use crate::schema::LessonType::Break; +use crate::schema::{ + Day, Lesson, LessonSubGroup, LessonTime, LessonType, ParseResult, ScheduleEntry, +}; +use calamine::{Reader, Xls, open_workbook_from_rs}; use chrono::{Duration, NaiveDateTime}; -use fuzzy_matcher::skim::SkimMatcherV2; use fuzzy_matcher::FuzzyMatcher; +use fuzzy_matcher::skim::SkimMatcherV2; use regex::Regex; use std::collections::HashMap; use std::io::Cursor; @@ -471,9 +473,9 @@ fn parse_name_and_subgroups(name: &String) -> (String, Vec) { } fn convert_groups_to_teachers( - groups: &HashMap, -) -> HashMap { - let mut teachers: HashMap = HashMap::new(); + groups: &HashMap, +) -> HashMap { + let mut teachers: HashMap = HashMap::new(); let empty_days: Vec = groups .values() @@ -510,7 +512,7 @@ fn convert_groups_to_teachers( if !teachers.contains_key(&subgroup.teacher) { teachers.insert( subgroup.teacher.clone(), - ScheduleEntity { + ScheduleEntry { name: subgroup.teacher.clone(), days: empty_days.to_vec(), }, @@ -538,12 +540,7 @@ fn convert_groups_to_teachers( teachers } -pub fn parse_xls( - buffer: &Vec, -) -> ( - HashMap, - HashMap, -) { +pub fn parse_xls(buffer: &Vec) -> ParseResult { let cursor = Cursor::new(&buffer); let mut workbook: Xls<_> = open_workbook_from_rs(cursor).expect("Can't open workbook"); @@ -556,13 +553,13 @@ pub fn parse_xls( let (days_markup, groups_markup) = parse_skeleton(&worksheet); - let mut groups: HashMap = HashMap::new(); + let mut groups: HashMap = HashMap::new(); let mut days_times: Vec> = Vec::new(); let saturday_end_row = worksheet.end().unwrap().0; for group_markup in groups_markup { - let mut group = ScheduleEntity { + let mut group = ScheduleEntry { name: group_markup.name, days: Vec::new(), }; @@ -686,7 +683,10 @@ pub fn parse_xls( groups.insert(group.name.clone(), group); } - (convert_groups_to_teachers(&groups), groups) + ParseResult { + teachers: convert_groups_to_teachers(&groups), + groups, + } } #[cfg(test)] @@ -698,7 +698,7 @@ mod tests { let buffer: Vec = include_bytes!("../../../../schedule.xls").to_vec(); let result = parse_xls(&buffer); - assert_ne!(result.0.len(), 0); - assert_ne!(result.1.len(), 0); + assert_ne!(result.groups.len(), 0); + assert_ne!(result.teachers.len(), 0); } } diff --git a/lib/schedule_parser/src/lib/schema.rs b/lib/schedule_parser/src/lib/schema.rs index 412127f..4c8f226 100644 --- a/lib/schedule_parser/src/lib/schema.rs +++ b/lib/schedule_parser/src/lib/schema.rs @@ -24,84 +24,104 @@ pub enum LessonType { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct LessonSubGroup { + /** + * Номер подгруппы. + */ pub number: u8, + /** + * Кабинет, если присутствует. + */ pub cabinet: Option, + /** + * Фио преподавателя. + */ 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, /** - * Начало и конец занятия + * Начало и конец занятия. */ pub time: LessonTime, /** - * Подгруппы + * Подгруппы. */ #[serde(rename = "subGroups")] pub subgroups: Option>, /** - * Группа (только для расписания преподавателей) + * Группа, если это расписание для преподавателей. */ pub group: Option, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Day { + /** + * День недели. + */ pub name: String, + /** + * Адрес другого корпуса. + */ pub street: Option, + /** + * Дата. + */ pub date: DateTime, + /** + * Список пар в этот день. + */ pub lessons: Vec, } #[derive(Serialize, Deserialize, Debug)] -pub struct ScheduleEntity { +pub struct ScheduleEntry { + /** + * Название группы или ФИО преподавателя. + */ pub name: String, + /** + * Список из шести дней. + */ pub days: Vec, } -#[derive(Serialize, Deserialize, Debug)] -pub struct Schedule { - #[serde(rename = "updatedAt")] - pub updated_at: DateTime, +pub struct ParseResult { + /** + * Список групп. + * Ключом является название группы. + */ + pub groups: HashMap, - pub groups: HashMap, - - #[serde(rename = "updatedGroups")] - pub updated_groups: Vec>, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct TeacherSchedule { - #[serde(rename = "updatedAt")] - pub updated_at: DateTime, - - pub teacher: ScheduleEntity, - - pub updated: Vec, + /** + * Список преподавателей. + * Ключом является ФИО преподавателя. + */ + pub teachers: HashMap, } diff --git a/src/app_state.rs b/src/app_state.rs new file mode 100644 index 0000000..0afbd42 --- /dev/null +++ b/src/app_state.rs @@ -0,0 +1,39 @@ +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}; + +pub struct Schedule { + pub etag: String, + pub updated_at: DateTime, + pub parsed_at: DateTime, + pub data: ParseResult, +} + +pub struct AppState { + pub downloader: Mutex, + pub schedule: Mutex>, + pub database: Mutex, +} + +impl AppState { + pub fn connection(&self) -> MutexGuard { + self.database.lock().unwrap() + } +} + +pub fn app_state() -> web::Data { + let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + + web::Data::new(AppState { + downloader: Mutex::new(BasicXlsDownloader::new()), + schedule: Mutex::new(None), + database: Mutex::new( + PgConnection::establish(&database_url) + .unwrap_or_else(|_| panic!("Error connecting to {}", database_url)), + ), + }) +} diff --git a/src/database/driver.rs b/src/database/driver.rs index a5f93c0..4f40a8e 100644 --- a/src/database/driver.rs +++ b/src/database/driver.rs @@ -1,9 +1,8 @@ pub mod users { use crate::database::models::User; - use crate::database::schema::fcm::user_id; use crate::database::schema::users::dsl::users; use crate::database::schema::users::dsl::*; - use diesel::{ExpressionMethods, QueryResult}; + use diesel::{insert_into, ExpressionMethods, QueryResult}; use diesel::{PgConnection, SelectableHelper}; use diesel::{QueryDsl, RunQueryDsl}; use std::ops::DerefMut; @@ -31,4 +30,42 @@ pub mod users { .select(User::as_select()) .first(con) } + + pub fn contains_by_username(connection: &Mutex, _username: String) -> bool { + let mut lock = connection.lock().unwrap(); + let con = lock.deref_mut(); + + match users + .filter(username.eq(_username)) + .count() + .get_result::(con) + { + Ok(count) => count > 0, + Err(_) => false, + } + } + + pub fn delete_by_username(connection: &Mutex, _username: String) -> bool { + let mut lock = connection.lock().unwrap(); + let con = lock.deref_mut(); + + match diesel::delete(users.filter(username.eq(_username))).execute(con) { + Ok(count) => count > 0, + Err(_) => false, + } + } + + pub fn insert(connection: &Mutex, user: &User) -> QueryResult { + let mut lock = connection.lock().unwrap(); + let con = lock.deref_mut(); + + insert_into(users).values(user).execute(con) + } + + pub fn insert_or_ignore(connection: &Mutex, user: &User) -> QueryResult { + let mut lock = connection.lock().unwrap(); + let con = lock.deref_mut(); + + insert_into(users).values(user).on_conflict_do_nothing().execute(con) + } } diff --git a/src/database/models.rs b/src/database/models.rs index ff1bd02..8b2555a 100644 --- a/src/database/models.rs +++ b/src/database/models.rs @@ -1,7 +1,7 @@ use diesel::prelude::*; -use serde::Serialize; +use serde::{Deserialize, Serialize}; -#[derive(diesel_derive_enum::DbEnum, Serialize, Debug, Clone, Copy, PartialEq)] +#[derive(diesel_derive_enum::DbEnum, Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] #[ExistingTypePath = "crate::database::schema::sql_types::UserRole"] #[DbValueStyle = "UPPERCASE"] #[serde(rename_all = "UPPERCASE")] @@ -11,7 +11,7 @@ pub enum UserRole { Admin, } -#[derive(Identifiable, AsChangeset, Queryable, Selectable, Serialize)] +#[derive(Identifiable, AsChangeset, Queryable, Selectable, Serialize, Insertable, Debug)] #[diesel(table_name = crate::database::schema::users)] #[diesel(treat_none_as_null = true)] pub struct User { diff --git a/src/main.rs b/src/main.rs index 0ec4793..859001f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,60 +1,27 @@ +use crate::app_state::{AppState, app_state}; use crate::routes::auth::sign_in::sign_in; -use crate::xls_downloader::basic_impl::BasicXlsDownloader; use actix_web::{App, HttpServer, web}; -use chrono::{DateTime, Utc}; -use diesel::{Connection, PgConnection}; use dotenvy::dotenv; -use schedule_parser::schema::ScheduleEntity; -use std::collections::HashMap; -use std::env; -use std::sync::{Mutex, MutexGuard}; +mod app_state; mod database; mod routes; + +#[cfg(test)] +mod test_env; + mod utility; mod xls_downloader; -pub struct AppState { - downloader: Mutex, - schedule: Mutex< - Option<( - String, - DateTime, - ( - HashMap, - HashMap, - ), - )>, - >, - database: Mutex, -} - -impl AppState { - pub fn connection(&self) -> MutexGuard { - self.database.lock().unwrap() - } -} - #[actix_web::main] async fn main() { dotenv().ok(); - let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); - - let data = web::Data::new(AppState { - downloader: Mutex::new(BasicXlsDownloader::new()), - schedule: Mutex::new(None), - database: Mutex::new( - PgConnection::establish(&database_url) - .unwrap_or_else(|_| panic!("Error connecting to {}", database_url)), - ), - }); - HttpServer::new(move || { let schedule_scope = web::scope("/auth").service(sign_in); let api_scope = web::scope("/api/v1").service(schedule_scope); - App::new().app_data(data.clone()).service(api_scope) + App::new().app_data(move || app_state()).service(api_scope) }) .bind(("127.0.0.1", 8080)) .unwrap() diff --git a/src/routes/auth/mod.rs b/src/routes/auth/mod.rs index 523aba4..49e270c 100644 --- a/src/routes/auth/mod.rs +++ b/src/routes/auth/mod.rs @@ -1,2 +1,3 @@ +mod schema; pub mod sign_in; -mod schema; \ No newline at end of file +pub mod sign_up; diff --git a/src/routes/auth/schema.rs b/src/routes/auth/schema.rs index d881008..4a0c12b 100644 --- a/src/routes/auth/schema.rs +++ b/src/routes/auth/schema.rs @@ -1,56 +1,109 @@ -use crate::database::models::User; -use serde::{Deserialize, Serialize, Serializer}; +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)] -pub struct SignInDto { - pub username: String, - pub password: String, -} - -pub struct SignInResult(Result); - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct SignInOk { - id: String, - access_token: String, - group: String, -} - -#[derive(Serialize)] -pub struct SignInErr { - code: SignInErrCode, -} - -#[derive(Serialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum SignInErrCode { - IncorrectCredentials, - InvalidVkAccessToken, -} - -impl SignInResult { - pub fn ok(user: &User) -> Self { - Self(Ok(SignInOk { - id: user.id.clone(), - access_token: user.access_token.clone(), - group: user.group.clone(), - })) + #[derive(Deserialize, Serialize)] + pub struct Request { + pub username: String, + pub password: String, } - pub fn err(code: SignInErrCode) -> SignInResult { - Self(Err(SignInErr { code })) - } -} + pub type Response = IResponse; -impl Serialize for SignInResult { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - match &self.0 { - Ok(ok) => serializer.serialize_some(&ok), - Err(err) => serializer.serialize_some(&err), + #[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; + + #[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 } } } diff --git a/src/routes/auth/sign_in.rs b/src/routes/auth/sign_in.rs index 430abf7..0d3147f 100644 --- a/src/routes/auth/sign_in.rs +++ b/src/routes/auth/sign_in.rs @@ -1,7 +1,6 @@ use crate::database::driver; use crate::database::models::User; -use crate::routes::auth::schema::SignInErrCode::IncorrectCredentials; -use crate::routes::auth::schema::{SignInDto, SignInResult}; +use crate::routes::auth::schema; use crate::{AppState, utility}; use actix_web::{post, web}; use diesel::SaveChangesDsl; @@ -9,8 +8,13 @@ use std::ops::DerefMut; use web::Json; #[post("/sign-in")] -pub async fn sign_in(data: Json, app_state: web::Data) -> Json { - let result = match driver::users::get_by_username(&app_state.database, data.username.clone()) { +pub async fn sign_in( + data: Json, + app_state: web::Data, +) -> schema::sign_in::Response { + use schema::sign_in::*; + + 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(); @@ -21,13 +25,108 @@ pub async fn sign_in(data: Json, app_state: web::Data) -> J user.save_changes::(conn) .expect("Failed to update user"); - SignInResult::ok(&user) + Response::ok(&user) } - Ok(false) | Err(_) => SignInResult::err(IncorrectCredentials), + Ok(false) | Err(_) => Response::err(ErrorCode::IncorrectCredentials), }, - Err(_) => SignInResult::err(IncorrectCredentials), - }; - - Json(result) + Err(_) => Response::err(ErrorCode::IncorrectCredentials), + } +} + +#[cfg(test)] +mod tests { + use crate::app_state::app_state; + 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::utility; + use actix_http::StatusCode; + use actix_web::dev::ServiceResponse; + use actix_web::http::Method; + use actix_web::test; + 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; + + let req = test::TestRequest::with_uri("/sign-in") + .method(Method::POST) + .set_json(data) + .to_request(); + + test::call_service(&app, req).await + } + + fn prepare(username: String) { + let id = { + let mut sha = Sha256::new(); + sha.update(&username); + + let result = sha.finalize(); + let bytes = &result[..12]; + + let mut hex = String::new(); + for byte in bytes { + write!(&mut hex, "{:02x}", byte).unwrap(); + } + + hex + }; + + test_env(); + + let app_state = static_app_state(); + driver::users::insert_or_ignore( + &app_state.database, + &User { + id: id.clone(), + username, + password: bcrypt::hash("example".to_string(), bcrypt::DEFAULT_COST).unwrap(), + vk_id: None, + access_token: utility::jwt::encode(&id), + group: "ИС-214/23".to_string(), + role: UserRole::Student, + version: "1.0.0".to_string(), + }, + ) + .unwrap(); + } + + #[actix_web::test] + async fn sign_in_ok() { + prepare("test::sign_in_ok".to_string()); + + let resp = sign_in_client(schema::sign_in::Request { + username: "test::sign_in_ok".to_string(), + password: "example".to_string(), + }) + .await; + + assert_eq!(resp.status(), StatusCode::OK); + } + + #[actix_web::test] + async fn sign_in_err() { + prepare("test::sign_in_err".to_string()); + + let invalid_username = sign_in_client(schema::sign_in::Request { + username: "test::sign_in_err::username".to_string(), + password: "example".to_string(), + }) + .await; + + assert_eq!(invalid_username.status(), StatusCode::NOT_ACCEPTABLE); + + let invalid_password = sign_in_client(schema::sign_in::Request { + username: "test::sign_in_err".to_string(), + password: "bad_password".to_string(), + }) + .await; + + assert_eq!(invalid_password.status(), StatusCode::NOT_ACCEPTABLE); + } } diff --git a/src/routes/auth/sign_up.rs b/src/routes/auth/sign_up.rs new file mode 100644 index 0000000..a2e6d6e --- /dev/null +++ b/src/routes/auth/sign_up.rs @@ -0,0 +1,153 @@ +use crate::database::driver; +use crate::database::models::{User, UserRole}; +use crate::routes::auth::schema; +use crate::{utility, AppState}; +use actix_web::{post, web}; +use objectid::ObjectId; +use web::Json; + +#[post("/sign-up")] +pub async fn sign_up( + data: Json, + app_state: web::Data, +) -> schema::sign_up::Response { + use schema::sign_up::*; + + if data.role == UserRole::Admin { + return Response::err(ErrorCode::DisallowedRole); + } + + let schedule_opt = app_state.schedule.lock().unwrap(); + + if let Some(schedule) = &*schedule_opt { + if !schedule.data.groups.contains_key(&data.group) { + return Response::err(ErrorCode::InvalidGroupName); + } + } + + if driver::users::contains_by_username(&app_state.database, data.username.clone()) { + 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(), + }; + + driver::users::insert(&app_state.database, &user).unwrap(); + + Response::ok(&user) +} + +#[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 actix_http::StatusCode; + use actix_web::dev::ServiceResponse; + use actix_web::http::Method; + use actix_web::test; + + struct SignUpPartial { + username: String, + group: String, + role: UserRole, + } + + async fn sign_up_client(data: SignUpPartial) -> ServiceResponse { + let app = test_app(app_state(), sign_up).await; + + let req = test::TestRequest::with_uri("/sign-up") + .method(Method::POST) + .set_json(schema::sign_up::Request { + username: data.username.clone(), + password: "example".to_string(), + group: data.group.clone(), + role: data.role.clone(), + version: "1.0.0".to_string(), + }) + .to_request(); + + test::call_service(&app, req).await + } + + #[actix_web::test] + async fn sign_up_valid() { + // prepare + + test_env(); + + let app_state = static_app_state(); + driver::users::delete_by_username(&app_state.database, "test::sign_up_valid".to_string()); + + // test + + let resp = sign_up_client(SignUpPartial { + username: "test::sign_up_valid".to_string(), + group: "ИС-214/23".to_string(), + role: UserRole::Student, + }) + .await; + + assert_eq!(resp.status(), StatusCode::OK); + } + + #[actix_web::test] + async fn sign_up_multiple() { + // prepare + + test_env(); + + let app_state = static_app_state(); + driver::users::delete_by_username( + &app_state.database, + "test::sign_up_multiple".to_string(), + ); + + let create = sign_up_client(SignUpPartial { + username: "test::sign_up_multiple".to_string(), + group: "ИС-214/23".to_string(), + role: UserRole::Student, + }) + .await; + + assert_eq!(create.status(), StatusCode::OK); + + let resp = sign_up_client(SignUpPartial { + username: "test::sign_up_multiple".to_string(), + group: "ИС-214/23".to_string(), + role: UserRole::Student, + }) + .await; + + assert_eq!(resp.status(), StatusCode::NOT_ACCEPTABLE); + } + + #[actix_web::test] + async fn sign_up_invalid_role() { + test_env(); + + // test + let resp = sign_up_client(SignUpPartial { + username: "test::sign_up_invalid_role".to_string(), + group: "ИС-214/23".to_string(), + role: UserRole::Admin, + }) + .await; + + assert_eq!(resp.status(), StatusCode::NOT_ACCEPTABLE); + } +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 5696e21..73fc450 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1 +1,2 @@ -pub mod auth; \ No newline at end of file +pub mod auth; +mod schema; diff --git a/src/routes/schema.rs b/src/routes/schema.rs new file mode 100644 index 0000000..6bac389 --- /dev/null +++ b/src/routes/schema.rs @@ -0,0 +1,69 @@ +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; + + pub struct IResponse(pub Result); + + pub trait ErrorToHttpCode { + fn to_http_status_code(&self) -> StatusCode; + } + + impl Responder for IResponse { + type Body = EitherBody; + + fn respond_to(self, _: &HttpRequest) -> HttpResponse { + 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(), + } + } + + Err(err) => { + HttpResponse::from_error(JsonPayloadError::Serialize(err)).map_into_right_body() + } + } + } + } +} + +pub mod user { + use crate::database::models::{User, UserRole}; + use serde::Serialize; + + #[derive(Serialize)] + #[serde(rename_all = "camelCase")] + pub struct ResponseOk { + id: String, + username: String, + group: String, + role: UserRole, + vk_id: Option, + access_token: String, + } + + impl ResponseOk { + pub fn from_user(user: &User) -> Self { + ResponseOk { + id: user.id.clone(), + username: user.username.clone(), + group: user.group.clone(), + role: user.role.clone(), + vk_id: user.vk_id.clone(), + access_token: user.access_token.clone(), + } + } + } +} diff --git a/src/test_env.rs b/src/test_env.rs new file mode 100644 index 0000000..58e2aca --- /dev/null +++ b/src/test_env.rs @@ -0,0 +1,27 @@ +#[cfg(test)] +pub(crate) mod tests { + use crate::app_state::{AppState, app_state}; + use actix_web::dev::{HttpServiceFactory, Service, ServiceResponse}; + use actix_web::{App, test, web}; + use std::sync::LazyLock; + + pub fn test_env() { + dotenvy::from_path(".env.test").expect("Failed to load test environment file"); + } + + pub async fn test_app( + app_state: web::Data, + factory: F, + ) -> impl Service + where + F: HttpServiceFactory + 'static, + { + test::init_service(App::new().app_data(app_state).service(factory)).await + } + + pub fn static_app_state() -> web::Data { + static STATE: LazyLock> = LazyLock::new(|| app_state()); + + STATE.clone() + } +} diff --git a/src/utility/jwt.rs b/src/utility/jwt.rs index 459db00..c510391 100644 --- a/src/utility/jwt.rs +++ b/src/utility/jwt.rs @@ -48,7 +48,9 @@ pub fn verify_and_decode(token: &String) -> Result { Ok(claims.get("id").cloned().unwrap()) } Err(err) => Err(match err { - jwt::Error::InvalidSignature => VerifyError::InvalidSignature, + jwt::Error::InvalidSignature | jwt::Error::RustCryptoMac(_) => { + VerifyError::InvalidSignature + } jwt::Error::Format | jwt::Error::Base64(_) | jwt::Error::NoClaimsComponent => { VerifyError::InvalidToken } @@ -86,18 +88,18 @@ pub fn encode(id: &String) -> String { #[cfg(test)] mod tests { use super::*; - use dotenvy::dotenv; + use crate::test_env::tests::test_env; #[test] fn test_encode() { - dotenv().unwrap(); + test_env(); assert_eq!(encode(&"test".to_string()).is_empty(), false); } #[test] fn test_decode_invalid_token() { - dotenv().unwrap(); + test_env(); let token = "".to_string(); let result = verify_and_decode(&token); @@ -108,20 +110,20 @@ mod tests { #[test] fn test_decode_invalid_signature() { - dotenv().unwrap(); + test_env(); - let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIxODY4ODEyOTI4IiwiaWF0IjoiMTc0MjY2ODkyOCIsImlkIjoiNjdkY2M5YTk1MDdiMDAwMDc3Mjc0NGEyIn0.DQYFYF-3DoJgCLOVdAWa47nUaCJAh16DXj-ChNSSmWz".to_string(); + let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIxNjE2NTI2Mzc2IiwiaWF0IjoiMTQ5MDM4MjM3NiIsImlkIjoiNjdkY2M5YTk1MDdiMDAwMDc3Mjc0NGEyIn0.Qc2LbMJTvl2hWzDM2XyQv4m9lIqR84COAESQAieUxz8".to_string(); let result = verify_and_decode(&token); assert!(result.is_err()); - assert_eq!(result.err().unwrap(), VerifyError::InvalidToken); + assert_eq!(result.err().unwrap(), VerifyError::InvalidSignature); } #[test] fn test_decode_expired() { - dotenv().unwrap(); + test_env(); - let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIxNjE2NTI2Mzc2IiwiaWF0IjoiMTQ5MDM4MjM3NiIsImlkIjoiNjdkY2M5YTk1MDdiMDAwMDc3Mjc0NGEyIn0.Qc2LbMJTvl2hWzDM2XyQv4m9lIqR84COAESQAieUxz8".to_string(); + let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3ZGNjOWE5NTA3YjAwMDA3NzI3NDRhMiIsImlhdCI6IjAiLCJleHAiOiIwIn0.GBsVYvnZIfHXt00t-qmAdUMyHSyWOBtC0Mrxwg1HQOM".to_string(); let result = verify_and_decode(&token); assert!(result.is_err()); @@ -130,9 +132,9 @@ mod tests { #[test] fn test_decode_ok() { - dotenv().unwrap(); + test_env(); - let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIxODY4ODEyOTI4IiwiaWF0IjoiMTc0MjY2ODkyOCIsImlkIjoiNjdkY2M5YTk1MDdiMDAwMDc3Mjc0NGEyIn0.DQYFYF-3DoJgCLOVdAWa47nUaCJAh16DXj-ChNSSmWw".to_string(); + let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3ZGNjOWE5NTA3YjAwMDA3NzI3NDRhMiIsImlhdCI6Ijk5OTk5OTk5OTkiLCJleHAiOiI5OTk5OTk5OTk5In0.o1vN-ze5iaJrnlHqe7WARXMBhhzjxTjTKkjlmTGEnOI".to_string(); let result = verify_and_decode(&token); assert!(result.is_ok());