diff options
Diffstat (limited to 'src/models')
| -rw-r--r-- | src/models/auth.rs | 99 | ||||
| -rw-r--r-- | src/models/mod.rs | 2 | ||||
| -rw-r--r-- | src/models/user.rs | 118 |
3 files changed, 219 insertions, 0 deletions
diff --git a/src/models/auth.rs b/src/models/auth.rs new file mode 100644 index 0000000..8b8f61c --- /dev/null +++ b/src/models/auth.rs @@ -0,0 +1,99 @@ +use crate::errors::AppError; +use axum::{ + async_trait, + extract::{FromRequest, RequestParts, TypedHeader}, + headers::{authorization::Bearer, Authorization}, +}; +use chrono::{Duration, Local}; +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; + +struct Keys { + encoding: EncodingKey, + decoding: DecodingKey, +} + +/// Claims struct +#[derive(Serialize, Deserialize)] +pub struct Claims { + /// ID from the user model + user_id: i32, + /// Expiration timestamp + exp: usize, +} + +/// Body used as response to login +#[derive(Serialize)] +pub struct AuthBody { + /// Access token string + access_token: String, + /// "Bearer" 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 { + /// Create a new Claim using the `user_id` and the current timestamp + 2 days + pub fn new(user_id: i32) -> Self { + let expiration = Local::now() + Duration::days(2); + + Self { + user_id, + exp: expiration.timestamp() as usize, + } + } + + /// Returns the token as a string. If a token is not encoded, raises an + /// `AppError::TokenCreation` + 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(), + } + } +} + +/// Parse a request to get the Authorization header and then decode it checking its validation +#[async_trait] +impl<B> FromRequest<B> for Claims +where + B: Send, +{ + type Rejection = AppError; + + async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> { + // Extract the token from the authorization header + let TypedHeader(Authorization(bearer)) = + TypedHeader::<Authorization<Bearer>>::from_request(req) + .await + .map_err(|_| AppError::InvalidToken)?; + // Decode the user data + let token_data = decode::<Claims>(bearer.token(), &KEYS.decoding, &Validation::default()) + .map_err(|_| AppError::InvalidToken)?; + + Ok(token_data.claims) + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..f9bae3d --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,2 @@ +pub mod auth; +pub mod user; diff --git a/src/models/user.rs b/src/models/user.rs new file mode 100644 index 0000000..06cde0a --- /dev/null +++ b/src/models/user.rs @@ -0,0 +1,118 @@ +use crate::db::get_client; +use crate::errors::AppError; + +use serde::{Deserialize, Serialize}; +use validator::Validate; + +/// User model +#[derive(Deserialize, Serialize, Validate)] +pub struct User { + id: i32, + #[validate(length(min = 1, message = "Can not be empty"))] + email: String, + #[validate(length(min = 8, message = "Must be min 8 chars length"))] + password: String, + is_staff: Option<bool>, +} + +/// Response used to print a user (or a users list) +#[derive(Deserialize, Serialize)] +pub struct UserList { + // It is public because it used by `Claims` creation + pub id: i32, + email: String, + is_staff: Option<bool>, +} + +/// Payload used for user creation +#[derive(Deserialize)] +pub struct UserCreate { + pub email: String, + pub password: String, +} + +impl User { + /// By default an user has id = 0. It is not created yet + pub fn new(email: String, password: String) -> Self { + Self { + id: 0, + email, + password, + is_staff: Some(false), + } + } + + /// Create a new user from the model using a SHA256 crypted password + pub async fn create(user: User) -> Result<UserList, AppError> { + let pool = unsafe { get_client() }; + + user.validate() + .map_err(|error| AppError::BadRequest(error.to_string()))?; + + let crypted_password = sha256::digest(user.password); + + let rec = sqlx::query_as!( + UserList, + r#" + INSERT INTO users (email, password) + VALUES ( $1, $2 ) + RETURNING id, email, is_staff + "#, + user.email, + crypted_password + ) + .fetch_one(pool) + .await?; + + Ok(rec) + } + + /// Find a user using the model. It used for login + 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) + } + + /// Returns the user with id = `user_id` + pub async fn find_by_id(user_id: i32) -> Result<UserList, AppError> { + let pool = unsafe { get_client() }; + + let rec = sqlx::query_as!( + UserList, + r#" + SELECT id, email, is_staff FROM "users" + WHERE id = $1 + "#, + user_id + ) + .fetch_one(pool) + .await?; + + Ok(rec) + } + + /// List all users + 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"#) + .fetch_all(pool) + .await?; + + Ok(rows) + } +} |
