Add Storage, Users, Secrets, Webhooks, Scheduled tasks, Audit log screens

Full management surfaces for the platform-admin tenant, mirroring the
existing Tenants pattern (DataTable + row actions + create/edit dialogs +
ConfirmDialog for destructive ops, all data-action tagged for the
command bus, useRegisterAdminContext publishing for the assistant).

- Storage (/storage): backends + credentials. Write-only secret fields,
  Validate/Activate/Deactivate/Set-default/Mark-degraded/Maintenance.
- Users (/users): tabs for Users, Invitations, Roles. Per-user View
  drawer with profile, role add/remove, API keys (one-time reveal on
  create), usage + quota.
- Secrets (/secrets): /api/v1/admin/secrets — create/rotate/rollback,
  versions dialog, enable/disable, generate-value helper.
- Webhooks (/webhooks): CRUD, pause/resume, regenerate-secret with
  one-time reveal, send test event, deliveries dialog.
- Scheduled tasks (/scheduled-tasks): cron CRUD, run-now trigger,
  enable/disable, expandable run history.
- Audit log (/activity): replaces the empty stub. Filter by severity,
  resource type, date range; click for full JSON detail.

All endpoints are hand-rolled HTTP because most aren't covered by the
generated OpenAPI typed paths yet — switch to arcadia.typed.* when the
backend wires them into OpenApiSpex.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jules
2026-05-01 22:50:09 +10:00
parent 45fa130951
commit a907e25a7c
19 changed files with 7439 additions and 25 deletions

171
app/lib/arcadia/secrets.ts Normal file
View File

@@ -0,0 +1,171 @@
// Arcadia secrets API helpers.
//
// Backed by /api/v1/admin/secrets — the platform Secrets Manager. Values are
// AES-encrypted at rest and never returned by index/show; only metadata is
// exposed by these endpoints. Tenant-side resolution (returning the value)
// goes through a separate runtime endpoint that's not used by the admin UI.
import type { ArcadiaClient } from "@crema/arcadia-client"
export type SecretCategory =
| "api_key"
| "smtp"
| "oauth_token"
| "webhook_secret"
| "generic"
export type SecretEnvironment = "production" | "staging" | "development" | "all"
export interface Secret {
id: string
tenant_id: string | null
name: string
description: string | null
category: SecretCategory
environment: SecretEnvironment
tags: string[]
used_by: string[]
allowed_ips: string[]
read_once: boolean
read_once_consumed: boolean
expires_at: string | null
last_rotated_at: string | null
rotation_interval_days: number | null
rotation_due: boolean
expired: boolean
enabled: boolean
inserted_at: string
updated_at: string
}
export interface SecretVersion {
id: string
secret_id: string
version: number
note: string | null
inserted_by: string | null
inserted_at: string
}
export interface SecretCreateInput {
name: string
value: string
category?: SecretCategory
description?: string | null
environment?: SecretEnvironment
tags?: string[]
used_by?: string[]
allowed_ips?: string[]
read_once?: boolean
expires_at?: string | null
rotation_interval_days?: number | null
}
export type SecretMetaInput = Omit<Partial<SecretCreateInput>, "value" | "name">
export interface RotateInput {
value: string
note?: string
}
export async function listSecrets(arcadia: ArcadiaClient): Promise<Secret[]> {
const res = await arcadia.GET<{ data: Secret[] }>("/api/v1/admin/secrets")
return res.data
}
export async function getSecret(arcadia: ArcadiaClient, id: string): Promise<Secret> {
const res = await arcadia.GET<{ data: Secret }>(`/api/v1/admin/secrets/${id}`)
return res.data
}
export async function createSecret(
arcadia: ArcadiaClient,
input: SecretCreateInput,
): Promise<Secret> {
const res = await arcadia.POST<{ data: Secret }>("/api/v1/admin/secrets", {
body: { secret: input },
})
return res.data
}
export async function updateSecretMeta(
arcadia: ArcadiaClient,
id: string,
input: SecretMetaInput,
): Promise<Secret> {
const res = await arcadia.PATCH<{ data: Secret }>(`/api/v1/admin/secrets/${id}`, {
body: { secret: input },
})
return res.data
}
export async function deleteSecret(arcadia: ArcadiaClient, id: string): Promise<void> {
await arcadia.DELETE(`/api/v1/admin/secrets/${id}`)
}
export async function rotateSecret(
arcadia: ArcadiaClient,
id: string,
input: RotateInput,
): Promise<Secret> {
const res = await arcadia.POST<{ data: Secret }>(`/api/v1/admin/secrets/${id}/rotate`, {
body: { secret: input },
})
return res.data
}
export async function rollbackSecret(
arcadia: ArcadiaClient,
id: string,
version: number,
): Promise<Secret> {
const res = await arcadia.POST<{ data: Secret }>(
`/api/v1/admin/secrets/${id}/rollback/${version}`,
)
return res.data
}
export async function enableSecret(arcadia: ArcadiaClient, id: string): Promise<Secret> {
const res = await arcadia.POST<{ data: Secret }>(`/api/v1/admin/secrets/${id}/enable`)
return res.data
}
export async function disableSecret(arcadia: ArcadiaClient, id: string): Promise<Secret> {
const res = await arcadia.POST<{ data: Secret }>(`/api/v1/admin/secrets/${id}/disable`)
return res.data
}
export async function listSecretVersions(
arcadia: ArcadiaClient,
id: string,
): Promise<SecretVersion[]> {
const res = await arcadia.GET<{ data: SecretVersion[] }>(
`/api/v1/admin/secrets/${id}/versions`,
)
return res.data
}
export async function generateSecretValue(
arcadia: ArcadiaClient,
params?: { length?: number; charset?: string },
): Promise<string> {
const res = await arcadia.GET<{ data: { value: string } }>("/api/v1/admin/secrets/generate", {
params: params as Record<string, string | number | boolean | null | undefined>,
})
return res.data.value
}
export const SECRET_CATEGORIES: { value: SecretCategory; label: string }[] = [
{ value: "api_key", label: "API key" },
{ value: "oauth_token", label: "OAuth token" },
{ value: "smtp", label: "SMTP credentials" },
{ value: "webhook_secret", label: "Webhook secret" },
{ value: "generic", label: "Generic" },
]
export const SECRET_ENVIRONMENTS: { value: SecretEnvironment; label: string }[] = [
{ value: "all", label: "All environments" },
{ value: "production", label: "Production" },
{ value: "staging", label: "Staging" },
{ value: "development", label: "Development" },
]