open-pincery 1.0.1

Multi-agent platform for durable, event-driven AI agents
Documentation
use axum::{
    extract::State,
    http::{header, StatusCode},
    response::IntoResponse,
    routing::post,
    Json, Router,
};
use serde::Serialize;
use uuid::Uuid;

use super::AppState;
use crate::error::AppError;
use crate::models::{user, workspace};

#[derive(Serialize)]
struct BootstrapResponse {
    user_id: Uuid,
    organization_id: Uuid,
    workspace_id: Uuid,
    session_token: String,
}

#[derive(Serialize)]
struct LoginResponse {
    user_id: Uuid,
    session_token: String,
}

pub fn router() -> Router<AppState> {
    Router::new()
        .route("/api/bootstrap", post(bootstrap))
        .route("/api/login", post(login))
}

async fn bootstrap(
    State(state): State<AppState>,
    req: axum::extract::Request,
) -> Result<impl IntoResponse, AppError> {
    let token = req
        .headers()
        .get(header::AUTHORIZATION)
        .and_then(|v| v.to_str().ok())
        .and_then(|v| v.strip_prefix("Bearer "))
        .ok_or(AppError::Unauthorized("Missing bearer token".into()))?;

    if token != state.config.bootstrap_token {
        return Err(AppError::Unauthorized("Invalid bootstrap token".into()));
    }

    // Check if already bootstrapped
    let existing = user::find_local_admin(&state.pool).await?;
    if existing.is_some() {
        return Err(AppError::Conflict(
            "System already bootstrapped. Use 'pcy login --bootstrap-token <token>' or POST /api/login to get a new session token.".into(),
        ));
    }

    let admin = user::create_local_admin(&state.pool, "admin@localhost", "Admin").await?;
    let org = workspace::create_organization(&state.pool, "default", "default", admin.id).await?;
    let ws = workspace::create_workspace(
        &state.pool,
        org.id,
        "default",
        "Default Workspace",
        admin.id,
    )
    .await?;
    workspace::add_org_membership(&state.pool, org.id, admin.id, "owner").await?;
    workspace::add_workspace_membership(&state.pool, ws.id, admin.id, "owner").await?;

    let raw_token = crate::auth::generate_token();
    let token_hash = crate::auth::hash_token(&raw_token);
    user::create_session(&state.pool, admin.id, &token_hash, "local_admin").await?;

    Ok((
        StatusCode::CREATED,
        Json(BootstrapResponse {
            user_id: admin.id,
            organization_id: org.id,
            workspace_id: ws.id,
            session_token: raw_token,
        }),
    ))
}

async fn login(
    State(state): State<AppState>,
    req: axum::extract::Request,
) -> Result<impl IntoResponse, AppError> {
    let token = req
        .headers()
        .get(header::AUTHORIZATION)
        .and_then(|v| v.to_str().ok())
        .and_then(|v| v.strip_prefix("Bearer "))
        .ok_or(AppError::Unauthorized("Missing bearer token".into()))?;

    if token != state.config.bootstrap_token {
        return Err(AppError::Unauthorized("Invalid bootstrap token".into()));
    }

    let admin = user::find_local_admin(&state.pool)
        .await?
        .ok_or(AppError::BadRequest(
            "System not bootstrapped yet. Run 'pcy bootstrap' first.".into(),
        ))?;

    let raw_token = crate::auth::generate_token();
    let token_hash = crate::auth::hash_token(&raw_token);
    user::create_session(&state.pool, admin.id, &token_hash, "local_admin").await?;

    Ok((
        StatusCode::OK,
        Json(LoginResponse {
            user_id: admin.id,
            session_token: raw_token,
        }),
    ))
}