Skip to main content

auth_password/
repositories.rs

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/// Token returned by register/login, varying by strategy.
18#[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}