slave 0.13.1

CLI for slave, a Laravel-inspired Rust framework for MCP-first apps: scaffold an MCP backend with auth, roles, teams and SurrealDB migrations, add server-rendered web apps, and route them by domain or path
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)
}