use std::{ops::Deref, sync::Arc}; use serde::{Deserialize, Serialize}; /// Cheap-to-clone handle to per-session user data. #[derive(Debug, Clone)] pub struct UserState(Arc); impl Deref for UserState { type Target = UserStateInner; fn deref(&self) -> &Self::Target { &self.0 } } impl From for UserState { fn from(value: UserStateInner) -> Self { Self(Arc::new(value)) } } /// Per-session user data stored in the tower-sessions session store. /// /// Persisted across requests for the lifetime of the session. #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct UserStateInner { /// Subject identifier from Keycloak (unique user ID). pub sub: String, /// OAuth2 access token. pub access_token: String, /// OAuth2 refresh token. pub refresh_token: String, /// Basic user profile. pub user: User, } /// Basic user profile stored alongside the session. #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct User { /// Email address. pub email: String, /// Display name (preferred_username or full name from Keycloak). pub name: String, /// Avatar / profile picture URL. pub avatar_url: String, } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; #[test] fn user_state_inner_default_has_empty_strings() { let inner = UserStateInner::default(); assert_eq!(inner.sub, ""); assert_eq!(inner.access_token, ""); assert_eq!(inner.refresh_token, ""); assert_eq!(inner.user.email, ""); assert_eq!(inner.user.name, ""); assert_eq!(inner.user.avatar_url, ""); } #[test] fn user_default_has_empty_strings() { let user = User::default(); assert_eq!(user.email, ""); assert_eq!(user.name, ""); assert_eq!(user.avatar_url, ""); } #[test] fn user_state_inner_serde_round_trip() { let inner = UserStateInner { sub: "user-123".into(), access_token: "tok-abc".into(), refresh_token: "ref-xyz".into(), user: User { email: "a@b.com".into(), name: "Alice".into(), avatar_url: "https://img.example.com/a.png".into(), }, }; let json = serde_json::to_string(&inner).expect("serialize UserStateInner"); let back: UserStateInner = serde_json::from_str(&json).expect("deserialize UserStateInner"); assert_eq!(inner.sub, back.sub); assert_eq!(inner.access_token, back.access_token); assert_eq!(inner.refresh_token, back.refresh_token); assert_eq!(inner.user.email, back.user.email); assert_eq!(inner.user.name, back.user.name); assert_eq!(inner.user.avatar_url, back.user.avatar_url); } #[test] fn user_state_from_inner_and_deref() { let inner = UserStateInner { sub: "sub-1".into(), access_token: "at".into(), refresh_token: "rt".into(), user: User { email: "e@e.com".into(), name: "Eve".into(), avatar_url: "".into(), }, }; let state = UserState::from(inner); // Deref should give access to inner fields assert_eq!(state.sub, "sub-1"); assert_eq!(state.user.name, "Eve"); } #[test] fn user_serde_round_trip() { let user = User { email: "bob@test.com".into(), name: "Bob".into(), avatar_url: "https://avatars.io/bob".into(), }; let json = serde_json::to_string(&user).expect("serialize User"); let back: User = serde_json::from_str(&json).expect("deserialize User"); assert_eq!(user.email, back.email); assert_eq!(user.name, back.name); assert_eq!(user.avatar_url, back.avatar_url); } #[test] fn user_state_clone_is_cheap() { let inner = UserStateInner::default(); let state = UserState::from(inner); let cloned = state.clone(); // Both point to the same Arc allocation assert_eq!(state.sub, cloned.sub); } }