use ocre::{Ctx, Error, Query, Result, Validator, params};
use serde::{Deserialize, Serialize};
pub const MIN_PASSWORD_LENGTH: usize = 8;
pub const MAX_PASSWORD_LENGTH: usize = 128;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct User {
pub id: i64,
pub email: String,
#[serde(skip_serializing)]
pub password_digest: String,
pub confirmed_at: Option<String>,
pub created_at: String,
pub updated_at: String,
}
impl User {
pub fn confirmed(&self) -> bool {
self.confirmed_at.is_some()
}
pub fn has_password(&self) -> bool {
!self.password_digest.is_empty()
}
}
#[derive(Clone, Default, Deserialize)]
#[serde(default)]
pub struct NewUser {
pub email: String,
pub password: String,
}
impl NewUser {
pub fn validate(&self) -> Validator {
let mut v = Validator::new();
validate_email(&mut v, &self.email);
validate_password(&mut v, &self.password);
v
}
}
pub fn normalize_email(email: &str) -> String {
email.trim().to_lowercase()
}
pub fn validate_email(v: &mut Validator, email: &str) {
let email = normalize_email(email);
if email.is_empty() {
v.required("email", &email);
} else {
v.email("email", &email).max_length("email", &email, 254);
}
}
pub fn validate_password(v: &mut Validator, password: &str) {
v.min_length("password", password, MIN_PASSWORD_LENGTH).max_length("password", password, MAX_PASSWORD_LENGTH);
}
pub async fn find(ctx: &Ctx, id: i64) -> Result<Option<User>> {
ctx.db()?.first("SELECT * FROM users WHERE id = ?1", params![id]).await
}
pub async fn find_many(ctx: &Ctx, ids: &[i64]) -> Result<Vec<User>> {
let db = ctx.db()?;
let mut rows = Vec::with_capacity(ids.len());
for chunk in ids.chunks(100) {
rows.extend(Query::<User>::table("users").is_in("id", chunk.iter().copied()).all(&db).await?);
}
Ok(rows)
}
pub async fn find_by_email(ctx: &Ctx, email: &str) -> Result<Option<User>> {
ctx.db()?.first("SELECT * FROM users WHERE email = ?1", params![normalize_email(email)]).await
}
pub async fn create(ctx: &Ctx, new: NewUser) -> Result<User> {
let db = ctx.db()?;
let email = normalize_email(&new.email);
let mut v = new.validate();
v.check(
"email",
db.exists("SELECT 1 FROM users WHERE email = ?1 LIMIT 1", params![&email]).await?,
"has already been taken",
);
v.finish()?;
let password_digest = ocre::password::hash(&new.password).await?;
db.first("INSERT INTO users (email, password_digest) VALUES (?1, ?2) RETURNING *", params![email, password_digest])
.await?
.ok_or_else(|| Error::internal("INSERT ... RETURNING returned no row"))
}
pub async fn create_confirmed(ctx: &Ctx, email: &str) -> Result<User> {
let mut v = Validator::new();
validate_email(&mut v, email);
v.finish()?;
ctx.db()?
.first(
"INSERT INTO users (email, password_digest, confirmed_at) VALUES (?1, '', datetime('now')) RETURNING *",
params![normalize_email(email)],
)
.await?
.ok_or_else(|| Error::internal("INSERT ... RETURNING returned no row"))
}
pub async fn authenticate(ctx: &Ctx, email: &str, password: &str) -> Result<Option<User>> {
let user = find_by_email(ctx, email).await?.filter(User::has_password);
let Some(user) = user else {
ocre::password::hash(password).await?;
return Ok(None);
};
let valid = ocre::password::verify(password, &user.password_digest).await?;
Ok(valid.then_some(user))
}
pub async fn update_password(ctx: &Ctx, id: i64, password: &str) -> Result<()> {
let mut v = Validator::new();
validate_password(&mut v, password);
v.finish()?;
let password_digest = ocre::password::hash(password).await?;
ctx.db()?
.execute(
"UPDATE users SET password_digest = ?1, updated_at = datetime('now') WHERE id = ?2",
params![password_digest, id],
)
.await?;
Ok(())
}
pub async fn confirm(ctx: &Ctx, id: i64) -> Result<()> {
ctx.db()?
.execute(
"UPDATE users SET confirmed_at = COALESCE(confirmed_at, datetime('now')), updated_at = datetime('now') \
WHERE id = ?1",
params![id],
)
.await?;
Ok(())
}
pub async fn delete(ctx: &Ctx, id: i64) -> Result<()> {
ctx.db()?.execute("DELETE FROM users WHERE id = ?1", params![id]).await?;
Ok(())
}
pub async fn deletion_confirmed(user: &User, confirmation: &str) -> Result<bool> {
if user.has_password() {
ocre::password::verify(confirmation, &user.password_digest).await
} else {
Ok(normalize_email(confirmation) == user.email)
}
}