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