Skip to main content

openauth_plugins/anonymous/
model.rs

1use openauth_core::auth::session::{GetSessionInput, SessionAuth};
2use openauth_core::context::AuthContext;
3use openauth_core::cookies::{delete_session_cookie, verify_cookie_value};
4use openauth_core::db::{Create, DbAdapter, DbRecord, DbValue, FindOne, Session, Where};
5use openauth_core::error::OpenAuthError;
6use openauth_core::session::{CreateSessionInput, DbSessionStore};
7use openauth_core::user::DbUserStore;
8use serde::Serialize;
9use time::{Duration, OffsetDateTime};
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
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>, OpenAuthError> {
41    let Some(result) = SessionAuth::new(adapter, 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, OpenAuthError> {
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    adapter: &dyn DbAdapter,
89    context: &AuthContext,
90    user_id: &str,
91    additional_fields: DbRecord,
92) -> Result<Session, OpenAuthError> {
93    let expires_at =
94        OffsetDateTime::now_utc() + Duration::seconds(context.session_config.expires_in as i64);
95    DbSessionStore::new(adapter)
96        .create_session(
97            CreateSessionInput::new(user_id, expires_at).additional_fields(additional_fields),
98        )
99        .await
100}
101
102pub async fn find_anonymous_user(
103    adapter: &dyn DbAdapter,
104    anonymous_field_name: &str,
105    user_id: &str,
106) -> Result<Option<AnonymousUser>, OpenAuthError> {
107    adapter
108        .find_one(
109            FindOne::new("user")
110                .where_clause(Where::new("id", DbValue::String(user_id.to_owned()))),
111        )
112        .await?
113        .map(|record| anonymous_user_from_record(record, anonymous_field_name))
114        .transpose()
115}
116
117pub async fn linked_session_from_token(
118    adapter: &dyn DbAdapter,
119    anonymous_field_name: &str,
120    token: &str,
121) -> Result<Option<LinkedSession>, OpenAuthError> {
122    let Some(session) = DbSessionStore::new(adapter).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<openauth_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>, OpenAuthError> {
143    verify_cookie_value(value, &context.secret)
144}
145
146pub async fn delete_anonymous_user_records(
147    adapter: &dyn DbAdapter,
148    user_id: &str,
149) -> Result<(), OpenAuthError> {
150    DbSessionStore::new(adapter)
151        .delete_user_sessions(user_id)
152        .await?;
153    DbUserStore::new(adapter)
154        .delete_user_accounts(user_id)
155        .await?;
156    DbUserStore::new(adapter).delete_user(user_id).await
157}
158
159fn anonymous_user_from_record(
160    record: DbRecord,
161    anonymous_field_name: &str,
162) -> Result<AnonymousUser, OpenAuthError> {
163    Ok(AnonymousUser {
164        id: required_string(&record, "id")?.to_owned(),
165        name: required_string(&record, "name")?.to_owned(),
166        email: required_string(&record, "email")?.to_owned(),
167        email_verified: required_bool(&record, "email_verified")?,
168        image: optional_string(&record, "image")?,
169        created_at: required_timestamp(&record, "created_at")?,
170        updated_at: required_timestamp(&record, "updated_at")?,
171        is_anonymous: optional_bool(&record, "is_anonymous")?
172            .or(optional_bool(&record, anonymous_field_name)?)
173            .unwrap_or(false),
174    })
175}
176
177fn required_string<'a>(record: &'a DbRecord, field: &str) -> Result<&'a str, OpenAuthError> {
178    match record.get(field) {
179        Some(DbValue::String(value)) => Ok(value),
180        Some(_) => Err(invalid_field(field, "string")),
181        None => Err(missing_field(field)),
182    }
183}
184
185fn optional_string(record: &DbRecord, field: &str) -> Result<Option<String>, OpenAuthError> {
186    match record.get(field) {
187        Some(DbValue::String(value)) => Ok(Some(value.to_owned())),
188        Some(DbValue::Null) | None => Ok(None),
189        Some(_) => Err(invalid_field(field, "string or null")),
190    }
191}
192
193fn required_bool(record: &DbRecord, field: &str) -> Result<bool, OpenAuthError> {
194    match record.get(field) {
195        Some(DbValue::Boolean(value)) => Ok(*value),
196        Some(_) => Err(invalid_field(field, "boolean")),
197        None => Err(missing_field(field)),
198    }
199}
200
201fn optional_bool(record: &DbRecord, field: &str) -> Result<Option<bool>, OpenAuthError> {
202    match record.get(field) {
203        Some(DbValue::Boolean(value)) => Ok(Some(*value)),
204        Some(DbValue::Null) | None => Ok(None),
205        Some(_) => Err(invalid_field(field, "boolean or null")),
206    }
207}
208
209fn required_timestamp(record: &DbRecord, field: &str) -> Result<OffsetDateTime, OpenAuthError> {
210    match record.get(field) {
211        Some(DbValue::Timestamp(value)) => Ok(*value),
212        Some(_) => Err(invalid_field(field, "timestamp")),
213        None => Err(missing_field(field)),
214    }
215}
216
217fn missing_field(field: &str) -> OpenAuthError {
218    OpenAuthError::Adapter(format!("anonymous user record is missing `{field}`"))
219}
220
221fn invalid_field(field: &str, expected: &str) -> OpenAuthError {
222    OpenAuthError::Adapter(format!(
223        "anonymous user record field `{field}` must be {expected}"
224    ))
225}