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