feat!: capabilities replace API token scopes; every admin endpoint declares one, with a picker UI [CI]
This commit is contained in:
1 parent
55b80f0a97
commit
ddb6608e32
38 files changed
+1148
-218
No files matched your search
@@ -153,7 +153,7 @@ export function App() {
|
||||
<span className="grow">
|
||||
<div style={{ fontWeight: 600 }}>{me.username ?? 'API 令牌'}</div>
|
||||
<div className="faint" style={{ fontSize: 12 }}>
|
||||
{me.scope}
|
||||
{me.kind === 'token' ? `API 令牌 · ${me.capabilities.length} 项能力` : '管理员'}
|
||||
</div>
|
||||
</span>
|
||||
<ThemeToggle />
|
||||
|
||||
@@ -16,7 +16,7 @@ export function BackendUpdateNotice() {
|
||||
const { busy, run } = useAction()
|
||||
|
||||
useEffect(() => {
|
||||
if (me?.scope !== 'admin') return
|
||||
if (!me?.capabilities.includes('update.read')) return
|
||||
let alive = true
|
||||
const receive = (value: BackendUpdateDto) => {
|
||||
if (!alive) return
|
||||
@@ -27,7 +27,7 @@ export function BackendUpdateNotice() {
|
||||
const source = openLive('/api/admin/backend/update/live')
|
||||
source.addEventListener('backend_update', (event) => receive(JSON.parse(event.data) as BackendUpdateDto))
|
||||
return () => { alive = false; source.close() }
|
||||
}, [me?.scope])
|
||||
}, [me?.capabilities])
|
||||
|
||||
useEffect(() => {
|
||||
if (!requested || !update) return
|
||||
|
||||
@@ -48,7 +48,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
const r = await api.post<LoginResponse>('/api/admin/auth/login', { username, password })
|
||||
setCsrf(r.csrf_token)
|
||||
setMe({ kind: 'user', scope: 'admin', user_id: r.user_id, username: r.username, csrf_token: r.csrf_token })
|
||||
// 登录响应没有能力列表:登录后取一次当前身份
|
||||
setMe(await api.get<MeDto>('/api/admin/auth/me'))
|
||||
}, [])
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import { KeyRound } from 'lucide-react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import type { CapabilityCatalogDto } from '@shared/api/types'
|
||||
import { Chip } from '@shared/ui'
|
||||
|
||||
/** 能力的中文名和说明。目录(分组、依赖、预设)来自后端,这里只管显示文字。 */
|
||||
const NAMES: Record<string, [string, string]> = {
|
||||
'public.read': ['查看展示数据', '看隐藏线路;站点私有时看展示页'],
|
||||
'fleet.read': ['查看节点', 'Agent、Probe、线路、加入代码'],
|
||||
'fleet.write': ['修改节点', '改名、标签、可见性、地址、线路开关、断开会话'],
|
||||
'fleet.enroll': ['添加节点', '生成加入代码、批准新节点'],
|
||||
'fleet.revoke': ['吊销或删除节点', '破坏性操作'],
|
||||
'tasks.read': ['查看任务', '任务模板、覆盖、测试记录'],
|
||||
'tasks.write': ['修改任务', '创建、修改、删除任务;发起或取消测试'],
|
||||
'data.read': ['读取原始数据', '原始块、导出'],
|
||||
'data.write': ['维护数据', '重算分析、清理'],
|
||||
'data.export': ['批量导出数据', '预留:按线路和时间批量拉取,带配额'],
|
||||
'settings.read': ['查看设置', '站点设置、保留策略'],
|
||||
'settings.write': ['修改设置', '站点设置、保留策略、重启计划'],
|
||||
'update.read': ['查看更新', '版本、发布清单、Backend 更新状态'],
|
||||
'update.confirm': ['确认更新', '确认 Backend 更新、请求节点更新'],
|
||||
'release.publish': ['登记发布清单', '给 CI 用,只能做这一件事'],
|
||||
'keys.read': ['查看 API 令牌', ''],
|
||||
'keys.manage': ['管理 API 令牌', '创建、吊销;只能授予自己拥有的能力'],
|
||||
'accounts.manage': ['管理账户', '面板用户、密码'],
|
||||
'events.read': ['查看事件', '节点上下线等事件流'],
|
||||
'audit.read': ['查看审计日志', ''],
|
||||
'ops.metrics': ['读取 /metrics', 'Prometheus 指标'],
|
||||
'webhooks.read': ['查看 webhook', '预留:外部回调订阅'],
|
||||
'webhooks.manage': ['管理 webhook', '预留:创建、修改、删除回调订阅,触发测试回调'],
|
||||
}
|
||||
|
||||
const GROUPS: Record<string, string> = {
|
||||
public: '展示',
|
||||
fleet: '节点',
|
||||
tasks: '任务',
|
||||
data: '数据',
|
||||
settings: '设置',
|
||||
update: '更新与发布',
|
||||
keys: 'API 令牌',
|
||||
accounts: '账户',
|
||||
ops: '运维与审计',
|
||||
integrations: '集成(预留)',
|
||||
}
|
||||
|
||||
const PRESETS: Record<string, string> = {
|
||||
'read-only': '只读',
|
||||
operations: '运维',
|
||||
enroll: '接入',
|
||||
'ci-release': 'CI 发布',
|
||||
all: '全部',
|
||||
}
|
||||
|
||||
/** 复选框:支持半选(indeterminate)。 */
|
||||
function Check({ checked, partial, disabled, onChange, label }: { checked: boolean; partial?: boolean; disabled?: boolean; onChange: (on: boolean) => void; label: React.ReactNode }) {
|
||||
const ref = useRef<HTMLInputElement>(null)
|
||||
useEffect(() => {
|
||||
if (ref.current) ref.current.indeterminate = !!partial && !checked
|
||||
}, [partial, checked])
|
||||
return (
|
||||
<label className="switch">
|
||||
<input ref={ref} type="checkbox" checked={checked} disabled={disabled} onChange={(e) => onChange(e.target.checked)} />
|
||||
{label}
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 能力选择器:分组复选框 + 预设 + 全选/组内全选(带半选)/反选 + 搜索。
|
||||
* `mine` 是创建者自己拥有的能力:不在里面的灰色不可选(不能授予自己没有的);写蕴含读,勾选写时读会一起被勾上。
|
||||
*/
|
||||
export function CapabilityPicker({ catalog, mine, value, onChange }: { catalog: CapabilityCatalogDto; mine: ReadonlySet<string>; value: ReadonlySet<string>; onChange: (next: Set<string>) => void }) {
|
||||
const [query, setQuery] = useState('')
|
||||
const info = useMemo(() => new Map(catalog.groups.flatMap((g) => g.capabilities.map((c) => [c.id, c] as const))), [catalog])
|
||||
const selectable = (id: string) => mine.has(id)
|
||||
|
||||
// 勾选一项:补上它蕴含的;取消一项:同时取消依赖它的(写依赖读)
|
||||
const closure = (ids: Iterable<string>): Set<string> => {
|
||||
const out = new Set<string>()
|
||||
const add = (id: string) => {
|
||||
if (out.has(id) || !selectable(id)) return
|
||||
out.add(id)
|
||||
info.get(id)?.implies.forEach(add)
|
||||
}
|
||||
for (const id of ids) add(id)
|
||||
return out
|
||||
}
|
||||
const set = (ids: Iterable<string>) => onChange(closure(ids))
|
||||
const toggle = (id: string, on: boolean) => {
|
||||
if (on) return set([...value, id])
|
||||
const next = new Set(value)
|
||||
next.delete(id)
|
||||
// 依赖被取消的项一起取消
|
||||
let changed = true
|
||||
while (changed) {
|
||||
changed = false
|
||||
for (const x of next) if (info.get(x)?.implies.some((i) => !next.has(i))) { next.delete(x); changed = true }
|
||||
}
|
||||
onChange(next)
|
||||
}
|
||||
|
||||
const needle = query.trim().toLowerCase()
|
||||
const visible = (id: string) => !needle || id.includes(needle) || (NAMES[id]?.[0] ?? '').includes(needle) || (NAMES[id]?.[1] ?? '').includes(needle)
|
||||
const all = [...info.keys()].filter(selectable)
|
||||
const allOn = all.length > 0 && all.every((id) => value.has(id))
|
||||
|
||||
return (
|
||||
<div className="stack" style={{ gap: 10 }}>
|
||||
<div className="row wrap" style={{ gap: 6 }}>
|
||||
<span className="muted">预设:</span>
|
||||
{catalog.presets.map((p) => (
|
||||
<button key={p.id} type="button" className="btn small" onClick={() => set(p.capabilities.filter(selectable))}>
|
||||
{PRESETS[p.id] ?? p.id}
|
||||
</button>
|
||||
))}
|
||||
<input className="input" style={{ maxWidth: 200, marginLeft: 'auto' }} placeholder="搜索能力" value={query} onChange={(e) => setQuery(e.target.value)} />
|
||||
</div>
|
||||
<div className="row wrap" style={{ gap: 12 }}>
|
||||
<Check checked={allOn} partial={value.size > 0} onChange={(on) => set(on ? all : [])} label={<b>全选</b>} />
|
||||
<button type="button" className="btn small text" onClick={() => set(all.filter((id) => !value.has(id)))}>
|
||||
反选
|
||||
</button>
|
||||
<span className="muted">已选 {value.size} 项</span>
|
||||
</div>
|
||||
{catalog.groups.map((g) => {
|
||||
const rows = g.capabilities.filter((c) => visible(c.id))
|
||||
if (rows.length === 0) return null
|
||||
const own = g.capabilities.filter((c) => selectable(c.id))
|
||||
const on = own.filter((c) => value.has(c.id))
|
||||
return (
|
||||
<div key={g.id} className="card" style={{ padding: 10 }}>
|
||||
<Check
|
||||
checked={own.length > 0 && on.length === own.length}
|
||||
partial={on.length > 0}
|
||||
disabled={own.length === 0}
|
||||
onChange={(next) => (next ? set([...value, ...own.map((c) => c.id)]) : onChange(new Set([...value].filter((id) => !own.some((c) => c.id === id)))))}
|
||||
label={<b>{GROUPS[g.id] ?? g.id}</b>}
|
||||
/>
|
||||
<div className="stack" style={{ gap: 4, marginTop: 6, paddingLeft: 22 }}>
|
||||
{rows.map((c) => (
|
||||
<div key={c.id} className="row wrap" style={{ gap: 8 }}>
|
||||
<Check
|
||||
checked={value.has(c.id)}
|
||||
disabled={!selectable(c.id)}
|
||||
onChange={(next) => toggle(c.id, next)}
|
||||
label={
|
||||
<>
|
||||
{NAMES[c.id]?.[0] ?? c.id}
|
||||
<span className="mono faint" style={{ marginLeft: 6 }}>
|
||||
{c.id}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{c.trust ? (
|
||||
<Chip tone="warn">
|
||||
<KeyRound size={12} /> 信任类
|
||||
</Chip>
|
||||
) : null}
|
||||
{c.reserved ? <Chip>预留</Chip> : null}
|
||||
{NAMES[c.id]?.[1] ? <span className="faint">{NAMES[c.id]![1]}</span> : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function capabilityName(id: string): string {
|
||||
return NAMES[id]?.[0] ?? id
|
||||
}
|
||||
+105
-26
@@ -1,12 +1,13 @@
|
||||
import { Download, KeyRound, Plus, Trash2 } from 'lucide-react'
|
||||
import { useEffect, useState, type ReactNode } from 'react'
|
||||
import { useEffect, useMemo, useState, type ReactNode } from 'react'
|
||||
import { api } from '@shared/api/client'
|
||||
import type { AgentDto, ApiTokenDto, BuildInfoDto, NodeUpdateDto, ReleaseDto, CreatedApiTokenDto, EventDto, Page, ProbeDto, RawChunkMetaDto, RecomputeDto, SettingDto, UserDto } from '@shared/api/types'
|
||||
import type { AgentDto, ApiTokenDto, BuildInfoDto, CapabilityCatalogDto, MeDto, NodeUpdateDto, ReleaseDto, CreatedApiTokenDto, EventDto, Page, ProbeDto, RawChunkMetaDto, RecomputeDto, SettingDto, UserDto } from '@shared/api/types'
|
||||
import { fmtBuiltAt, frontendBuild } from '@shared/buildInfo'
|
||||
import { fmtNum, fmtTime } from '@shared/format'
|
||||
import { translateSetting } from '@shared/i18n'
|
||||
import { useQuery } from '@shared/hooks'
|
||||
import { Chip, Confirm, CopyButton, Dialog, Empty, ErrorNotice, Field, Segmented, Spinner, useAction } from '@shared/ui'
|
||||
import { CapabilityPicker, capabilityName } from './CapabilityPicker'
|
||||
|
||||
type Tab = 'settings' | 'versions' | 'tokens' | 'users' | 'events' | 'raw' | 'analysis'
|
||||
|
||||
@@ -304,47 +305,93 @@ function Versions() {
|
||||
|
||||
function Tokens() {
|
||||
const tokens = useQuery(() => api.get<Page<ApiTokenDto>>('/api/admin/api-tokens'), [])
|
||||
const catalog = useQuery(() => api.get<CapabilityCatalogDto>('/api/admin/capabilities'), [])
|
||||
const me = useQuery(() => api.get<MeDto>('/api/admin/auth/me'), [])
|
||||
const { run } = useAction()
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [name, setName] = useState('')
|
||||
const [scope, setScope] = useState<'read' | 'admin' | 'release'>('read')
|
||||
const [days, setDays] = useState<number | null>(null)
|
||||
const [caps, setCaps] = useState<Set<string>>(new Set())
|
||||
const [created, setCreated] = useState<CreatedApiTokenDto>()
|
||||
const [revoking, setRevoking] = useState<ApiTokenDto>()
|
||||
const [revoking, setRevoking] = useState<ApiTokenDto[]>()
|
||||
const [picked, setPicked] = useState<Set<string>>(new Set())
|
||||
const items = tokens.data?.items ?? []
|
||||
const active = items.filter((t) => !t.revoked_at)
|
||||
const allPicked = active.length > 0 && active.every((t) => picked.has(t.id))
|
||||
const mine = useMemo(() => new Set(me.data?.capabilities ?? []), [me.data])
|
||||
const trust = useMemo(() => new Set((catalog.data?.groups ?? []).flatMap((g) => g.capabilities.filter((c) => c.trust).map((c) => c.id))), [catalog.data])
|
||||
const open = () => {
|
||||
setName('')
|
||||
setDays(null)
|
||||
setCaps(new Set())
|
||||
setCreating(true)
|
||||
}
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="row" style={{ marginBottom: 12 }}>
|
||||
<span className="muted">给脚本和其他程序用的令牌:read 只能读展示数据,admin 可以调用全部管理接口。</span>
|
||||
<button type="button" className="btn primary right" onClick={() => setCreating(true)}>
|
||||
<Plus size={18} />
|
||||
创建令牌
|
||||
</button>
|
||||
<span className="muted">给脚本和其他程序用的令牌:按需勾选能力,只能授予自己拥有的能力。</span>
|
||||
<span className="right row">
|
||||
{picked.size > 0 ? (
|
||||
<button type="button" className="btn small danger" onClick={() => setRevoking(active.filter((t) => picked.has(t.id)))}>
|
||||
<Trash2 size={14} />
|
||||
吊销所选({picked.size})
|
||||
</button>
|
||||
) : null}
|
||||
<button type="button" className="btn primary" onClick={open}>
|
||||
<Plus size={18} />
|
||||
创建令牌
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<ErrorNotice error={tokens.error} />
|
||||
{(tokens.data?.items ?? []).length === 0 ? (
|
||||
{items.length === 0 ? (
|
||||
<Empty>没有 API 令牌</Empty>
|
||||
) : (
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 28 }}>
|
||||
<input type="checkbox" aria-label="全选" checked={allPicked} onChange={(e) => setPicked(e.target.checked ? new Set(active.map((t) => t.id)) : new Set())} />
|
||||
</th>
|
||||
<th>名称</th>
|
||||
<th>权限</th>
|
||||
<th>能力</th>
|
||||
<th>过期</th>
|
||||
<th>创建</th>
|
||||
<th>最后使用</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tokens.data?.items.map((t) => (
|
||||
{items.map((t) => (
|
||||
<tr key={t.id}>
|
||||
<td style={{ fontWeight: 600 }}>{t.name}</td>
|
||||
<td>
|
||||
<Chip tone={t.scope === 'admin' ? 'warn' : ''}>{t.scope}</Chip>
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label={`选择 ${t.name}`}
|
||||
disabled={!!t.revoked_at}
|
||||
checked={picked.has(t.id)}
|
||||
onChange={(e) => {
|
||||
const next = new Set(picked)
|
||||
if (e.target.checked) next.add(t.id)
|
||||
else next.delete(t.id)
|
||||
setPicked(next)
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td style={{ fontWeight: 600 }}>{t.name}</td>
|
||||
<td title={t.capabilities.map(capabilityName).join('、')}>
|
||||
<Chip tone={t.capabilities.some((c) => trust.has(c)) ? 'warn' : ''}>{t.capabilities.length} 项</Chip>{' '}
|
||||
<span className="faint">
|
||||
{t.capabilities.slice(0, 2).map(capabilityName).join('、')}
|
||||
{t.capabilities.length > 2 ? '…' : ''}
|
||||
</span>
|
||||
</td>
|
||||
<td className="muted">{t.expires_at ? fmtTime(t.expires_at) : '不过期'}</td>
|
||||
<td className="muted">{fmtTime(t.created_at)}</td>
|
||||
<td className="muted">{t.revoked_at ? <Chip tone="bad">已吊销</Chip> : fmtTime(t.last_used_at)}</td>
|
||||
<td className="r">
|
||||
{!t.revoked_at ? (
|
||||
<button type="button" className="btn small danger" onClick={() => setRevoking(t)}>
|
||||
<button type="button" className="btn small danger" onClick={() => setRevoking([t])}>
|
||||
<Trash2 size={14} />
|
||||
吊销
|
||||
</button>
|
||||
@@ -367,16 +414,15 @@ function Tokens() {
|
||||
<button
|
||||
type="button"
|
||||
className="btn primary"
|
||||
disabled={!name.trim()}
|
||||
disabled={!name.trim() || caps.size === 0}
|
||||
onClick={async () => {
|
||||
let result: CreatedApiTokenDto | undefined
|
||||
const ok = await run(async () => {
|
||||
result = await api.post<CreatedApiTokenDto>('/api/admin/api-tokens', { name: name.trim(), scope })
|
||||
result = await api.post<CreatedApiTokenDto>('/api/admin/api-tokens', { name: name.trim(), capabilities: [...caps], expires_in_days: days })
|
||||
})
|
||||
if (ok && result) {
|
||||
setCreated(result)
|
||||
setCreating(false)
|
||||
setName('')
|
||||
tokens.reload()
|
||||
}
|
||||
}}
|
||||
@@ -387,29 +433,62 @@ function Tokens() {
|
||||
}
|
||||
>
|
||||
<div className="stack">
|
||||
<Field label="名称">
|
||||
<input className="input" value={name} onChange={(e) => setName(e.target.value)} autoFocus />
|
||||
</Field>
|
||||
<Field label="权限">
|
||||
<Segmented options={[{ value: 'read' as const, label: 'read(只读)' }, { value: 'admin' as const, label: 'admin(全部)' }, { value: 'release' as const, label: 'release(只登记发布,给 CI)' }]} value={scope} onChange={setScope} />
|
||||
<div className="form-grid">
|
||||
<Field label="名称">
|
||||
<input className="input" value={name} onChange={(e) => setName(e.target.value)} autoFocus />
|
||||
</Field>
|
||||
<Field label="有效期">
|
||||
<select className="input" value={days ?? ''} onChange={(e) => setDays(e.target.value ? Number(e.target.value) : null)}>
|
||||
<option value="">不过期</option>
|
||||
<option value="1">1 天</option>
|
||||
<option value="7">7 天</option>
|
||||
<option value="30">30 天</option>
|
||||
<option value="90">90 天</option>
|
||||
<option value="365">365 天</option>
|
||||
</select>
|
||||
</Field>
|
||||
</div>
|
||||
<Field label="能力" hint="写蕴含读:勾选“修改”会自动带上对应的“查看”。灰色的是你自己没有的能力,不能授予。">
|
||||
{catalog.data ? <CapabilityPicker catalog={catalog.data} mine={mine} value={caps} onChange={setCaps} /> : <Spinner />}
|
||||
</Field>
|
||||
</div>
|
||||
</Dialog>
|
||||
) : null}
|
||||
{created ? (
|
||||
<Dialog title="令牌已创建" onClose={() => setCreated(undefined)} actions={<button type="button" className="btn primary" onClick={() => setCreated(undefined)}>完成</button>}>
|
||||
<Dialog
|
||||
title="令牌已创建"
|
||||
onClose={() => setCreated(undefined)}
|
||||
actions={
|
||||
<button type="button" className="btn primary" onClick={() => setCreated(undefined)}>
|
||||
完成
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<div className="stack">
|
||||
<div className="notice warn">令牌明文只显示这一次,请现在就复制。</div>
|
||||
<pre className="code">{created.plaintext}</pre>
|
||||
<div>
|
||||
<CopyButton text={created.plaintext} />
|
||||
</div>
|
||||
<p className="muted">
|
||||
{created.token.capabilities.length} 项能力:{created.token.capabilities.map(capabilityName).join('、')}。{created.token.expires_at ? `${fmtTime(created.token.expires_at)} 过期。` : '不会过期。'}
|
||||
</p>
|
||||
</div>
|
||||
</Dialog>
|
||||
) : null}
|
||||
{revoking ? (
|
||||
<Confirm title="吊销令牌" danger confirmText="吊销" onClose={() => setRevoking(undefined)} onConfirm={async () => { await run(() => api.del(`/api/admin/api-tokens/${revoking.id}`), '已吊销'); tokens.reload() }}>
|
||||
使用「{revoking.name}」的程序会立刻失去访问权限。
|
||||
<Confirm
|
||||
title="吊销令牌"
|
||||
danger
|
||||
confirmText={`吊销 ${revoking.length} 个`}
|
||||
onClose={() => setRevoking(undefined)}
|
||||
onConfirm={async () => {
|
||||
for (const t of revoking) await run(() => api.del(`/api/admin/api-tokens/${t.id}`), `已吊销 ${t.name}`)
|
||||
setPicked(new Set())
|
||||
tokens.reload()
|
||||
}}
|
||||
>
|
||||
使用 {revoking.map((t) => `「${t.name}」`).join('、')} 的程序会立刻失去访问权限。
|
||||
</Confirm>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -738,7 +738,7 @@ export interface paths {
|
||||
/** API tokens */
|
||||
get: operations["api_tokens_list"];
|
||||
put?: never;
|
||||
/** Create an API token (scope read or admin); the plaintext is returned only once */
|
||||
/** Create an API token with the chosen capabilities (a subset of your own); the plaintext is returned only once */
|
||||
post: operations["api_tokens_create"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
@@ -763,6 +763,23 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/admin/capabilities": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** The capability catalog: groups, which ones change trust, which are reserved, and the presets */
|
||||
get: operations["capabilities_list"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/admin/settings": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -943,7 +960,7 @@ export interface paths {
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Prometheus metrics (admin token) */
|
||||
/** Prometheus metrics (requires ops.metrics) */
|
||||
get: operations["metrics_get"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
@@ -1069,12 +1086,14 @@ export interface components {
|
||||
ApiTokenDto: {
|
||||
id: string;
|
||||
name: string;
|
||||
scope: string;
|
||||
capabilities: string[];
|
||||
/** Format: date-time */
|
||||
created_at: unknown;
|
||||
/** Format: date-time */
|
||||
last_used_at: unknown;
|
||||
/** Format: date-time */
|
||||
expires_at: unknown;
|
||||
/** Format: date-time */
|
||||
revoked_at: unknown;
|
||||
};
|
||||
BackendUpdateDto: {
|
||||
@@ -1104,6 +1123,24 @@ export interface components {
|
||||
nodes: components["schemas"]["NodeBuildDto"][];
|
||||
backend_update?: null | components["schemas"]["BackendUpdateDto"];
|
||||
};
|
||||
CapabilityCatalogDto: {
|
||||
groups: components["schemas"]["CapabilityGroupDto"][];
|
||||
presets: components["schemas"]["CapabilityPresetDto"][];
|
||||
};
|
||||
CapabilityDto: {
|
||||
id: string;
|
||||
trust: boolean;
|
||||
reserved: boolean;
|
||||
implies: string[];
|
||||
};
|
||||
CapabilityGroupDto: {
|
||||
id: string;
|
||||
capabilities: components["schemas"]["CapabilityDto"][];
|
||||
};
|
||||
CapabilityPresetDto: {
|
||||
id: string;
|
||||
capabilities: string[];
|
||||
};
|
||||
ChangePasswordRequest: {
|
||||
password: string;
|
||||
};
|
||||
@@ -1146,7 +1183,9 @@ export interface components {
|
||||
};
|
||||
CreateApiTokenRequest: {
|
||||
name: string;
|
||||
scope: string;
|
||||
capabilities: string[];
|
||||
/** Format: int32 */
|
||||
expires_in_days?: null | number | string;
|
||||
};
|
||||
CreateJoinTokenRequest: {
|
||||
note: null | string;
|
||||
@@ -1407,7 +1446,7 @@ export interface components {
|
||||
};
|
||||
MeDto: {
|
||||
kind: string;
|
||||
scope: string;
|
||||
capabilities: string[];
|
||||
user_id: null | string;
|
||||
username: null | string;
|
||||
csrf_token: null | string;
|
||||
@@ -4485,6 +4524,44 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
capabilities_list: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["CapabilityCatalogDto"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDto"];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDto"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
settings_list: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
@@ -39,6 +39,8 @@ export type JoinTokenDto = Narrow<S['JoinTokenDto']>
|
||||
export type CreatedJoinTokenDto = Narrow<S['CreatedJoinTokenDto']>
|
||||
export type ApiTokenDto = Narrow<S['ApiTokenDto']>
|
||||
export type CreatedApiTokenDto = Narrow<S['CreatedApiTokenDto']>
|
||||
export type CapabilityCatalogDto = Narrow<S['CapabilityCatalogDto']>
|
||||
export type MeDto = Narrow<S['MeDto']>
|
||||
export type SettingDto = Narrow<S['SettingDto']>
|
||||
export type UserDto = Narrow<S['UserDto']>
|
||||
export type EventDto = Narrow<S['EventDto']>
|
||||
@@ -46,7 +48,6 @@ export type GapDto = Narrow<S['GapDto']>
|
||||
export type RecomputeDto = Narrow<S['RecomputeDto']>
|
||||
export type RawChunkMetaDto = Narrow<S['RawChunkMetaDto']>
|
||||
export type LoginResponse = Narrow<S['LoginResponse']>
|
||||
export type MeDto = Narrow<S['MeDto']>
|
||||
|
||||
export interface Page<T> {
|
||||
items: T[]
|
||||
|
||||
Reference in new issue
Block a user