login endpoint

This commit is contained in:
2026-02-26 00:39:33 +01:00
parent ba3b3413d0
commit 969401658f
5 changed files with 57 additions and 7 deletions

42
src/api/auth.rs Normal file
View File

@@ -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<LoginForm>) -> Result<Response, AuthError> {
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<Response, AuthError> {
todo!()
}

View File

@@ -1,7 +1,7 @@
use axum::{ use axum::{
Router, Router,
response::{IntoResponse, Response}, response::{IntoResponse, Response},
routing::get, routing::{get, post},
}; };
use crate::{ use crate::{
@@ -9,6 +9,7 @@ use crate::{
users::{UserError, auth::AuthError, sessions::SessionError}, users::{UserError, auth::AuthError, sessions::SessionError},
}; };
mod auth;
mod sessions; mod sessions;
mod tags; mod tags;
mod users; mod users;
@@ -17,6 +18,8 @@ mod users;
pub fn api_router() -> Router { pub fn api_router() -> Router {
Router::new() Router::new()
.route("/api/live", get(async || "Mnemosyne lives")) .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/me", get(users::get_me))
.route("/api/users/{id}", get(users::get_by_id)) .route("/api/users/{id}", get(users::get_by_id))
.route("/api/users/@{handle}", get(users::get_by_handle)) .route("/api/users/@{handle}", get(users::get_by_handle))

View File

@@ -170,13 +170,19 @@ fn authenticate_basic(credentials: &str) -> Result<Option<User>, AuthError> {
let decoded = BASE64_STANDARD.decode(credentials)?; let decoded = BASE64_STANDARD.decode(credentials)?;
let credentials_str = String::from_utf8(decoded)?; 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); return Err(AuthError::InvalidFormat);
}; };
authenticate_via_credentials(handle, password)
}
pub fn authenticate_via_credentials(
handle: &str,
password: &str,
) -> Result<Option<User>, AuthError> {
let conn = database::conn()?; let conn = database::conn()?;
let user: Option<(Uuid, Option<String>)> = conn let user: Option<(Uuid, Option<String>)> = conn
.prepare("SELECT id, password FROM users WHERE handle = ?1")? .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()?; .optional()?;
match user { match user {

View File

@@ -3,7 +3,7 @@ use rand08::{RngCore, rngs::OsRng};
use crate::users::{User, UserError, sessions::SessionError}; use crate::users::{User, UserError, sessions::SessionError};
mod implementation; pub mod implementation;
pub const COOKIE_NAME: &str = "mnemohash"; pub const COOKIE_NAME: &str = "mnemohash";

View File

@@ -104,7 +104,6 @@ impl Session {
None => Err(SessionError::NoSessionWithToken(token.to_string())), None => Err(SessionError::NoSessionWithToken(token.to_string())),
} }
} }
#[allow(unused)]
pub fn new_for_user(user: &User) -> Result<(Session, String), SessionError> { pub fn new_for_user(user: &User) -> Result<(Session, String), SessionError> {
let id = Uuid::now_v7(); let id = Uuid::now_v7();
let token = auth::generate_token(auth::TokenSize::Char64); let token = auth::generate_token(auth::TokenSize::Char64);
@@ -112,7 +111,7 @@ impl Session {
let expiry = Utc::now() + Session::DEFAULT_PROLONGATION; let expiry = Utc::now() + Session::DEFAULT_PROLONGATION;
database::conn()? 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))?; .execute((&id, &hashed, user.id, expiry))?;
let s = Session { let s = Session {
id, id,
@@ -123,7 +122,7 @@ impl Session {
Ok((s, token)) 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); const PROLONGATION_THRESHOLD: Duration = Duration::hours(2);
pub fn prolong(&mut self) -> Result<(), SessionError> { pub fn prolong(&mut self) -> Result<(), SessionError> {
if self.expiry - Session::DEFAULT_PROLONGATION + Session::PROLONGATION_THRESHOLD if self.expiry - Session::DEFAULT_PROLONGATION + Session::PROLONGATION_THRESHOLD