use anyhow::{Context, Result};
use chrono::Utc;
use sea_orm::*;
use uuid::Uuid;
use crate::auth::{password, tokens};
use crate::entities::{user_sessions, users};
#[derive(Debug)]
pub struct CreateUserRequest {
pub username: String,
pub email: String,
pub password: String,
pub registration_token: String,
}
#[derive(Debug)]
pub struct LoginRequest {
pub username: String,
pub password: String,
}
/// Create a user after validating and consuming their invite token.
pub async fn create_user(db: &DatabaseConnection, request: CreateUserRequest) -> Result<users::Model> {
let reg_token = tokens::validate_registration_token(db, &request.registration_token, &request.email)
.await
.context("failed to validate registration token")?
.ok_or_else(|| anyhow::anyhow!("invalid or expired registration token"))?;
let existing = users::Entity::find()
.filter(
Condition::any()
.add(users::Column::Username.eq(&request.username))
.add(users::Column::Email.eq(&request.email)),
)
.one(db)
.await
.context("failed to check existing users")?;
if existing.is_some() {
return Err(anyhow::anyhow!("username or email already exists"));
}
let password_hash = password::hash_password(&request.password)?;
let now = Utc::now();
let user = users::ActiveModel {
id: Set(Uuid::now_v7()),
username: Set(request.username),
email: Set(request.email),
password_hash: Set(password_hash),
is_active: Set(true),
created_at: Set(now.into()),
updated_at: Set(now.into()),
last_login_at: Set(None),
}
.insert(db)
.await
.context("failed to create user")?;
tokens::mark_registration_token_used(db, reg_token.id, user.id)
.await
.context("failed to mark registration token used")?;
Ok(user)
}
/// Authenticate by username + password. Returns `Ok(None)` on bad credentials.
pub async fn authenticate_user(
db: &DatabaseConnection,
request: LoginRequest,
) -> Result<Option<users::Model>> {
let user = users::Entity::find()
.filter(users::Column::Username.eq(&request.username))
.filter(users::Column::IsActive.eq(true))
.one(db)
.await
.context("failed to query user")?;
let Some(user) = user else { return Ok(None) };
if !password::verify_password(&request.password, &user.password_hash)? {
return Ok(None);
}
let mut update: users::ActiveModel = user.into();
update.last_login_at = Set(Some(Utc::now().into()));
let user = update.update(db).await.context("failed to update last login")?;
Ok(Some(user))
}
pub async fn get_user_by_id(db: &DatabaseConnection, user_id: Uuid) -> Result<Option<users::Model>> {
users::Entity::find_by_id(user_id).one(db).await.context("failed to query user")
}
/// Update username and/or email, enforcing uniqueness.
pub async fn update_user(
db: &DatabaseConnection,
user_id: Uuid,
username: Option<String>,
email: Option<String>,
) -> Result<users::Model> {
let user = get_user_by_id(db, user_id)
.await?
.ok_or_else(|| anyhow::anyhow!("user not found"))?;
let mut update: users::ActiveModel = user.into();
update.updated_at = Set(Utc::now().into());
if let Some(username) = username {
let taken = users::Entity::find()
.filter(users::Column::Username.eq(&username))
.filter(users::Column::Id.ne(user_id))
.one(db)
.await
.context("failed to check username")?;
if taken.is_some() {
return Err(anyhow::anyhow!("username already taken"));
}
update.username = Set(username);
}
if let Some(email) = email {
let taken = users::Entity::find()
.filter(users::Column::Email.eq(&email))
.filter(users::Column::Id.ne(user_id))
.one(db)
.await
.context("failed to check email")?;
if taken.is_some() {
return Err(anyhow::anyhow!("email already taken"));
}
update.email = Set(email);
}
update.update(db).await.context("failed to update user")
}
/// Change a user's password after verifying the current one.
pub async fn change_password(
db: &DatabaseConnection,
user_id: Uuid,
old_password: &str,
new_password: &str,
) -> Result<()> {
let user = get_user_by_id(db, user_id)
.await?
.ok_or_else(|| anyhow::anyhow!("user not found"))?;
if !password::verify_password(old_password, &user.password_hash)? {
return Err(anyhow::anyhow!("invalid old password"));
}
let new_hash = password::hash_password(new_password)?;
let mut update: users::ActiveModel = user.into();
update.password_hash = Set(new_hash);
update.updated_at = Set(Utc::now().into());
update.update(db).await.context("failed to update password")?;
Ok(())
}
/// Deactivate an account and drop all of its sessions.
pub async fn deactivate_user(db: &DatabaseConnection, user_id: Uuid) -> Result<()> {
let user = get_user_by_id(db, user_id)
.await?
.ok_or_else(|| anyhow::anyhow!("user not found"))?;
let mut update: users::ActiveModel = user.into();
update.is_active = Set(false);
update.updated_at = Set(Utc::now().into());
update.update(db).await.context("failed to deactivate user")?;
user_sessions::Entity::delete_many()
.filter(user_sessions::Column::UserId.eq(user_id))
.exec(db)
.await
.context("failed to delete sessions")?;
Ok(())
}
neiam
/fieri-forever
the flavortown lunch engine
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
5.6 KiB · text
History
7a00c02