use std::sync::Arc; use argon2::{Argon2, PasswordHash, PasswordVerifier}; use axum::{extract::State, response::IntoResponse, Json}; use axum_extra::{headers::Authorization, routing::TypedPath, TypedHeader}; use serde::Deserialize; use crate::{ error::AuthError, jwt::Claims, model::{LoginSchema, UserSchema}, state::AppState, Error, }; #[derive(Debug, Deserialize, TypedPath)] #[typed_path("/api/login")] pub struct Login; impl Login { #[tracing::instrument(skip(state, password))] pub async fn post( self, State(state): State>, Json(LoginSchema { email, password }): Json, ) -> Result { let UserSchema { uuid, password_hash, .. } = sqlx::query_as!( UserSchema, "SELECT * FROM users WHERE email = $1", email.to_ascii_lowercase() ) .fetch_optional(&state.pool) .await? .ok_or(AuthError::LoginInvalid)?; Argon2::default() .verify_password(password.as_bytes(), &PasswordHash::new(&password_hash)?)?; let token = Claims::new(uuid, state.jwt_max_age).encode(state.jwt_secret.as_ref())?; Authorization::bearer(&token) .map(TypedHeader) .map_err(Into::into) } } #[derive(Debug, Deserialize, TypedPath)] #[typed_path("/api/logout")] pub struct Logout; impl Logout { #[tracing::instrument] pub async fn get(self) -> impl IntoResponse { todo!("Invalidate jwt somehow..."); } } #[cfg(test)] mod tests { use super::*; use axum::{ body::Body, http::{header, Request, StatusCode}, }; use sqlx::PgPool; use tower::ServiceExt; use crate::init_router; const JWT_SECRET: &str = "test-jwt-secret-token"; const JWT_MAX_AGE: time::Duration = time::Duration::HOUR; type TestResult> = std::result::Result; #[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))] async fn test_login_unauthorized(pool: PgPool) -> TestResult { let state = Arc::new(AppState { pool, jwt_secret: JWT_SECRET.to_string(), jwt_max_age: JWT_MAX_AGE, }); let router = init_router(state.clone()); let user = LoginSchema { email: "adent@earth.sol".to_string(), password: "hunter2".to_string(), }; let request = Request::builder() .uri("/api/login") .method("POST") .header(header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref()) .body(Body::from(serde_json::to_vec(&user)?))?; let response = router.oneshot(request).await?; assert_eq!(StatusCode::UNAUTHORIZED, response.status()); Ok(()) } #[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))] async fn test_login_ok(pool: PgPool) -> TestResult { let state = Arc::new(AppState { pool, jwt_secret: JWT_SECRET.to_string(), jwt_max_age: JWT_MAX_AGE, }); let router = init_router(state.clone()); let user = LoginSchema { email: "adent@earth.sol".to_string(), password: "solongandthanksforallthefish".to_string(), }; let request = Request::builder() .uri("/api/login") .method("POST") .header(header::CONTENT_TYPE, mime::APPLICATION_JSON.as_ref()) .body(Body::from(serde_json::to_vec(&user)?))?; let response = router.oneshot(request).await?; assert_eq!(StatusCode::OK, response.status()); Ok(()) } }