diff options
-rw-r--r-- | server/Cargo.lock | 67 | ||||
-rw-r--r-- | server/Cargo.toml | 1 | ||||
-rw-r--r-- | server/src/errors.rs | 7 | ||||
-rw-r--r-- | server/src/main.rs | 4 | ||||
-rw-r--r-- | server/src/models/auth.rs | 63 | ||||
-rw-r--r-- | server/src/models/mod.rs | 1 | ||||
-rw-r--r-- | server/src/models/user.rs | 22 | ||||
-rw-r--r-- | server/src/routes/auth.rs | 22 | ||||
-rw-r--r-- | server/src/routes/mod.rs | 1 |
9 files changed, 183 insertions, 5 deletions
diff --git a/server/Cargo.lock b/server/Cargo.lock index 9148b0d..514f558 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -38,6 +38,15 @@ dependencies = [ ] [[package]] +name = "android_system_properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e" +dependencies = [ + "libc", +] + +[[package]] name = "ansi_term" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -213,6 +222,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] +name = "chrono" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time 0.1.44", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] name = "cpufeatures" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -413,7 +443,7 @@ checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -565,6 +595,19 @@ dependencies = [ ] [[package]] +name = "iana-time-zone" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] name = "idna" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -675,6 +718,7 @@ version = "0.1.0" dependencies = [ "async-trait", "axum", + "chrono", "http-body", "jsonwebtoken", "once_cell", @@ -745,7 +789,7 @@ checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys", ] @@ -1218,7 +1262,7 @@ dependencies = [ "num-bigint", "num-traits", "thiserror", - "time", + "time 0.3.13", ] [[package]] @@ -1411,6 +1455,17 @@ dependencies = [ [[package]] name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "time" version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db76ff9fa4b1458b3c7f077f3ff9887394058460d21e634355b273aaf11eea45" @@ -1752,6 +1807,12 @@ dependencies = [ [[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" diff --git a/server/Cargo.toml b/server/Cargo.toml index 8527fdb..f5cf075 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -19,3 +19,4 @@ tower-http = { version = "0.3.4", features = ["trace", "compression-br", "propag sqlx = { version = "0.6", features = [ "runtime-tokio-rustls", "postgres" ] } sha256 = "1.0.3" validator = { version = "0.16.0", features = ["derive"] } +chrono = "0.4" diff --git a/server/src/errors.rs b/server/src/errors.rs index 9221fea..d991132 100644 --- a/server/src/errors.rs +++ b/server/src/errors.rs @@ -9,6 +9,8 @@ pub enum AppError { Generic, Database, BadRequest(String), + NotFound, + TokenCreation, } impl IntoResponse for AppError { @@ -23,6 +25,11 @@ impl IntoResponse for AppError { "Error with database connection".to_string(), ), AppError::BadRequest(value) => (StatusCode::BAD_REQUEST, value), + AppError::NotFound => (StatusCode::NOT_FOUND, "Element not found".to_string()), + AppError::TokenCreation => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Token creation error".to_string(), + ), }; let body = Json(json!({ diff --git a/server/src/main.rs b/server/src/main.rs index e2536ca..3436e2c 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -31,7 +31,9 @@ async fn create_app() -> Router { logger::setup(); let _ = db::setup().await; - let api_routes = Router::new().nest("/users", routes::user::create_route()); + let api_routes = Router::new() + .nest("/users", routes::user::create_route()) + .nest("/auth", routes::auth::create_route()); Router::new() .nest("/v1", api_routes) diff --git a/server/src/models/auth.rs b/server/src/models/auth.rs new file mode 100644 index 0000000..03b198b --- /dev/null +++ b/server/src/models/auth.rs @@ -0,0 +1,63 @@ +use crate::errors::AppError; +use chrono::{Duration, Local}; +use jsonwebtoken::{encode, DecodingKey, EncodingKey, Header, Validation}; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; + +struct Keys { + encoding: EncodingKey, + decoding: DecodingKey, +} + +#[derive(Serialize, Deserialize)] +pub struct Claims { + user_id: i32, + exp: usize, +} + +#[derive(Serialize)] +pub struct AuthBody { + access_token: String, + token_type: String, +} + +static KEYS: Lazy<Keys> = Lazy::new(|| { + let secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set"); + Keys::new(secret.as_bytes()) +}); + +impl Keys { + fn new(secret: &[u8]) -> Self { + Self { + encoding: EncodingKey::from_secret(secret), + decoding: DecodingKey::from_secret(secret), + } + } +} + +impl Claims { + pub fn new(user_id: i32) -> Self { + let expiration = Local::now() + Duration::days(2); + + Self { + user_id, + exp: expiration.timestamp() as usize, + } + } + + pub fn get_token(&self) -> Result<String, AppError> { + let token = encode(&Header::default(), &self, &KEYS.encoding) + .map_err(|_| AppError::TokenCreation)?; + + Ok(token) + } +} + +impl AuthBody { + pub fn new(access_token: String) -> Self { + Self { + access_token, + token_type: "Bearer".to_string(), + } + } +} diff --git a/server/src/models/mod.rs b/server/src/models/mod.rs index 22d12a3..f9bae3d 100644 --- a/server/src/models/mod.rs +++ b/server/src/models/mod.rs @@ -1 +1,2 @@ +pub mod auth; pub mod user; diff --git a/server/src/models/user.rs b/server/src/models/user.rs index 964f04a..38ec121 100644 --- a/server/src/models/user.rs +++ b/server/src/models/user.rs @@ -16,7 +16,7 @@ pub struct User { #[derive(Deserialize, Serialize)] pub struct UserList { - id: i32, + pub id: i32, email: String, is_staff: Option<bool>, } @@ -61,6 +61,26 @@ impl User { Ok(rec) } + pub async fn find(user: User) -> Result<UserList, AppError> { + let pool = unsafe { get_client() }; + + let crypted_password = sha256::digest(user.password); + + let rec = sqlx::query_as!( + UserList, + r#" + SELECT id, email, is_staff FROM "users" + WHERE email = $1 AND password = $2 + "#, + user.email, + crypted_password + ) + .fetch_one(pool) + .await?; + + Ok(rec) + } + pub async fn list() -> Result<Vec<UserList>, AppError> { let pool = unsafe { get_client() }; let rows = sqlx::query_as!(UserList, r#"SELECT id, email, is_staff FROM users"#) diff --git a/server/src/routes/auth.rs b/server/src/routes/auth.rs new file mode 100644 index 0000000..629ed33 --- /dev/null +++ b/server/src/routes/auth.rs @@ -0,0 +1,22 @@ +use crate::errors::AppError; +use crate::models::{ + auth::{AuthBody, Claims}, + user::{User, UserCreate}, +}; +use axum::{routing::post, Json, Router}; + +pub fn create_route() -> Router { + Router::new().route("/login", post(make_login)) +} + +async fn make_login(Json(payload): Json<UserCreate>) -> Result<Json<AuthBody>, AppError> { + let user = User::new(payload.email, payload.password); + match User::find(user).await { + Ok(user) => { + let claims = Claims::new(user.id); + let token = claims.get_token()?; + Ok(Json(AuthBody::new(token))) + } + Err(_) => Err(AppError::NotFound), + } +} diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index 22d12a3..f9bae3d 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -1 +1,2 @@ +pub mod auth; pub mod user; |