100 lines
3.5 KiB
Rust
100 lines
3.5 KiB
Rust
//! SQL query methods for the `User` entity.
|
|
//!
|
|
//! These methods operate on the database directly, returning `sqlx::Error`.
|
|
//! The service layer is responsible for converting errors into `AppError`.
|
|
|
|
use sqlx::PgPool;
|
|
use uuid::Uuid;
|
|
|
|
use super::user::User;
|
|
|
|
impl User {
|
|
/// Find an active, non-deleted user by primary key.
|
|
pub async fn find_by_id(pool: &PgPool, id: Uuid) -> Result<Option<Self>, sqlx::Error> {
|
|
sqlx::query_as::<_, User>(
|
|
r#"SELECT id, username, display_name, avatar_url, bio, status, role, visibility,
|
|
is_active, is_bot, last_login_at, created_at, updated_at, deleted_at
|
|
FROM "user"
|
|
WHERE id = $1 AND is_active = true AND status = 'active' AND deleted_at IS NULL"#,
|
|
)
|
|
.bind(id)
|
|
.fetch_optional(pool)
|
|
.await
|
|
}
|
|
|
|
/// Find an active user by username (case-insensitive).
|
|
pub async fn find_by_username(
|
|
pool: &PgPool,
|
|
username: &str,
|
|
) -> Result<Option<Self>, sqlx::Error> {
|
|
sqlx::query_as::<_, User>(
|
|
r#"SELECT id, username, display_name, avatar_url, bio, status, role, visibility,
|
|
is_active, is_bot, last_login_at, created_at, updated_at, deleted_at
|
|
FROM "user"
|
|
WHERE lower(username) = lower($1)
|
|
AND is_active = true AND status = 'active' AND deleted_at IS NULL"#,
|
|
)
|
|
.bind(username)
|
|
.fetch_optional(pool)
|
|
.await
|
|
}
|
|
|
|
/// Find an active user by verified email (case-insensitive).
|
|
pub async fn find_by_email(pool: &PgPool, email: &str) -> Result<Option<Self>, sqlx::Error> {
|
|
sqlx::query_as::<_, User>(
|
|
r#"SELECT u.id, u.username, u.display_name, u.avatar_url, u.bio, u.status, u.role,
|
|
u.visibility, u.is_active, u.is_bot, u.last_login_at,
|
|
u.created_at, u.updated_at, u.deleted_at
|
|
FROM "user" u
|
|
INNER JOIN user_mail e ON e.user_id = u.id
|
|
WHERE lower(e.email) = lower($1)
|
|
AND e.is_verified = true
|
|
AND u.is_active = true AND u.status = 'active' AND u.deleted_at IS NULL"#,
|
|
)
|
|
.bind(email)
|
|
.fetch_optional(pool)
|
|
.await
|
|
}
|
|
|
|
/// Check if a username already exists (excluding a given user id).
|
|
pub async fn username_exists(
|
|
pool: &PgPool,
|
|
username: &str,
|
|
exclude_id: Option<Uuid>,
|
|
) -> Result<bool, sqlx::Error> {
|
|
let exists = if let Some(id) = exclude_id {
|
|
sqlx::query_scalar::<_, bool>(
|
|
r#"SELECT EXISTS(
|
|
SELECT 1 FROM "user"
|
|
WHERE lower(username) = lower($1) AND id <> $2 AND deleted_at IS NULL
|
|
)"#,
|
|
)
|
|
.bind(username)
|
|
.bind(id)
|
|
.fetch_one(pool)
|
|
.await?
|
|
} else {
|
|
sqlx::query_scalar::<_, bool>(
|
|
r#"SELECT EXISTS(
|
|
SELECT 1 FROM "user"
|
|
WHERE lower(username) = lower($1) AND deleted_at IS NULL
|
|
)"#,
|
|
)
|
|
.bind(username)
|
|
.fetch_one(pool)
|
|
.await?
|
|
};
|
|
Ok(exists)
|
|
}
|
|
|
|
/// Update the last_login_at timestamp.
|
|
pub async fn touch_login(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Error> {
|
|
sqlx::query(r#"UPDATE "user" SET last_login_at = $1, updated_at = $1 WHERE id = $2"#)
|
|
.bind(chrono::Utc::now())
|
|
.bind(id)
|
|
.execute(pool)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
}
|