diff options
| author | Daniel Hader <[email protected]> | 2026-05-20 22:04:16 -0500 |
|---|---|---|
| committer | Daniel Hader <[email protected]> | 2026-05-20 22:04:16 -0500 |
| commit | 92436c8bb9eafcc56219e784f8b374edfb1907a3 (patch) | |
| tree | 45c9ebc50220414c20700e653cb17dd0c21ec00f /src/routes | |
| parent | 74bc939843ae5c35fbd367c1ef0144b6074cfefe (diff) | |
basic login route with JWT
Diffstat (limited to 'src/routes')
| -rw-r--r-- | src/routes/auth.rs | 66 | ||||
| -rw-r--r-- | src/routes/errors.rs | 8 |
2 files changed, 74 insertions, 0 deletions
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<AppState>, + Json(request): Json<LoginRequest> +) -> Result<impl IntoResponse, RouteError> { + + 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")) } }; |
