1use crate::reactive::Signal;
31use std::cell::RefCell;
32use std::collections::HashSet;
33
34fn deserialize_user_id<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
40where
41 D: serde::Deserializer<'de>,
42{
43 use serde::Deserialize;
44 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
45 Ok(value.and_then(|v| match v {
46 serde_json::Value::String(s) if s.is_empty() => None,
47 serde_json::Value::String(s) => Some(s),
48 serde_json::Value::Number(n) => Some(n.to_string()),
49 serde_json::Value::Null => None,
50 _ => None,
51 }))
52}
53
54pub const SESSION_KEY_USER_ID: &str = "_auth_user_id";
56
57pub const SESSION_KEY_USERNAME: &str = "_auth_username";
59
60pub const SESSION_COOKIE_NAME: &str = "sessionid";
62
63thread_local! {
64 static AUTH_STATE: RefCell<Option<AuthState>> = const { RefCell::new(None) };
66}
67
68pub fn auth_state() -> AuthState {
73 AUTH_STATE.with(|state| {
74 let mut state = state.borrow_mut();
75 if state.is_none() {
76 *state = Some(AuthState::new());
77 }
78 state.clone().unwrap()
79 })
80}
81
82#[derive(Debug, Clone)]
88pub struct AuthState {
89 is_authenticated: Signal<bool>,
91 user_id: Signal<Option<String>>,
93 username: Signal<Option<String>>,
95 email: Signal<Option<String>>,
97 is_staff: Signal<bool>,
99 is_superuser: Signal<bool>,
101 permissions: Signal<HashSet<String>>,
103}
104
105impl Default for AuthState {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111impl AuthState {
112 pub fn new() -> Self {
114 Self {
115 is_authenticated: Signal::new(false),
116 user_id: Signal::new(None),
117 username: Signal::new(None),
118 email: Signal::new(None),
119 is_staff: Signal::new(false),
120 is_superuser: Signal::new(false),
121 permissions: Signal::new(HashSet::new()),
122 }
123 }
124
125 pub fn from_server_data(data: AuthData) -> Self {
130 Self {
131 is_authenticated: Signal::new(data.is_authenticated),
132 user_id: Signal::new(data.user_id),
133 username: Signal::new(data.username),
134 email: Signal::new(data.email),
135 is_staff: Signal::new(data.is_staff),
136 is_superuser: Signal::new(data.is_superuser),
137 permissions: Signal::new(data.permissions.into_iter().collect()),
138 }
139 }
140
141 pub fn is_authenticated(&self) -> bool {
143 self.is_authenticated.get()
144 }
145
146 pub fn user_id(&self) -> Option<String> {
148 self.user_id.get()
149 }
150
151 pub fn username(&self) -> Option<String> {
153 self.username.get()
154 }
155
156 pub fn email(&self) -> Option<String> {
158 self.email.get()
159 }
160
161 pub fn is_staff(&self) -> bool {
163 self.is_staff.get()
164 }
165
166 pub fn is_superuser(&self) -> bool {
168 self.is_superuser.get()
169 }
170
171 pub fn is_authenticated_signal(&self) -> Signal<bool> {
175 self.is_authenticated.clone()
176 }
177
178 pub fn user_id_signal(&self) -> Signal<Option<String>> {
180 self.user_id.clone()
181 }
182
183 pub fn username_signal(&self) -> Signal<Option<String>> {
185 self.username.clone()
186 }
187
188 pub fn email_signal(&self) -> Signal<Option<String>> {
190 self.email.clone()
191 }
192
193 pub fn is_staff_signal(&self) -> Signal<bool> {
195 self.is_staff.clone()
196 }
197
198 pub fn is_superuser_signal(&self) -> Signal<bool> {
200 self.is_superuser.clone()
201 }
202
203 pub fn update(&self, data: AuthData) {
208 self.is_authenticated.set(data.is_authenticated);
209 self.user_id.set(data.user_id);
210 self.username.set(data.username);
211 self.email.set(data.email);
212 self.is_staff.set(data.is_staff);
213 self.is_superuser.set(data.is_superuser);
214 self.permissions.set(data.permissions.into_iter().collect());
215 }
216
217 pub fn login(&self, user_id: impl Into<String>, username: impl Into<String>) {
222 self.is_authenticated.set(true);
223 self.user_id.set(Some(user_id.into()));
224 self.username.set(Some(username.into()));
225 self.email.set(None);
226 self.is_staff.set(false);
227 self.is_superuser.set(false);
228 self.permissions.set(HashSet::new());
229 }
230
231 pub fn login_full(
233 &self,
234 user_id: impl Into<String>,
235 username: impl Into<String>,
236 email: Option<String>,
237 is_staff: bool,
238 is_superuser: bool,
239 ) {
240 self.is_authenticated.set(true);
241 self.user_id.set(Some(user_id.into()));
242 self.username.set(Some(username.into()));
243 self.email.set(email);
244 self.is_staff.set(is_staff);
245 self.is_superuser.set(is_superuser);
246 }
247
248 pub fn logout(&self) {
250 self.is_authenticated.set(false);
251 self.user_id.set(None);
252 self.username.set(None);
253 self.email.set(None);
254 self.is_staff.set(false);
255 self.is_superuser.set(false);
256 self.permissions.set(HashSet::new());
257 }
258
259 pub fn has_permission(&self, permission: &str) -> bool {
264 if self.is_superuser() {
266 return true;
267 }
268
269 self.permissions.get().contains(permission)
271 }
272
273 pub fn has_any_permission(&self, permissions: &[&str]) -> bool {
278 if self.is_superuser() {
279 return true;
280 }
281 let cached = self.permissions.get();
282 permissions.iter().any(|p| cached.contains(*p))
283 }
284
285 pub fn has_all_permissions(&self, permissions: &[&str]) -> bool {
290 if self.is_superuser() {
291 return true;
292 }
293 let cached = self.permissions.get();
294 permissions.iter().all(|p| cached.contains(*p))
295 }
296
297 pub fn set_permissions(&self, permissions: HashSet<String>) {
299 self.permissions.set(permissions);
300 }
301
302 pub fn permissions_signal(&self) -> Signal<HashSet<String>> {
304 self.permissions.clone()
305 }
306
307 #[cfg(wasm)]
311 pub async fn fetch_permissions(&self, endpoint: Option<&str>) -> Result<(), AuthError> {
312 use crate::csrf::csrf_headers;
313 use crate::fetch;
314
315 let endpoint = endpoint.unwrap_or("/api/auth/permissions");
316 let mut headers = Vec::new();
317 if let Some((header_name, header_value)) = csrf_headers() {
318 headers.push((header_name.to_string(), header_value));
319 }
320
321 let response = fetch::request("GET", endpoint, None, headers)
322 .await
323 .map_err(|e| AuthError::Network(e.to_string()))?;
324
325 if !response.is_success() {
326 return Err(AuthError::Server {
327 status: response.status(),
328 message: response.into_text(),
329 });
330 }
331
332 let permissions: Vec<String> = response
333 .json()
334 .map_err(|e| AuthError::Parse(e.to_string()))?;
335
336 self.permissions.set(permissions.into_iter().collect());
337 Ok(())
338 }
339
340 #[cfg(native)]
342 pub async fn fetch_permissions(&self, _endpoint: Option<&str>) -> Result<(), AuthError> {
343 Ok(())
344 }
345
346 #[cfg(wasm)]
351 pub fn init_from_page(&self) {
352 use web_sys::window;
353
354 let Some(window) = window() else { return };
355 let Some(document) = window.document() else {
356 return;
357 };
358
359 let Ok(Some(element)) = document.query_selector("#auth-data") else {
361 return;
362 };
363
364 let Some(json_str) = element.text_content() else {
365 return;
366 };
367
368 if let Ok(data) = serde_json::from_str::<AuthData>(&json_str) {
369 self.update(data);
370 }
371 }
372
373 #[cfg(native)]
375 pub fn init_from_page(&self) {
376 }
378
379 #[cfg(wasm)]
384 pub async fn fetch_from_server(&self, endpoint: &str) -> Result<(), AuthError> {
385 use crate::csrf::csrf_headers;
386 use crate::fetch;
387
388 let mut headers = Vec::new();
389 if let Some((header_name, header_value)) = csrf_headers() {
390 headers.push((header_name.to_string(), header_value));
391 }
392
393 let response = fetch::request("GET", endpoint, None, headers)
394 .await
395 .map_err(|e| AuthError::Network(e.to_string()))?;
396
397 if !response.is_success() {
398 return Err(AuthError::Server {
399 status: response.status(),
400 message: response.into_text(),
401 });
402 }
403
404 let data: AuthData = response
405 .json()
406 .map_err(|e| AuthError::Parse(e.to_string()))?;
407
408 self.update(data);
409 Ok(())
410 }
411
412 #[cfg(native)]
414 pub async fn fetch_from_server(&self, _endpoint: &str) -> Result<(), AuthError> {
415 Ok(())
416 }
417}
418
419#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
426pub struct AuthData {
427 pub is_authenticated: bool,
429 #[serde(default, deserialize_with = "deserialize_user_id")]
431 pub user_id: Option<String>,
432 #[serde(default)]
434 pub username: Option<String>,
435 #[serde(default)]
437 pub email: Option<String>,
438 #[serde(default)]
440 pub is_staff: bool,
441 #[serde(default)]
443 pub is_superuser: bool,
444 #[serde(default)]
446 pub permissions: Vec<String>,
447}
448
449impl AuthData {
450 pub fn anonymous() -> Self {
452 Self::default()
453 }
454
455 pub fn authenticated(user_id: impl Into<String>, username: impl Into<String>) -> Self {
457 Self {
458 is_authenticated: true,
459 user_id: Some(user_id.into()),
460 username: Some(username.into()),
461 ..Default::default()
462 }
463 }
464
465 pub fn full(
467 user_id: impl Into<String>,
468 username: impl Into<String>,
469 email: Option<String>,
470 is_staff: bool,
471 is_superuser: bool,
472 ) -> Self {
473 Self {
474 is_authenticated: true,
475 user_id: Some(user_id.into()),
476 username: Some(username.into()),
477 email,
478 is_staff,
479 is_superuser,
480 permissions: Vec::new(),
481 }
482 }
483}
484
485#[derive(Debug, Clone)]
487pub enum AuthError {
488 Network(String),
490 Server {
492 status: u16,
494 message: String,
496 },
497 Parse(String),
499}
500
501impl std::fmt::Display for AuthError {
502 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
503 match self {
504 AuthError::Network(msg) => write!(f, "Network error: {}", msg),
505 AuthError::Server { status, message } => {
506 write!(f, "Server error ({}): {}", status, message)
507 }
508 AuthError::Parse(msg) => write!(f, "Parse error: {}", msg),
509 }
510 }
511}
512
513impl std::error::Error for AuthError {}
514
515pub const AUTH_HEADER_NAME: &str = "Authorization";
521
522pub const JWT_STORAGE_KEY: &str = "__admin_jwt";
528
529#[cfg(wasm)]
545pub fn auth_headers() -> Option<(&'static str, String)> {
546 get_jwt_token().map(|token| (AUTH_HEADER_NAME, format!("Bearer {}", token)))
547}
548
549#[cfg(native)]
551pub fn auth_headers() -> Option<(&'static str, String)> {
552 None
553}
554
555#[cfg(wasm)]
559pub fn get_jwt_token() -> Option<String> {
560 let window = web_sys::window()?;
561 let storage = window.session_storage().ok()??;
562 storage.get_item(JWT_STORAGE_KEY).ok()?
563}
564
565#[cfg(native)]
567pub fn get_jwt_token() -> Option<String> {
568 None
569}
570
571#[cfg(wasm)]
575pub fn set_jwt_token(token: &str) {
576 if let Some(window) = web_sys::window()
577 && let Ok(Some(storage)) = window.session_storage()
578 {
579 let _ = storage.set_item(JWT_STORAGE_KEY, token);
580 }
581}
582
583#[cfg(native)]
585pub fn set_jwt_token(_token: &str) {
586 }
588
589#[cfg(wasm)]
593pub fn clear_jwt_token() {
594 if let Some(window) = web_sys::window()
595 && let Ok(Some(storage)) = window.session_storage()
596 {
597 let _ = storage.remove_item(JWT_STORAGE_KEY);
598 }
599}
600
601#[cfg(native)]
603pub fn clear_jwt_token() {
604 }
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610
611 #[test]
612 fn test_auth_state_creation() {
613 let state = AuthState::new();
614 assert!(!state.is_authenticated());
615 assert!(state.user_id().is_none());
616 assert!(state.username().is_none());
617 }
618
619 #[test]
620 fn test_auth_state_login() {
621 let state = AuthState::new();
622 state.login("42", "testuser");
623
624 assert!(state.is_authenticated());
625 assert_eq!(state.user_id(), Some("42".to_string()));
626 assert_eq!(state.username(), Some("testuser".to_string()));
627 }
628
629 #[test]
630 fn test_auth_state_logout() {
631 let state = AuthState::new();
632 state.login("42", "testuser");
633 state.logout();
634
635 assert!(!state.is_authenticated());
636 assert!(state.user_id().is_none());
637 assert!(state.username().is_none());
638 }
639
640 #[test]
641 fn test_auth_state_from_server_data() {
642 let data = AuthData::full(
643 "1",
644 "admin",
645 Some("admin@example.com".to_string()),
646 true,
647 true,
648 );
649 let state = AuthState::from_server_data(data);
650
651 assert!(state.is_authenticated());
652 assert_eq!(state.user_id(), Some("1".to_string()));
653 assert_eq!(state.username(), Some("admin".to_string()));
654 assert_eq!(state.email(), Some("admin@example.com".to_string()));
655 assert!(state.is_staff());
656 assert!(state.is_superuser());
657 }
658
659 #[test]
660 fn test_auth_data_anonymous() {
661 let data = AuthData::anonymous();
662 assert!(!data.is_authenticated);
663 assert!(data.user_id.is_none());
664 }
665
666 #[test]
667 fn test_auth_data_authenticated() {
668 let data = AuthData::authenticated("1", "user");
669 assert!(data.is_authenticated);
670 assert_eq!(data.user_id, Some("1".to_string()));
671 assert_eq!(data.username, Some("user".to_string()));
672 }
673
674 #[test]
675 fn test_auth_state_update() {
676 let state = AuthState::new();
677 let data = AuthData::authenticated("99", "updated");
678 state.update(data);
679
680 assert!(state.is_authenticated());
681 assert_eq!(state.user_id(), Some("99".to_string()));
682 assert_eq!(state.username(), Some("updated".to_string()));
683 }
684
685 #[test]
686 fn test_global_auth_state() {
687 let state1 = auth_state();
688 let state2 = auth_state();
689
690 state1.login("1", "test");
691 assert!(state2.is_authenticated());
692 }
693
694 #[test]
695 fn test_auth_error_display() {
696 let network_err = AuthError::Network("timeout".to_string());
697 assert_eq!(network_err.to_string(), "Network error: timeout");
698
699 let server_err = AuthError::Server {
700 status: 401,
701 message: "Unauthorized".to_string(),
702 };
703 assert_eq!(server_err.to_string(), "Server error (401): Unauthorized");
704
705 let parse_err = AuthError::Parse("invalid json".to_string());
706 assert_eq!(parse_err.to_string(), "Parse error: invalid json");
707 }
708
709 #[test]
710 fn test_has_permission_with_cache() {
711 let state = AuthState::new();
712 let mut perms = HashSet::new();
713 perms.insert("blog.add_post".to_string());
714 perms.insert("blog.edit_post".to_string());
715 state.set_permissions(perms);
716
717 assert!(state.has_permission("blog.add_post"));
718 assert!(!state.has_permission("blog.delete_post"));
719 }
720
721 #[test]
722 fn test_superuser_has_all_permissions() {
723 let state = AuthState::new();
724 state.login_full("1", "admin", None, true, true);
725
726 assert!(state.has_permission("any.permission"));
727 assert!(state.has_permission("another.permission"));
728 }
729
730 #[test]
731 fn test_has_any_permission() {
732 let state = AuthState::new();
733 let mut perms = HashSet::new();
734 perms.insert("blog.view".to_string());
735 state.set_permissions(perms);
736
737 assert!(state.has_any_permission(&["blog.view", "blog.edit"]));
738 assert!(!state.has_any_permission(&["blog.delete", "blog.edit"]));
739 }
740
741 #[test]
742 fn test_has_all_permissions() {
743 let state = AuthState::new();
744 let mut perms = HashSet::new();
745 perms.insert("blog.view".to_string());
746 perms.insert("blog.edit".to_string());
747 state.set_permissions(perms);
748
749 assert!(state.has_all_permissions(&["blog.view", "blog.edit"]));
750 assert!(!state.has_all_permissions(&["blog.view", "blog.delete"]));
751 }
752
753 #[test]
754 fn test_permissions_cleared_on_logout() {
755 let state = AuthState::new();
756 let mut perms = HashSet::new();
757 perms.insert("blog.add_post".to_string());
758 state.set_permissions(perms);
759 state.login("1", "user");
760
761 state.logout();
762
763 assert!(!state.has_permission("blog.add_post"));
764 assert_eq!(state.permissions.get().len(), 0);
765 }
766
767 #[test]
768 fn test_permissions_from_auth_data() {
769 let data = AuthData {
770 is_authenticated: true,
771 user_id: Some("1".to_string()),
772 username: Some("user".to_string()),
773 email: None,
774 is_staff: false,
775 is_superuser: false,
776 permissions: vec!["blog.view".to_string(), "blog.edit".to_string()],
777 };
778 let state = AuthState::from_server_data(data);
779
780 assert!(state.has_permission("blog.view"));
781 assert!(state.has_permission("blog.edit"));
782 assert!(!state.has_permission("blog.delete"));
783 }
784
785 #[test]
786 fn test_permissions_update() {
787 let state = AuthState::new();
788 state.login("1", "user");
789
790 let data = AuthData {
791 is_authenticated: true,
792 user_id: Some("1".to_string()),
793 username: Some("user".to_string()),
794 email: None,
795 is_staff: false,
796 is_superuser: false,
797 permissions: vec!["blog.view".to_string()],
798 };
799 state.update(data);
800
801 assert!(state.has_permission("blog.view"));
802 assert!(!state.has_permission("blog.edit"));
803 }
804
805 #[test]
806 fn test_auth_headers_non_wasm() {
807 assert!(auth_headers().is_none());
809 }
810
811 #[test]
812 fn test_get_jwt_token_non_wasm() {
813 assert!(get_jwt_token().is_none());
815 }
816
817 #[test]
818 fn test_set_jwt_token_non_wasm() {
819 set_jwt_token("test-token");
821 }
822
823 #[test]
824 fn test_clear_jwt_token_non_wasm() {
825 clear_jwt_token();
827 }
828
829 #[test]
830 fn test_jwt_constants() {
831 assert_eq!(AUTH_HEADER_NAME, "Authorization");
832 assert_eq!(JWT_STORAGE_KEY, "__admin_jwt");
833 }
834}