Skip to main content

rustauth_core/api/
output.rs

1//! Shared API output helpers for core routes and server-side plugins.
2
3use serde::Serialize;
4use serde_json::Value;
5use time::OffsetDateTime;
6
7use crate::api::additional_fields::{db_value_to_json, insert_returned_fields_http};
8use crate::api::http_json::{session_to_http_value, snake_to_camel, user_to_http_value};
9use crate::context::AuthContext;
10use crate::cookies::{
11    set_cookie_cache, set_session_cookie, Cookie, CookieCachePayload, CookieOptions,
12    SessionCookieOptions,
13};
14use crate::db::{filter_output_fields, DbAdapter, DbRecord, DbValue, FindOne, Session, User};
15use crate::error::RustAuthError;
16
17#[derive(Debug, Serialize)]
18pub struct SessionUserOutput {
19    pub session: Value,
20    pub user: Value,
21}
22
23pub async fn session_user_output(
24    adapter: &dyn DbAdapter,
25    context: &AuthContext,
26    session: &Session,
27    user: &User,
28) -> Result<SessionUserOutput, RustAuthError> {
29    Ok(SessionUserOutput {
30        session: session_output_value(adapter, context, session).await?,
31        user: user_output_value(adapter, context, user).await?,
32    })
33}
34
35/// Render a user output value from request-supplied additional fields instead
36/// of loading them from storage. Used for synthetic duplicate sign-up
37/// responses so the payload mirrors a real sign-up without leaking persisted
38/// account data.
39pub fn user_output_value_from_fields(
40    context: &AuthContext,
41    user: &User,
42    additional_fields: &DbRecord,
43) -> Result<Value, RustAuthError> {
44    let mut value = user_to_http_value(user)?;
45    if context.options.user.additional_fields.is_empty() {
46        return Ok(value);
47    }
48    let Some(object) = value.as_object_mut() else {
49        return Err(RustAuthError::Serialization {
50            context: "serializing user output",
51            message: "expected JSON object".to_owned(),
52        });
53    };
54    insert_returned_fields_http(
55        object,
56        &context.options.user.additional_fields,
57        additional_fields,
58    )?;
59    Ok(value)
60}
61
62pub async fn user_output_value(
63    adapter: &dyn DbAdapter,
64    context: &AuthContext,
65    user: &User,
66) -> Result<Value, RustAuthError> {
67    let users = context.schema().table("user")?;
68    let record = adapter
69        .find_one(
70            FindOne::new(users.model())
71                .where_clause(users.where_eq("id", DbValue::String(user.id.clone()))?),
72        )
73        .await?
74        .map(|record| users.map_record(record))
75        .transpose()?;
76    let mut value = user_to_http_value(user)?;
77    let Some(object) = value.as_object_mut() else {
78        return Err(RustAuthError::Serialization {
79            context: "serializing user output",
80            message: "expected JSON object".to_owned(),
81        });
82    };
83    if let Some(record) = record {
84        insert_returned_fields_http(object, &context.options.user.additional_fields, &record)?;
85        insert_schema_returned_fields(context, "user", object, &record)?;
86    }
87    Ok(value)
88}
89
90pub async fn session_output_value(
91    adapter: &dyn DbAdapter,
92    context: &AuthContext,
93    session: &Session,
94) -> Result<Value, RustAuthError> {
95    let record =
96        if let Some(sessions) = context.schema().try_table("session") {
97            adapter
98                .find_one(FindOne::new(sessions.model()).where_clause(
99                    sessions.where_eq("token", DbValue::String(session.token.clone()))?,
100                ))
101                .await?
102                .map(|record| sessions.map_record(record))
103                .transpose()?
104        } else {
105            None
106        };
107    match record {
108        Some(record) => session_value_from_record(context, &record, session),
109        None => session_to_http_value(session),
110    }
111}
112
113pub fn session_value_from_record(
114    context: &AuthContext,
115    record: &DbRecord,
116    session: &Session,
117) -> Result<Value, RustAuthError> {
118    let mut value = session_to_http_value(session)?;
119    let Some(object) = value.as_object_mut() else {
120        return Err(RustAuthError::Serialization {
121            context: "serializing session output",
122            message: "expected JSON object".to_owned(),
123        });
124    };
125    insert_returned_fields_http(object, &context.options.session.additional_fields, record)?;
126    insert_schema_returned_fields(context, "session", object, record)?;
127    Ok(value)
128}
129
130fn insert_schema_returned_fields(
131    context: &AuthContext,
132    table: &str,
133    object: &mut serde_json::Map<String, Value>,
134    record: &DbRecord,
135) -> Result<(), RustAuthError> {
136    let Some(table) = context.db_schema.table(table) else {
137        return Ok(());
138    };
139    for (logical_name, value) in filter_output_fields(record, &table.fields) {
140        let http_key = snake_to_camel(&logical_name);
141        if object.contains_key(&http_key) || object.contains_key(&logical_name) {
142            continue;
143        }
144        object.insert(http_key, db_value_to_json(&value)?);
145    }
146    Ok(())
147}
148
149pub fn session_response_cookies(
150    context: &AuthContext,
151    session: &Session,
152    user: &User,
153    dont_remember: bool,
154) -> Result<Vec<Cookie>, RustAuthError> {
155    let mut cookies = set_session_cookie(
156        &context.auth_cookies,
157        &context.secret,
158        &session.token,
159        SessionCookieOptions {
160            dont_remember,
161            overrides: CookieOptions::default(),
162        },
163    )?;
164    if context.options.session.cookie_cache.enabled {
165        let max_age = context
166            .options
167            .session
168            .cookie_cache
169            .max_age
170            .unwrap_or(time::Duration::minutes(5));
171        cookies.extend(set_cookie_cache(
172            &context.auth_cookies,
173            &context.secret,
174            &CookieCachePayload {
175                session: session.clone(),
176                user: user.clone(),
177                updated_at: OffsetDateTime::now_utc().unix_timestamp(),
178                version: context
179                    .options
180                    .session
181                    .cookie_cache
182                    .version
183                    .clone()
184                    .unwrap_or_else(|| "1".to_owned()),
185            },
186            context.options.session.cookie_cache.strategy,
187            max_age.whole_seconds() as u64,
188        )?);
189    }
190    Ok(cookies)
191}