From 969401658fcd4d542706aa06d482ebd2a8e4cb1b Mon Sep 17 00:00:00 2001 From: jakubmanczak Date: Thu, 26 Feb 2026 00:39:33 +0100 Subject: [PATCH] login endpoint --- src/api/auth.rs | 42 ++++++++++++++++++++++++++++++++ src/api/mod.rs | 5 +++- src/users/auth/implementation.rs | 10 ++++++-- src/users/auth/mod.rs | 2 +- src/users/sessions.rs | 5 ++-- 5 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 src/api/auth.rs diff --git a/src/api/auth.rs b/src/api/auth.rs new file mode 100644 index 0000000..87ed78d --- /dev/null +++ b/src/api/auth.rs @@ -0,0 +1,42 @@ +use axum::{ + Json, + http::{HeaderMap, header}, + response::{IntoResponse, Response}, +}; +use serde::Deserialize; + +use crate::users::{ + User, + auth::{ + AuthError, COOKIE_NAME, UserAuthRequired, UserAuthenticate, + implementation::authenticate_via_credentials, + }, + sessions::Session, +}; + +#[derive(Deserialize)] +pub struct LoginForm { + handle: String, + password: String, +} + +pub async fn login(Json(creds): Json) -> Result { + let u = authenticate_via_credentials(&creds.handle, &creds.password)?.required()?; + let (_, token) = Session::new_for_user(&u)?; + + let secure = match cfg!(debug_assertions) { + false => "; Secure", + true => "", + }; + let cookie = format!( + "{COOKIE_NAME}={token}; Path=/; HttpOnly; SameSite=Lax; Max-Age={}{}", + Session::DEFAULT_PROLONGATION.num_seconds(), + secure + ); + + Ok(([(header::SET_COOKIE, cookie)], token).into_response()) +} + +pub async fn logout(headers: HeaderMap) -> Result { + todo!() +} diff --git a/src/api/mod.rs b/src/api/mod.rs index af9a8d1..1f5c969 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,7 +1,7 @@ use axum::{ Router, response::{IntoResponse, Response}, - routing::get, + routing::{get, post}, }; use crate::{ @@ -9,6 +9,7 @@ use crate::{ users::{UserError, auth::AuthError, sessions::SessionError}, }; +mod auth; mod sessions; mod tags; mod users; @@ -17,6 +18,8 @@ mod users; pub fn api_router() -> Router { Router::new() .route("/api/live", get(async || "Mnemosyne lives")) + .route("/api/auth/login", post(auth::login)) + .route("/api/auth/logout", post(auth::logout)) .route("/api/users/me", get(users::get_me)) .route("/api/users/{id}", get(users::get_by_id)) .route("/api/users/@{handle}", get(users::get_by_handle)) diff --git a/src/users/auth/implementation.rs b/src/users/auth/implementation.rs index c7a2a29..f886ce3 100644 --- a/src/users/auth/implementation.rs +++ b/src/users/auth/implementation.rs @@ -170,13 +170,19 @@ fn authenticate_basic(credentials: &str) -> Result, AuthError> { let decoded = BASE64_STANDARD.decode(credentials)?; let credentials_str = String::from_utf8(decoded)?; - let Some((username, password)) = credentials_str.split_once(':') else { + let Some((handle, password)) = credentials_str.split_once(':') else { return Err(AuthError::InvalidFormat); }; + authenticate_via_credentials(handle, password) +} +pub fn authenticate_via_credentials( + handle: &str, + password: &str, +) -> Result, AuthError> { let conn = database::conn()?; let user: Option<(Uuid, Option)> = conn .prepare("SELECT id, password FROM users WHERE handle = ?1")? - .query_row([username], |r| Ok((r.get(0)?, r.get(1)?))) + .query_row([handle], |r| Ok((r.get(0)?, r.get(1)?))) .optional()?; match user { diff --git a/src/users/auth/mod.rs b/src/users/auth/mod.rs index f22b43a..db34a0c 100644 --- a/src/users/auth/mod.rs +++ b/src/users/auth/mod.rs @@ -3,7 +3,7 @@ use rand08::{RngCore, rngs::OsRng}; use crate::users::{User, UserError, sessions::SessionError}; -mod implementation; +pub mod implementation; pub const COOKIE_NAME: &str = "mnemohash"; diff --git a/src/users/sessions.rs b/src/users/sessions.rs index 938bd1e..bf113b1 100644 --- a/src/users/sessions.rs +++ b/src/users/sessions.rs @@ -104,7 +104,6 @@ impl Session { None => Err(SessionError::NoSessionWithToken(token.to_string())), } } - #[allow(unused)] pub fn new_for_user(user: &User) -> Result<(Session, String), SessionError> { let id = Uuid::now_v7(); let token = auth::generate_token(auth::TokenSize::Char64); @@ -112,7 +111,7 @@ impl Session { let expiry = Utc::now() + Session::DEFAULT_PROLONGATION; database::conn()? - .prepare("INSERT INTO sessions VALUES (?1, ?2, ?3, ?4)")? + .prepare("INSERT INTO sessions(id, token, user_id, expiry) VALUES (?1, ?2, ?3, ?4)")? .execute((&id, &hashed, user.id, expiry))?; let s = Session { id, @@ -123,7 +122,7 @@ impl Session { Ok((s, token)) } - const DEFAULT_PROLONGATION: Duration = Duration::days(14); + pub const DEFAULT_PROLONGATION: Duration = Duration::days(14); const PROLONGATION_THRESHOLD: Duration = Duration::hours(2); pub fn prolong(&mut self) -> Result<(), SessionError> { if self.expiry - Session::DEFAULT_PROLONGATION + Session::PROLONGATION_THRESHOLD