use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, PgPool}; use uuid::Uuid; // ── Structs ─────────────────────────────────────────────────────────────────── #[derive(Debug, Serialize, Deserialize, FromRow)] pub struct User { pub id: Uuid, pub email: String, pub password_hash: String, pub full_name: Option, pub phone: Option, pub email_verified: bool, pub phone_verified: bool, pub status: String, // ACTIVE, SUSPENDED, BANNED pub email_verification_token: Option, pub email_verification_expires_at: Option>, pub reset_password_token: Option, pub reset_password_expires_at: Option>, pub created_at: DateTime, pub updated_at: DateTime, pub deleted_at: Option>, } #[derive(Debug, Serialize, Deserialize)] pub struct CreateUserPayload { pub full_name: String, pub email: String, pub phone: Option, pub password_hash: String, } #[derive(Debug, Serialize, Deserialize, FromRow)] pub struct RefreshToken { pub id: Uuid, pub user_id: Uuid, pub token_hash: String, pub expires_at: DateTime, pub revoked: bool, pub created_at: DateTime, } // ── Repository ──────────────────────────────────────────────────────────────── pub struct UserRepository; impl UserRepository { pub async fn create(pool: &PgPool, payload: CreateUserPayload) -> Result { let user = sqlx::query_as::<_, User>( r#" INSERT INTO users (full_name, email, phone, password_hash, email_verified, phone_verified) VALUES ($1, $2, $3, $4, false, false) RETURNING id, email, password_hash, full_name, phone, email_verified, phone_verified, status, email_verification_token, email_verification_expires_at, reset_password_token, reset_password_expires_at, created_at, updated_at, deleted_at "#, ) .bind(payload.full_name) .bind(payload.email.to_lowercase()) .bind(payload.phone) .bind(payload.password_hash) .fetch_one(pool) .await?; Ok(user) } pub async fn get_by_email(pool: &PgPool, email: &str) -> Result { sqlx::query_as::<_, User>( r#" SELECT id, email, password_hash, full_name, phone, email_verified, phone_verified, status, email_verification_token, email_verification_expires_at, reset_password_token, reset_password_expires_at, created_at, updated_at, deleted_at FROM users WHERE email = $1 AND deleted_at IS NULL "#, ) .bind(email.to_lowercase()) .fetch_one(pool) .await } pub async fn get_by_id(pool: &PgPool, id: Uuid) -> Result { sqlx::query_as::<_, User>( r#" SELECT id, email, password_hash, full_name, phone, email_verified, phone_verified, status, email_verification_token, email_verification_expires_at, reset_password_token, reset_password_expires_at, created_at, updated_at, deleted_at FROM users WHERE id = $1 AND deleted_at IS NULL "#, ) .bind(id) .fetch_one(pool) .await } /// Returns all approved role keys for a user (e.g. ["COMPANY", "JOB_SEEKER"]) pub async fn get_user_role_keys(pool: &PgPool, user_id: Uuid) -> Result, sqlx::Error> { let rows = sqlx::query_scalar::<_, String>( r#" SELECT r.key FROM user_roles ur JOIN roles r ON ur.role_id = r.id WHERE ur.user_id = $1 AND ur.status = 'APPROVED' ORDER BY ur.approved_at ASC "#, ) .bind(user_id) .fetch_all(pool) .await?; Ok(rows) } pub async fn set_email_verification_token( pool: &PgPool, user_id: Uuid, token: &str, expires_at: DateTime, ) -> Result<(), sqlx::Error> { sqlx::query( r#" UPDATE users SET email_verification_token = $1, email_verification_expires_at = $2, updated_at = NOW() WHERE id = $3 "#, ) .bind(token) .bind(expires_at) .bind(user_id) .execute(pool) .await?; Ok(()) } pub async fn get_by_verification_token(pool: &PgPool, token: &str) -> Result { sqlx::query_as::<_, User>( r#" SELECT id, email, password_hash, full_name, phone, email_verified, phone_verified, status, email_verification_token, email_verification_expires_at, reset_password_token, reset_password_expires_at, created_at, updated_at, deleted_at FROM users WHERE email_verification_token = $1 AND deleted_at IS NULL "#, ) .bind(token) .fetch_one(pool) .await } pub async fn set_email_verified(pool: &PgPool, user_id: Uuid) -> Result<(), sqlx::Error> { sqlx::query( "UPDATE users SET email_verified = true, email_verification_token = NULL, email_verification_expires_at = NULL, updated_at = NOW() WHERE id = $1", ) .bind(user_id) .execute(pool) .await?; Ok(()) } pub async fn set_reset_token( pool: &PgPool, user_id: Uuid, token: &str, expires_at: DateTime, ) -> Result<(), sqlx::Error> { sqlx::query( r#" UPDATE users SET reset_password_token = $1, reset_password_expires_at = $2, updated_at = NOW() WHERE id = $3 "#, ) .bind(token) .bind(expires_at) .bind(user_id) .execute(pool) .await?; Ok(()) } pub async fn get_by_reset_token(pool: &PgPool, token: &str) -> Result { sqlx::query_as::<_, User>( r#" SELECT id, email, password_hash, full_name, phone, email_verified, phone_verified, status, email_verification_token, email_verification_expires_at, reset_password_token, reset_password_expires_at, created_at, updated_at, deleted_at FROM users WHERE reset_password_token = $1 AND deleted_at IS NULL "#, ) .bind(token) .fetch_one(pool) .await } pub async fn clear_reset_token(pool: &PgPool, user_id: Uuid) -> Result<(), sqlx::Error> { sqlx::query( "UPDATE users SET reset_password_token = NULL, reset_password_expires_at = NULL, updated_at = NOW() WHERE id = $1", ) .bind(user_id) .execute(pool) .await?; Ok(()) } pub async fn update_password(pool: &PgPool, user_id: Uuid, password_hash: &str) -> Result<(), sqlx::Error> { sqlx::query( "UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2", ) .bind(password_hash) .bind(user_id) .execute(pool) .await?; Ok(()) } pub async fn update_status(pool: &PgPool, user_id: Uuid, status: &str) -> Result<(), sqlx::Error> { sqlx::query( "UPDATE users SET status = $1, updated_at = NOW() WHERE id = $2", ) .bind(status) .bind(user_id) .execute(pool) .await?; Ok(()) } pub async fn store_refresh_token( pool: &PgPool, user_id: Uuid, token_hash: &str, expires_at: DateTime, ) -> Result { sqlx::query_as::<_, RefreshToken>( r#" INSERT INTO refresh_tokens (user_id, token_hash, expires_at) VALUES ($1, $2, $3) RETURNING id, user_id, token_hash, expires_at, revoked, created_at "#, ) .bind(user_id) .bind(token_hash) .bind(expires_at) .fetch_one(pool) .await } pub async fn get_valid_refresh_token( pool: &PgPool, token_hash: &str, ) -> Result { sqlx::query_as::<_, RefreshToken>( r#" SELECT id, user_id, token_hash, expires_at, revoked, created_at FROM refresh_tokens WHERE token_hash = $1 AND revoked = false AND expires_at > NOW() "#, ) .bind(token_hash) .fetch_one(pool) .await } pub async fn revoke_refresh_token(pool: &PgPool, token_hash: &str) -> Result<(), sqlx::Error> { sqlx::query( "UPDATE refresh_tokens SET revoked = true WHERE token_hash = $1", ) .bind(token_hash) .execute(pool) .await?; Ok(()) } pub async fn revoke_all_for_user(pool: &PgPool, user_id: Uuid) -> Result<(), sqlx::Error> { sqlx::query( "UPDATE refresh_tokens SET revoked = true WHERE user_id = $1", ) .bind(user_id) .execute(pool) .await?; Ok(()) } }