nxtgauge-backend-rust/crates/db/src/models/user.rs

302 lines
9.6 KiB
Rust

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<String>,
pub phone: Option<String>,
pub email_verified: bool,
pub phone_verified: bool,
pub status: String, // ACTIVE, SUSPENDED, BANNED
pub email_verification_token: Option<String>,
pub email_verification_expires_at: Option<DateTime<Utc>>,
pub reset_password_token: Option<String>,
pub reset_password_expires_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub deleted_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateUserPayload {
pub full_name: String,
pub email: String,
pub phone: Option<String>,
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<Utc>,
pub revoked: bool,
pub created_at: DateTime<Utc>,
}
// ── Repository ────────────────────────────────────────────────────────────────
pub struct UserRepository;
impl UserRepository {
pub async fn create(pool: &PgPool, payload: CreateUserPayload) -> Result<User, sqlx::Error> {
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<User, sqlx::Error> {
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<User, sqlx::Error> {
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<Vec<String>, 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<Utc>,
) -> 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<User, sqlx::Error> {
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<Utc>,
) -> 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<User, sqlx::Error> {
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<Utc>,
) -> Result<RefreshToken, sqlx::Error> {
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<RefreshToken, sqlx::Error> {
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(())
}
}