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, disabled_reason, disabled_until)
25 values ($1, $2, null, null, 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 or users.disabled_until <= now())
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
106 from auth.users
107 where id = $1
108 and (disabled_at is null or disabled_until <= now())
109 )
110 "#,
111 )
112 .bind(&user_id.0)
113 .fetch_one(&mut **tx)
114 .await
115 .map_err(map_sql_error)?;
116
117 if !active_user_exists {
118 return Err(AppError::new(ErrorCode::Forbidden, "Auth user is disabled"));
119 }
120
121 sqlx::query(
122 r#"
123 insert into auth.sessions (id, user_id, token_hash, created_at, expires_at, revoked_at)
124 values ($1, $2, $3, $4, $5, null)
125 "#,
126 )
127 .bind(&session_id)
128 .bind(&user_id.0)
129 .bind(session_token_hash(&token))
130 .bind(created_at)
131 .bind(expires_at)
132 .execute(&mut **tx)
133 .await
134 .map_err(map_sql_error)?;
135
136 Ok(AuthSession {
137 id: session_id,
138 user_id: user_id.clone(),
139 token,
140 expires_at,
141 })
142}
143
144type IdentityRow = (String, String);
145
146fn identity_from_row(row: IdentityRow) -> AuthIdentity {
147 let (id, user_id) = row;
148 AuthIdentity {
149 id,
150 user_id: AuthUserId(user_id),
151 }
152}
153
154fn map_sql_error(source: sqlx::Error) -> AppError {
155 if let sqlx::Error::Database(database_error) = &source {
156 if database_error.constraint() == Some("identities_provider_subject_key") {
157 return AppError::new(ErrorCode::Conflict, "An auth identity already exists")
158 .with_source(source);
159 }
160 }
161
162 AppError::new(ErrorCode::Internal, "Internal server error").with_source(source)
163}