Skip to main content

auth/
public.rs

1use crate::resolver::session_token_hash;
2use chrono::{DateTime, Utc};
3use platform_core::{AppError, AppResult, DbPool, ErrorCode};
4use sqlx::{Postgres, Transaction};
5
6pub use crate::models::{AuthSession, AuthUserId};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct AuthIdentity {
10    pub id: String,
11    pub user_id: AuthUserId,
12}
13
14pub async fn create_user_identity_in_tx(
15    tx: &mut Transaction<'_, Postgres>,
16    user_id: AuthUserId,
17    identity_id: String,
18    provider: &str,
19    provider_subject: &str,
20    created_at: DateTime<Utc>,
21) -> AppResult<AuthIdentity> {
22    sqlx::query(
23        r#"
24        insert into auth.users (id, created_at, disabled_at)
25        values ($1, $2, null)
26        "#,
27    )
28    .bind(&user_id.0)
29    .bind(created_at)
30    .execute(&mut **tx)
31    .await
32    .map_err(map_sql_error)?;
33
34    sqlx::query(
35        r#"
36        insert into auth.identities (id, user_id, provider, provider_subject, created_at, updated_at)
37        values ($1, $2, $3, $4, $5, $5)
38        "#,
39    )
40    .bind(&identity_id)
41    .bind(&user_id.0)
42    .bind(provider)
43    .bind(provider_subject)
44    .bind(created_at)
45    .execute(&mut **tx)
46    .await
47    .map_err(map_sql_error)?;
48
49    Ok(AuthIdentity {
50        id: identity_id,
51        user_id,
52    })
53}
54
55pub async fn find_active_identity(
56    pool: &DbPool,
57    provider: &str,
58    provider_subject: &str,
59) -> AppResult<Option<AuthIdentity>> {
60    sqlx::query_as::<_, IdentityRow>(
61        r#"
62        select identities.id, identities.user_id
63        from auth.identities identities
64        join auth.users users on users.id = identities.user_id
65        where identities.provider = $1
66          and identities.provider_subject = $2
67          and users.disabled_at is null
68        limit 1
69        "#,
70    )
71    .bind(provider)
72    .bind(provider_subject)
73    .fetch_optional(pool)
74    .await
75    .map(|row| row.map(identity_from_row))
76    .map_err(map_sql_error)
77}
78
79pub async fn create_session(
80    pool: &DbPool,
81    user_id: &AuthUserId,
82    session_id: String,
83    token: String,
84    created_at: DateTime<Utc>,
85    expires_at: DateTime<Utc>,
86) -> AppResult<AuthSession> {
87    let mut tx = pool.begin().await.map_err(map_sql_error)?;
88    let session =
89        create_session_in_tx(&mut tx, user_id, session_id, token, created_at, expires_at).await?;
90    tx.commit().await.map_err(map_sql_error)?;
91    Ok(session)
92}
93
94pub async fn create_session_in_tx(
95    tx: &mut Transaction<'_, Postgres>,
96    user_id: &AuthUserId,
97    session_id: String,
98    token: String,
99    created_at: DateTime<Utc>,
100    expires_at: DateTime<Utc>,
101) -> AppResult<AuthSession> {
102    let active_user_exists = sqlx::query_scalar::<_, bool>(
103        r#"
104        select exists(
105            select 1 from auth.users where id = $1 and disabled_at is null
106        )
107        "#,
108    )
109    .bind(&user_id.0)
110    .fetch_one(&mut **tx)
111    .await
112    .map_err(map_sql_error)?;
113
114    if !active_user_exists {
115        return Err(AppError::new(ErrorCode::Forbidden, "Auth user is disabled"));
116    }
117
118    sqlx::query(
119        r#"
120        insert into auth.sessions (id, user_id, token_hash, created_at, expires_at, revoked_at)
121        values ($1, $2, $3, $4, $5, null)
122        "#,
123    )
124    .bind(&session_id)
125    .bind(&user_id.0)
126    .bind(session_token_hash(&token))
127    .bind(created_at)
128    .bind(expires_at)
129    .execute(&mut **tx)
130    .await
131    .map_err(map_sql_error)?;
132
133    Ok(AuthSession {
134        id: session_id,
135        user_id: user_id.clone(),
136        token,
137        expires_at,
138    })
139}
140
141type IdentityRow = (String, String);
142
143fn identity_from_row(row: IdentityRow) -> AuthIdentity {
144    let (id, user_id) = row;
145    AuthIdentity {
146        id,
147        user_id: AuthUserId(user_id),
148    }
149}
150
151fn map_sql_error(source: sqlx::Error) -> AppError {
152    if let sqlx::Error::Database(database_error) = &source {
153        if database_error.constraint() == Some("identities_provider_subject_key") {
154            return AppError::new(ErrorCode::Conflict, "An auth identity already exists")
155                .with_source(source);
156        }
157    }
158
159    AppError::new(ErrorCode::Internal, "Internal server error").with_source(source)
160}