1pub mod middleware;
12pub mod password;
13pub mod routes;
14
15use std::sync::{Arc, OnceLock};
16
17use koan_core::auth::{Claims, Role};
18use koan_core::db::pool::Pool;
19use koan_core::db::queries::auth as auth_queries;
20
21pub(crate) type Keypair = (Arc<Vec<u8>>, Arc<Vec<u8>>);
23
24static SIGNING: OnceLock<Keypair> = OnceLock::new();
25
26pub(crate) fn set_signing_keys(private: Arc<Vec<u8>>, public: Arc<Vec<u8>>) {
30 let _ = SIGNING.set((private, public));
31}
32
33pub(crate) fn signing_keys() -> Result<&'static Keypair, koan_core::auth::AuthError> {
36 if let Some(keys) = SIGNING.get() {
37 return Ok(keys);
38 }
39 let (private, public) = koan_core::auth::load_or_generate_keypair()?;
40 Ok(SIGNING.get_or_init(|| (Arc::new(private), Arc::new(public))))
41}
42
43#[derive(Debug, Clone)]
45pub struct AuthUser {
46 pub user_id: i64,
47 pub username: String,
48 pub role: Role,
49}
50
51pub(crate) async fn current_user(pool: &Arc<Pool>, claims: Claims) -> Option<AuthUser> {
58 let pool = pool.clone();
59 tokio::task::spawn_blocking(move || {
60 let db = pool.get().ok()?;
61 let user = auth_queries::get_user_by_id(&db.conn, claims.sub).ok()??;
62 (user.username == claims.username).then_some(AuthUser {
63 user_id: user.id,
64 username: user.username,
65 role: user.role,
66 })
67 })
68 .await
69 .ok()
70 .flatten()
71}
72
73pub(crate) async fn key_user(pool: &Arc<Pool>, key: String) -> Option<AuthUser> {
76 let pool = pool.clone();
77 tokio::task::spawn_blocking(move || {
78 let db = pool.get().ok()?;
79 let user =
80 koan_core::db::queries::api_keys::authenticate_api_key(&db.conn, &key).ok()??;
81 Some(AuthUser {
82 user_id: user.id,
83 username: user.username,
84 role: user.role,
85 })
86 })
87 .await
88 .ok()
89 .flatten()
90}
91
92#[derive(Debug, Clone, Copy)]
96pub(crate) struct Lease {
97 pub user_id: i64,
98 pub mark: u64,
99 pub expires: Option<u64>,
101}
102
103impl Lease {
104 pub(crate) async fn ended(self) {
108 let lapsed = async {
109 match self.expires {
110 Some(at) => {
111 let left = at.saturating_sub(koan_core::auth::now_unix());
112 tokio::time::sleep(std::time::Duration::from_secs(left)).await;
113 }
114 None => std::future::pending().await,
115 }
116 };
117 tokio::select! {
118 _ = koan_core::auth::account_changed_since(self.user_id, self.mark) => {}
119 _ = lapsed => {}
120 }
121 }
122}
123
124impl AuthUser {
125 pub fn anonymous_admin() -> Self {
127 Self {
128 user_id: 0,
129 username: koan_core::auth::ANONYMOUS.into(),
130 role: Role::Admin,
131 }
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use std::time::Duration;
139
140 #[tokio::test]
141 async fn a_lease_ends_when_its_account_changes_and_no_other() {
142 let lease = Lease {
143 user_id: 9001,
144 mark: koan_core::auth::account_mark(),
145 expires: None,
146 };
147 koan_core::auth::account_changed(9002);
148 assert!(
149 tokio::time::timeout(Duration::from_millis(50), lease.ended())
150 .await
151 .is_err()
152 );
153 koan_core::auth::account_changed(9001);
154 tokio::time::timeout(Duration::from_secs(1), lease.ended())
155 .await
156 .unwrap();
157 }
158
159 #[tokio::test]
160 async fn a_lease_ends_when_its_token_lapses() {
161 let lease = Lease {
162 user_id: 9003,
163 mark: koan_core::auth::account_mark(),
164 expires: Some(koan_core::auth::now_unix()),
165 };
166 tokio::time::timeout(Duration::from_secs(1), lease.ended())
167 .await
168 .unwrap();
169 }
170}