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}