From ba86dfc3fe735bf091be37e1a029abe22676ffd7 Mon Sep 17 00:00:00 2001 From: N08I40K Date: Sat, 22 Mar 2025 22:44:52 +0400 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D0=BB=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D1=8C=D1=8E=20=D1=80=D0=B0=D0=B1=D0=BE=D1=87=D0=B0=D1=8F=20?= =?UTF-8?q?=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 92 +++++++++++++++++++++++++++++++++++++- Cargo.toml | 4 ++ src/database/driver.rs | 34 ++++++++++++++ src/database/mod.rs | 1 + src/database/models.rs | 7 +-- src/main.rs | 11 ++++- src/routes/auth/sign_in.rs | 36 +++++++++------ src/utility/jwt.rs | 76 +++++++++++++++++++++++++++++++ src/utility/mod.rs | 1 + 9 files changed, 241 insertions(+), 21 deletions(-) create mode 100644 src/database/driver.rs create mode 100644 src/utility/jwt.rs create mode 100644 src/utility/mod.rs diff --git a/Cargo.lock b/Cargo.lock index d5c15c9..95dbadf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,7 +29,7 @@ dependencies = [ "actix-rt", "actix-service", "actix-utils", - "base64", + "base64 0.22.1", "bitflags", "brotli", "bytes", @@ -287,12 +287,31 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bcrypt" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92758ad6077e4c76a6cadbce5005f666df70d4f13b19976b1a8062eef880040f" +dependencies = [ + "base64 0.22.1", + "blowfish", + "getrandom 0.3.2", + "subtle", + "zeroize", +] + [[package]] name = "bitflags" version = "2.9.0" @@ -308,6 +327,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "brotli" version = "7.0.0" @@ -436,6 +465,16 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.5.32" @@ -722,6 +761,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -991,6 +1031,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "0.2.12" @@ -1304,6 +1353,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1355,6 +1413,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jwt" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f" +dependencies = [ + "base64 0.13.1", + "crypto-common", + "digest", + "hmac", + "serde", + "serde_json", + "sha2", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1782,7 +1855,7 @@ version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", @@ -1927,13 +2000,17 @@ name = "schedule-parser-rusted" version = "0.3.0" dependencies = [ "actix-web", + "bcrypt", "chrono", "diesel", "diesel-derive-enum", "dotenvy", + "hmac", + "jwt", "reqwest", "schedule_parser", "serde", + "sha2", "tokio", ] @@ -2045,6 +2122,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index 2094a0c..0629a9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,10 @@ edition = "2024" publish = false [dependencies] +bcrypt = "0.17.0" +jwt = "0.16.0" +hmac = "0.12.1" +sha2 = "0.10.8" 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" diff --git a/src/database/driver.rs b/src/database/driver.rs new file mode 100644 index 0000000..a5f93c0 --- /dev/null +++ b/src/database/driver.rs @@ -0,0 +1,34 @@ +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::{PgConnection, SelectableHelper}; + use diesel::{QueryDsl, RunQueryDsl}; + use std::ops::DerefMut; + use std::sync::Mutex; + + pub fn get(connection: &Mutex, _id: String) -> QueryResult { + let mut lock = connection.lock().unwrap(); + let con = lock.deref_mut(); + + users + .filter(id.eq(_id)) + .select(User::as_select()) + .first(con) + } + + pub fn get_by_username( + connection: &Mutex, + _username: String, + ) -> QueryResult { + let mut lock = connection.lock().unwrap(); + let con = lock.deref_mut(); + + users + .filter(username.eq(_username)) + .select(User::as_select()) + .first(con) + } +} diff --git a/src/database/mod.rs b/src/database/mod.rs index 2e2cb1f..2ff4d25 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,2 +1,3 @@ pub mod schema; pub mod models; +pub mod driver; diff --git a/src/database/models.rs b/src/database/models.rs index 18ab713..76aec48 100644 --- a/src/database/models.rs +++ b/src/database/models.rs @@ -1,7 +1,8 @@ use diesel::prelude::*; +use diesel::{AsExpression, FromSqlRow}; use serde::Serialize; -#[derive(diesel_derive_enum::DbEnum, Serialize, Debug)] +#[derive(diesel_derive_enum::DbEnum, Serialize, Debug, Clone, Copy, PartialEq)] #[ExistingTypePath = "crate::database::schema::sql_types::UserRole"] #[DbValueStyle = "UPPERCASE"] #[serde(rename_all = "UPPERCASE")] @@ -11,9 +12,9 @@ pub enum UserRole { Admin, } -#[derive(Queryable, Selectable, Serialize)] +#[derive(Identifiable, AsChangeset, Queryable, Selectable, Serialize)] #[diesel(table_name = crate::database::schema::users)] -#[diesel(check_for_backend(diesel::pg::Pg))] +#[changeset_options(treat_none_as_null = "true")] pub struct User { pub id: String, pub username: String, diff --git a/src/main.rs b/src/main.rs index c453f95..0ec4793 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,17 @@ use crate::routes::auth::sign_in::sign_in; use crate::xls_downloader::basic_impl::BasicXlsDownloader; -use actix_web::{web, App, HttpServer}; +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::sync::Mutex; use std::env; +use std::sync::{Mutex, MutexGuard}; mod database; mod routes; +mod utility; mod xls_downloader; pub struct AppState { @@ -28,6 +29,12 @@ pub struct AppState { database: Mutex, } +impl AppState { + pub fn connection(&self) -> MutexGuard { + self.database.lock().unwrap() + } +} + #[actix_web::main] async fn main() { dotenv().ok(); diff --git a/src/routes/auth/sign_in.rs b/src/routes/auth/sign_in.rs index 83d953d..d717d77 100644 --- a/src/routes/auth/sign_in.rs +++ b/src/routes/auth/sign_in.rs @@ -1,26 +1,34 @@ +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::AppState; +use crate::{AppState, utility}; use actix_web::{post, web}; -use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper}; +use diesel::SaveChangesDsl; use std::ops::DerefMut; use web::Json; #[post("/sign-in")] pub async fn sign_in(data: Json, app_state: web::Data) -> Json { - use crate::database::schema::users::dsl::*; + let result = 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(); - match { - let mut lock = app_state.database.lock().unwrap(); - let connection = lock.deref_mut(); + user.access_token = + utility::jwt::encode(&user.id).expect("Failed to generate jet token"); - users - .filter(username.eq(data.username.clone())) - .select(User::as_select()) - .first(connection) - } { - Ok(user) => Json(SignInResult::ok(&user)), - Err(_) => Json(SignInResult::err(IncorrectCredentials)), - } + user.save_changes::(conn) + .expect("Failed to update user"); + + SignInResult::ok(&user) + } + Ok(false) | Err(_) => SignInResult::err(IncorrectCredentials), + }, + + Err(_) => SignInResult::err(IncorrectCredentials), + }; + + Json(result) } diff --git a/src/utility/jwt.rs b/src/utility/jwt.rs new file mode 100644 index 0000000..f606379 --- /dev/null +++ b/src/utility/jwt.rs @@ -0,0 +1,76 @@ +use chrono::{DateTime, Duration, Utc}; +use hmac::{Hmac, Mac}; +use jwt::{SignWithKey, Token, VerifyWithKey}; +use sha2::Sha256; +use std::collections::BTreeMap; +use std::env; +use std::sync::LazyLock; + +static JWT_SECRET: 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") +}); + +#[derive(Debug)] +pub enum VerifyError { + JwtError(jwt::Error), + InvalidSignature, + NoExpirationTag, + Expired, + NoId, +} + +pub fn verify_and_decode(token: &String) -> Result { + let jwt = &*JWT_SECRET; + + let result: Result, jwt::Error> = token.verify_with_key(jwt); + + match result { + Ok(claims) => match claims.get("exp") { + None => Err(VerifyError::NoExpirationTag), + Some(exp) => { + 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); + } + + match claims.get("id").cloned() { + None => Err(VerifyError::NoId), + Some(id) => Ok(id), + } + } + }, + Err(err) => Err(match err { + jwt::Error::InvalidSignature => VerifyError::InvalidSignature, + + _ => VerifyError::JwtError(err), + }), + } +} + +pub fn encode(id: &String) -> Result { + let header = jwt::Header { + type_: Some(jwt::header::HeaderType::JsonWebToken), + ..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(); + + claims.insert("id", id.as_str()); + claims.insert("iat", iat_str.as_str()); + claims.insert("exp", exp_str.as_str()); + + match Token::new(header, claims).sign_with_key(&*JWT_SECRET) { + Ok(token) => Ok(token.as_str().to_string()), + Err(err) => Err(err), + } +} diff --git a/src/utility/mod.rs b/src/utility/mod.rs new file mode 100644 index 0000000..6dbefcf --- /dev/null +++ b/src/utility/mod.rs @@ -0,0 +1 @@ +pub mod jwt; \ No newline at end of file