1use crate::config::AuthConfig;
4use crate::identity::AuthIdentity;
5use crate::strategy::AuthStrategy;
6use async_trait::async_trait;
7use doido_controller::session::{EncryptedCookieSessionStore, Session};
8use doido_core::Result;
9use doido_model::sea_orm::DatabaseConnection;
10use http::header;
11use http::request::Parts;
12
13pub const USER_ID_KEY: &str = "user_id";
15
16pub const SESSION_COOKIE: &str = "_doido_session";
18
19pub struct SessionStrategy {
21 store: EncryptedCookieSessionStore,
22}
23
24impl SessionStrategy {
25 pub fn new(secret: impl Into<Vec<u8>>) -> Self {
26 Self {
27 store: EncryptedCookieSessionStore::new(secret),
28 }
29 }
30
31 pub fn default_dev() -> Self {
32 Self {
33 store: EncryptedCookieSessionStore::default(),
34 }
35 }
36
37 fn session_from_parts(&self, parts: &Parts) -> Option<Session> {
38 let header = parts.headers.get(header::COOKIE)?.to_str().ok()?;
39 let raw = header
40 .split(';')
41 .filter_map(|pair| pair.trim().split_once('='))
42 .find(|(k, _)| *k == SESSION_COOKIE)
43 .map(|(_, v)| v.to_string())?;
44 self.store.decode(&raw)
45 }
46}
47
48#[async_trait]
49impl AuthStrategy for SessionStrategy {
50 fn name(&self) -> &str {
51 "cookie"
52 }
53
54 async fn authenticate(
55 &self,
56 parts: &Parts,
57 _db: &DatabaseConnection,
58 ) -> Result<Option<AuthIdentity>> {
59 let session = match self.session_from_parts(parts) {
60 Some(s) => s,
61 None => return Ok(None),
62 };
63 let user_id = match session.data.get(USER_ID_KEY) {
64 Some(value) => value.clone(),
65 None => return Ok(None),
66 };
67 if user_id.is_null() {
68 return Ok(None);
69 }
70 Ok(Some(AuthIdentity { user_id }))
71 }
72}
73
74pub fn sign_in_session(session: &mut Session, user_id: impl serde::Serialize) {
76 session.set(USER_ID_KEY, user_id);
77}
78
79pub fn sign_out_session(session: &mut Session) {
81 if session.data.is_object() {
82 if let serde_json::Value::Object(map) = &mut session.data {
83 map.remove(USER_ID_KEY);
84 }
85 }
86}
87
88pub fn encode_session_cookie(store: &EncryptedCookieSessionStore, session: &Session) -> String {
90 let value = store.encode(session);
91 format!("{SESSION_COOKIE}={value}; Path=/; HttpOnly; SameSite=Lax")
92}
93
94pub fn clear_session_cookie() -> String {
96 format!("{SESSION_COOKIE}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax")
97}
98
99pub fn from_config(_config: &AuthConfig) -> SessionStrategy {
101 SessionStrategy::new(doido_controller::secret::key_base())
102}