use super::{AuthState, AuthStatus};
use crate::ssr::{InitialAuthSnapshot, InitialAuthStatus};
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct AuthRuntimeState {
status: AuthRuntimeStateKind,
is_loaded: bool,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(not(clerk_client), allow(dead_code))]
enum AuthRuntimeStateKind {
Loading,
SignedOut,
SignedInSnapshot {
user_id: String,
session_id: Option<String>,
org_id: Option<String>,
org_slug: Option<String>,
org_role: Option<String>,
org_permissions: Vec<String>,
},
SignedIn {
user: User,
session: Session,
org_id: Option<String>,
org_slug: Option<String>,
org_role: Option<String>,
org_permissions: Vec<String>,
},
Pending {
user: User,
session: Session,
},
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(not(clerk_client), allow(dead_code))]
pub(crate) enum AuthObservation {
Loading,
SignedOut,
SignedIn {
user: User,
session: Session,
},
Pending {
user: User,
session: Session,
},
}
impl Default for AuthRuntimeState {
fn default() -> Self {
Self::loading()
}
}
#[cfg_attr(not(clerk_client), allow(dead_code))]
impl AuthRuntimeState {
pub fn loading() -> Self {
Self::with_status(AuthRuntimeStateKind::Loading, false)
}
fn with_status(status: AuthRuntimeStateKind, is_loaded: bool) -> Self {
Self { status, is_loaded }
}
fn signed_out_with_loaded(is_loaded: bool) -> Self {
Self::with_status(AuthRuntimeStateKind::SignedOut, is_loaded)
}
fn with_loaded(mut self, is_loaded: bool) -> Self {
self.is_loaded = is_loaded;
self
}
pub fn from_initial_auth_snapshot(snapshot: &InitialAuthSnapshot) -> Self {
match snapshot.status {
InitialAuthStatus::Unverified => return Self::loading(),
InitialAuthStatus::SignedOut => return Self::signed_out_with_loaded(false),
InitialAuthStatus::SignedIn => {}
}
let Some(user_id) = snapshot.user_id.clone().filter(|id| !id.is_empty()) else {
return Self::loading();
};
Self::with_status(
AuthRuntimeStateKind::SignedInSnapshot {
user_id,
session_id: snapshot.session_id.clone(),
org_id: snapshot.org_id.clone(),
org_slug: snapshot.org_slug.clone(),
org_role: snapshot.org_role.clone(),
org_permissions: snapshot.org_permissions.clone(),
},
false,
)
}
#[cfg(test)]
pub fn from_js_session(user: User, session: Session, previous: &Self) -> Self {
Self::from_js_session_with_loaded(user, session, previous, true)
}
fn matches_user_session_org(
previous_user_id: &str,
previous_session_id: Option<&str>,
previous_org_id: Option<&str>,
user: &User,
session: &Session,
) -> bool {
previous_user_id == user.id
&& previous_session_id == Some(session.id.as_str())
&& previous_org_id == session.last_active_organization_id.as_deref()
}
fn from_js_session_with_loaded(
user: User,
session: Session,
previous: &Self,
is_loaded: bool,
) -> Self {
let (org_id, org_slug, org_role, org_permissions) = match &previous.status {
AuthRuntimeStateKind::SignedInSnapshot {
user_id,
session_id,
org_id,
org_slug,
org_role,
org_permissions,
} if Self::matches_user_session_org(
user_id,
session_id.as_deref(),
org_id.as_deref(),
&user,
&session,
) =>
{
(
org_id.clone(),
org_slug.clone(),
org_role.clone(),
org_permissions.clone(),
)
}
AuthRuntimeStateKind::SignedIn {
user: previous_user,
session: previous_session,
org_id,
org_slug,
org_role,
org_permissions,
} if Self::matches_user_session_org(
&previous_user.id,
Some(previous_session.id.as_str()),
org_id.as_deref(),
&user,
&session,
) =>
{
(
org_id.clone(),
org_slug.clone(),
org_role.clone(),
org_permissions.clone(),
)
}
_ => (None, None, None, vec![]),
};
Self::with_status(
AuthRuntimeStateKind::SignedIn {
user,
session,
org_id,
org_slug,
org_role,
org_permissions,
},
is_loaded,
)
}
pub fn apply_observation(&self, observation: AuthObservation) -> Self {
match observation {
AuthObservation::Loading if self.is_signed_in() || self.is_pending() => self.clone(),
AuthObservation::Loading => {
Self::with_status(AuthRuntimeStateKind::Loading, self.is_loaded)
}
AuthObservation::SignedOut => Self::signed_out_with_loaded(self.is_loaded),
AuthObservation::SignedIn { user, session } => {
Self::from_js_session_with_loaded(user, session, self, self.is_loaded)
}
AuthObservation::Pending { user, session } => Self::with_status(
AuthRuntimeStateKind::Pending { user, session },
self.is_loaded,
),
}
}
pub fn apply_loaded_observation(&self, observation: AuthObservation) -> Self {
self.apply_observation(observation).with_loaded(true)
}
pub fn to_state(&self) -> AuthState {
match &self.status {
AuthRuntimeStateKind::Loading => AuthState {
status: AuthStatus::Loading,
is_loaded: self.is_loaded,
..AuthState::signed_out()
},
AuthRuntimeStateKind::SignedOut => AuthState {
status: AuthStatus::SignedOut,
is_loaded: self.is_loaded,
..AuthState::signed_out()
},
AuthRuntimeStateKind::SignedInSnapshot {
user_id,
session_id,
org_id,
org_slug,
org_role,
org_permissions,
} => AuthState {
status: AuthStatus::SignedIn,
is_loaded: self.is_loaded,
user_id: Some(user_id.clone()),
session_id: session_id.clone(),
org_id: org_id.clone(),
org_slug: org_slug.clone(),
org_role: org_role.clone(),
org_permissions: org_permissions.clone(),
},
AuthRuntimeStateKind::SignedIn {
user,
session,
org_id,
org_slug,
org_role,
org_permissions,
} => AuthState {
status: AuthStatus::SignedIn,
is_loaded: self.is_loaded,
user_id: Some(user.id.clone()),
session_id: Some(session.id.clone()),
org_id: org_id.clone(),
org_slug: org_slug.clone(),
org_role: org_role.clone(),
org_permissions: org_permissions.clone(),
},
AuthRuntimeStateKind::Pending { .. } => AuthState {
status: AuthStatus::SignedOut,
is_loaded: self.is_loaded,
..AuthState::signed_out()
},
}
}
pub fn is_loaded(&self) -> bool {
self.is_loaded
}
pub fn is_signed_in(&self) -> bool {
matches!(
&self.status,
AuthRuntimeStateKind::SignedInSnapshot { .. } | AuthRuntimeStateKind::SignedIn { .. }
)
}
pub fn is_pending(&self) -> bool {
matches!(&self.status, AuthRuntimeStateKind::Pending { .. })
}
pub fn resolve_pending(&self, treat_pending_as_signed_out: bool) -> Cow<'_, Self> {
match &self.status {
AuthRuntimeStateKind::Pending { user, session } if !treat_pending_as_signed_out => {
Cow::Owned(Self::with_status(
AuthRuntimeStateKind::SignedIn {
org_id: session.last_active_organization_id.clone(),
org_slug: None,
org_role: None,
org_permissions: Vec::new(),
user: user.clone(),
session: session.clone(),
},
self.is_loaded,
))
}
_ => Cow::Borrowed(self),
}
}
pub fn should_render_signed_in(&self) -> bool {
self.is_signed_in()
}
pub fn should_render_signed_out(&self) -> bool {
matches!(
&self.status,
AuthRuntimeStateKind::SignedOut | AuthRuntimeStateKind::Pending { .. }
)
}
pub fn user(&self) -> Option<&User> {
match &self.status {
AuthRuntimeStateKind::SignedIn { user, .. }
| AuthRuntimeStateKind::Pending { user, .. } => Some(user),
AuthRuntimeStateKind::Loading
| AuthRuntimeStateKind::SignedOut
| AuthRuntimeStateKind::SignedInSnapshot { .. } => None,
}
}
pub fn session(&self) -> Option<&Session> {
match &self.status {
AuthRuntimeStateKind::SignedIn { session, .. }
| AuthRuntimeStateKind::Pending { session, .. } => Some(session),
AuthRuntimeStateKind::Loading
| AuthRuntimeStateKind::SignedOut
| AuthRuntimeStateKind::SignedInSnapshot { .. } => None,
}
}
pub fn allows_signed_in_gate(&self, role: Option<&str>, permission: Option<&str>) -> bool {
if !self.should_render_signed_in() {
return false;
}
let role = if permission.is_some() { None } else { role };
if role.is_none() && permission.is_none() {
return true;
}
let (org_role, org_permissions) = match &self.status {
AuthRuntimeStateKind::SignedInSnapshot {
org_role,
org_permissions,
..
}
| AuthRuntimeStateKind::SignedIn {
org_role,
org_permissions,
..
} => (org_role.as_deref(), org_permissions.as_slice()),
AuthRuntimeStateKind::Loading
| AuthRuntimeStateKind::SignedOut
| AuthRuntimeStateKind::Pending { .. } => (None, &[][..]),
};
let role_allowed = match role {
Some(required) => org_role == Some(required),
None => true,
};
let permission_allowed = match permission {
Some(required) => org_permissions.iter().any(|actual| actual == required),
None => true,
};
role_allowed && permission_allowed
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct User {
pub id: String,
#[serde(default, alias = "firstName")]
pub first_name: Option<String>,
#[serde(default, alias = "lastName")]
pub last_name: Option<String>,
#[serde(
default,
alias = "primaryEmailAddress",
deserialize_with = "deserialize_primary_email_address"
)]
pub primary_email_address: Option<String>,
#[serde(default, alias = "imageUrl")]
pub image_url: Option<String>,
}
impl User {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
first_name: None,
last_name: None,
primary_email_address: None,
image_url: None,
}
}
}
fn deserialize_primary_email_address<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum PrimaryEmailAddress {
Address(String),
Object(EmailAddressObject),
}
#[derive(Deserialize)]
struct EmailAddressObject {
#[serde(default, alias = "emailAddress")]
email_address: Option<String>,
}
Ok(
match Option::<PrimaryEmailAddress>::deserialize(deserializer)? {
None => None,
Some(PrimaryEmailAddress::Address(address)) => Some(address),
Some(PrimaryEmailAddress::Object(object)) => object.email_address,
},
)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
#[non_exhaustive]
pub enum SessionStatus {
Active,
Abandoned,
Ended,
Expired,
Pending,
Removed,
Replaced,
Revoked,
Other(OtherStatus),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct OtherStatus(String);
impl OtherStatus {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for OtherStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl SessionStatus {
pub fn as_str(&self) -> &str {
match self {
Self::Active => "active",
Self::Abandoned => "abandoned",
Self::Ended => "ended",
Self::Expired => "expired",
Self::Pending => "pending",
Self::Removed => "removed",
Self::Replaced => "replaced",
Self::Revoked => "revoked",
Self::Other(status) => status.as_str(),
}
}
}
impl SessionStatus {
fn from_known(status: &str) -> Option<Self> {
Some(match status {
"active" => Self::Active,
"abandoned" => Self::Abandoned,
"ended" => Self::Ended,
"expired" => Self::Expired,
"pending" => Self::Pending,
"removed" => Self::Removed,
"replaced" => Self::Replaced,
"revoked" => Self::Revoked,
_ => return None,
})
}
}
impl From<&str> for SessionStatus {
fn from(status: &str) -> Self {
Self::from_known(status).unwrap_or_else(|| Self::Other(OtherStatus(status.to_owned())))
}
}
impl From<String> for SessionStatus {
fn from(status: String) -> Self {
Self::from_known(&status).unwrap_or(Self::Other(OtherStatus(status)))
}
}
impl From<SessionStatus> for String {
fn from(status: SessionStatus) -> Self {
status.as_str().to_owned()
}
}
impl std::str::FromStr for SessionStatus {
type Err = std::convert::Infallible;
fn from_str(status: &str) -> Result<Self, Self::Err> {
Ok(Self::from(status))
}
}
impl std::fmt::Display for SessionStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SessionTask {
pub key: SessionTaskKey,
}
impl SessionTask {
pub fn new(key: impl Into<SessionTaskKey>) -> Self {
Self { key: key.into() }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
#[non_exhaustive]
pub enum SessionTaskKey {
ChooseOrganization,
ResetPassword,
SetupMfa,
Other(OtherTaskKey),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct OtherTaskKey(String);
impl OtherTaskKey {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for OtherTaskKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl SessionTaskKey {
pub fn as_str(&self) -> &str {
match self {
Self::ChooseOrganization => "choose-organization",
Self::ResetPassword => "reset-password",
Self::SetupMfa => "setup-mfa",
Self::Other(key) => key.as_str(),
}
}
fn from_known(key: &str) -> Option<Self> {
Some(match key {
"choose-organization" => Self::ChooseOrganization,
"reset-password" => Self::ResetPassword,
"setup-mfa" => Self::SetupMfa,
_ => return None,
})
}
}
impl From<&str> for SessionTaskKey {
fn from(key: &str) -> Self {
Self::from_known(key).unwrap_or_else(|| Self::Other(OtherTaskKey(key.to_owned())))
}
}
impl From<String> for SessionTaskKey {
fn from(key: String) -> Self {
Self::from_known(&key).unwrap_or(Self::Other(OtherTaskKey(key)))
}
}
impl From<SessionTaskKey> for String {
fn from(key: SessionTaskKey) -> Self {
key.as_str().to_owned()
}
}
impl std::str::FromStr for SessionTaskKey {
type Err = std::convert::Infallible;
fn from_str(key: &str) -> Result<Self, Self::Err> {
Ok(Self::from(key))
}
}
impl std::fmt::Display for SessionTaskKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Session {
pub id: String,
pub status: SessionStatus,
#[serde(default, alias = "lastActiveOrganizationId")]
pub last_active_organization_id: Option<String>,
#[serde(default, alias = "lastActiveAt")]
pub last_active_at: Option<i64>,
#[serde(default, alias = "expireAt")]
pub expire_at: Option<i64>,
#[serde(default, alias = "currentTask")]
pub current_task: Option<SessionTask>,
#[serde(default, deserialize_with = "deserialize_null_default")]
pub tasks: Vec<SessionTask>,
}
fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: serde::Deserializer<'de>,
T: Deserialize<'de> + Default,
{
Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
}
impl Session {
pub fn new(id: impl Into<String>, status: impl Into<SessionStatus>) -> Self {
Self {
id: id.into(),
status: status.into(),
last_active_organization_id: None,
last_active_at: None,
expire_at: None,
current_task: None,
tasks: Vec::new(),
}
}
pub fn is_active(&self) -> bool {
self.status == SessionStatus::Active
}
}
#[cfg(test)]
mod tests;