//! Request authentication: the [`AuthUser`] and [`AdminUser`] extractors.
//!
//! This lives in `base` rather than in the `auth` module because every module
//! guards its routes with it; keeping it here means modules never depend on
//! each other just to check who is calling.
use axum::extract::FromRequestParts;
use axum::http::header::AUTHORIZATION;
use axum::http::request::Parts;
use super::error::AppError;
use super::state::AppState;
/// Role with full access, granted to the first registered user.
pub const ADMIN_ROLE: &str = "admin";
/// Role granted to every other newly registered user.
pub const USER_ROLE: &str = "user";
/// The user making the request, authenticated by `Authorization: Bearer <token>`.
///
/// Add it as a handler argument to require a valid, unexpired token.
#[derive(Debug, Clone)]
pub struct AuthUser {
pub id: i64,
pub name: String,
pub email: String,
pub roles: Vec<String>,
token_hash: String,
}
impl AuthUser {
pub fn has_role(&self, role: &str) -> bool {
self.roles.iter().any(|r| r == role)
}
pub fn is_admin(&self) -> bool {
self.has_role(ADMIN_ROLE)
}
/// Stored hash of the token this request was authenticated with.
pub fn token_hash(&self) -> &str {
&self.token_hash
}
}
impl FromRequestParts<AppState> for AuthUser {
type Rejection = AppError;
async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, AppError> {
let token = bearer_token(parts).ok_or(AppError::Unauthorized)?;
let token_hash = slave_framework::auth::hash_token(token);
let conn = state.database.connect().await?;
let mut rows = conn
.query(
"SELECT u.id, u.name, u.email FROM auth_tokens t
JOIN users u ON u.id = t.user_id
WHERE t.token_hash = ?1 AND t.expires_at > CURRENT_TIMESTAMP",
(token_hash.as_str(),),
)
.await?;
let Some(row) = rows.next().await? else {
return Err(AppError::Unauthorized);
};
let (id, name, email) = (row.get(0)?, row.get(1)?, row.get(2)?);
drop(rows);
let mut rows = conn
.query(
"SELECT r.name FROM roles r
JOIN user_roles ur ON ur.role_id = r.id
WHERE ur.user_id = ?1 ORDER BY r.name",
(id,),
)
.await?;
let mut roles = Vec::new();
while let Some(row) = rows.next().await? {
roles.push(row.get(0)?);
}
Ok(Self {
id,
name,
email,
roles,
token_hash,
})
}
}
/// An [`AuthUser`] that has the `admin` role; other users get 403 Forbidden.
#[derive(Debug, Clone)]
pub struct AdminUser(pub AuthUser);
impl FromRequestParts<AppState> for AdminUser {
type Rejection = AppError;
async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, AppError> {
let user = AuthUser::from_request_parts(parts, state).await?;
if user.is_admin() {
Ok(Self(user))
} else {
Err(AppError::Forbidden)
}
}
}
fn bearer_token(parts: &Parts) -> Option<&str> {
let value = parts.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)
}