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
//! Request authentication: [`AuthUser`].
//!
//! Callers sign in with a code sent to their email or with a passkey (the
//! `auth_*` tools) and send the session token they get as
//! `Authorization: Bearer <token>`; `slave_framework::auth` keeps the
//! sessions. There are no passwords.
//!
//! This lives in `base` rather than in the `auth` module because every module
//! checks it; keeping it here means modules never depend on each other just to
//! check who is calling.

use axum::extract::FromRef;
use axum::http::HeaderMap;
use axum::http::header::AUTHORIZATION;

use super::error::{AppError, AppResult};
use super::state::AppState;
use crate::modules::auth::AuthService;

/// The platform operators: full access to every team and user. Granted to
/// the first user who signs in. Per-team roles (owner, admin, staff) live in
/// the `team` module.
pub const SUPER_ADMIN_ROLE: &str = "super_admin";
/// Role granted to every later user.
pub const USER_ROLE: &str = "user";

/// The user making the request, authenticated by `Authorization: Bearer <session token>`.
#[derive(Debug, Clone)]
pub struct AuthUser {
    pub id: i64,
    pub name: String,
    pub email: Option<String>,
    pub roles: Vec<String>,
    /// Signed in through a customer-facing app (a storefront): only tools that call
    /// `McpServer::authenticate_customer` accept it.
    pub customer: bool,
}

impl AuthUser {
    /// Resolves the caller from a live session token in `headers`.
    pub async fn authenticate(state: &AppState, headers: &HeaderMap) -> AppResult<Self> {
        let token = bearer_token(headers).ok_or(AppError::Unauthorized)?;
        let session = state
            .auth
            .session(token)
            .await?
            .ok_or(AppError::Unauthorized)?;
        let (user, roles) = AuthService::from_ref(state).load(session.user_id).await?;
        Ok(Self {
            id: user.id,
            name: user.name,
            email: user.email,
            roles,
            customer: session.customer,
        })
    }

    pub fn has_role(&self, role: &str) -> bool {
        self.roles.iter().any(|r| r == role)
    }

    pub fn is_super_admin(&self) -> bool {
        self.has_role(SUPER_ADMIN_ROLE)
    }

    /// Fails with `Forbidden` unless this user is a super admin.
    pub fn require_super_admin(&self) -> AppResult<()> {
        if self.is_super_admin() {
            Ok(())
        } else {
            Err(AppError::Forbidden)
        }
    }

    /// Fails with `Forbidden` unless this user is `user_id` or a super admin.
    pub fn require_self_or_super_admin(&self, user_id: i64) -> AppResult<()> {
        if self.id == user_id || self.is_super_admin() {
            Ok(())
        } else {
            Err(AppError::Forbidden)
        }
    }
}

/// The token of `Authorization: Bearer <token>`.
pub fn bearer_token(headers: &HeaderMap) -> Option<&str> {
    let value = headers.get(AUTHORIZATION)?.to_str().ok()?;
    let (scheme, token) = value.split_once(' ')?;
    let token = token.trim();
    (scheme.eq_ignore_ascii_case("bearer") && !token.is_empty()).then_some(token)
}