diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 47a5f8e..15f10fa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: - name: Create .env.test run: touch .env.test - name: Run tests - run: cargo test -p schedule-parser-rusted -p schedule_parser + run: cargo test env: DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }} JWT_SECRET: "test-secret-at-least-256-bits-used" \ No newline at end of file diff --git a/.idea/discord.xml b/.idea/discord.xml new file mode 100644 index 0000000..912db82 --- /dev/null +++ b/.idea/discord.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/.idea/schedule-parser-rusted.iml b/.idea/schedule-parser-rusted.iml index 38f6e7d..eff20f9 100644 --- a/.idea/schedule-parser-rusted.iml +++ b/.idea/schedule-parser-rusted.iml @@ -2,9 +2,9 @@ - + diff --git a/Cargo.lock b/Cargo.lock index e0b3b06..19250d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,7 +29,7 @@ dependencies = [ "actix-rt", "actix-service", "actix-utils", - "base64 0.22.1", + "base64", "bitflags", "brotli", "bytes", @@ -287,12 +287,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.22.1" @@ -305,7 +299,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92758ad6077e4c76a6cadbce5005f666df70d4f13b19976b1a8062eef880040f" dependencies = [ - "base64 0.22.1", + "base64", "blowfish", "getrandom 0.3.2", "subtle", @@ -327,6 +321,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.11.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a229bfd78e4827c91b9b95784f69492c1b77c1ab75a45a8a037b139215086f94" +dependencies = [ + "hybrid-array", +] + [[package]] name = "blowfish" version = "0.9.1" @@ -477,7 +480,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "inout", ] @@ -515,6 +518,12 @@ dependencies = [ "encoding_rs", ] +[[package]] +name = "const-oid" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cb3c4a0d3776f7535c32793be81d6d5fec0d48ac70955d9834e643aa249a52f" + [[package]] name = "cookie" version = "0.16.2" @@ -637,6 +646,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170d71b5b14dec99db7739f6fc7d6ec2db80b78c3acb77db48392ccc3d8a9ea0" +dependencies = [ + "hybrid-array", +] + [[package]] name = "darling" version = "0.20.10" @@ -679,6 +697,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -765,9 +784,19 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", - "subtle", + "block-buffer 0.10.4", + "crypto-common 0.1.6", +] + +[[package]] +name = "digest" +version = "0.11.0-pre.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c478574b20020306f98d61c8ca3322d762e1ff08117422ac6106438605ea516" +dependencies = [ + "block-buffer 0.11.0-rc.4", + "const-oid", + "crypto-common 0.2.0-rc.2", ] [[package]] @@ -961,8 +990,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -995,7 +1026,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.8.0", "slab", "tokio", "tokio-util", @@ -1014,7 +1045,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap", + "indexmap 2.8.0", "slab", "tokio", "tokio-util", @@ -1031,6 +1062,12 @@ dependencies = [ "crunchy", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.15.2" @@ -1050,13 +1087,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" [[package]] -name = "hmac" -version = "0.12.1" +name = "hex" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hostname" @@ -1125,6 +1159,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dab50e193aebe510fe0e40230145820e02f48dae0cf339ea4204e6e708ff7bd" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.6.0" @@ -1371,6 +1414,17 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.8.0" @@ -1378,7 +1432,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", + "serde", ] [[package]] @@ -1442,18 +1497,18 @@ dependencies = [ ] [[package]] -name = "jwt" -version = "0.16.0" +name = "jsonwebtoken" +version = "9.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" dependencies = [ - "base64 0.13.1", - "crypto-common", - "digest", - "hmac", + "base64", + "js-sys", + "pem", + "ring", "serde", "serde_json", - "sha2", + "simple_asn1", ] [[package]] @@ -1575,12 +1630,31 @@ dependencies = [ "tempfile", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1694,6 +1768,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1958,7 +2042,7 @@ version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "encoding_rs", "futures-core", @@ -2119,38 +2203,30 @@ dependencies = [ [[package]] name = "schedule-parser-rusted" -version = "0.3.0" +version = "0.4.0" dependencies = [ "actix-http", "actix-web", "bcrypt", - "chrono", - "diesel", - "diesel-derive-enum", - "dotenvy", - "hmac", - "jwt", - "mime", - "objectid", - "reqwest", - "schedule_parser", - "serde", - "serde_json", - "sha2", - "tokio", -] - -[[package]] -name = "schedule_parser" -version = "0.2.0" -dependencies = [ "calamine", "chrono", "criterion", + "diesel", + "diesel-derive-enum", + "dotenvy", "fuzzy-matcher", + "jsonwebtoken", + "mime", + "objectid", + "rand 0.9.0", "regex", + "reqwest", "serde", + "serde_json", "serde_repr", + "serde_with", + "sha2", + "tokio", ] [[package]] @@ -2237,6 +2313,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.8.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time 0.3.40", +] + +[[package]] +name = "serde_with_macros" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2245,18 +2351,18 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", ] [[package]] name = "sha2" -version = "0.10.8" +version = "0.11.0-pre.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "19b4241d1a56954dce82cecda5c8e9c794eef6f53abe5e5216bac0a0ea71ffa7" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.11.0-pre.10", ] [[package]] @@ -2280,6 +2386,18 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time 0.3.40", +] + [[package]] name = "slab" version = "0.4.9" @@ -3142,7 +3260,7 @@ dependencies = [ "crossbeam-utils", "displaydoc", "flate2", - "indexmap", + "indexmap 2.8.0", "memchr", "thiserror", "zopfli", diff --git a/Cargo.toml b/Cargo.toml index dc6567f..9c79236 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,27 +1,35 @@ -[workspace] -members = ["lib/schedule_parser"] - [package] name = "schedule-parser-rusted" -version = "0.3.0" +version = "0.4.0" edition = "2024" publish = false [dependencies] -actix-http = "3.10.0" actix-web = "4.10.2" bcrypt = "0.17.0" -chrono = "0.4.40" +calamine = "0.26.1" +chrono = { version = "0.4.40", features = ["serde"] } 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" -hmac = "0.12.1" -jwt = "0.16.0" +fuzzy-matcher = "0.3.7" +jsonwebtoken = { version = "9.3.1", features = ["use_pem"] } mime = "0.3.17" objectid = "0.2.0" +regex = "1.11.1" 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" +serde_with = "3.12.0" +serde_repr = "0.1.20" +sha2 = "0.11.0-pre.5" tokio = { version = "1.44.1", features = ["macros", "rt-multi-thread"] } +rand = "0.9.0" + +[dev-dependencies] +actix-http = "3.10.0" +criterion = "0.5.1" + +[[bench]] +name = "parse" +harness = false diff --git a/lib/schedule_parser/benches/parse.rs b/benches/parse.rs similarity index 67% rename from lib/schedule_parser/benches/parse.rs rename to benches/parse.rs index 5891b45..fefb411 100644 --- a/lib/schedule_parser/benches/parse.rs +++ b/benches/parse.rs @@ -1,8 +1,9 @@ use criterion::{Criterion, criterion_group, criterion_main}; -use schedule_parser::parse_xls; + +use schedule_parser_rusted::parser::parse_xls; pub fn bench_parse_xls(c: &mut Criterion) { - let buffer: Vec = include_bytes!("../../../schedule.xls").to_vec(); + let buffer: Vec = include_bytes!("../schedule.xls").to_vec(); c.bench_function("parse_xls", |b| b.iter(|| parse_xls(&buffer))); } diff --git a/lib/schedule_parser/Cargo.toml b/lib/schedule_parser/Cargo.toml deleted file mode 100644 index 4b1906b..0000000 --- a/lib/schedule_parser/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "schedule_parser" -version = "0.2.0" -edition = "2024" - -[lib] -name = "schedule_parser" -path = "src/lib/lib.rs" - -[dependencies] -serde = { version = "1.0.219", features = ["derive"] } -serde_repr = "0.1.20" -chrono = { version = "0.4.40", features = ["serde"] } -calamine = "0.26.1" -regex = "1.11.1" -fuzzy-matcher = "0.3.7" - -[dev-dependencies] -criterion = "0.5.1" - -[[bench]] -name = "parse" -harness = false diff --git a/src/app_state.rs b/src/app_state.rs index 0afbd42..3d8c432 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -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, diff --git a/src/database/driver.rs b/src/database/driver.rs index 4f40a8e..df2aa29 100644 --- a/src/database/driver.rs +++ b/src/database/driver.rs @@ -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, _id: String) -> QueryResult { + pub fn get(connection: &Mutex, _id: &String) -> QueryResult { 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, - _username: String, + _username: &String, ) -> QueryResult { 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, + _vk_id: i32, + ) -> QueryResult { + let mut lock = connection.lock().unwrap(); + let con = lock.deref_mut(); - pub fn contains_by_username(connection: &Mutex, _username: String) -> bool { + users + .filter(vk_id.eq(_vk_id)) + .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(); @@ -45,7 +58,21 @@ pub mod users { } } - pub fn delete_by_username(connection: &Mutex, _username: String) -> bool { + pub fn contains_by_vk_id(connection: &Mutex, _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::(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(); @@ -61,11 +88,14 @@ pub mod users { 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) + insert_into(users) + .values(user) + .on_conflict_do_nothing() + .execute(con) } } diff --git a/src/database/models.rs b/src/database/models.rs index 8b2555a..2c0cd6b 100644 --- a/src/database/models.rs +++ b/src/database/models.rs @@ -23,4 +23,4 @@ pub struct User { pub group: String, pub role: UserRole, pub version: String, -} +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b2819a7 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +pub mod parser; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 859001f..b93981f 100644 --- a/src/main.rs +++ b/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) }) diff --git a/lib/schedule_parser/src/lib/lib.rs b/src/parser/mod.rs similarity index 98% rename from lib/schedule_parser/src/lib/lib.rs rename to src/parser/mod.rs index d5897d2..cc3a31c 100644 --- a/lib/schedule_parser/src/lib/lib.rs +++ b/src/parser/mod.rs @@ -1,6 +1,6 @@ -use crate::LessonParseResult::{Lessons, Street}; -use crate::schema::LessonType::Break; -use crate::schema::{ +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}; @@ -690,13 +690,18 @@ pub fn parse_xls(buffer: &Vec) -> ParseResult { } #[cfg(test)] -mod tests { +pub mod tests { use super::*; + pub fn test_result() -> ParseResult { + let buffer: Vec = include_bytes!("../../schedule.xls").to_vec(); + + parse_xls(&buffer) + } + #[test] fn read() { - let buffer: Vec = include_bytes!("../../../../schedule.xls").to_vec(); - let result = parse_xls(&buffer); + let result = test_result(); assert_ne!(result.groups.len(), 0); assert_ne!(result.teachers.len(), 0); diff --git a/lib/schedule_parser/src/lib/schema.rs b/src/parser/schema.rs similarity index 100% rename from lib/schedule_parser/src/lib/schema.rs rename to src/parser/schema.rs diff --git a/src/routes/auth/mod.rs b/src/routes/auth/mod.rs index 49e270c..7122840 100644 --- a/src/routes/auth/mod.rs +++ b/src/routes/auth/mod.rs @@ -1,3 +1,3 @@ -mod schema; pub mod sign_in; pub mod sign_up; +mod shared; diff --git a/src/routes/auth/schema.rs b/src/routes/auth/schema.rs deleted file mode 100644 index 4a0c12b..0000000 --- a/src/routes/auth/schema.rs +++ /dev/null @@ -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; - - #[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/shared.rs b/src/routes/auth/shared.rs new file mode 100644 index 0000000..f2c833d --- /dev/null +++ b/src/routes/auth/shared.rs @@ -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 = LazyLock::new(|| { + env::var("VK_ID_CLIENT_ID") + .expect("VK_ID_CLIENT_ID must be set") + .parse::() + .expect("VK_ID_CLIENT_ID must be i32") +}); + +pub fn parse_vk_id(token_str: &String) -> Result { + let dkey = DecodingKey::from_rsa_pem(VK_PUBLIC_KEY.as_bytes()).unwrap(); + + match decode::(&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::() { + 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), + }), + } +} diff --git a/src/routes/auth/sign_in.rs b/src/routes/auth/sign_in.rs index 0d3147f..f36c116 100644 --- a/src/routes/auth/sign_in.rs +++ b/src/routes/auth/sign_in.rs @@ -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, - app_state: web::Data, -) -> schema::sign_in::Response { - use schema::sign_in::*; +async fn sign_in(data: SignInData, app_state: &web::Data) -> 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::(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::(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, app_state: web::Data) -> Response { + sign_in(Default(data.into_inner()), &app_state).await +} + +#[post("/sign-in-vk")] +pub async fn sign_in_vk(data_json: Json, app_state: web::Data) -> 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; + + #[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(), }) diff --git a/src/routes/auth/sign_up.rs b/src/routes/auth/sign_up.rs index a2e6d6e..ae47c05 100644 --- a/src/routes/auth/sign_up.rs +++ b/src/routes/auth/sign_up.rs @@ -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, - app_state: web::Data, -) -> schema::sign_up::Response { - use schema::sign_up::*; - +async fn sign_up(data: SignUpData, app_state: &web::Data) -> 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, app_state: web::Data) -> 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, app_state: web::Data) -> 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; + + #[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, + 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); + } } diff --git a/src/routes/schema.rs b/src/routes/schema.rs index 6bac389..0ed3d78 100644 --- a/src/routes/schema.rs +++ b/src/routes/schema.rs @@ -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(pub Result); +pub struct IResponse(pub Result); - pub trait ErrorToHttpCode { - fn to_http_status_code(&self) -> StatusCode; - } +pub trait ErrorToHttpCode { + fn to_http_status_code(&self) -> StatusCode; +} - impl Responder for IResponse { - type Body = EitherBody; +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(), - }; + 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(), - } + 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() } } } diff --git a/src/test_env.rs b/src/test_env.rs index 58e2aca..e1d6318 100644 --- a/src/test_env.rs +++ b/src/test_env.rs @@ -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 { + 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 { - static STATE: LazyLock> = LazyLock::new(|| app_state()); + static STATE: LazyLock> = LazyLock::new(|| test_app_state()); STATE.clone() } diff --git a/src/utility/jwt.rs b/src/utility/jwt.rs index c510391..e758e06 100644 --- a/src/utility/jwt.rs +++ b/src/utility/jwt.rs @@ -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> = LazyLock::new(|| { +static DECODING_KEY: LazyLock = 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 = 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 { - 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, jwt::Error> = token.verify_with_key(jwt); +pub(crate) const DEFAULT_ALGORITHM: Algorithm = Algorithm::HS256; + +pub fn verify_and_decode(token: &String) -> Result { + let mut validation = Validation::new(DEFAULT_ALGORITHM); + + validation.required_spec_claims.remove("exp"); + validation.validate_exp = false; + + let result = decode::(&token, &*DECODING_KEY, &validation); match result { - Ok(claims) => { - let exp = claims.get("exp").unwrap(); - let exp_date = DateTime::from_timestamp(exp.parse::().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]