ordinary-api 0.8.0

API server for Ordinary
// Copyright (C) 2026 Ordinary Labs, LLC.
//
// SPDX-License-Identifier: AGPL-3.0-only

use crate::server::OrdinaryApiServerState;
use anyhow::bail;
use axum::http::{HeaderMap, StatusCode};
use ordinary_utils::get_bearer_token_as_bytes;
use saferlmdb::{Database, DatabaseOptions, Environment, ReadTransaction, WriteTransaction, put};
use std::sync::Arc;

pub struct AccountLockManager {
    env: Arc<Environment>,
    lock_db: Arc<Database<'static>>,
}

impl AccountLockManager {
    pub fn new(env: &Arc<Environment>) -> anyhow::Result<Self> {
        let lock_db = Arc::new(Database::open(
            env.clone(),
            Some("locked"),
            &DatabaseOptions::new(saferlmdb::db::Flags::CREATE),
        )?);

        Ok(Self {
            env: env.clone(),
            lock_db,
        })
    }

    pub fn lock_account(&self, account: &str) -> anyhow::Result<()> {
        if account == "root" {
            bail!("cannot lock the root account");
        }

        let txn = WriteTransaction::new(self.env.clone())?;

        {
            let mut access = txn.access();
            access.put(
                &self.lock_db,
                account.as_bytes(),
                &[0u8],
                &put::Flags::empty(),
            )?;
        }

        txn.commit()?;
        Ok(())
    }

    pub fn unlock_account(&self, account: &str) -> anyhow::Result<()> {
        let txn = WriteTransaction::new(self.env.clone())?;

        {
            let mut access = txn.access();
            access.del_key(&self.lock_db, account.as_bytes())?;
        }

        txn.commit()?;

        Ok(())
    }

    pub fn is_locked(&self, account: &str) -> anyhow::Result<bool> {
        let txn = ReadTransaction::new(self.env.clone())?;
        let access = txn.access();

        let is_locked = access
            .get::<[u8], [u8]>(&self.lock_db, account.as_bytes())
            .is_ok();

        Ok(is_locked)
    }
}

/// "admin" 0 | "read" 1 | "write" 2 | "update" 3 | "upload" 4 | "install" 5 | "deploy" 6 | "bridge" 7 | "kill" 8 | "erase" 9
pub(crate) fn check(
    state: &OrdinaryApiServerState,
    headers: &HeaderMap,
    permission: u8,
    domain: &str,
) -> Result<String, StatusCode> {
    if let Ok(token) = get_bearer_token_as_bytes(headers) {
        match state.auth.verify_access_token(token.as_ref()) {
            Ok((account, claims)) => {
                let span = tracing::info_span!("permission", account);

                span.in_scope(|| {
                    if account == "root" {
                        return Ok(account.to_string());
                    }

                    if domain == "root" {
                        tracing::error!("root-only operation disallowed");
                        return Err(StatusCode::UNAUTHORIZED);
                    }

                    let check_domain = claims.idx(1).as_str();

                    if check_domain != domain {
                        tracing::error!(domain, "token domain '{check_domain}' does not match");
                        return Err(StatusCode::UNAUTHORIZED);
                    }

                    let permissions = claims.idx(2).as_vector();

                    for claim_permission in &permissions {
                        if claim_permission.as_u8() == 0 || claim_permission.as_u8() == permission {
                            tracing::info!(
                                role = match claim_permission.as_u8() {
                                    0 => "admin",
                                    1 => "read",
                                    2 => "write",
                                    3 => "update",
                                    4 => "upload",
                                    5 => "install",
                                    6 => "deploy",
                                    7 => "bridge",
                                    8 => "kill",
                                    9 => "erase",
                                    _ => "invalid",
                                },
                                "granted"
                            );

                            match state.account_lock_manager.is_locked(account) {
                                Ok(is_locked) => {
                                    if is_locked {
                                        tracing::error!(%account, "locked");
                                        return Err(StatusCode::UNAUTHORIZED);
                                    }
                                }
                                Err(err) => {
                                    tracing::error!(%err);
                                    return Err(StatusCode::UNAUTHORIZED);
                                }
                            }

                            return Ok(account.to_string());
                        }
                    }

                    tracing::error!(
                        permission = match permission {
                            0 => "admin",
                            1 => "read",
                            2 => "write",
                            3 => "update",
                            4 => "upload",
                            5 => "install",
                            6 => "deploy",
                            7 => "bridge",
                            8 => "kill",
                            9 => "erase",
                            _ => "invalid",
                        },
                        "not in token permissions"
                    );
                    Err(StatusCode::UNAUTHORIZED)
                })
            }
            Err(err) => {
                tracing::error!(%err, "access denied");
                Err(StatusCode::UNAUTHORIZED)
            }
        }
    } else {
        tracing::error!("invalid token");
        Err(StatusCode::UNAUTHORIZED)
    }
}