use axum::extract::FromRef;
use slave_framework::database::{Db, Executor};
use crate::base::auth::{SUPER_ADMIN_ROLE, USER_ROLE};
use crate::base::error::{AppError, AppResult};
use crate::base::state::AppState;
use crate::modules::role::roles_of;
use crate::modules::user::User;
/// Finds and creates the users who sign in.
#[derive(Debug, Clone)]
pub struct AuthService {
db: Db,
}
impl FromRef<AppState> for AuthService {
fn from_ref(state: &AppState) -> Self {
Self {
db: state.db.clone(),
}
}
}
impl AuthService {
/// The user signing in with `email` (normalized), created on their
/// first sign-in: the first user ever becomes super admin, later ones
/// get `user`.
pub async fn sign_in(&self, email: &str) -> AppResult<User> {
match find_by_email(&self.db, email).await? {
Some(user) => Ok(user),
None => match create_user(&self.db, email).await {
// A concurrent first sign-in of the same person created them.
Err(err) if err.is_already_exists() => {
find_by_email(&self.db, email).await?.ok_or(err)
}
result => result,
},
}
}
/// User `user_id` and their roles (`Unauthorized` once deleted).
pub async fn load(&self, user_id: i64) -> AppResult<(User, Vec<String>)> {
let user: Option<User> = self
.db
.query(format!(
"SELECT {} FROM type::record('user', $id)",
User::FIELDS
))
.bind(("id", user_id))
.await?
.take(0)?;
let user = user.ok_or(AppError::Unauthorized)?;
let roles = roles_of(&self.db, user.id)
.await?
.into_iter()
.map(|role| role.name)
.collect();
Ok((user, roles))
}
}
/// Creates the user for `email` with role `super_admin` if they are the
/// first user, else `user`.
///
/// The first user is whoever claims the `bootstrap:super_admin` record.
/// While it is unclaimed, every creation writes it, so two concurrent first
/// sign-ins conflict and one is retried: they cannot both become super admin.
async fn create_user(db: &Db, email: &str) -> AppResult<User> {
// Until they choose one, users are called by their email's local part.
let name = email.split('@').next().unwrap_or(email);
slave_framework::transaction!(db, |tx| {
let user: Option<User> = tx
.query(format!(
"CREATE ONLY type::record('user', sequence::nextval('user_id'))
SET email = $email, name = $name
RETURN {}",
User::FIELDS
))
.bind(("email", email.to_owned()))
.bind(("name", name.to_owned()))
.await?
.take(0)?;
let user = user.ok_or_else(|| AppError::Internal("user not created".into()))?;
let claimed: Option<i64> = tx
.query("SELECT VALUE user_id FROM ONLY bootstrap:super_admin")
.await?
.take(0)?;
let first = match claimed {
Some(user_id) => Some(user_id),
None => tx
.query(
"CREATE ONLY bootstrap:super_admin SET user_id = $user_id RETURN VALUE user_id",
)
.bind(("user_id", user.id))
.await?
.take(0)?,
};
let role_name = if first == Some(user.id) {
SUPER_ADMIN_ROLE
} else {
USER_ROLE
};
let role_id: Option<i64> = tx
.query("SELECT VALUE record::id(id) FROM ONLY role WHERE name = $name LIMIT 1")
.bind(("name", role_name))
.await?
.take(0)?;
let role_id = role_id.ok_or_else(|| {
AppError::Internal(format!(
"role `{role_name}` is missing; run `slave migrate`"
))
})?;
tx.query(
"CREATE type::record('user_role', [$user_id, $role_id])
SET user_id = $user_id, role_id = $role_id",
)
.bind(("user_id", user.id))
.bind(("role_id", role_id))
.await?
.check()?;
Ok(user)
})
}
async fn find_by_email(db: &impl Executor, email: &str) -> AppResult<Option<User>> {
let user: Option<User> = db
.query(format!(
"SELECT {} FROM user WHERE email = $email",
User::FIELDS
))
.bind(("email", email.to_owned()))
.await?
.take(0)?;
Ok(user)
}