diff options
| author | Santo Cariotti <santo@dcariotti.me> | 2022-10-17 20:08:09 +0000 |
|---|---|---|
| committer | Santo Cariotti <santo@dcariotti.me> | 2022-10-17 20:08:09 +0000 |
| commit | 74976dab57887a4d7e29b426cdf7422722fa58ee (patch) | |
| tree | 5f1bbed3dbcf3ba520866cb4eb060aaf5a771915 /src/auth | |
| parent | 611293122213f83e82d851cd8dc83fd1e4f79dcd (diff) | |
Refactoring of mods
Diffstat (limited to 'src/auth')
| -rw-r--r-- | src/auth/mod.rs | 2 | ||||
| -rw-r--r-- | src/auth/models.rs | 122 | ||||
| -rw-r--r-- | src/auth/routes.rs | 66 |
3 files changed, 190 insertions, 0 deletions
diff --git a/src/auth/mod.rs b/src/auth/mod.rs new file mode 100644 index 0000000..a0e1883 --- /dev/null +++ b/src/auth/mod.rs @@ -0,0 +1,2 @@ +pub mod models; +pub mod routes; diff --git a/src/auth/models.rs b/src/auth/models.rs new file mode 100644 index 0000000..8a673dd --- /dev/null +++ b/src/auth/models.rs @@ -0,0 +1,122 @@ +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 + pub 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, +} + +/// Payload used for login +#[derive(Deserialize)] +pub struct LoginCredentials { + pub username: String, + pub password: String, +} + +/// Paylod used for user creation +#[derive(Deserialize)] +pub struct SignUpForm { + pub name: String, + pub email: String, + pub username: String, + pub password1: String, + pub password2: String, +} + +static KEYS: Lazy<Keys> = Lazy::new(|| { + let secret = &crate::config::CONFIG.jwt_secret; + 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(1); + + 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)?; + + let now = Local::now().timestamp() as usize; + + if token_data.claims.exp < now { + return Err(AppError::InvalidToken); + } + + Ok(token_data.claims) + } +} diff --git a/src/auth/routes.rs b/src/auth/routes.rs new file mode 100644 index 0000000..65a5cb5 --- /dev/null +++ b/src/auth/routes.rs @@ -0,0 +1,66 @@ +use crate::{ + errors::AppError, + auth::models::{AuthBody, Claims, LoginCredentials, SignUpForm}, + user::models::User, + routes::JsonCreate, +}; +use axum::{routing::post, Json, Router}; + +/// Create routes for `/v1/auth/` namespace +pub fn create_route() -> Router { + Router::new() + .route("/login", post(make_login)) + .route("/signup", post(signup)) +} + +/// Make login. Check if a user with the email and password passed in request body exists into the +/// database +async fn make_login(Json(payload): Json<LoginCredentials>) -> Result<Json<AuthBody>, AppError> { + let user = User::new( + String::new(), + String::new(), + payload.username, + 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("User not found".to_string())), + } +} + +/// Create a new user +async fn signup(Json(payload): Json<SignUpForm>) -> Result<JsonCreate<AuthBody>, AppError> { + if payload.password1 != payload.password2 { + return Err(AppError::BadRequest( + "The inserted passwords do not match".to_string(), + )); + } + + if User::email_has_taken(&payload.email).await? { + return Err(AppError::BadRequest( + "An user with this email already exists".to_string(), + )); + } + + if User::username_has_taken(&payload.username).await? { + return Err(AppError::BadRequest( + "An user with this username already exists".to_string(), + )); + } + + let user = User::new( + payload.name, + payload.email, + payload.username, + payload.password1, + ); + let user = User::create(user).await?; + + let claims = Claims::new(user.id); + let token = claims.get_token()?; + Ok(JsonCreate(AuthBody::new(token))) +} |
