1use super::{AuthState, AuthStatus};
4use crate::ssr::{InitialAuthSnapshot, InitialAuthStatus};
5use serde::{Deserialize, Serialize};
6use std::borrow::Cow;
7
8#[doc(hidden)]
10#[derive(Debug, Clone, PartialEq)]
11pub(crate) struct AuthRuntimeState {
12 status: AuthRuntimeStateKind,
13 is_loaded: bool,
14}
15
16#[derive(Debug, Clone, PartialEq)]
17#[cfg_attr(not(clerk_client), allow(dead_code))]
18enum AuthRuntimeStateKind {
19 Loading,
21 SignedOut,
23 SignedInSnapshot {
26 user_id: String,
28 session_id: Option<String>,
30 org_id: Option<String>,
32 org_slug: Option<String>,
34 org_role: Option<String>,
36 org_permissions: Vec<String>,
38 },
39 SignedIn {
41 user: User,
43 session: Session,
45 org_id: Option<String>,
47 org_slug: Option<String>,
49 org_role: Option<String>,
51 org_permissions: Vec<String>,
53 },
54 Pending {
59 user: User,
61 session: Session,
63 },
64}
65
66#[allow(clippy::large_enum_variant)]
71#[derive(Debug, Clone, PartialEq)]
72#[cfg_attr(not(clerk_client), allow(dead_code))]
73pub(crate) enum AuthObservation {
74 Loading,
76 SignedOut,
78 SignedIn {
80 user: User,
82 session: Session,
84 },
85 Pending {
89 user: User,
91 session: Session,
93 },
94}
95
96impl Default for AuthRuntimeState {
97 fn default() -> Self {
98 Self::loading()
99 }
100}
101
102#[cfg_attr(not(clerk_client), allow(dead_code))]
103impl AuthRuntimeState {
104 pub fn loading() -> Self {
106 Self::with_status(AuthRuntimeStateKind::Loading, false)
107 }
108
109 fn with_status(status: AuthRuntimeStateKind, is_loaded: bool) -> Self {
110 Self { status, is_loaded }
111 }
112
113 fn signed_out_with_loaded(is_loaded: bool) -> Self {
114 Self::with_status(AuthRuntimeStateKind::SignedOut, is_loaded)
115 }
116
117 fn with_loaded(mut self, is_loaded: bool) -> Self {
118 self.is_loaded = is_loaded;
119 self
120 }
121
122 pub fn from_initial_auth_snapshot(snapshot: &InitialAuthSnapshot) -> Self {
131 match snapshot.status {
132 InitialAuthStatus::Unverified => return Self::loading(),
133 InitialAuthStatus::SignedOut => return Self::signed_out_with_loaded(false),
134 InitialAuthStatus::SignedIn => {}
135 }
136
137 let Some(user_id) = snapshot.user_id.clone().filter(|id| !id.is_empty()) else {
138 return Self::loading();
139 };
140
141 Self::with_status(
142 AuthRuntimeStateKind::SignedInSnapshot {
143 user_id,
144 session_id: snapshot.session_id.clone(),
145 org_id: snapshot.org_id.clone(),
146 org_slug: snapshot.org_slug.clone(),
147 org_role: snapshot.org_role.clone(),
148 org_permissions: snapshot.org_permissions.clone(),
149 },
150 false,
151 )
152 }
153
154 #[cfg(test)]
157 pub fn from_js_session(user: User, session: Session, previous: &Self) -> Self {
158 Self::from_js_session_with_loaded(user, session, previous, true)
159 }
160
161 fn matches_user_session_org(
167 previous_user_id: &str,
168 previous_session_id: Option<&str>,
169 previous_org_id: Option<&str>,
170 user: &User,
171 session: &Session,
172 ) -> bool {
173 previous_user_id == user.id
174 && previous_session_id == Some(session.id.as_str())
175 && previous_org_id == session.last_active_organization_id.as_deref()
176 }
177
178 fn from_js_session_with_loaded(
179 user: User,
180 session: Session,
181 previous: &Self,
182 is_loaded: bool,
183 ) -> Self {
184 let (org_id, org_slug, org_role, org_permissions) = match &previous.status {
185 AuthRuntimeStateKind::SignedInSnapshot {
186 user_id,
187 session_id,
188 org_id,
189 org_slug,
190 org_role,
191 org_permissions,
192 } if Self::matches_user_session_org(
193 user_id,
194 session_id.as_deref(),
195 org_id.as_deref(),
196 &user,
197 &session,
198 ) =>
199 {
200 (
201 org_id.clone(),
202 org_slug.clone(),
203 org_role.clone(),
204 org_permissions.clone(),
205 )
206 }
207 AuthRuntimeStateKind::SignedIn {
208 user: previous_user,
209 session: previous_session,
210 org_id,
211 org_slug,
212 org_role,
213 org_permissions,
214 } if Self::matches_user_session_org(
215 &previous_user.id,
216 Some(previous_session.id.as_str()),
217 org_id.as_deref(),
218 &user,
219 &session,
220 ) =>
221 {
222 (
223 org_id.clone(),
224 org_slug.clone(),
225 org_role.clone(),
226 org_permissions.clone(),
227 )
228 }
229 _ => (None, None, None, vec![]),
230 };
231 Self::with_status(
232 AuthRuntimeStateKind::SignedIn {
233 user,
234 session,
235 org_id,
236 org_slug,
237 org_role,
238 org_permissions,
239 },
240 is_loaded,
241 )
242 }
243
244 pub fn apply_observation(&self, observation: AuthObservation) -> Self {
246 match observation {
247 AuthObservation::Loading if self.is_signed_in() || self.is_pending() => self.clone(),
250 AuthObservation::Loading => {
251 Self::with_status(AuthRuntimeStateKind::Loading, self.is_loaded)
252 }
253 AuthObservation::SignedOut => Self::signed_out_with_loaded(self.is_loaded),
254 AuthObservation::SignedIn { user, session } => {
255 Self::from_js_session_with_loaded(user, session, self, self.is_loaded)
256 }
257 AuthObservation::Pending { user, session } => Self::with_status(
258 AuthRuntimeStateKind::Pending { user, session },
259 self.is_loaded,
260 ),
261 }
262 }
263
264 pub fn apply_loaded_observation(&self, observation: AuthObservation) -> Self {
266 self.apply_observation(observation).with_loaded(true)
267 }
268
269 pub fn to_state(&self) -> AuthState {
271 match &self.status {
272 AuthRuntimeStateKind::Loading => AuthState {
273 status: AuthStatus::Loading,
274 is_loaded: self.is_loaded,
275 ..AuthState::signed_out()
276 },
277 AuthRuntimeStateKind::SignedOut => AuthState {
278 status: AuthStatus::SignedOut,
279 is_loaded: self.is_loaded,
280 ..AuthState::signed_out()
281 },
282 AuthRuntimeStateKind::SignedInSnapshot {
283 user_id,
284 session_id,
285 org_id,
286 org_slug,
287 org_role,
288 org_permissions,
289 } => AuthState {
290 status: AuthStatus::SignedIn,
291 is_loaded: self.is_loaded,
292 user_id: Some(user_id.clone()),
293 session_id: session_id.clone(),
294 org_id: org_id.clone(),
295 org_slug: org_slug.clone(),
296 org_role: org_role.clone(),
297 org_permissions: org_permissions.clone(),
298 },
299 AuthRuntimeStateKind::SignedIn {
300 user,
301 session,
302 org_id,
303 org_slug,
304 org_role,
305 org_permissions,
306 } => AuthState {
307 status: AuthStatus::SignedIn,
308 is_loaded: self.is_loaded,
309 user_id: Some(user.id.clone()),
310 session_id: Some(session.id.clone()),
311 org_id: org_id.clone(),
312 org_slug: org_slug.clone(),
313 org_role: org_role.clone(),
314 org_permissions: org_permissions.clone(),
315 },
316 AuthRuntimeStateKind::Pending { .. } => AuthState {
320 status: AuthStatus::SignedOut,
321 is_loaded: self.is_loaded,
322 ..AuthState::signed_out()
323 },
324 }
325 }
326
327 pub fn is_loaded(&self) -> bool {
329 self.is_loaded
330 }
331
332 pub fn is_signed_in(&self) -> bool {
336 matches!(
337 &self.status,
338 AuthRuntimeStateKind::SignedInSnapshot { .. } | AuthRuntimeStateKind::SignedIn { .. }
339 )
340 }
341
342 pub fn is_pending(&self) -> bool {
345 matches!(&self.status, AuthRuntimeStateKind::Pending { .. })
346 }
347
348 pub fn resolve_pending(&self, treat_pending_as_signed_out: bool) -> Cow<'_, Self> {
362 match &self.status {
363 AuthRuntimeStateKind::Pending { user, session } if !treat_pending_as_signed_out => {
364 Cow::Owned(Self::with_status(
365 AuthRuntimeStateKind::SignedIn {
366 org_id: session.last_active_organization_id.clone(),
367 org_slug: None,
368 org_role: None,
369 org_permissions: Vec::new(),
370 user: user.clone(),
371 session: session.clone(),
372 },
373 self.is_loaded,
374 ))
375 }
376 _ => Cow::Borrowed(self),
377 }
378 }
379
380 pub fn should_render_signed_in(&self) -> bool {
382 self.is_signed_in()
383 }
384
385 pub fn should_render_signed_out(&self) -> bool {
389 matches!(
390 &self.status,
391 AuthRuntimeStateKind::SignedOut | AuthRuntimeStateKind::Pending { .. }
392 )
393 }
394
395 pub fn user(&self) -> Option<&User> {
397 match &self.status {
398 AuthRuntimeStateKind::SignedIn { user, .. }
399 | AuthRuntimeStateKind::Pending { user, .. } => Some(user),
400 AuthRuntimeStateKind::Loading
401 | AuthRuntimeStateKind::SignedOut
402 | AuthRuntimeStateKind::SignedInSnapshot { .. } => None,
403 }
404 }
405
406 pub fn session(&self) -> Option<&Session> {
410 match &self.status {
411 AuthRuntimeStateKind::SignedIn { session, .. }
412 | AuthRuntimeStateKind::Pending { session, .. } => Some(session),
413 AuthRuntimeStateKind::Loading
414 | AuthRuntimeStateKind::SignedOut
415 | AuthRuntimeStateKind::SignedInSnapshot { .. } => None,
416 }
417 }
418
419 pub fn allows_signed_in_gate(&self, role: Option<&str>, permission: Option<&str>) -> bool {
424 if !self.should_render_signed_in() {
425 return false;
426 }
427
428 let role = if permission.is_some() { None } else { role };
429
430 if role.is_none() && permission.is_none() {
431 return true;
432 }
433
434 let (org_role, org_permissions) = match &self.status {
435 AuthRuntimeStateKind::SignedInSnapshot {
436 org_role,
437 org_permissions,
438 ..
439 }
440 | AuthRuntimeStateKind::SignedIn {
441 org_role,
442 org_permissions,
443 ..
444 } => (org_role.as_deref(), org_permissions.as_slice()),
445 AuthRuntimeStateKind::Loading
448 | AuthRuntimeStateKind::SignedOut
449 | AuthRuntimeStateKind::Pending { .. } => (None, &[][..]),
450 };
451 let role_allowed = match role {
452 Some(required) => org_role == Some(required),
453 None => true,
454 };
455 let permission_allowed = match permission {
456 Some(required) => org_permissions.iter().any(|actual| actual == required),
457 None => true,
458 };
459
460 role_allowed && permission_allowed
461 }
462}
463
464#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
472#[non_exhaustive]
473pub struct User {
474 pub id: String,
476 #[serde(default, alias = "firstName")]
478 pub first_name: Option<String>,
479 #[serde(default, alias = "lastName")]
481 pub last_name: Option<String>,
482 #[serde(
487 default,
488 alias = "primaryEmailAddress",
489 deserialize_with = "deserialize_primary_email_address"
490 )]
491 pub primary_email_address: Option<String>,
492 #[serde(default, alias = "imageUrl")]
494 pub image_url: Option<String>,
495}
496
497impl User {
498 pub fn new(id: impl Into<String>) -> Self {
501 Self {
502 id: id.into(),
503 first_name: None,
504 last_name: None,
505 primary_email_address: None,
506 image_url: None,
507 }
508 }
509}
510
511fn deserialize_primary_email_address<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
514where
515 D: serde::Deserializer<'de>,
516{
517 #[derive(Deserialize)]
518 #[serde(untagged)]
519 enum PrimaryEmailAddress {
520 Address(String),
521 Object(EmailAddressObject),
522 }
523
524 #[derive(Deserialize)]
525 struct EmailAddressObject {
526 #[serde(default, alias = "emailAddress")]
527 email_address: Option<String>,
528 }
529
530 Ok(
531 match Option::<PrimaryEmailAddress>::deserialize(deserializer)? {
532 None => None,
533 Some(PrimaryEmailAddress::Address(address)) => Some(address),
534 Some(PrimaryEmailAddress::Object(object)) => object.email_address,
535 },
536 )
537}
538
539#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
545#[serde(from = "String", into = "String")]
546#[non_exhaustive]
547pub enum SessionStatus {
548 Active,
550 Abandoned,
552 Ended,
554 Expired,
556 Pending,
559 Removed,
561 Replaced,
563 Revoked,
565 Other(OtherStatus),
573}
574
575#[derive(Debug, Clone, PartialEq, Eq, Hash)]
583pub struct OtherStatus(String);
584
585impl OtherStatus {
586 pub fn as_str(&self) -> &str {
588 &self.0
589 }
590}
591
592impl std::fmt::Display for OtherStatus {
593 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
594 f.write_str(&self.0)
595 }
596}
597
598impl SessionStatus {
599 pub fn as_str(&self) -> &str {
601 match self {
602 Self::Active => "active",
603 Self::Abandoned => "abandoned",
604 Self::Ended => "ended",
605 Self::Expired => "expired",
606 Self::Pending => "pending",
607 Self::Removed => "removed",
608 Self::Replaced => "replaced",
609 Self::Revoked => "revoked",
610 Self::Other(status) => status.as_str(),
611 }
612 }
613}
614
615impl SessionStatus {
616 fn from_known(status: &str) -> Option<Self> {
617 Some(match status {
618 "active" => Self::Active,
619 "abandoned" => Self::Abandoned,
620 "ended" => Self::Ended,
621 "expired" => Self::Expired,
622 "pending" => Self::Pending,
623 "removed" => Self::Removed,
624 "replaced" => Self::Replaced,
625 "revoked" => Self::Revoked,
626 _ => return None,
627 })
628 }
629}
630
631impl From<&str> for SessionStatus {
632 fn from(status: &str) -> Self {
633 Self::from_known(status).unwrap_or_else(|| Self::Other(OtherStatus(status.to_owned())))
634 }
635}
636
637impl From<String> for SessionStatus {
638 fn from(status: String) -> Self {
639 Self::from_known(&status).unwrap_or(Self::Other(OtherStatus(status)))
641 }
642}
643
644impl From<SessionStatus> for String {
645 fn from(status: SessionStatus) -> Self {
646 status.as_str().to_owned()
647 }
648}
649
650impl std::str::FromStr for SessionStatus {
651 type Err = std::convert::Infallible;
652
653 fn from_str(status: &str) -> Result<Self, Self::Err> {
654 Ok(Self::from(status))
655 }
656}
657
658impl std::fmt::Display for SessionStatus {
659 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
660 f.write_str(self.as_str())
661 }
662}
663
664#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
671#[non_exhaustive]
672pub struct SessionTask {
673 pub key: SessionTaskKey,
675}
676
677impl SessionTask {
678 pub fn new(key: impl Into<SessionTaskKey>) -> Self {
680 Self { key: key.into() }
681 }
682}
683
684#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
690#[serde(from = "String", into = "String")]
691#[non_exhaustive]
692pub enum SessionTaskKey {
693 ChooseOrganization,
695 ResetPassword,
697 SetupMfa,
699 Other(OtherTaskKey),
706}
707
708#[derive(Debug, Clone, PartialEq, Eq, Hash)]
714pub struct OtherTaskKey(String);
715
716impl OtherTaskKey {
717 pub fn as_str(&self) -> &str {
719 &self.0
720 }
721}
722
723impl std::fmt::Display for OtherTaskKey {
724 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
725 f.write_str(&self.0)
726 }
727}
728
729impl SessionTaskKey {
730 pub fn as_str(&self) -> &str {
732 match self {
733 Self::ChooseOrganization => "choose-organization",
734 Self::ResetPassword => "reset-password",
735 Self::SetupMfa => "setup-mfa",
736 Self::Other(key) => key.as_str(),
737 }
738 }
739
740 fn from_known(key: &str) -> Option<Self> {
741 Some(match key {
742 "choose-organization" => Self::ChooseOrganization,
743 "reset-password" => Self::ResetPassword,
744 "setup-mfa" => Self::SetupMfa,
745 _ => return None,
746 })
747 }
748}
749
750impl From<&str> for SessionTaskKey {
751 fn from(key: &str) -> Self {
752 Self::from_known(key).unwrap_or_else(|| Self::Other(OtherTaskKey(key.to_owned())))
753 }
754}
755
756impl From<String> for SessionTaskKey {
757 fn from(key: String) -> Self {
758 Self::from_known(&key).unwrap_or(Self::Other(OtherTaskKey(key)))
760 }
761}
762
763impl From<SessionTaskKey> for String {
764 fn from(key: SessionTaskKey) -> Self {
765 key.as_str().to_owned()
766 }
767}
768
769impl std::str::FromStr for SessionTaskKey {
770 type Err = std::convert::Infallible;
771
772 fn from_str(key: &str) -> Result<Self, Self::Err> {
773 Ok(Self::from(key))
774 }
775}
776
777impl std::fmt::Display for SessionTaskKey {
778 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
779 f.write_str(self.as_str())
780 }
781}
782
783#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
789#[non_exhaustive]
790pub struct Session {
791 pub id: String,
793 pub status: SessionStatus,
795 #[serde(default, alias = "lastActiveOrganizationId")]
799 pub last_active_organization_id: Option<String>,
800 #[serde(default, alias = "lastActiveAt")]
802 pub last_active_at: Option<i64>,
803 #[serde(default, alias = "expireAt")]
805 pub expire_at: Option<i64>,
806 #[serde(default, alias = "currentTask")]
810 pub current_task: Option<SessionTask>,
811 #[serde(default, deserialize_with = "deserialize_null_default")]
814 pub tasks: Vec<SessionTask>,
815}
816
817fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
821where
822 D: serde::Deserializer<'de>,
823 T: Deserialize<'de> + Default,
824{
825 Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
826}
827
828impl Session {
829 pub fn new(id: impl Into<String>, status: impl Into<SessionStatus>) -> Self {
832 Self {
833 id: id.into(),
834 status: status.into(),
835 last_active_organization_id: None,
836 last_active_at: None,
837 expire_at: None,
838 current_task: None,
839 tasks: Vec::new(),
840 }
841 }
842
843 pub fn is_active(&self) -> bool {
845 self.status == SessionStatus::Active
846 }
847}
848
849#[cfg(test)]
850mod tests;