openauth-plugins 0.0.3

Official OpenAuth plugin modules.
Documentation
use http::StatusCode;
use openauth_core::api::{parse_request_body, ApiRequest, ApiResponse};
use openauth_core::context::AuthContext;
use openauth_core::error::OpenAuthError;
use serde_json::json;
use time::{Duration, OffsetDateTime};

use crate::admin::access::has_permission;
use crate::admin::cookies::{
    cookie_header, expire_admin_cookie, read_admin_cookie, session_cookie, set_admin_cookie,
};
use crate::admin::errors;
use crate::admin::models::{RevokeSessionBody, UserIdBody};
use crate::admin::options::AdminOptions;
use crate::admin::response;
use crate::admin::store::AdminStore;

use super::{current_admin, permission, require_adapter};

pub async fn list_user_sessions(
    options: AdminOptions,
    context: &AuthContext,
    request: ApiRequest,
) -> Result<ApiResponse, OpenAuthError> {
    let Some((_session, admin)) = current_admin(context, &request).await? else {
        return errors::unauthorized();
    };
    if !has_permission(
        Some(&admin.id),
        admin.role.as_deref(),
        &options,
        &permission("session", "list"),
    ) {
        return errors::forbidden(errors::not_allowed_to_list_sessions());
    }
    let body = parse_request_body::<UserIdBody>(&request)?;
    let adapter = require_adapter(context)?;
    let sessions = AdminStore::new(adapter.as_ref())
        .list_user_sessions(&body.user_id)
        .await?;
    response::json(StatusCode::OK, &json!({ "sessions": sessions }))
}

pub async fn impersonate_user(
    options: AdminOptions,
    context: &AuthContext,
    request: ApiRequest,
) -> Result<ApiResponse, OpenAuthError> {
    let Some((admin_session, admin)) = current_admin(context, &request).await? else {
        return errors::unauthorized();
    };
    if !has_permission(
        Some(&admin.id),
        admin.role.as_deref(),
        &options,
        &permission("user", "impersonate"),
    ) {
        return errors::forbidden(errors::not_allowed_to_impersonate_users());
    }
    let body = parse_request_body::<UserIdBody>(&request)?;
    let adapter = require_adapter(context)?;
    let store = AdminStore::new(adapter.as_ref());
    let Some(target) = store.find_user_by_id(&body.user_id).await? else {
        return errors::not_found_user();
    };
    if target_is_admin(&target, &options)
        && !options.allow_impersonating_admins
        && !has_permission(
            Some(&admin.id),
            admin.role.as_deref(),
            &options,
            &permission("user", "impersonate-admins"),
        )
    {
        return errors::forbidden(errors::cannot_impersonate_admins());
    }
    if target.banned {
        return errors::forbidden(errors::banned_user(&options.banned_user_message));
    }
    let expires_at =
        OffsetDateTime::now_utc() + Duration::seconds(options.impersonation_session_duration);
    let session = store
        .create_session(&target.id, expires_at, Some(admin.id.clone()))
        .await?;
    let dont_remember = None;
    let mut cookies = vec![set_admin_cookie(
        context,
        &admin_session.token,
        dont_remember,
    )?];
    cookies.extend(session_cookie(context, &session.token)?);
    response::json_with_cookies(
        StatusCode::OK,
        &json!({ "session": session, "user": target }),
        cookies,
    )
}

pub async fn stop_impersonating(
    context: &AuthContext,
    request: ApiRequest,
) -> Result<ApiResponse, OpenAuthError> {
    let Some((session, _user)) = current_admin(context, &request).await? else {
        return errors::unauthorized();
    };
    if session.impersonated_by.is_none() {
        return errors::error_response(
            StatusCode::BAD_REQUEST,
            "BAD_REQUEST",
            "You are not impersonating anyone",
        );
    }
    let adapter = require_adapter(context)?;
    let store = AdminStore::new(adapter.as_ref());
    let header = cookie_header(&request);
    let Some((admin_token, _dont_remember)) = read_admin_cookie(context, &header)? else {
        return errors::error_response(
            StatusCode::INTERNAL_SERVER_ERROR,
            "ADMIN_SESSION_NOT_FOUND",
            "Failed to find admin session",
        );
    };
    let Some((admin_session, admin_user)) = store.find_session(&admin_token).await? else {
        return errors::error_response(
            StatusCode::INTERNAL_SERVER_ERROR,
            "ADMIN_SESSION_NOT_FOUND",
            "Failed to find admin session",
        );
    };
    store.delete_session(&session.token).await?;
    let mut cookies = session_cookie(context, &admin_session.token)?;
    cookies.push(expire_admin_cookie(context));
    response::json_with_cookies(
        StatusCode::OK,
        &json!({ "session": admin_session, "user": admin_user }),
        cookies,
    )
}

pub async fn revoke_user_session(
    options: AdminOptions,
    context: &AuthContext,
    request: ApiRequest,
) -> Result<ApiResponse, OpenAuthError> {
    let Some((_session, admin)) = current_admin(context, &request).await? else {
        return errors::unauthorized();
    };
    if !has_permission(
        Some(&admin.id),
        admin.role.as_deref(),
        &options,
        &permission("session", "revoke"),
    ) {
        return errors::forbidden(errors::not_allowed_to_revoke_sessions());
    }
    let body = parse_request_body::<RevokeSessionBody>(&request)?;
    let adapter = require_adapter(context)?;
    AdminStore::new(adapter.as_ref())
        .delete_session(&body.session_token)
        .await?;
    response::json(StatusCode::OK, &json!({ "success": true }))
}

pub async fn revoke_user_sessions(
    options: AdminOptions,
    context: &AuthContext,
    request: ApiRequest,
) -> Result<ApiResponse, OpenAuthError> {
    let Some((_session, admin)) = current_admin(context, &request).await? else {
        return errors::unauthorized();
    };
    if !has_permission(
        Some(&admin.id),
        admin.role.as_deref(),
        &options,
        &permission("session", "revoke"),
    ) {
        return errors::forbidden(errors::not_allowed_to_revoke_sessions());
    }
    let body = parse_request_body::<UserIdBody>(&request)?;
    let adapter = require_adapter(context)?;
    AdminStore::new(adapter.as_ref())
        .delete_user_sessions(&body.user_id)
        .await?;
    response::json(StatusCode::OK, &json!({ "success": true }))
}

fn target_is_admin(user: &crate::admin::AdminUser, options: &AdminOptions) -> bool {
    options.admin_user_ids.iter().any(|id| id == &user.id)
        || user.role.as_deref().is_some_and(|roles| {
            roles.split(',').any(|role| {
                options
                    .admin_roles
                    .iter()
                    .any(|admin_role| admin_role.trim() == role.trim())
            })
        })
}