1use crate::config::{AuthPasswordConfig, TokenStrategy};
2use crate::jwt;
3use crate::password::{
4 hash_password, new_session_token, normalize_identifier, validate_password, verify_password,
5};
6use auth::public::{self, AuthSession, AuthUserId};
7use chrono::{DateTime, Utc};
8use platform_core::{AppError, AppResult, DbPool, ErrorCode};
9
10const PASSWORD_PROVIDER: &str = "password";
11
12#[derive(Debug, Clone)]
13pub struct PasswordAuthRepository {
14 pool: DbPool,
15}
16
17#[derive(Debug)]
19pub enum AuthToken {
20 Session(AuthSession),
21 Jwt {
22 user_id: String,
23 token: String,
24 expires_at: DateTime<Utc>,
25 },
26}
27
28impl PasswordAuthRepository {
29 #[must_use]
30 pub fn new(pool: DbPool) -> Self {
31 Self { pool }
32 }
33
34 pub async fn register(
35 &self,
36 identifier: &str,
37 password: &str,
38 user_id: String,
39 identity_id: String,
40 session_id: String,
41 now: DateTime<Utc>,
42 expires_at: DateTime<Utc>,
43 config: &AuthPasswordConfig,
44 ) -> AppResult<AuthToken> {
45 let normalized_identifier = normalize_identifier(identifier)?;
46 validate_password(password)?;
47 let password_hash = hash_password(password, config)?;
48
49 match config.token_strategy {
50 TokenStrategy::Session => {
51 let token = new_session_token();
52 let mut tx = self.pool.begin().await.map_err(map_sql_error)?;
53 let identity = public::create_user_identity_in_tx(
54 &mut tx,
55 AuthUserId(user_id),
56 identity_id,
57 PASSWORD_PROVIDER,
58 &normalized_identifier,
59 now,
60 )
61 .await?;
62
63 sqlx::query(
64 r#"
65 insert into auth_password.credentials (identity_id, password_hash, created_at, updated_at)
66 values ($1, $2, $3, $3)
67 "#,
68 )
69 .bind(&identity.id)
70 .bind(password_hash)
71 .bind(now)
72 .execute(&mut *tx)
73 .await
74 .map_err(map_sql_error)?;
75
76 let session = public::create_session_in_tx(
77 &mut tx,
78 &identity.user_id,
79 session_id,
80 token,
81 now,
82 expires_at,
83 )
84 .await?;
85
86 tx.commit().await.map_err(map_sql_error)?;
87 Ok(AuthToken::Session(session))
88 }
89 TokenStrategy::Jwt => {
90 let jwt_config = config.jwt_config()?.ok_or_else(|| {
91 AppError::new(ErrorCode::Internal, "JWT configuration is required")
92 })?;
93 let mut tx = self.pool.begin().await.map_err(map_sql_error)?;
94 let identity = public::create_user_identity_in_tx(
95 &mut tx,
96 AuthUserId(user_id),
97 identity_id,
98 PASSWORD_PROVIDER,
99 &normalized_identifier,
100 now,
101 )
102 .await?;
103
104 sqlx::query(
105 r#"
106 insert into auth_password.credentials (identity_id, password_hash, created_at, updated_at)
107 values ($1, $2, $3, $3)
108 "#,
109 )
110 .bind(&identity.id)
111 .bind(password_hash)
112 .bind(now)
113 .execute(&mut *tx)
114 .await
115 .map_err(map_sql_error)?;
116
117 tx.commit().await.map_err(map_sql_error)?;
118
119 let user_id_str = identity.user_id.0.clone();
120 let token = jwt::create_token(&user_id_str, &jwt_config, now);
121
122 Ok(AuthToken::Jwt {
123 user_id: user_id_str,
124 token,
125 expires_at,
126 })
127 }
128 }
129 }
130
131 pub async fn login(
132 &self,
133 identifier: &str,
134 password: &str,
135 session_id: String,
136 now: DateTime<Utc>,
137 expires_at: DateTime<Utc>,
138 config: &AuthPasswordConfig,
139 ) -> AppResult<AuthToken> {
140 let normalized_identifier = normalize_identifier(identifier)?;
141 validate_password(password)?;
142
143 let Some(identity) =
144 public::find_active_identity(&self.pool, PASSWORD_PROVIDER, &normalized_identifier)
145 .await?
146 else {
147 return Err(invalid_credentials());
148 };
149
150 let Some(password_hash) = sqlx::query_scalar::<_, String>(
151 r#"
152 select password_hash
153 from auth_password.credentials
154 where identity_id = $1
155 "#,
156 )
157 .bind(&identity.id)
158 .fetch_optional(&self.pool)
159 .await
160 .map_err(map_sql_error)?
161 else {
162 return Err(invalid_credentials());
163 };
164
165 if !verify_password(&password_hash, password)? {
166 return Err(invalid_credentials());
167 }
168
169 match config.token_strategy {
170 TokenStrategy::Session => {
171 let session = public::create_session(
172 &self.pool,
173 &identity.user_id,
174 session_id,
175 new_session_token(),
176 now,
177 expires_at,
178 )
179 .await?;
180 Ok(AuthToken::Session(session))
181 }
182 TokenStrategy::Jwt => {
183 let jwt_config = config.jwt_config()?.ok_or_else(|| {
184 AppError::new(ErrorCode::Internal, "JWT configuration is required")
185 })?;
186 let user_id_str = identity.user_id.0.clone();
187 let token = jwt::create_token(&user_id_str, &jwt_config, now);
188 Ok(AuthToken::Jwt {
189 user_id: user_id_str,
190 token,
191 expires_at,
192 })
193 }
194 }
195 }
196}
197
198fn invalid_credentials() -> AppError {
199 AppError::new(ErrorCode::Unauthorized, "Invalid identifier or password")
200}
201
202fn map_sql_error(source: sqlx::Error) -> AppError {
203 AppError::new(ErrorCode::Internal, "Internal server error").with_source(source)
204}