1use crate::Authenticatable;
6use larust_core::AppError;
7use larust_http::session::Session;
8
9const SESSION_KEY: &str = "_auth_user_id";
10
11fn session_key(guard: &str) -> String {
18 if guard == "web" {
19 SESSION_KEY.to_string()
20 } else {
21 format!("_auth_{guard}_user_id")
22 }
23}
24
25pub async fn login<U: Authenticatable>(session: &Session, user: &U) -> Result<(), AppError> {
34 session
35 .cycle_id()
36 .await
37 .map_err(|source| AppError::Internal(Box::new(source)))?;
38 session
39 .insert(&session_key(U::GUARD), user.auth_id())
40 .await
41 .map_err(|source| AppError::Internal(Box::new(source)))
42}
43
44pub async fn logout(session: &Session) -> Result<(), AppError> {
52 session
53 .flush()
54 .await
55 .map_err(|source| AppError::Internal(Box::new(source)))
56}
57
58pub async fn logout_for<U: Authenticatable>(session: &Session) -> Result<(), AppError> {
62 session
63 .remove::<i64>(&session_key(U::GUARD))
64 .await
65 .map(|_| ())
66 .map_err(|source| AppError::Internal(Box::new(source)))
67}
68
69pub async fn id(session: &Session) -> Result<Option<i64>, AppError> {
73 session
74 .get::<i64>(SESSION_KEY)
75 .await
76 .map_err(|source| AppError::Internal(Box::new(source)))
77}
78
79pub async fn id_for<U: Authenticatable>(session: &Session) -> Result<Option<i64>, AppError> {
81 session
82 .get::<i64>(&session_key(U::GUARD))
83 .await
84 .map_err(|source| AppError::Internal(Box::new(source)))
85}
86
87pub async fn check(session: &Session) -> Result<bool, AppError> {
91 Ok(id(session).await?.is_some())
92}
93
94pub async fn check_for<U: Authenticatable>(session: &Session) -> Result<bool, AppError> {
97 Ok(id_for::<U>(session).await?.is_some())
98}
99
100pub async fn user<U: Authenticatable>(session: &Session) -> Result<Option<U>, AppError> {
107 match id_for::<U>(session).await? {
108 Some(user_id) => U::find_for_auth(user_id).await,
109 None => Ok(None),
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use std::sync::Arc;
117 use tower_sessions::MemoryStore;
123
124 #[derive(Clone)]
125 struct TestUser {
126 id: i64,
127 }
128
129 impl Authenticatable for TestUser {
130 fn auth_id(&self) -> i64 {
131 self.id
132 }
133
134 async fn find_for_auth(id: i64) -> Result<Option<Self>, AppError> {
135 Ok((id == 1).then_some(TestUser { id }))
138 }
139 }
140
141 #[derive(Clone)]
142 struct AdminUser {
143 id: i64,
144 }
145
146 impl Authenticatable for AdminUser {
147 const GUARD: &'static str = "admin";
148
149 fn auth_id(&self) -> i64 {
150 self.id
151 }
152
153 async fn find_for_auth(id: i64) -> Result<Option<Self>, AppError> {
154 Ok((id == 1).then_some(AdminUser { id }))
155 }
156 }
157
158 fn new_session() -> Session {
159 Session::new(None, Arc::new(MemoryStore::default()), None)
160 }
161
162 #[tokio::test]
163 async fn login_sets_id_and_check_reflects_it() {
164 let session = new_session();
165 assert!(!check(&session).await.unwrap());
166
167 login(&session, &TestUser { id: 1 }).await.unwrap();
168 assert_eq!(id(&session).await.unwrap(), Some(1));
169 assert!(check(&session).await.unwrap());
170 }
171
172 #[tokio::test]
173 async fn logout_clears_the_whole_session() {
174 let session = new_session();
175 login(&session, &TestUser { id: 1 }).await.unwrap();
176
177 logout(&session).await.unwrap();
178
179 assert_eq!(id(&session).await.unwrap(), None);
180 assert!(!check(&session).await.unwrap());
181 }
182
183 #[tokio::test]
184 async fn user_resolves_the_logged_in_user() {
185 let session = new_session();
186 login(&session, &TestUser { id: 1 }).await.unwrap();
187
188 let resolved = user::<TestUser>(&session).await.unwrap();
189 assert_eq!(resolved.map(|u| u.id), Some(1));
190 }
191
192 #[tokio::test]
193 async fn user_returns_none_when_logged_out() {
194 let session = new_session();
195 assert_eq!(
196 user::<TestUser>(&session).await.unwrap().map(|u| u.id),
197 None
198 );
199 }
200
201 #[tokio::test]
202 async fn user_returns_none_for_an_id_that_no_longer_resolves() {
203 let session = new_session();
207 login(&session, &TestUser { id: 999 }).await.unwrap();
208
209 let resolved = user::<TestUser>(&session).await.unwrap();
210 assert!(resolved.is_none());
211 }
212
213 #[tokio::test]
214 async fn two_guards_stay_logged_in_independently_in_the_same_session() {
215 let session = new_session();
216
217 login(&session, &TestUser { id: 1 }).await.unwrap();
218 login(&session, &AdminUser { id: 1 }).await.unwrap();
219
220 assert!(check(&session).await.unwrap());
221 assert!(check_for::<AdminUser>(&session).await.unwrap());
222 assert_eq!(
223 user::<TestUser>(&session).await.unwrap().map(|u| u.id),
224 Some(1)
225 );
226 assert_eq!(
227 user::<AdminUser>(&session).await.unwrap().map(|u| u.id),
228 Some(1)
229 );
230 }
231
232 #[tokio::test]
233 async fn logout_for_only_clears_its_own_guard() {
234 let session = new_session();
235 login(&session, &TestUser { id: 1 }).await.unwrap();
236 login(&session, &AdminUser { id: 1 }).await.unwrap();
237
238 logout_for::<AdminUser>(&session).await.unwrap();
239
240 assert!(!check_for::<AdminUser>(&session).await.unwrap());
241 assert!(check(&session).await.unwrap());
243 }
244
245 #[tokio::test]
246 async fn a_guard_that_never_logged_in_reads_as_logged_out() {
247 let session = new_session();
248 login(&session, &TestUser { id: 1 }).await.unwrap();
249
250 assert!(!check_for::<AdminUser>(&session).await.unwrap());
251 assert_eq!(id_for::<AdminUser>(&session).await.unwrap(), None);
252 }
253}