From 92436c8bb9eafcc56219e784f8b374edfb1907a3 Mon Sep 17 00:00:00 2001 From: Daniel Hader Date: Wed, 20 May 2026 22:04:16 -0500 Subject: basic login route with JWT --- src/routes/auth.rs | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/routes/errors.rs | 8 +++++++ 2 files changed, 74 insertions(+) create mode 100644 src/routes/auth.rs (limited to 'src/routes') diff --git a/src/routes/auth.rs b/src/routes/auth.rs new file mode 100644 index 0000000..0b17ef4 --- /dev/null +++ b/src/routes/auth.rs @@ -0,0 +1,66 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use axum::extract::{Json, State}; +use axum::response::IntoResponse; +use jsonwebtoken::{encode, Header}; +use serde::{Deserialize, Serialize}; + +use crate::AppState; +use crate::auth::{check_password}; +use super::errors::RouteError; + +#[derive(Deserialize)] +pub struct LoginRequest { + email: String, + password: String, +} + +#[derive(Serialize)] +struct LoginResponse { + token: String, +} + +#[derive(Serialize)] +struct JWTClaims { + sub: String, + exp: usize, + iat: usize, + is_admin: bool, +} + +pub async fn login( + State(state): State, + Json(request): Json +) -> Result { + + let user = match state.database.fetch_user_by_email(&request.email) { + Err(_) => return Err(RouteError::Internal("database action failed".into())), + Ok(None) => return Err(RouteError::UnregisteredEmail(request.email)), + Ok(Some(user)) => user + }; + + match check_password(&request.password, user.password_hash()) { + Err(_) => return Err(RouteError::Internal("failed to check password".into())), + Ok(false) => return Err(RouteError::AuthorizationFailure()), + Ok(true) => {}, + } + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| RouteError::Internal("failed to access system clock".into()))? + .as_secs() as usize; + + let claims = JWTClaims { + sub: user.email().to_owned(), + iat: now, + exp: now + 60 * 60 * 24, + is_admin: false + }; + + let token= encode(&Header::default(), &claims, &state.api_key) + .map_err(|_| RouteError::Internal("failed to encode jwt".into()))?; + + Ok(Json(LoginResponse { token })) +} + + diff --git a/src/routes/errors.rs b/src/routes/errors.rs index 6261d75..78b4e3a 100644 --- a/src/routes/errors.rs +++ b/src/routes/errors.rs @@ -5,6 +5,8 @@ pub enum RouteError { Internal(String), UserCreateEmailExists(String), UserCreateUsernameExists(String), + UnregisteredEmail(String), + AuthorizationFailure(), } impl IntoResponse for RouteError { @@ -18,6 +20,12 @@ impl IntoResponse for RouteError { }, RouteError::UserCreateUsernameExists(username) => { (StatusCode::BAD_REQUEST, format!("user with username \"{}\" already exists", username)) + }, + RouteError::UnregisteredEmail(email) => { + (StatusCode::BAD_REQUEST, format!("email \"{}\" is not registered", email)) + }, + RouteError::AuthorizationFailure() => { + (StatusCode::UNAUTHORIZED, format!("failed to authorize")) } }; -- cgit v1.2.3