feat(manager-core,picloud): api_keys_api + deactivation cascade

* auth: generate_api_key() mints pic_<base32(32 bytes)>, splits the
  indexed 8-char prefix, and Argon2-hashes the body. Adds the
  data-encoding workspace dep for unpadded base32.
* api_keys_api: POST /api/v1/admin/api-keys (mint, returns raw_token
  exactly once), GET (caller's own, no raw), DELETE {id} (caller's
  own; 404 deliberately covers both 'missing' and 'not yours').
  Mint validation rejects bound keys carrying instance:* scopes (422).
* AdminsState gains the api keys repo; PATCH set_active(false) now
  expires every active key for that user alongside session wipe —
  Phase 3.5 deactivation symmetry.
* picloud lib wires PostgresApiKeyRepository through AuthDeps into
  AdminsState + ApiKeysState; api_keys_router merges into the
  guarded_admin layer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-26 22:00:33 +02:00
parent 5f7ddd23ab
commit 8659a58eb2
8 changed files with 393 additions and 13 deletions

View File

@@ -10,13 +10,14 @@ use axum::middleware::from_fn_with_state;
use axum::{routing::get, Json, Router};
use picloud_executor_core::{Engine, Limits};
use picloud_manager_core::{
admin_router, admins_router, apps_api, apps_router, auth_router, compile_routes, migrations,
require_authenticated, route_admin_router, AdminSessionRepository, AdminState,
AdminUserRepository, AdminsState, ApiKeyRepository, AppDomainRepository, AppRepository,
AppsState, AuthState, PostgresAdminSessionRepository, PostgresAdminUserRepository,
PostgresApiKeyRepository, PostgresAppDomainRepository, PostgresAppRepository,
PostgresExecutionLogRepository, PostgresExecutionLogSink, PostgresRouteRepository,
PostgresScriptRepository, RepoResolver, RouteAdminState, RouteRepository, SandboxCeiling,
admin_router, admins_router, api_keys_router, apps_api, apps_router, auth_router,
compile_routes, migrations, require_authenticated, route_admin_router, AdminSessionRepository,
AdminState, AdminUserRepository, AdminsState, ApiKeyRepository, ApiKeysState,
AppDomainRepository, AppRepository, AppsState, AuthState, PostgresAdminSessionRepository,
PostgresAdminUserRepository, PostgresApiKeyRepository, PostgresAppDomainRepository,
PostgresAppRepository, PostgresExecutionLogRepository, PostgresExecutionLogSink,
PostgresRouteRepository, PostgresScriptRepository, RepoResolver, RouteAdminState,
RouteRepository, SandboxCeiling,
};
use picloud_orchestrator_core::routing::{AppDomainTable, RouteTable};
use picloud_orchestrator_core::{
@@ -148,12 +149,16 @@ pub async fn build_app(pool: PgPool, auth: AuthDeps) -> anyhow::Result<Router> {
let auth_state = AuthState {
users: auth.users.clone(),
sessions: auth.sessions.clone(),
keys: auth.keys,
keys: auth.keys.clone(),
ttl: auth.ttl,
};
let admins_state = AdminsState {
users: auth.users,
sessions: auth.sessions,
keys: auth.keys.clone(),
};
let api_keys_state = ApiKeysState {
keys: auth.keys,
};
// /admin/auth/login + /logout are unguarded by design (login is how
@@ -167,6 +172,7 @@ pub async fn build_app(pool: PgPool, auth: AuthDeps) -> anyhow::Result<Router> {
.merge(route_admin_router(route_admin))
.merge(admins_router(admins_state))
.merge(apps_router(apps_state))
.merge(api_keys_router(api_keys_state))
.layer(from_fn_with_state(
auth_state.clone(),
require_authenticated,