anvilsign in

collin/anvil

1//! Web login sessions: opaque random tokens stored server-side and referenced
2//! by a cookie.
3
4use argon2::password_hash::rand_core::{OsRng, RngCore};
5
6use crate::error::Result;
7use crate::models::{Session, User};
8
9/// Default session lifetime: 30 days.
10const SESSION_TTL_SECS: i64 = 60 * 60 * 24 * 30;
11
12/// Generate a 256-bit random session token, hex-encoded.
13fn generate_token() -> String {
14 let mut bytes = [0u8; 32];
15 let mut rng = OsRng;
16 rng.fill_bytes(&mut bytes);
17 bytes.iter().map(|b| format!("{b:02x}")).collect()
18}
19
20/// Create a new session for `user_id`, returning it (the `token` goes in a cookie).
21pub async fn create(db: &toasty::Db, user_id: i64) -> Result<Session> {
22 let mut conn = db.clone();
23 let now = crate::now();
24 let session = toasty::create!(Session {
25 token: generate_token(),
26 user_id: user_id,
27 created_at: now,
28 expires_at: now + SESSION_TTL_SECS,
29 })
30 .exec(&mut conn)
31 .await?;
32 Ok(session)
33}
34
35/// Resolve a session token to its user, or `None` if missing/expired.
36/// Expired sessions are deleted as a side effect.
37pub async fn lookup_user(db: &toasty::Db, token: &str) -> Result<Option<User>> {
38 let mut conn = db.clone();
39 let Some(session) = Session::filter(Session::fields().token().eq(token))
40 .first()
41 .exec(&mut conn)
42 .await?
43 else {
44 return Ok(None);
45 };
46
47 if session.expires_at < crate::now() {
48 let mut conn = db.clone();
49 let _ = session.delete().exec(&mut conn).await;
50 return Ok(None);
51 }
52
53 let mut conn = db.clone();
54 let user = User::filter(User::fields().id().eq(session.user_id))
55 .first()
56 .exec(&mut conn)
57 .await?;
58 Ok(user)
59}
60
61/// Delete a session (logout). No-op if the token is unknown.
62pub async fn delete(db: &toasty::Db, token: &str) -> Result<()> {
63 let mut conn = db.clone();
64 if let Some(session) = Session::filter(Session::fields().token().eq(token))
65 .first()
66 .exec(&mut conn)
67 .await?
68 {
69 let mut conn = db.clone();
70 session.delete().exec(&mut conn).await?;
71 }
72 Ok(())
73}