koan_server/auth/mod.rs
1//! Authentication layer for the koan server.
2//!
3//! When `auth_enabled = true`:
4//! - GraphQL requests must carry a valid JWT (see `middleware` for where it is
5//! read from); the web UI takes it only as the `koan_access` cookie
6//! - Auth routes (/auth/login, /auth/refresh, /auth/logout) are always accessible
7//!
8//! When `auth_enabled = false` (opt-in, not the default):
9//! - All requests are treated as admin — no auth required.
10
11pub mod middleware;
12pub mod password;
13pub mod routes;
14
15use std::sync::Arc;
16
17use koan_core::auth::{Claims, Role};
18use koan_core::db::pool::Pool;
19use koan_core::db::queries::auth as auth_queries;
20
21/// Authenticated user context injected into request extensions and GraphQL context.
22#[derive(Debug, Clone)]
23pub struct AuthUser {
24 pub user_id: i64,
25 pub username: String,
26 pub role: Role,
27}
28
29/// The account a token names, as it stands now.
30///
31/// A token's claims hold for its whole lifetime, so taken at their word a role
32/// change or a deletion would not reach GraphQL or the web UI until it
33/// expired: time enough for a demoted admin to restore the role. `None` once
34/// the account is gone, or when its id now belongs to another account.
35pub(crate) async fn current_user(pool: &Arc<Pool>, claims: Claims) -> Option<AuthUser> {
36 let pool = pool.clone();
37 tokio::task::spawn_blocking(move || {
38 let db = pool.get().ok()?;
39 let user = auth_queries::get_user_by_id(&db.conn, claims.sub).ok()??;
40 (user.username == claims.username).then_some(AuthUser {
41 user_id: user.id,
42 username: user.username,
43 role: user.role,
44 })
45 })
46 .await
47 .ok()
48 .flatten()
49}
50
51impl AuthUser {
52 /// Anonymous admin user for when auth is disabled.
53 pub fn anonymous_admin() -> Self {
54 Self {
55 user_id: 0,
56 username: koan_core::auth::ANONYMOUS.into(),
57 role: Role::Admin,
58 }
59 }
60}