rustauth_plugins/anonymous/
model.rs1use rustauth_core::auth::session::{GetSessionInput, SessionAuth};
2use rustauth_core::context::AuthContext;
3use rustauth_core::cookies::{delete_session_cookie, verify_cookie_value};
4use rustauth_core::db::{Create, DbAdapter, DbRecord, DbValue, FindOne, Session, Where};
5use rustauth_core::error::RustAuthError;
6use rustauth_core::session::CreateSessionInput;
7use serde::Serialize;
8use time::OffsetDateTime;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
11#[serde(rename_all = "camelCase")]
12pub struct AnonymousUser {
13 pub id: String,
14 pub name: String,
15 pub email: String,
16 pub email_verified: bool,
17 pub image: Option<String>,
18 pub created_at: OffsetDateTime,
19 pub updated_at: OffsetDateTime,
20 pub is_anonymous: bool,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
24pub struct AnonymousSession {
25 pub session: Session,
26 pub user: AnonymousUser,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
30pub struct LinkedSession {
31 pub session: Session,
32 pub user: AnonymousUser,
33}
34
35pub async fn current_anonymous_session(
36 adapter: &dyn DbAdapter,
37 context: &AuthContext,
38 anonymous_field_name: &str,
39 cookie_header: String,
40) -> Result<Option<AnonymousSession>, RustAuthError> {
41 let Some(result) = SessionAuth::new(context)?
42 .get_session(GetSessionInput::new(cookie_header).disable_refresh())
43 .await?
44 else {
45 return Ok(None);
46 };
47 let Some(session) = result.session else {
48 return Ok(None);
49 };
50 let Some(user) = find_anonymous_user(adapter, anonymous_field_name, &session.user_id).await?
51 else {
52 return Ok(None);
53 };
54 Ok(Some(AnonymousSession { session, user }))
55}
56
57pub async fn create_anonymous_user(
58 adapter: &dyn DbAdapter,
59 anonymous_field_name: &str,
60 additional_fields: DbRecord,
61 id: String,
62 name: String,
63 email: String,
64) -> Result<AnonymousUser, RustAuthError> {
65 let now = OffsetDateTime::now_utc();
66 let mut query = Create::new("user")
67 .data("id", DbValue::String(id))
68 .data("name", DbValue::String(name))
69 .data("email", DbValue::String(email.to_lowercase()))
70 .data("email_verified", DbValue::Boolean(false))
71 .data("image", DbValue::Null)
72 .data("created_at", DbValue::Timestamp(now))
73 .data("updated_at", DbValue::Timestamp(now));
74 for (field, value) in additional_fields {
75 query = query.data(field, value);
76 }
77 let record = adapter
78 .create(
79 query
80 .data(anonymous_field_name, DbValue::Boolean(true))
81 .force_allow_id(),
82 )
83 .await?;
84 anonymous_user_from_record(record, anonymous_field_name)
85}
86
87pub async fn create_session(
88 context: &AuthContext,
89 user_id: &str,
90 additional_fields: DbRecord,
91) -> Result<Session, RustAuthError> {
92 let expires_at = OffsetDateTime::now_utc() + context.session_config.expires_in;
93 context
94 .sessions()?
95 .create_session(
96 CreateSessionInput::new(user_id, expires_at).additional_fields(additional_fields),
97 )
98 .await
99}
100
101pub async fn find_anonymous_user(
102 adapter: &dyn DbAdapter,
103 anonymous_field_name: &str,
104 user_id: &str,
105) -> Result<Option<AnonymousUser>, RustAuthError> {
106 adapter
107 .find_one(
108 FindOne::new("user")
109 .where_clause(Where::new("id", DbValue::String(user_id.to_owned()))),
110 )
111 .await?
112 .map(|record| anonymous_user_from_record(record, anonymous_field_name))
113 .transpose()
114}
115
116pub async fn linked_session_from_token(
117 context: &AuthContext,
118 adapter: &dyn DbAdapter,
119 anonymous_field_name: &str,
120 token: &str,
121) -> Result<Option<LinkedSession>, RustAuthError> {
122 let Some(session) = context.sessions()?.find_session(token).await? else {
123 return Ok(None);
124 };
125 let Some(user) = find_anonymous_user(adapter, anonymous_field_name, &session.user_id).await?
126 else {
127 return Ok(None);
128 };
129 Ok(Some(LinkedSession { session, user }))
130}
131
132pub fn delete_session_cookies(
133 context: &AuthContext,
134 cookie_header: &str,
135) -> Vec<rustauth_core::cookies::Cookie> {
136 delete_session_cookie(&context.auth_cookies, cookie_header, false)
137}
138
139pub fn verified_cookie_value(
140 context: &AuthContext,
141 value: &str,
142) -> Result<Option<String>, RustAuthError> {
143 verify_cookie_value(value, &context.secret)
144}
145
146pub async fn delete_anonymous_user_records(
147 context: &AuthContext,
148 user_id: &str,
149) -> Result<(), RustAuthError> {
150 context.sessions()?.delete_user_sessions(user_id).await?;
151 context.users()?.delete_user_accounts(user_id).await?;
152 context.users()?.delete_user(user_id).await
153}
154
155fn anonymous_user_from_record(
156 record: DbRecord,
157 anonymous_field_name: &str,
158) -> Result<AnonymousUser, RustAuthError> {
159 Ok(AnonymousUser {
160 id: required_string(&record, "id")?.to_owned(),
161 name: required_string(&record, "name")?.to_owned(),
162 email: required_string(&record, "email")?.to_owned(),
163 email_verified: required_bool(&record, "email_verified")?,
164 image: optional_string(&record, "image")?,
165 created_at: required_timestamp(&record, "created_at")?,
166 updated_at: required_timestamp(&record, "updated_at")?,
167 is_anonymous: optional_bool(&record, "is_anonymous")?
168 .or(optional_bool(&record, anonymous_field_name)?)
169 .unwrap_or(false),
170 })
171}
172
173fn required_string<'a>(record: &'a DbRecord, field: &str) -> Result<&'a str, RustAuthError> {
174 match record.get(field) {
175 Some(DbValue::String(value)) => Ok(value),
176 Some(_) => Err(invalid_field(field, "string")),
177 None => Err(missing_field(field)),
178 }
179}
180
181fn optional_string(record: &DbRecord, field: &str) -> Result<Option<String>, RustAuthError> {
182 match record.get(field) {
183 Some(DbValue::String(value)) => Ok(Some(value.to_owned())),
184 Some(DbValue::Null) | None => Ok(None),
185 Some(_) => Err(invalid_field(field, "string or null")),
186 }
187}
188
189fn required_bool(record: &DbRecord, field: &str) -> Result<bool, RustAuthError> {
190 match record.get(field) {
191 Some(DbValue::Boolean(value)) => Ok(*value),
192 Some(_) => Err(invalid_field(field, "boolean")),
193 None => Err(missing_field(field)),
194 }
195}
196
197fn optional_bool(record: &DbRecord, field: &str) -> Result<Option<bool>, RustAuthError> {
198 match record.get(field) {
199 Some(DbValue::Boolean(value)) => Ok(Some(*value)),
200 Some(DbValue::Null) | None => Ok(None),
201 Some(_) => Err(invalid_field(field, "boolean or null")),
202 }
203}
204
205fn required_timestamp(record: &DbRecord, field: &str) -> Result<OffsetDateTime, RustAuthError> {
206 match record.get(field) {
207 Some(DbValue::Timestamp(value)) => Ok(*value),
208 Some(_) => Err(invalid_field(field, "timestamp")),
209 None => Err(missing_field(field)),
210 }
211}
212
213fn missing_field(field: &str) -> RustAuthError {
214 RustAuthError::Adapter(format!("anonymous user record is missing `{field}`"))
215}
216
217fn invalid_field(field: &str, expected: &str) -> RustAuthError {
218 RustAuthError::Adapter(format!(
219 "anonymous user record field `{field}` must be {expected}"
220 ))
221}