Skip to main content

dioxus_clerk/core/
auth_state.rs

1//! Auth state: the app-visible authentication knowledge consumed by
2//! components and hooks.
3
4use super::claims::ClerkAuth;
5use super::error::ClerkError;
6
7/// App-visible authentication state for components and hooks.
8///
9/// [`status`](AuthState::status) is the single source of truth for the
10/// signed-in answer; signed-in checks derive from it so contradictory states
11/// cannot be represented. The fields are private so `status` and `user_id`
12/// cannot be mutated out of step with each other; read them through the
13/// accessors ([`status`](AuthState::status), [`user_id`](AuthState::user_id),
14/// …) and build values through the constructors and `with_*` setters. SSR
15/// initial state carries `crate::ssr::InitialAuthSnapshot` instead. Browser
16/// loadedness is client lifecycle state, so server-rendered auth snapshots do
17/// not set `is_loaded`.
18#[derive(Debug, Clone, PartialEq, Eq)]
19#[non_exhaustive]
20pub struct AuthState {
21    pub(crate) status: AuthStatus,
22    pub(crate) is_loaded: bool,
23    pub(crate) user_id: Option<String>,
24    pub(crate) session_id: Option<String>,
25    // Org claims come from verified server auth. A client-side org switch
26    // (`setActive`) does not update `org_id` until the server re-verifies.
27    pub(crate) org_id: Option<String>,
28    pub(crate) org_slug: Option<String>,
29    pub(crate) org_role: Option<String>,
30    pub(crate) org_permissions: Vec<String>,
31}
32
33/// Resolved authentication status for app-facing rendering decisions.
34#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
35#[non_exhaustive]
36pub enum AuthStatus {
37    /// Clerk has not supplied a reliable auth answer yet.
38    #[default]
39    Loading,
40    /// Clerk has resolved and no active session is known.
41    SignedOut,
42    /// A signed-in session is known, either from SSR initial state or clerk-js.
43    SignedIn,
44}
45
46impl AuthStatus {
47    /// True while auth has not resolved to signed-in or signed-out yet.
48    pub fn is_loading(self) -> bool {
49        matches!(self, Self::Loading)
50    }
51
52    /// True only when auth has resolved and no active session is known.
53    pub fn is_signed_out(self) -> bool {
54        matches!(self, Self::SignedOut)
55    }
56
57    /// True when a signed-in session is known.
58    pub fn is_signed_in(self) -> bool {
59        matches!(self, Self::SignedIn)
60    }
61}
62
63/// Client-side rendering authorization check.
64#[derive(Debug, Clone, PartialEq, Eq)]
65#[non_exhaustive]
66pub enum AuthRequirement {
67    /// Require any signed-in session.
68    SignedIn,
69    /// Require a server-verified organization role.
70    Role(String),
71    /// Require a server-verified organization permission.
72    Permission(String),
73}
74
75impl AuthRequirement {
76    /// Require any signed-in session.
77    pub fn signed_in() -> Self {
78        Self::SignedIn
79    }
80
81    /// Require a server-verified organization role.
82    pub fn role(role: impl Into<String>) -> Self {
83        Self::Role(role.into())
84    }
85
86    /// Require a server-verified organization permission.
87    pub fn permission(permission: impl Into<String>) -> Self {
88        Self::Permission(permission.into())
89    }
90}
91
92impl AuthState {
93    /// Auth state before Clerk has supplied a reliable answer.
94    pub fn loading() -> Self {
95        Self {
96            status: AuthStatus::Loading,
97            ..Self::signed_out()
98        }
99    }
100
101    /// Auth state representing a resolved signed-out session.
102    ///
103    /// `is_loaded` stays `false`: browser loadedness is client lifecycle
104    /// state, not part of the auth answer. Set it explicitly when modeling a
105    /// client where clerk-js has finished loading.
106    pub fn signed_out() -> Self {
107        Self {
108            status: AuthStatus::SignedOut,
109            is_loaded: false,
110            user_id: None,
111            session_id: None,
112            org_id: None,
113            org_slug: None,
114            org_role: None,
115            org_permissions: vec![],
116        }
117    }
118
119    /// Auth state representing a signed-in session for the given user id.
120    /// Chain the `with_*` setters to add optional session/org fields (and
121    /// `is_loaded`).
122    ///
123    /// An empty user id cannot represent a signed-in session and produces
124    /// [`AuthState::signed_out`], matching `From<&ClerkAuth>`.
125    pub fn signed_in(user_id: impl Into<String>) -> Self {
126        let user_id = user_id.into();
127        if user_id.is_empty() {
128            return Self::signed_out();
129        }
130
131        Self {
132            status: AuthStatus::SignedIn,
133            user_id: Some(user_id),
134            ..Self::signed_out()
135        }
136    }
137
138    /// Set whether clerk-js has finished loading on the client.
139    #[must_use]
140    pub fn with_loaded(mut self, is_loaded: bool) -> Self {
141        self.is_loaded = is_loaded;
142        self
143    }
144
145    /// Set the active session id.
146    #[must_use]
147    pub fn with_session_id(mut self, session_id: impl Into<String>) -> Self {
148        self.session_id = Some(session_id.into());
149        self
150    }
151
152    /// Set the active organization id.
153    #[must_use]
154    pub fn with_org_id(mut self, org_id: impl Into<String>) -> Self {
155        self.org_id = Some(org_id.into());
156        self
157    }
158
159    /// Set the active organization slug.
160    #[must_use]
161    pub fn with_org_slug(mut self, org_slug: impl Into<String>) -> Self {
162        self.org_slug = Some(org_slug.into());
163        self
164    }
165
166    /// Set the organization role from verified server auth.
167    #[must_use]
168    pub fn with_org_role(mut self, org_role: impl Into<String>) -> Self {
169        self.org_role = Some(org_role.into());
170        self
171    }
172
173    /// Set the organization permissions from verified server auth.
174    #[must_use]
175    pub fn with_org_permissions(
176        mut self,
177        org_permissions: impl IntoIterator<Item = impl Into<String>>,
178    ) -> Self {
179        self.org_permissions = org_permissions.into_iter().map(Into::into).collect();
180        self
181    }
182
183    /// Explicit auth resolution status: the single source of truth for the
184    /// signed-in answer.
185    pub fn status(&self) -> AuthStatus {
186        self.status
187    }
188
189    /// Whether clerk-js has finished loading on the client.
190    pub fn is_loaded(&self) -> bool {
191        self.is_loaded
192    }
193
194    /// Clerk user id, if signed in.
195    pub fn user_id(&self) -> Option<&str> {
196        self.user_id.as_deref()
197    }
198
199    /// Active session id, if any.
200    pub fn session_id(&self) -> Option<&str> {
201        self.session_id.as_deref()
202    }
203
204    /// Org id from verified server auth, if any.
205    pub fn org_id(&self) -> Option<&str> {
206        self.org_id.as_deref()
207    }
208
209    /// Org slug from verified server auth, if any.
210    pub fn org_slug(&self) -> Option<&str> {
211        self.org_slug.as_deref()
212    }
213
214    /// Org role from verified server auth, if any.
215    pub fn org_role(&self) -> Option<&str> {
216        self.org_role.as_deref()
217    }
218
219    /// Org permissions from verified server auth.
220    pub fn org_permissions(&self) -> &[String] {
221        &self.org_permissions
222    }
223
224    /// True when a signed-in session is known.
225    pub fn is_signed_in(&self) -> bool {
226        self.status.is_signed_in()
227    }
228
229    /// True while auth has not resolved to signed-in or signed-out yet.
230    pub fn is_loading(&self) -> bool {
231        self.status.is_loading()
232    }
233
234    /// True only when auth has resolved and no active session is known;
235    /// `false` while auth is still loading.
236    pub fn is_signed_out(&self) -> bool {
237        self.status.is_signed_out()
238    }
239
240    /// Return the user id for signed-in auth state.
241    ///
242    /// Preserves the loading/signed-out distinction the rest of the API keeps:
243    /// [`ClerkError::NotLoaded`] while auth has not resolved yet (retry or
244    /// wait), [`ClerkError::Unauthenticated`] only once auth has resolved
245    /// without a signed-in user (redirect to sign-in).
246    pub fn require_signed_in(&self) -> Result<&str, ClerkError> {
247        if self.status.is_loading() {
248            return Err(ClerkError::NotLoaded);
249        }
250        if !self.status.is_signed_in() {
251            return Err(ClerkError::Unauthenticated);
252        }
253
254        self.user_id.as_deref().ok_or(ClerkError::Unauthenticated)
255    }
256
257    /// True if the auth state includes the given server-verified org role.
258    pub fn has_role(&self, role: &str) -> bool {
259        self.org_role.as_deref() == Some(role)
260    }
261
262    /// True if the auth state includes the given server-verified org permission.
263    pub fn has_permission(&self, permission: &str) -> bool {
264        self.org_permissions.iter().any(|p| p == permission)
265    }
266
267    /// True if the auth state satisfies a rendering auth requirement.
268    ///
269    /// Role and permission requirements imply a signed-in session; they are
270    /// never satisfied by a loading or signed-out state.
271    pub fn has(&self, requirement: &AuthRequirement) -> bool {
272        if !self.is_signed_in() {
273            return false;
274        }
275        match requirement {
276            AuthRequirement::SignedIn => true,
277            AuthRequirement::Role(role) => self.has_role(role),
278            AuthRequirement::Permission(permission) => self.has_permission(permission),
279        }
280    }
281}
282
283/// Converts server-verified claims into app-visible auth state.
284///
285/// `is_loaded` stays `false`: these claims come from the server, where
286/// clerk-js does not exist, so browser loadedness cannot be inferred.
287impl From<ClerkAuth> for AuthState {
288    fn from(c: ClerkAuth) -> Self {
289        if c.user_id.is_empty() {
290            return Self::signed_out();
291        }
292
293        Self {
294            status: AuthStatus::SignedIn,
295            is_loaded: false,
296            user_id: Some(c.user_id),
297            session_id: c.session_id,
298            org_id: c.org_id,
299            org_slug: c.org_slug,
300            org_role: c.org_role,
301            org_permissions: c.org_permissions,
302        }
303    }
304}
305
306/// Converts server-verified claims into app-visible auth state, cloning the
307/// claim fields. See `From<ClerkAuth>`.
308impl From<&ClerkAuth> for AuthState {
309    fn from(c: &ClerkAuth) -> Self {
310        Self::from(c.clone())
311    }
312}