mod entities;
mod export;
pub(crate) mod health;
mod identity;
mod members;
mod relations;
mod schemas;
mod search;
mod setup;
mod template_library;
pub(crate) mod whoami;
mod workspaces;
use axum::Router;
use axum::routing::{get, post};
use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
use utoipa::{Modify, OpenApi};
use uuid::Uuid;
use yorishiro_core::YorishiroError;
use yorishiro_core::repositories::tenancy::{self, MembershipRole};
use crate::state::AppState;
pub(crate) fn parse_filter_param(
raw: Option<String>,
) -> Result<Option<serde_json::Value>, YorishiroError> {
let Some(raw) = raw.filter(|s| !s.is_empty()) else {
return Ok(None);
};
serde_json::from_str(&raw).map_err(|err| YorishiroError::ValidationFailed {
message: "filter is not valid JSON".into(),
details: vec![],
hint: format!("filter must be a JSON object, e.g. {{\"status\":\"active\"}}: {err}"),
})
}
pub(crate) async fn require_tenant_admin(
state: &AppState,
tenant_id: Uuid,
user_id: Option<Uuid>,
) -> Result<(), YorishiroError> {
let user_id = user_id.ok_or(YorishiroError::Unauthenticated)?;
tenancy::get_membership_role(&state.identity_pool, tenant_id, user_id)
.await?
.filter(|role| matches!(role, MembershipRole::Owner | MembershipRole::Admin))
.ok_or_else(|| YorishiroError::ScopeInsufficient {
message: "this operation is restricted to tenant owners/admins".into(),
hint: "ask a tenant owner to grant you the admin role".into(),
})?;
Ok(())
}
struct SecurityAddon;
impl Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
let components = openapi.components.get_or_insert_with(Default::default);
components.add_security_scheme(
"bearer_auth",
SecurityScheme::Http(
HttpBuilder::new()
.scheme(HttpAuthScheme::Bearer)
.bearer_format("yorishiro-api-key")
.build(),
),
);
}
}
#[derive(OpenApi)]
#[openapi(
paths(
identity::signup,
identity::login,
setup::status,
setup::setup,
members::list_members,
members::add_member,
entities::create_entity,
entities::get_entity,
entities::update_entity,
entities::delete_entity,
entities::list_entities,
entities::get_entity_context,
relations::create_relation,
relations::get_relation,
relations::delete_relation,
relations::list_relations,
schemas::list_schemas,
schemas::get_active_schema,
schemas::get_schema_by_id,
schemas::create_schema,
schemas::get_entity_type_json_schema,
schemas::list_templates,
search::search_entities,
export::export_jsonl,
workspaces::list_workspaces,
workspaces::create_workspace,
workspaces::get_workspace,
workspaces::delete_workspace,
template_library::list_templates,
template_library::get_template,
template_library::create_template,
template_library::update_template,
template_library::delete_template,
template_library::fork_template,
),
components(schemas(
identity::SignupRequest,
identity::SignupResponse,
identity::WorkspaceSummary,
identity::LoginRequest,
identity::LoginResponse,
setup::SetupStatusResponse,
setup::SetupRequest,
setup::SetupResponse,
members::AddMemberRequest,
yorishiro_core::repositories::tenancy::MembershipRole,
yorishiro_core::repositories::tenancy::MembershipRecord,
yorishiro_core::repositories::tenancy::WorkspaceRecord,
yorishiro_core::services::auth::ApiKeyScope,
entities::CreateEntityRequest,
entities::UpdateEntityRequest,
relations::CreateRelationRequest,
schemas::CreateSchemaResponse,
schemas::CreateSchemaRequest,
workspaces::CreateWorkspaceRequest,
workspaces::WorkspaceDetail,
yorishiro_core::repositories::tenancy::TemplateRecord,
template_library::CreateTemplateRequest,
template_library::UpdateTemplateRequest,
template_library::ForkTemplateRequest,
)),
modifiers(&SecurityAddon),
security(("bearer_auth" = [])),
tags(
(name = "auth", description = "Signup and login (no bearer token required)"),
(name = "members", description = "Tenant member management (owner/admin only)"),
(name = "workspaces", description = "Workspace management (listing is open to any tenant member; create/delete are owner/admin only)"),
(name = "entities", description = "Entity operations"),
(name = "relations", description = "Relation operations"),
(name = "schemas", description = "Meta-schema operations"),
(name = "search", description = "Vector similarity search"),
(name = "export", description = "Bulk data export"),
(name = "template-library", description = "Tenant-scoped, DB-backed schema template library (create/delete are owner/admin only)"),
),
info(
title = "Yorishiro API",
description = "REST API for a user-defined-schema, MCP-native knowledge store",
),
)]
pub struct ApiDoc;
pub fn router() -> Router<AppState> {
let rate_limiter =
std::sync::Arc::new(crate::http::middleware::rate_limit::RateLimiter::from_env());
let auth_routes = Router::new()
.route("/auth/signup", post(identity::signup))
.route("/auth/login", post(identity::login))
.route("/setup", post(setup::setup))
.route("/setup/status", get(setup::status))
.layer(axum::middleware::from_fn_with_state(
rate_limiter,
crate::http::middleware::rate_limit::enforce,
));
Router::new()
.merge(auth_routes)
.route(
"/api/members",
post(members::add_member).get(members::list_members),
)
.route(
"/api/workspaces",
post(workspaces::create_workspace).get(workspaces::list_workspaces),
)
.route(
"/api/workspaces/{id}",
get(workspaces::get_workspace).delete(workspaces::delete_workspace),
)
.route(
"/api/entities",
post(entities::create_entity).get(entities::list_entities),
)
.route(
"/api/entities/{id}",
get(entities::get_entity)
.put(entities::update_entity)
.delete(entities::delete_entity),
)
.route(
"/api/entities/{id}/context",
get(entities::get_entity_context),
)
.route(
"/api/relations",
post(relations::create_relation).get(relations::list_relations),
)
.route(
"/api/relations/{id}",
get(relations::get_relation).delete(relations::delete_relation),
)
.route(
"/api/schemas",
post(schemas::create_schema).get(schemas::list_schemas),
)
.route(
"/api/schemas/active/{name}",
get(schemas::get_active_schema),
)
.route(
"/api/schemas/active/{name}/entity-types/{entity_type}/json-schema",
get(schemas::get_entity_type_json_schema),
)
.route("/api/schemas/{schema_id}", get(schemas::get_schema_by_id))
.route("/api/templates", get(schemas::list_templates))
.route(
"/api/template-library",
post(template_library::create_template).get(template_library::list_templates),
)
.route(
"/api/template-library/{id}",
get(template_library::get_template)
.put(template_library::update_template)
.delete(template_library::delete_template),
)
.route(
"/api/template-library/{id}/fork",
post(template_library::fork_template),
)
.route("/api/search", get(search::search_entities))
.route("/api/export.jsonl", get(export::export_jsonl))
}