use axum_login::AuthUser;
use serde::{Deserialize, Serialize};
use sqlx::{Result, SqlitePool};
use time::OffsetDateTime;
use super::Model;
#[derive(Clone, Serialize, Deserialize)]
pub struct User {
pub user_id: i64,
pub username: String,
pub password_hash: String,
pub created_at: OffsetDateTime,
}
impl std::fmt::Debug for User {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("User")
.field("user_id", &self.user_id)
.field("username", &self.username)
.field("password_hash", &"[redacted]")
.field("created_at", &self.created_at)
.finish()
}
}
impl AuthUser for User {
type Id = i64;
fn id(&self) -> Self::Id {
self.user_id
}
fn session_auth_hash(&self) -> &[u8] {
self.password_hash.as_bytes() }
}
#[allow(dead_code)]
pub struct Default {
pub user_id: i64,
}
impl Default {
#[allow(clippy::unused_self)]
pub fn created_at(&self) -> OffsetDateTime {
OffsetDateTime::now_utc()
}
}
impl Model for User {
type Default = Default;
const DEFAULT: Default = Default { user_id: -1 };
async fn create_using_self(&mut self, pool: &SqlitePool) -> Result<()> {
let result = sqlx::query!(
r#"
INSERT INTO users (username, password_hash, created_at)
VALUES (?, ?, ?)
RETURNING user_id
"#,
self.username,
self.password_hash,
self.created_at
)
.fetch_one(pool)
.await?;
self.user_id = result.user_id;
Ok(())
}
async fn get_using_id(pool: &SqlitePool, id: i64) -> Result<Self> {
sqlx::query_as!(
User,
r#"
SELECT *
FROM users
WHERE user_id = ?
"#,
id
)
.fetch_one(pool)
.await
}
async fn update_using_self(&self, pool: &SqlitePool) -> Result<()> {
sqlx::query!(
r#"
UPDATE users
SET username = ?, password_hash = ?, created_at = ?
WHERE user_id = ?
RETURNING user_id
"#,
self.username,
self.password_hash,
self.created_at,
self.user_id
)
.fetch_one(pool)
.await?;
Ok(())
}
async fn delete_using_id(pool: &SqlitePool, id: i64) -> Result<()> {
sqlx::query!(
r#"
DELETE
FROM users
WHERE user_id = ?
RETURNING user_id
"#,
id
)
.fetch_one(pool)
.await?;
Ok(())
}
}
impl User {
pub async fn get_using_username(pool: &SqlitePool, username: &str) -> Result<Self> {
sqlx::query_as!(
User,
r#"
SELECT *
FROM users
WHERE username = ?
"#,
username
)
.fetch_one(pool)
.await
}
pub async fn get_all(pool: &SqlitePool) -> Result<Vec<Self>> {
sqlx::query_as!(
User,
r#"
SELECT *
FROM users
"#,
)
.fetch_all(pool)
.await
}
#[must_use]
pub fn to_redacted_clone(&self) -> Self {
Self {
user_id: self.user_id,
username: self.username.clone(),
password_hash: "[redacted]".to_string(),
created_at: self.created_at,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
async fn create(pool: SqlitePool) -> Result<()> {
let mut user = User {
user_id: User::DEFAULT.user_id,
username: "test_user".to_string(),
password_hash: "test_hash".to_string(),
created_at: User::DEFAULT.created_at(),
};
user.create_using_self(&pool).await?;
assert_eq!(user.user_id, 5);
let returned_user = User::get_using_id(&pool, 5).await?;
assert_eq!(returned_user.username, user.username);
assert_eq!(returned_user.password_hash, user.password_hash);
assert_eq!(returned_user.created_at, user.created_at);
Ok(())
}
#[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
async fn create_existing(pool: SqlitePool) -> Result<()> {
let mut user = User {
user_id: User::DEFAULT.user_id,
username: "piotrpdev".to_string(),
password_hash: "test_hash".to_string(),
created_at: User::DEFAULT.created_at(),
};
let returned_user_result = user.create_using_self(&pool).await;
assert!(returned_user_result.is_err());
Ok(())
}
#[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
async fn get(pool: SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
let user_id = 2;
let returned_user = User::get_using_id(&pool, user_id).await?;
assert_eq!(returned_user.user_id, user_id);
assert_eq!(returned_user.username, "piotrpdev");
assert_eq!(returned_user.password_hash, "$argon2id$v=19$m=19456,t=2,p=1$VE0e3g7DalWHgDwou3nuRA$uC6TER156UQpk0lNQ5+jHM0l5poVjPA1he/Tyn9J4Zw");
assert_eq!(
returned_user.created_at,
OffsetDateTime::from_unix_timestamp(1_729_530_138)?
);
Ok(())
}
#[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
async fn get_using_username(pool: SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
let username = "piotrpdev";
let returned_user = User::get_using_username(&pool, username).await?;
assert_eq!(returned_user.user_id, 2);
assert_eq!(returned_user.username, username);
assert_eq!(returned_user.password_hash, "$argon2id$v=19$m=19456,t=2,p=1$VE0e3g7DalWHgDwou3nuRA$uC6TER156UQpk0lNQ5+jHM0l5poVjPA1he/Tyn9J4Zw");
assert_eq!(
returned_user.created_at,
OffsetDateTime::from_unix_timestamp(1_729_530_138)?
);
Ok(())
}
#[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
async fn get_all(pool: SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
let usernames = ["admin", "piotrpdev", "joedaly", "guest"];
let returned_users = User::get_all(&pool).await?;
assert_eq!(returned_users.len(), 4);
assert!(returned_users
.iter()
.all(|user| usernames.contains(&user.username.as_str())));
Ok(())
}
#[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
async fn update(pool: SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
let old_user = User::get_using_id(&pool, 2).await?;
let updated_user = User {
user_id: old_user.user_id,
username: "new_joedaly".to_string(),
password_hash: old_user.password_hash,
created_at: OffsetDateTime::from_unix_timestamp(1_729_530_138)?,
};
let updated = updated_user.update_using_self(&pool).await;
assert!(updated.is_ok());
let returned_user = User::get_using_id(&pool, old_user.user_id).await?;
assert_eq!(returned_user.user_id, updated_user.user_id);
assert_eq!(returned_user.username, updated_user.username);
assert_eq!(returned_user.password_hash, updated_user.password_hash);
assert_eq!(returned_user.created_at, updated_user.created_at);
Ok(())
}
#[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
async fn delete(pool: SqlitePool) -> Result<()> {
let user_id = 2;
let deleted = User::delete_using_id(&pool, user_id).await;
assert!(deleted.is_ok());
let returned_user = User::get_using_id(&pool, user_id).await;
assert!(returned_user.is_err());
let impossible_deleted = User::delete_using_id(&pool, user_id).await;
assert!(impossible_deleted.is_err());
Ok(())
}
#[sqlx::test(fixtures(path = "../../fixtures", scripts("users")))]
async fn to_redacted_clone(pool: SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
let user_id = 2;
let returned_user = User::get_using_id(&pool, user_id).await?;
let redacted_user = returned_user.to_redacted_clone();
assert_eq!(redacted_user.user_id, user_id);
assert_eq!(redacted_user.username, "piotrpdev");
assert_eq!(redacted_user.password_hash, "[redacted]");
assert_eq!(
redacted_user.created_at,
OffsetDateTime::from_unix_timestamp(1_729_530_138)?
);
Ok(())
}
}