Skip to main content

arc_web/helpers/
session.rs

1//! Cookie-session-backed identity for the server-rendered admin UI.
2//!
3//! Sessions hold a [`SessionUser`] — a lightweight projection-backed POD
4//! carrying the `aggregate_id` UUID, name, and email. Reads from
5//! `users_view` (Step 2 projection); never touches the retired Diesel
6//! `users` table.
7
8use actix_session::Session;
9use arc_core::read_model_store::ReadModelStore;
10use serde::{Deserialize, Serialize};
11
12/// Session key holding the cached [`SessionUser`].
13const SESSION_USER_KEY: &str = "user";
14
15/// Identity stored in the cookie session after sign-in.
16#[derive(Serialize, Deserialize, Clone, Debug)]
17pub struct SessionUser {
18    /// Aggregate UUID — same value used in JWT `sub` and audit `actor_id`.
19    pub id: String,
20    pub name: String,
21    pub email: String,
22}
23
24impl SessionUser {
25    /// Build from a `users_view` row, or `None` if the row is missing the
26    /// fields the cookie session needs.
27    pub fn from_row(row: &serde_json::Value) -> Option<Self> {
28        Some(Self {
29            id: row.get("id")?.as_str()?.to_string(),
30            name: row.get("name")?.as_str()?.to_string(),
31            email: row.get("email")?.as_str()?.to_string(),
32        })
33    }
34
35    /// Refresh from the projection (e.g. after profile update). `view` is the
36    /// read-model table the application's user projector maintains.
37    pub async fn from_projection(store: &dyn ReadModelStore, view: &str, id: &str) -> Option<Self> {
38        let row = store.get(view, id).await.ok().flatten()?;
39        Self::from_row(&row)
40    }
41}
42
43/// `true` when the cookie session carries a [`SessionUser`].
44///
45/// Trusts the signed/encrypted cookie store — no DB round-trip per request.
46pub fn is_authenticated(session: &Session) -> bool {
47    session
48        .get::<SessionUser>(SESSION_USER_KEY)
49        .ok()
50        .flatten()
51        .is_some()
52}
53
54/// Read the cached [`SessionUser`].
55pub fn get_session_user(session: &Session) -> Option<SessionUser> {
56    session.get::<SessionUser>(SESSION_USER_KEY).ok().flatten()
57}
58
59/// Cache identity in the session after a successful sign-in or profile change.
60pub fn set_session_user(session: &Session, user: &SessionUser) {
61    let _ = session.insert(SESSION_USER_KEY, user.clone());
62}
63
64/// Wipe identity (sign-out path).
65pub fn clear_session_user(session: &Session) {
66    session.remove(SESSION_USER_KEY);
67    // Pre-cutover keys — clear in case of session carry-over from old cookies.
68    session.remove("user_id");
69    session.remove("user_data");
70}
71
72/// Retrieves and clears the flash message from the session.
73/// Returns a tuple of (message_type, message_text) where type is "success" or "error".
74/// Set `is_json` to true for structured JSON messages (used by the sign-in page).
75pub fn get_session_message(session: &Session, is_json: bool) -> (String, String) {
76    if !is_json {
77        let simple_message = (
78            "success".to_string(),
79            session
80                .get::<String>("message")
81                .unwrap_or(Some("".to_string()))
82                .unwrap_or("".to_string()),
83        );
84
85        session.remove("message");
86
87        return simple_message;
88    }
89
90    let session_message = session
91        .get::<serde_json::Value>("message")
92        .unwrap_or(Some(serde_json::json!({})))
93        .unwrap_or(serde_json::json!({}));
94
95    if session_message.is_null() {
96        session.remove("message");
97
98        return ("success".to_string(), "".to_string());
99    }
100
101    if session_message["error"].is_string()
102        && !session_message["error"].as_str().unwrap().is_empty()
103    {
104        session.remove("message");
105
106        return (
107            "error".to_string(),
108            session_message["error"].as_str().unwrap().to_string(),
109        );
110    }
111
112    if session_message["success"].is_string()
113        && !session_message["success"].as_str().unwrap().is_empty()
114    {
115        session.remove("message");
116
117        return (
118            "success".to_string(),
119            session_message["success"].as_str().unwrap().to_string(),
120        );
121    }
122
123    session.remove("message");
124
125    ("success".to_string(), "".to_string())
126}