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}