hive-c0re: annotate remaining dashboard routes with utoipa

This commit is contained in:
damocles 2026-07-31 23:00:12 +02:00 committed by mara
commit 582ebe5eee
21 changed files with 738 additions and 45 deletions

View file

@ -22,16 +22,17 @@ use std::path::Path;
use axum::extract::{Form, Query};
use axum::response::{IntoResponse, Response};
use serde::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use super::{Ident, error_response};
use crate::coordinator::Coordinator;
#[derive(Deserialize)]
#[derive(Deserialize, IntoParams)]
pub(super) struct MatrixAccountsQuery {
agent: String,
}
#[derive(Serialize)]
#[derive(Serialize, ToSchema)]
struct MatrixAccount {
name: String,
/// Effective homeserver, backfilled from the daemon snapshot; `None` when
@ -46,7 +47,7 @@ struct MatrixAccount {
user_id: Option<String>,
}
#[derive(Serialize)]
#[derive(Serialize, ToSchema)]
struct MatrixAccountsResponse {
accounts: Vec<MatrixAccount>,
/// Unix mtime of the daemon's `matrix-accounts.json` snapshot (when the
@ -102,6 +103,19 @@ fn account_name_from_filename(fname: &str) -> Option<String> {
Some(suffix.to_owned())
}
/// `GET /api/matrix-accounts?agent=<name>` — matrix accounts provisioned
/// for `agent`, backfilled with homeserver/live/user_id from the daemon's
/// snapshot.
#[utoipa::path(
get,
path = "/api/matrix-accounts",
params(MatrixAccountsQuery),
responses(
(status = 200, description = "provisioned matrix accounts for the agent", body = MatrixAccountsResponse),
(status = 500, description = "invalid agent name, or a state-dir read failed"),
),
tag = "matrix_accounts"
)]
pub(super) async fn get_matrix_accounts(Query(q): Query<MatrixAccountsQuery>) -> Response {
let agent = q.agent.trim();
// Validate through the single `Ident` type so a crafted `agent` can't
@ -156,7 +170,7 @@ pub(super) async fn get_matrix_accounts(Query(q): Query<MatrixAccountsQuery>) ->
/// mutation convention). `mode` is `"password"` (needs `user_id` +
/// `password`) or `"token"` (needs `token`; `user_id` is recovered via
/// whoami).
#[derive(Deserialize)]
#[derive(Deserialize, ToSchema)]
pub(super) struct MatrixLoginForm {
agent: String,
account: String,
@ -167,7 +181,7 @@ pub(super) struct MatrixLoginForm {
token: Option<String>,
}
#[derive(Serialize)]
#[derive(Serialize, ToSchema)]
struct MatrixLoginResult {
ok: bool,
user_id: String,
@ -178,6 +192,16 @@ struct MatrixLoginResult {
/// On success writes the token to `matrix-token-<account>` via hive-priv and
/// kicks the daemon. Operator-authenticated (dashboard). Never echoes the
/// token back — only `{ ok, user_id }`.
#[utoipa::path(
post,
path = "/api/matrix-account-login",
request_body(content = MatrixLoginForm, content_type = "application/x-www-form-urlencoded"),
responses(
(status = 200, description = "account provisioned", body = MatrixLoginResult),
(status = 500, description = "invalid input, or the homeserver login/whoami failed"),
),
tag = "matrix_accounts"
)]
pub(super) async fn post_matrix_account_login(Form(f): Form<MatrixLoginForm>) -> Response {
let agent = f.agent.trim();
let account = f.account.trim();
@ -257,13 +281,13 @@ pub(super) async fn post_matrix_account_login(Form(f): Form<MatrixLoginForm>) ->
/// `github-token` file. The GitHub counterpart of the matrix login form, but
/// far simpler: no account creation, no homeserver, no login modes — the
/// operator pastes a PAT for an existing account.
#[derive(Deserialize)]
#[derive(Deserialize, ToSchema)]
pub(super) struct GithubAccountForm {
agent: String,
token: String,
}
#[derive(Serialize)]
#[derive(Serialize, ToSchema)]
struct GithubAccountResult {
ok: bool,
}
@ -275,6 +299,16 @@ struct GithubAccountResult {
/// helper read the file live, so the new token takes effect immediately.
/// Operator-authenticated (dashboard). Never echoes the token back — only
/// `{ ok: true }`.
#[utoipa::path(
post,
path = "/api/github-account",
request_body(content = GithubAccountForm, content_type = "application/x-www-form-urlencoded"),
responses(
(status = 200, description = "PAT provisioned", body = GithubAccountResult),
(status = 500, description = "invalid agent name, empty token, or the write failed"),
),
tag = "matrix_accounts"
)]
pub(super) async fn post_github_account(Form(f): Form<GithubAccountForm>) -> Response {
let agent = f.agent.trim();
let token = f.token.trim();
@ -291,12 +325,12 @@ pub(super) async fn post_github_account(Form(f): Form<GithubAccountForm>) -> Res
axum::Json(GithubAccountResult { ok: true }).into_response()
}
#[derive(Deserialize)]
#[derive(Deserialize, IntoParams)]
pub(super) struct GithubAccountQuery {
agent: String,
}
#[derive(Serialize)]
#[derive(Serialize, ToSchema)]
struct GithubAccountStatus {
/// A `github-token` file exists in the agent's state dir (a PAT has been
/// provisioned). A static PAT has no live/heartbeat concept, so this is
@ -308,6 +342,16 @@ struct GithubAccountStatus {
/// PAT provisioned (its `github-token` file exists). Lets the credentials tab
/// show "token stored" vs "not set" instead of a black-hole paste field.
/// Never returns the token itself.
#[utoipa::path(
get,
path = "/api/github-account",
params(GithubAccountQuery),
responses(
(status = 200, description = "whether a github PAT is provisioned", body = GithubAccountStatus),
(status = 500, description = "invalid agent name"),
),
tag = "matrix_accounts"
)]
pub(super) async fn get_github_account(Query(q): Query<GithubAccountQuery>) -> Response {
let agent = q.agent.trim();
let Ok(agent) = Ident::parse(agent) else {