Skip to content
Clone

refactor(admin)!: serve the admin API over gRPC instead of an embedded SPA

The admin console moved to its own repository, so the core no longer
bundles or serves the React admin SPA.

- Delete the `admin` crate (admin-web) and drop it from the workspace,
  the Docker build (which also stops installing bun builds for it) and
  the dev scripts/justfile recipes.
- `app/adminhost` no longer mounts the SPA, the session store, CORS and
  request-body limits; it now only serves healthz plus the admin gRPC
  service on `admin.grpc_port` (default 3014) for a trusted backend.
- Add `lib/admin-rpc`: the protobuf contracts (act/chat/logs/mcp/queues/
  repos/runners/spaces/storage/system/tasks/admin) and the tonic server
  that maps them onto `lib/admin`'s services, with a trusted-key
  interceptor (`admin.grpc_trusted_keys`, empty rejects every call).
- `AdminConfig` drops `seed_username`/`seed_password` and the session
  cookie/table names in favour of `grpc_port` and `grpc_trusted_keys`.
- The chart drops the adminhost Ingress and its seed secret and exposes
  the new gRPC port; `app/openapi` only emits the web document again.
ZhenYi committed
parent 808e816 commit eba4fd1 Browse files

156 files changed

+688 −18044

This change is very large, so some diffs are not shown. Clone the repository to see the full change.

File tree 156
Deleted admin/src/app/index.tsx
@@ -1,17 +0,0 @@
1import { StrictMode } from 'react'
2import { createRoot } from 'react-dom/client'
3import { RouterProvider } from 'react-router-dom'
4import { SessionProvider } from '@/app/session'
5import { ToastProvider } from '@/shared/ui'
6import { router } from './routes'
7import './styles.css'
8
9createRoot(document.getElementById('root')!).render(
10 <StrictMode>
11 <ToastProvider>
12 <SessionProvider>
13 <RouterProvider router={router} />
14 </SessionProvider>
15 </ToastProvider>
16 </StrictMode>,
17)
No newline at end of file
Deleted admin/src/app/layout.tsx
@@ -1,161 +0,0 @@
1import {
2 Activity,
3 Database,
4 FolderGit2,
5 HardDrive,
6 LayoutDashboard,
7 ListChecks,
8 LogOut,
9 MessagesSquare,
10 Moon,
11 ScrollText,
12 Settings,
13 ShieldCheck,
14 Sun,
15 Users,
16 Workflow,
17} from 'lucide-react'
18import { useEffect, useState } from 'react'
19import { Menu, X } from 'lucide-react'
20import { NavLink, Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom'
21import { Cpu } from 'lucide-react'
22import { useSession } from '@/app/session'
23import { useTheme } from '@/app/theme'
24import { Badge, Button, Loading } from '@/shared/ui'
25
26const NAV_ITEMS = [
27 { to: '/', label: '系统状态', icon: LayoutDashboard, end: true },
28 { to: '/users', label: '用户', icon: Users },
29 { to: '/spaces', label: 'Space', icon: Database },
30 { to: '/repos', label: '仓库', icon: FolderGit2 },
31 { to: '/chat', label: '聊天记录', icon: MessagesSquare },
32 { to: '/storage', label: '存储', icon: HardDrive },
33 { to: '/queues', label: '队列', icon: Activity },
34 { to: '/act', label: 'ACT', icon: Workflow },
35 { to: '/runners', label: 'Runner', icon: Cpu },
36 { to: '/tasks', label: '任务', icon: ListChecks },
37 { to: '/logs', label: '日志', icon: ScrollText },
38 { to: '/admins', label: '管理员', icon: ShieldCheck },
39 { to: '/system', label: '系统配置', icon: Settings },
40]
41
42function roleLabel(role: string): string {
43 switch (role) {
44 case 'super_admin':
45 return '超级管理员'
46 case 'operator':
47 return '运维'
48 default:
49 return '只读'
50 }
51}
52
53export function AdminShell() {
54 const { me, loading, logout } = useSession()
55 const { dark, toggle } = useTheme()
56 const navigate = useNavigate()
57 const location = useLocation()
58 const [menuOpen, setMenuOpen] = useState(false)
59
60 // The drawer belongs to the current page only: any navigation closes it.
61 useEffect(() => { setMenuOpen(false) }, [location.pathname])
62
63 if (loading) {
64 return <Loading label="正在校验管理会话…" />
65 }
66 if (!me) {
67 return <Navigate to="/login" replace />
68 }
69 if (me.must_change_password) {
70 return <Navigate to="/password" replace />
71 }
72
73 return (
74 <div className="flex min-h-full">
75 {menuOpen ? (
76 <button
77 type="button"
78 aria-label="关闭导航"
79 tabIndex={-1}
80 onClick={() => setMenuOpen(false)}
81 className="fixed inset-0 z-40 bg-slate-900/40 md:hidden"
82 />
83 ) : null}
84 <aside
85 className={`fixed inset-y-0 left-0 z-50 flex w-56 shrink-0 flex-col overflow-y-auto border-r border-slate-200 bg-white p-3 transition-transform duration-200 dark:border-slate-800 dark:bg-slate-950 md:static md:z-auto md:translate-x-0 ${
86 menuOpen ? 'translate-x-0' : 'invisible -translate-x-full md:visible md:translate-x-0'
87 }`}
88 >
89 <div className="mb-4 flex items-start justify-between gap-2 px-2">
90 <div>
91 <div className="text-sm font-semibold text-slate-900 dark:text-slate-100">GitData</div>
92 <div className="text-xs text-slate-500">管理后台</div>
93 </div>
94 <span className="md:hidden">
95 <Button variant="ghost" size="sm" title="关闭导航" onClick={() => setMenuOpen(false)}>
96 <X size={16} />
97 </Button>
98 </span>
99 </div>
100 <nav className="flex flex-1 flex-col gap-1">
101 {NAV_ITEMS.map((item) => (
102 <NavLink
103 key={item.to}
104 to={item.to}
105 end={item.end}
106 className={({ isActive }) =>
107 `flex items-center gap-2 rounded-md px-2 py-1.5 text-sm ${
108 isActive
109 ? 'bg-slate-900 text-white dark:bg-slate-100 dark:text-slate-900'
110 : 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'
111 }`
112 }
113 >
114 <item.icon size={16} />
115 {item.label}
116 </NavLink>
117 ))}
118 </nav>
119 <div className="mt-4 border-t border-slate-200 pt-3 text-xs text-slate-500 dark:border-slate-800">
120 adminhost
121 </div>
122 </aside>
123
124 <div className="flex min-w-0 flex-1 flex-col">
125 <header className="flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 bg-white px-4 py-3 dark:border-slate-800 dark:bg-slate-950">
126 <div className="flex min-w-0 items-center gap-2">
127 <span className="md:hidden">
128 <Button variant="ghost" size="sm" title="打开导航" onClick={() => setMenuOpen(true)}>
129 <Menu size={18} />
130 </Button>
131 </span>
132 <span className="truncate font-medium text-slate-800 dark:text-slate-100">{me.display_name}</span>
133 <Badge tone={me.role === 'super_admin' ? 'danger' : 'info'}>{roleLabel(me.role)}</Badge>
134 </div>
135 <div className="flex flex-wrap items-center gap-2">
136 <Button variant="ghost" size="sm" title="切换主题" onClick={toggle}>
137 {dark ? <Sun size={16} /> : <Moon size={16} />}
138 </Button>
139 <Button variant="secondary" size="sm" onClick={() => navigate('/password')}>
140 修改口令
141 </Button>
142 <Button
143 variant="secondary"
144 size="sm"
145 onClick={async () => {
146 await logout()
147 navigate('/login')
148 }}
149 >
150 <LogOut size={14} />
151 退出
152 </Button>
153 </div>
154 </header>
155 <main className="min-w-0 flex-1 overflow-y-auto p-4">
156 <Outlet />
157 </main>
158 </div>
159 </div>
160 )
161}
Deleted admin/src/app/routes.tsx
@@ -1,52 +0,0 @@
1import { createBrowserRouter, Navigate } from 'react-router-dom'
2import { AdminShell } from '@/app/layout'
3import { ActPage } from '@/pages/act'
4import { AdminsPage } from '@/pages/admins'
5import { ChatRoomDetailPage } from '@/pages/chat-room-detail'
6import { ChatRoomsPage } from '@/pages/chat-rooms'
7import { ChatMessagesPage } from '@/pages/chat-messages'
8import { DashboardPage } from '@/pages/dashboard'
9import { LoginPage } from '@/pages/login'
10import { LogsPage } from '@/pages/logs'
11import { NotFoundPage } from '@/pages/not-found'
12import { PasswordPage } from '@/pages/password'
13import { QueuesPage } from '@/pages/queues'
14import { ReposPage } from '@/pages/repos'
15import { RunnersPage } from '@/pages/runners'
16import { SpaceDetailPage } from '@/pages/space-detail'
17import { SpacesPage } from '@/pages/spaces'
18import { StoragePage } from '@/pages/storage'
19import { SystemPage } from '@/pages/system'
20import { TasksPage } from '@/pages/tasks'
21import { UserDetailPage } from '@/pages/user-detail'
22import { UsersPage } from '@/pages/users'
23
24export const router = createBrowserRouter([
25 { path: '/login', element: <LoginPage /> },
26 { path: '/password', element: <PasswordPage /> },
27 {
28 path: '/',
29 element: <AdminShell />,
30 children: [
31 { index: true, element: <DashboardPage /> },
32 { path: 'users', element: <UsersPage /> },
33 { path: 'users/:uid', element: <UserDetailPage /> },
34 { path: 'spaces', element: <SpacesPage /> },
35 { path: 'spaces/:id', element: <SpaceDetailPage /> },
36 { path: 'repos', element: <ReposPage /> },
37 { path: 'chat', element: <ChatRoomsPage /> },
38 { path: 'chat/rooms/:id', element: <ChatRoomDetailPage /> },
39 { path: 'chat/messages', element: <ChatMessagesPage /> },
40 { path: 'storage', element: <StoragePage /> },
41 { path: 'queues', element: <QueuesPage /> },
42 { path: 'act', element: <ActPage /> },
43 { path: 'runners', element: <RunnersPage /> },
44 { path: 'tasks', element: <TasksPage /> },
45 { path: 'logs', element: <LogsPage /> },
46 { path: 'admins', element: <AdminsPage /> },
47 { path: 'system', element: <SystemPage /> },
48 { path: '*', element: <NotFoundPage /> },
49 ],
50 },
51 { path: '*', element: <Navigate to="/" replace /> },
52])
Deleted admin/src/app/session.tsx
@@ -1,53 +0,0 @@
1import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
2import type { ReactNode } from 'react'
3import { adminAuth } from '@/shared/api'
4import type { AdminDto } from '@/shared/api'
5
6interface SessionValue {
7 me: AdminDto | null
8 loading: boolean
9 refresh: () => Promise<void>
10 logout: () => Promise<void>
11}
12
13const SessionContext = createContext<SessionValue>({
14 me: null,
15 loading: true,
16 refresh: async () => {},
17 logout: async () => {},
18})
19
20export function SessionProvider({ children }: { children: ReactNode }) {
21 const [me, setMe] = useState<AdminDto | null>(null)
22 const [loading, setLoading] = useState(true)
23
24 const refresh = useCallback(async () => {
25 try {
26 const value = await adminAuth.me()
27 setMe(value)
28 } catch {
29 setMe(null)
30 } finally {
31 setLoading(false)
32 }
33 }, [])
34
35 useEffect(() => {
36 void refresh()
37 }, [refresh])
38
39 const logout = useCallback(async () => {
40 try {
41 await adminAuth.logout()
42 } catch {
43 }
44 setMe(null)
45 }, [])
46
47 const value = useMemo(() => ({ me, loading, refresh, logout }), [me, loading, refresh, logout])
48 return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>
49}
50
51export function useSession() {
52 return useContext(SessionContext)
53}
No newline at end of file
Deleted admin/src/app/styles.css
@@ -1,21 +0,0 @@
1@import "tailwindcss";
2
3@custom-variant dark (&:where(.dark, .dark *));
4
5html,
6body,
7#root {
8 height: 100%;
9}
10
11body {
12 margin: 0;
13 font-family: "Geist", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
14 background-color: #f8fafc;
15 color: #0f172a;
16}
17
18.dark body {
19 background-color: #020617;
20 color: #e2e8f0;
21}
No newline at end of file
Deleted admin/src/app/theme.ts
@@ -1,21 +0,0 @@
1import { useCallback, useEffect, useState } from 'react'
2
3const THEME_KEY = 'gitdata-admin-theme'
4
5export function useTheme() {
6 const [dark, setDark] = useState(() => {
7 const stored = window.localStorage.getItem(THEME_KEY)
8 if (stored) {
9 return stored === 'dark'
10 }
11 return window.matchMedia('(prefers-color-scheme: dark)').matches
12 })
13
14 useEffect(() => {
15 document.documentElement.classList.toggle('dark', dark)
16 window.localStorage.setItem(THEME_KEY, dark ? 'dark' : 'light')
17 }, [dark])
18
19 const toggle = useCallback(() => setDark((value) => !value), [])
20 return { dark, toggle }
21}
No newline at end of file
Deleted admin/src/pages/act.tsx
@@ -1,297 +0,0 @@
1import { useCallback, useEffect, useState } from 'react'
2import { useNavigate } from 'react-router-dom'
3import { adminAct, adminRunners } from '@/shared/api'
4import type { AdminActRun, AdminActRunPage, AdminActSummary, RunnerSummary } from '@/shared/api'
5import { ApiError } from '@/shared/api'
6import { formatDateTime, shortId } from '@/shared/format'
7import {
8 Badge,
9 Button,
10 Card,
11 ConfirmDialog,
12 DataTable,
13 ErrorBanner,
14 Field,
15 Loading,
16 Pagination,
17 Select,
18 statusTone,
19 TextInput,
20 useToast,
21 type Column,
22} from '@/shared/ui'
23
24const PAGE_SIZE = 20
25
26export function ActPage() {
27 const navigate = useNavigate()
28 const toast = useToast()
29 const [summary, setSummary] = useState<AdminActSummary | null>(null)
30 const [runners, setRunners] = useState<RunnerSummary[] | null>(null)
31 const [runs, setRuns] = useState<AdminActRunPage | null>(null)
32 const [error, setError] = useState<string | null>(null)
33 const [busy, setBusy] = useState(false)
34 const [query, setQuery] = useState('')
35 const [state, setState] = useState('')
36 const [applied, setApplied] = useState({ query: '', state: '' })
37 const [offset, setOffset] = useState(0)
38 const [cancelTarget, setCancelTarget] = useState<AdminActRun | null>(null)
39
40 const load = useCallback(async () => {
41 setBusy(true)
42 setError(null)
43 try {
44 const [nextSummary, nextRuns, nextRunners] = await Promise.all([
45 adminAct.summary(),
46 adminAct.runs({
47 q: applied.query || undefined,
48 state: applied.state || undefined,
49 limit: PAGE_SIZE,
50 offset,
51 }),
52 adminRunners.list({ limit: 50, offset: 0 }),
53 ])
54 setSummary(nextSummary)
55 setRuns(nextRuns)
56 setRunners(nextRunners.items)
57 } catch (err) {
58 setError(err instanceof ApiError ? err.message : 'ACT 数据加载失败')
59 } finally {
60 setBusy(false)
61 }
62 }, [applied, offset])
63
64 useEffect(() => {
65 void load()
66 }, [load])
67
68 const runAction = async (action: () => Promise<unknown>, success: string) => {
69 try {
70 await action()
71 toast(success)
72 void load()
73 } catch (err) {
74 toast(err instanceof ApiError ? err.message : '操作失败', 'danger')
75 }
76 }
77
78 const runnerColumns: Column<RunnerSummary>[] = [
79 { key: 'name', title: '名称', render: (row) => row.name },
80 {
81 key: 'protocol',
82 title: '协议',
83 render: (row) => (row.protocol_version ? `v${row.protocol_version}` : '—'),
84 },
85 {
86 key: 'capabilities',
87 title: '能力',
88 render: (row) => (row.capabilities.length > 0 ? row.capabilities.join(', ') : '—'),
89 },
90 {
91 key: 'load',
92 title: '负载',
93 render: (row) => `${row.active_jobs}/${row.capacity}`,
94 },
95 {
96 key: 'state',
97 title: '状态',
98 render: (row) => (
99 <Badge tone={row.state === 'active' ? 'success' : 'warning'}>
100 {row.state === 'active' ? '启用' : '停用'}
101 </Badge>
102 ),
103 },
104 {
105 key: 'last_seen',
106 title: '最近心跳',
107 render: (row) => (row.last_seen_at ? formatDateTime(row.last_seen_at) : '—'),
108 },
109 ]
110
111 const runColumns: Column<AdminActRun>[] = [
112 {
113 key: 'repo',
114 title: '仓库',
115 render: (row) => <span className="font-medium">{row.repo_full_name}</span>,
116 },
117 {
118 key: 'workflow',
119 title: '工作流',
120 render: (row) => (
121 <div>
122 <div>{row.display_name ?? row.workflow_name}</div>
123 <div className="text-xs text-slate-500">{row.workflow_file}</div>
124 </div>
125 ),
126 },
127 {
128 key: 'run',
129 title: '运行',
130 render: (row) => (
131 <div>
132 <div>{row.run_number ? `#${row.run_number}` : shortId(row.id)}</div>
133 <div className="text-xs text-slate-500">{row.event} · {row.branch ?? row.tag ?? '—'}</div>
134 </div>
135 ),
136 },
137 {
138 key: 'state',
139 title: '状态',
140 render: (row) => <Badge tone={statusTone(row.state)}>{row.state}</Badge>,
141 },
142 {
143 key: 'jobs',
144 title: 'Job',
145 render: (row) => `${row.jobs_active}/${row.jobs_total}`,
146 },
147 {
148 key: 'created',
149 title: '创建时间',
150 render: (row) => <span className="text-xs text-slate-500">{formatDateTime(row.created_at)}</span>,
151 },
152 {
153 key: 'actions',
154 title: '操作',
155 render: (row) => {
156 const active = ['queued', 'running', 'cancel_requested'].includes(row.state)
157 const finished = ['success', 'failure', 'cancelled'].includes(row.state)
158 return (
159 <div className="flex flex-wrap gap-1">
160 {active ? (
161 <Button size="sm" variant="danger" onClick={() => setCancelTarget(row)}>
162 取消
163 </Button>
164 ) : null}
165 {finished ? (
166 <Button
167 size="sm"
168 variant="secondary"
169 onClick={() => void runAction(() => adminAct.rerunRun(row.id), '已创建重跑任务')}
170 >
171 重跑
172 </Button>
173 ) : null}
174 </div>
175 )
176 },
177 },
178 ]
179
180 return (
181 <div className="space-y-5">
182 <div className="flex flex-wrap items-center justify-between gap-3">
183 <div>
184 <h1 className="text-xl font-semibold">ACT 管理</h1>
185 <p className="mt-1 text-xs text-slate-500">监控 act runner,并管理 Actions 工作流运行。</p>
186 </div>
187 <div className="flex gap-2">
188 <Button variant="secondary" onClick={() => navigate('/runners')}>
189 Runner 与令牌
190 </Button>
191 <Button disabled={busy} onClick={() => void load()}>
192 刷新
193 </Button>
194 </div>
195 </div>
196
197 <ErrorBanner message={error} />
198
199 <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
200 <Card title="Runner">
201 <div className="text-2xl font-semibold">
202 {summary?.runners_active ?? '—'}/{summary?.runners_total ?? '—'}
203 </div>
204 <div className="mt-1 text-xs text-slate-500">启用 / 总数,忙碌 {summary?.runners_busy ?? '—'}</div>
205 </Card>
206 <Card title="作业">
207 <div className="text-2xl font-semibold">{summary?.jobs_running ?? '—'}</div>
208 <div className="mt-1 text-xs text-slate-500">运行中,排队 {summary?.jobs_queued ?? '—'}</div>
209 </Card>
210 <Card title="运行">
211 <div className="text-2xl font-semibold">{summary?.runs_running ?? '—'}</div>
212 <div className="mt-1 text-xs text-slate-500">运行中,排队 {summary?.runs_queued ?? '—'}</div>
213 </Card>
214 <Card title="累计结果">
215 <div className="text-2xl font-semibold">
216 <span className="text-emerald-600">{summary?.runs_success ?? '—'}</span>
217 <span className="mx-1 text-slate-400">/</span>
218 <span className="text-red-600">{summary?.runs_failure ?? '—'}</span>
219 </div>
220 <div className="mt-1 text-xs text-slate-500">成功 / 失败</div>
221 </Card>
222 </div>
223
224 <Card title="ACT Runner">
225 {runners === null ? (
226 <Loading />
227 ) : (
228 <DataTable
229 columns={runnerColumns}
230 rows={runners}
231 rowKey={(row) => row.id}
232 empty="还没有 act runner 注册"
233 />
234 )}
235 </Card>
236
237 <Card title="工作流运行">
238 <form
239 className="mb-4 flex flex-wrap items-end gap-2"
240 onSubmit={(event) => {
241 event.preventDefault()
242 setOffset(0)
243 setApplied({ query, state })
244 }}
245 >
246 <Field label="关键字">
247 <TextInput value={query} onChange={setQuery} placeholder="工作流 / 提交 SHA" />
248 </Field>
249 <Field label="状态">
250 <Select
251 value={state}
252 onChange={setState}
253 options={[
254 { value: '', label: '全部' },
255 { value: 'queued', label: '排队' },
256 { value: 'running', label: '运行中' },
257 { value: 'success', label: '成功' },
258 { value: 'failure', label: '失败' },
259 { value: 'cancelled', label: '已取消' },
260 ]}
261 />
262 </Field>
263 <Button type="submit" disabled={busy}>查询</Button>
264 </form>
265
266 {runs === null ? (
267 <Loading />
268 ) : (
269 <>
270 <DataTable
271 columns={runColumns}
272 rows={runs.items}
273 rowKey={(row) => row.id}
274 empty="没有匹配的工作流运行"
275 />
276 <Pagination total={runs.total} limit={runs.limit} offset={runs.offset} onChange={setOffset} />
277 </>
278 )}
279 </Card>
280
281 <ConfirmDialog
282 open={cancelTarget !== null}
283 title="取消 ACT 运行"
284 description={`确定取消运行「${cancelTarget?.display_name ?? cancelTarget?.workflow_name ?? ''}」吗?`}
285 confirmLabel="取消运行"
286 onClose={() => setCancelTarget(null)}
287 onConfirm={() => {
288 const target = cancelTarget
289 setCancelTarget(null)
290 if (target) {
291 void runAction(() => adminAct.cancelRun(target.id), '已请求取消')
292 }
293 }}
294 />
295 </div>
296 )
297}
Deleted admin/src/pages/admins.tsx
@@ -1,330 +0,0 @@
1import { useCallback, useEffect, useState } from 'react'
2import { useSession } from '@/app/session'
3import { adminAccounts } from '@/shared/api'
4import type { AdminDto, Page } from '@/shared/api'
5import { ApiError } from '@/shared/api'
6import { formatDateTime } from '@/shared/format'
7import {
8 Badge,
9 Button,
10 Card,
11 ConfirmDialog,
12 DataTable,
13 ErrorBanner,
14 Field,
15 Loading,
16 Modal,
17 Pagination,
18 Select,
19 TextInput,
20 useToast,
21} from '@/shared/ui'
22
23const PAGE_SIZE = 20
24
25const ROLE_OPTIONS = [
26 { value: 'super_admin', label: '超级管理员' },
27 { value: 'operator', label: '运维' },
28 { value: 'viewer', label: '只读' },
29]
30
31export function AdminsPage() {
32 const { me } = useSession()
33 const toast = useToast()
34 const [data, setData] = useState<Page<AdminDto> | null>(null)
35 const [offset, setOffset] = useState(0)
36 const [error, setError] = useState<string | null>(null)
37 const [createOpen, setCreateOpen] = useState(false)
38 const [username, setUsername] = useState('')
39 const [displayName, setDisplayName] = useState('')
40 const [password, setPassword] = useState('')
41 const [role, setRole] = useState('operator')
42 const [editTarget, setEditTarget] = useState<AdminDto | null>(null)
43 const [editDisplayName, setEditDisplayName] = useState('')
44 const [editRole, setEditRole] = useState('operator')
45 const [editActive, setEditActive] = useState(true)
46 const [passwordTarget, setPasswordTarget] = useState<AdminDto | null>(null)
47 const [newPassword, setNewPassword] = useState('')
48 const [deleteTarget, setDeleteTarget] = useState<AdminDto | null>(null)
49
50 const load = useCallback(async () => {
51 if (me?.role !== 'super_admin') {
52 return
53 }
54 setError(null)
55 try {
56 setData(await adminAccounts.list({ limit: PAGE_SIZE, offset }))
57 } catch (err) {
58 setError(err instanceof ApiError ? err.message : '管理员列表加载失败')
59 }
60 }, [me, offset])
61
62 useEffect(() => {
63 void load()
64 }, [load])
65
66 if (me?.role !== 'super_admin') {
67 return (
68 <Card title="管理员">
69 <p className="text-sm text-slate-500">仅超级管理员可以管理管理员账号。</p>
70 </Card>
71 )
72 }
73
74 const run = async (action: () => Promise<unknown>, message: string) => {
75 try {
76 await action()
77 toast(message)
78 setCreateOpen(false)
79 setEditTarget(null)
80 setPasswordTarget(null)
81 setDeleteTarget(null)
82 await load()
83 } catch (err) {
84 toast(err instanceof ApiError ? err.message : '操作失败', 'danger')
85 }
86 }
87
88 return (
89 <div className="space-y-4">
90 <div className="flex items-center justify-between">
91 <div>
92 <h1 className="text-lg font-semibold text-slate-900 dark:text-slate-100">管理员</h1>
93 <p className="text-xs text-slate-500">管理账号、角色与口令重置</p>
94 </div>
95 <Button
96 size="sm"
97 onClick={() => {
98 setUsername('')
99 setDisplayName('')
100 setPassword('')
101 setRole('operator')
102 setCreateOpen(true)
103 }}
104 >
105 新建管理员
106 </Button>
107 </div>
108
109 <ErrorBanner message={error} />
110
111 <Card title="管理员账号">
112 {data === null ? (
113 <Loading />
114 ) : (
115 <>
116 <DataTable
117 columns={[
118 { key: 'username', title: '账号', render: (row) => row.username },
119 { key: 'display_name', title: '显示名', render: (row) => row.display_name },
120 {
121 key: 'role',
122 title: '角色',
123 render: (row) => (
124 <Badge tone={row.role === 'super_admin' ? 'danger' : row.role === 'operator' ? 'info' : 'neutral'}>
125 {ROLE_OPTIONS.find((option) => option.value === row.role)?.label ?? row.role}
126 </Badge>
127 ),
128 },
129 {
130 key: 'active',
131 title: '状态',
132 render: (row) => (
133 <Badge tone={row.is_active ? 'success' : 'danger'}>{row.is_active ? '启用' : '停用'}</Badge>
134 ),
135 },
136 {
137 key: 'last_login',
138 title: '最后登录',
139 render: (row) => (
140 <span className="text-xs text-slate-500">{formatDateTime(row.last_login_at)}</span>
141 ),
142 },
143 {
144 key: 'actions',
145 title: '操作',
146 render: (row) => (
147 <div className="flex flex-wrap gap-1">
148 <Button
149 size="sm"
150 variant="secondary"
151 onClick={() => {
152 setEditTarget(row)
153 setEditDisplayName(row.display_name)
154 setEditRole(row.role)
155 setEditActive(row.is_active)
156 }}
157 >
158 编辑
159 </Button>
160 <Button
161 size="sm"
162 variant="secondary"
163 onClick={() => {
164 setPasswordTarget(row)
165 setNewPassword('')
166 }}
167 >
168 重置口令
169 </Button>
170 <Button
171 size="sm"
172 variant="secondary"
173 onClick={() =>
174 void run(() => adminAccounts.revokeSessions(row.id), '该管理员的会话已失效')
175 }
176 >
177 踢出会话
178 </Button>
179 <Button size="sm" variant="danger" onClick={() => setDeleteTarget(row)}>
180 删除
181 </Button>
182 </div>
183 ),
184 },
185 ]}
186 rows={data.items}
187 rowKey={(row) => row.id}
188 />
189 <Pagination total={data.total} limit={data.limit} offset={data.offset} onChange={setOffset} />
190 </>
191 )}
192 </Card>
193
194 <Modal
195 open={createOpen}
196 title="新建管理员"
197 onClose={() => setCreateOpen(false)}
198 footer={
199 <>
200 <Button variant="secondary" onClick={() => setCreateOpen(false)}>
201 取消
202 </Button>
203 <Button
204 onClick={() =>
205 void run(
206 () =>
207 adminAccounts.create({
208 username,
209 display_name: displayName || undefined,
210 password,
211 role,
212 }),
213 '管理员已创建',
214 )
215 }
216 >
217 创建
218 </Button>
219 </>
220 }
221 >
222 <Field label="账号" hint="3-32 位,字母/数字/_/-/.">
223 <TextInput value={username} onChange={setUsername} />
224 </Field>
225 <Field label="显示名">
226 <TextInput value={displayName} onChange={setDisplayName} />
227 </Field>
228 <Field label="初始口令">
229 <TextInput value={password} onChange={setPassword} type="password" />
230 </Field>
231 <Field label="角色">
232 <Select value={role} onChange={setRole} options={ROLE_OPTIONS} />
233 </Field>
234 </Modal>
235
236 <Modal
237 open={editTarget !== null}
238 title={`编辑管理员 ${editTarget?.username ?? ''}`}
239 onClose={() => setEditTarget(null)}
240 footer={
241 <>
242 <Button variant="secondary" onClick={() => setEditTarget(null)}>
243 取消
244 </Button>
245 <Button
246 onClick={() => {
247 if (!editTarget) {
248 return
249 }
250 void run(
251 () =>
252 adminAccounts.update(editTarget.id, {
253 display_name: editDisplayName,
254 role: editRole,
255 is_active: editActive,
256 }),
257 '管理员已更新',
258 )
259 }}
260 >
261 保存
262 </Button>
263 </>
264 }
265 >
266 <Field label="显示名">
267 <TextInput value={editDisplayName} onChange={setEditDisplayName} />
268 </Field>
269 <Field label="角色">
270 <Select value={editRole} onChange={setEditRole} options={ROLE_OPTIONS} />
271 </Field>
272 <Field label="状态">
273 <Select
274 value={editActive ? 'active' : 'inactive'}
275 onChange={(value) => setEditActive(value === 'active')}
276 options={[
277 { value: 'active', label: '启用' },
278 { value: 'inactive', label: '停用' },
279 ]}
280 />
281 </Field>
282 </Modal>
283
284 <Modal
285 open={passwordTarget !== null}
286 title={`重置口令 ${passwordTarget?.username ?? ''}`}
287 onClose={() => setPasswordTarget(null)}
288 footer={
289 <>
290 <Button variant="secondary" onClick={() => setPasswordTarget(null)}>
291 取消
292 </Button>
293 <Button
294 onClick={() => {
295 if (!passwordTarget || !newPassword) {
296 return
297 }
298 void run(
299 () => adminAccounts.resetPassword(passwordTarget.id, newPassword),
300 '口令已重置,下次登录需改密',
301 )
302 }}
303 >
304 确认重置
305 </Button>
306 </>
307 }
308 >
309 <Field label="新口令">
310 <TextInput value={newPassword} onChange={setNewPassword} type="password" />
311 </Field>
312 </Modal>
313
314 <ConfirmDialog
315 open={deleteTarget !== null}
316 title="删除管理员"
317 description="删除后该账号立即失效;系统会阻止删除自己与最后一个超级管理员。"
318 confirmLabel="删除管理员"
319 requireText={deleteTarget?.username}
320 onClose={() => setDeleteTarget(null)}
321 onConfirm={() => {
322 if (!deleteTarget) {
323 return
324 }
325 void run(() => adminAccounts.remove(deleteTarget.id), '管理员已删除')
326 }}
327 />
328 </div>
329 )
330}
No newline at end of file
Deleted admin/src/pages/chat-messages.tsx
@@ -1,615 +0,0 @@
1import { useCallback, useEffect, useMemo, useState } from 'react'
2import { Link, useSearchParams } from 'react-router-dom'
3import { adminChat } from '@/shared/api'
4import type {
5 AdminChatAttachment,
6 AdminChatMessage,
7 AdminChatMessageDetail,
8} from '@/shared/api'
9import { ApiError } from '@/shared/api'
10import { formatBytes, formatDateTime, prettyJson } from '@/shared/format'
11import {
12 Badge,
13 Button,
14 Card,
15 ConfirmDialog,
16 DataTable,
17 ErrorBanner,
18 Field,
19 Loading,
20 Modal,
21 Pagination,
22 Select,
23 TextInput,
24 statusTone,
25 useToast,
26} from '@/shared/ui'
27
28const PAGE_SIZE = 20
29
30const STATUS_OPTIONS = [
31 { value: '', label: '全部消息' },
32 { value: 'active', label: '未删除' },
33 { value: 'deleted', label: '已删除' },
34 { value: 'pinned', label: '已置顶' },
35]
36
37const ATTACHMENT_STATUS_OPTIONS = [
38 { value: '', label: '全部状态' },
39 { value: 'uploaded', label: '已上传' },
40 { value: 'pending', label: '待上传' },
41]
42
43function toIso(value: string): string | undefined {
44 if (!value) {
45 return undefined
46 }
47 const date = new Date(value)
48 return Number.isNaN(date.getTime()) ? undefined : date.toISOString()
49}
50
51export function ChatMessagesPage() {
52 const toast = useToast()
53 const [params, setParams] = useSearchParams()
54 const roomId = params.get('room_id') ?? ''
55 const [authorId, setAuthorId] = useState('')
56 const [status, setStatus] = useState('')
57 const [query, setQuery] = useState('')
58 const [from, setFrom] = useState('')
59 const [to, setTo] = useState('')
60 const [applied, setApplied] = useState({
61 authorId: '',
62 status: '',
63 query: '',
64 from: '',
65 to: '',
66 })
67 const [offset, setOffset] = useState(0)
68 const [data, setData] = useState<{
69 items: AdminChatMessage[]
70 total: number
71 limit: number
72 offset: number
73 } | null>(null)
74 const [detail, setDetail] = useState<AdminChatMessageDetail | null>(null)
75 const [error, setError] = useState<string | null>(null)
76 const [busy, setBusy] = useState(false)
77 const [selected, setSelected] = useState<string[]>([])
78 const [bulkOpen, setBulkOpen] = useState(false)
79 const [bulkReason, setBulkReason] = useState('')
80 const [tab, setTab] = useState<'messages' | 'attachments'>('messages')
81 const [attachmentStatus, setAttachmentStatus] = useState('')
82 const [attachmentQuery, setAttachmentQuery] = useState('')
83 const [attachmentApplied, setAttachmentApplied] = useState({ status: '', query: '' })
84 const [attachments, setAttachments] = useState<{
85 items: AdminChatAttachment[]
86 total: number
87 limit: number
88 offset: number
89 } | null>(null)
90 const [attachmentOffset, setAttachmentOffset] = useState(0)
91 const [attachmentDelete, setAttachmentDelete] = useState<AdminChatAttachment | null>(null)
92
93 const messageParams = useMemo(
94 () => ({
95 room_id: roomId || undefined,
96 author_id: applied.authorId || undefined,
97 status: applied.status || undefined,
98 q: applied.query || undefined,
99 from: toIso(applied.from),
100 to: toIso(applied.to),
101 limit: PAGE_SIZE,
102 offset,
103 }),
104 [roomId, applied, offset],
105 )
106
107 const load = useCallback(async () => {
108 setBusy(true)
109 setError(null)
110 try {
111 const [page, attachmentPage] = await Promise.all([
112 adminChat.messages(messageParams),
113 adminChat.attachments({
114 room_id: roomId || undefined,
115 status: attachmentApplied.status || undefined,
116 q: attachmentApplied.query || undefined,
117 limit: PAGE_SIZE,
118 offset: attachmentOffset,
119 }),
120 ])
121 setData(page)
122 setAttachments(attachmentPage)
123 setSelected((current) =>
124 current.filter((id) => page.items.some((item) => item.id === id)),
125 )
126 } catch (err) {
127 setError(err instanceof ApiError ? err.message : '消息列表加载失败')
128 } finally {
129 setBusy(false)
130 }
131 }, [messageParams, roomId, attachmentApplied, attachmentOffset])
132
133 useEffect(() => {
134 void load()
135 }, [load])
136
137 const run = async (action: () => Promise<unknown>, message: string) => {
138 try {
139 await action()
140 toast(message)
141 setBulkOpen(false)
142 setAttachmentDelete(null)
143 await load()
144 } catch (err) {
145 toast(err instanceof ApiError ? err.message : '操作失败', 'danger')
146 }
147 }
148
149 const allSelected = data !== null && data.items.length > 0 && selected.length === data.items.length
150
151 const toggleRoomFilter = (value: string) => {
152 const next = new URLSearchParams(params)
153 if (value) {
154 next.set('room_id', value)
155 } else {
156 next.delete('room_id')
157 }
158 setParams(next)
159 setOffset(0)
160 }
161
162 return (
163 <div className="space-y-4">
164 <div className="flex flex-wrap items-end justify-between gap-3">
165 <div>
166 <h1 className="text-lg font-semibold text-slate-900 dark:text-slate-100">消息管理</h1>
167 <p className="text-xs text-slate-500">
168 全站消息检索、内容审核、批量删除与聊天附件清理
169 </p>
170 </div>
171 <div className="flex items-end gap-2">
172 <Button
173 variant="secondary"
174 onClick={() => adminChat.exportMessagesCsv(messageParams)}
175 disabled={!data || data.total === 0}
176 >
177 导出 CSV
178 </Button>
179 <Link
180 to="/chat"
181 className="inline-flex items-center rounded-md border border-slate-300 bg-white px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-100 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-200"
182 >
183 返回房间
184 </Link>
185 </div>
186 </div>
187
188 <div className="flex gap-2">
189 <Button
190 variant={tab === 'messages' ? 'primary' : 'secondary'}
191 size="sm"
192 onClick={() => setTab('messages')}
193 >
194 消息
195 </Button>
196 <Button
197 variant={tab === 'attachments' ? 'primary' : 'secondary'}
198 size="sm"
199 onClick={() => setTab('attachments')}
200 >
201 附件
202 </Button>
203 </div>
204
205 <ErrorBanner message={error} />
206
207 {tab === 'messages' ? (
208 <Card
209 title={`消息列表${roomId ? `(房间 ${roomId})` : ''}`}
210 action={
211 selected.length > 0 ? (
212 <div className="flex items-center gap-2">
213 <span className="text-xs text-slate-500">已选 {selected.length} 条</span>
214 <Button size="sm" variant="danger" onClick={() => setBulkOpen(true)}>
215 批量删除
216 </Button>
217 </div>
218 ) : (
219 <span className="text-xs text-slate-400">勾选消息后可批量删除</span>
220 )
221 }
222 >
223 <form
224 className="mb-3 flex flex-wrap items-end gap-2"
225 onSubmit={(event) => {
226 event.preventDefault()
227 setOffset(0)
228 setApplied({ authorId, status, query, from, to })
229 }}
230 >
231 <Field label="房间 ID">
232 <TextInput
233 value={roomId}
234 onChange={toggleRoomFilter}
235 placeholder="room uuid"
236 />
237 </Field>
238 <Field label="作者 ID">
239 <TextInput value={authorId} onChange={setAuthorId} placeholder="user uuid" />
240 </Field>
241 <Field label="状态">
242 <Select value={status} onChange={setStatus} options={STATUS_OPTIONS} />
243 </Field>
244 <Field label="关键字">
245 <TextInput value={query} onChange={setQuery} placeholder="消息内容" />
246 </Field>
247 <Field label="起始时间">
248 <TextInput value={from} onChange={setFrom} type="datetime-local" />
249 </Field>
250 <Field label="结束时间">
251 <TextInput value={to} onChange={setTo} type="datetime-local" />
252 </Field>
253 <Button type="submit" disabled={busy}>
254 查询
255 </Button>
256 </form>
257
258 {data ? (
259 <>
260 <DataTable
261 columns={[
262 {
263 key: 'select',
264 title: (
265 <input
266 type="checkbox"
267 checked={allSelected}
268 onChange={(event) =>
269 setSelected(event.target.checked ? data.items.map((row) => row.id) : [])
270 }
271 />
272 ),
273 render: (row) => (
274 <input
275 type="checkbox"
276 checked={selected.includes(row.id)}
277 onChange={(event) =>
278 setSelected((current) =>
279 event.target.checked
280 ? [...current, row.id]
281 : current.filter((id) => id !== row.id),
282 )
283 }
284 />
285 ),
286 },
287 {
288 key: 'content',
289 title: '内容',
290 render: (row) => (
291 <div className="min-w-0 max-w-[320px]">
292 <div className="truncate text-sm">
293 {row.deleted_at ? (
294 <span className="text-slate-400">(已删除)</span>
295 ) : (
296 row.content || '-'
297 )}
298 </div>
299 <div className="text-xs text-slate-400">{row.id}</div>
300 </div>
301 ),
302 },
303 {
304 key: 'room',
305 title: '房间',
306 render: (row) => (
307 <Link
308 to={`/chat/rooms/${row.room_id}`}
309 className="text-xs text-sky-600 hover:underline dark:text-sky-400"
310 >
311 {row.room_name ?? row.room_id}
312 </Link>
313 ),
314 },
315 {
316 key: 'author',
317 title: '作者',
318 render: (row) => (
319 <div className="text-xs">
320 <div>{row.author_display_name ?? row.author_username ?? row.author_id}</div>
321 <div className="text-slate-400">{row.author_type}</div>
322 </div>
323 ),
324 },
325 {
326 key: 'stats',
327 title: '附件/表情',
328 render: (row) => (
329 <span className="text-xs text-slate-500">
330 {row.attachment_count} / {row.reaction_count}
331 </span>
332 ),
333 },
334 {
335 key: 'flags',
336 title: '状态',
337 render: (row) => (
338 <div className="flex flex-wrap gap-1">
339 <Badge tone={row.deleted_at ? 'danger' : 'success'}>
340 {row.deleted_at ? '已删除' : '正常'}
341 </Badge>
342 {row.pinned && <Badge tone="info">置顶</Badge>}
343 {row.edited_at && <Badge tone="warning">已编辑</Badge>}
344 </div>
345 ),
346 },
347 {
348 key: 'created',
349 title: '时间',
350 render: (row) => (
351 <span className="text-xs text-slate-500">{formatDateTime(row.created_at)}</span>
352 ),
353 },
354 {
355 key: 'actions',
356 title: '操作',
357 render: (row) => (
358 <Button
359 size="sm"
360 variant="secondary"
361 onClick={async () => {
362 try {
363 setDetail(await adminChat.message(row.id))
364 } catch (err) {
365 toast(err instanceof ApiError ? err.message : '详情加载失败', 'danger')
366 }
367 }}
368 >
369 详情
370 </Button>
371 ),
372 },
373 ]}
374 rows={data.items}
375 rowKey={(row) => row.id}
376 empty="没有消息"
377 />
378 <Pagination
379 total={data.total}
380 limit={data.limit}
381 offset={data.offset}
382 onChange={setOffset}
383 />
384 </>
385 ) : (
386 <Loading />
387 )}
388 </Card>
389 ) : (
390 <Card title={`聊天附件${roomId ? `(房间 ${roomId})` : ''}`}>
391 <form
392 className="mb-3 flex flex-wrap items-end gap-2"
393 onSubmit={(event) => {
394 event.preventDefault()
395 setAttachmentOffset(0)
396 setAttachmentApplied({ status: attachmentStatus, query: attachmentQuery })
397 }}
398 >
399 <Field label="状态">
400 <Select
401 value={attachmentStatus}
402 onChange={setAttachmentStatus}
403 options={ATTACHMENT_STATUS_OPTIONS}
404 />
405 </Field>
406 <Field label="关键字">
407 <TextInput
408 value={attachmentQuery}
409 onChange={setAttachmentQuery}
410 placeholder="文件名 / S3 key"
411 />
412 </Field>
413 <Button type="submit" disabled={busy}>
414 查询
415 </Button>
416 </form>
417
418 {attachments ? (
419 <>
420 <DataTable
421 columns={[
422 { key: 'filename', title: '文件', render: (row) => row.filename },
423 {
424 key: 'owner',
425 title: '上传者',
426 render: (row) => row.display_name ?? row.username ?? row.user_id,
427 },
428 {
429 key: 'type',
430 title: '类型',
431 render: (row) => (
432 <span className="text-xs text-slate-500">{row.content_type}</span>
433 ),
434 },
435 {
436 key: 'size',
437 title: '大小',
438 render: (row) => formatBytes(row.size),
439 },
440 {
441 key: 'status',
442 title: '状态',
443 render: (row) => (
444 <Badge tone={row.status === 'uploaded' ? 'success' : 'warning'}>
445 {row.status}
446 </Badge>
447 ),
448 },
449 {
450 key: 'created',
451 title: '时间',
452 render: (row) => (
453 <span className="text-xs text-slate-500">{formatDateTime(row.created_at)}</span>
454 ),
455 },
456 {
457 key: 'actions',
458 title: '操作',
459 render: (row) => (
460 <Button size="sm" variant="danger" onClick={() => setAttachmentDelete(row)}>
461 删除
462 </Button>
463 ),
464 },
465 ]}
466 rows={attachments.items}
467 rowKey={(row) => row.id}
468 empty="没有附件"
469 />
470 <Pagination
471 total={attachments.total}
472 limit={attachments.limit}
473 offset={attachments.offset}
474 onChange={setAttachmentOffset}
475 />
476 </>
477 ) : (
478 <Loading />
479 )}
480 </Card>
481 )}
482
483 <Modal open={detail !== null} title="消息详情" onClose={() => setDetail(null)}>
484 {detail && (
485 <div className="space-y-3">
486 <div className="flex flex-wrap items-center gap-2">
487 <Badge tone={statusTone(detail.message.deleted_at ? 'deleted' : 'active')}>
488 {detail.message.deleted_at ? '已删除' : '正常'}
489 </Badge>
490 <span className="text-xs text-slate-500">
491 {detail.message.message_type} · {detail.message.id}
492 </span>
493 </div>
494 <div className="rounded bg-slate-100 p-2 text-sm break-all whitespace-pre-wrap dark:bg-slate-800">
495 {detail.message.content || '(空内容)'}
496 </div>
497 <dl className="grid grid-cols-2 gap-2 text-xs text-slate-600 dark:text-slate-300">
498 <div>房间:{detail.message.room_name ?? detail.message.room_id}</div>
499 <div>作者:{detail.message.author_username ?? detail.message.author_id}</div>
500 <div>序号:{detail.message.seq}</div>
501 <div>创建:{formatDateTime(detail.message.created_at)}</div>
502 <div>编辑:{formatDateTime(detail.message.edited_at)}</div>
503 <div>删除:{formatDateTime(detail.message.deleted_at)}</div>
504 </dl>
505 {detail.reactions.length > 0 && (
506 <div>
507 <div className="mb-1 text-xs font-medium text-slate-500">表情反应</div>
508 <div className="flex flex-wrap gap-1">
509 {detail.reactions.map((reaction) => (
510 <Badge key={reaction.emoji} tone="info">
511 {reaction.emoji} {reaction.count}
512 </Badge>
513 ))}
514 </div>
515 </div>
516 )}
517 {detail.thread && (
518 <div className="text-xs text-slate-500">
519 线程:{detail.thread.name ?? detail.thread.id}({detail.thread.reply_count} 回复 /{' '}
520 {detail.thread.participant_count} 参与者)
521 </div>
522 )}
523 {detail.edits.length > 0 && (
524 <div>
525 <div className="mb-1 text-xs font-medium text-slate-500">编辑历史</div>
526 <div className="max-h-40 space-y-1 overflow-y-auto">
527 {detail.edits.map((edit) => (
528 <div key={edit.id} className="rounded bg-slate-100 p-2 text-xs dark:bg-slate-800">
529 <div className="text-slate-400">
530 {edit.editor_username ?? edit.editor_id} · {formatDateTime(edit.created_at)}
531 </div>
532 <div className="break-all whitespace-pre-wrap">{edit.content}</div>
533 </div>
534 ))}
535 </div>
536 </div>
537 )}
538 {detail.attachments.length > 0 && (
539 <div>
540 <div className="mb-1 text-xs font-medium text-slate-500">附件</div>
541 <div className="space-y-1">
542 {detail.attachments.map((attachment) => (
543 <div
544 key={attachment.id}
545 className="flex items-center justify-between rounded bg-slate-100 p-2 text-xs dark:bg-slate-800"
546 >
547 <span className="break-all">{attachment.filename}</span>
548 <span className="text-slate-500">{formatBytes(attachment.size)}</span>
549 </div>
550 ))}
551 </div>
552 </div>
553 )}
554 <details>
555 <summary className="cursor-pointer text-xs text-slate-500">原始记录</summary>
556 <pre className="mt-1 max-h-60 overflow-auto rounded bg-slate-100 p-2 text-xs dark:bg-slate-800">
557 {prettyJson(detail.message)}
558 </pre>
559 </details>
560 </div>
561 )}
562 </Modal>
563
564 <Modal
565 open={bulkOpen}
566 title="批量删除消息"
567 onClose={() => setBulkOpen(false)}
568 footer={
569 <>
570 <Button variant="secondary" onClick={() => setBulkOpen(false)}>
571 取消
572 </Button>
573 <Button
574 variant="danger"
575 onClick={() =>
576 void run(
577 () =>
578 adminChat.bulkDelete({
579 message_ids: selected,
580 reason: bulkReason || undefined,
581 }),
582 '批量删除已完成',
583 )
584 }
585 >
586 删除 {selected.length} 条
587 </Button>
588 </>
589 }
590 >
591 <p>将清空所选消息内容并移除其表情、编辑记录与附件引用,操作写入审计日志。</p>
592 <Field label="删除原因" hint="可选,会记录在审计日志中">
593 <TextInput value={bulkReason} onChange={setBulkReason} />
594 </Field>
595 </Modal>
596
597 <ConfirmDialog
598 open={attachmentDelete !== null}
599 title="删除聊天附件"
600 description={`将删除附件 ${attachmentDelete?.filename ?? ''} 的记录,并在没有其它引用时清理对象存储文件。`}
601 confirmLabel="删除附件"
602 requireText={attachmentDelete?.filename}
603 onClose={() => setAttachmentDelete(null)}
604 onConfirm={() => {
605 if (!attachmentDelete) {
606 return
607 }
608 void run(() => adminChat.removeAttachment(attachmentDelete.id), '附件已删除')
609 }}
610 />
611
612 {busy && <Loading />}
613 </div>
614 )
615}
Deleted admin/src/pages/chat-room-detail.tsx
@@ -1,316 +0,0 @@
1import { useCallback, useEffect, useState } from 'react'
2import { Link, useParams } from 'react-router-dom'
3import { adminChat } from '@/shared/api'
4import type { AdminChatRoomDetail, AdminChatRoomMember } from '@/shared/api'
5import { ApiError } from '@/shared/api'
6import { formatBytes, formatDateTime } from '@/shared/format'
7import {
8 Badge,
9 Button,
10 Card,
11 Checkbox,
12 ConfirmDialog,
13 DataTable,
14 ErrorBanner,
15 Field,
16 Loading,
17 Modal,
18 Pagination,
19 TextInput,
20 useToast,
21} from '@/shared/ui'
22
23const PAGE_SIZE = 20
24
25export function ChatRoomDetailPage() {
26 const { id = '' } = useParams()
27 const toast = useToast()
28 const [detail, setDetail] = useState<AdminChatRoomDetail | null>(null)
29 const [error, setError] = useState<string | null>(null)
30 const [busy, setBusy] = useState(false)
31 const [name, setName] = useState('')
32 const [topic, setTopic] = useState('')
33 const [editOpen, setEditOpen] = useState(false)
34 const [purgeOpen, setPurgeOpen] = useState(false)
35 const [deleteOpen, setDeleteOpen] = useState(false)
36 const [members, setMembers] = useState<{
37 items: AdminChatRoomMember[]
38 total: number
39 limit: number
40 offset: number
41 } | null>(null)
42 const [memberOffset, setMemberOffset] = useState(0)
43 const [memberStatus, setMemberStatus] = useState('active')
44
45 const load = useCallback(async () => {
46 setBusy(true)
47 setError(null)
48 try {
49 const [value, memberPage] = await Promise.all([
50 adminChat.room(id),
51 adminChat.members(id, { status: memberStatus, limit: PAGE_SIZE, offset: memberOffset }),
52 ])
53 setDetail(value)
54 setName(value.room.name)
55 setTopic(value.room.topic ?? '')
56 setMembers(memberPage)
57 } catch (err) {
58 setError(err instanceof ApiError ? err.message : '房间详情加载失败')
59 } finally {
60 setBusy(false)
61 }
62 }, [id, memberStatus, memberOffset])
63
64 useEffect(() => {
65 void load()
66 }, [load])
67
68 const run = async (action: () => Promise<unknown>, message: string) => {
69 try {
70 await action()
71 toast(message)
72 setEditOpen(false)
73 setPurgeOpen(false)
74 setDeleteOpen(false)
75 await load()
76 } catch (err) {
77 toast(err instanceof ApiError ? err.message : '操作失败', 'danger')
78 }
79 }
80
81 if (!detail) {
82 return (
83 <div className="space-y-3">
84 <ErrorBanner message={error} />
85 <Loading />
86 </div>
87 )
88 }
89
90 const room = detail.room
91
92 return (
93 <div className="space-y-4">
94 <div className="flex flex-wrap items-center justify-between gap-3">
95 <div>
96 <h1 className="text-lg font-semibold text-slate-900 dark:text-slate-100">{room.name}</h1>
97 <p className="text-xs text-slate-500">
98 {room.kind} · {room.id}
99 </p>
100 </div>
101 <div className="flex flex-wrap gap-2">
102 <Link
103 to={`/chat/messages?room_id=${room.id}`}
104 className="inline-flex items-center rounded-md border border-slate-300 bg-white px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-100 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-200"
105 >
106 查看消息
107 </Link>
108 <Button variant="secondary" size="md" onClick={() => setEditOpen(true)}>
109 编辑属性
110 </Button>
111 <Button variant="secondary" size="md" onClick={() => setPurgeOpen(true)}>
112 清空消息
113 </Button>
114 <Button variant="danger" size="md" onClick={() => setDeleteOpen(true)}>
115 删除房间
116 </Button>
117 </div>
118 </div>
119
120 <ErrorBanner message={error} />
121
122 <div className="grid gap-3 lg:grid-cols-3">
123 <Card title="房间状态">
124 <div className="flex flex-wrap gap-1">
125 <Badge tone={room.is_active ? 'success' : 'danger'}>{room.is_active ? '启用' : '停用'}</Badge>
126 {room.is_private && <Badge tone="warning">私有</Badge>}
127 {room.nsfw && <Badge tone="danger">NSFW</Badge>}
128 </div>
129 <dl className="mt-3 space-y-1 text-xs text-slate-600 dark:text-slate-300">
130 <div>所有者:{room.owner_username ?? room.owner_id}</div>
131 <div>Space:{room.space_name ?? '-'}</div>
132 <div>主题:{room.topic ?? '-'}</div>
133 <div>创建:{formatDateTime(room.created_at)}</div>
134 <div>更新:{formatDateTime(room.updated_at)}</div>
135 <div>最近消息:{formatDateTime(room.last_message_at)}</div>
136 </dl>
137 </Card>
138
139 <Card title="内容统计">
140 <dl className="divide-y divide-slate-100 text-sm dark:divide-slate-800">
141 {(
142 [
143 ['消息', room.messages],
144 ['已删除消息', detail.deleted_messages],
145 ['线程', detail.threads],
146 ['成员', room.members],
147 ['封禁', detail.bans],
148 ['邀请', detail.invites],
149 ['附件', detail.attachments],
150 ['附件用量', formatBytes(detail.attachment_bytes)],
151 ] as [string, string | number][]
152 ).map(([label, value]) => (
153 <div key={label} className="flex items-center justify-between py-1.5">
154 <dt className="text-slate-500 dark:text-slate-400">{label}</dt>
155 <dd className="text-slate-800 dark:text-slate-100">{value}</dd>
156 </div>
157 ))}
158 </dl>
159 </Card>
160
161 <Card title="成员管理">
162 <div className="flex items-center gap-2">
163 <Checkbox
164 checked={memberStatus === 'active'}
165 onChange={(value) => {
166 setMemberOffset(0)
167 setMemberStatus(value ? 'active' : '')
168 }}
169 label="仅在席成员"
170 />
171 </div>
172 {members ? (
173 <>
174 <DataTable
175 columns={[
176 {
177 key: 'user',
178 title: '成员',
179 render: (row) => (
180 <div>
181 <div className="text-sm">{row.display_name ?? row.username ?? row.user_id}</div>
182 <div className="text-xs text-slate-400">{row.username ?? row.user_id}</div>
183 </div>
184 ),
185 },
186 { key: 'role', title: '角色', render: (row) => row.role },
187 {
188 key: 'state',
189 title: '状态',
190 render: (row) =>
191 row.left_at ? (
192 <Badge tone="neutral">已离开</Badge>
193 ) : (
194 <Badge tone="success">在席</Badge>
195 ),
196 },
197 {
198 key: 'actions',
199 title: '操作',
200 render: (row) =>
201 row.left_at ? (
202 <span className="text-xs text-slate-400">-</span>
203 ) : (
204 <Button
205 size="sm"
206 variant="danger"
207 onClick={() =>
208 void run(
209 () => adminChat.removeMember(room.id, row.user_id),
210 '成员已移出房间',
211 )
212 }
213 >
214 移出
215 </Button>
216 ),
217 },
218 ]}
219 rows={members.items}
220 rowKey={(row) => row.user_id}
221 empty="没有成员"
222 />
223 <Pagination
224 total={members.total}
225 limit={members.limit}
226 offset={members.offset}
227 onChange={setMemberOffset}
228 />
229 </>
230 ) : (
231 <Loading />
232 )}
233 </Card>
234 </div>
235
236 <Modal
237 open={editOpen}
238 title="编辑房间属性"
239 onClose={() => setEditOpen(false)}
240 footer={
241 <>
242 <Button variant="secondary" onClick={() => setEditOpen(false)}>
243 取消
244 </Button>
245 <Button
246 onClick={() =>
247 void run(
248 () =>
249 adminChat.updateRoom(room.id, {
250 name,
251 topic,
252 }),
253 '房间属性已更新',
254 )
255 }
256 >
257 保存
258 </Button>
259 </>
260 }
261 >
262 <Field label="名称">
263 <TextInput value={name} onChange={setName} />
264 </Field>
265 <Field label="主题">
266 <TextInput value={topic} onChange={setTopic} />
267 </Field>
268 <div className="flex flex-wrap gap-4">
269 <Checkbox
270 checked={room.is_private}
271 onChange={(value) =>
272 void run(() => adminChat.updateRoom(room.id, { is_private: value }), '可见性已更新')
273 }
274 label="私有房间"
275 />
276 <Checkbox
277 checked={room.nsfw}
278 onChange={(value) =>
279 void run(() => adminChat.updateRoom(room.id, { nsfw: value }), 'NSFW 标记已更新')
280 }
281 label="NSFW"
282 />
283 <Checkbox
284 checked={room.is_active}
285 onChange={(value) =>
286 void run(() => adminChat.updateRoom(room.id, { is_active: value }), '启停已更新')
287 }
288 label="启用房间"
289 />
290 </div>
291 </Modal>
292
293 <ConfirmDialog
294 open={purgeOpen}
295 title="清空房间消息"
296 description="将永久删除该房间全部消息、线程与附件(保留房间本身),操作不可恢复,并写入审计日志。"
297 confirmLabel="清空消息"
298 requireText={room.name}
299 onClose={() => setPurgeOpen(false)}
300 onConfirm={() => void run(() => adminChat.purgeRoom(room.id), '房间消息已清空')}
301 />
302
303 <ConfirmDialog
304 open={deleteOpen}
305 title="删除房间"
306 description="将永久删除该房间及其全部消息、成员、线程与附件,操作不可恢复。"
307 confirmLabel="删除房间"
308 requireText={room.name}
309 onClose={() => setDeleteOpen(false)}
310 onConfirm={() => void run(() => adminChat.removeRoom(room.id), '房间已删除')}
311 />
312
313 {busy && <Loading />}
314 </div>
315 )
316}
Deleted admin/src/pages/chat-rooms.tsx
@@ -1,234 +0,0 @@
1import { useCallback, useEffect, useState } from 'react'
2import { Link } from 'react-router-dom'
3import { adminChat } from '@/shared/api'
4import type { AdminChatRoomSummary, AdminChatStats } from '@/shared/api'
5import { ApiError } from '@/shared/api'
6import { formatDateTime } from '@/shared/format'
7import {
8 Badge,
9 Button,
10 Card,
11 DataTable,
12 ErrorBanner,
13 Field,
14 Loading,
15 Pagination,
16 Select,
17 TextInput,
18} from '@/shared/ui'
19
20const PAGE_SIZE = 20
21
22const KIND_OPTIONS = [
23 { value: '', label: '全部类型' },
24 { value: 'text', label: 'text' },
25 { value: 'voice', label: 'voice' },
26 { value: 'category', label: 'category' },
27 { value: 'news', label: 'news' },
28 { value: 'stage', label: 'stage' },
29 { value: 'thread', label: 'thread' },
30 { value: 'guild_room', label: 'guild_room' },
31 { value: 'group_dm', label: 'group_dm' },
32 { value: 'dm', label: 'dm' },
33 { value: 'direct_message', label: 'direct_message' },
34]
35
36const STATUS_OPTIONS = [
37 { value: '', label: '全部状态' },
38 { value: 'active', label: '启用' },
39 { value: 'disabled', label: '停用' },
40]
41
42const VISIBILITY_OPTIONS = [
43 { value: '', label: '全部可见性' },
44 { value: 'public', label: '公开' },
45 { value: 'private', label: '私有' },
46]
47
48export function ChatRoomsPage() {
49 const [query, setQuery] = useState('')
50 const [kind, setKind] = useState('')
51 const [status, setStatus] = useState('')
52 const [visibility, setVisibility] = useState('')
53 const [applied, setApplied] = useState({ query: '', kind: '', status: '', visibility: '' })
54 const [offset, setOffset] = useState(0)
55 const [data, setData] = useState<{ items: AdminChatRoomSummary[]; total: number; limit: number; offset: number } | null>(null)
56 const [stats, setStats] = useState<AdminChatStats | null>(null)
57 const [error, setError] = useState<string | null>(null)
58 const [busy, setBusy] = useState(false)
59
60 const load = useCallback(async () => {
61 setBusy(true)
62 setError(null)
63 try {
64 const [page, statsValue] = await Promise.all([
65 adminChat.rooms({
66 q: applied.query || undefined,
67 kind: applied.kind || undefined,
68 status: applied.status || undefined,
69 visibility: applied.visibility || undefined,
70 limit: PAGE_SIZE,
71 offset,
72 }),
73 adminChat.stats(),
74 ])
75 setData(page)
76 setStats(statsValue)
77 } catch (err) {
78 setError(err instanceof ApiError ? err.message : '房间列表加载失败')
79 } finally {
80 setBusy(false)
81 }
82 }, [applied, offset])
83
84 useEffect(() => {
85 void load()
86 }, [load])
87
88 return (
89 <div className="space-y-4">
90 <div className="flex flex-wrap items-end justify-between gap-3">
91 <div>
92 <h1 className="text-lg font-semibold text-slate-900 dark:text-slate-100">聊天记录</h1>
93 <p className="text-xs text-slate-500">房间、消息、线程与聊天附件的统一管理视图</p>
94 </div>
95 <form
96 className="flex flex-wrap items-end gap-2"
97 onSubmit={(event) => {
98 event.preventDefault()
99 setOffset(0)
100 setApplied({ query, kind, status, visibility })
101 }}
102 >
103 <Field label="关键字">
104 <TextInput value={query} onChange={setQuery} placeholder="房间名" />
105 </Field>
106 <Field label="类型">
107 <Select value={kind} onChange={setKind} options={KIND_OPTIONS} />
108 </Field>
109 <Field label="状态">
110 <Select value={status} onChange={setStatus} options={STATUS_OPTIONS} />
111 </Field>
112 <Field label="可见性">
113 <Select value={visibility} onChange={setVisibility} options={VISIBILITY_OPTIONS} />
114 </Field>
115 <Button type="submit" disabled={busy}>
116 查询
117 </Button>
118 </form>
119 </div>
120
121 <ErrorBanner message={error} />
122
123 {stats && (
124 <div className="grid gap-3 md:grid-cols-3 xl:grid-cols-6">
125 {(
126 [
127 ['房间', stats.rooms],
128 ['启用房间', stats.active_rooms],
129 ['私有房间', stats.private_rooms],
130 ['消息', stats.messages],
131 ['24h 消息', stats.messages_24h],
132 ['已删除消息', stats.deleted_messages],
133 ['线程', stats.threads],
134 ['附件', stats.attachments],
135 ['待上传附件', stats.pending_attachments],
136 ['成员', stats.members],
137 ['封禁', stats.bans],
138 ] as [string, number][]
139 ).map(([label, value]) => (
140 <Card key={label} title={label}>
141 <div className="text-xl font-semibold text-slate-900 dark:text-slate-100">{value}</div>
142 </Card>
143 ))}
144 </div>
145 )}
146
147 <Card
148 title="房间列表"
149 action={
150 <Link
151 to="/chat/messages"
152 className="text-xs text-sky-600 hover:underline dark:text-sky-400"
153 >
154 消息管理 →
155 </Link>
156 }
157 >
158 {data ? (
159 <>
160 <DataTable
161 columns={[
162 {
163 key: 'name',
164 title: '房间',
165 render: (row) => (
166 <div className="min-w-0">
167 <Link
168 to={`/chat/rooms/${row.id}`}
169 className="font-medium text-sky-600 hover:underline dark:text-sky-400"
170 >
171 {row.name}
172 </Link>
173 <div className="text-xs text-slate-400">{row.id}</div>
174 </div>
175 ),
176 },
177 {
178 key: 'kind',
179 title: '类型',
180 render: (row) => <Badge tone="info">{row.kind}</Badge>,
181 },
182 {
183 key: 'space',
184 title: 'Space',
185 render: (row) => row.space_name ?? <span className="text-slate-400">-</span>,
186 },
187 {
188 key: 'owner',
189 title: '所有者',
190 render: (row) => row.owner_username ?? row.owner_id,
191 },
192 {
193 key: 'counts',
194 title: '成员/消息',
195 render: (row) => (
196 <span className="text-xs text-slate-500">
197 {row.members} / {row.messages}
198 </span>
199 ),
200 },
201 {
202 key: 'flags',
203 title: '可见性',
204 render: (row) => (
205 <div className="flex flex-wrap gap-1">
206 <Badge tone={row.is_active ? 'success' : 'danger'}>
207 {row.is_active ? '启用' : '停用'}
208 </Badge>
209 {row.is_private && <Badge tone="warning">私有</Badge>}
210 {row.nsfw && <Badge tone="danger">NSFW</Badge>}
211 </div>
212 ),
213 },
214 {
215 key: 'last',
216 title: '最近消息',
217 render: (row) => (
218 <span className="text-xs text-slate-500">{formatDateTime(row.last_message_at)}</span>
219 ),
220 },
221 ]}
222 rows={data.items}
223 rowKey={(row) => row.id}
224 empty="没有房间"
225 />
226 <Pagination total={data.total} limit={data.limit} offset={data.offset} onChange={setOffset} />
227 </>
228 ) : (
229 <Loading />
230 )}
231 </Card>
232 </div>
233 )
234}
Deleted admin/src/pages/dashboard.tsx
@@ -1,287 +0,0 @@
1import { useCallback, useEffect, useState } from 'react'
2import { Link } from 'react-router-dom'
3import { adminSystem } from '@/shared/api'
4import type { Dashboard, SystemStatus, TrendPoint } from '@/shared/api'
5import { formatDateTime, formatUptime, shortId } from '@/shared/format'
6import { Badge, Button, Card, DataTable, ErrorBanner, Loading, statusTone } from '@/shared/ui'
7
8function DependencyCard({
9 name,
10 dependency,
11}: {
12 name: string
13 dependency: { ok: boolean; latency_ms: number | null; detail: string | null }
14}) {
15 return (
16 <Card title={name}>
17 <div className="flex flex-wrap items-center justify-between gap-2">
18 <Badge tone={dependency.ok ? 'success' : 'danger'}>{dependency.ok ? '正常' : '异常'}</Badge>
19 <span className="text-xs text-slate-500">
20 {dependency.latency_ms === null ? '-' : `${dependency.latency_ms} ms`}
21 </span>
22 </div>
23 {dependency.detail && (
24 <p className="mt-2 break-all text-xs text-slate-500">{dependency.detail}</p>
25 )}
26 </Card>
27 )
28}
29
30function TrendChart({ trend }: { trend: TrendPoint[] }) {
31 const max = Math.max(1, ...trend.map((point) => Math.max(point.users, point.spaces, point.repos)))
32 return (
33 <div>
34 <div className="flex h-36 items-end gap-1">
35 {trend.map((point) => (
36 <div
37 key={point.date}
38 className="flex h-full flex-1 flex-col items-center justify-end gap-1"
39 title={`${point.date} 用户 ${point.users} · Space ${point.spaces} · 仓库 ${point.repos}`}
40 >
41 <div className="flex h-full w-full items-end justify-center gap-[2px]">
42 <div
43 className="w-1/4 rounded-t bg-sky-500"
44 style={{ height: `${Math.round((point.users / max) * 100)}%` }}
45 />
46 <div
47 className="w-1/4 rounded-t bg-emerald-500"
48 style={{ height: `${Math.round((point.spaces / max) * 100)}%` }}
49 />
50 <div
51 className="w-1/4 rounded-t bg-violet-500"
52 style={{ height: `${Math.round((point.repos / max) * 100)}%` }}
53 />
54 </div>
55 <span className="text-[10px] text-slate-400">{point.date.slice(5)}</span>
56 </div>
57 ))}
58 </div>
59 <div className="mt-2 flex gap-4 text-xs text-slate-500">
60 <span className="flex items-center gap-1">
61 <span className="inline-block h-2 w-2 rounded bg-sky-500" /> 新用户
62 </span>
63 <span className="flex items-center gap-1">
64 <span className="inline-block h-2 w-2 rounded bg-emerald-500" /> 新 Space
65 </span>
66 <span className="flex items-center gap-1">
67 <span className="inline-block h-2 w-2 rounded bg-violet-500" /> 新仓库
68 </span>
69 </div>
70 </div>
71 )
72}
73
74export function DashboardPage() {
75 const [status, setStatus] = useState<SystemStatus | null>(null)
76 const [dashboard, setDashboard] = useState<Dashboard | null>(null)
77 const [error, setError] = useState<string | null>(null)
78 const [busy, setBusy] = useState(false)
79
80 const load = useCallback(async () => {
81 setBusy(true)
82 setError(null)
83 try {
84 const [statusValue, dashboardValue] = await Promise.all([
85 adminSystem.status(),
86 adminSystem.dashboard(),
87 ])
88 setStatus(statusValue)
89 setDashboard(dashboardValue)
90 } catch {
91 setError('系统状态加载失败')
92 } finally {
93 setBusy(false)
94 }
95 }, [])
96
97 useEffect(() => {
98 void load()
99 const timer = window.setInterval(() => {
100 void load()
101 }, 30000)
102 return () => window.clearInterval(timer)
103 }, [load])
104
105 if (!status || !dashboard) {
106 return (
107 <div className="space-y-3">
108 <ErrorBanner message={error} />
109 <Loading />
110 </div>
111 )
112 }
113
114 const counts = dashboard.counts
115 const trendTotals = dashboard.trend.reduce(
116 (acc, point) => ({
117 users: acc.users + point.users,
118 spaces: acc.spaces + point.spaces,
119 repos: acc.repos + point.repos,
120 }),
121 { users: 0, spaces: 0, repos: 0 },
122 )
123
124 return (
125 <div className="space-y-4">
126 <div className="flex flex-wrap items-center justify-between gap-2">
127 <div className="min-w-0">
128 <h1 className="text-lg font-semibold text-slate-900 dark:text-slate-100">系统状态</h1>
129 <p className="break-words text-xs text-slate-500">
130 adminhost v{status.process.version} · 运行 {formatUptime(status.process.uptime_secs)} · 更新于{' '}
131 {formatDateTime(dashboard.generated_at)}
132 </p>
133 </div>
134 <Button variant="secondary" size="sm" disabled={busy} onClick={() => void load()}>
135 刷新
136 </Button>
137 </div>
138
139 <ErrorBanner message={error} />
140
141 <div className="grid gap-3 md:grid-cols-4">
142 <DependencyCard name="PostgreSQL" dependency={status.dependencies.database} />
143 <DependencyCard name="Redis" dependency={status.dependencies.redis} />
144 <DependencyCard name="S3 存储" dependency={status.dependencies.storage} />
145 <DependencyCard name="搜索" dependency={status.dependencies.search} />
146 </div>
147
148 <div className="grid gap-3 md:grid-cols-3 xl:grid-cols-6">
149 {(
150 [
151 ['用户', counts.users],
152 ['活跃用户', counts.active_users],
153 ['活跃会话', dashboard.active_sessions],
154 ['受限用户', dashboard.restricted_users],
155 ['Space', counts.spaces],
156 ['活跃 Space', counts.active_spaces],
157 ['仓库', counts.repos],
158 ['房间', counts.rooms],
159 ['工单', counts.issues],
160 ['消息', counts.messages],
161 ['管理员', counts.admin_users],
162 ['14 天新增用户', trendTotals.users],
163 ] as [string, number][]
164 ).map(([label, value]) => (
165 <Card key={label} title={label}>
166 <div className="text-2xl font-semibold text-slate-900 dark:text-slate-100">{value}</div>
167 </Card>
168 ))}
169 </div>
170
171 <Card
172 title="14 天增长趋势"
173 action={
174 <span className="text-xs text-slate-500">
175 新增 {trendTotals.users} 用户 / {trendTotals.spaces} Space / {trendTotals.repos} 仓库
176 </span>
177 }
178 >
179 <TrendChart trend={dashboard.trend} />
180 </Card>
181
182 <div className="grid gap-3 lg:grid-cols-2">
183 <Card title="队列概览" action={<Link className="text-xs text-sky-600 hover:underline dark:text-sky-400" to="/queues">进入队列</Link>}>
184 <DataTable
185 columns={[
186 { key: 'name', title: '队列', render: (row) => row.name },
187 {
188 key: 'depth',
189 title: '深度',
190 render: (row) => (row.depth === null ? '-' : row.depth),
191 },
192 { key: 'failed', title: '失败', render: (row) => row.failed },
193 {
194 key: 'enabled',
195 title: '状态',
196 render: (row) => (
197 <Badge tone={row.enabled ? 'success' : 'neutral'}>
198 {row.enabled ? '启用' : '停用'}
199 </Badge>
200 ),
201 },
202 ]}
203 rows={dashboard.queues}
204 rowKey={(row) => row.name}
205 />
206 </Card>
207
208 <Card title="最近失败" action={<Link className="text-xs text-sky-600 hover:underline dark:text-sky-400" to="/tasks">进入任务</Link>}>
209 <DataTable
210 columns={[
211 { key: 'source', title: '来源', render: (row) => row.source },
212 {
213 key: 'state',
214 title: '状态',
215 render: (row) => <Badge tone={statusTone(row.state)}>{row.state}</Badge>,
216 },
217 {
218 key: 'error',
219 title: '错误',
220 render: (row) => (
221 <span className="line-clamp-2 max-w-[280px] break-all text-xs text-slate-500">
222 {row.error ?? '-'}
223 </span>
224 ),
225 },
226 {
227 key: 'created_at',
228 title: '时间',
229 render: (row) => (
230 <span className="text-xs text-slate-500">{formatDateTime(row.created_at)}</span>
231 ),
232 },
233 ]}
234 rows={dashboard.recent_failures}
235 rowKey={(row) => `${row.source}-${row.id}`}
236 empty="没有失败记录"
237 />
238 </Card>
239 </div>
240
241 <Card
242 title="最近管理操作"
243 action={<Link className="text-xs text-sky-600 hover:underline dark:text-sky-400" to="/logs">进入审计日志</Link>}
244 >
245 <DataTable
246 columns={[
247 { key: 'admin', title: '管理员', render: (row) => row.admin_username },
248 { key: 'action', title: '动作', render: (row) => row.action },
249 {
250 key: 'target',
251 title: '目标',
252 render: (row) => (
253 <span className="text-xs text-slate-500">
254 {row.target_type ? `${row.target_type} ${shortId(row.target_id ?? '')}` : '-'}
255 </span>
256 ),
257 },
258 {
259 key: 'ip',
260 title: '来源',
261 render: (row) => <span className="text-xs text-slate-500">{row.ip ?? '-'}</span>,
262 },
263 {
264 key: 'created_at',
265 title: '时间',
266 render: (row) => (
267 <span className="text-xs text-slate-500">{formatDateTime(row.created_at)}</span>
268 ),
269 },
270 ]}
271 rows={dashboard.recent_audit}
272 rowKey={(row) => row.id}
273 empty="暂无审计记录"
274 />
275 </Card>
276
277 <Card title="功能开关" action={<Link className="text-xs text-sky-600 hover:underline dark:text-sky-400" to="/system">查看配置</Link>}>
278 <p className="text-xs text-slate-500">
279 搜索 {status.features.search_enabled ? '开启' : '关闭'},导入{' '}
280 {status.features.import_enabled ? '开启' : '关闭'},邮件{' '}
281 {status.features.email_enabled ? '开启' : '关闭'},邮件队列{' '}
282 {status.features.email_queue_enabled ? '开启' : '关闭'}。
283 </p>
284 </Card>
285 </div>
286 )
287}
No newline at end of file
Deleted admin/src/pages/login.tsx
@@ -1,65 +0,0 @@
1import { useEffect, useState } from 'react'
2import { useNavigate } from 'react-router-dom'
3import { useSession } from '@/app/session'
4import { adminAuth } from '@/shared/api'
5import { ApiError } from '@/shared/api'
6import { Button, Card, ErrorBanner, Field, TextInput, useToast } from '@/shared/ui'
7
8export function LoginPage() {
9 const { me, loading, refresh } = useSession()
10 const navigate = useNavigate()
11 const toast = useToast()
12 const [username, setUsername] = useState('')
13 const [password, setPassword] = useState('')
14 const [error, setError] = useState<string | null>(null)
15 const [busy, setBusy] = useState(false)
16
17 useEffect(() => {
18 if (!loading && me) {
19 navigate(me.must_change_password ? '/password' : '/', { replace: true })
20 }
21 }, [loading, me, navigate])
22
23 const submit = async () => {
24 setBusy(true)
25 setError(null)
26 try {
27 const resp = await adminAuth.login(username, password)
28 await refresh()
29 toast('登录成功')
30 navigate(resp.must_change_password ? '/password' : '/', { replace: true })
31 } catch (err) {
32 setError(err instanceof ApiError ? err.message : '登录失败,请稍后重试。')
33 } finally {
34 setBusy(false)
35 }
36 }
37
38 return (
39 <div className="flex min-h-full items-center justify-center p-4">
40 <Card title="GitDataAI 管理后台" className="w-full max-w-sm">
41 <form
42 className="space-y-3"
43 onSubmit={(event) => {
44 event.preventDefault()
45 void submit()
46 }}
47 >
48 <ErrorBanner message={error} />
49 <Field label="管理员账号">
50 <TextInput value={username} onChange={setUsername} placeholder="username" />
51 </Field>
52 <Field label="密码">
53 <TextInput value={password} onChange={setPassword} type="password" />
54 </Field>
55 <Button type="submit" disabled={busy || !username || !password}>
56 {busy ? '登录中…' : '登录'}
57 </Button>
58 <p className="text-xs text-slate-500">
59 管理端使用独立会话,与普通用户登录互不影响。
60 </p>
61 </form>
62 </Card>
63 </div>
64 )
65}
No newline at end of file
Deleted admin/src/pages/logs.tsx
@@ -1,263 +0,0 @@
1import { useCallback, useEffect, useState } from 'react'
2import { adminLogs, csvDownload } from '@/shared/api'
3import type { AuditLogPage, JobLogPage } from '@/shared/api'
4import { ApiError } from '@/shared/api'
5import { formatDateTime, prettyJson } from '@/shared/format'
6import {
7 Badge,
8 Button,
9 Card,
10 DataTable,
11 ErrorBanner,
12 Field,
13 Loading,
14 Modal,
15 Pagination,
16 Select,
17 TextInput,
18 statusTone,
19} from '@/shared/ui'
20
21const PAGE_SIZE = 20
22
23export function LogsPage() {
24 const [tab, setTab] = useState<'audit' | 'jobs'>('audit')
25 const [action, setAction] = useState('')
26 const [targetType, setTargetType] = useState('')
27 const [keyword, setKeyword] = useState('')
28 const [adminUsername, setAdminUsername] = useState('')
29 const [from, setFrom] = useState('')
30 const [to, setTo] = useState('')
31 const [source, setSource] = useState('')
32 const [offset, setOffset] = useState(0)
33 const [audit, setAudit] = useState<AuditLogPage | null>(null)
34 const [jobs, setJobs] = useState<JobLogPage | null>(null)
35 const [detail, setDetail] = useState<unknown>(null)
36 const [error, setError] = useState<string | null>(null)
37
38 const load = useCallback(async () => {
39 setError(null)
40 try {
41 if (tab === 'audit') {
42 setAudit(
43 await adminLogs.audit({
44 action: action || undefined,
45 target_type: targetType || undefined,
46 q: keyword || undefined,
47 admin_username: adminUsername || undefined,
48 from: from ? new Date(from).toISOString() : undefined,
49 to: to ? new Date(to).toISOString() : undefined,
50 limit: PAGE_SIZE,
51 offset,
52 }),
53 )
54 } else {
55 setJobs(await adminLogs.jobs({ source: source || undefined, limit: PAGE_SIZE, offset }))
56 }
57 } catch (err) {
58 setError(err instanceof ApiError ? err.message : '日志加载失败')
59 }
60 }, [tab, action, targetType, keyword, adminUsername, from, to, source, offset])
61
62 useEffect(() => {
63 void load()
64 }, [load])
65
66 return (
67 <div className="space-y-4">
68 <div className="flex items-center justify-between">
69 <div>
70 <h1 className="text-lg font-semibold text-slate-900 dark:text-slate-100">日志</h1>
71 <p className="text-xs text-slate-500">管理员操作审计与作业错误日志</p>
72 </div>
73 <div className="flex gap-2">
74 <Button
75 size="sm"
76 variant={tab === 'audit' ? 'primary' : 'secondary'}
77 onClick={() => {
78 setTab('audit')
79 setOffset(0)
80 }}
81 >
82 审计日志
83 </Button>
84 <Button
85 size="sm"
86 variant={tab === 'jobs' ? 'primary' : 'secondary'}
87 onClick={() => {
88 setTab('jobs')
89 setOffset(0)
90 }}
91 >
92 作业日志
93 </Button>
94 </div>
95 </div>
96
97 <ErrorBanner message={error} />
98
99 {tab === 'audit' ? (
100 <Card
101 title="操作审计"
102 action={
103 <form
104 className="flex flex-wrap items-end gap-2"
105 onSubmit={(event) => {
106 event.preventDefault()
107 setOffset(0)
108 void load()
109 }}
110 >
111 <Field label="关键字">
112 <TextInput value={keyword} onChange={setKeyword} placeholder="管理员 / 动作 / 目标 ID" />
113 </Field>
114 <Field label="管理员">
115 <TextInput value={adminUsername} onChange={setAdminUsername} placeholder="admin" />
116 </Field>
117 <Field label="动作">
118 <TextInput value={action} onChange={setAction} placeholder="user.suspended …" />
119 </Field>
120 <Field label="目标类型">
121 <TextInput value={targetType} onChange={setTargetType} placeholder="user / space …" />
122 </Field>
123 <Field label="开始时间">
124 <TextInput value={from} onChange={setFrom} type="datetime-local" />
125 </Field>
126 <Field label="结束时间">
127 <TextInput value={to} onChange={setTo} type="datetime-local" />
128 </Field>
129 <Button type="submit" size="sm" variant="secondary">
130 过滤
131 </Button>
132 <Button
133 size="sm"
134 variant="secondary"
135 onClick={() =>
136 csvDownload('/admin/logs/audit/export', {
137 q: keyword || undefined,
138 admin_username: adminUsername || undefined,
139 action: action || undefined,
140 target_type: targetType || undefined,
141 from: from ? new Date(from).toISOString() : undefined,
142 to: to ? new Date(to).toISOString() : undefined,
143 })
144 }
145 >
146 导出 CSV
147 </Button>
148 </form>
149 }
150 >
151 {audit === null ? (
152 <Loading />
153 ) : (
154 <>
155 <DataTable
156 columns={[
157 {
158 key: 'time',
159 title: '时间',
160 render: (row) => (
161 <span className="text-xs text-slate-500">{formatDateTime(row.created_at)}</span>
162 ),
163 },
164 { key: 'admin', title: '管理员', render: (row) => row.admin_username },
165 { key: 'action', title: '动作', render: (row) => <Badge tone="info">{row.action}</Badge> },
166 {
167 key: 'target',
168 title: '目标',
169 render: (row) => (
170 <span className="text-xs text-slate-500">
171 {row.target_type ?? '-'} {row.target_id ? `(${row.target_id.slice(0, 8)}…)` : ''}
172 </span>
173 ),
174 },
175 {
176 key: 'ip',
177 title: 'IP',
178 render: (row) => <span className="text-xs text-slate-500">{row.ip ?? '-'}</span>,
179 },
180 {
181 key: 'actions',
182 title: '详情',
183 render: (row) => (
184 <Button size="sm" variant="secondary" onClick={() => setDetail(row.detail)}>
185 查看
186 </Button>
187 ),
188 },
189 ]}
190 rows={audit.items}
191 rowKey={(row) => row.id}
192 />
193 <Pagination total={audit.total} limit={audit.limit} offset={audit.offset} onChange={setOffset} />
194 </>
195 )}
196 </Card>
197 ) : (
198 <Card
199 title="作业错误"
200 action={
201 <Select
202 value={source}
203 onChange={(value) => {
204 setSource(value)
205 setOffset(0)
206 }}
207 options={[
208 { value: '', label: '全部来源' },
209 { value: 'import', label: 'import' },
210 { value: 'search_index', label: 'search_index' },
211 { value: 'chat_webhook', label: 'chat_webhook' },
212 { value: 'pipeline', label: 'pipeline' },
213 { value: 'system_tasks', label: 'system_tasks' },
214 ]}
215 />
216 }
217 >
218 {jobs === null ? (
219 <Loading />
220 ) : (
221 <>
222 <DataTable
223 columns={[
224 {
225 key: 'time',
226 title: '时间',
227 render: (row) => (
228 <span className="text-xs text-slate-500">{formatDateTime(row.created_at)}</span>
229 ),
230 },
231 { key: 'source', title: '来源', render: (row) => row.source },
232 {
233 key: 'state',
234 title: '状态',
235 render: (row) => <Badge tone={statusTone(row.state)}>{row.state}</Badge>,
236 },
237 {
238 key: 'error',
239 title: '错误',
240 render: (row) => (
241 <span className="line-clamp-2 max-w-[420px] break-all text-xs text-slate-500">
242 {row.error ?? '-'}
243 </span>
244 ),
245 },
246 ]}
247 rows={jobs.items}
248 rowKey={(row) => `${row.source}-${row.id}`}
249 />
250 <Pagination total={jobs.total} limit={jobs.limit} offset={jobs.offset} onChange={setOffset} />
251 </>
252 )}
253 </Card>
254 )}
255
256 <Modal open={detail !== null} title="审计详情" onClose={() => setDetail(null)}>
257 <pre className="max-h-80 overflow-auto rounded bg-slate-100 p-2 text-xs dark:bg-slate-800">
258 {prettyJson(detail)}
259 </pre>
260 </Modal>
261 </div>
262 )
263}
No newline at end of file
Deleted admin/src/pages/not-found.tsx
@@ -1,13 +0,0 @@
1import { Link } from 'react-router-dom'
2import { Card } from '@/shared/ui'
3
4export function NotFoundPage() {
5 return (
6 <Card title="页面不存在">
7 <p className="text-sm text-slate-500">没有找到对应的管理页面。</p>
8 <Link className="mt-3 inline-block text-sm text-sky-600 hover:underline dark:text-sky-400" to="/">
9 返回系统状态
10 </Link>
11 </Card>
12 )
13}
No newline at end of file
Deleted admin/src/pages/password.tsx

Diff too large to display.

Deleted admin/src/pages/queues.tsx

Diff too large to display.

Deleted admin/src/pages/repos.tsx

Diff too large to display.

Deleted admin/src/pages/runners.tsx

Diff too large to display.

Deleted admin/src/pages/space-detail.tsx

Diff too large to display.

Deleted admin/src/pages/spaces.tsx

Diff too large to display.

Deleted admin/src/pages/storage.tsx

Diff too large to display.

Deleted admin/src/pages/system.tsx

Diff too large to display.

Deleted admin/src/pages/tasks.tsx

Diff too large to display.

Deleted admin/src/pages/user-detail.tsx

Diff too large to display.

Deleted admin/src/pages/users.tsx

Diff too large to display.

Deleted admin/src/shared/api-types.ts

Diff too large to display.

Deleted admin/src/shared/api.ts

Diff too large to display.

Deleted admin/src/shared/format.ts

Diff too large to display.

Deleted admin/src/shared/ui.tsx

Diff too large to display.

Deleted admin/build.rs
@@ -1,135 +0,0 @@
1use std::collections::BTreeMap;
2use std::env;
3use std::fs;
4use std::path::{Path, PathBuf};
5use std::process::{Command, Stdio};
6
7fn main() {
8 println!("cargo:rerun-if-changed=build.rs");
9 for watched in [
10 "src",
11 "index.html",
12 "vite.config.ts",
13 "package.json",
14 "bun.lock",
15 "tsconfig.json",
16 "tsconfig.app.json",
17 "tsconfig.node.json",
18 ] {
19 println!("cargo:rerun-if-changed={watched}");
20 }
21
22 let root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
23 let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR"));
24
25 if env::var_os("ADMIN_UI_SKIP_FRONTEND_BUILD").is_none() && !build_frontend(&root) {
26 println!(
27 "cargo:warning=admin frontend build via bun/npm unavailable or failed; \
28 embedding existing dist/ as-is"
29 );
30 }
31
32 let dist = root.join("dist");
33 let index = dist.join("index.html");
34 if !index.is_file() {
35 panic!(
36 "dist/index.html not found — run `bun install && bun run build` \
37 (or `npm run build`) inside admin/ first"
38 );
39 }
40
41 let mut files: BTreeMap<String, PathBuf> = BTreeMap::new();
42 collect(&dist, &dist, &mut files);
43
44 let mut src = String::from("// @generated by build.rs — embedded admin frontend (dist/)\n\n");
45 src.push_str(&format!(
46 "pub const INDEX: &[u8] = include_bytes!({:?});\n\n",
47 index
48 ));
49 src.push_str("pub const FILES: &[(&str, &[u8])] = &[\n");
50 for (rel, abs) in &files {
51 if rel == "index.html" {
52 continue;
53 }
54 src.push_str(&format!(" ({:?}, include_bytes!({:?})),\n", rel, abs));
55 }
56 src.push_str("];\n\npub const PATHS: &[&str] = &[\n");
57 for rel in files.keys() {
58 src.push_str(&format!(" {:?},\n", rel));
59 }
60 src.push_str("];\n");
61
62 fs::write(out_dir.join("dist_embed.rs"), src).expect("write dist_embed.rs");
63}
64
65fn build_frontend(root: &Path) -> bool {
66 if !root.join("package.json").is_file() {
67 return false;
68 }
69 let tool = if command_ok("bun") {
70 "bun"
71 } else if command_ok("npm") {
72 npm_cmd()
73 } else {
74 return false;
75 };
76
77 let mut steps: Vec<(&str, &[&str])> = Vec::new();
78 if !root.join("node_modules").is_dir() {
79 steps.push((tool, &["install"]));
80 }
81 steps.push((tool, &["run", "build"]));
82
83 for (t, args) in steps {
84 eprintln!(
85 "cargo:warning=running `{t} {}` in {}",
86 args.join(" "),
87 root.display()
88 );
89 let ok = Command::new(t)
90 .args(args)
91 .current_dir(root)
92 .status()
93 .map(|s| s.success())
94 .unwrap_or(false);
95 if !ok {
96 return false;
97 }
98 }
99 true
100}
101
102#[cfg(windows)]
103fn npm_cmd() -> &'static str {
104 "npm.cmd"
105}
106#[cfg(not(windows))]
107fn npm_cmd() -> &'static str {
108 "npm"
109}
110
111fn command_ok(name: &str) -> bool {
112 Command::new(name)
113 .arg("--version")
114 .stdin(Stdio::null())
115 .stdout(Stdio::null())
116 .stderr(Stdio::null())
117 .status()
118 .is_ok_and(|s| s.success())
119}
120
121fn collect(base: &Path, dir: &Path, out: &mut BTreeMap<String, PathBuf>) {
122 for entry in fs::read_dir(dir).expect("read dist dir") {
123 let path = entry.expect("dist entry").path();
124 if path.is_dir() {
125 collect(base, &path, out);
126 } else {
127 let rel = path
128 .strip_prefix(base)
129 .unwrap()
130 .to_string_lossy()
131 .replace('\\', "/");
132 out.insert(rel, path);
133 }
134 }
135}
Deleted admin/bun.lock
@@ -1,224 +0,0 @@
1{
2 "lockfileVersion": 1,
3 "configVersion": 1,
4 "workspaces": {
5 "": {
6 "name": "gitdata-admin",
7 "dependencies": {
8 "@tailwindcss/vite": "^4.3.3",
9 "lucide-react": "^1.39.0",
10 "react": "^19.2.8",
11 "react-dom": "^19.2.8",
12 "react-router-dom": "^7.18.3",
13 "tailwindcss": "^4.3.3",
14 },
15 "devDependencies": {
16 "@types/node": "^24.13.3",
17 "@types/react": "^19.2.18",
18 "@types/react-dom": "^19.2.4",
19 "@vitejs/plugin-react": "^6.1.0",
20 "typescript": "~6.0.2",
21 "vite": "^8.2.2",
22 },
23 },
24 },
25 "packages": {
26 "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
27
28 "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
29
30 "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
31
32 "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="],
33
34 "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
35
36 "@oxc-project/types": ["@oxc-project/types@0.149.0", "", {}, "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA=="],
37
38 "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.8", "", { "os": "android", "cpu": "arm" }, "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw=="],
39
40 "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.8", "", { "os": "android", "cpu": "arm64" }, "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ=="],
41
42 "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA=="],
43
44 "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA=="],
45
46 "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ=="],
47
48 "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.8", "", { "os": "linux", "cpu": "arm" }, "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q=="],
49
50 "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew=="],
51
52 "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA=="],
53
54 "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.8", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA=="],
55
56 "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.8", "", { "os": "linux", "cpu": "s390x" }, "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ=="],
57
58 "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.8", "", { "os": "linux", "cpu": "x64" }, "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g=="],
59
60 "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.8", "", { "os": "linux", "cpu": "x64" }, "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA=="],
61
62 "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.8", "", { "os": "none", "cpu": "arm64" }, "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg=="],
63
64 "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A=="],
65
66 "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.8", "", { "os": "win32", "cpu": "x64" }, "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng=="],
67
68 "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
69
70 "@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="],
71
72 "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="],
73
74 "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="],
75
76 "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="],
77
78 "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="],
79
80 "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="],
81
82 "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="],
83
84 "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="],
85
86 "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="],
87
88 "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="],
89
90 "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="],
91
92 "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="],
93
94 "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="],
95
96 "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="],
97
98 "@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="],
99
100 "@types/node": ["@types/node@24.13.4", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw=="],
101
102 "@types/react": ["@types/react@19.3.0", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg=="],
103
104 "@types/react-dom": ["@types/react-dom@19.3.0", "", { "peerDependencies": { "@types/react": "^19.3.0" } }, "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q=="],
105
106 "@vitejs/plugin-react": ["@vitejs/plugin-react@6.1.1", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "oxc-transform-react": "^0.145.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler", "oxc-transform-react"] }, "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw=="],
107
108 "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
109
110 "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
111
112 "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
113
114 "enhanced-resolve": ["enhanced-resolve@5.25.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w=="],
115
116 "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
117
118 "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
119
120 "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
121
122 "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
123
124 "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
125
126 "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="],
127
128 "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="],
129
130 "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="],
131
132 "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="],
133
134 "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="],
135
136 "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="],
137
138 "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="],
139
140 "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="],
141
142 "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="],
143
144 "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="],
145
146 "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="],
147
148 "lucide-react": ["lucide-react@1.46.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Bv+FZXgZPrxc/NCl1e7JJVQFLdiCxYgxNVhqoV7X0p6I8ADJo8DxBnK1auH0fZz4AmqOJ3jgneL4f1i8LJQRAA=="],
149
150 "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
151
152 "nanoid": ["nanoid@3.3.19", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="],
153
154 "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
155
156 "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
157
158 "postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="],
159
160 "react": ["react@19.3.0", "", {}, "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog=="],
161
162 "react-dom": ["react-dom@19.3.0", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.0" } }, "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q=="],
163
164 "react-router": ["react-router@7.18.3", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-gyXgtdr5uACJ5b1Q4udzjVV+tb/rlHIMJKuJ0e89R4Kzgz47z/rgP0dIKxktqIEUhDHluGTPJJH/wRha7CyqsA=="],
165
166 "react-router-dom": ["react-router-dom@7.18.3", "", { "dependencies": { "react-router": "7.18.3" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-ytVbyBBM7vMfRCam25r0WMhSVSom909A8p+8m0/f1w853dz/xfFu6etAT2SEbVoSnI+ZoPRDqIsQXVT89gp7kg=="],
167
168 "rolldown": ["rolldown@1.2.8", "", { "dependencies": { "@oxc-project/types": "=0.149.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.8", "@rolldown/binding-android-arm64": "1.2.8", "@rolldown/binding-darwin-arm64": "1.2.8", "@rolldown/binding-darwin-x64": "1.2.8", "@rolldown/binding-freebsd-x64": "1.2.8", "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", "@rolldown/binding-linux-arm64-gnu": "1.2.8", "@rolldown/binding-linux-arm64-musl": "1.2.8", "@rolldown/binding-linux-ppc64-gnu": "1.2.8", "@rolldown/binding-linux-s390x-gnu": "1.2.8", "@rolldown/binding-linux-x64-gnu": "1.2.8", "@rolldown/binding-linux-x64-musl": "1.2.8", "@rolldown/binding-openharmony-arm64": "1.2.8", "@rolldown/binding-win32-arm64-msvc": "1.2.8", "@rolldown/binding-win32-x64-msvc": "1.2.8" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ=="],
169
170 "scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="],
171
172 "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
173
174 "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
175
176 "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
177
178 "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
179
180 "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
181
182 "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
183
184 "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
185
186 "vite": ["vite@8.3.0", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", "rolldown": "~1.2.6", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ=="],
187
188 "@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
189
190 "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
191
192 "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
193
194 "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="],
195
196 "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g=="],
197
198 "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="],
199
200 "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
201
202 "@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
203
204 "@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
205
206 "@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
207
208 "@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
209
210 "@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
211
212 "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
213
214 "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
215
216 "@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
217
218 "@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
219
220 "@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
221
222 "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
223 }
224}
Deleted admin/Cargo.toml
@@ -1,7 +0,0 @@
1[package]
2name = "admin-web"
3version.workspace = true
4edition.workspace = true
5
6[lib]
7path = "lib.rs"
No newline at end of file
Deleted admin/index.html
@@ -1,13 +0,0 @@
1<!doctype html>
2<html lang="zh-CN">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 <meta name="robots" content="noindex,nofollow" />
7 <title>GitData 管理后台</title>
8 </head>
9 <body>
10 <div id="root"></div>
11 <script type="module" src="/src/app/index.tsx"></script>
12 </body>
13</html>
No newline at end of file
Deleted admin/lib.rs
@@ -1,92 +0,0 @@
1mod dist_embed {
2 include!(concat!(env!("OUT_DIR"), "/dist_embed.rs"));
3}
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub struct Asset {
7 pub path: &'static str,
8 pub mime: &'static str,
9 pub content: &'static [u8],
10}
11
12pub fn index() -> Asset {
13 Asset {
14 path: "index.html",
15 mime: "text/html; charset=utf-8",
16 content: dist_embed::INDEX,
17 }
18}
19
20pub fn file(path: &str) -> Option<Asset> {
21 let key = normalize(path)?;
22 if key.is_empty() || key == "index.html" {
23 return Some(index());
24 }
25 dist_embed::FILES
26 .iter()
27 .find(|(p, _)| *p == key.as_str())
28 .map(|(p, content)| Asset {
29 path: p,
30 mime: mime_of(p),
31 content,
32 })
33}
34
35pub fn exists(path: &str) -> bool {
36 match normalize(path) {
37 None => false,
38 Some(key) => {
39 key.is_empty()
40 || key == "index.html"
41 || dist_embed::FILES.iter().any(|(p, _)| *p == key.as_str())
42 }
43 }
44}
45
46pub fn paths() -> &'static [&'static str] {
47 dist_embed::PATHS
48}
49
50fn normalize(path: &str) -> Option<String> {
51 let path = path.split(['?', '#']).next().unwrap_or(path);
52 let mut segs: Vec<&str> = Vec::new();
53 for seg in path.split('/') {
54 match seg {
55 "" | "." => {}
56 ".." => return None,
57 s => segs.push(s),
58 }
59 }
60 Some(segs.join("/"))
61}
62
63fn mime_of(path: &str) -> &'static str {
64 let ext = path
65 .rsplit_once('.')
66 .map_or("", |(_, e)| e)
67 .to_ascii_lowercase();
68 match ext.as_str() {
69 "html" | "htm" => "text/html; charset=utf-8",
70 "js" | "mjs" => "text/javascript; charset=utf-8",
71 "css" => "text/css; charset=utf-8",
72 "json" | "map" => "application/json",
73 "webmanifest" => "application/manifest+json",
74 "svg" => "image/svg+xml",
75 "png" => "image/png",
76 "jpg" | "jpeg" => "image/jpeg",
77 "gif" => "image/gif",
78 "webp" => "image/webp",
79 "avif" => "image/avif",
80 "ico" => "image/x-icon",
81 "wasm" => "application/wasm",
82 "ttf" => "font/ttf",
83 "otf" => "font/otf",
84 "woff" => "font/woff",
85 "woff2" => "font/woff2",
86 "eot" => "application/vnd.ms-fontobject",
87 "txt" => "text/plain; charset=utf-8",
88 "xml" => "application/xml",
89 "pdf" => "application/pdf",
90 _ => "application/octet-stream",
91 }
92}
Deleted admin/openapi.json
@@ -1,13683 +0,0 @@
1{
2 "openapi": "3.1.0",
3 "info": {
4 "title": "gitdata admin api",
5 "description": "GitData 管理后台 API。独立进程(adminhost)、独立会话 Cookie(gitdata_admin_session)、固定角色 super_admin/operator/viewer。",
6 "license": {
7 "name": ""
8 },
9 "version": "1.0"
10 },
11 "servers": [
12 {
13 "url": "/api/v1"
14 }
15 ],
16 "paths": {
17 "/admin/act/runs": {
18 "get": {
19 "tags": [
20 "admin-act"
21 ],
22 "description": "分页列出 act 工作流运行。",
23 "operationId": "list_runs",
24 "parameters": [
25 {
26 "name": "q",
27 "in": "query",
28 "required": false,
29 "schema": {
30 "type": [
31 "string",
32 "null"
33 ]
34 }
35 },
36 {
37 "name": "state",
38 "in": "query",
39 "required": false,
40 "schema": {
41 "oneOf": [
42 {
43 "type": "null"
44 },
45 {
46 "$ref": "#/components/schemas/WorkflowRunState"
47 }
48 ]
49 }
50 },
51 {
52 "name": "repo_id",
53 "in": "query",
54 "required": false,
55 "schema": {
56 "type": [
57 "string",
58 "null"
59 ],
60 "format": "uuid"
61 }
62 },
63 {
64 "name": "limit",
65 "in": "query",
66 "required": false,
67 "schema": {
68 "type": [
69 "integer",
70 "null"
71 ],
72 "format": "int64",
73 "minimum": 0
74 }
75 },
76 {
77 "name": "offset",
78 "in": "query",
79 "required": false,
80 "schema": {
81 "type": [
82 "integer",
83 "null"
84 ],
85 "format": "int64",
86 "minimum": 0
87 }
88 }
89 ],
90 "responses": {
91 "200": {
92 "description": "act runs",
93 "content": {
94 "application/json": {
95 "schema": {
96 "$ref": "#/components/schemas/AppResponseData_AdminActRunPage"
97 }
98 }
99 }
100 }
101 }
102 }
103 },
104 "/admin/act/runs/{id}/cancel": {
105 "post": {
106 "tags": [
107 "admin-act"
108 ],
109 "description": "请求取消 act 工作流运行。",
110 "operationId": "cancel_run",
111 "parameters": [
112 {
113 "name": "id",
114 "in": "path",
115 "required": true,
116 "schema": {
117 "type": "string",
118 "format": "uuid"
119 }
120 }
121 ],
122 "responses": {
123 "200": {
124 "description": "cancel requested",
125 "content": {
126 "application/json": {
127 "schema": {
128 "$ref": "#/components/schemas/AppResponseData_String"
129 }
130 }
131 }
132 }
133 }
134 }
135 },
136 "/admin/act/runs/{id}/rerun": {
137 "post": {
138 "tags": [
139 "admin-act"
140 ],
141 "description": "重新运行 act 工作流。",
142 "operationId": "rerun_run",
143 "parameters": [
144 {
145 "name": "id",
146 "in": "path",
147 "required": true,
148 "schema": {
149 "type": "string",
150 "format": "uuid"
151 }
152 }
153 ],
154 "responses": {
155 "200": {
156 "description": "rerun created",
157 "content": {
158 "application/json": {
159 "schema": {
160 "$ref": "#/components/schemas/AppResponseData_String"
161 }
162 }
163 }
164 }
165 }
166 }
167 },
168 "/admin/act/summary": {
169 "get": {
170 "tags": [
171 "admin-act"
172 ],
173 "description": "读取 act runner、作业和运行概览。",
174 "operationId": "summary",
175 "responses": {
176 "200": {
177 "description": "act summary",
178 "content": {
179 "application/json": {
180 "schema": {
181 "$ref": "#/components/schemas/AppResponseData_AdminActSummary"
182 }
183 }
184 }
185 }
186 }
187 }
188 },
189 "/admin/admins": {
190 "get": {
191 "tags": [
192 "admin-admins"
193 ],
194 "description": "管理员账号列表;仅超级管理员可见。",
195 "operationId": "list_admins",
196 "parameters": [
197 {
198 "name": "limit",
199 "in": "query",
200 "required": false,
201 "schema": {
202 "type": [
203 "integer",
204 "null"
205 ],
206 "format": "int64",
207 "minimum": 0
208 }
209 },
210 {
211 "name": "offset",
212 "in": "query",
213 "required": false,
214 "schema": {
215 "type": [
216 "integer",
217 "null"
218 ],
219 "format": "int64",
220 "minimum": 0
221 }
222 },
223 {
224 "name": "sort",
225 "in": "query",
226 "required": false,
227 "schema": {
228 "type": [
229 "string",
230 "null"
231 ]
232 }
233 }
234 ],
235 "responses": {
236 "200": {
237 "description": "admin page",
238 "content": {
239 "application/json": {
240 "schema": {
241 "$ref": "#/components/schemas/AppResponseData_Page_AdminDto"
242 }
243 }
244 }
245 },
246 "401": {
247 "description": "authentication required",
248 "content": {
249 "application/json": {
250 "schema": {
251 "$ref": "#/components/schemas/AppResponseData_NoData"
252 }
253 }
254 }
255 },
256 "403": {
257 "description": "operation not allowed",
258 "content": {
259 "application/json": {
260 "schema": {
261 "$ref": "#/components/schemas/AppResponseData_NoData"
262 }
263 }
264 }
265 }
266 }
267 },
268 "post": {
269 "tags": [
270 "admin-admins"
271 ],
272 "description": "创建管理员账号;仅超级管理员可操作。",
273 "operationId": "create_admin",
274 "requestBody": {
275 "content": {
276 "application/json": {
277 "schema": {
278 "$ref": "#/components/schemas/CreateAdminCmd"
279 }
280 }
281 },
282 "required": true
283 },
284 "responses": {
285 "200": {
286 "description": "admin created",
287 "content": {
288 "application/json": {
289 "schema": {
290 "$ref": "#/components/schemas/AppResponseData_AdminDto"
291 }
292 }
293 }
294 },
295 "400": {
296 "description": "request failed validation",
297 "content": {
298 "application/json": {
299 "schema": {
300 "$ref": "#/components/schemas/AppResponseData_NoData"
301 }
302 }
303 }
304 },
305 "409": {
306 "description": "resource conflict",
307 "content": {
308 "application/json": {
309 "schema": {
310 "$ref": "#/components/schemas/AppResponseData_NoData"
311 }
312 }
313 }
314 }
315 }
316 }
317 },
318 "/admin/admins/{id}": {
319 "get": {
320 "tags": [
321 "admin-admins"
322 ],
323 "description": "管理员账号详情。",
324 "operationId": "get_admin",
325 "parameters": [
326 {
327 "name": "id",
328 "in": "path",
329 "description": "admin id",
330 "required": true,
331 "schema": {
332 "type": "string",
333 "format": "uuid"
334 }
335 }
336 ],
337 "responses": {
338 "200": {
339 "description": "admin detail",
340 "content": {
341 "application/json": {
342 "schema": {
343 "$ref": "#/components/schemas/AppResponseData_AdminDto"
344 }
345 }
346 }
347 },
348 "404": {
349 "description": "resource not found",
350 "content": {
351 "application/json": {
352 "schema": {
353 "$ref": "#/components/schemas/AppResponseData_NoData"
354 }
355 }
356 }
357 }
358 }
359 },
360 "delete": {
361 "tags": [
362 "admin-admins"
363 ],
364 "description": "删除管理员账号;禁止删除自己与最后一个超级管理员。",
365 "operationId": "delete_admin",
366 "parameters": [
367 {
368 "name": "id",
369 "in": "path",
370 "description": "admin id",
371 "required": true,
372 "schema": {
373 "type": "string",
374 "format": "uuid"
375 }
376 }
377 ],
378 "responses": {
379 "200": {
380 "description": "admin deleted",
381 "content": {
382 "application/json": {
383 "schema": {
384 "$ref": "#/components/schemas/AppResponseData_NoData"
385 }
386 }
387 }
388 },
389 "404": {
390 "description": "resource not found",
391 "content": {
392 "application/json": {
393 "schema": {
394 "$ref": "#/components/schemas/AppResponseData_NoData"
395 }
396 }
397 }
398 },
399 "409": {
400 "description": "resource conflict",
401 "content": {
402 "application/json": {
403 "schema": {
404 "$ref": "#/components/schemas/AppResponseData_NoData"
405 }
406 }
407 }
408 }
409 }
410 },
411 "patch": {
412 "tags": [
413 "admin-admins"
414 ],
415 "description": "更新管理员显示名/角色/启用状态;禁止操作最后一个超级管理员。",
416 "operationId": "update_admin",
417 "parameters": [
418 {
419 "name": "id",
420 "in": "path",
421 "description": "admin id",
422 "required": true,
423 "schema": {
424 "type": "string",
425 "format": "uuid"
426 }
427 }
428 ],
429 "requestBody": {
430 "content": {
431 "application/json": {
432 "schema": {
433 "$ref": "#/components/schemas/UpdateAdminCmd"
434 }
435 }
436 },
437 "required": true
438 },
439 "responses": {
440 "200": {
441 "description": "admin updated",
442 "content": {
443 "application/json": {
444 "schema": {
445 "$ref": "#/components/schemas/AppResponseData_AdminDto"
446 }
447 }
448 }
449 },
450 "409": {
451 "description": "resource conflict",
452 "content": {
453 "application/json": {
454 "schema": {
455 "$ref": "#/components/schemas/AppResponseData_NoData"
456 }
457 }
458 }
459 }
460 }
461 }
462 },
463 "/admin/admins/{id}/reset-password": {
464 "post": {
465 "tags": [
466 "admin-admins"
467 ],
468 "description": "重置管理员口令并要求下次登录强制改密。",
469 "operationId": "reset_admin_password",
470 "parameters": [
471 {
472 "name": "id",
473 "in": "path",
474 "description": "admin id",
475 "required": true,
476 "schema": {
477 "type": "string",
478 "format": "uuid"
479 }
480 }
481 ],
482 "requestBody": {
483 "content": {
484 "application/json": {
485 "schema": {
486 "$ref": "#/components/schemas/ResetAdminPasswordCmd"
487 }
488 }
489 },
490 "required": true
491 },
492 "responses": {
493 "200": {
494 "description": "password reset",
495 "content": {
496 "application/json": {
497 "schema": {
498 "$ref": "#/components/schemas/AppResponseData_NoData"
499 }
500 }
501 }
502 },
503 "400": {
504 "description": "request failed validation",
505 "content": {
506 "application/json": {
507 "schema": {
508 "$ref": "#/components/schemas/AppResponseData_NoData"
509 }
510 }
511 }
512 },
513 "404": {
514 "description": "resource not found",
515 "content": {
516 "application/json": {
517 "schema": {
518 "$ref": "#/components/schemas/AppResponseData_NoData"
519 }
520 }
521 }
522 }
523 }
524 }
525 },
526 "/admin/admins/{id}/revoke-sessions": {
527 "post": {
528 "tags": [
529 "admin-admins"
530 ],
531 "description": "踢出管理员全部会话:提升口令版本使已签发会话立即失效;操作他人需超级管理员。",
532 "operationId": "revoke_admin_sessions",
533 "parameters": [
534 {
535 "name": "id",
536 "in": "path",
537 "description": "admin id",
538 "required": true,
539 "schema": {
540 "type": "string",
541 "format": "uuid"
542 }
543 }
544 ],
545 "responses": {
546 "200": {
547 "description": "sessions revoked",
548 "content": {
549 "application/json": {
550 "schema": {
551 "$ref": "#/components/schemas/AppResponseData_NoData"
552 }
553 }
554 }
555 },
556 "403": {
557 "description": "operation not allowed",
558 "content": {
559 "application/json": {
560 "schema": {
561 "$ref": "#/components/schemas/AppResponseData_NoData"
562 }
563 }
564 }
565 },
566 "404": {
567 "description": "resource not found",
568 "content": {
569 "application/json": {
570 "schema": {
571 "$ref": "#/components/schemas/AppResponseData_NoData"
572 }
573 }
574 }
575 }
576 }
577 }
578 },
579 "/admin/auth/login": {
580 "post": {
581 "tags": [
582 "admin-auth"
583 ],
584 "description": "管理员用户名口令登录;会话使用独立的 gitdata_admin_session Cookie。",
585 "operationId": "login",
586 "requestBody": {
587 "content": {
588 "application/json": {
589 "schema": {
590 "$ref": "#/components/schemas/AdminLoginCmd"
591 }
592 }
593 },
594 "required": true
595 },
596 "responses": {
597 "200": {
598 "description": "login accepted",
599 "content": {
600 "application/json": {
601 "schema": {
602 "$ref": "#/components/schemas/AppResponseData_AdminLoginResp"
603 }
604 }
605 }
606 },
607 "400": {
608 "description": "request failed validation",
609 "content": {
610 "application/json": {
611 "schema": {
612 "$ref": "#/components/schemas/AppResponseData_NoData"
613 }
614 }
615 }
616 },
617 "401": {
618 "description": "invalid credentials",
619 "content": {
620 "application/json": {
621 "schema": {
622 "$ref": "#/components/schemas/AppResponseData_NoData"
623 }
624 }
625 }
626 },
627 "429": {
628 "description": "rate limit exceeded",
629 "content": {
630 "application/json": {
631 "schema": {
632 "$ref": "#/components/schemas/AppResponseData_NoData"
633 }
634 }
635 }
636 }
637 }
638 }
639 },
640 "/admin/auth/logout": {
641 "post": {
642 "tags": [
643 "admin-auth"
644 ],
645 "description": "退出管理端会话;重复调用始终返回成功。",
646 "operationId": "logout",
647 "responses": {
648 "200": {
649 "description": "logged out",
650 "content": {
651 "application/json": {
652 "schema": {
653 "$ref": "#/components/schemas/AppResponseData_NoData"
654 }
655 }
656 }
657 }
658 }
659 }
660 },
661 "/admin/chat/attachments": {
662 "get": {
663 "tags": [
664 "admin-chat"
665 ],
666 "description": "聊天附件列表:按房间/上传者/状态/文件名筛选。",
667 "operationId": "list_attachments",
668 "parameters": [
669 {
670 "name": "room_id",
671 "in": "query",
672 "required": false,
673 "schema": {
674 "type": [
675 "string",
676 "null"
677 ],
678 "format": "uuid"
679 }
680 },
681 {
682 "name": "user_id",
683 "in": "query",
684 "required": false,
685 "schema": {
686 "type": [
687 "string",
688 "null"
689 ],
690 "format": "uuid"
691 }
692 },
693 {
694 "name": "status",
695 "in": "query",
696 "required": false,
697 "schema": {
698 "type": [
699 "string",
700 "null"
701 ]
702 }
703 },
704 {
705 "name": "q",
706 "in": "query",
707 "required": false,
708 "schema": {
709 "type": [
710 "string",
711 "null"
712 ]
713 }
714 },
715 {
716 "name": "limit",
717 "in": "query",
718 "required": false,
719 "schema": {
720 "type": [
721 "integer",
722 "null"
723 ],
724 "format": "int64",
725 "minimum": 0
726 }
727 },
728 {
729 "name": "offset",
730 "in": "query",
731 "required": false,
732 "schema": {
733 "type": [
734 "integer",
735 "null"
736 ],
737 "format": "int64",
738 "minimum": 0
739 }
740 }
741 ],
742 "responses": {
743 "200": {
744 "description": "attachment page",
745 "content": {
746 "application/json": {
747 "schema": {
748 "$ref": "#/components/schemas/AppResponseData_Page_AdminChatAttachmentDto"
749 }
750 }
751 }
752 },
753 "401": {
754 "description": "authentication required",
755 "content": {
756 "application/json": {
757 "schema": {
758 "$ref": "#/components/schemas/AppResponseData_NoData"
759 }
760 }
761 }
762 }
763 }
764 }
765 },
766 "/admin/chat/attachments/{id}": {
767 "delete": {
768 "tags": [
769 "admin-chat"
770 ],
771 "description": "删除聊天附件记录并清理未被其它记录引用的对象存储文件;仅超级管理员可操作。",
772 "operationId": "delete_attachment",
773 "parameters": [
774 {
775 "name": "id",
776 "in": "path",
777 "description": "attachment id",
778 "required": true,
779 "schema": {
780 "type": "string",
781 "format": "uuid"
782 }
783 }
784 ],
785 "responses": {
786 "200": {
787 "description": "attachment deleted",
788 "content": {
789 "application/json": {
790 "schema": {
791 "$ref": "#/components/schemas/AppResponseData_DeleteChatAttachmentResp"
792 }
793 }
794 }
795 },
796 "404": {
797 "description": "resource not found",
798 "content": {
799 "application/json": {
800 "schema": {
801 "$ref": "#/components/schemas/AppResponseData_NoData"
802 }
803 }
804 }
805 }
806 }
807 }
808 },
809 "/admin/chat/messages": {
810 "get": {
811 "tags": [
812 "admin-chat"
813 ],
814 "description": "消息列表:按房间/作者/状态/关键字/时间筛选,返回附件与表情计数。",
815 "operationId": "list_messages",
816 "parameters": [
817 {
818 "name": "room_id",
819 "in": "query",
820 "required": false,
821 "schema": {
822 "type": [
823 "string",
824 "null"
825 ],
826 "format": "uuid"
827 }
828 },
829 {
830 "name": "author_id",
831 "in": "query",
832 "required": false,
833 "schema": {
834 "type": [
835 "string",
836 "null"
837 ],
838 "format": "uuid"
839 }
840 },
841 {
842 "name": "status",
843 "in": "query",
844 "required": false,
845 "schema": {
846 "type": [
847 "string",
848 "null"
849 ]
850 }
851 },
852 {
853 "name": "q",
854 "in": "query",
855 "required": false,
856 "schema": {
857 "type": [
858 "string",
859 "null"
860 ]
861 }
862 },
863 {
864 "name": "from",
865 "in": "query",
866 "required": false,
867 "schema": {
868 "type": [
869 "string",
870 "null"
871 ],
872 "format": "date-time"
873 }
874 },
875 {
876 "name": "to",
877 "in": "query",
878 "required": false,
879 "schema": {
880 "type": [
881 "string",
882 "null"
883 ],
884 "format": "date-time"
885 }
886 },
887 {
888 "name": "limit",
889 "in": "query",
890 "required": false,
891 "schema": {
892 "type": [
893 "integer",
894 "null"
895 ],
896 "format": "int64",
897 "minimum": 0
898 }
899 },
900 {
901 "name": "offset",
902 "in": "query",
903 "required": false,
904 "schema": {
905 "type": [
906 "integer",
907 "null"
908 ],
909 "format": "int64",
910 "minimum": 0
911 }
912 }
913 ],
914 "responses": {
915 "200": {
916 "description": "message page",
917 "content": {
918 "application/json": {
919 "schema": {
920 "$ref": "#/components/schemas/AppResponseData_Page_AdminChatMessageDto"
921 }
922 }
923 }
924 },
925 "401": {
926 "description": "authentication required",
927 "content": {
928 "application/json": {
929 "schema": {
930 "$ref": "#/components/schemas/AppResponseData_NoData"
931 }
932 }
933 }
934 }
935 }
936 }
937 },
938 "/admin/chat/messages/bulk-delete": {
939 "post": {
940 "tags": [
941 "admin-chat"
942 ],
943 "description": "批量软删除消息(≤100 条),清空内容并移除附件/表情/编辑等从属记录。",
944 "operationId": "bulk_delete_messages",
945 "requestBody": {
946 "content": {
947 "application/json": {
948 "schema": {
949 "$ref": "#/components/schemas/BulkDeleteChatMessagesCmd"
950 }
951 }
952 },
953 "required": true
954 },
955 "responses": {
956 "200": {
957 "description": "bulk delete result",
958 "content": {
959 "application/json": {
960 "schema": {
961 "$ref": "#/components/schemas/AppResponseData_BulkDeleteChatMessagesResp"
962 }
963 }
964 }
965 },
966 "400": {
967 "description": "request failed validation",
968 "content": {
969 "application/json": {
970 "schema": {
971 "$ref": "#/components/schemas/AppResponseData_NoData"
972 }
973 }
974 }
975 }
976 }
977 }
978 },
979 "/admin/chat/messages/export": {
980 "get": {
981 "tags": [
982 "admin-chat"
983 ],
984 "description": "按当前筛选导出消息 CSV(最多 10000 行)。",
985 "operationId": "export_messages",
986 "parameters": [
987 {
988 "name": "room_id",
989 "in": "query",
990 "required": false,
991 "schema": {
992 "type": [
993 "string",
994 "null"
995 ],
996 "format": "uuid"
997 }
998 },
999 {
1000 "name": "author_id",
1001 "in": "query",
1002 "required": false,
1003 "schema": {
1004 "type": [
1005 "string",
1006 "null"
1007 ],
1008 "format": "uuid"
1009 }
1010 },
1011 {
1012 "name": "status",
1013 "in": "query",
1014 "required": false,
1015 "schema": {
1016 "type": [
1017 "string",
1018 "null"
1019 ]
1020 }
1021 },
1022 {
1023 "name": "q",
1024 "in": "query",
1025 "required": false,
1026 "schema": {
1027 "type": [
1028 "string",
1029 "null"
1030 ]
1031 }
1032 },
1033 {
1034 "name": "from",
1035 "in": "query",
1036 "required": false,
1037 "schema": {
1038 "type": [
1039 "string",
1040 "null"
1041 ],
1042 "format": "date-time"
1043 }
1044 },
1045 {
1046 "name": "to",
1047 "in": "query",
1048 "required": false,
1049 "schema": {
1050 "type": [
1051 "string",
1052 "null"
1053 ],
1054 "format": "date-time"
1055 }
1056 },
1057 {
1058 "name": "limit",
1059 "in": "query",
1060 "required": false,
1061 "schema": {
1062 "type": [
1063 "integer",
1064 "null"
1065 ],
1066 "format": "int64",
1067 "minimum": 0
1068 }
1069 },
1070 {
1071 "name": "offset",
1072 "in": "query",
1073 "required": false,
1074 "schema": {
1075 "type": [
1076 "integer",
1077 "null"
1078 ],
1079 "format": "int64",
1080 "minimum": 0
1081 }
1082 }
1083 ],
1084 "responses": {
1085 "200": {
1086 "description": "csv export"
1087 }
1088 }
1089 }
1090 },
1091 "/admin/chat/messages/{id}": {
1092 "get": {
1093 "tags": [
1094 "admin-chat"
1095 ],
1096 "description": "消息详情:正文、编辑历史、表情聚合、附件与所属线程。",
1097 "operationId": "get_message",
1098 "parameters": [
1099 {
1100 "name": "id",
1101 "in": "path",
1102 "description": "message id",
1103 "required": true,
1104 "schema": {
1105 "type": "string",
1106 "format": "uuid"
1107 }
1108 }
1109 ],
1110 "responses": {
1111 "200": {
1112 "description": "message detail",
1113 "content": {
1114 "application/json": {
1115 "schema": {
1116 "$ref": "#/components/schemas/AppResponseData_AdminChatMessageDetail"
1117 }
1118 }
1119 }
1120 },
1121 "404": {
1122 "description": "resource not found",
1123 "content": {
1124 "application/json": {
1125 "schema": {
1126 "$ref": "#/components/schemas/AppResponseData_NoData"
1127 }
1128 }
1129 }
1130 }
1131 }
1132 }
1133 },
1134 "/admin/chat/rooms": {
1135 "get": {
1136 "tags": [
1137 "admin-chat"
1138 ],
1139 "description": "房间列表:关键字、Space、类型、可见性与启停状态筛选。",
1140 "operationId": "list_rooms",
1141 "parameters": [
1142 {
1143 "name": "q",
1144 "in": "query",
1145 "required": false,
1146 "schema": {
1147 "type": [
1148 "string",
1149 "null"
1150 ]
1151 }
1152 },
1153 {
1154 "name": "space_id",
1155 "in": "query",
1156 "required": false,
1157 "schema": {
1158 "type": [
1159 "string",
1160 "null"
1161 ],
1162 "format": "uuid"
1163 }
1164 },
1165 {
1166 "name": "kind",
1167 "in": "query",
1168 "required": false,
1169 "schema": {
1170 "type": [
1171 "string",
1172 "null"
1173 ]
1174 }
1175 },
1176 {
1177 "name": "status",
1178 "in": "query",
1179 "required": false,
1180 "schema": {
1181 "type": [
1182 "string",
1183 "null"
1184 ]
1185 }
1186 },
1187 {
1188 "name": "visibility",
1189 "in": "query",
1190 "required": false,
1191 "schema": {
1192 "type": [
1193 "string",
1194 "null"
1195 ]
1196 }
1197 },
1198 {
1199 "name": "limit",
1200 "in": "query",
1201 "required": false,
1202 "schema": {
1203 "type": [
1204 "integer",
1205 "null"
1206 ],
1207 "format": "int64",
1208 "minimum": 0
1209 }
1210 },
1211 {
1212 "name": "offset",
1213 "in": "query",
1214 "required": false,
1215 "schema": {
1216 "type": [
1217 "integer",
1218 "null"
1219 ],
1220 "format": "int64",
1221 "minimum": 0
1222 }
1223 }
1224 ],
1225 "responses": {
1226 "200": {
1227 "description": "room page",
1228 "content": {
1229 "application/json": {
1230 "schema": {
1231 "$ref": "#/components/schemas/AppResponseData_Page_AdminChatRoomSummary"
1232 }
1233 }
1234 }
1235 },
1236 "400": {
1237 "description": "request failed validation",
1238 "content": {
1239 "application/json": {
1240 "schema": {
1241 "$ref": "#/components/schemas/AppResponseData_NoData"
1242 }
1243 }
1244 }
1245 }
1246 }
1247 }
1248 },
1249 "/admin/chat/rooms/{id}": {
1250 "get": {
1251 "tags": [
1252 "admin-chat"
1253 ],
1254 "description": "房间详情:基础信息、成员/消息计数与删除/封禁/邀请统计。",
1255 "operationId": "get_room",
1256 "parameters": [
1257 {
1258 "name": "id",
1259 "in": "path",
1260 "description": "room id",
1261 "required": true,
1262 "schema": {
1263 "type": "string",
1264 "format": "uuid"
1265 }
1266 }
1267 ],
1268 "responses": {
1269 "200": {
1270 "description": "room detail",
1271 "content": {
1272 "application/json": {
1273 "schema": {
1274 "$ref": "#/components/schemas/AppResponseData_AdminChatRoomDetail"
1275 }
1276 }
1277 }
1278 },
1279 "404": {
1280 "description": "resource not found",
1281 "content": {
1282 "application/json": {
1283 "schema": {
1284 "$ref": "#/components/schemas/AppResponseData_NoData"
1285 }
1286 }
1287 }
1288 }
1289 }
1290 },
1291 "delete": {
1292 "tags": [
1293 "admin-chat"
1294 ],
1295 "description": "删除房间及其消息/成员/线程/附件,动作不可恢复;仅超级管理员可操作。",
1296 "operationId": "delete_room",
1297 "parameters": [
1298 {
1299 "name": "id",
1300 "in": "path",
1301 "description": "room id",
1302 "required": true,
1303 "schema": {
1304 "type": "string",
1305 "format": "uuid"
1306 }
1307 }
1308 ],
1309 "responses": {
1310 "200": {
1311 "description": "room deleted",
1312 "content": {
1313 "application/json": {
1314 "schema": {
1315 "$ref": "#/components/schemas/AppResponseData_DeleteChatRoomResp"
1316 }
1317 }
1318 }
1319 },
1320 "409": {
1321 "description": "child rooms exist",
1322 "content": {
1323 "application/json": {
1324 "schema": {
1325 "$ref": "#/components/schemas/AppResponseData_NoData"
1326 }
1327 }
1328 }
1329 }
1330 }
1331 },
1332 "patch": {
1333 "tags": [
1334 "admin-chat"
1335 ],
1336 "description": "更新房间属性:名称、主题、私有、NSFW 与启停。",
1337 "operationId": "update_room",
1338 "parameters": [
1339 {
1340 "name": "id",
1341 "in": "path",
1342 "description": "room id",
1343 "required": true,
1344 "schema": {
1345 "type": "string",
1346 "format": "uuid"
1347 }
1348 }
1349 ],
1350 "requestBody": {
1351 "content": {
1352 "application/json": {
1353 "schema": {
1354 "$ref": "#/components/schemas/UpdateChatRoomCmd"
1355 }
1356 }
1357 },
1358 "required": true
1359 },
1360 "responses": {
1361 "200": {
1362 "description": "room updated",
1363 "content": {
1364 "application/json": {
1365 "schema": {
1366 "$ref": "#/components/schemas/AppResponseData_AdminChatRoomDetail"
1367 }
1368 }
1369 }
1370 },
1371 "409": {
1372 "description": "resource conflict",
1373 "content": {
1374 "application/json": {
1375 "schema": {
1376 "$ref": "#/components/schemas/AppResponseData_NoData"
1377 }
1378 }
1379 }
1380 }
1381 }
1382 }
1383 },
1384 "/admin/chat/rooms/{id}/members": {
1385 "get": {
1386 "tags": [
1387 "admin-chat"
1388 ],
1389 "description": "房间成员列表:可按在席/已离开筛选。",
1390 "operationId": "list_members",
1391 "parameters": [
1392 {
1393 "name": "id",
1394 "in": "path",
1395 "description": "room id",
1396 "required": true,
1397 "schema": {
1398 "type": "string",
1399 "format": "uuid"
1400 }
1401 },
1402 {
1403 "name": "status",
1404 "in": "query",
1405 "required": false,
1406 "schema": {
1407 "type": [
1408 "string",
1409 "null"
1410 ]
1411 }
1412 },
1413 {
1414 "name": "limit",
1415 "in": "query",
1416 "required": false,
1417 "schema": {
1418 "type": [
1419 "integer",
1420 "null"
1421 ],
1422 "format": "int64",
1423 "minimum": 0
1424 }
1425 },
1426 {
1427 "name": "offset",
1428 "in": "query",
1429 "required": false,
1430 "schema": {
1431 "type": [
1432 "integer",
1433 "null"
1434 ],
1435 "format": "int64",
1436 "minimum": 0
1437 }
1438 }
1439 ],
1440 "responses": {
1441 "200": {
1442 "description": "member page",
1443 "content": {
1444 "application/json": {
1445 "schema": {
1446 "$ref": "#/components/schemas/AppResponseData_Page_AdminChatRoomMemberDto"
1447 }
1448 }
1449 }
1450 },
1451 "404": {
1452 "description": "resource not found",
1453 "content": {
1454 "application/json": {
1455 "schema": {
1456 "$ref": "#/components/schemas/AppResponseData_NoData"
1457 }
1458 }
1459 }
1460 }
1461 }
1462 }
1463 },
1464 "/admin/chat/rooms/{id}/members/{uid}": {
1465 "delete": {
1466 "tags": [
1467 "admin-chat"
1468 ],
1469 "description": "将成员移出房间,动作即时生效并广播 member_left 事件。",
1470 "operationId": "remove_member",
1471 "parameters": [
1472 {
1473 "name": "id",
1474 "in": "path",
1475 "description": "room id",
1476 "required": true,
1477 "schema": {
1478 "type": "string",
1479 "format": "uuid"
1480 }
1481 },
1482 {
1483 "name": "uid",
1484 "in": "path",
1485 "description": "user id",
1486 "required": true,
1487 "schema": {
1488 "type": "string",
1489 "format": "uuid"
1490 }
1491 }
1492 ],
1493 "responses": {
1494 "200": {
1495 "description": "member removed",
1496 "content": {
1497 "application/json": {
1498 "schema": {
1499 "$ref": "#/components/schemas/AppResponseData_RemoveChatMemberResp"
1500 }
1501 }
1502 }
1503 },
1504 "404": {
1505 "description": "resource not found",
1506 "content": {
1507 "application/json": {
1508 "schema": {
1509 "$ref": "#/components/schemas/AppResponseData_NoData"
1510 }
1511 }
1512 }
1513 }
1514 }
1515 }
1516 },
1517 "/admin/chat/rooms/{id}/purge": {
1518 "post": {
1519 "tags": [
1520 "admin-chat"
1521 ],
1522 "description": "清空房间全部消息、线程与附件(保留房间本身);仅超级管理员可操作。",
1523 "operationId": "purge_room",
1524 "parameters": [
1525 {
1526 "name": "id",
1527 "in": "path",
1528 "description": "room id",
1529 "required": true,
1530 "schema": {
1531 "type": "string",
1532 "format": "uuid"
1533 }
1534 }
1535 ],
1536 "responses": {
1537 "200": {
1538 "description": "room messages purged",
1539 "content": {
1540 "application/json": {
1541 "schema": {
1542 "$ref": "#/components/schemas/AppResponseData_PurgeChatMessagesResp"
1543 }
1544 }
1545 }
1546 },
1547 "404": {
1548 "description": "resource not found",
1549 "content": {
1550 "application/json": {
1551 "schema": {
1552 "$ref": "#/components/schemas/AppResponseData_NoData"
1553 }
1554 }
1555 }
1556 }
1557 }
1558 }
1559 },
1560 "/admin/chat/stats": {
1561 "get": {
1562 "tags": [
1563 "admin-chat"
1564 ],
1565 "description": "聊天域统计:房间/消息/线程/附件/成员/封禁汇总与房间类型分布。",
1566 "operationId": "stats",
1567 "responses": {
1568 "200": {
1569 "description": "chat stats",
1570 "content": {
1571 "application/json": {
1572 "schema": {
1573 "$ref": "#/components/schemas/AppResponseData_AdminChatStatsDto"
1574 }
1575 }
1576 }
1577 },
1578 "401": {
1579 "description": "authentication required",
1580 "content": {
1581 "application/json": {
1582 "schema": {
1583 "$ref": "#/components/schemas/AppResponseData_NoData"
1584 }
1585 }
1586 }
1587 }
1588 }
1589 }
1590 },
1591 "/admin/dashboard": {
1592 "get": {
1593 "tags": [
1594 "admin-system"
1595 ],
1596 "description": "管理看板聚合:核心计数、14 天增长趋势、活跃会话、最近审计与失败、队列概览。",
1597 "operationId": "dashboard",
1598 "responses": {
1599 "200": {
1600 "description": "dashboard",
1601 "content": {
1602 "application/json": {
1603 "schema": {
1604 "$ref": "#/components/schemas/AppResponseData_DashboardDto"
1605 }
1606 }
1607 }
1608 },
1609 "401": {
1610 "description": "authentication required",
1611 "content": {
1612 "application/json": {
1613 "schema": {
1614 "$ref": "#/components/schemas/AppResponseData_NoData"
1615 }
1616 }
1617 }
1618 }
1619 }
1620 }
1621 },
1622 "/admin/environments/{id}": {
1623 "delete": {
1624 "tags": [
1625 "admin-runners"
1626 ],
1627 "description": "删除部署环境及其 secrets 与部署历史。",
1628 "operationId": "delete_environment",
1629 "parameters": [
1630 {
1631 "name": "id",
1632 "in": "path",
1633 "required": true,
1634 "schema": {
1635 "type": "string",
1636 "format": "uuid"
1637 }
1638 }
1639 ],
1640 "responses": {
1641 "200": {
1642 "description": "environment deleted",
1643 "content": {
1644 "application/json": {
1645 "schema": {
1646 "$ref": "#/components/schemas/AppResponseData_Vec_EnvironmentAdminDto"
1647 }
1648 }
1649 }
1650 }
1651 }
1652 }
1653 },
1654 "/admin/environments/{id}/secrets": {
1655 "put": {
1656 "tags": [
1657 "admin-runners"
1658 ],
1659 "description": "创建或更新环境级 secret。",
1660 "operationId": "upsert_environment_secret",
1661 "parameters": [
1662 {
1663 "name": "id",
1664 "in": "path",
1665 "required": true,
1666 "schema": {
1667 "type": "string",
1668 "format": "uuid"
1669 }
1670 }
1671 ],
1672 "requestBody": {
1673 "content": {
1674 "application/json": {
1675 "schema": {
1676 "$ref": "#/components/schemas/UpsertEnvironmentSecretCmd"
1677 }
1678 }
1679 },
1680 "required": true
1681 },
1682 "responses": {
1683 "200": {
1684 "description": "secret upserted",
1685 "content": {
1686 "application/json": {
1687 "schema": {
1688 "$ref": "#/components/schemas/AppResponseData_Vec_EnvironmentAdminDto"
1689 }
1690 }
1691 }
1692 }
1693 }
1694 }
1695 },
1696 "/admin/environments/{id}/secrets/{name}": {
1697 "delete": {
1698 "tags": [
1699 "admin-runners"
1700 ],
1701 "description": "删除环境级 secret。",
1702 "operationId": "delete_environment_secret",
1703 "parameters": [
1704 {
1705 "name": "id",
1706 "in": "path",
1707 "required": true,
1708 "schema": {
1709 "type": "string",
1710 "format": "uuid"
1711 }
1712 },
1713 {
1714 "name": "name",
1715 "in": "path",
1716 "required": true,
1717 "schema": {
1718 "type": "string"
1719 }
1720 }
1721 ],
1722 "responses": {
1723 "200": {
1724 "description": "secret deleted",
1725 "content": {
1726 "application/json": {
1727 "schema": {
1728 "$ref": "#/components/schemas/AppResponseData_Vec_EnvironmentAdminDto"
1729 }
1730 }
1731 }
1732 }
1733 }
1734 }
1735 },
1736 "/admin/logs/audit": {
1737 "get": {
1738 "tags": [
1739 "admin-logs"
1740 ],
1741 "description": "管理操作审计日志(登录事件与成功变更)。",
1742 "operationId": "list_audit_logs",
1743 "parameters": [
1744 {
1745 "name": "admin_id",
1746 "in": "query",
1747 "required": false,
1748 "schema": {
1749 "type": [
1750 "string",
1751 "null"
1752 ],
1753 "format": "uuid"
1754 }
1755 },
1756 {
1757 "name": "admin_username",
1758 "in": "query",
1759 "required": false,
1760 "schema": {
1761 "type": [
1762 "string",
1763 "null"
1764 ]
1765 }
1766 },
1767 {
1768 "name": "action",
1769 "in": "query",
1770 "required": false,
1771 "schema": {
1772 "type": [
1773 "string",
1774 "null"
1775 ]
1776 }
1777 },
1778 {
1779 "name": "target_type",
1780 "in": "query",
1781 "required": false,
1782 "schema": {
1783 "type": [
1784 "string",
1785 "null"
1786 ]
1787 }
1788 },
1789 {
1790 "name": "target_id",
1791 "in": "query",
1792 "required": false,
1793 "schema": {
1794 "type": [
1795 "string",
1796 "null"
1797 ]
1798 }
1799 },
1800 {
1801 "name": "q",
1802 "in": "query",
1803 "required": false,
1804 "schema": {
1805 "type": [
1806 "string",
1807 "null"
1808 ]
1809 }
1810 },
1811 {
1812 "name": "from",
1813 "in": "query",
1814 "required": false,
1815 "schema": {
1816 "type": [
1817 "string",
1818 "null"
1819 ],
1820 "format": "date-time"
1821 }
1822 },
1823 {
1824 "name": "to",
1825 "in": "query",
1826 "required": false,
1827 "schema": {
1828 "type": [
1829 "string",
1830 "null"
1831 ],
1832 "format": "date-time"
1833 }
1834 },
1835 {
1836 "name": "limit",
1837 "in": "query",
1838 "required": false,
1839 "schema": {
1840 "type": [
1841 "integer",
1842 "null"
1843 ],
1844 "format": "int64",
1845 "minimum": 0
1846 }
1847 },
1848 {
1849 "name": "offset",
1850 "in": "query",
1851 "required": false,
1852 "schema": {
1853 "type": [
1854 "integer",
1855 "null"
1856 ],
1857 "format": "int64",
1858 "minimum": 0
1859 }
1860 }
1861 ],
1862 "responses": {
1863 "200": {
1864 "description": "audit page",
1865 "content": {
1866 "application/json": {
1867 "schema": {
1868 "$ref": "#/components/schemas/AppResponseData_AuditLogPageDto"
1869 }
1870 }
1871 }
1872 },
1873 "401": {
1874 "description": "authentication required",
1875 "content": {
1876 "application/json": {
1877 "schema": {
1878 "$ref": "#/components/schemas/AppResponseData_NoData"
1879 }
1880 }
1881 }
1882 }
1883 }
1884 }
1885 },
1886 "/admin/logs/audit/export": {
1887 "get": {
1888 "tags": [
1889 "admin-logs"
1890 ],
1891 "description": "按筛选条件导出审计日志 CSV(最多 10000 行)。",
1892 "operationId": "export_audit_logs",
1893 "parameters": [
1894 {
1895 "name": "admin_id",
1896 "in": "query",
1897 "required": false,
1898 "schema": {
1899 "type": [
1900 "string",
1901 "null"
1902 ],
1903 "format": "uuid"
1904 }
1905 },
1906 {
1907 "name": "admin_username",
1908 "in": "query",
1909 "required": false,
1910 "schema": {
1911 "type": [
1912 "string",
1913 "null"
1914 ]
1915 }
1916 },
1917 {
1918 "name": "action",
1919 "in": "query",
1920 "required": false,
1921 "schema": {
1922 "type": [
1923 "string",
1924 "null"
1925 ]
1926 }
1927 },
1928 {
1929 "name": "target_type",
1930 "in": "query",
1931 "required": false,
1932 "schema": {
1933 "type": [
1934 "string",
1935 "null"
1936 ]
1937 }
1938 },
1939 {
1940 "name": "target_id",
1941 "in": "query",
1942 "required": false,
1943 "schema": {
1944 "type": [
1945 "string",
1946 "null"
1947 ]
1948 }
1949 },
1950 {
1951 "name": "q",
1952 "in": "query",
1953 "required": false,
1954 "schema": {
1955 "type": [
1956 "string",
1957 "null"
1958 ]
1959 }
1960 },
1961 {
1962 "name": "from",
1963 "in": "query",
1964 "required": false,
1965 "schema": {
1966 "type": [
1967 "string",
1968 "null"
1969 ],
1970 "format": "date-time"
1971 }
1972 },
1973 {
1974 "name": "to",
1975 "in": "query",
1976 "required": false,
1977 "schema": {
1978 "type": [
1979 "string",
1980 "null"
1981 ],
1982 "format": "date-time"
1983 }
1984 },
1985 {
1986 "name": "limit",
1987 "in": "query",
1988 "required": false,
1989 "schema": {
1990 "type": [
1991 "integer",
1992 "null"
1993 ],
1994 "format": "int64",
1995 "minimum": 0
1996 }
1997 },
1998 {
1999 "name": "offset",
2000 "in": "query",
2001 "required": false,
2002 "schema": {
2003 "type": [
2004 "integer",
2005 "null"
2006 ],
2007 "format": "int64",
2008 "minimum": 0
2009 }
2010 }
2011 ],
2012 "responses": {
2013 "200": {
2014 "description": "csv export"
2015 }
2016 }
2017 }
2018 },
2019 "/admin/logs/jobs": {
2020 "get": {
2021 "tags": [
2022 "admin-logs"
2023 ],
2024 "description": "作业错误日志:聚合 import/search/system_tasks/webhook/pipeline 的失败信息。",
2025 "operationId": "list_job_logs",
2026 "parameters": [
2027 {
2028 "name": "source",
2029 "in": "query",
2030 "required": false,
2031 "schema": {
2032 "type": [
2033 "string",
2034 "null"
2035 ]
2036 }
2037 },
2038 {
2039 "name": "from",
2040 "in": "query",
2041 "required": false,
2042 "schema": {
2043 "type": [
2044 "string",
2045 "null"
2046 ],
2047 "format": "date-time"
2048 }
2049 },
2050 {
2051 "name": "to",
2052 "in": "query",
2053 "required": false,
2054 "schema": {
2055 "type": [
2056 "string",
2057 "null"
2058 ],
2059 "format": "date-time"
2060 }
2061 },
2062 {
2063 "name": "limit",
2064 "in": "query",
2065 "required": false,
2066 "schema": {
2067 "type": [
2068 "integer",
2069 "null"
2070 ],
2071 "format": "int64",
2072 "minimum": 0
2073 }
2074 },
2075 {
2076 "name": "offset",
2077 "in": "query",
2078 "required": false,
2079 "schema": {
2080 "type": [
2081 "integer",
2082 "null"
2083 ],
2084 "format": "int64",
2085 "minimum": 0
2086 }
2087 }
2088 ],
2089 "responses": {
2090 "200": {
2091 "description": "job log page",
2092 "content": {
2093 "application/json": {
2094 "schema": {
2095 "$ref": "#/components/schemas/AppResponseData_JobLogPageDto"
2096 }
2097 }
2098 }
2099 },
2100 "401": {
2101 "description": "authentication required",
2102 "content": {
2103 "application/json": {
2104 "schema": {
2105 "$ref": "#/components/schemas/AppResponseData_NoData"
2106 }
2107 }
2108 }
2109 }
2110 }
2111 }
2112 },
2113 "/admin/me": {
2114 "get": {
2115 "tags": [
2116 "admin-auth"
2117 ],
2118 "description": "当前管理员资料。",
2119 "operationId": "me",
2120 "responses": {
2121 "200": {
2122 "description": "current admin",
2123 "content": {
2124 "application/json": {
2125 "schema": {
2126 "$ref": "#/components/schemas/AppResponseData_AdminDto"
2127 }
2128 }
2129 }
2130 },
2131 "401": {
2132 "description": "authentication required",
2133 "content": {
2134 "application/json": {
2135 "schema": {
2136 "$ref": "#/components/schemas/AppResponseData_NoData"
2137 }
2138 }
2139 }
2140 }
2141 }
2142 }
2143 },
2144 "/admin/me/password": {
2145 "patch": {
2146 "tags": [
2147 "admin-auth"
2148 ],
2149 "description": "修改当前管理员口令;口令版本变更会失效其他管理端会话。",
2150 "operationId": "change_password",
2151 "requestBody": {
2152 "content": {
2153 "application/json": {
2154 "schema": {
2155 "$ref": "#/components/schemas/ChangePasswordCmd"
2156 }
2157 }
2158 },
2159 "required": true
2160 },
2161 "responses": {
2162 "200": {
2163 "description": "password changed",
2164 "content": {
2165 "application/json": {
2166 "schema": {
2167 "$ref": "#/components/schemas/AppResponseData_NoData"
2168 }
2169 }
2170 }
2171 },
2172 "400": {
2173 "description": "request failed validation",
2174 "content": {
2175 "application/json": {
2176 "schema": {
2177 "$ref": "#/components/schemas/AppResponseData_NoData"
2178 }
2179 }
2180 }
2181 },
2182 "401": {
2183 "description": "authentication required",
2184 "content": {
2185 "application/json": {
2186 "schema": {
2187 "$ref": "#/components/schemas/AppResponseData_NoData"
2188 }
2189 }
2190 }
2191 }
2192 }
2193 }
2194 },
2195 "/admin/queues": {
2196 "get": {
2197 "tags": [
2198 "admin-queues"
2199 ],
2200 "description": "队列快照:Redis git 同步队列、邮件 pub/sub 与各数据库作业队列深度/状态。",
2201 "operationId": "list_queues",
2202 "responses": {
2203 "200": {
2204 "description": "queue snapshots",
2205 "content": {
2206 "application/json": {
2207 "schema": {
2208 "$ref": "#/components/schemas/AppResponseData_Vec_QueueSnapshotDto"
2209 }
2210 }
2211 }
2212 },
2213 "401": {
2214 "description": "authentication required",
2215 "content": {
2216 "application/json": {
2217 "schema": {
2218 "$ref": "#/components/schemas/AppResponseData_NoData"
2219 }
2220 }
2221 }
2222 }
2223 }
2224 }
2225 },
2226 "/admin/queues/{name}/failed": {
2227 "get": {
2228 "tags": [
2229 "admin-queues"
2230 ],
2231 "description": "队列失败明细:返回该队列来源的失败作业(数据库队列),可按任务模块重试。",
2232 "operationId": "failed_jobs",
2233 "parameters": [
2234 {
2235 "name": "name",
2236 "in": "path",
2237 "description": "queue name",
2238 "required": true,
2239 "schema": {
2240 "type": "string"
2241 }
2242 },
2243 {
2244 "name": "limit",
2245 "in": "query",
2246 "required": false,
2247 "schema": {
2248 "type": [
2249 "integer",
2250 "null"
2251 ],
2252 "format": "int64",
2253 "minimum": 0
2254 }
2255 },
2256 {
2257 "name": "offset",
2258 "in": "query",
2259 "required": false,
2260 "schema": {
2261 "type": [
2262 "integer",
2263 "null"
2264 ],
2265 "format": "int64",
2266 "minimum": 0
2267 }
2268 }
2269 ],
2270 "responses": {
2271 "200": {
2272 "description": "failed job page",
2273 "content": {
2274 "application/json": {
2275 "schema": {
2276 "$ref": "#/components/schemas/AppResponseData_JobLogPageDto"
2277 }
2278 }
2279 }
2280 },
2281 "400": {
2282 "description": "unsupported queue",
2283 "content": {
2284 "application/json": {
2285 "schema": {
2286 "$ref": "#/components/schemas/AppResponseData_NoData"
2287 }
2288 }
2289 }
2290 }
2291 }
2292 }
2293 },
2294 "/admin/queues/{name}/purge": {
2295 "post": {
2296 "tags": [
2297 "admin-queues"
2298 ],
2299 "description": "清空队列:git 同步队列直接删除 Redis list,数据库队列删除终态记录;仅超级管理员可操作。",
2300 "operationId": "purge",
2301 "parameters": [
2302 {
2303 "name": "name",
2304 "in": "path",
2305 "description": "queue name",
2306 "required": true,
2307 "schema": {
2308 "type": "string"
2309 }
2310 }
2311 ],
2312 "responses": {
2313 "200": {
2314 "description": "queue purged",
2315 "content": {
2316 "application/json": {
2317 "schema": {
2318 "$ref": "#/components/schemas/AppResponseData_QueueActionResp"
2319 }
2320 }
2321 }
2322 },
2323 "400": {
2324 "description": "unsupported queue",
2325 "content": {
2326 "application/json": {
2327 "schema": {
2328 "$ref": "#/components/schemas/AppResponseData_NoData"
2329 }
2330 }
2331 }
2332 }
2333 }
2334 }
2335 },
2336 "/admin/queues/{name}/requeue-failed": {
2337 "post": {
2338 "tags": [
2339 "admin-queues"
2340 ],
2341 "description": "重排失败作业(search_index/import/chat_webhook/pipeline/deletion/system_tasks/ai)。AI 队列会按持久化载荷重新投递,缺载荷的历史作业保持失败。",
2342 "operationId": "requeue_failed",
2343 "parameters": [
2344 {
2345 "name": "name",
2346 "in": "path",
2347 "description": "queue name",
2348 "required": true,
2349 "schema": {
2350 "type": "string"
2351 }
2352 }
2353 ],
2354 "responses": {
2355 "200": {
2356 "description": "failed items requeued",
2357 "content": {
2358 "application/json": {
2359 "schema": {
2360 "$ref": "#/components/schemas/AppResponseData_QueueActionResp"
2361 }
2362 }
2363 }
2364 },
2365 "400": {
2366 "description": "unsupported queue",
2367 "content": {
2368 "application/json": {
2369 "schema": {
2370 "$ref": "#/components/schemas/AppResponseData_NoData"
2371 }
2372 }
2373 }
2374 }
2375 }
2376 }
2377 },
2378 "/admin/repos": {
2379 "get": {
2380 "tags": [
2381 "admin-repos"
2382 ],
2383 "description": "分页列出仓库及其归属 Space。",
2384 "operationId": "list_repos",
2385 "parameters": [
2386 {
2387 "name": "q",
2388 "in": "query",
2389 "required": false,
2390 "schema": {
2391 "type": [
2392 "string",
2393 "null"
2394 ]
2395 }
2396 },
2397 {
2398 "name": "space_id",
2399 "in": "query",
2400 "required": false,
2401 "schema": {
2402 "type": [
2403 "string",
2404 "null"
2405 ],
2406 "format": "uuid"
2407 }
2408 },
2409 {
2410 "name": "archived",
2411 "in": "query",
2412 "required": false,
2413 "schema": {
2414 "type": [
2415 "boolean",
2416 "null"
2417 ]
2418 }
2419 },
2420 {
2421 "name": "private",
2422 "in": "query",
2423 "required": false,
2424 "schema": {
2425 "type": [
2426 "boolean",
2427 "null"
2428 ]
2429 }
2430 },
2431 {
2432 "name": "limit",
2433 "in": "query",
2434 "required": false,
2435 "schema": {
2436 "type": [
2437 "integer",
2438 "null"
2439 ],
2440 "format": "int64",
2441 "minimum": 0
2442 }
2443 },
2444 {
2445 "name": "offset",
2446 "in": "query",
2447 "required": false,
2448 "schema": {
2449 "type": [
2450 "integer",
2451 "null"
2452 ],
2453 "format": "int64",
2454 "minimum": 0
2455 }
2456 }
2457 ],
2458 "responses": {
2459 "200": {
2460 "description": "repo list",
2461 "content": {
2462 "application/json": {
2463 "schema": {
2464 "$ref": "#/components/schemas/AppResponseData_AdminRepoPage"
2465 }
2466 }
2467 }
2468 }
2469 }
2470 }
2471 },
2472 "/admin/repos/{id}": {
2473 "get": {
2474 "tags": [
2475 "admin-repos"
2476 ],
2477 "description": "读取仓库管理详情。",
2478 "operationId": "get_repo",
2479 "parameters": [
2480 {
2481 "name": "id",
2482 "in": "path",
2483 "required": true,
2484 "schema": {
2485 "type": "string",
2486 "format": "uuid"
2487 }
2488 }
2489 ],
2490 "responses": {
2491 "200": {
2492 "description": "repo detail",
2493 "content": {
2494 "application/json": {
2495 "schema": {
2496 "$ref": "#/components/schemas/AppResponseData_AdminRepoSummary"
2497 }
2498 }
2499 }
2500 }
2501 }
2502 },
2503 "patch": {
2504 "tags": [
2505 "admin-repos"
2506 ],
2507 "description": "更新仓库默认分支、可见性、归档、LFS 或配额。",
2508 "operationId": "update_repo",
2509 "parameters": [
2510 {
2511 "name": "id",
2512 "in": "path",
2513 "required": true,
2514 "schema": {
2515 "type": "string",
2516 "format": "uuid"
2517 }
2518 }
2519 ],
2520 "requestBody": {
2521 "content": {
2522 "application/json": {
2523 "schema": {
2524 "$ref": "#/components/schemas/UpdateRepoCmd"
2525 }
2526 }
2527 },
2528 "required": true
2529 },
2530 "responses": {
2531 "200": {
2532 "description": "repo updated",
2533 "content": {
2534 "application/json": {
2535 "schema": {
2536 "$ref": "#/components/schemas/AppResponseData_AdminRepoSummary"
2537 }
2538 }
2539 }
2540 }
2541 }
2542 }
2543 },
2544 "/admin/runner-tokens": {
2545 "get": {
2546 "tags": [
2547 "admin-runners"
2548 ],
2549 "description": "列出注册令牌(不含明文)。",
2550 "operationId": "list_runner_tokens",
2551 "responses": {
2552 "200": {
2553 "description": "token list",
2554 "content": {
2555 "application/json": {
2556 "schema": {
2557 "$ref": "#/components/schemas/AppResponseData_Vec_RunnerTokenDto"
2558 }
2559 }
2560 }
2561 }
2562 }
2563 },
2564 "post": {
2565 "tags": [
2566 "admin-runners"
2567 ],
2568 "description": "创建一次性 runner 注册令牌;明文只在本次响应中返回。",
2569 "operationId": "create_runner_token",
2570 "requestBody": {
2571 "content": {
2572 "application/json": {
2573 "schema": {
2574 "$ref": "#/components/schemas/CreateRunnerTokenCmd"
2575 }
2576 }
2577 },
2578 "required": true
2579 },
2580 "responses": {
2581 "200": {
2582 "description": "token created",
2583 "content": {
2584 "application/json": {
2585 "schema": {
2586 "$ref": "#/components/schemas/AppResponseData_CreatedRunnerTokenDto"
2587 }
2588 }
2589 }
2590 }
2591 }
2592 }
2593 },
2594 "/admin/runner-tokens/{id}/revoke": {
2595 "post": {
2596 "tags": [
2597 "admin-runners"
2598 ],
2599 "description": "吊销注册令牌或 runner 凭据。",
2600 "operationId": "revoke_runner_token",
2601 "parameters": [
2602 {
2603 "name": "id",
2604 "in": "path",
2605 "required": true,
2606 "schema": {
2607 "type": "string",
2608 "format": "uuid"
2609 }
2610 }
2611 ],
2612 "responses": {
2613 "200": {
2614 "description": "token revoked",
2615 "content": {
2616 "application/json": {
2617 "schema": {
2618 "$ref": "#/components/schemas/AppResponseData_Vec_RunnerTokenDto"
2619 }
2620 }
2621 }
2622 }
2623 }
2624 }
2625 },
2626 "/admin/runners": {
2627 "get": {
2628 "tags": [
2629 "admin-runners"
2630 ],
2631 "description": "列出已注册的 runner 及其活跃作业数。",
2632 "operationId": "list_runners",
2633 "parameters": [
2634 {
2635 "name": "limit",
2636 "in": "query",
2637 "required": false,
2638 "schema": {
2639 "type": [
2640 "integer",
2641 "null"
2642 ],
2643 "format": "int64",
2644 "minimum": 0
2645 }
2646 },
2647 {
2648 "name": "offset",
2649 "in": "query",
2650 "required": false,
2651 "schema": {
2652 "type": [
2653 "integer",
2654 "null"
2655 ],
2656 "format": "int64",
2657 "minimum": 0
2658 }
2659 }
2660 ],
2661 "responses": {
2662 "200": {
2663 "description": "runner list",
2664 "content": {
2665 "application/json": {
2666 "schema": {
2667 "$ref": "#/components/schemas/AppResponseData_RunnerPageDto"
2668 }
2669 }
2670 }
2671 }
2672 }
2673 }
2674 },
2675 "/admin/runners/{id}": {
2676 "delete": {
2677 "tags": [
2678 "admin-runners"
2679 ],
2680 "description": "删除 runner 及其凭据。",
2681 "operationId": "delete_runner",
2682 "parameters": [
2683 {
2684 "name": "id",
2685 "in": "path",
2686 "required": true,
2687 "schema": {
2688 "type": "string",
2689 "format": "uuid"
2690 }
2691 }
2692 ],
2693 "responses": {
2694 "200": {
2695 "description": "runner deleted",
2696 "content": {
2697 "application/json": {
2698 "schema": {
2699 "$ref": "#/components/schemas/AppResponseData_Vec_RunnerDto"
2700 }
2701 }
2702 }
2703 }
2704 }
2705 },
2706 "patch": {
2707 "tags": [
2708 "admin-runners"
2709 ],
2710 "description": "更新 runner 的标签、容量或启用状态。",
2711 "operationId": "update_runner",
2712 "parameters": [
2713 {
2714 "name": "id",
2715 "in": "path",
2716 "required": true,
2717 "schema": {
2718 "type": "string",
2719 "format": "uuid"
2720 }
2721 }
2722 ],
2723 "requestBody": {
2724 "content": {
2725 "application/json": {
2726 "schema": {
2727 "$ref": "#/components/schemas/UpdateRunnerCmd"
2728 }
2729 }
2730 },
2731 "required": true
2732 },
2733 "responses": {
2734 "200": {
2735 "description": "runner updated",
2736 "content": {
2737 "application/json": {
2738 "schema": {
2739 "$ref": "#/components/schemas/AppResponseData_RunnerDto"
2740 }
2741 }
2742 }
2743 }
2744 }
2745 }
2746 },
2747 "/admin/spaces": {
2748 "get": {
2749 "tags": [
2750 "admin-spaces"
2751 ],
2752 "description": "Space 列表:关键字、状态、Owner 与分页。",
2753 "operationId": "list_spaces",
2754 "parameters": [
2755 {
2756 "name": "q",
2757 "in": "query",
2758 "required": false,
2759 "schema": {
2760 "type": [
2761 "string",
2762 "null"
2763 ]
2764 }
2765 },
2766 {
2767 "name": "status",
2768 "in": "query",
2769 "required": false,
2770 "schema": {
2771 "type": [
2772 "string",
2773 "null"
2774 ]
2775 }
2776 },
2777 {
2778 "name": "owner_id",
2779 "in": "query",
2780 "required": false,
2781 "schema": {
2782 "type": [
2783 "string",
2784 "null"
2785 ],
2786 "format": "uuid"
2787 }
2788 },
2789 {
2790 "name": "limit",
2791 "in": "query",
2792 "required": false,
2793 "schema": {
2794 "type": [
2795 "integer",
2796 "null"
2797 ],
2798 "format": "int64",
2799 "minimum": 0
2800 }
2801 },
2802 {
2803 "name": "offset",
2804 "in": "query",
2805 "required": false,
2806 "schema": {
2807 "type": [
2808 "integer",
2809 "null"
2810 ],
2811 "format": "int64",
2812 "minimum": 0
2813 }
2814 }
2815 ],
2816 "responses": {
2817 "200": {
2818 "description": "space page",
2819 "content": {
2820 "application/json": {
2821 "schema": {
2822 "$ref": "#/components/schemas/AppResponseData_Page_AdminSpaceSummary"
2823 }
2824 }
2825 }
2826 },
2827 "401": {
2828 "description": "authentication required",
2829 "content": {
2830 "application/json": {
2831 "schema": {
2832 "$ref": "#/components/schemas/AppResponseData_NoData"
2833 }
2834 }
2835 }
2836 }
2837 }
2838 }
2839 },
2840 "/admin/spaces/{id}": {
2841 "get": {
2842 "tags": [
2843 "admin-spaces"
2844 ],
2845 "description": "Space 详情:Owner、成员数、仓库/房间/目录统计。",
2846 "operationId": "get_space",
2847 "parameters": [
2848 {
2849 "name": "id",
2850 "in": "path",
2851 "description": "space id",
2852 "required": true,
2853 "schema": {
2854 "type": "string",
2855 "format": "uuid"
2856 }
2857 }
2858 ],
2859 "responses": {
2860 "200": {
2861 "description": "space detail",
2862 "content": {
2863 "application/json": {
2864 "schema": {
2865 "$ref": "#/components/schemas/AppResponseData_AdminSpaceDetail"
2866 }
2867 }
2868 }
2869 },
2870 "404": {
2871 "description": "resource not found",
2872 "content": {
2873 "application/json": {
2874 "schema": {
2875 "$ref": "#/components/schemas/AppResponseData_NoData"
2876 }
2877 }
2878 }
2879 }
2880 }
2881 },
2882 "delete": {
2883 "tags": [
2884 "admin-spaces"
2885 ],
2886 "description": "停用 Space(软删除);仅超级管理员可操作。",
2887 "operationId": "delete_space",
2888 "parameters": [
2889 {
2890 "name": "id",
2891 "in": "path",
2892 "description": "space id",
2893 "required": true,
2894 "schema": {
2895 "type": "string",
2896 "format": "uuid"
2897 }
2898 }
2899 ],
2900 "responses": {
2901 "200": {
2902 "description": "space deleted",
2903 "content": {
2904 "application/json": {
2905 "schema": {
2906 "$ref": "#/components/schemas/AppResponseData_NoData"
2907 }
2908 }
2909 }
2910 },
2911 "404": {
2912 "description": "resource not found",
2913 "content": {
2914 "application/json": {
2915 "schema": {
2916 "$ref": "#/components/schemas/AppResponseData_NoData"
2917 }
2918 }
2919 }
2920 }
2921 }
2922 },
2923 "patch": {
2924 "tags": [
2925 "admin-spaces"
2926 ],
2927 "description": "更新 Space:名称、描述、私有与启用状态。",
2928 "operationId": "update_space",
2929 "parameters": [
2930 {
2931 "name": "id",
2932 "in": "path",
2933 "description": "space id",
2934 "required": true,
2935 "schema": {
2936 "type": "string",
2937 "format": "uuid"
2938 }
2939 }
2940 ],
2941 "requestBody": {
2942 "content": {
2943 "application/json": {
2944 "schema": {
2945 "$ref": "#/components/schemas/UpdateSpaceCmd"
2946 }
2947 }
2948 },
2949 "required": true
2950 },
2951 "responses": {
2952 "200": {
2953 "description": "space updated",
2954 "content": {
2955 "application/json": {
2956 "schema": {
2957 "$ref": "#/components/schemas/AppResponseData_AdminSpaceDetail"
2958 }
2959 }
2960 }
2961 },
2962 "404": {
2963 "description": "resource not found",
2964 "content": {
2965 "application/json": {
2966 "schema": {
2967 "$ref": "#/components/schemas/AppResponseData_NoData"
2968 }
2969 }
2970 }
2971 }
2972 }
2973 }
2974 },
2975 "/admin/spaces/{id}/environments": {
2976 "get": {
2977 "tags": [
2978 "admin-runners"
2979 ],
2980 "description": "列出某个空间的部署环境与已配置的 secret 名称。",
2981 "operationId": "list_environments",
2982 "parameters": [
2983 {
2984 "name": "id",
2985 "in": "path",
2986 "required": true,
2987 "schema": {
2988 "type": "string",
2989 "format": "uuid"
2990 }
2991 }
2992 ],
2993 "responses": {
2994 "200": {
2995 "description": "environment list",
2996 "content": {
2997 "application/json": {
2998 "schema": {
2999 "$ref": "#/components/schemas/AppResponseData_Vec_EnvironmentAdminDto"
3000 }
3001 }
3002 }
3003 }
3004 }
3005 },
3006 "put": {
3007 "tags": [
3008 "admin-runners"
3009 ],
3010 "description": "创建或更新空间下的部署环境。",
3011 "operationId": "upsert_environment",
3012 "parameters": [
3013 {
3014 "name": "id",
3015 "in": "path",
3016 "required": true,
3017 "schema": {
3018 "type": "string",
3019 "format": "uuid"
3020 }
3021 }
3022 ],
3023 "requestBody": {
3024 "content": {
3025 "application/json": {
3026 "schema": {
3027 "$ref": "#/components/schemas/UpsertEnvironmentAdminCmd"
3028 }
3029 }
3030 },
3031 "required": true
3032 },
3033 "responses": {
3034 "200": {
3035 "description": "environment upserted",
3036 "content": {
3037 "application/json": {
3038 "schema": {
3039 "$ref": "#/components/schemas/AppResponseData_EnvironmentAdminDto"
3040 }
3041 }
3042 }
3043 }
3044 }
3045 }
3046 },
3047 "/admin/spaces/{id}/members": {
3048 "get": {
3049 "tags": [
3050 "admin-spaces"
3051 ],
3052 "description": "Space 成员列表。",
3053 "operationId": "list_space_members",
3054 "parameters": [
3055 {
3056 "name": "id",
3057 "in": "path",
3058 "description": "space id",
3059 "required": true,
3060 "schema": {
3061 "type": "string",
3062 "format": "uuid"
3063 }
3064 },
3065 {
3066 "name": "status",
3067 "in": "query",
3068 "required": false,
3069 "schema": {
3070 "type": [
3071 "string",
3072 "null"
3073 ]
3074 }
3075 },
3076 {
3077 "name": "limit",
3078 "in": "query",
3079 "required": false,
3080 "schema": {
3081 "type": [
3082 "integer",
3083 "null"
3084 ],
3085 "format": "int64",
3086 "minimum": 0
3087 }
3088 },
3089 {
3090 "name": "offset",
3091 "in": "query",
3092 "required": false,
3093 "schema": {
3094 "type": [
3095 "integer",
3096 "null"
3097 ],
3098 "format": "int64",
3099 "minimum": 0
3100 }
3101 }
3102 ],
3103 "responses": {
3104 "200": {
3105 "description": "member page",
3106 "content": {
3107 "application/json": {
3108 "schema": {
3109 "$ref": "#/components/schemas/AppResponseData_Page_AdminSpaceMemberDto"
3110 }
3111 }
3112 }
3113 },
3114 "404": {
3115 "description": "resource not found",
3116 "content": {
3117 "application/json": {
3118 "schema": {
3119 "$ref": "#/components/schemas/AppResponseData_NoData"
3120 }
3121 }
3122 }
3123 }
3124 }
3125 },
3126 "post": {
3127 "tags": [
3128 "admin-spaces"
3129 ],
3130 "description": "管理员将用户加入 Space:已存在记录则重新激活,并同步成员计数。",
3131 "operationId": "add_space_member",
3132 "parameters": [
3133 {
3134 "name": "id",
3135 "in": "path",
3136 "description": "space id",
3137 "required": true,
3138 "schema": {
3139 "type": "string",
3140 "format": "uuid"
3141 }
3142 }
3143 ],
3144 "requestBody": {
3145 "content": {
3146 "application/json": {
3147 "schema": {
3148 "$ref": "#/components/schemas/AddSpaceMemberCmd"
3149 }
3150 }
3151 },
3152 "required": true
3153 },
3154 "responses": {
3155 "200": {
3156 "description": "member added",
3157 "content": {
3158 "application/json": {
3159 "schema": {
3160 "$ref": "#/components/schemas/AppResponseData_AddSpaceMemberResp"
3161 }
3162 }
3163 }
3164 },
3165 "400": {
3166 "description": "request failed validation",
3167 "content": {
3168 "application/json": {
3169 "schema": {
3170 "$ref": "#/components/schemas/AppResponseData_NoData"
3171 }
3172 }
3173 }
3174 },
3175 "404": {
3176 "description": "resource not found",
3177 "content": {
3178 "application/json": {
3179 "schema": {
3180 "$ref": "#/components/schemas/AppResponseData_NoData"
3181 }
3182 }
3183 }
3184 }
3185 }
3186 }
3187 },
3188 "/admin/spaces/{id}/members/{uid}": {
3189 "delete": {
3190 "tags": [
3191 "admin-spaces"
3192 ],
3193 "description": "将成员移出 Space。",
3194 "operationId": "remove_space_member",
3195 "parameters": [
3196 {
3197 "name": "id",
3198 "in": "path",
3199 "description": "space id",
3200 "required": true,
3201 "schema": {
3202 "type": "string",
3203 "format": "uuid"
3204 }
3205 },
3206 {
3207 "name": "uid",
3208 "in": "path",
3209 "description": "user id",
3210 "required": true,
3211 "schema": {
3212 "type": "string",
3213 "format": "uuid"
3214 }
3215 }
3216 ],
3217 "responses": {
3218 "200": {
3219 "description": "member removed",
3220 "content": {
3221 "application/json": {
3222 "schema": {
3223 "$ref": "#/components/schemas/AppResponseData_RemoveMemberResp"
3224 }
3225 }
3226 }
3227 },
3228 "404": {
3229 "description": "resource not found",
3230 "content": {
3231 "application/json": {
3232 "schema": {
3233 "$ref": "#/components/schemas/AppResponseData_NoData"
3234 }
3235 }
3236 }
3237 }
3238 }
3239 }
3240 },
3241 "/admin/spaces/{id}/transfer": {
3242 "post": {
3243 "tags": [
3244 "admin-spaces"
3245 ],
3246 "description": "转移 Space 所有权;仅超级管理员可操作。",
3247 "operationId": "transfer_space",
3248 "parameters": [
3249 {
3250 "name": "id",
3251 "in": "path",
3252 "description": "space id",
3253 "required": true,
3254 "schema": {
3255 "type": "string",
3256 "format": "uuid"
3257 }
3258 }
3259 ],
3260 "requestBody": {
3261 "content": {
3262 "application/json": {
3263 "schema": {
3264 "$ref": "#/components/schemas/TransferSpaceCmd"
3265 }
3266 }
3267 },
3268 "required": true
3269 },
3270 "responses": {
3271 "200": {
3272 "description": "space transferred",
3273 "content": {
3274 "application/json": {
3275 "schema": {
3276 "$ref": "#/components/schemas/AppResponseData_AdminSpaceDetail"
3277 }
3278 }
3279 }
3280 },
3281 "400": {
3282 "description": "request failed validation",
3283 "content": {
3284 "application/json": {
3285 "schema": {
3286 "$ref": "#/components/schemas/AppResponseData_NoData"
3287 }
3288 }
3289 }
3290 },
3291 "409": {
3292 "description": "resource conflict",
3293 "content": {
3294 "application/json": {
3295 "schema": {
3296 "$ref": "#/components/schemas/AppResponseData_NoData"
3297 }
3298 }
3299 }
3300 }
3301 }
3302 }
3303 },
3304 "/admin/storage/download-url": {
3305 "get": {
3306 "tags": [
3307 "admin-storage"
3308 ],
3309 "description": "生成对象预签名下载地址(最长 1 小时)。",
3310 "operationId": "download_url",
3311 "parameters": [
3312 {
3313 "name": "key",
3314 "in": "query",
3315 "required": true,
3316 "schema": {
3317 "type": "string"
3318 }
3319 }
3320 ],
3321 "responses": {
3322 "200": {
3323 "description": "presigned download url",
3324 "content": {
3325 "application/json": {
3326 "schema": {
3327 "$ref": "#/components/schemas/AppResponseData_DownloadUrlDto"
3328 }
3329 }
3330 }
3331 },
3332 "404": {
3333 "description": "resource not found",
3334 "content": {
3335 "application/json": {
3336 "schema": {
3337 "$ref": "#/components/schemas/AppResponseData_NoData"
3338 }
3339 }
3340 }
3341 }
3342 }
3343 }
3344 },
3345 "/admin/storage/objects": {
3346 "get": {
3347 "tags": [
3348 "admin-storage"
3349 ],
3350 "description": "对象浏览:前缀过滤与 token 分页。",
3351 "operationId": "list_objects",
3352 "parameters": [
3353 {
3354 "name": "prefix",
3355 "in": "query",
3356 "required": false,
3357 "schema": {
3358 "type": [
3359 "string",
3360 "null"
3361 ]
3362 }
3363 },
3364 {
3365 "name": "token",
3366 "in": "query",
3367 "required": false,
3368 "schema": {
3369 "type": [
3370 "string",
3371 "null"
3372 ]
3373 }
3374 },
3375 {
3376 "name": "limit",
3377 "in": "query",
3378 "required": false,
3379 "schema": {
3380 "type": [
3381 "integer",
3382 "null"
3383 ],
3384 "minimum": 0
3385 }
3386 }
3387 ],
3388 "responses": {
3389 "200": {
3390 "description": "object page",
3391 "content": {
3392 "application/json": {
3393 "schema": {
3394 "$ref": "#/components/schemas/AppResponseData_StorageObjectPageDto"
3395 }
3396 }
3397 }
3398 },
3399 "401": {
3400 "description": "authentication required",
3401 "content": {
3402 "application/json": {
3403 "schema": {
3404 "$ref": "#/components/schemas/AppResponseData_NoData"
3405 }
3406 }
3407 }
3408 }
3409 }
3410 },
3411 "delete": {
3412 "tags": [
3413 "admin-storage"
3414 ],
3415 "description": "删除存储对象;仅超级管理员可操作。",
3416 "operationId": "delete_object",
3417 "parameters": [
3418 {
3419 "name": "key",
3420 "in": "query",
3421 "required": true,
3422 "schema": {
3423 "type": "string"
3424 }
3425 }
3426 ],
3427 "responses": {
3428 "200": {
3429 "description": "object deleted",
3430 "content": {
3431 "application/json": {
3432 "schema": {
3433 "$ref": "#/components/schemas/AppResponseData_DeleteObjectResp"
3434 }
3435 }
3436 }
3437 },
3438 "404": {
3439 "description": "resource not found",
3440 "content": {
3441 "application/json": {
3442 "schema": {
3443 "$ref": "#/components/schemas/AppResponseData_NoData"
3444 }
3445 }
3446 }
3447 }
3448 }
3449 }
3450 },
3451 "/admin/storage/overview": {
3452 "get": {
3453 "tags": [
3454 "admin-storage"
3455 ],
3456 "description": "存储总览:单桶按前缀聚合的用量与对象数(60s 缓存,refresh=true 强制重算)。",
3457 "operationId": "overview",
3458 "parameters": [
3459 {
3460 "name": "refresh",
3461 "in": "query",
3462 "required": false,
3463 "schema": {
3464 "type": [
3465 "boolean",
3466 "null"
3467 ]
3468 }
3469 }
3470 ],
3471 "responses": {
3472 "200": {
3473 "description": "storage overview",
3474 "content": {
3475 "application/json": {
3476 "schema": {
3477 "$ref": "#/components/schemas/AppResponseData_StorageOverviewDto"
3478 }
3479 }
3480 }
3481 },
3482 "401": {
3483 "description": "authentication required",
3484 "content": {
3485 "application/json": {
3486 "schema": {
3487 "$ref": "#/components/schemas/AppResponseData_NoData"
3488 }
3489 }
3490 }
3491 }
3492 }
3493 }
3494 },
3495 "/admin/system/config": {
3496 "get": {
3497 "tags": [
3498 "admin-system"
3499 ],
3500 "description": "生效配置快照(脱敏):进程监听、功能开关、检索/导入/Git 限制、存储与数据库/Redis/会话/邮件端点,不含任何密钥。",
3501 "operationId": "config",
3502 "responses": {
3503 "200": {
3504 "description": "system config",
3505 "content": {
3506 "application/json": {
3507 "schema": {
3508 "$ref": "#/components/schemas/AppResponseData_SystemConfigDto"
3509 }
3510 }
3511 }
3512 },
3513 "401": {
3514 "description": "authentication required",
3515 "content": {
3516 "application/json": {
3517 "schema": {
3518 "$ref": "#/components/schemas/AppResponseData_NoData"
3519 }
3520 }
3521 }
3522 }
3523 }
3524 }
3525 },
3526 "/admin/system/status": {
3527 "get": {
3528 "tags": [
3529 "admin-system"
3530 ],
3531 "description": "系统状态:进程信息、DB/Redis/S3/搜索健康、核心计数、队列汇总与最近失败。",
3532 "operationId": "status",
3533 "responses": {
3534 "200": {
3535 "description": "system status",
3536 "content": {
3537 "application/json": {
3538 "schema": {
3539 "$ref": "#/components/schemas/AppResponseData_SystemStatusDto"
3540 }
3541 }
3542 }
3543 },
3544 "401": {
3545 "description": "authentication required",
3546 "content": {
3547 "application/json": {
3548 "schema": {
3549 "$ref": "#/components/schemas/AppResponseData_NoData"
3550 }
3551 }
3552 }
3553 }
3554 }
3555 }
3556 },
3557 "/admin/tasks": {
3558 "get": {
3559 "tags": [
3560 "admin-tasks"
3561 ],
3562 "description": "统一任务视图:import/search_index/chat_webhook/pipeline/deletion/system_tasks。",
3563 "operationId": "list_tasks",
3564 "parameters": [
3565 {
3566 "name": "source",
3567 "in": "query",
3568 "required": false,
3569 "schema": {
3570 "type": [
3571 "string",
3572 "null"
3573 ]
3574 }
3575 },
3576 {
3577 "name": "state",
3578 "in": "query",
3579 "required": false,
3580 "schema": {
3581 "type": [
3582 "string",
3583 "null"
3584 ]
3585 }
3586 },
3587 {
3588 "name": "q",
3589 "in": "query",
3590 "required": false,
3591 "schema": {
3592 "type": [
3593 "string",
3594 "null"
3595 ]
3596 }
3597 },
3598 {
3599 "name": "from",
3600 "in": "query",
3601 "required": false,
3602 "schema": {
3603 "type": [
3604 "string",
3605 "null"
3606 ],
3607 "format": "date-time"
3608 }
3609 },
3610 {
3611 "name": "to",
3612 "in": "query",
3613 "required": false,
3614 "schema": {
3615 "type": [
3616 "string",
3617 "null"
3618 ],
3619 "format": "date-time"
3620 }
3621 },
3622 {
3623 "name": "limit",
3624 "in": "query",
3625 "required": false,
3626 "schema": {
3627 "type": [
3628 "integer",
3629 "null"
3630 ],
3631 "format": "int64",
3632 "minimum": 0
3633 }
3634 },
3635 {
3636 "name": "offset",
3637 "in": "query",
3638 "required": false,
3639 "schema": {
3640 "type": [
3641 "integer",
3642 "null"
3643 ],
3644 "format": "int64",
3645 "minimum": 0
3646 }
3647 }
3648 ],
3649 "responses": {
3650 "200": {
3651 "description": "task page",
3652 "content": {
3653 "application/json": {
3654 "schema": {
3655 "$ref": "#/components/schemas/AppResponseData_TaskPageDto"
3656 }
3657 }
3658 }
3659 },
3660 "400": {
3661 "description": "request failed validation",
3662 "content": {
3663 "application/json": {
3664 "schema": {
3665 "$ref": "#/components/schemas/AppResponseData_NoData"
3666 }
3667 }
3668 }
3669 }
3670 }
3671 }
3672 },
3673 "/admin/tasks/stats": {
3674 "get": {
3675 "tags": [
3676 "admin-tasks"
3677 ],
3678 "description": "任务统计:各来源的排队/执行中/成功/失败计数,用于任务总览与看板。",
3679 "operationId": "task_stats",
3680 "responses": {
3681 "200": {
3682 "description": "task stats",
3683 "content": {
3684 "application/json": {
3685 "schema": {
3686 "$ref": "#/components/schemas/AppResponseData_TaskStatsDto"
3687 }
3688 }
3689 }
3690 }
3691 }
3692 }
3693 },
3694 "/admin/tasks/{source}/bulk": {
3695 "post": {
3696 "tags": [
3697 "admin-tasks"
3698 ],
3699 "description": "批量重试或取消同一来源的任务,单次最多 100 个;逐条返回结果。",
3700 "operationId": "bulk_tasks",
3701 "parameters": [
3702 {
3703 "name": "source",
3704 "in": "path",
3705 "description": "task source",
3706 "required": true,
3707 "schema": {
3708 "type": "string"
3709 }
3710 }
3711 ],
3712 "requestBody": {
3713 "content": {
3714 "application/json": {
3715 "schema": {
3716 "$ref": "#/components/schemas/BulkTaskCmd"
3717 }
3718 }
3719 },
3720 "required": true
3721 },
3722 "responses": {
3723 "200": {
3724 "description": "bulk result",
3725 "content": {
3726 "application/json": {
3727 "schema": {
3728 "$ref": "#/components/schemas/AppResponseData_BulkTaskResp"
3729 }
3730 }
3731 }
3732 },
3733 "400": {
3734 "description": "request failed validation",
3735 "content": {
3736 "application/json": {
3737 "schema": {
3738 "$ref": "#/components/schemas/AppResponseData_NoData"
3739 }
3740 }
3741 }
3742 }
3743 }
3744 }
3745 },
3746 "/admin/tasks/{source}/{id}": {
3747 "get": {
3748 "tags": [
3749 "admin-tasks"
3750 ],
3751 "description": "任务详情;import token 与 webhook payload 已脱敏。",
3752 "operationId": "get_task",
3753 "parameters": [
3754 {
3755 "name": "source",
3756 "in": "path",
3757 "description": "task source",
3758 "required": true,
3759 "schema": {
3760 "type": "string"
3761 }
3762 },
3763 {
3764 "name": "id",
3765 "in": "path",
3766 "description": "task id",
3767 "required": true,
3768 "schema": {
3769 "type": "string",
3770 "format": "uuid"
3771 }
3772 }
3773 ],
3774 "responses": {
3775 "200": {
3776 "description": "task detail",
3777 "content": {
3778 "application/json": {
3779 "schema": {
3780 "$ref": "#/components/schemas/AppResponseData_TaskDetailDto"
3781 }
3782 }
3783 }
3784 },
3785 "404": {
3786 "description": "resource not found",
3787 "content": {
3788 "application/json": {
3789 "schema": {
3790 "$ref": "#/components/schemas/AppResponseData_NoData"
3791 }
3792 }
3793 }
3794 }
3795 }
3796 },
3797 "delete": {
3798 "tags": [
3799 "admin-tasks"
3800 ],
3801 "description": "删除终态任务记录;仅超级管理员可操作。",
3802 "operationId": "delete_task",
3803 "parameters": [
3804 {
3805 "name": "source",
3806 "in": "path",
3807 "description": "task source",
3808 "required": true,
3809 "schema": {
3810 "type": "string"
3811 }
3812 },
3813 {
3814 "name": "id",
3815 "in": "path",
3816 "description": "task id",
3817 "required": true,
3818 "schema": {
3819 "type": "string",
3820 "format": "uuid"
3821 }
3822 }
3823 ],
3824 "responses": {
3825 "200": {
3826 "description": "task deleted",
3827 "content": {
3828 "application/json": {
3829 "schema": {
3830 "$ref": "#/components/schemas/AppResponseData_NoData"
3831 }
3832 }
3833 }
3834 },
3835 "409": {
3836 "description": "resource conflict",
3837 "content": {
3838 "application/json": {
3839 "schema": {
3840 "$ref": "#/components/schemas/AppResponseData_NoData"
3841 }
3842 }
3843 }
3844 }
3845 }
3846 }
3847 },
3848 "/admin/tasks/{source}/{id}/cancel": {
3849 "post": {
3850 "tags": [
3851 "admin-tasks"
3852 ],
3853 "description": "取消任务:队列中/进行中的作业按来源置取消态。",
3854 "operationId": "cancel_task",
3855 "parameters": [
3856 {
3857 "name": "source",
3858 "in": "path",
3859 "description": "task source",
3860 "required": true,
3861 "schema": {
3862 "type": "string"
3863 }
3864 },
3865 {
3866 "name": "id",
3867 "in": "path",
3868 "description": "task id",
3869 "required": true,
3870 "schema": {
3871 "type": "string",
3872 "format": "uuid"
3873 }
3874 }
3875 ],
3876 "responses": {
3877 "200": {
3878 "description": "task cancelled",
3879 "content": {
3880 "application/json": {
3881 "schema": {
3882 "$ref": "#/components/schemas/AppResponseData_TaskActionResp"
3883 }
3884 }
3885 }
3886 },
3887 "409": {
3888 "description": "resource conflict",
3889 "content": {
3890 "application/json": {
3891 "schema": {
3892 "$ref": "#/components/schemas/AppResponseData_NoData"
3893 }
3894 }
3895 }
3896 }
3897 }
3898 }
3899 },
3900 "/admin/tasks/{source}/{id}/retry": {
3901 "post": {
3902 "tags": [
3903 "admin-tasks"
3904 ],
3905 "description": "重试任务:仅失败/取消态可重试。",
3906 "operationId": "retry_task",
3907 "parameters": [
3908 {
3909 "name": "source",
3910 "in": "path",
3911 "description": "task source",
3912 "required": true,
3913 "schema": {
3914 "type": "string"
3915 }
3916 },
3917 {
3918 "name": "id",
3919 "in": "path",
3920 "description": "task id",
3921 "required": true,
3922 "schema": {
3923 "type": "string",
3924 "format": "uuid"
3925 }
3926 }
3927 ],
3928 "responses": {
3929 "200": {
3930 "description": "task retried",
3931 "content": {
3932 "application/json": {
3933 "schema": {
3934 "$ref": "#/components/schemas/AppResponseData_TaskActionResp"
3935 }
3936 }
3937 }
3938 },
3939 "409": {
3940 "description": "resource conflict",
3941 "content": {
3942 "application/json": {
3943 "schema": {
3944 "$ref": "#/components/schemas/AppResponseData_NoData"
3945 }
3946 }
3947 }
3948 }
3949 }
3950 }
3951 },
3952 "/admin/users": {
3953 "get": {
3954 "tags": [
3955 "admin-users"
3956 ],
3957 "description": "用户列表:关键字(用户名/显示名/邮箱)、状态筛选与分页。",
3958 "operationId": "list_users",
3959 "parameters": [
3960 {
3961 "name": "q",
3962 "in": "query",
3963 "required": false,
3964 "schema": {
3965 "type": [
3966 "string",
3967 "null"
3968 ]
3969 }
3970 },
3971 {
3972 "name": "status",
3973 "in": "query",
3974 "required": false,
3975 "schema": {
3976 "type": [
3977 "string",
3978 "null"
3979 ]
3980 }
3981 },
3982 {
3983 "name": "limit",
3984 "in": "query",
3985 "required": false,
3986 "schema": {
3987 "type": [
3988 "integer",
3989 "null"
3990 ],
3991 "format": "int64",
3992 "minimum": 0
3993 }
3994 },
3995 {
3996 "name": "offset",
3997 "in": "query",
3998 "required": false,
3999 "schema": {
4000 "type": [
4001 "integer",
4002 "null"
4003 ],
4004 "format": "int64",
4005 "minimum": 0
4006 }
4007 },
4008 {
4009 "name": "sort",
4010 "in": "query",
4011 "required": false,
4012 "schema": {
4013 "type": [
4014 "string",
4015 "null"
4016 ]
4017 }
4018 }
4019 ],
4020 "responses": {
4021 "200": {
4022 "description": "user page",
4023 "content": {
4024 "application/json": {
4025 "schema": {
4026 "$ref": "#/components/schemas/AppResponseData_Page_AdminUserSummary"
4027 }
4028 }
4029 }
4030 },
4031 "401": {
4032 "description": "authentication required",
4033 "content": {
4034 "application/json": {
4035 "schema": {
4036 "$ref": "#/components/schemas/AppResponseData_NoData"
4037 }
4038 }
4039 }
4040 }
4041 }
4042 },
4043 "post": {
4044 "tags": [
4045 "admin-users"
4046 ],
4047 "description": "创建用户(管理员代办):直接写入账号、资料与已激活邮箱;仅超级管理员可操作。",
4048 "operationId": "create_user",
4049 "requestBody": {
4050 "content": {
4051 "application/json": {
4052 "schema": {
4053 "$ref": "#/components/schemas/CreateUserCmd"
4054 }
4055 }
4056 },
4057 "required": true
4058 },
4059 "responses": {
4060 "200": {
4061 "description": "user created",
4062 "content": {
4063 "application/json": {
4064 "schema": {
4065 "$ref": "#/components/schemas/AppResponseData_CreateUserResp"
4066 }
4067 }
4068 }
4069 },
4070 "400": {
4071 "description": "request failed validation",
4072 "content": {
4073 "application/json": {
4074 "schema": {
4075 "$ref": "#/components/schemas/AppResponseData_NoData"
4076 }
4077 }
4078 }
4079 },
4080 "403": {
4081 "description": "operation not allowed",
4082 "content": {
4083 "application/json": {
4084 "schema": {
4085 "$ref": "#/components/schemas/AppResponseData_NoData"
4086 }
4087 }
4088 }
4089 },
4090 "409": {
4091 "description": "resource conflict",
4092 "content": {
4093 "application/json": {
4094 "schema": {
4095 "$ref": "#/components/schemas/AppResponseData_NoData"
4096 }
4097 }
4098 }
4099 }
4100 }
4101 }
4102 },
4103 "/admin/users/bulk": {
4104 "post": {
4105 "tags": [
4106 "admin-users"
4107 ],
4108 "description": "批量用户操作:activate/deactivate/restrict/unrestrict/revoke_sessions/suspend/delete,单次最多 100 个目标。",
4109 "operationId": "bulk_users",
4110 "requestBody": {
4111 "content": {
4112 "application/json": {
4113 "schema": {
4114 "$ref": "#/components/schemas/BulkUsersCmd"
4115 }
4116 }
4117 },
4118 "required": true
4119 },
4120 "responses": {
4121 "200": {
4122 "description": "bulk result",
4123 "content": {
4124 "application/json": {
4125 "schema": {
4126 "$ref": "#/components/schemas/AppResponseData_BulkUsersResp"
4127 }
4128 }
4129 }
4130 },
4131 "400": {
4132 "description": "request failed validation",
4133 "content": {
4134 "application/json": {
4135 "schema": {
4136 "$ref": "#/components/schemas/AppResponseData_NoData"
4137 }
4138 }
4139 }
4140 },
4141 "403": {
4142 "description": "operation not allowed",
4143 "content": {
4144 "application/json": {
4145 "schema": {
4146 "$ref": "#/components/schemas/AppResponseData_NoData"
4147 }
4148 }
4149 }
4150 }
4151 }
4152 }
4153 },
4154 "/admin/users/export": {
4155 "get": {
4156 "tags": [
4157 "admin-users"
4158 ],
4159 "description": "按当前筛选条件导出用户 CSV(最多 5000 行)。",
4160 "operationId": "export_users",
4161 "parameters": [
4162 {
4163 "name": "q",
4164 "in": "query",
4165 "required": false,
4166 "schema": {
4167 "type": [
4168 "string",
4169 "null"
4170 ]
4171 }
4172 },
4173 {
4174 "name": "status",
4175 "in": "query",
4176 "required": false,
4177 "schema": {
4178 "type": [
4179 "string",
4180 "null"
4181 ]
4182 }
4183 },
4184 {
4185 "name": "limit",
4186 "in": "query",
4187 "required": false,
4188 "schema": {
4189 "type": [
4190 "integer",
4191 "null"
4192 ],
4193 "format": "int64",
4194 "minimum": 0
4195 }
4196 },
4197 {
4198 "name": "offset",
4199 "in": "query",
4200 "required": false,
4201 "schema": {
4202 "type": [
4203 "integer",
4204 "null"
4205 ],
4206 "format": "int64",
4207 "minimum": 0
4208 }
4209 },
4210 {
4211 "name": "sort",
4212 "in": "query",
4213 "required": false,
4214 "schema": {
4215 "type": [
4216 "string",
4217 "null"
4218 ]
4219 }
4220 }
4221 ],
4222 "responses": {
4223 "200": {
4224 "description": "csv export"
4225 }
4226 }
4227 }
4228 },
4229 "/admin/users/{uid}": {
4230 "get": {
4231 "tags": [
4232 "admin-users"
4233 ],
4234 "description": "用户详情:账号状态、邮箱、会话、所属 Space 与封禁记录。",
4235 "operationId": "get_user",
4236 "parameters": [
4237 {
4238 "name": "uid",
4239 "in": "path",
4240 "description": "user id",
4241 "required": true,
4242 "schema": {
4243 "type": "string",
4244 "format": "uuid"
4245 }
4246 }
4247 ],
4248 "responses": {
4249 "200": {
4250 "description": "user detail",
4251 "content": {
4252 "application/json": {
4253 "schema": {
4254 "$ref": "#/components/schemas/AppResponseData_AdminUserDetail"
4255 }
4256 }
4257 }
4258 },
4259 "404": {
4260 "description": "resource not found",
4261 "content": {
4262 "application/json": {
4263 "schema": {
4264 "$ref": "#/components/schemas/AppResponseData_NoData"
4265 }
4266 }
4267 }
4268 }
4269 }
4270 },
4271 "delete": {
4272 "tags": [
4273 "admin-users"
4274 ],
4275 "description": "强制注销用户:立即执行匿名化并禁用账号;仅超级管理员可操作。",
4276 "operationId": "delete_user",
4277 "parameters": [
4278 {
4279 "name": "uid",
4280 "in": "path",
4281 "description": "user id",
4282 "required": true,
4283 "schema": {
4284 "type": "string",
4285 "format": "uuid"
4286 }
4287 }
4288 ],
4289 "responses": {
4290 "200": {
4291 "description": "user deleted",
4292 "content": {
4293 "application/json": {
4294 "schema": {
4295 "$ref": "#/components/schemas/AppResponseData_DeleteUserResp"
4296 }
4297 }
4298 }
4299 },
4300 "403": {
4301 "description": "operation not allowed",
4302 "content": {
4303 "application/json": {
4304 "schema": {
4305 "$ref": "#/components/schemas/AppResponseData_NoData"
4306 }
4307 }
4308 }
4309 }
4310 }
4311 },
4312 "patch": {
4313 "tags": [
4314 "admin-users"
4315 ],
4316 "description": "更新用户账号标志:显示名、启用、受限、禁止登录。",
4317 "operationId": "update_user",
4318 "parameters": [
4319 {
4320 "name": "uid",
4321 "in": "path",
4322 "description": "user id",
4323 "required": true,
4324 "schema": {
4325 "type": "string",
4326 "format": "uuid"
4327 }
4328 }
4329 ],
4330 "requestBody": {
4331 "content": {
4332 "application/json": {
4333 "schema": {
4334 "$ref": "#/components/schemas/UpdateUserCmd"
4335 }
4336 }
4337 },
4338 "required": true
4339 },
4340 "responses": {
4341 "200": {
4342 "description": "user updated",
4343 "content": {
4344 "application/json": {
4345 "schema": {
4346 "$ref": "#/components/schemas/AppResponseData_AdminUserSummary"
4347 }
4348 }
4349 }
4350 },
4351 "404": {
4352 "description": "resource not found",
4353 "content": {
4354 "application/json": {
4355 "schema": {
4356 "$ref": "#/components/schemas/AppResponseData_NoData"
4357 }
4358 }
4359 }
4360 }
4361 }
4362 }
4363 },
4364 "/admin/users/{uid}/reset-password": {
4365 "post": {
4366 "tags": [
4367 "admin-users"
4368 ],
4369 "description": "重置用户口令并吊销全部会话。",
4370 "operationId": "reset_user_password",
4371 "parameters": [
4372 {
4373 "name": "uid",
4374 "in": "path",
4375 "description": "user id",
4376 "required": true,
4377 "schema": {
4378 "type": "string",
4379 "format": "uuid"
4380 }
4381 }
4382 ],
4383 "requestBody": {
4384 "content": {
4385 "application/json": {
4386 "schema": {
4387 "$ref": "#/components/schemas/ResetUserPasswordCmd"
4388 }
4389 }
4390 },
4391 "required": true
4392 },
4393 "responses": {
4394 "200": {
4395 "description": "password reset",
4396 "content": {
4397 "application/json": {
4398 "schema": {
4399 "$ref": "#/components/schemas/AppResponseData_NoData"
4400 }
4401 }
4402 }
4403 },
4404 "400": {
4405 "description": "request failed validation",
4406 "content": {
4407 "application/json": {
4408 "schema": {
4409 "$ref": "#/components/schemas/AppResponseData_NoData"
4410 }
4411 }
4412 }
4413 },
4414 "404": {
4415 "description": "resource not found",
4416 "content": {
4417 "application/json": {
4418 "schema": {
4419 "$ref": "#/components/schemas/AppResponseData_NoData"
4420 }
4421 }
4422 }
4423 }
4424 }
4425 }
4426 },
4427 "/admin/users/{uid}/revoke-sessions": {
4428 "post": {
4429 "tags": [
4430 "admin-users"
4431 ],
4432 "description": "吊销用户全部有效会话。",
4433 "operationId": "revoke_user_sessions",
4434 "parameters": [
4435 {
4436 "name": "uid",
4437 "in": "path",
4438 "description": "user id",
4439 "required": true,
4440 "schema": {
4441 "type": "string",
4442 "format": "uuid"
4443 }
4444 }
4445 ],
4446 "responses": {
4447 "200": {
4448 "description": "sessions revoked",
4449 "content": {
4450 "application/json": {
4451 "schema": {
4452 "$ref": "#/components/schemas/AppResponseData_RevokeSessionsResp"
4453 }
4454 }
4455 }
4456 },
4457 "404": {
4458 "description": "resource not found",
4459 "content": {
4460 "application/json": {
4461 "schema": {
4462 "$ref": "#/components/schemas/AppResponseData_NoData"
4463 }
4464 }
4465 }
4466 }
4467 }
4468 }
4469 },
4470 "/admin/users/{uid}/security": {
4471 "get": {
4472 "tags": [
4473 "admin-users"
4474 ],
4475 "description": "用户安全概览:二次验证状态、恢复码、活跃会话、SSH Key 与访问密钥数量。",
4476 "operationId": "get_user_security",
4477 "parameters": [
4478 {
4479 "name": "uid",
4480 "in": "path",
4481 "description": "user id",
4482 "required": true,
4483 "schema": {
4484 "type": "string",
4485 "format": "uuid"
4486 }
4487 }
4488 ],
4489 "responses": {
4490 "200": {
4491 "description": "security overview",
4492 "content": {
4493 "application/json": {
4494 "schema": {
4495 "$ref": "#/components/schemas/AppResponseData_AdminUserSecurityDto"
4496 }
4497 }
4498 }
4499 },
4500 "404": {
4501 "description": "resource not found",
4502 "content": {
4503 "application/json": {
4504 "schema": {
4505 "$ref": "#/components/schemas/AppResponseData_NoData"
4506 }
4507 }
4508 }
4509 }
4510 }
4511 }
4512 },
4513 "/admin/users/{uid}/sessions": {
4514 "get": {
4515 "tags": [
4516 "admin-users"
4517 ],
4518 "description": "用户会话列表:可按 kind 过滤、包含已吊销会话,用于会话级踢下线。",
4519 "operationId": "list_user_sessions",
4520 "parameters": [
4521 {
4522 "name": "uid",
4523 "in": "path",
4524 "description": "user id",
4525 "required": true,
4526 "schema": {
4527 "type": "string",
4528 "format": "uuid"
4529 }
4530 },
4531 {
4532 "name": "kind",
4533 "in": "query",
4534 "required": false,
4535 "schema": {
4536 "type": [
4537 "string",
4538 "null"
4539 ]
4540 }
4541 },
4542 {
4543 "name": "include_revoked",
4544 "in": "query",
4545 "required": false,
4546 "schema": {
4547 "type": [
4548 "boolean",
4549 "null"
4550 ]
4551 }
4552 },
4553 {
4554 "name": "limit",
4555 "in": "query",
4556 "required": false,
4557 "schema": {
4558 "type": [
4559 "integer",
4560 "null"
4561 ],
4562 "format": "int64",
4563 "minimum": 0
4564 }
4565 },
4566 {
4567 "name": "offset",
4568 "in": "query",
4569 "required": false,
4570 "schema": {
4571 "type": [
4572 "integer",
4573 "null"
4574 ],
4575 "format": "int64",
4576 "minimum": 0
4577 }
4578 }
4579 ],
4580 "responses": {
4581 "200": {
4582 "description": "session page",
4583 "content": {
4584 "application/json": {
4585 "schema": {
4586 "$ref": "#/components/schemas/AppResponseData_AdminUserSessionPage"
4587 }
4588 }
4589 }
4590 },
4591 "404": {
4592 "description": "resource not found",
4593 "content": {
4594 "application/json": {
4595 "schema": {
4596 "$ref": "#/components/schemas/AppResponseData_NoData"
4597 }
4598 }
4599 }
4600 }
4601 }
4602 }
4603 },
4604 "/admin/users/{uid}/sessions/{session_id}": {
4605 "delete": {
4606 "tags": [
4607 "admin-users"
4608 ],
4609 "description": "吊销用户单个会话;运维及以上角色可操作。",
4610 "operationId": "revoke_user_session",
4611 "parameters": [
4612 {
4613 "name": "uid",
4614 "in": "path",
4615 "description": "user id",
4616 "required": true,
4617 "schema": {
4618 "type": "string",
4619 "format": "uuid"
4620 }
4621 },
4622 {
4623 "name": "session_id",
4624 "in": "path",
4625 "description": "session id",
4626 "required": true,
4627 "schema": {
4628 "type": "string",
4629 "format": "uuid"
4630 }
4631 }
4632 ],
4633 "responses": {
4634 "200": {
4635 "description": "session revoked",
4636 "content": {
4637 "application/json": {
4638 "schema": {
4639 "$ref": "#/components/schemas/AppResponseData_NoData"
4640 }
4641 }
4642 }
4643 },
4644 "404": {
4645 "description": "resource not found",
4646 "content": {
4647 "application/json": {
4648 "schema": {
4649 "$ref": "#/components/schemas/AppResponseData_NoData"
4650 }
4651 }
4652 }
4653 }
4654 }
4655 }
4656 },
4657 "/admin/users/{uid}/suspend": {
4658 "post": {
4659 "tags": [
4660 "admin-users"
4661 ],
4662 "description": "封禁用户:写入封禁记录、标记受限并吊销全部会话。",
4663 "operationId": "suspend_user",
4664 "parameters": [
4665 {
4666 "name": "uid",
4667 "in": "path",
4668 "description": "user id",
4669 "required": true,
4670 "schema": {
4671 "type": "string",
4672 "format": "uuid"
4673 }
4674 }
4675 ],
4676 "requestBody": {
4677 "content": {
4678 "application/json": {
4679 "schema": {
4680 "$ref": "#/components/schemas/SuspendUserCmd"
4681 }
4682 }
4683 },
4684 "required": true
4685 },
4686 "responses": {
4687 "200": {
4688 "description": "user suspended",
4689 "content": {
4690 "application/json": {
4691 "schema": {
4692 "$ref": "#/components/schemas/AppResponseData_AdminSuspensionDto"
4693 }
4694 }
4695 }
4696 },
4697 "400": {
4698 "description": "request failed validation",
4699 "content": {
4700 "application/json": {
4701 "schema": {
4702 "$ref": "#/components/schemas/AppResponseData_NoData"
4703 }
4704 }
4705 }
4706 },
4707 "404": {
4708 "description": "resource not found",
4709 "content": {
4710 "application/json": {
4711 "schema": {
4712 "$ref": "#/components/schemas/AppResponseData_NoData"
4713 }
4714 }
4715 }
4716 }
4717 }
4718 }
4719 },
4720 "/admin/users/{uid}/suspensions/{suspension_id}/revoke": {
4721 "post": {
4722 "tags": [
4723 "admin-users"
4724 ],
4725 "description": "撤销用户封禁记录;无剩余生效封禁时解除受限标志。",
4726 "operationId": "revoke_suspension",
4727 "parameters": [
4728 {
4729 "name": "uid",
4730 "in": "path",
4731 "description": "user id",
4732 "required": true,
4733 "schema": {
4734 "type": "string",
4735 "format": "uuid"
4736 }
4737 },
4738 {
4739 "name": "suspension_id",
4740 "in": "path",
4741 "description": "suspension id",
4742 "required": true,
4743 "schema": {
4744 "type": "string",
4745 "format": "uuid"
4746 }
4747 }
4748 ],
4749 "responses": {
4750 "200": {
4751 "description": "suspension revoked",
4752 "content": {
4753 "application/json": {
4754 "schema": {
4755 "$ref": "#/components/schemas/AppResponseData_NoData"
4756 }
4757 }
4758 }
4759 },
4760 "404": {
4761 "description": "resource not found",
4762 "content": {
4763 "application/json": {
4764 "schema": {
4765 "$ref": "#/components/schemas/AppResponseData_NoData"
4766 }
4767 }
4768 }
4769 }
4770 }
4771 }
4772 },
4773 "/admin/users/{uid}/two-factor": {
4774 "delete": {
4775 "tags": [
4776 "admin-users"
4777 ],
4778 "description": "强制清除用户二次验证(用户遗失设备时使用);仅超级管理员可操作。",
4779 "operationId": "clear_user_two_factor",
4780 "parameters": [
4781 {
4782 "name": "uid",
4783 "in": "path",
4784 "description": "user id",
4785 "required": true,
4786 "schema": {
4787 "type": "string",
4788 "format": "uuid"
4789 }
4790 }
4791 ],
4792 "responses": {
4793 "200": {
4794 "description": "two-factor cleared",
4795 "content": {
4796 "application/json": {
4797 "schema": {
4798 "$ref": "#/components/schemas/AppResponseData_NoData"
4799 }
4800 }
4801 }
4802 },
4803 "403": {
4804 "description": "operation not allowed",
4805 "content": {
4806 "application/json": {
4807 "schema": {
4808 "$ref": "#/components/schemas/AppResponseData_NoData"
4809 }
4810 }
4811 }
4812 }
4813 }
4814 }
4815 }
4816 },
4817 "components": {
4818 "schemas": {
4819 "AddSpaceMemberCmd": {
4820 "type": "object",
4821 "required": [
4822 "user_id"
4823 ],
4824 "properties": {
4825 "alias_name": {
4826 "type": [
4827 "string",
4828 "null"
4829 ]
4830 },
4831 "user_id": {
4832 "type": "string",
4833 "format": "uuid"
4834 }
4835 }
4836 },
4837 "AddSpaceMemberResp": {
4838 "type": "object",
4839 "required": [
4840 "space_id",
4841 "user_id",
4842 "status",
4843 "added"
4844 ],
4845 "properties": {
4846 "added": {
4847 "type": "boolean"
4848 },
4849 "space_id": {
4850 "type": "string",
4851 "format": "uuid"
4852 },
4853 "status": {
4854 "type": "string"
4855 },
4856 "user_id": {
4857 "type": "string",
4858 "format": "uuid"
4859 }
4860 }
4861 },
4862 "AdminActRunDto": {
4863 "type": "object",
4864 "required": [
4865 "id",
4866 "repo_id",
4867 "repo_name",
4868 "repo_full_name",
4869 "workflow_file",
4870 "workflow_name",
4871 "event",
4872 "state",
4873 "commit_sha",
4874 "jobs_total",
4875 "jobs_active",
4876 "jobs_failed",
4877 "created_at"
4878 ],
4879 "properties": {
4880 "actions_id": {
4881 "type": [
4882 "integer",
4883 "null"
4884 ],
4885 "format": "int64"
4886 },
4887 "branch": {
4888 "type": [
4889 "string",
4890 "null"
4891 ]
4892 },
4893 "commit_sha": {
4894 "type": "string"
4895 },
4896 "created_at": {
4897 "type": "string",
4898 "format": "date-time"
4899 },
4900 "display_name": {
4901 "type": [
4902 "string",
4903 "null"
4904 ]
4905 },
4906 "event": {
4907 "type": "string"
4908 },
4909 "finished_at": {
4910 "type": [
4911 "string",
4912 "null"
4913 ],
4914 "format": "date-time"
4915 },
4916 "id": {
4917 "type": "string",
4918 "format": "uuid"
4919 },
4920 "jobs_active": {
4921 "type": "integer",
4922 "format": "int64",
4923 "minimum": 0
4924 },
4925 "jobs_failed": {
4926 "type": "integer",
4927 "format": "int64",
4928 "minimum": 0
4929 },
4930 "jobs_total": {
4931 "type": "integer",
4932 "format": "int64",
4933 "minimum": 0
4934 },
4935 "pr_number": {
4936 "type": [
4937 "integer",
4938 "null"
4939 ],
4940 "format": "int64"
4941 },
4942 "repo_full_name": {
4943 "type": "string"
4944 },
4945 "repo_id": {
4946 "type": "string",
4947 "format": "uuid"
4948 },
4949 "repo_name": {
4950 "type": "string"
4951 },
4952 "run_number": {
4953 "type": [
4954 "integer",
4955 "null"
4956 ],
4957 "format": "int64"
4958 },
4959 "space_id": {
4960 "type": [
4961 "string",
4962 "null"
4963 ],
4964 "format": "uuid"
4965 },
4966 "space_name": {
4967 "type": [
4968 "string",
4969 "null"
4970 ]
4971 },
4972 "started_at": {
4973 "type": [
4974 "string",
4975 "null"
4976 ],
4977 "format": "date-time"
4978 },
4979 "state": {
4980 "type": "string"
4981 },
4982 "tag": {
4983 "type": [
4984 "string",
4985 "null"
4986 ]
4987 },
4988 "workflow_file": {
4989 "type": "string"
4990 },
4991 "workflow_name": {
4992 "type": "string"
4993 }
4994 }
4995 },
4996 "AdminActRunPage": {
4997 "type": "object",
4998 "required": [
4999 "items",
5000 "total",
5001 "limit",
5002 "offset"
5003 ],
5004 "properties": {
5005 "items": {
5006 "type": "array",
5007 "items": {
5008 "$ref": "#/components/schemas/AdminActRunDto"
5009 }
5010 },
5011 "limit": {
5012 "type": "integer",
5013 "format": "int64",
5014 "minimum": 0
5015 },
5016 "offset": {
5017 "type": "integer",
5018 "format": "int64",
5019 "minimum": 0
5020 },
5021 "total": {
5022 "type": "integer",
5023 "format": "int64",
5024 "minimum": 0
5025 }
5026 }
5027 },
5028 "AdminActSummary": {
5029 "type": "object",
5030 "required": [
5031 "runners_total",
5032 "runners_active",
5033 "runners_busy",
5034 "jobs_queued",
5035 "jobs_running",
5036 "runs_queued",
5037 "runs_running",
5038 "runs_success",
5039 "runs_failure",
5040 "generated_at"
5041 ],
5042 "properties": {
5043 "generated_at": {
5044 "type": "string",
5045 "format": "date-time"
5046 },
5047 "jobs_queued": {
5048 "type": "integer",
5049 "format": "int64",
5050 "minimum": 0
5051 },
5052 "jobs_running": {
5053 "type": "integer",
5054 "format": "int64",
5055 "minimum": 0
5056 },
5057 "runners_active": {
5058 "type": "integer",
5059 "format": "int64",
5060 "minimum": 0
5061 },
5062 "runners_busy": {
5063 "type": "integer",
5064 "format": "int64",
5065 "minimum": 0
5066 },
5067 "runners_total": {
5068 "type": "integer",
5069 "format": "int64",
5070 "minimum": 0
5071 },
5072 "runs_failure": {
5073 "type": "integer",
5074 "format": "int64",
5075 "minimum": 0
5076 },
5077 "runs_queued": {
5078 "type": "integer",
5079 "format": "int64",
5080 "minimum": 0
5081 },
5082 "runs_running": {
5083 "type": "integer",
5084 "format": "int64",
5085 "minimum": 0
5086 },
5087 "runs_success": {
5088 "type": "integer",
5089 "format": "int64",
5090 "minimum": 0
5091 }
5092 }
5093 },
5094 "AdminChatAttachmentDto": {
5095 "type": "object",
5096 "required": [
5097 "id",
5098 "user_id",
5099 "filename",
5100 "content_type",
5101 "size",
5102 "bucket",
5103 "s3_key",
5104 "status",
5105 "created_at"
5106 ],
5107 "properties": {
5108 "bucket": {
5109 "type": "string"
5110 },
5111 "content_type": {
5112 "type": "string"
5113 },
5114 "created_at": {
5115 "type": "string",
5116 "format": "date-time"
5117 },
5118 "description": {
5119 "type": [
5120 "string",
5121 "null"
5122 ]
5123 },
5124 "display_name": {
5125 "type": [
5126 "string",
5127 "null"
5128 ]
5129 },
5130 "filename": {
5131 "type": "string"
5132 },
5133 "id": {
5134 "type": "string",
5135 "format": "uuid"
5136 },
5137 "message_id": {
5138 "type": [
5139 "string",
5140 "null"
5141 ],
5142 "format": "uuid"
5143 },
5144 "room_id": {
5145 "type": [
5146 "string",
5147 "null"
5148 ],
5149 "format": "uuid"
5150 },
5151 "s3_key": {
5152 "type": "string"
5153 },
5154 "size": {
5155 "type": "integer",
5156 "format": "int64"
5157 },
5158 "status": {
5159 "type": "string"
5160 },
5161 "user_id": {
5162 "type": "string",
5163 "format": "uuid"
5164 },
5165 "username": {
5166 "type": [
5167 "string",
5168 "null"
5169 ]
5170 }
5171 }
5172 },
5173 "AdminChatKindCount": {
5174 "type": "object",
5175 "required": [
5176 "kind",
5177 "count"
5178 ],
5179 "properties": {
5180 "count": {
5181 "type": "integer",
5182 "format": "int64"
5183 },
5184 "kind": {
5185 "type": "string"
5186 }
5187 }
5188 },
5189 "AdminChatMessageDetail": {
5190 "type": "object",
5191 "required": [
5192 "message",
5193 "edits",
5194 "reactions",
5195 "attachments"
5196 ],
5197 "properties": {
5198 "attachments": {
5199 "type": "array",
5200 "items": {
5201 "$ref": "#/components/schemas/AdminChatAttachmentDto"
5202 }
5203 },
5204 "edits": {
5205 "type": "array",
5206 "items": {
5207 "$ref": "#/components/schemas/AdminChatMessageEditDto"
5208 }
5209 },
5210 "message": {
5211 "$ref": "#/components/schemas/AdminChatMessageDto"
5212 },
5213 "reactions": {
5214 "type": "array",
5215 "items": {
5216 "$ref": "#/components/schemas/AdminChatReactionGroupDto"
5217 }
5218 },
5219 "thread": {
5220 "oneOf": [
5221 {
5222 "type": "null"
5223 },
5224 {
5225 "$ref": "#/components/schemas/AdminChatThreadDto"
5226 }
5227 ]
5228 }
5229 }
5230 },
5231 "AdminChatMessageDto": {
5232 "type": "object",
5233 "required": [
5234 "id",
5235 "room_id",
5236 "author_id",
5237 "author_type",
5238 "message_type",
5239 "content",
5240 "seq",
5241 "flags",
5242 "pinned",
5243 "attachment_count",
5244 "reaction_count",
5245 "created_at"
5246 ],
5247 "properties": {
5248 "attachment_count": {
5249 "type": "integer",
5250 "format": "int64"
5251 },
5252 "author_display_name": {
5253 "type": [
5254 "string",
5255 "null"
5256 ]
5257 },
5258 "author_id": {
5259 "type": "string",
5260 "format": "uuid"
5261 },
5262 "author_type": {
5263 "type": "string"
5264 },
5265 "author_username": {
5266 "type": [
5267 "string",
5268 "null"
5269 ]
5270 },
5271 "content": {
5272 "type": "string"
5273 },
5274 "created_at": {
5275 "type": "string",
5276 "format": "date-time"
5277 },
5278 "deleted_at": {
5279 "type": [
5280 "string",
5281 "null"
5282 ],
5283 "format": "date-time"
5284 },
5285 "edited_at": {
5286 "type": [
5287 "string",
5288 "null"
5289 ],
5290 "format": "date-time"
5291 },
5292 "flags": {
5293 "type": "integer",
5294 "format": "int64"
5295 },
5296 "id": {
5297 "type": "string",
5298 "format": "uuid"
5299 },
5300 "message_type": {
5301 "type": "string"
5302 },
5303 "pinned": {
5304 "type": "boolean"
5305 },
5306 "reaction_count": {
5307 "type": "integer",
5308 "format": "int64"
5309 },
5310 "reply_to_id": {
5311 "type": [
5312 "string",
5313 "null"
5314 ],
5315 "format": "uuid"
5316 },
5317 "room_id": {
5318 "type": "string",
5319 "format": "uuid"
5320 },
5321 "room_name": {
5322 "type": [
5323 "string",
5324 "null"
5325 ]
5326 },
5327 "seq": {
5328 "type": "integer",
5329 "format": "int64"
5330 },
5331 "thread_id": {
5332 "type": [
5333 "string",
5334 "null"
5335 ],
5336 "format": "uuid"
5337 }
5338 }
5339 },
5340 "AdminChatMessageEditDto": {
5341 "type": "object",
5342 "required": [
5343 "id",
5344 "editor_id",
5345 "content",
5346 "created_at"
5347 ],
5348 "properties": {
5349 "content": {
5350 "type": "string"
5351 },
5352 "created_at": {
5353 "type": "string",
5354 "format": "date-time"
5355 },
5356 "editor_id": {
5357 "type": "string",
5358 "format": "uuid"
5359 },
5360 "editor_username": {
5361 "type": [
5362 "string",
5363 "null"
5364 ]
5365 },
5366 "id": {
5367 "type": "string",
5368 "format": "uuid"
5369 }
5370 }
5371 },
5372 "AdminChatReactionGroupDto": {
5373 "type": "object",
5374 "required": [
5375 "emoji",
5376 "count"
5377 ],
5378 "properties": {
5379 "count": {
5380 "type": "integer",
5381 "format": "int64"
5382 },
5383 "emoji": {
5384 "type": "string"
5385 }
5386 }
5387 },
5388 "AdminChatRoomDetail": {
5389 "type": "object",
5390 "required": [
5391 "room",
5392 "deleted_messages",
5393 "threads",
5394 "attachments",
5395 "attachment_bytes",
5396 "bans",
5397 "invites"
5398 ],
5399 "properties": {
5400 "attachment_bytes": {
5401 "type": "integer",
5402 "format": "int64"
5403 },
5404 "attachments": {
5405 "type": "integer",
5406 "format": "int64"
5407 },
5408 "bans": {
5409 "type": "integer",
5410 "format": "int64"
5411 },
5412 "deleted_messages": {
5413 "type": "integer",
5414 "format": "int64"
5415 },
5416 "invites": {
5417 "type": "integer",
5418 "format": "int64"
5419 },
5420 "room": {
5421 "$ref": "#/components/schemas/AdminChatRoomSummary"
5422 },
5423 "threads": {
5424 "type": "integer",
5425 "format": "int64"
5426 }
5427 }
5428 },
5429 "AdminChatRoomMemberDto": {
5430 "type": "object",
5431 "required": [
5432 "user_id",
5433 "role",
5434 "joined_at"
5435 ],
5436 "properties": {
5437 "display_name": {
5438 "type": [
5439 "string",
5440 "null"
5441 ]
5442 },
5443 "joined_at": {
5444 "type": "string",
5445 "format": "date-time"
5446 },
5447 "left_at": {
5448 "type": [
5449 "string",
5450 "null"
5451 ],
5452 "format": "date-time"
5453 },
5454 "nick": {
5455 "type": [
5456 "string",
5457 "null"
5458 ]
5459 },
5460 "role": {
5461 "type": "string"
5462 },
5463 "user_id": {
5464 "type": "string",
5465 "format": "uuid"
5466 },
5467 "username": {
5468 "type": [
5469 "string",
5470 "null"
5471 ]
5472 }
5473 }
5474 },
5475 "AdminChatRoomSummary": {
5476 "type": "object",
5477 "required": [
5478 "id",
5479 "kind",
5480 "name",
5481 "lower_name",
5482 "owner_id",
5483 "is_private",
5484 "nsfw",
5485 "is_active",
5486 "members",
5487 "messages",
5488 "created_at",
5489 "updated_at"
5490 ],
5491 "properties": {
5492 "created_at": {
5493 "type": "string",
5494 "format": "date-time"
5495 },
5496 "id": {
5497 "type": "string",
5498 "format": "uuid"
5499 },
5500 "is_active": {
5501 "type": "boolean"
5502 },
5503 "is_private": {
5504 "type": "boolean"
5505 },
5506 "kind": {
5507 "type": "string"
5508 },
5509 "last_message_at": {
5510 "type": [
5511 "string",
5512 "null"
5513 ],
5514 "format": "date-time"
5515 },
5516 "lower_name": {
5517 "type": "string"
5518 },
5519 "members": {
5520 "type": "integer",
5521 "format": "int64"
5522 },
5523 "messages": {
5524 "type": "integer",
5525 "format": "int64"
5526 },
5527 "name": {
5528 "type": "string"
5529 },
5530 "nsfw": {
5531 "type": "boolean"
5532 },
5533 "owner_id": {
5534 "type": "string",
5535 "format": "uuid"
5536 },
5537 "owner_username": {
5538 "type": [
5539 "string",
5540 "null"
5541 ]
5542 },
5543 "space_id": {
5544 "type": [
5545 "string",
5546 "null"
5547 ],
5548 "format": "uuid"
5549 },
5550 "space_name": {
5551 "type": [
5552 "string",
5553 "null"
5554 ]
5555 },
5556 "topic": {
5557 "type": [
5558 "string",
5559 "null"
5560 ]
5561 },
5562 "updated_at": {
5563 "type": "string",
5564 "format": "date-time"
5565 }
5566 }
5567 },
5568 "AdminChatStatsDto": {
5569 "type": "object",
5570 "required": [
5571 "rooms",
5572 "active_rooms",
5573 "private_rooms",
5574 "kinds",
5575 "messages",
5576 "deleted_messages",
5577 "messages_24h",
5578 "threads",
5579 "attachments",
5580 "attachment_bytes",
5581 "pending_attachments",
5582 "members",
5583 "bans"
5584 ],
5585 "properties": {
5586 "active_rooms": {
5587 "type": "integer",
5588 "format": "int64"
5589 },
5590 "attachment_bytes": {
5591 "type": "integer",
5592 "format": "int64"
5593 },
5594 "attachments": {
5595 "type": "integer",
5596 "format": "int64"
5597 },
5598 "bans": {
5599 "type": "integer",
5600 "format": "int64"
5601 },
5602 "deleted_messages": {
5603 "type": "integer",
5604 "format": "int64"
5605 },
5606 "kinds": {
5607 "type": "array",
5608 "items": {
5609 "$ref": "#/components/schemas/AdminChatKindCount"
5610 }
5611 },
5612 "members": {
5613 "type": "integer",
5614 "format": "int64"
5615 },
5616 "messages": {
5617 "type": "integer",
5618 "format": "int64"
5619 },
5620 "messages_24h": {
5621 "type": "integer",
5622 "format": "int64"
5623 },
5624 "pending_attachments": {
5625 "type": "integer",
5626 "format": "int64"
5627 },
5628 "private_rooms": {
5629 "type": "integer",
5630 "format": "int64"
5631 },
5632 "rooms": {
5633 "type": "integer",
5634 "format": "int64"
5635 },
5636 "threads": {
5637 "type": "integer",
5638 "format": "int64"
5639 }
5640 }
5641 },
5642 "AdminChatThreadDto": {
5643 "type": "object",
5644 "required": [
5645 "id",
5646 "owner_id",
5647 "reply_count",
5648 "participant_count",
5649 "archived",
5650 "locked",
5651 "created_at"
5652 ],
5653 "properties": {
5654 "archived": {
5655 "type": "boolean"
5656 },
5657 "created_at": {
5658 "type": "string",
5659 "format": "date-time"
5660 },
5661 "id": {
5662 "type": "string",
5663 "format": "uuid"
5664 },
5665 "locked": {
5666 "type": "boolean"
5667 },
5668 "name": {
5669 "type": [
5670 "string",
5671 "null"
5672 ]
5673 },
5674 "owner_id": {
5675 "type": "string",
5676 "format": "uuid"
5677 },
5678 "participant_count": {
5679 "type": "integer",
5680 "format": "int64"
5681 },
5682 "reply_count": {
5683 "type": "integer",
5684 "format": "int64"
5685 }
5686 }
5687 },
5688 "AdminDto": {
5689 "type": "object",
5690 "required": [
5691 "id",
5692 "username",
5693 "display_name",
5694 "role",
5695 "is_active",
5696 "must_change_password",
5697 "created_at",
5698 "updated_at"
5699 ],
5700 "properties": {
5701 "created_at": {
5702 "type": "string",
5703 "format": "date-time"
5704 },
5705 "display_name": {
5706 "type": "string"
5707 },
5708 "id": {
5709 "type": "string",
5710 "format": "uuid"
5711 },
5712 "is_active": {
5713 "type": "boolean"
5714 },
5715 "last_login_at": {
5716 "type": [
5717 "string",
5718 "null"
5719 ],
5720 "format": "date-time"
5721 },
5722 "last_login_ip": {
5723 "type": [
5724 "string",
5725 "null"
5726 ]
5727 },
5728 "must_change_password": {
5729 "type": "boolean"
5730 },
5731 "role": {
5732 "$ref": "#/components/schemas/AdminRole"
5733 },
5734 "updated_at": {
5735 "type": "string",
5736 "format": "date-time"
5737 },
5738 "username": {
5739 "type": "string"
5740 }
5741 }
5742 },
5743 "AdminEndpointDto": {
5744 "type": "object",
5745 "required": [
5746 "enabled",
5747 "bind_addr",
5748 "port",
5749 "session_cookie_name",
5750 "session_table_name",
5751 "cors_allow_origins",
5752 "cors_allow_credentials"
5753 ],
5754 "properties": {
5755 "bind_addr": {
5756 "type": "string"
5757 },
5758 "cors_allow_credentials": {
5759 "type": "boolean"
5760 },
5761 "cors_allow_origins": {
5762 "type": "array",
5763 "items": {
5764 "type": "string"
5765 }
5766 },
5767 "enabled": {
5768 "type": "boolean"
5769 },
5770 "port": {
5771 "type": "integer",
5772 "format": "int32",
5773 "minimum": 0
5774 },
5775 "session_cookie_name": {
5776 "type": "string"
5777 },
5778 "session_table_name": {
5779 "type": "string"
5780 }
5781 }
5782 },
5783 "AdminLoginCmd": {
5784 "type": "object",
5785 "required": [
5786 "username",
5787 "password"
5788 ],
5789 "properties": {
5790 "password": {
5791 "type": "string"
5792 },
5793 "username": {
5794 "type": "string"
5795 }
5796 }
5797 },
5798 "AdminLoginResp": {
5799 "type": "object",
5800 "required": [
5801 "admin",
5802 "must_change_password"
5803 ],
5804 "properties": {
5805 "admin": {
5806 "$ref": "#/components/schemas/AdminDto"
5807 },
5808 "must_change_password": {
5809 "type": "boolean"
5810 }
5811 }
5812 },
5813 "AdminRepoPage": {
5814 "type": "object",
5815 "required": [
5816 "items",
5817 "total",
5818 "limit",
5819 "offset"
5820 ],
5821 "properties": {
5822 "items": {
5823 "type": "array",
5824 "items": {
5825 "$ref": "#/components/schemas/AdminRepoSummary"
5826 }
5827 },
5828 "limit": {
5829 "type": "integer",
5830 "format": "int64",
5831 "minimum": 0
5832 },
5833 "offset": {
5834 "type": "integer",
5835 "format": "int64",
5836 "minimum": 0
5837 },
5838 "total": {
5839 "type": "integer",
5840 "format": "int64",
5841 "minimum": 0
5842 }
5843 }
5844 },
5845 "AdminRepoSummary": {
5846 "type": "object",
5847 "required": [
5848 "id",
5849 "dir_id",
5850 "name",
5851 "lower_name",
5852 "full_name",
5853 "default_branch",
5854 "is_private",
5855 "is_archived",
5856 "enable_lfs",
5857 "size",
5858 "git_size",
5859 "lfs_size",
5860 "nums_commits",
5861 "nums_branches",
5862 "nums_tags",
5863 "created_at",
5864 "updated_at"
5865 ],
5866 "properties": {
5867 "created_at": {
5868 "type": "string",
5869 "format": "date-time"
5870 },
5871 "default_branch": {
5872 "type": "string"
5873 },
5874 "description": {
5875 "type": [
5876 "string",
5877 "null"
5878 ]
5879 },
5880 "dir_id": {
5881 "type": "string",
5882 "format": "uuid"
5883 },
5884 "enable_lfs": {
5885 "type": "boolean"
5886 },
5887 "full_name": {
5888 "type": "string"
5889 },
5890 "git_size": {
5891 "type": "integer",
5892 "format": "int64"
5893 },
5894 "id": {
5895 "type": "string",
5896 "format": "uuid"
5897 },
5898 "is_archived": {
5899 "type": "boolean"
5900 },
5901 "is_private": {
5902 "type": "boolean"
5903 },
5904 "last_pushed_at": {
5905 "type": [
5906 "string",
5907 "null"
5908 ],
5909 "format": "date-time"
5910 },
5911 "lfs_size": {
5912 "type": "integer",
5913 "format": "int64"
5914 },
5915 "lower_name": {
5916 "type": "string"
5917 },
5918 "name": {
5919 "type": "string"
5920 },
5921 "nums_branches": {
5922 "type": "integer",
5923 "format": "int32"
5924 },
5925 "nums_commits": {
5926 "type": "integer",
5927 "format": "int32"
5928 },
5929 "nums_tags": {
5930 "type": "integer",
5931 "format": "int32"
5932 },
5933 "primary_language": {
5934 "type": [
5935 "string",
5936 "null"
5937 ]
5938 },
5939 "size": {
5940 "type": "integer",
5941 "format": "int64"
5942 },
5943 "space_id": {
5944 "type": [
5945 "string",
5946 "null"
5947 ],
5948 "format": "uuid"
5949 },
5950 "space_lower_name": {
5951 "type": [
5952 "string",
5953 "null"
5954 ]
5955 },
5956 "space_name": {
5957 "type": [
5958 "string",
5959 "null"
5960 ]
5961 },
5962 "storage_limit_size": {
5963 "type": [
5964 "integer",
5965 "null"
5966 ],
5967 "format": "int64"
5968 },
5969 "updated_at": {
5970 "type": "string",
5971 "format": "date-time"
5972 }
5973 }
5974 },
5975 "AdminRole": {
5976 "type": "string",
5977 "enum": [
5978 "super_admin",
5979 "operator",
5980 "viewer"
5981 ]
5982 },
5983 "AdminSpaceDetail": {
5984 "type": "object",
5985 "required": [
5986 "space",
5987 "repos_count",
5988 "repos_size",
5989 "rooms_count",
5990 "dirs_count"
5991 ],
5992 "properties": {
5993 "dirs_count": {
5994 "type": "integer",
5995 "format": "int64"
5996 },
5997 "repos_count": {
5998 "type": "integer",
5999 "format": "int64"
6000 },
6001 "repos_size": {
6002 "type": "integer",
6003 "format": "int64"
6004 },
6005 "rooms_count": {
6006 "type": "integer",
6007 "format": "int64"
6008 },
6009 "space": {
6010 "$ref": "#/components/schemas/AdminSpaceSummary"
6011 }
6012 }
6013 },
6014 "AdminSpaceMemberDto": {
6015 "type": "object",
6016 "required": [
6017 "user_id",
6018 "status",
6019 "joined_at"
6020 ],
6021 "properties": {
6022 "alias_name": {
6023 "type": [
6024 "string",
6025 "null"
6026 ]
6027 },
6028 "display_name": {
6029 "type": [
6030 "string",
6031 "null"
6032 ]
6033 },
6034 "joined_at": {
6035 "type": "string",
6036 "format": "date-time"
6037 },
6038 "last_active_at": {
6039 "type": [
6040 "string",
6041 "null"
6042 ],
6043 "format": "date-time"
6044 },
6045 "status": {
6046 "type": "string"
6047 },
6048 "user_id": {
6049 "type": "string",
6050 "format": "uuid"
6051 },
6052 "username": {
6053 "type": [
6054 "string",
6055 "null"
6056 ]
6057 }
6058 }
6059 },
6060 "AdminSpaceSummary": {
6061 "type": "object",
6062 "required": [
6063 "id",
6064 "name",
6065 "lower_name",
6066 "owner_id",
6067 "is_private",
6068 "is_active",
6069 "allow_join",
6070 "allow_invite",
6071 "member_must_enable_2fa",
6072 "nums_members",
6073 "created_at",
6074 "updated_at"
6075 ],
6076 "properties": {
6077 "allow_invite": {
6078 "type": "boolean"
6079 },
6080 "allow_join": {
6081 "type": "boolean"
6082 },
6083 "created_at": {
6084 "type": "string",
6085 "format": "date-time"
6086 },
6087 "description": {
6088 "type": [
6089 "string",
6090 "null"
6091 ]
6092 },
6093 "id": {
6094 "type": "string",
6095 "format": "uuid"
6096 },
6097 "is_active": {
6098 "type": "boolean"
6099 },
6100 "is_private": {
6101 "type": "boolean"
6102 },
6103 "lower_name": {
6104 "type": "string"
6105 },
6106 "member_must_enable_2fa": {
6107 "type": "boolean"
6108 },
6109 "name": {
6110 "type": "string"
6111 },
6112 "nums_members": {
6113 "type": "integer",
6114 "format": "int64"
6115 },
6116 "owner_id": {
6117 "type": "string",
6118 "format": "uuid"
6119 },
6120 "owner_username": {
6121 "type": [
6122 "string",
6123 "null"
6124 ]
6125 },
6126 "updated_at": {
6127 "type": "string",
6128 "format": "date-time"
6129 }
6130 }
6131 },
6132 "AdminSuspensionDto": {
6133 "type": "object",
6134 "required": [
6135 "id",
6136 "suspension_type",
6137 "starts_at"
6138 ],
6139 "properties": {
6140 "ends_at": {
6141 "type": [
6142 "string",
6143 "null"
6144 ],
6145 "format": "date-time"
6146 },
6147 "id": {
6148 "type": "string",
6149 "format": "uuid"
6150 },
6151 "reason": {
6152 "type": [
6153 "string",
6154 "null"
6155 ]
6156 },
6157 "revoked_at": {
6158 "type": [
6159 "string",
6160 "null"
6161 ],
6162 "format": "date-time"
6163 },
6164 "starts_at": {
6165 "type": "string",
6166 "format": "date-time"
6167 },
6168 "suspended_by": {
6169 "type": [
6170 "string",
6171 "null"
6172 ],
6173 "format": "uuid"
6174 },
6175 "suspension_type": {
6176 "type": "string"
6177 }
6178 }
6179 },
6180 "AdminUserDetail": {
6181 "type": "object",
6182 "required": [
6183 "user",
6184 "emails",
6185 "sessions",
6186 "spaces",
6187 "suspensions"
6188 ],
6189 "properties": {
6190 "emails": {
6191 "type": "array",
6192 "items": {
6193 "$ref": "#/components/schemas/AdminUserEmailDto"
6194 }
6195 },
6196 "sessions": {
6197 "type": "array",
6198 "items": {
6199 "$ref": "#/components/schemas/AdminUserSessionDto"
6200 }
6201 },
6202 "spaces": {
6203 "type": "array",
6204 "items": {
6205 "$ref": "#/components/schemas/AdminUserSpaceDto"
6206 }
6207 },
6208 "suspensions": {
6209 "type": "array",
6210 "items": {
6211 "$ref": "#/components/schemas/AdminSuspensionDto"
6212 }
6213 },
6214 "user": {
6215 "$ref": "#/components/schemas/AdminUserSummary"
6216 }
6217 }
6218 },
6219 "AdminUserEmailDto": {
6220 "type": "object",
6221 "required": [
6222 "id",
6223 "email",
6224 "is_primary",
6225 "is_activated",
6226 "created_at"
6227 ],
6228 "properties": {
6229 "created_at": {
6230 "type": "string",
6231 "format": "date-time"
6232 },
6233 "email": {
6234 "type": "string"
6235 },
6236 "id": {
6237 "type": "string",
6238 "format": "uuid"
6239 },
6240 "is_activated": {
6241 "type": "boolean"
6242 },
6243 "is_primary": {
6244 "type": "boolean"
6245 }
6246 }
6247 },
6248 "AdminUserSecurityDto": {
6249 "type": "object",
6250 "required": [
6251 "totp_enabled",
6252 "recovery_codes_total",
6253 "recovery_codes_used",
6254 "active_sessions",
6255 "ssh_keys",
6256 "access_keys",
6257 "passwd_version"
6258 ],
6259 "properties": {
6260 "access_keys": {
6261 "type": "integer",
6262 "format": "int64"
6263 },
6264 "active_sessions": {
6265 "type": "integer",
6266 "format": "int64"
6267 },
6268 "passwd_version": {
6269 "type": "integer",
6270 "format": "int64"
6271 },
6272 "password_last_changed_at": {
6273 "type": [
6274 "string",
6275 "null"
6276 ],
6277 "format": "date-time"
6278 },
6279 "recovery_codes_total": {
6280 "type": "integer",
6281 "format": "int64"
6282 },
6283 "recovery_codes_used": {
6284 "type": "integer",
6285 "format": "int64"
6286 },
6287 "ssh_keys": {
6288 "type": "integer",
6289 "format": "int64"
6290 },
6291 "totp_enabled": {
6292 "type": "boolean"
6293 }
6294 }
6295 },
6296 "AdminUserSessionDto": {
6297 "type": "object",
6298 "required": [
6299 "id",
6300 "kind",
6301 "last_seen_at",
6302 "created_at",
6303 "expires_at",
6304 "is_revoked"
6305 ],
6306 "properties": {
6307 "created_at": {
6308 "type": "string",
6309 "format": "date-time"
6310 },
6311 "expires_at": {
6312 "type": "string",
6313 "format": "date-time"
6314 },
6315 "id": {
6316 "type": "string",
6317 "format": "uuid"
6318 },
6319 "is_revoked": {
6320 "type": "boolean"
6321 },
6322 "kind": {
6323 "type": "string"
6324 },
6325 "last_seen_at": {
6326 "type": "string",
6327 "format": "date-time"
6328 }
6329 }
6330 },
6331 "AdminUserSessionPage": {
6332 "type": "object",
6333 "required": [
6334 "items",
6335 "total",
6336 "limit",
6337 "offset"
6338 ],
6339 "properties": {
6340 "items": {
6341 "type": "array",
6342 "items": {
6343 "$ref": "#/components/schemas/AdminUserSessionRow"
6344 }
6345 },
6346 "limit": {
6347 "type": "integer",
6348 "format": "int64",
6349 "minimum": 0
6350 },
6351 "offset": {
6352 "type": "integer",
6353 "format": "int64",
6354 "minimum": 0
6355 },
6356 "total": {
6357 "type": "integer",
6358 "format": "int64"
6359 }
6360 }
6361 },
6362 "AdminUserSessionRow": {
6363 "type": "object",
6364 "required": [
6365 "id",
6366 "kind",
6367 "last_seen_at",
6368 "created_at",
6369 "expires_at",
6370 "is_revoked"
6371 ],
6372 "properties": {
6373 "app_id": {
6374 "type": [
6375 "string",
6376 "null"
6377 ]
6378 },
6379 "client_ip_hash": {
6380 "type": [
6381 "string",
6382 "null"
6383 ]
6384 },
6385 "created_at": {
6386 "type": "string",
6387 "format": "date-time"
6388 },
6389 "device_id": {
6390 "type": [
6391 "string",
6392 "null"
6393 ],
6394 "format": "uuid"
6395 },
6396 "expires_at": {
6397 "type": "string",
6398 "format": "date-time"
6399 },
6400 "id": {
6401 "type": "string",
6402 "format": "uuid"
6403 },
6404 "is_revoked": {
6405 "type": "boolean"
6406 },
6407 "kind": {
6408 "type": "string"
6409 },
6410 "last_seen_at": {
6411 "type": "string",
6412 "format": "date-time"
6413 },
6414 "revoked_at": {
6415 "type": [
6416 "string",
6417 "null"
6418 ],
6419 "format": "date-time"
6420 },
6421 "user_agent_hash": {
6422 "type": [
6423 "string",
6424 "null"
6425 ]
6426 }
6427 }
6428 },
6429 "AdminUserSpaceDto": {
6430 "type": "object",
6431 "required": [
6432 "id",
6433 "name",
6434 "lower_name",
6435 "is_active"
6436 ],
6437 "properties": {
6438 "id": {
6439 "type": "string",
6440 "format": "uuid"
6441 },
6442 "is_active": {
6443 "type": "boolean"
6444 },
6445 "lower_name": {
6446 "type": "string"
6447 },
6448 "name": {
6449 "type": "string"
6450 }
6451 }
6452 },
6453 "AdminUserSummary": {
6454 "type": "object",
6455 "required": [
6456 "uid",
6457 "username",
6458 "display_name",
6459 "avatar_url",
6460 "is_active",
6461 "is_restricted",
6462 "prohibit_login",
6463 "bot",
6464 "system",
6465 "suspended",
6466 "created_at"
6467 ],
6468 "properties": {
6469 "avatar_url": {
6470 "type": "string"
6471 },
6472 "bot": {
6473 "type": "boolean"
6474 },
6475 "created_at": {
6476 "type": "string",
6477 "format": "date-time"
6478 },
6479 "display_name": {
6480 "type": "string"
6481 },
6482 "is_active": {
6483 "type": "boolean"
6484 },
6485 "is_restricted": {
6486 "type": "boolean"
6487 },
6488 "last_login_at": {
6489 "type": [
6490 "string",
6491 "null"
6492 ],
6493 "format": "date-time"
6494 },
6495 "prohibit_login": {
6496 "type": "boolean"
6497 },
6498 "suspended": {
6499 "type": "boolean"
6500 },
6501 "system": {
6502 "type": "boolean"
6503 },
6504 "uid": {
6505 "type": "string",
6506 "format": "uuid"
6507 },
6508 "username": {
6509 "type": "string"
6510 }
6511 }
6512 },
6513 "AppResponseData_AddSpaceMemberResp": {
6514 "type": "object",
6515 "required": [
6516 "code",
6517 "message",
6518 "request_id",
6519 "log_id"
6520 ],
6521 "properties": {
6522 "code": {
6523 "type": "integer",
6524 "format": "int32"
6525 },
6526 "data": {
6527 "type": "object",
6528 "required": [
6529 "space_id",
6530 "user_id",
6531 "status",
6532 "added"
6533 ],
6534 "properties": {
6535 "added": {
6536 "type": "boolean"
6537 },
6538 "space_id": {
6539 "type": "string",
6540 "format": "uuid"
6541 },
6542 "status": {
6543 "type": "string"
6544 },
6545 "user_id": {
6546 "type": "string",
6547 "format": "uuid"
6548 }
6549 }
6550 },
6551 "error": {
6552 "type": [
6553 "string",
6554 "null"
6555 ]
6556 },
6557 "error_code": {
6558 "type": [
6559 "string",
6560 "null"
6561 ]
6562 },
6563 "log_id": {
6564 "type": "string"
6565 },
6566 "message": {
6567 "type": "string"
6568 },
6569 "request_id": {
6570 "type": "string"
6571 }
6572 }
6573 },
6574 "AppResponseData_AdminActRunPage": {
6575 "type": "object",
6576 "required": [
6577 "code",
6578 "message",
6579 "request_id",
6580 "log_id"
6581 ],
6582 "properties": {
6583 "code": {
6584 "type": "integer",
6585 "format": "int32"
6586 },
6587 "data": {
6588 "type": "object",
6589 "required": [
6590 "items",
6591 "total",
6592 "limit",
6593 "offset"
6594 ],
6595 "properties": {
6596 "items": {
6597 "type": "array",
6598 "items": {
6599 "$ref": "#/components/schemas/AdminActRunDto"
6600 }
6601 },
6602 "limit": {
6603 "type": "integer",
6604 "format": "int64",
6605 "minimum": 0
6606 },
6607 "offset": {
6608 "type": "integer",
6609 "format": "int64",
6610 "minimum": 0
6611 },
6612 "total": {
6613 "type": "integer",
6614 "format": "int64",
6615 "minimum": 0
6616 }
6617 }
6618 },
6619 "error": {
6620 "type": [
6621 "string",
6622 "null"
6623 ]
6624 },
6625 "error_code": {
6626 "type": [
6627 "string",
6628 "null"
6629 ]
6630 },
6631 "log_id": {
6632 "type": "string"
6633 },
6634 "message": {
6635 "type": "string"
6636 },
6637 "request_id": {
6638 "type": "string"
6639 }
6640 }
6641 },
6642 "AppResponseData_AdminActSummary": {
6643 "type": "object",
6644 "required": [
6645 "code",
6646 "message",
6647 "request_id",
6648 "log_id"
6649 ],
6650 "properties": {
6651 "code": {
6652 "type": "integer",
6653 "format": "int32"
6654 },
6655 "data": {
6656 "type": "object",
6657 "required": [
6658 "runners_total",
6659 "runners_active",
6660 "runners_busy",
6661 "jobs_queued",
6662 "jobs_running",
6663 "runs_queued",
6664 "runs_running",
6665 "runs_success",
6666 "runs_failure",
6667 "generated_at"
6668 ],
6669 "properties": {
6670 "generated_at": {
6671 "type": "string",
6672 "format": "date-time"
6673 },
6674 "jobs_queued": {
6675 "type": "integer",
6676 "format": "int64",
6677 "minimum": 0
6678 },
6679 "jobs_running": {
6680 "type": "integer",
6681 "format": "int64",
6682 "minimum": 0
6683 },
6684 "runners_active": {
6685 "type": "integer",
6686 "format": "int64",
6687 "minimum": 0
6688 },
6689 "runners_busy": {
6690 "type": "integer",
6691 "format": "int64",
6692 "minimum": 0
6693 },
6694 "runners_total": {
6695 "type": "integer",
6696 "format": "int64",
6697 "minimum": 0
6698 },
6699 "runs_failure": {
6700 "type": "integer",
6701 "format": "int64",
6702 "minimum": 0
6703 },
6704 "runs_queued": {
6705 "type": "integer",
6706 "format": "int64",
6707 "minimum": 0
6708 },
6709 "runs_running": {
6710 "type": "integer",
6711 "format": "int64",
6712 "minimum": 0
6713 },
6714 "runs_success": {
6715 "type": "integer",
6716 "format": "int64",
6717 "minimum": 0
6718 }
6719 }
6720 },
6721 "error": {
6722 "type": [
6723 "string",
6724 "null"
6725 ]
6726 },
6727 "error_code": {
6728 "type": [
6729 "string",
6730 "null"
6731 ]
6732 },
6733 "log_id": {
6734 "type": "string"
6735 },
6736 "message": {
6737 "type": "string"
6738 },
6739 "request_id": {
6740 "type": "string"
6741 }
6742 }
6743 },
6744 "AppResponseData_AdminChatMessageDetail": {
6745 "type": "object",
6746 "required": [
6747 "code",
6748 "message",
6749 "request_id",
6750 "log_id"
6751 ],
6752 "properties": {
6753 "code": {
6754 "type": "integer",
6755 "format": "int32"
6756 },
6757 "data": {
6758 "type": "object",
6759 "required": [
6760 "message",
6761 "edits",
6762 "reactions",
6763 "attachments"
6764 ],
6765 "properties": {
6766 "attachments": {
6767 "type": "array",
6768 "items": {
6769 "$ref": "#/components/schemas/AdminChatAttachmentDto"
6770 }
6771 },
6772 "edits": {
6773 "type": "array",
6774 "items": {
6775 "$ref": "#/components/schemas/AdminChatMessageEditDto"
6776 }
6777 },
6778 "message": {
6779 "$ref": "#/components/schemas/AdminChatMessageDto"
6780 },
6781 "reactions": {
6782 "type": "array",
6783 "items": {
6784 "$ref": "#/components/schemas/AdminChatReactionGroupDto"
6785 }
6786 },
6787 "thread": {
6788 "oneOf": [
6789 {
6790 "type": "null"
6791 },
6792 {
6793 "$ref": "#/components/schemas/AdminChatThreadDto"
6794 }
6795 ]
6796 }
6797 }
6798 },
6799 "error": {
6800 "type": [
6801 "string",
6802 "null"
6803 ]
6804 },
6805 "error_code": {
6806 "type": [
6807 "string",
6808 "null"
6809 ]
6810 },
6811 "log_id": {
6812 "type": "string"
6813 },
6814 "message": {
6815 "type": "string"
6816 },
6817 "request_id": {
6818 "type": "string"
6819 }
6820 }
6821 },
6822 "AppResponseData_AdminChatRoomDetail": {
6823 "type": "object",
6824 "required": [
6825 "code",
6826 "message",
6827 "request_id",
6828 "log_id"
6829 ],
6830 "properties": {
6831 "code": {
6832 "type": "integer",
6833 "format": "int32"
6834 },
6835 "data": {
6836 "type": "object",
6837 "required": [
6838 "room",
6839 "deleted_messages",
6840 "threads",
6841 "attachments",
6842 "attachment_bytes",
6843 "bans",
6844 "invites"
6845 ],
6846 "properties": {
6847 "attachment_bytes": {
6848 "type": "integer",
6849 "format": "int64"
6850 },
6851 "attachments": {
6852 "type": "integer",
6853 "format": "int64"
6854 },
6855 "bans": {
6856 "type": "integer",
6857 "format": "int64"
6858 },
6859 "deleted_messages": {
6860 "type": "integer",
6861 "format": "int64"
6862 },
6863 "invites": {
6864 "type": "integer",
6865 "format": "int64"
6866 },
6867 "room": {
6868 "$ref": "#/components/schemas/AdminChatRoomSummary"
6869 },
6870 "threads": {
6871 "type": "integer",
6872 "format": "int64"
6873 }
6874 }
6875 },
6876 "error": {
6877 "type": [
6878 "string",
6879 "null"
6880 ]
6881 },
6882 "error_code": {
6883 "type": [
6884 "string",
6885 "null"
6886 ]
6887 },
6888 "log_id": {
6889 "type": "string"
6890 },
6891 "message": {
6892 "type": "string"
6893 },
6894 "request_id": {
6895 "type": "string"
6896 }
6897 }
6898 },
6899 "AppResponseData_AdminChatStatsDto": {
6900 "type": "object",
6901 "required": [
6902 "code",
6903 "message",
6904 "request_id",
6905 "log_id"
6906 ],
6907 "properties": {
6908 "code": {
6909 "type": "integer",
6910 "format": "int32"
6911 },
6912 "data": {
6913 "type": "object",
6914 "required": [
6915 "rooms",
6916 "active_rooms",
6917 "private_rooms",
6918 "kinds",
6919 "messages",
6920 "deleted_messages",
6921 "messages_24h",
6922 "threads",
6923 "attachments",
6924 "attachment_bytes",
6925 "pending_attachments",
6926 "members",
6927 "bans"
6928 ],
6929 "properties": {
6930 "active_rooms": {
6931 "type": "integer",
6932 "format": "int64"
6933 },
6934 "attachment_bytes": {
6935 "type": "integer",
6936 "format": "int64"
6937 },
6938 "attachments": {
6939 "type": "integer",
6940 "format": "int64"
6941 },
6942 "bans": {
6943 "type": "integer",
6944 "format": "int64"
6945 },
6946 "deleted_messages": {
6947 "type": "integer",
6948 "format": "int64"
6949 },
6950 "kinds": {
6951 "type": "array",
6952 "items": {
6953 "$ref": "#/components/schemas/AdminChatKindCount"
6954 }
6955 },
6956 "members": {
6957 "type": "integer",
6958 "format": "int64"
6959 },
6960 "messages": {
6961 "type": "integer",
6962 "format": "int64"
6963 },
6964 "messages_24h": {
6965 "type": "integer",
6966 "format": "int64"
6967 },
6968 "pending_attachments": {
6969 "type": "integer",
6970 "format": "int64"
6971 },
6972 "private_rooms": {
6973 "type": "integer",
6974 "format": "int64"
6975 },
6976 "rooms": {
6977 "type": "integer",
6978 "format": "int64"
6979 },
6980 "threads": {
6981 "type": "integer",
6982 "format": "int64"
6983 }
6984 }
6985 },
6986 "error": {
6987 "type": [
6988 "string",
6989 "null"
6990 ]
6991 },
6992 "error_code": {
6993 "type": [
6994 "string",
6995 "null"
6996 ]
6997 },
6998 "log_id": {
6999 "type": "string"
7000 },
7001 "message": {
7002 "type": "string"
7003 },
7004 "request_id": {
7005 "type": "string"
7006 }
7007 }
7008 },
7009 "AppResponseData_AdminDto": {
7010 "type": "object",
7011 "required": [
7012 "code",
7013 "message",
7014 "request_id",
7015 "log_id"
7016 ],
7017 "properties": {
7018 "code": {
7019 "type": "integer",
7020 "format": "int32"
7021 },
7022 "data": {
7023 "type": "object",
7024 "required": [
7025 "id",
7026 "username",
7027 "display_name",
7028 "role",
7029 "is_active",
7030 "must_change_password",
7031 "created_at",
7032 "updated_at"
7033 ],
7034 "properties": {
7035 "created_at": {
7036 "type": "string",
7037 "format": "date-time"
7038 },
7039 "display_name": {
7040 "type": "string"
7041 },
7042 "id": {
7043 "type": "string",
7044 "format": "uuid"
7045 },
7046 "is_active": {
7047 "type": "boolean"
7048 },
7049 "last_login_at": {
7050 "type": [
7051 "string",
7052 "null"
7053 ],
7054 "format": "date-time"
7055 },
7056 "last_login_ip": {
7057 "type": [
7058 "string",
7059 "null"
7060 ]
7061 },
7062 "must_change_password": {
7063 "type": "boolean"
7064 },
7065 "role": {
7066 "$ref": "#/components/schemas/AdminRole"
7067 },
7068 "updated_at": {
7069 "type": "string",
7070 "format": "date-time"
7071 },
7072 "username": {
7073 "type": "string"
7074 }
7075 }
7076 },
7077 "error": {
7078 "type": [
7079 "string",
7080 "null"
7081 ]
7082 },
7083 "error_code": {
7084 "type": [
7085 "string",
7086 "null"
7087 ]
7088 },
7089 "log_id": {
7090 "type": "string"
7091 },
7092 "message": {
7093 "type": "string"
7094 },
7095 "request_id": {
7096 "type": "string"
7097 }
7098 }
7099 },
7100 "AppResponseData_AdminLoginResp": {
7101 "type": "object",
7102 "required": [
7103 "code",
7104 "message",
7105 "request_id",
7106 "log_id"
7107 ],
7108 "properties": {
7109 "code": {
7110 "type": "integer",
7111 "format": "int32"
7112 },
7113 "data": {
7114 "type": "object",
7115 "required": [
7116 "admin",
7117 "must_change_password"
7118 ],
7119 "properties": {
7120 "admin": {
7121 "$ref": "#/components/schemas/AdminDto"
7122 },
7123 "must_change_password": {
7124 "type": "boolean"
7125 }
7126 }
7127 },
7128 "error": {
7129 "type": [
7130 "string",
7131 "null"
7132 ]
7133 },
7134 "error_code": {
7135 "type": [
7136 "string",
7137 "null"
7138 ]
7139 },
7140 "log_id": {
7141 "type": "string"
7142 },
7143 "message": {
7144 "type": "string"
7145 },
7146 "request_id": {
7147 "type": "string"
7148 }
7149 }
7150 },
7151 "AppResponseData_AdminRepoPage": {
7152 "type": "object",
7153 "required": [
7154 "code",
7155 "message",
7156 "request_id",
7157 "log_id"
7158 ],
7159 "properties": {
7160 "code": {
7161 "type": "integer",
7162 "format": "int32"
7163 },
7164 "data": {
7165 "type": "object",
7166 "required": [
7167 "items",
7168 "total",
7169 "limit",
7170 "offset"
7171 ],
7172 "properties": {
7173 "items": {
7174 "type": "array",
7175 "items": {
7176 "$ref": "#/components/schemas/AdminRepoSummary"
7177 }
7178 },
7179 "limit": {
7180 "type": "integer",
7181 "format": "int64",
7182 "minimum": 0
7183 },
7184 "offset": {
7185 "type": "integer",
7186 "format": "int64",
7187 "minimum": 0
7188 },
7189 "total": {
7190 "type": "integer",
7191 "format": "int64",
7192 "minimum": 0
7193 }
7194 }
7195 },
7196 "error": {
7197 "type": [
7198 "string",
7199 "null"
7200 ]
7201 },
7202 "error_code": {
7203 "type": [
7204 "string",
7205 "null"
7206 ]
7207 },
7208 "log_id": {
7209 "type": "string"
7210 },
7211 "message": {
7212 "type": "string"
7213 },
7214 "request_id": {
7215 "type": "string"
7216 }
7217 }
7218 },
7219 "AppResponseData_AdminRepoSummary": {
7220 "type": "object",
7221 "required": [
7222 "code",
7223 "message",
7224 "request_id",
7225 "log_id"
7226 ],
7227 "properties": {
7228 "code": {
7229 "type": "integer",
7230 "format": "int32"
7231 },
7232 "data": {
7233 "type": "object",
7234 "required": [
7235 "id",
7236 "dir_id",
7237 "name",
7238 "lower_name",
7239 "full_name",
7240 "default_branch",
7241 "is_private",
7242 "is_archived",
7243 "enable_lfs",
7244 "size",
7245 "git_size",
7246 "lfs_size",
7247 "nums_commits",
7248 "nums_branches",
7249 "nums_tags",
7250 "created_at",
7251 "updated_at"
7252 ],
7253 "properties": {
7254 "created_at": {
7255 "type": "string",
7256 "format": "date-time"
7257 },
7258 "default_branch": {
7259 "type": "string"
7260 },
7261 "description": {
7262 "type": [
7263 "string",
7264 "null"
7265 ]
7266 },
7267 "dir_id": {
7268 "type": "string",
7269 "format": "uuid"
7270 },
7271 "enable_lfs": {
7272 "type": "boolean"
7273 },
7274 "full_name": {
7275 "type": "string"
7276 },
7277 "git_size": {
7278 "type": "integer",
7279 "format": "int64"
7280 },
7281 "id": {
7282 "type": "string",
7283 "format": "uuid"
7284 },
7285 "is_archived": {
7286 "type": "boolean"
7287 },
7288 "is_private": {
7289 "type": "boolean"
7290 },
7291 "last_pushed_at": {
7292 "type": [
7293 "string",
7294 "null"
7295 ],
7296 "format": "date-time"
7297 },
7298 "lfs_size": {
7299 "type": "integer",
7300 "format": "int64"
7301 },
7302 "lower_name": {
7303 "type": "string"
7304 },
7305 "name": {
7306 "type": "string"
7307 },
7308 "nums_branches": {
7309 "type": "integer",
7310 "format": "int32"
7311 },
7312 "nums_commits": {
7313 "type": "integer",
7314 "format": "int32"
7315 },
7316 "nums_tags": {
7317 "type": "integer",
7318 "format": "int32"
7319 },
7320 "primary_language": {
7321 "type": [
7322 "string",
7323 "null"
7324 ]
7325 },
7326 "size": {
7327 "type": "integer",
7328 "format": "int64"
7329 },
7330 "space_id": {
7331 "type": [
7332 "string",
7333 "null"
7334 ],
7335 "format": "uuid"
7336 },
7337 "space_lower_name": {
7338 "type": [
7339 "string",
7340 "null"
7341 ]
7342 },
7343 "space_name": {
7344 "type": [
7345 "string",
7346 "null"
7347 ]
7348 },
7349 "storage_limit_size": {
7350 "type": [
7351 "integer",
7352 "null"
7353 ],
7354 "format": "int64"
7355 },
7356 "updated_at": {
7357 "type": "string",
7358 "format": "date-time"
7359 }
7360 }
7361 },
7362 "error": {
7363 "type": [
7364 "string",
7365 "null"
7366 ]
7367 },
7368 "error_code": {
7369 "type": [
7370 "string",
7371 "null"
7372 ]
7373 },
7374 "log_id": {
7375 "type": "string"
7376 },
7377 "message": {
7378 "type": "string"
7379 },
7380 "request_id": {
7381 "type": "string"
7382 }
7383 }
7384 },
7385 "AppResponseData_AdminSpaceDetail": {
7386 "type": "object",
7387 "required": [
7388 "code",
7389 "message",
7390 "request_id",
7391 "log_id"
7392 ],
7393 "properties": {
7394 "code": {
7395 "type": "integer",
7396 "format": "int32"
7397 },
7398 "data": {
7399 "type": "object",
7400 "required": [
7401 "space",
7402 "repos_count",
7403 "repos_size",
7404 "rooms_count",
7405 "dirs_count"
7406 ],
7407 "properties": {
7408 "dirs_count": {
7409 "type": "integer",
7410 "format": "int64"
7411 },
7412 "repos_count": {
7413 "type": "integer",
7414 "format": "int64"
7415 },
7416 "repos_size": {
7417 "type": "integer",
7418 "format": "int64"
7419 },
7420 "rooms_count": {
7421 "type": "integer",
7422 "format": "int64"
7423 },
7424 "space": {
7425 "$ref": "#/components/schemas/AdminSpaceSummary"
7426 }
7427 }
7428 },
7429 "error": {
7430 "type": [
7431 "string",
7432 "null"
7433 ]
7434 },
7435 "error_code": {
7436 "type": [
7437 "string",
7438 "null"
7439 ]
7440 },
7441 "log_id": {
7442 "type": "string"
7443 },
7444 "message": {
7445 "type": "string"
7446 },
7447 "request_id": {
7448 "type": "string"
7449 }
7450 }
7451 },
7452 "AppResponseData_AdminSuspensionDto": {
7453 "type": "object",
7454 "required": [
7455 "code",
7456 "message",
7457 "request_id",
7458 "log_id"
7459 ],
7460 "properties": {
7461 "code": {
7462 "type": "integer",
7463 "format": "int32"
7464 },
7465 "data": {
7466 "type": "object",
7467 "required": [
7468 "id",
7469 "suspension_type",
7470 "starts_at"
7471 ],
7472 "properties": {
7473 "ends_at": {
7474 "type": [
7475 "string",
7476 "null"
7477 ],
7478 "format": "date-time"
7479 },
7480 "id": {
7481 "type": "string",
7482 "format": "uuid"
7483 },
7484 "reason": {
7485 "type": [
7486 "string",
7487 "null"
7488 ]
7489 },
7490 "revoked_at": {
7491 "type": [
7492 "string",
7493 "null"
7494 ],
7495 "format": "date-time"
7496 },
7497 "starts_at": {
7498 "type": "string",
7499 "format": "date-time"
7500 },
7501 "suspended_by": {
7502 "type": [
7503 "string",
7504 "null"
7505 ],
7506 "format": "uuid"
7507 },
7508 "suspension_type": {
7509 "type": "string"
7510 }
7511 }
7512 },
7513 "error": {
7514 "type": [
7515 "string",
7516 "null"
7517 ]
7518 },
7519 "error_code": {
7520 "type": [
7521 "string",
7522 "null"
7523 ]
7524 },
7525 "log_id": {
7526 "type": "string"
7527 },
7528 "message": {
7529 "type": "string"
7530 },
7531 "request_id": {
7532 "type": "string"
7533 }
7534 }
7535 },
7536 "AppResponseData_AdminUserDetail": {
7537 "type": "object",
7538 "required": [
7539 "code",
7540 "message",
7541 "request_id",
7542 "log_id"
7543 ],
7544 "properties": {
7545 "code": {
7546 "type": "integer",
7547 "format": "int32"
7548 },
7549 "data": {
7550 "type": "object",
7551 "required": [
7552 "user",
7553 "emails",
7554 "sessions",
7555 "spaces",
7556 "suspensions"
7557 ],
7558 "properties": {
7559 "emails": {
7560 "type": "array",
7561 "items": {
7562 "$ref": "#/components/schemas/AdminUserEmailDto"
7563 }
7564 },
7565 "sessions": {
7566 "type": "array",
7567 "items": {
7568 "$ref": "#/components/schemas/AdminUserSessionDto"
7569 }
7570 },
7571 "spaces": {
7572 "type": "array",
7573 "items": {
7574 "$ref": "#/components/schemas/AdminUserSpaceDto"
7575 }
7576 },
7577 "suspensions": {
7578 "type": "array",
7579 "items": {
7580 "$ref": "#/components/schemas/AdminSuspensionDto"
7581 }
7582 },
7583 "user": {
7584 "$ref": "#/components/schemas/AdminUserSummary"
7585 }
7586 }
7587 },
7588 "error": {
7589 "type": [
7590 "string",
7591 "null"
7592 ]
7593 },
7594 "error_code": {
7595 "type": [
7596 "string",
7597 "null"
7598 ]
7599 },
7600 "log_id": {
7601 "type": "string"
7602 },
7603 "message": {
7604 "type": "string"
7605 },
7606 "request_id": {
7607 "type": "string"
7608 }
7609 }
7610 },
7611 "AppResponseData_AdminUserSecurityDto": {
7612 "type": "object",
7613 "required": [
7614 "code",
7615 "message",
7616 "request_id",
7617 "log_id"
7618 ],
7619 "properties": {
7620 "code": {
7621 "type": "integer",
7622 "format": "int32"
7623 },
7624 "data": {
7625 "type": "object",
7626 "required": [
7627 "totp_enabled",
7628 "recovery_codes_total",
7629 "recovery_codes_used",
7630 "active_sessions",
7631 "ssh_keys",
7632 "access_keys",
7633 "passwd_version"
7634 ],
7635 "properties": {
7636 "access_keys": {
7637 "type": "integer",
7638 "format": "int64"
7639 },
7640 "active_sessions": {
7641 "type": "integer",
7642 "format": "int64"
7643 },
7644 "passwd_version": {
7645 "type": "integer",
7646 "format": "int64"
7647 },
7648 "password_last_changed_at": {
7649 "type": [
7650 "string",
7651 "null"
7652 ],
7653 "format": "date-time"
7654 },
7655 "recovery_codes_total": {
7656 "type": "integer",
7657 "format": "int64"
7658 },
7659 "recovery_codes_used": {
7660 "type": "integer",
7661 "format": "int64"
7662 },
7663 "ssh_keys": {
7664 "type": "integer",
7665 "format": "int64"
7666 },
7667 "totp_enabled": {
7668 "type": "boolean"
7669 }
7670 }
7671 },
7672 "error": {
7673 "type": [
7674 "string",
7675 "null"
7676 ]
7677 },
7678 "error_code": {
7679 "type": [
7680 "string",
7681 "null"
7682 ]
7683 },
7684 "log_id": {
7685 "type": "string"
7686 },
7687 "message": {
7688 "type": "string"
7689 },
7690 "request_id": {
7691 "type": "string"
7692 }
7693 }
7694 },
7695 "AppResponseData_AdminUserSessionPage": {
7696 "type": "object",
7697 "required": [
7698 "code",
7699 "message",
7700 "request_id",
7701 "log_id"
7702 ],
7703 "properties": {
7704 "code": {
7705 "type": "integer",
7706 "format": "int32"
7707 },
7708 "data": {
7709 "type": "object",
7710 "required": [
7711 "items",
7712 "total",
7713 "limit",
7714 "offset"
7715 ],
7716 "properties": {
7717 "items": {
7718 "type": "array",
7719 "items": {
7720 "$ref": "#/components/schemas/AdminUserSessionRow"
7721 }
7722 },
7723 "limit": {
7724 "type": "integer",
7725 "format": "int64",
7726 "minimum": 0
7727 },
7728 "offset": {
7729 "type": "integer",
7730 "format": "int64",
7731 "minimum": 0
7732 },
7733 "total": {
7734 "type": "integer",
7735 "format": "int64"
7736 }
7737 }
7738 },
7739 "error": {
7740 "type": [
7741 "string",
7742 "null"
7743 ]
7744 },
7745 "error_code": {
7746 "type": [
7747 "string",
7748 "null"
7749 ]
7750 },
7751 "log_id": {
7752 "type": "string"
7753 },
7754 "message": {
7755 "type": "string"
7756 },
7757 "request_id": {
7758 "type": "string"
7759 }
7760 }
7761 },
7762 "AppResponseData_AdminUserSummary": {
7763 "type": "object",
7764 "required": [
7765 "code",
7766 "message",
7767 "request_id",
7768 "log_id"
7769 ],
7770 "properties": {
7771 "code": {
7772 "type": "integer",
7773 "format": "int32"
7774 },
7775 "data": {
7776 "type": "object",
7777 "required": [
7778 "uid",
7779 "username",
7780 "display_name",
7781 "avatar_url",
7782 "is_active",
7783 "is_restricted",
7784 "prohibit_login",
7785 "bot",
7786 "system",
7787 "suspended",
7788 "created_at"
7789 ],
7790 "properties": {
7791 "avatar_url": {
7792 "type": "string"
7793 },
7794 "bot": {
7795 "type": "boolean"
7796 },
7797 "created_at": {
7798 "type": "string",
7799 "format": "date-time"
7800 },
7801 "display_name": {
7802 "type": "string"
7803 },
7804 "is_active": {
7805 "type": "boolean"
7806 },
7807 "is_restricted": {
7808 "type": "boolean"
7809 },
7810 "last_login_at": {
7811 "type": [
7812 "string",
7813 "null"
7814 ],
7815 "format": "date-time"
7816 },
7817 "prohibit_login": {
7818 "type": "boolean"
7819 },
7820 "suspended": {
7821 "type": "boolean"
7822 },
7823 "system": {
7824 "type": "boolean"
7825 },
7826 "uid": {
7827 "type": "string",
7828 "format": "uuid"
7829 },
7830 "username": {
7831 "type": "string"
7832 }
7833 }
7834 },
7835 "error": {
7836 "type": [
7837 "string",
7838 "null"
7839 ]
7840 },
7841 "error_code": {
7842 "type": [
7843 "string",
7844 "null"
7845 ]
7846 },
7847 "log_id": {
7848 "type": "string"
7849 },
7850 "message": {
7851 "type": "string"
7852 },
7853 "request_id": {
7854 "type": "string"
7855 }
7856 }
7857 },
7858 "AppResponseData_AuditLogPageDto": {
7859 "type": "object",
7860 "required": [
7861 "code",
7862 "message",
7863 "request_id",
7864 "log_id"
7865 ],
7866 "properties": {
7867 "code": {
7868 "type": "integer",
7869 "format": "int32"
7870 },
7871 "data": {
7872 "type": "object",
7873 "required": [
7874 "items",
7875 "total",
7876 "limit",
7877 "offset"
7878 ],
7879 "properties": {
7880 "items": {
7881 "type": "array",
7882 "items": {
7883 "$ref": "#/components/schemas/AuditLogDto"
7884 }
7885 },
7886 "limit": {
7887 "type": "integer",
7888 "format": "int64",
7889 "minimum": 0
7890 },
7891 "offset": {
7892 "type": "integer",
7893 "format": "int64",
7894 "minimum": 0
7895 },
7896 "total": {
7897 "type": "integer",
7898 "format": "int64"
7899 }
7900 }
7901 },
7902 "error": {
7903 "type": [
7904 "string",
7905 "null"
7906 ]
7907 },
7908 "error_code": {
7909 "type": [
7910 "string",
7911 "null"
7912 ]
7913 },
7914 "log_id": {
7915 "type": "string"
7916 },
7917 "message": {
7918 "type": "string"
7919 },
7920 "request_id": {
7921 "type": "string"
7922 }
7923 }
7924 },
7925 "AppResponseData_BulkDeleteChatMessagesResp": {
7926 "type": "object",
7927 "required": [
7928 "code",
7929 "message",
7930 "request_id",
7931 "log_id"
7932 ],
7933 "properties": {
7934 "code": {
7935 "type": "integer",
7936 "format": "int32"
7937 },
7938 "data": {
7939 "type": "object",
7940 "required": [
7941 "succeeded",
7942 "failed"
7943 ],
7944 "properties": {
7945 "failed": {
7946 "type": "array",
7947 "items": {
7948 "$ref": "#/components/schemas/BulkChatMessageFailure"
7949 }
7950 },
7951 "succeeded": {
7952 "type": "array",
7953 "items": {
7954 "type": "string",
7955 "format": "uuid"
7956 }
7957 }
7958 }
7959 },
7960 "error": {
7961 "type": [
7962 "string",
7963 "null"
7964 ]
7965 },
7966 "error_code": {
7967 "type": [
7968 "string",
7969 "null"
7970 ]
7971 },
7972 "log_id": {
7973 "type": "string"
7974 },
7975 "message": {
7976 "type": "string"
7977 },
7978 "request_id": {
7979 "type": "string"
7980 }
7981 }
7982 },
7983 "AppResponseData_BulkTaskResp": {
7984 "type": "object",
7985 "required": [
7986 "code",
7987 "message",
7988 "request_id",
7989 "log_id"
7990 ],
7991 "properties": {
7992 "code": {
7993 "type": "integer",
7994 "format": "int32"
7995 },
7996 "data": {
7997 "type": "object",
7998 "required": [
7999 "source",
8000 "action",
8001 "succeeded",
8002 "failed"
8003 ],
8004 "properties": {
8005 "action": {
8006 "type": "string"
8007 },
8008 "failed": {
8009 "type": "array",
8010 "items": {
8011 "$ref": "#/components/schemas/BulkTaskFailure"
8012 }
8013 },
8014 "source": {
8015 "type": "string"
8016 },
8017 "succeeded": {
8018 "type": "array",
8019 "items": {
8020 "type": "string",
8021 "format": "uuid"
8022 }
8023 }
8024 }
8025 },
8026 "error": {
8027 "type": [
8028 "string",
8029 "null"
8030 ]
8031 },
8032 "error_code": {
8033 "type": [
8034 "string",
8035 "null"
8036 ]
8037 },
8038 "log_id": {
8039 "type": "string"
8040 },
8041 "message": {
8042 "type": "string"
8043 },
8044 "request_id": {
8045 "type": "string"
8046 }
8047 }
8048 },
8049 "AppResponseData_BulkUsersResp": {
8050 "type": "object",
8051 "required": [
8052 "code",
8053 "message",
8054 "request_id",
8055 "log_id"
8056 ],
8057 "properties": {
8058 "code": {
8059 "type": "integer",
8060 "format": "int32"
8061 },
8062 "data": {
8063 "type": "object",
8064 "required": [
8065 "action",
8066 "succeeded",
8067 "failed"
8068 ],
8069 "properties": {
8070 "action": {
8071 "type": "string"
8072 },
8073 "failed": {
8074 "type": "array",
8075 "items": {
8076 "$ref": "#/components/schemas/BulkUserFailure"
8077 }
8078 },
8079 "succeeded": {
8080 "type": "array",
8081 "items": {
8082 "type": "string",
8083 "format": "uuid"
8084 }
8085 }
8086 }
8087 },
8088 "error": {
8089 "type": [
8090 "string",
8091 "null"
8092 ]
8093 },
8094 "error_code": {
8095 "type": [
8096 "string",
8097 "null"
8098 ]
8099 },
8100 "log_id": {
8101 "type": "string"
8102 },
8103 "message": {
8104 "type": "string"
8105 },
8106 "request_id": {
8107 "type": "string"
8108 }
8109 }
8110 },
8111 "AppResponseData_CreateUserResp": {
8112 "type": "object",
8113 "required": [
8114 "code",
8115 "message",
8116 "request_id",
8117 "log_id"
8118 ],
8119 "properties": {
8120 "code": {
8121 "type": "integer",
8122 "format": "int32"
8123 },
8124 "data": {
8125 "type": "object",
8126 "required": [
8127 "uid",
8128 "username",
8129 "email"
8130 ],
8131 "properties": {
8132 "email": {
8133 "type": "string"
8134 },
8135 "uid": {
8136 "type": "string",
8137 "format": "uuid"
8138 },
8139 "username": {
8140 "type": "string"
8141 }
8142 }
8143 },
8144 "error": {
8145 "type": [
8146 "string",
8147 "null"
8148 ]
8149 },
8150 "error_code": {
8151 "type": [
8152 "string",
8153 "null"
8154 ]
8155 },
8156 "log_id": {
8157 "type": "string"
8158 },
8159 "message": {
8160 "type": "string"
8161 },
8162 "request_id": {
8163 "type": "string"
8164 }
8165 }
8166 },
8167 "AppResponseData_CreatedRunnerTokenDto": {
8168 "type": "object",
8169 "required": [
8170 "code",
8171 "message",
8172 "request_id",
8173 "log_id"
8174 ],
8175 "properties": {
8176 "code": {
8177 "type": "integer",
8178 "format": "int32"
8179 },
8180 "data": {
8181 "type": "object",
8182 "required": [
8183 "token",
8184 "plaintext"
8185 ],
8186 "properties": {
8187 "plaintext": {
8188 "type": "string"
8189 },
8190 "token": {
8191 "$ref": "#/components/schemas/RunnerTokenDto"
8192 }
8193 }
8194 },
8195 "error": {
8196 "type": [
8197 "string",
8198 "null"
8199 ]
8200 },
8201 "error_code": {
8202 "type": [
8203 "string",
8204 "null"
8205 ]
8206 },
8207 "log_id": {
8208 "type": "string"
8209 },
8210 "message": {
8211 "type": "string"
8212 },
8213 "request_id": {
8214 "type": "string"
8215 }
8216 }
8217 },
8218 "AppResponseData_DashboardDto": {
8219 "type": "object",
8220 "required": [
8221 "code",
8222 "message",
8223 "request_id",
8224 "log_id"
8225 ],
8226 "properties": {
8227 "code": {
8228 "type": "integer",
8229 "format": "int32"
8230 },
8231 "data": {
8232 "type": "object",
8233 "required": [
8234 "counts",
8235 "trend",
8236 "active_sessions",
8237 "restricted_users",
8238 "recent_audit",
8239 "recent_failures",
8240 "queues",
8241 "generated_at"
8242 ],
8243 "properties": {
8244 "active_sessions": {
8245 "type": "integer",
8246 "format": "int64"
8247 },
8248 "counts": {
8249 "$ref": "#/components/schemas/CountsDto"
8250 },
8251 "generated_at": {
8252 "type": "string",
8253 "format": "date-time"
8254 },
8255 "queues": {
8256 "type": "array",
8257 "items": {
8258 "$ref": "#/components/schemas/QueueSnapshotDto"
8259 }
8260 },
8261 "recent_audit": {
8262 "type": "array",
8263 "items": {
8264 "$ref": "#/components/schemas/AuditLogDto"
8265 }
8266 },
8267 "recent_failures": {
8268 "type": "array",
8269 "items": {
8270 "$ref": "#/components/schemas/JobLogDto"
8271 }
8272 },
8273 "restricted_users": {
8274 "type": "integer",
8275 "format": "int64"
8276 },
8277 "trend": {
8278 "type": "array",
8279 "items": {
8280 "$ref": "#/components/schemas/TrendPointDto"
8281 }
8282 }
8283 }
8284 },
8285 "error": {
8286 "type": [
8287 "string",
8288 "null"
8289 ]
8290 },
8291 "error_code": {
8292 "type": [
8293 "string",
8294 "null"
8295 ]
8296 },
8297 "log_id": {
8298 "type": "string"
8299 },
8300 "message": {
8301 "type": "string"
8302 },
8303 "request_id": {
8304 "type": "string"
8305 }
8306 }
8307 },
8308 "AppResponseData_DeleteChatAttachmentResp": {
8309 "type": "object",
8310 "required": [
8311 "code",
8312 "message",
8313 "request_id",
8314 "log_id"
8315 ],
8316 "properties": {
8317 "code": {
8318 "type": "integer",
8319 "format": "int32"
8320 },
8321 "data": {
8322 "type": "object",
8323 "required": [
8324 "id",
8325 "s3_key",
8326 "deleted",
8327 "storage_deleted"
8328 ],
8329 "properties": {
8330 "deleted": {
8331 "type": "boolean"
8332 },
8333 "id": {
8334 "type": "string",
8335 "format": "uuid"
8336 },
8337 "s3_key": {
8338 "type": "string"
8339 },
8340 "storage_deleted": {
8341 "type": "boolean"
8342 }
8343 }
8344 },
8345 "error": {
8346 "type": [
8347 "string",
8348 "null"
8349 ]
8350 },
8351 "error_code": {
8352 "type": [
8353 "string",
8354 "null"
8355 ]
8356 },
8357 "log_id": {
8358 "type": "string"
8359 },
8360 "message": {
8361 "type": "string"
8362 },
8363 "request_id": {
8364 "type": "string"
8365 }
8366 }
8367 },
8368 "AppResponseData_DeleteChatRoomResp": {
8369 "type": "object",
8370 "required": [
8371 "code",
8372 "message",
8373 "request_id",
8374 "log_id"
8375 ],
8376 "properties": {
8377 "code": {
8378 "type": "integer",
8379 "format": "int32"
8380 },
8381 "data": {
8382 "type": "object",
8383 "required": [
8384 "room_id",
8385 "messages_deleted",
8386 "members_deleted",
8387 "attachments_deleted",
8388 "threads_deleted"
8389 ],
8390 "properties": {
8391 "attachments_deleted": {
8392 "type": "integer",
8393 "format": "int64",
8394 "minimum": 0
8395 },
8396 "members_deleted": {
8397 "type": "integer",
8398 "format": "int64",
8399 "minimum": 0
8400 },
8401 "messages_deleted": {
8402 "type": "integer",
8403 "format": "int64",
8404 "minimum": 0
8405 },
8406 "room_id": {
8407 "type": "string",
8408 "format": "uuid"
8409 },
8410 "threads_deleted": {
8411 "type": "integer",
8412 "format": "int64",
8413 "minimum": 0
8414 }
8415 }
8416 },
8417 "error": {
8418 "type": [
8419 "string",
8420 "null"
8421 ]
8422 },
8423 "error_code": {
8424 "type": [
8425 "string",
8426 "null"
8427 ]
8428 },
8429 "log_id": {
8430 "type": "string"
8431 },
8432 "message": {
8433 "type": "string"
8434 },
8435 "request_id": {
8436 "type": "string"
8437 }
8438 }
8439 },
8440 "AppResponseData_DeleteObjectResp": {
8441 "type": "object",
8442 "required": [
8443 "code",
8444 "message",
8445 "request_id",
8446 "log_id"
8447 ],
8448 "properties": {
8449 "code": {
8450 "type": "integer",
8451 "format": "int32"
8452 },
8453 "data": {
8454 "type": "object",
8455 "required": [
8456 "key",
8457 "deleted"
8458 ],
8459 "properties": {
8460 "deleted": {
8461 "type": "boolean"
8462 },
8463 "key": {
8464 "type": "string"
8465 }
8466 }
8467 },
8468 "error": {
8469 "type": [
8470 "string",
8471 "null"
8472 ]
8473 },
8474 "error_code": {
8475 "type": [
8476 "string",
8477 "null"
8478 ]
8479 },
8480 "log_id": {
8481 "type": "string"
8482 },
8483 "message": {
8484 "type": "string"
8485 },
8486 "request_id": {
8487 "type": "string"
8488 }
8489 }
8490 },
8491 "AppResponseData_DeleteUserResp": {
8492 "type": "object",
8493 "required": [
8494 "code",
8495 "message",
8496 "request_id",
8497 "log_id"
8498 ],
8499 "properties": {
8500 "code": {
8501 "type": "integer",
8502 "format": "int32"
8503 },
8504 "data": {
8505 "type": "object",
8506 "required": [
8507 "user_id",
8508 "anonymized"
8509 ],
8510 "properties": {
8511 "anonymized": {
8512 "type": "boolean"
8513 },
8514 "user_id": {
8515 "type": "string",
8516 "format": "uuid"
8517 }
8518 }
8519 },
8520 "error": {
8521 "type": [
8522 "string",
8523 "null"
8524 ]
8525 },
8526 "error_code": {
8527 "type": [
8528 "string",
8529 "null"
8530 ]
8531 },
8532 "log_id": {
8533 "type": "string"
8534 },
8535 "message": {
8536 "type": "string"
8537 },
8538 "request_id": {
8539 "type": "string"
8540 }
8541 }
8542 },
8543 "AppResponseData_DownloadUrlDto": {
8544 "type": "object",
8545 "required": [
8546 "code",
8547 "message",
8548 "request_id",
8549 "log_id"
8550 ],
8551 "properties": {
8552 "code": {
8553 "type": "integer",
8554 "format": "int32"
8555 },
8556 "data": {
8557 "type": "object",
8558 "required": [
8559 "key",
8560 "url",
8561 "expires_in_secs"
8562 ],
8563 "properties": {
8564 "expires_in_secs": {
8565 "type": "integer",
8566 "format": "int32",
8567 "minimum": 0
8568 },
8569 "key": {
8570 "type": "string"
8571 },
8572 "url": {
8573 "type": "string"
8574 }
8575 }
8576 },
8577 "error": {
8578 "type": [
8579 "string",
8580 "null"
8581 ]
8582 },
8583 "error_code": {
8584 "type": [
8585 "string",
8586 "null"
8587 ]
8588 },
8589 "log_id": {
8590 "type": "string"
8591 },
8592 "message": {
8593 "type": "string"
8594 },
8595 "request_id": {
8596 "type": "string"
8597 }
8598 }
8599 },
8600 "AppResponseData_EnvironmentAdminDto": {
8601 "type": "object",
8602 "required": [
8603 "code",
8604 "message",
8605 "request_id",
8606 "log_id"
8607 ],
8608 "properties": {
8609 "code": {
8610 "type": "integer",
8611 "format": "int32"
8612 },
8613 "data": {
8614 "type": "object",
8615 "required": [
8616 "id",
8617 "space_id",
8618 "name",
8619 "required_labels",
8620 "secrets",
8621 "created_at",
8622 "updated_at"
8623 ],
8624 "properties": {
8625 "created_at": {
8626 "type": "string",
8627 "format": "date-time"
8628 },
8629 "description": {
8630 "type": [
8631 "string",
8632 "null"
8633 ]
8634 },
8635 "id": {
8636 "type": "string",
8637 "format": "uuid"
8638 },
8639 "name": {
8640 "type": "string"
8641 },
8642 "required_labels": {
8643 "type": "array",
8644 "items": {
8645 "type": "string"
8646 }
8647 },
8648 "secrets": {
8649 "type": "array",
8650 "items": {
8651 "type": "string"
8652 }
8653 },
8654 "space_id": {
8655 "type": "string",
8656 "format": "uuid"
8657 },
8658 "updated_at": {
8659 "type": "string",
8660 "format": "date-time"
8661 }
8662 }
8663 },
8664 "error": {
8665 "type": [
8666 "string",
8667 "null"
8668 ]
8669 },
8670 "error_code": {
8671 "type": [
8672 "string",
8673 "null"
8674 ]
8675 },
8676 "log_id": {
8677 "type": "string"
8678 },
8679 "message": {
8680 "type": "string"
8681 },
8682 "request_id": {
8683 "type": "string"
8684 }
8685 }
8686 },
8687 "AppResponseData_JobLogPageDto": {
8688 "type": "object",
8689 "required": [
8690 "code",
8691 "message",
8692 "request_id",
8693 "log_id"
8694 ],
8695 "properties": {
8696 "code": {
8697 "type": "integer",
8698 "format": "int32"
8699 },
8700 "data": {
8701 "type": "object",
8702 "required": [
8703 "items",
8704 "total",
8705 "limit",
8706 "offset"
8707 ],
8708 "properties": {
8709 "items": {
8710 "type": "array",
8711 "items": {
8712 "$ref": "#/components/schemas/JobLogDto"
8713 }
8714 },
8715 "limit": {
8716 "type": "integer",
8717 "format": "int64",
8718 "minimum": 0
8719 },
8720 "offset": {
8721 "type": "integer",
8722 "format": "int64",
8723 "minimum": 0
8724 },
8725 "total": {
8726 "type": "integer",
8727 "format": "int64"
8728 }
8729 }
8730 },
8731 "error": {
8732 "type": [
8733 "string",
8734 "null"
8735 ]
8736 },
8737 "error_code": {
8738 "type": [
8739 "string",
8740 "null"
8741 ]
8742 },
8743 "log_id": {
8744 "type": "string"
8745 },
8746 "message": {
8747 "type": "string"
8748 },
8749 "request_id": {
8750 "type": "string"
8751 }
8752 }
8753 },
8754 "AppResponseData_NoData": {
8755 "type": "object",
8756 "required": [
8757 "code",
8758 "message",
8759 "request_id",
8760 "log_id"
8761 ],
8762 "properties": {
8763 "code": {
8764 "type": "integer",
8765 "format": "int32"
8766 },
8767 "data": {
8768 "default": null
8769 },
8770 "error": {
8771 "type": [
8772 "string",
8773 "null"
8774 ]
8775 },
8776 "error_code": {
8777 "type": [
8778 "string",
8779 "null"
8780 ]
8781 },
8782 "log_id": {
8783 "type": "string"
8784 },
8785 "message": {
8786 "type": "string"
8787 },
8788 "request_id": {
8789 "type": "string"
8790 }
8791 }
8792 },
8793 "AppResponseData_Page_AdminChatAttachmentDto": {
8794 "type": "object",
8795 "required": [
8796 "code",
8797 "message",
8798 "request_id",
8799 "log_id"
8800 ],
8801 "properties": {
8802 "code": {
8803 "type": "integer",
8804 "format": "int32"
8805 },
8806 "data": {
8807 "type": "object",
8808 "required": [
8809 "items",
8810 "total",
8811 "limit",
8812 "offset"
8813 ],
8814 "properties": {
8815 "items": {
8816 "type": "array",
8817 "items": {
8818 "type": "object",
8819 "required": [
8820 "id",
8821 "user_id",
8822 "filename",
8823 "content_type",
8824 "size",
8825 "bucket",
8826 "s3_key",
8827 "status",
8828 "created_at"
8829 ],
8830 "properties": {
8831 "bucket": {
8832 "type": "string"
8833 },
8834 "content_type": {
8835 "type": "string"
8836 },
8837 "created_at": {
8838 "type": "string",
8839 "format": "date-time"
8840 },
8841 "description": {
8842 "type": [
8843 "string",
8844 "null"
8845 ]
8846 },
8847 "display_name": {
8848 "type": [
8849 "string",
8850 "null"
8851 ]
8852 },
8853 "filename": {
8854 "type": "string"
8855 },
8856 "id": {
8857 "type": "string",
8858 "format": "uuid"
8859 },
8860 "message_id": {
8861 "type": [
8862 "string",
8863 "null"
8864 ],
8865 "format": "uuid"
8866 },
8867 "room_id": {
8868 "type": [
8869 "string",
8870 "null"
8871 ],
8872 "format": "uuid"
8873 },
8874 "s3_key": {
8875 "type": "string"
8876 },
8877 "size": {
8878 "type": "integer",
8879 "format": "int64"
8880 },
8881 "status": {
8882 "type": "string"
8883 },
8884 "user_id": {
8885 "type": "string",
8886 "format": "uuid"
8887 },
8888 "username": {
8889 "type": [
8890 "string",
8891 "null"
8892 ]
8893 }
8894 }
8895 }
8896 },
8897 "limit": {
8898 "type": "integer",
8899 "format": "int64",
8900 "minimum": 0
8901 },
8902 "offset": {
8903 "type": "integer",
8904 "format": "int64",
8905 "minimum": 0
8906 },
8907 "total": {
8908 "type": "integer",
8909 "format": "int64"
8910 }
8911 }
8912 },
8913 "error": {
8914 "type": [
8915 "string",
8916 "null"
8917 ]
8918 },
8919 "error_code": {
8920 "type": [
8921 "string",
8922 "null"
8923 ]
8924 },
8925 "log_id": {
8926 "type": "string"
8927 },
8928 "message": {
8929 "type": "string"
8930 },
8931 "request_id": {
8932 "type": "string"
8933 }
8934 }
8935 },
8936 "AppResponseData_Page_AdminChatMessageDto": {
8937 "type": "object",
8938 "required": [
8939 "code",
8940 "message",
8941 "request_id",
8942 "log_id"
8943 ],
8944 "properties": {
8945 "code": {
8946 "type": "integer",
8947 "format": "int32"
8948 },
8949 "data": {
8950 "type": "object",
8951 "required": [
8952 "items",
8953 "total",
8954 "limit",
8955 "offset"
8956 ],
8957 "properties": {
8958 "items": {
8959 "type": "array",
8960 "items": {
8961 "type": "object",
8962 "required": [
8963 "id",
8964 "room_id",
8965 "author_id",
8966 "author_type",
8967 "message_type",
8968 "content",
8969 "seq",
8970 "flags",
8971 "pinned",
8972 "attachment_count",
8973 "reaction_count",
8974 "created_at"
8975 ],
8976 "properties": {
8977 "attachment_count": {
8978 "type": "integer",
8979 "format": "int64"
8980 },
8981 "author_display_name": {
8982 "type": [
8983 "string",
8984 "null"
8985 ]
8986 },
8987 "author_id": {
8988 "type": "string",
8989 "format": "uuid"
8990 },
8991 "author_type": {
8992 "type": "string"
8993 },
8994 "author_username": {
8995 "type": [
8996 "string",
8997 "null"
8998 ]
8999 },
9000 "content": {
9001 "type": "string"
9002 },
9003 "created_at": {
9004 "type": "string",
9005 "format": "date-time"
9006 },
9007 "deleted_at": {
9008 "type": [
9009 "string",
9010 "null"
9011 ],
9012 "format": "date-time"
9013 },
9014 "edited_at": {
9015 "type": [
9016 "string",
9017 "null"
9018 ],
9019 "format": "date-time"
9020 },
9021 "flags": {
9022 "type": "integer",
9023 "format": "int64"
9024 },
9025 "id": {
9026 "type": "string",
9027 "format": "uuid"
9028 },
9029 "message_type": {
9030 "type": "string"
9031 },
9032 "pinned": {
9033 "type": "boolean"
9034 },
9035 "reaction_count": {
9036 "type": "integer",
9037 "format": "int64"
9038 },
9039 "reply_to_id": {
9040 "type": [
9041 "string",
9042 "null"
9043 ],
9044 "format": "uuid"
9045 },
9046 "room_id": {
9047 "type": "string",
9048 "format": "uuid"
9049 },
9050 "room_name": {
9051 "type": [
9052 "string",
9053 "null"
9054 ]
9055 },
9056 "seq": {
9057 "type": "integer",
9058 "format": "int64"
9059 },
9060 "thread_id": {
9061 "type": [
9062 "string",
9063 "null"
9064 ],
9065 "format": "uuid"
9066 }
9067 }
9068 }
9069 },
9070 "limit": {
9071 "type": "integer",
9072 "format": "int64",
9073 "minimum": 0
9074 },
9075 "offset": {
9076 "type": "integer",
9077 "format": "int64",
9078 "minimum": 0
9079 },
9080 "total": {
9081 "type": "integer",
9082 "format": "int64"
9083 }
9084 }
9085 },
9086 "error": {
9087 "type": [
9088 "string",
9089 "null"
9090 ]
9091 },
9092 "error_code": {
9093 "type": [
9094 "string",
9095 "null"
9096 ]
9097 },
9098 "log_id": {
9099 "type": "string"
9100 },
9101 "message": {
9102 "type": "string"
9103 },
9104 "request_id": {
9105 "type": "string"
9106 }
9107 }
9108 },
9109 "AppResponseData_Page_AdminChatRoomMemberDto": {
9110 "type": "object",
9111 "required": [
9112 "code",
9113 "message",
9114 "request_id",
9115 "log_id"
9116 ],
9117 "properties": {
9118 "code": {
9119 "type": "integer",
9120 "format": "int32"
9121 },
9122 "data": {
9123 "type": "object",
9124 "required": [
9125 "items",
9126 "total",
9127 "limit",
9128 "offset"
9129 ],
9130 "properties": {
9131 "items": {
9132 "type": "array",
9133 "items": {
9134 "type": "object",
9135 "required": [
9136 "user_id",
9137 "role",
9138 "joined_at"
9139 ],
9140 "properties": {
9141 "display_name": {
9142 "type": [
9143 "string",
9144 "null"
9145 ]
9146 },
9147 "joined_at": {
9148 "type": "string",
9149 "format": "date-time"
9150 },
9151 "left_at": {
9152 "type": [
9153 "string",
9154 "null"
9155 ],
9156 "format": "date-time"
9157 },
9158 "nick": {
9159 "type": [
9160 "string",
9161 "null"
9162 ]
9163 },
9164 "role": {
9165 "type": "string"
9166 },
9167 "user_id": {
9168 "type": "string",
9169 "format": "uuid"
9170 },
9171 "username": {
9172 "type": [
9173 "string",
9174 "null"
9175 ]
9176 }
9177 }
9178 }
9179 },
9180 "limit": {
9181 "type": "integer",
9182 "format": "int64",
9183 "minimum": 0
9184 },
9185 "offset": {
9186 "type": "integer",
9187 "format": "int64",
9188 "minimum": 0
9189 },
9190 "total": {
9191 "type": "integer",
9192 "format": "int64"
9193 }
9194 }
9195 },
9196 "error": {
9197 "type": [
9198 "string",
9199 "null"
9200 ]
9201 },
9202 "error_code": {
9203 "type": [
9204 "string",
9205 "null"
9206 ]
9207 },
9208 "log_id": {
9209 "type": "string"
9210 },
9211 "message": {
9212 "type": "string"
9213 },
9214 "request_id": {
9215 "type": "string"
9216 }
9217 }
9218 },
9219 "AppResponseData_Page_AdminChatRoomSummary": {
9220 "type": "object",
9221 "required": [
9222 "code",
9223 "message",
9224 "request_id",
9225 "log_id"
9226 ],
9227 "properties": {
9228 "code": {
9229 "type": "integer",
9230 "format": "int32"
9231 },
9232 "data": {
9233 "type": "object",
9234 "required": [
9235 "items",
9236 "total",
9237 "limit",
9238 "offset"
9239 ],
9240 "properties": {
9241 "items": {
9242 "type": "array",
9243 "items": {
9244 "type": "object",
9245 "required": [
9246 "id",
9247 "kind",
9248 "name",
9249 "lower_name",
9250 "owner_id",
9251 "is_private",
9252 "nsfw",
9253 "is_active",
9254 "members",
9255 "messages",
9256 "created_at",
9257 "updated_at"
9258 ],
9259 "properties": {
9260 "created_at": {
9261 "type": "string",
9262 "format": "date-time"
9263 },
9264 "id": {
9265 "type": "string",
9266 "format": "uuid"
9267 },
9268 "is_active": {
9269 "type": "boolean"
9270 },
9271 "is_private": {
9272 "type": "boolean"
9273 },
9274 "kind": {
9275 "type": "string"
9276 },
9277 "last_message_at": {
9278 "type": [
9279 "string",
9280 "null"
9281 ],
9282 "format": "date-time"
9283 },
9284 "lower_name": {
9285 "type": "string"
9286 },
9287 "members": {
9288 "type": "integer",
9289 "format": "int64"
9290 },
9291 "messages": {
9292 "type": "integer",
9293 "format": "int64"
9294 },
9295 "name": {
9296 "type": "string"
9297 },
9298 "nsfw": {
9299 "type": "boolean"
9300 },
9301 "owner_id": {
9302 "type": "string",
9303 "format": "uuid"
9304 },
9305 "owner_username": {
9306 "type": [
9307 "string",
9308 "null"
9309 ]
9310 },
9311 "space_id": {
9312 "type": [
9313 "string",
9314 "null"
9315 ],
9316 "format": "uuid"
9317 },
9318 "space_name": {
9319 "type": [
9320 "string",
9321 "null"
9322 ]
9323 },
9324 "topic": {
9325 "type": [
9326 "string",
9327 "null"
9328 ]
9329 },
9330 "updated_at": {
9331 "type": "string",
9332 "format": "date-time"
9333 }
9334 }
9335 }
9336 },
9337 "limit": {
9338 "type": "integer",
9339 "format": "int64",
9340 "minimum": 0
9341 },
9342 "offset": {
9343 "type": "integer",
9344 "format": "int64",
9345 "minimum": 0
9346 },
9347 "total": {
9348 "type": "integer",
9349 "format": "int64"
9350 }
9351 }
9352 },
9353 "error": {
9354 "type": [
9355 "string",
9356 "null"
9357 ]
9358 },
9359 "error_code": {
9360 "type": [
9361 "string",
9362 "null"
9363 ]
9364 },
9365 "log_id": {
9366 "type": "string"
9367 },
9368 "message": {
9369 "type": "string"
9370 },
9371 "request_id": {
9372 "type": "string"
9373 }
9374 }
9375 },
9376 "AppResponseData_Page_AdminDto": {
9377 "type": "object",
9378 "required": [
9379 "code",
9380 "message",
9381 "request_id",
9382 "log_id"
9383 ],
9384 "properties": {
9385 "code": {
9386 "type": "integer",
9387 "format": "int32"
9388 },
9389 "data": {
9390 "type": "object",
9391 "required": [
9392 "items",
9393 "total",
9394 "limit",
9395 "offset"
9396 ],
9397 "properties": {
9398 "items": {
9399 "type": "array",
9400 "items": {
9401 "type": "object",
9402 "required": [
9403 "id",
9404 "username",
9405 "display_name",
9406 "role",
9407 "is_active",
9408 "must_change_password",
9409 "created_at",
9410 "updated_at"
9411 ],
9412 "properties": {
9413 "created_at": {
9414 "type": "string",
9415 "format": "date-time"
9416 },
9417 "display_name": {
9418 "type": "string"
9419 },
9420 "id": {
9421 "type": "string",
9422 "format": "uuid"
9423 },
9424 "is_active": {
9425 "type": "boolean"
9426 },
9427 "last_login_at": {
9428 "type": [
9429 "string",
9430 "null"
9431 ],
9432 "format": "date-time"
9433 },
9434 "last_login_ip": {
9435 "type": [
9436 "string",
9437 "null"
9438 ]
9439 },
9440 "must_change_password": {
9441 "type": "boolean"
9442 },
9443 "role": {
9444 "$ref": "#/components/schemas/AdminRole"
9445 },
9446 "updated_at": {
9447 "type": "string",
9448 "format": "date-time"
9449 },
9450 "username": {
9451 "type": "string"
9452 }
9453 }
9454 }
9455 },
9456 "limit": {
9457 "type": "integer",
9458 "format": "int64",
9459 "minimum": 0
9460 },
9461 "offset": {
9462 "type": "integer",
9463 "format": "int64",
9464 "minimum": 0
9465 },
9466 "total": {
9467 "type": "integer",
9468 "format": "int64"
9469 }
9470 }
9471 },
9472 "error": {
9473 "type": [
9474 "string",
9475 "null"
9476 ]
9477 },
9478 "error_code": {
9479 "type": [
9480 "string",
9481 "null"
9482 ]
9483 },
9484 "log_id": {
9485 "type": "string"
9486 },
9487 "message": {
9488 "type": "string"
9489 },
9490 "request_id": {
9491 "type": "string"
9492 }
9493 }
9494 },
9495 "AppResponseData_Page_AdminSpaceMemberDto": {
9496 "type": "object",
9497 "required": [
9498 "code",
9499 "message",
9500 "request_id",
9501 "log_id"
9502 ],
9503 "properties": {
9504 "code": {
9505 "type": "integer",
9506 "format": "int32"
9507 },
9508 "data": {
9509 "type": "object",
9510 "required": [
9511 "items",
9512 "total",
9513 "limit",
9514 "offset"
9515 ],
9516 "properties": {
9517 "items": {
9518 "type": "array",
9519 "items": {
9520 "type": "object",
9521 "required": [
9522 "user_id",
9523 "status",
9524 "joined_at"
9525 ],
9526 "properties": {
9527 "alias_name": {
9528 "type": [
9529 "string",
9530 "null"
9531 ]
9532 },
9533 "display_name": {
9534 "type": [
9535 "string",
9536 "null"
9537 ]
9538 },
9539 "joined_at": {
9540 "type": "string",
9541 "format": "date-time"
9542 },
9543 "last_active_at": {
9544 "type": [
9545 "string",
9546 "null"
9547 ],
9548 "format": "date-time"
9549 },
9550 "status": {
9551 "type": "string"
9552 },
9553 "user_id": {
9554 "type": "string",
9555 "format": "uuid"
9556 },
9557 "username": {
9558 "type": [
9559 "string",
9560 "null"
9561 ]
9562 }
9563 }
9564 }
9565 },
9566 "limit": {
9567 "type": "integer",
9568 "format": "int64",
9569 "minimum": 0
9570 },
9571 "offset": {
9572 "type": "integer",
9573 "format": "int64",
9574 "minimum": 0
9575 },
9576 "total": {
9577 "type": "integer",
9578 "format": "int64"
9579 }
9580 }
9581 },
9582 "error": {
9583 "type": [
9584 "string",
9585 "null"
9586 ]
9587 },
9588 "error_code": {
9589 "type": [
9590 "string",
9591 "null"
9592 ]
9593 },
9594 "log_id": {
9595 "type": "string"
9596 },
9597 "message": {
9598 "type": "string"
9599 },
9600 "request_id": {
9601 "type": "string"
9602 }
9603 }
9604 },
9605 "AppResponseData_Page_AdminSpaceSummary": {
9606 "type": "object",
9607 "required": [
9608 "code",
9609 "message",
9610 "request_id",
9611 "log_id"
9612 ],
9613 "properties": {
9614 "code": {
9615 "type": "integer",
9616 "format": "int32"
9617 },
9618 "data": {
9619 "type": "object",
9620 "required": [
9621 "items",
9622 "total",
9623 "limit",
9624 "offset"
9625 ],
9626 "properties": {
9627 "items": {
9628 "type": "array",
9629 "items": {
9630 "type": "object",
9631 "required": [
9632 "id",
9633 "name",
9634 "lower_name",
9635 "owner_id",
9636 "is_private",
9637 "is_active",
9638 "allow_join",
9639 "allow_invite",
9640 "member_must_enable_2fa",
9641 "nums_members",
9642 "created_at",
9643 "updated_at"
9644 ],
9645 "properties": {
9646 "allow_invite": {
9647 "type": "boolean"
9648 },
9649 "allow_join": {
9650 "type": "boolean"
9651 },
9652 "created_at": {
9653 "type": "string",
9654 "format": "date-time"
9655 },
9656 "description": {
9657 "type": [
9658 "string",
9659 "null"
9660 ]
9661 },
9662 "id": {
9663 "type": "string",
9664 "format": "uuid"
9665 },
9666 "is_active": {
9667 "type": "boolean"
9668 },
9669 "is_private": {
9670 "type": "boolean"
9671 },
9672 "lower_name": {
9673 "type": "string"
9674 },
9675 "member_must_enable_2fa": {
9676 "type": "boolean"
9677 },
9678 "name": {
9679 "type": "string"
9680 },
9681 "nums_members": {
9682 "type": "integer",
9683 "format": "int64"
9684 },
9685 "owner_id": {
9686 "type": "string",
9687 "format": "uuid"
9688 },
9689 "owner_username": {
9690 "type": [
9691 "string",
9692 "null"
9693 ]
9694 },
9695 "updated_at": {
9696 "type": "string",
9697 "format": "date-time"
9698 }
9699 }
9700 }
9701 },
9702 "limit": {
9703 "type": "integer",
9704 "format": "int64",
9705 "minimum": 0
9706 },
9707 "offset": {
9708 "type": "integer",
9709 "format": "int64",
9710 "minimum": 0
9711 },
9712 "total": {
9713 "type": "integer",
9714 "format": "int64"
9715 }
9716 }
9717 },
9718 "error": {
9719 "type": [
9720 "string",
9721 "null"
9722 ]
9723 },
9724 "error_code": {
9725 "type": [
9726 "string",
9727 "null"
9728 ]
9729 },
9730 "log_id": {
9731 "type": "string"
9732 },
9733 "message": {
9734 "type": "string"
9735 },
9736 "request_id": {
9737 "type": "string"
9738 }
9739 }
9740 },
9741 "AppResponseData_Page_AdminUserSummary": {
9742 "type": "object",
9743 "required": [
9744 "code",
9745 "message",
9746 "request_id",
9747 "log_id"
9748 ],
9749 "properties": {
9750 "code": {
9751 "type": "integer",
9752 "format": "int32"
9753 },
9754 "data": {
9755 "type": "object",
9756 "required": [
9757 "items",
9758 "total",
9759 "limit",
9760 "offset"
9761 ],
9762 "properties": {
9763 "items": {
9764 "type": "array",
9765 "items": {
9766 "type": "object",
9767 "required": [
9768 "uid",
9769 "username",
9770 "display_name",
9771 "avatar_url",
9772 "is_active",
9773 "is_restricted",
9774 "prohibit_login",
9775 "bot",
9776 "system",
9777 "suspended",
9778 "created_at"
9779 ],
9780 "properties": {
9781 "avatar_url": {
9782 "type": "string"
9783 },
9784 "bot": {
9785 "type": "boolean"
9786 },
9787 "created_at": {
9788 "type": "string",
9789 "format": "date-time"
9790 },
9791 "display_name": {
9792 "type": "string"
9793 },
9794 "is_active": {
9795 "type": "boolean"
9796 },
9797 "is_restricted": {
9798 "type": "boolean"
9799 },
9800 "last_login_at": {
9801 "type": [
9802 "string",
9803 "null"
9804 ],
9805 "format": "date-time"
9806 },
9807 "prohibit_login": {
9808 "type": "boolean"
9809 },
9810 "suspended": {
9811 "type": "boolean"
9812 },
9813 "system": {
9814 "type": "boolean"
9815 },
9816 "uid": {
9817 "type": "string",
9818 "format": "uuid"
9819 },
9820 "username": {
9821 "type": "string"
9822 }
9823 }
9824 }
9825 },
9826 "limit": {
9827 "type": "integer",
9828 "format": "int64",
9829 "minimum": 0
9830 },
9831 "offset": {
9832 "type": "integer",
9833 "format": "int64",
9834 "minimum": 0
9835 },
9836 "total": {
9837 "type": "integer",
9838 "format": "int64"
9839 }
9840 }
9841 },
9842 "error": {
9843 "type": [
9844 "string",
9845 "null"
9846 ]
9847 },
9848 "error_code": {
9849 "type": [
9850 "string",
9851 "null"
9852 ]
9853 },
9854 "log_id": {
9855 "type": "string"
9856 },
9857 "message": {
9858 "type": "string"
9859 },
9860 "request_id": {
9861 "type": "string"
9862 }
9863 }
9864 },
9865 "AppResponseData_PurgeChatMessagesResp": {
9866 "type": "object",
9867 "required": [
9868 "code",
9869 "message",
9870 "request_id",
9871 "log_id"
9872 ],
9873 "properties": {
9874 "code": {
9875 "type": "integer",
9876 "format": "int32"
9877 },
9878 "data": {
9879 "type": "object",
9880 "required": [
9881 "room_id",
9882 "messages_deleted",
9883 "attachments_deleted",
9884 "threads_deleted"
9885 ],
9886 "properties": {
9887 "attachments_deleted": {
9888 "type": "integer",
9889 "format": "int64",
9890 "minimum": 0
9891 },
9892 "messages_deleted": {
9893 "type": "integer",
9894 "format": "int64",
9895 "minimum": 0
9896 },
9897 "room_id": {
9898 "type": "string",
9899 "format": "uuid"
9900 },
9901 "threads_deleted": {
9902 "type": "integer",
9903 "format": "int64",
9904 "minimum": 0
9905 }
9906 }
9907 },
9908 "error": {
9909 "type": [
9910 "string",
9911 "null"
9912 ]
9913 },
9914 "error_code": {
9915 "type": [
9916 "string",
9917 "null"
9918 ]
9919 },
9920 "log_id": {
9921 "type": "string"
9922 },
9923 "message": {
9924 "type": "string"
9925 },
9926 "request_id": {
9927 "type": "string"
9928 }
9929 }
9930 },
9931 "AppResponseData_QueueActionResp": {
9932 "type": "object",
9933 "required": [
9934 "code",
9935 "message",
9936 "request_id",
9937 "log_id"
9938 ],
9939 "properties": {
9940 "code": {
9941 "type": "integer",
9942 "format": "int32"
9943 },
9944 "data": {
9945 "type": "object",
9946 "required": [
9947 "queue",
9948 "affected"
9949 ],
9950 "properties": {
9951 "affected": {
9952 "type": "integer",
9953 "format": "int64",
9954 "minimum": 0
9955 },
9956 "queue": {
9957 "type": "string"
9958 }
9959 }
9960 },
9961 "error": {
9962 "type": [
9963 "string",
9964 "null"
9965 ]
9966 },
9967 "error_code": {
9968 "type": [
9969 "string",
9970 "null"
9971 ]
9972 },
9973 "log_id": {
9974 "type": "string"
9975 },
9976 "message": {
9977 "type": "string"
9978 },
9979 "request_id": {
9980 "type": "string"
9981 }
9982 }
9983 },
9984 "AppResponseData_RemoveChatMemberResp": {
9985 "type": "object",
9986 "required": [
9987 "code",
9988 "message",
9989 "request_id",
9990 "log_id"
9991 ],
9992 "properties": {
9993 "code": {
9994 "type": "integer",
9995 "format": "int32"
9996 },
9997 "data": {
9998 "type": "object",
9999 "required": [
10000 "room_id",
10001 "user_id",
10002 "removed"
10003 ],
10004 "properties": {
10005 "removed": {
10006 "type": "boolean"
10007 },
10008 "room_id": {
10009 "type": "string",
10010 "format": "uuid"
10011 },
10012 "user_id": {
10013 "type": "string",
10014 "format": "uuid"
10015 }
10016 }
10017 },
10018 "error": {
10019 "type": [
10020 "string",
10021 "null"
10022 ]
10023 },
10024 "error_code": {
10025 "type": [
10026 "string",
10027 "null"
10028 ]
10029 },
10030 "log_id": {
10031 "type": "string"
10032 },
10033 "message": {
10034 "type": "string"
10035 },
10036 "request_id": {
10037 "type": "string"
10038 }
10039 }
10040 },
10041 "AppResponseData_RemoveMemberResp": {
10042 "type": "object",
10043 "required": [
10044 "code",
10045 "message",
10046 "request_id",
10047 "log_id"
10048 ],
10049 "properties": {
10050 "code": {
10051 "type": "integer",
10052 "format": "int32"
10053 },
10054 "data": {
10055 "type": "object",
10056 "required": [
10057 "space_id",
10058 "user_id",
10059 "removed"
10060 ],
10061 "properties": {
10062 "removed": {
10063 "type": "boolean"
10064 },
10065 "space_id": {
10066 "type": "string",
10067 "format": "uuid"
10068 },
10069 "user_id": {
10070 "type": "string",
10071 "format": "uuid"
10072 }
10073 }
10074 },
10075 "error": {
10076 "type": [
10077 "string",
10078 "null"
10079 ]
10080 },
10081 "error_code": {
10082 "type": [
10083 "string",
10084 "null"
10085 ]
10086 },
10087 "log_id": {
10088 "type": "string"
10089 },
10090 "message": {
10091 "type": "string"
10092 },
10093 "request_id": {
10094 "type": "string"
10095 }
10096 }
10097 },
10098 "AppResponseData_RevokeSessionsResp": {
10099 "type": "object",
10100 "required": [
10101 "code",
10102 "message",
10103 "request_id",
10104 "log_id"
10105 ],
10106 "properties": {
10107 "code": {
10108 "type": "integer",
10109 "format": "int32"
10110 },
10111 "data": {
10112 "type": "object",
10113 "required": [
10114 "revoked"
10115 ],
10116 "properties": {
10117 "revoked": {
10118 "type": "integer",
10119 "format": "int64",
10120 "minimum": 0
10121 }
10122 }
10123 },
10124 "error": {
10125 "type": [
10126 "string",
10127 "null"
10128 ]
10129 },
10130 "error_code": {
10131 "type": [
10132 "string",
10133 "null"
10134 ]
10135 },
10136 "log_id": {
10137 "type": "string"
10138 },
10139 "message": {
10140 "type": "string"
10141 },
10142 "request_id": {
10143 "type": "string"
10144 }
10145 }
10146 },
10147 "AppResponseData_RunnerDto": {
10148 "type": "object",
10149 "required": [
10150 "code",
10151 "message",
10152 "request_id",
10153 "log_id"
10154 ],
10155 "properties": {
10156 "code": {
10157 "type": "integer",
10158 "format": "int32"
10159 },
10160 "data": {
10161 "type": "object",
10162 "required": [
10163 "id",
10164 "name",
10165 "labels",
10166 "capacity",
10167 "capabilities",
10168 "state",
10169 "active_jobs",
10170 "created_at"
10171 ],
10172 "properties": {
10173 "active_jobs": {
10174 "type": "integer",
10175 "format": "int64"
10176 },
10177 "capabilities": {
10178 "type": "array",
10179 "items": {
10180 "type": "string"
10181 }
10182 },
10183 "capacity": {
10184 "type": "integer",
10185 "format": "int32"
10186 },
10187 "created_at": {
10188 "type": "string",
10189 "format": "date-time"
10190 },
10191 "id": {
10192 "type": "string",
10193 "format": "uuid"
10194 },
10195 "labels": {
10196 "type": "array",
10197 "items": {
10198 "type": "string"
10199 }
10200 },
10201 "last_seen_at": {
10202 "type": [
10203 "string",
10204 "null"
10205 ],
10206 "format": "date-time"
10207 },
10208 "name": {
10209 "type": "string"
10210 },
10211 "protocol_version": {
10212 "type": [
10213 "integer",
10214 "null"
10215 ],
10216 "format": "int32"
10217 },
10218 "space_id": {
10219 "type": [
10220 "string",
10221 "null"
10222 ],
10223 "format": "uuid"
10224 },
10225 "state": {
10226 "type": "string"
10227 },
10228 "version": {
10229 "type": [
10230 "string",
10231 "null"
10232 ]
10233 }
10234 }
10235 },
10236 "error": {
10237 "type": [
10238 "string",
10239 "null"
10240 ]
10241 },
10242 "error_code": {
10243 "type": [
10244 "string",
10245 "null"
10246 ]
10247 },
10248 "log_id": {
10249 "type": "string"
10250 },
10251 "message": {
10252 "type": "string"
10253 },
10254 "request_id": {
10255 "type": "string"
10256 }
10257 }
10258 },
10259 "AppResponseData_RunnerPageDto": {
10260 "type": "object",
10261 "required": [
10262 "code",
10263 "message",
10264 "request_id",
10265 "log_id"
10266 ],
10267 "properties": {
10268 "code": {
10269 "type": "integer",
10270 "format": "int32"
10271 },
10272 "data": {
10273 "type": "object",
10274 "required": [
10275 "items",
10276 "total",
10277 "limit",
10278 "offset"
10279 ],
10280 "properties": {
10281 "items": {
10282 "type": "array",
10283 "items": {
10284 "$ref": "#/components/schemas/RunnerDto"
10285 }
10286 },
10287 "limit": {
10288 "type": "integer",
10289 "format": "int64",
10290 "minimum": 0
10291 },
10292 "offset": {
10293 "type": "integer",
10294 "format": "int64",
10295 "minimum": 0
10296 },
10297 "total": {
10298 "type": "integer",
10299 "format": "int64",
10300 "minimum": 0
10301 }
10302 }
10303 },
10304 "error": {
10305 "type": [
10306 "string",
10307 "null"
10308 ]
10309 },
10310 "error_code": {
10311 "type": [
10312 "string",
10313 "null"
10314 ]
10315 },
10316 "log_id": {
10317 "type": "string"
10318 },
10319 "message": {
10320 "type": "string"
10321 },
10322 "request_id": {
10323 "type": "string"
10324 }
10325 }
10326 },
10327 "AppResponseData_StorageObjectPageDto": {
10328 "type": "object",
10329 "required": [
10330 "code",
10331 "message",
10332 "request_id",
10333 "log_id"
10334 ],
10335 "properties": {
10336 "code": {
10337 "type": "integer",
10338 "format": "int32"
10339 },
10340 "data": {
10341 "type": "object",
10342 "required": [
10343 "prefix",
10344 "items"
10345 ],
10346 "properties": {
10347 "items": {
10348 "type": "array",
10349 "items": {
10350 "$ref": "#/components/schemas/StorageObjectDto"
10351 }
10352 },
10353 "next_token": {
10354 "type": [
10355 "string",
10356 "null"
10357 ]
10358 },
10359 "prefix": {
10360 "type": "string"
10361 }
10362 }
10363 },
10364 "error": {
10365 "type": [
10366 "string",
10367 "null"
10368 ]
10369 },
10370 "error_code": {
10371 "type": [
10372 "string",
10373 "null"
10374 ]
10375 },
10376 "log_id": {
10377 "type": "string"
10378 },
10379 "message": {
10380 "type": "string"
10381 },
10382 "request_id": {
10383 "type": "string"
10384 }
10385 }
10386 },
10387 "AppResponseData_StorageOverviewDto": {
10388 "type": "object",
10389 "required": [
10390 "code",
10391 "message",
10392 "request_id",
10393 "log_id"
10394 ],
10395 "properties": {
10396 "code": {
10397 "type": "integer",
10398 "format": "int32"
10399 },
10400 "data": {
10401 "type": "object",
10402 "required": [
10403 "bucket",
10404 "total_bytes",
10405 "total_objects",
10406 "categories",
10407 "generated_at",
10408 "cached"
10409 ],
10410 "properties": {
10411 "bucket": {
10412 "type": "string"
10413 },
10414 "cached": {
10415 "type": "boolean"
10416 },
10417 "categories": {
10418 "type": "array",
10419 "items": {
10420 "$ref": "#/components/schemas/StorageCategoryDto"
10421 }
10422 },
10423 "generated_at": {
10424 "type": "string",
10425 "format": "date-time"
10426 },
10427 "total_bytes": {
10428 "type": "integer",
10429 "format": "int64",
10430 "minimum": 0
10431 },
10432 "total_objects": {
10433 "type": "integer",
10434 "format": "int64",
10435 "minimum": 0
10436 }
10437 }
10438 },
10439 "error": {
10440 "type": [
10441 "string",
10442 "null"
10443 ]
10444 },
10445 "error_code": {
10446 "type": [
10447 "string",
10448 "null"
10449 ]
10450 },
10451 "log_id": {
10452 "type": "string"
10453 },
10454 "message": {
10455 "type": "string"
10456 },
10457 "request_id": {
10458 "type": "string"
10459 }
10460 }
10461 },
10462 "AppResponseData_String": {
10463 "type": "object",
10464 "required": [
10465 "code",
10466 "message",
10467 "request_id",
10468 "log_id"
10469 ],
10470 "properties": {
10471 "code": {
10472 "type": "integer",
10473 "format": "int32"
10474 },
10475 "data": {
10476 "type": "string"
10477 },
10478 "error": {
10479 "type": [
10480 "string",
10481 "null"
10482 ]
10483 },
10484 "error_code": {
10485 "type": [
10486 "string",
10487 "null"
10488 ]
10489 },
10490 "log_id": {
10491 "type": "string"
10492 },
10493 "message": {
10494 "type": "string"
10495 },
10496 "request_id": {
10497 "type": "string"
10498 }
10499 }
10500 },
10501 "AppResponseData_SystemConfigDto": {
10502 "type": "object",
10503 "required": [
10504 "code",
10505 "message",
10506 "request_id",
10507 "log_id"
10508 ],
10509 "properties": {
10510 "code": {
10511 "type": "integer",
10512 "format": "int32"
10513 },
10514 "data": {
10515 "type": "object",
10516 "required": [
10517 "version",
10518 "hosts",
10519 "admin",
10520 "features",
10521 "search",
10522 "import",
10523 "git",
10524 "storage",
10525 "database",
10526 "redis",
10527 "session",
10528 "email",
10529 "generated_at"
10530 ],
10531 "properties": {
10532 "admin": {
10533 "$ref": "#/components/schemas/AdminEndpointDto"
10534 },
10535 "database": {
10536 "$ref": "#/components/schemas/DatabaseConfigDto"
10537 },
10538 "email": {
10539 "$ref": "#/components/schemas/EmailConfigDto"
10540 },
10541 "features": {
10542 "$ref": "#/components/schemas/FeaturesDto"
10543 },
10544 "generated_at": {
10545 "type": "string",
10546 "format": "date-time"
10547 },
10548 "git": {
10549 "$ref": "#/components/schemas/GitConfigDto"
10550 },
10551 "hosts": {
10552 "type": "array",
10553 "items": {
10554 "$ref": "#/components/schemas/HostEntryDto"
10555 }
10556 },
10557 "import": {
10558 "$ref": "#/components/schemas/ImportConfigDto"
10559 },
10560 "redis": {
10561 "$ref": "#/components/schemas/RedisConfigDto"
10562 },
10563 "search": {
10564 "$ref": "#/components/schemas/SearchConfigDto"
10565 },
10566 "session": {
10567 "$ref": "#/components/schemas/SessionConfigDto"
10568 },
10569 "storage": {
10570 "$ref": "#/components/schemas/StorageConfigDto"
10571 },
10572 "version": {
10573 "type": "string"
10574 }
10575 }
10576 },
10577 "error": {
10578 "type": [
10579 "string",
10580 "null"
10581 ]
10582 },
10583 "error_code": {
10584 "type": [
10585 "string",
10586 "null"
10587 ]
10588 },
10589 "log_id": {
10590 "type": "string"
10591 },
10592 "message": {
10593 "type": "string"
10594 },
10595 "request_id": {
10596 "type": "string"
10597 }
10598 }
10599 },
10600 "AppResponseData_SystemStatusDto": {
10601 "type": "object",
10602 "required": [
10603 "code",
10604 "message",
10605 "request_id",
10606 "log_id"
10607 ],
10608 "properties": {
10609 "code": {
10610 "type": "integer",
10611 "format": "int32"
10612 },
10613 "data": {
10614 "type": "object",
10615 "required": [
10616 "process",
10617 "dependencies",
10618 "features",
10619 "counts",
10620 "queues",
10621 "recent_failures",
10622 "generated_at"
10623 ],
10624 "properties": {
10625 "counts": {
10626 "$ref": "#/components/schemas/CountsDto"
10627 },
10628 "dependencies": {
10629 "$ref": "#/components/schemas/DependenciesDto"
10630 },
10631 "features": {
10632 "$ref": "#/components/schemas/FeaturesDto"
10633 },
10634 "generated_at": {
10635 "type": "string",
10636 "format": "date-time"
10637 },
10638 "process": {
10639 "$ref": "#/components/schemas/ProcessInfoDto"
10640 },
10641 "queues": {
10642 "type": "array",
10643 "items": {
10644 "$ref": "#/components/schemas/QueueSnapshotDto"
10645 }
10646 },
10647 "recent_failures": {
10648 "type": "array",
10649 "items": {
10650 "$ref": "#/components/schemas/JobLogDto"
10651 }
10652 }
10653 }
10654 },
10655 "error": {
10656 "type": [
10657 "string",
10658 "null"
10659 ]
10660 },
10661 "error_code": {
10662 "type": [
10663 "string",
10664 "null"
10665 ]
10666 },
10667 "log_id": {
10668 "type": "string"
10669 },
10670 "message": {
10671 "type": "string"
10672 },
10673 "request_id": {
10674 "type": "string"
10675 }
10676 }
10677 },
10678 "AppResponseData_TaskActionResp": {
10679 "type": "object",
10680 "required": [
10681 "code",
10682 "message",
10683 "request_id",
10684 "log_id"
10685 ],
10686 "properties": {
10687 "code": {
10688 "type": "integer",
10689 "format": "int32"
10690 },
10691 "data": {
10692 "type": "object",
10693 "required": [
10694 "source",
10695 "id",
10696 "state"
10697 ],
10698 "properties": {
10699 "id": {
10700 "type": "string",
10701 "format": "uuid"
10702 },
10703 "source": {
10704 "type": "string"
10705 },
10706 "state": {
10707 "type": "string"
10708 }
10709 }
10710 },
10711 "error": {
10712 "type": [
10713 "string",
10714 "null"
10715 ]
10716 },
10717 "error_code": {
10718 "type": [
10719 "string",
10720 "null"
10721 ]
10722 },
10723 "log_id": {
10724 "type": "string"
10725 },
10726 "message": {
10727 "type": "string"
10728 },
10729 "request_id": {
10730 "type": "string"
10731 }
10732 }
10733 },
10734 "AppResponseData_TaskDetailDto": {
10735 "type": "object",
10736 "required": [
10737 "code",
10738 "message",
10739 "request_id",
10740 "log_id"
10741 ],
10742 "properties": {
10743 "code": {
10744 "type": "integer",
10745 "format": "int32"
10746 },
10747 "data": {
10748 "type": "object",
10749 "required": [
10750 "task",
10751 "detail"
10752 ],
10753 "properties": {
10754 "detail": {},
10755 "task": {
10756 "$ref": "#/components/schemas/TaskDto"
10757 }
10758 }
10759 },
10760 "error": {
10761 "type": [
10762 "string",
10763 "null"
10764 ]
10765 },
10766 "error_code": {
10767 "type": [
10768 "string",
10769 "null"
10770 ]
10771 },
10772 "log_id": {
10773 "type": "string"
10774 },
10775 "message": {
10776 "type": "string"
10777 },
10778 "request_id": {
10779 "type": "string"
10780 }
10781 }
10782 },
10783 "AppResponseData_TaskPageDto": {
10784 "type": "object",
10785 "required": [
10786 "code",
10787 "message",
10788 "request_id",
10789 "log_id"
10790 ],
10791 "properties": {
10792 "code": {
10793 "type": "integer",
10794 "format": "int32"
10795 },
10796 "data": {
10797 "type": "object",
10798 "required": [
10799 "items",
10800 "total",
10801 "limit",
10802 "offset"
10803 ],
10804 "properties": {
10805 "items": {
10806 "type": "array",
10807 "items": {
10808 "$ref": "#/components/schemas/TaskDto"
10809 }
10810 },
10811 "limit": {
10812 "type": "integer",
10813 "format": "int64",
10814 "minimum": 0
10815 },
10816 "offset": {
10817 "type": "integer",
10818 "format": "int64",
10819 "minimum": 0
10820 },
10821 "total": {
10822 "type": "integer",
10823 "format": "int64"
10824 }
10825 }
10826 },
10827 "error": {
10828 "type": [
10829 "string",
10830 "null"
10831 ]
10832 },
10833 "error_code": {
10834 "type": [
10835 "string",
10836 "null"
10837 ]
10838 },
10839 "log_id": {
10840 "type": "string"
10841 },
10842 "message": {
10843 "type": "string"
10844 },
10845 "request_id": {
10846 "type": "string"
10847 }
10848 }
10849 },
10850 "AppResponseData_TaskStatsDto": {
10851 "type": "object",
10852 "required": [
10853 "code",
10854 "message",
10855 "request_id",
10856 "log_id"
10857 ],
10858 "properties": {
10859 "code": {
10860 "type": "integer",
10861 "format": "int32"
10862 },
10863 "data": {
10864 "type": "object",
10865 "required": [
10866 "sources",
10867 "total",
10868 "generated_at"
10869 ],
10870 "properties": {
10871 "generated_at": {
10872 "type": "string",
10873 "format": "date-time"
10874 },
10875 "sources": {
10876 "type": "array",
10877 "items": {
10878 "$ref": "#/components/schemas/TaskSourceStatDto"
10879 }
10880 },
10881 "total": {
10882 "type": "integer",
10883 "format": "int64"
10884 }
10885 }
10886 },
10887 "error": {
10888 "type": [
10889 "string",
10890 "null"
10891 ]
10892 },
10893 "error_code": {
10894 "type": [
10895 "string",
10896 "null"
10897 ]
10898 },
10899 "log_id": {
10900 "type": "string"
10901 },
10902 "message": {
10903 "type": "string"
10904 },
10905 "request_id": {
10906 "type": "string"
10907 }
10908 }
10909 },
10910 "AppResponseData_Vec_EnvironmentAdminDto": {
10911 "type": "object",
10912 "required": [
10913 "code",
10914 "message",
10915 "request_id",
10916 "log_id"
10917 ],
10918 "properties": {
10919 "code": {
10920 "type": "integer",
10921 "format": "int32"
10922 },
10923 "data": {
10924 "type": "array",
10925 "items": {
10926 "type": "object",
10927 "required": [
10928 "id",
10929 "space_id",
10930 "name",
10931 "required_labels",
10932 "secrets",
10933 "created_at",
10934 "updated_at"
10935 ],
10936 "properties": {
10937 "created_at": {
10938 "type": "string",
10939 "format": "date-time"
10940 },
10941 "description": {
10942 "type": [
10943 "string",
10944 "null"
10945 ]
10946 },
10947 "id": {
10948 "type": "string",
10949 "format": "uuid"
10950 },
10951 "name": {
10952 "type": "string"
10953 },
10954 "required_labels": {
10955 "type": "array",
10956 "items": {
10957 "type": "string"
10958 }
10959 },
10960 "secrets": {
10961 "type": "array",
10962 "items": {
10963 "type": "string"
10964 }
10965 },
10966 "space_id": {
10967 "type": "string",
10968 "format": "uuid"
10969 },
10970 "updated_at": {
10971 "type": "string",
10972 "format": "date-time"
10973 }
10974 }
10975 }
10976 },
10977 "error": {
10978 "type": [
10979 "string",
10980 "null"
10981 ]
10982 },
10983 "error_code": {
10984 "type": [
10985 "string",
10986 "null"
10987 ]
10988 },
10989 "log_id": {
10990 "type": "string"
10991 },
10992 "message": {
10993 "type": "string"
10994 },
10995 "request_id": {
10996 "type": "string"
10997 }
10998 }
10999 },
11000 "AppResponseData_Vec_QueueSnapshotDto": {
11001 "type": "object",
11002 "required": [
11003 "code",
11004 "message",
11005 "request_id",
11006 "log_id"
11007 ],
11008 "properties": {
11009 "code": {
11010 "type": "integer",
11011 "format": "int32"
11012 },
11013 "data": {
11014 "type": "array",
11015 "items": {
11016 "type": "object",
11017 "required": [
11018 "name",
11019 "kind",
11020 "enabled",
11021 "queued",
11022 "running",
11023 "failed",
11024 "succeeded"
11025 ],
11026 "properties": {
11027 "depth": {
11028 "type": [
11029 "integer",
11030 "null"
11031 ],
11032 "format": "int64"
11033 },
11034 "enabled": {
11035 "type": "boolean"
11036 },
11037 "failed": {
11038 "type": "integer",
11039 "format": "int64"
11040 },
11041 "kind": {
11042 "type": "string"
11043 },
11044 "name": {
11045 "type": "string"
11046 },
11047 "note": {
11048 "type": [
11049 "string",
11050 "null"
11051 ]
11052 },
11053 "queued": {
11054 "type": "integer",
11055 "format": "int64"
11056 },
11057 "running": {
11058 "type": "integer",
11059 "format": "int64"
11060 },
11061 "succeeded": {
11062 "type": "integer",
11063 "format": "int64"
11064 }
11065 }
11066 }
11067 },
11068 "error": {
11069 "type": [
11070 "string",
11071 "null"
11072 ]
11073 },
11074 "error_code": {
11075 "type": [
11076 "string",
11077 "null"
11078 ]
11079 },
11080 "log_id": {
11081 "type": "string"
11082 },
11083 "message": {
11084 "type": "string"
11085 },
11086 "request_id": {
11087 "type": "string"
11088 }
11089 }
11090 },
11091 "AppResponseData_Vec_RunnerDto": {
11092 "type": "object",
11093 "required": [
11094 "code",
11095 "message",
11096 "request_id",
11097 "log_id"
11098 ],
11099 "properties": {
11100 "code": {
11101 "type": "integer",
11102 "format": "int32"
11103 },
11104 "data": {
11105 "type": "array",
11106 "items": {
11107 "type": "object",
11108 "required": [
11109 "id",
11110 "name",
11111 "labels",
11112 "capacity",
11113 "capabilities",
11114 "state",
11115 "active_jobs",
11116 "created_at"
11117 ],
11118 "properties": {
11119 "active_jobs": {
11120 "type": "integer",
11121 "format": "int64"
11122 },
11123 "capabilities": {
11124 "type": "array",
11125 "items": {
11126 "type": "string"
11127 }
11128 },
11129 "capacity": {
11130 "type": "integer",
11131 "format": "int32"
11132 },
11133 "created_at": {
11134 "type": "string",
11135 "format": "date-time"
11136 },
11137 "id": {
11138 "type": "string",
11139 "format": "uuid"
11140 },
11141 "labels": {
11142 "type": "array",
11143 "items": {
11144 "type": "string"
11145 }
11146 },
11147 "last_seen_at": {
11148 "type": [
11149 "string",
11150 "null"
11151 ],
11152 "format": "date-time"
11153 },
11154 "name": {
11155 "type": "string"
11156 },
11157 "protocol_version": {
11158 "type": [
11159 "integer",
11160 "null"
11161 ],
11162 "format": "int32"
11163 },
11164 "space_id": {
11165 "type": [
11166 "string",
11167 "null"
11168 ],
11169 "format": "uuid"
11170 },
11171 "state": {
11172 "type": "string"
11173 },
11174 "version": {
11175 "type": [
11176 "string",
11177 "null"
11178 ]
11179 }
11180 }
11181 }
11182 },
11183 "error": {
11184 "type": [
11185 "string",
11186 "null"
11187 ]
11188 },
11189 "error_code": {
11190 "type": [
11191 "string",
11192 "null"
11193 ]
11194 },
11195 "log_id": {
11196 "type": "string"
11197 },
11198 "message": {
11199 "type": "string"
11200 },
11201 "request_id": {
11202 "type": "string"
11203 }
11204 }
11205 },
11206 "AppResponseData_Vec_RunnerTokenDto": {
11207 "type": "object",
11208 "required": [
11209 "code",
11210 "message",
11211 "request_id",
11212 "log_id"
11213 ],
11214 "properties": {
11215 "code": {
11216 "type": "integer",
11217 "format": "int32"
11218 },
11219 "data": {
11220 "type": "array",
11221 "items": {
11222 "type": "object",
11223 "required": [
11224 "id",
11225 "name",
11226 "single_use",
11227 "created_at"
11228 ],
11229 "properties": {
11230 "created_at": {
11231 "type": "string",
11232 "format": "date-time"
11233 },
11234 "expires_at": {
11235 "type": [
11236 "string",
11237 "null"
11238 ],
11239 "format": "date-time"
11240 },
11241 "id": {
11242 "type": "string",
11243 "format": "uuid"
11244 },
11245 "name": {
11246 "type": "string"
11247 },
11248 "revoked_at": {
11249 "type": [
11250 "string",
11251 "null"
11252 ],
11253 "format": "date-time"
11254 },
11255 "single_use": {
11256 "type": "boolean"
11257 },
11258 "space_id": {
11259 "type": [
11260 "string",
11261 "null"
11262 ],
11263 "format": "uuid"
11264 },
11265 "used_at": {
11266 "type": [
11267 "string",
11268 "null"
11269 ],
11270 "format": "date-time"
11271 }
11272 }
11273 }
11274 },
11275 "error": {
11276 "type": [
11277 "string",
11278 "null"
11279 ]
11280 },
11281 "error_code": {
11282 "type": [
11283 "string",
11284 "null"
11285 ]
11286 },
11287 "log_id": {
11288 "type": "string"
11289 },
11290 "message": {
11291 "type": "string"
11292 },
11293 "request_id": {
11294 "type": "string"
11295 }
11296 }
11297 },
11298 "AuditLogDto": {
11299 "type": "object",
11300 "required": [
11301 "id",
11302 "admin_username",
11303 "action",
11304 "detail",
11305 "created_at"
11306 ],
11307 "properties": {
11308 "action": {
11309 "type": "string"
11310 },
11311 "admin_id": {
11312 "type": [
11313 "string",
11314 "null"
11315 ],
11316 "format": "uuid"
11317 },
11318 "admin_username": {
11319 "type": "string"
11320 },
11321 "created_at": {
11322 "type": "string",
11323 "format": "date-time"
11324 },
11325 "detail": {},
11326 "id": {
11327 "type": "string",
11328 "format": "uuid"
11329 },
11330 "ip": {
11331 "type": [
11332 "string",
11333 "null"
11334 ]
11335 },
11336 "request_id": {
11337 "type": [
11338 "string",
11339 "null"
11340 ]
11341 },
11342 "target_id": {
11343 "type": [
11344 "string",
11345 "null"
11346 ]
11347 },
11348 "target_type": {
11349 "type": [
11350 "string",
11351 "null"
11352 ]
11353 },
11354 "user_agent": {
11355 "type": [
11356 "string",
11357 "null"
11358 ]
11359 }
11360 }
11361 },
11362 "AuditLogPageDto": {
11363 "type": "object",
11364 "required": [
11365 "items",
11366 "total",
11367 "limit",
11368 "offset"
11369 ],
11370 "properties": {
11371 "items": {
11372 "type": "array",
11373 "items": {
11374 "$ref": "#/components/schemas/AuditLogDto"
11375 }
11376 },
11377 "limit": {
11378 "type": "integer",
11379 "format": "int64",
11380 "minimum": 0
11381 },
11382 "offset": {
11383 "type": "integer",
11384 "format": "int64",
11385 "minimum": 0
11386 },
11387 "total": {
11388 "type": "integer",
11389 "format": "int64"
11390 }
11391 }
11392 },
11393 "BulkChatMessageFailure": {
11394 "type": "object",
11395 "required": [
11396 "id",
11397 "error"
11398 ],
11399 "properties": {
11400 "error": {
11401 "type": "string"
11402 },
11403 "id": {
11404 "type": "string",
11405 "format": "uuid"
11406 }
11407 }
11408 },
11409 "BulkDeleteChatMessagesCmd": {
11410 "type": "object",
11411 "required": [
11412 "message_ids"
11413 ],
11414 "properties": {
11415 "message_ids": {
11416 "type": "array",
11417 "items": {
11418 "type": "string",
11419 "format": "uuid"
11420 }
11421 },
11422 "reason": {
11423 "type": [
11424 "string",
11425 "null"
11426 ]
11427 }
11428 }
11429 },
11430 "BulkDeleteChatMessagesResp": {
11431 "type": "object",
11432 "required": [
11433 "succeeded",
11434 "failed"
11435 ],
11436 "properties": {
11437 "failed": {
11438 "type": "array",
11439 "items": {
11440 "$ref": "#/components/schemas/BulkChatMessageFailure"
11441 }
11442 },
11443 "succeeded": {
11444 "type": "array",
11445 "items": {
11446 "type": "string",
11447 "format": "uuid"
11448 }
11449 }
11450 }
11451 },
11452 "BulkTaskCmd": {
11453 "type": "object",
11454 "required": [
11455 "action",
11456 "ids"
11457 ],
11458 "properties": {
11459 "action": {
11460 "type": "string"
11461 },
11462 "ids": {
11463 "type": "array",
11464 "items": {
11465 "type": "string",
11466 "format": "uuid"
11467 }
11468 }
11469 }
11470 },
11471 "BulkTaskFailure": {
11472 "type": "object",
11473 "required": [
11474 "id",
11475 "error"
11476 ],
11477 "properties": {
11478 "error": {
11479 "type": "string"
11480 },
11481 "id": {
11482 "type": "string",
11483 "format": "uuid"
11484 }
11485 }
11486 },
11487 "BulkTaskResp": {
11488 "type": "object",
11489 "required": [
11490 "source",
11491 "action",
11492 "succeeded",
11493 "failed"
11494 ],
11495 "properties": {
11496 "action": {
11497 "type": "string"
11498 },
11499 "failed": {
11500 "type": "array",
11501 "items": {
11502 "$ref": "#/components/schemas/BulkTaskFailure"
11503 }
11504 },
11505 "source": {
11506 "type": "string"
11507 },
11508 "succeeded": {
11509 "type": "array",
11510 "items": {
11511 "type": "string",
11512 "format": "uuid"
11513 }
11514 }
11515 }
11516 },
11517 "BulkUserFailure": {
11518 "type": "object",
11519 "required": [
11520 "uid",
11521 "error"
11522 ],
11523 "properties": {
11524 "error": {
11525 "type": "string"
11526 },
11527 "uid": {
11528 "type": "string",
11529 "format": "uuid"
11530 }
11531 }
11532 },
11533 "BulkUsersCmd": {
11534 "type": "object",
11535 "required": [
11536 "action",
11537 "ids"
11538 ],
11539 "properties": {
11540 "action": {
11541 "type": "string"
11542 },
11543 "ends_at": {
11544 "type": [
11545 "string",
11546 "null"
11547 ],
11548 "format": "date-time"
11549 },
11550 "ids": {
11551 "type": "array",
11552 "items": {
11553 "type": "string",
11554 "format": "uuid"
11555 }
11556 },
11557 "reason": {
11558 "type": [
11559 "string",
11560 "null"
11561 ]
11562 },
11563 "suspension_type": {
11564 "oneOf": [
11565 {
11566 "type": "null"
11567 },
11568 {
11569 "$ref": "#/components/schemas/SuspensionType"
11570 }
11571 ]
11572 }
11573 }
11574 },
11575 "BulkUsersResp": {
11576 "type": "object",
11577 "required": [
11578 "action",
11579 "succeeded",
11580 "failed"
11581 ],
11582 "properties": {
11583 "action": {
11584 "type": "string"
11585 },
11586 "failed": {
11587 "type": "array",
11588 "items": {
11589 "$ref": "#/components/schemas/BulkUserFailure"
11590 }
11591 },
11592 "succeeded": {
11593 "type": "array",
11594 "items": {
11595 "type": "string",
11596 "format": "uuid"
11597 }
11598 }
11599 }
11600 },
11601 "ChangePasswordCmd": {
11602 "type": "object",
11603 "required": [
11604 "current_password",
11605 "new_password"
11606 ],
11607 "properties": {
11608 "current_password": {
11609 "type": "string"
11610 },
11611 "new_password": {
11612 "type": "string"
11613 }
11614 }
11615 },
11616 "CountsDto": {
11617 "type": "object",
11618 "required": [
11619 "users",
11620 "active_users",
11621 "spaces",
11622 "active_spaces",
11623 "repos",
11624 "rooms",
11625 "issues",
11626 "messages",
11627 "admin_users"
11628 ],
11629 "properties": {
11630 "active_spaces": {
11631 "type": "integer",
11632 "format": "int64"
11633 },
11634 "active_users": {
11635 "type": "integer",
11636 "format": "int64"
11637 },
11638 "admin_users": {
11639 "type": "integer",
11640 "format": "int64"
11641 },
11642 "issues": {
11643 "type": "integer",
11644 "format": "int64"
11645 },
11646 "messages": {
11647 "type": "integer",
11648 "format": "int64"
11649 },
11650 "repos": {
11651 "type": "integer",
11652 "format": "int64"
11653 },
11654 "rooms": {
11655 "type": "integer",
11656 "format": "int64"
11657 },
11658 "spaces": {
11659 "type": "integer",
11660 "format": "int64"
11661 },
11662 "users": {
11663 "type": "integer",
11664 "format": "int64"
11665 }
11666 }
11667 },
11668 "CreateAdminCmd": {
11669 "type": "object",
11670 "required": [
11671 "username",
11672 "password",
11673 "role"
11674 ],
11675 "properties": {
11676 "display_name": {
11677 "type": [
11678 "string",
11679 "null"
11680 ]
11681 },
11682 "password": {
11683 "type": "string"
11684 },
11685 "role": {
11686 "$ref": "#/components/schemas/AdminRole"
11687 },
11688 "username": {
11689 "type": "string"
11690 }
11691 }
11692 },
11693 "CreateRunnerTokenCmd": {
11694 "type": "object",
11695 "required": [
11696 "name"
11697 ],
11698 "properties": {
11699 "expires_in_days": {
11700 "type": [
11701 "integer",
11702 "null"
11703 ],
11704 "format": "int64"
11705 },
11706 "name": {
11707 "type": "string"
11708 },
11709 "space_id": {
11710 "type": [
11711 "string",
11712 "null"
11713 ],
11714 "format": "uuid"
11715 }
11716 }
11717 },
11718 "CreateUserCmd": {
11719 "type": "object",
11720 "required": [
11721 "username",
11722 "email",
11723 "password"
11724 ],
11725 "properties": {
11726 "display_name": {
11727 "type": [
11728 "string",
11729 "null"
11730 ]
11731 },
11732 "email": {
11733 "type": "string"
11734 },
11735 "email_activated": {
11736 "type": [
11737 "boolean",
11738 "null"
11739 ]
11740 },
11741 "is_active": {
11742 "type": [
11743 "boolean",
11744 "null"
11745 ]
11746 },
11747 "password": {
11748 "type": "string"
11749 },
11750 "username": {
11751 "type": "string"
11752 }
11753 }
11754 },
11755 "CreateUserResp": {
11756 "type": "object",
11757 "required": [
11758 "uid",
11759 "username",
11760 "email"
11761 ],
11762 "properties": {
11763 "email": {
11764 "type": "string"
11765 },
11766 "uid": {
11767 "type": "string",
11768 "format": "uuid"
11769 },
11770 "username": {
11771 "type": "string"
11772 }
11773 }
11774 },
11775 "CreatedRunnerTokenDto": {
11776 "type": "object",
11777 "required": [
11778 "token",
11779 "plaintext"
11780 ],
11781 "properties": {
11782 "plaintext": {
11783 "type": "string"
11784 },
11785 "token": {
11786 "$ref": "#/components/schemas/RunnerTokenDto"
11787 }
11788 }
11789 },
11790 "DashboardDto": {
11791 "type": "object",
11792 "required": [
11793 "counts",
11794 "trend",
11795 "active_sessions",
11796 "restricted_users",
11797 "recent_audit",
11798 "recent_failures",
11799 "queues",
11800 "generated_at"
11801 ],
11802 "properties": {
11803 "active_sessions": {
11804 "type": "integer",
11805 "format": "int64"
11806 },
11807 "counts": {
11808 "$ref": "#/components/schemas/CountsDto"
11809 },
11810 "generated_at": {
11811 "type": "string",
11812 "format": "date-time"
11813 },
11814 "queues": {
11815 "type": "array",
11816 "items": {
11817 "$ref": "#/components/schemas/QueueSnapshotDto"
11818 }
11819 },
11820 "recent_audit": {
11821 "type": "array",
11822 "items": {
11823 "$ref": "#/components/schemas/AuditLogDto"
11824 }
11825 },
11826 "recent_failures": {
11827 "type": "array",
11828 "items": {
11829 "$ref": "#/components/schemas/JobLogDto"
11830 }
11831 },
11832 "restricted_users": {
11833 "type": "integer",
11834 "format": "int64"
11835 },
11836 "trend": {
11837 "type": "array",
11838 "items": {
11839 "$ref": "#/components/schemas/TrendPointDto"
11840 }
11841 }
11842 }
11843 },
11844 "DatabaseConfigDto": {
11845 "type": "object",
11846 "required": [
11847 "replica_count"
11848 ],
11849 "properties": {
11850 "database": {
11851 "type": [
11852 "string",
11853 "null"
11854 ]
11855 },
11856 "endpoint": {
11857 "type": [
11858 "string",
11859 "null"
11860 ]
11861 },
11862 "host": {
11863 "type": [
11864 "string",
11865 "null"
11866 ]
11867 },
11868 "max_connections": {
11869 "type": [
11870 "integer",
11871 "null"
11872 ],
11873 "format": "int32",
11874 "minimum": 0
11875 },
11876 "min_connections": {
11877 "type": [
11878 "integer",
11879 "null"
11880 ],
11881 "format": "int32",
11882 "minimum": 0
11883 },
11884 "port": {
11885 "type": [
11886 "integer",
11887 "null"
11888 ],
11889 "format": "int32",
11890 "minimum": 0
11891 },
11892 "replica_count": {
11893 "type": "integer",
11894 "minimum": 0
11895 },
11896 "ssl_mode": {
11897 "type": [
11898 "string",
11899 "null"
11900 ]
11901 }
11902 }
11903 },
11904 "DeleteChatAttachmentResp": {
11905 "type": "object",
11906 "required": [
11907 "id",
11908 "s3_key",
11909 "deleted",
11910 "storage_deleted"
11911 ],
11912 "properties": {
11913 "deleted": {
11914 "type": "boolean"
11915 },
11916 "id": {
11917 "type": "string",
11918 "format": "uuid"
11919 },
11920 "s3_key": {
11921 "type": "string"
11922 },
11923 "storage_deleted": {
11924 "type": "boolean"
11925 }
11926 }
11927 },
11928 "DeleteChatRoomResp": {
11929 "type": "object",
11930 "required": [
11931 "room_id",
11932 "messages_deleted",
11933 "members_deleted",
11934 "attachments_deleted",
11935 "threads_deleted"
11936 ],
11937 "properties": {
11938 "attachments_deleted": {
11939 "type": "integer",
11940 "format": "int64",
11941 "minimum": 0
11942 },
11943 "members_deleted": {
11944 "type": "integer",
11945 "format": "int64",
11946 "minimum": 0
11947 },
11948 "messages_deleted": {
11949 "type": "integer",
11950 "format": "int64",
11951 "minimum": 0
11952 },
11953 "room_id": {
11954 "type": "string",
11955 "format": "uuid"
11956 },
11957 "threads_deleted": {
11958 "type": "integer",
11959 "format": "int64",
11960 "minimum": 0
11961 }
11962 }
11963 },
11964 "DeleteObjectResp": {
11965 "type": "object",
11966 "required": [
11967 "key",
11968 "deleted"
11969 ],
11970 "properties": {
11971 "deleted": {
11972 "type": "boolean"
11973 },
11974 "key": {
11975 "type": "string"
11976 }
11977 }
11978 },
11979 "DeleteUserResp": {
11980 "type": "object",
11981 "required": [
11982 "user_id",
11983 "anonymized"
11984 ],
11985 "properties": {
11986 "anonymized": {
11987 "type": "boolean"
11988 },
11989 "user_id": {
11990 "type": "string",
11991 "format": "uuid"
11992 }
11993 }
11994 },
11995 "DependenciesDto": {
11996 "type": "object",
11997 "required": [
11998 "database",
11999 "redis",
12000 "storage",
12001 "search"
12002 ],
12003 "properties": {
12004 "database": {
12005 "$ref": "#/components/schemas/DependencyDto"
12006 },
12007 "redis": {
12008 "$ref": "#/components/schemas/DependencyDto"
12009 },
12010 "search": {
12011 "$ref": "#/components/schemas/DependencyDto"
12012 },
12013 "storage": {
12014 "$ref": "#/components/schemas/DependencyDto"
12015 }
12016 }
12017 },
12018 "DependencyDto": {
12019 "type": "object",
12020 "required": [
12021 "ok"
12022 ],
12023 "properties": {
12024 "detail": {
12025 "type": [
12026 "string",
12027 "null"
12028 ]
12029 },
12030 "latency_ms": {
12031 "type": [
12032 "integer",
12033 "null"
12034 ],
12035 "format": "int64",
12036 "minimum": 0
12037 },
12038 "ok": {
12039 "type": "boolean"
12040 }
12041 }
12042 },
12043 "DownloadUrlDto": {
12044 "type": "object",
12045 "required": [
12046 "key",
12047 "url",
12048 "expires_in_secs"
12049 ],
12050 "properties": {
12051 "expires_in_secs": {
12052 "type": "integer",
12053 "format": "int32",
12054 "minimum": 0
12055 },
12056 "key": {
12057 "type": "string"
12058 },
12059 "url": {
12060 "type": "string"
12061 }
12062 }
12063 },
12064 "EmailConfigDto": {
12065 "type": "object",
12066 "required": [
12067 "enabled",
12068 "queue_enabled",
12069 "smtp_host",
12070 "smtp_port"
12071 ],
12072 "properties": {
12073 "enabled": {
12074 "type": "boolean"
12075 },
12076 "from_address": {
12077 "type": [
12078 "string",
12079 "null"
12080 ]
12081 },
12082 "from_name": {
12083 "type": [
12084 "string",
12085 "null"
12086 ]
12087 },
12088 "queue_enabled": {
12089 "type": "boolean"
12090 },
12091 "smtp_host": {
12092 "type": "string"
12093 },
12094 "smtp_port": {
12095 "type": "integer",
12096 "format": "int32",
12097 "minimum": 0
12098 }
12099 }
12100 },
12101 "EnvironmentAdminDto": {
12102 "type": "object",
12103 "required": [
12104 "id",
12105 "space_id",
12106 "name",
12107 "required_labels",
12108 "secrets",
12109 "created_at",
12110 "updated_at"
12111 ],
12112 "properties": {
12113 "created_at": {
12114 "type": "string",
12115 "format": "date-time"
12116 },
12117 "description": {
12118 "type": [
12119 "string",
12120 "null"
12121 ]
12122 },
12123 "id": {
12124 "type": "string",
12125 "format": "uuid"
12126 },
12127 "name": {
12128 "type": "string"
12129 },
12130 "required_labels": {
12131 "type": "array",
12132 "items": {
12133 "type": "string"
12134 }
12135 },
12136 "secrets": {
12137 "type": "array",
12138 "items": {
12139 "type": "string"
12140 }
12141 },
12142 "space_id": {
12143 "type": "string",
12144 "format": "uuid"
12145 },
12146 "updated_at": {
12147 "type": "string",
12148 "format": "date-time"
12149 }
12150 }
12151 },
12152 "FeaturesDto": {
12153 "type": "object",
12154 "required": [
12155 "search_enabled",
12156 "import_enabled",
12157 "email_enabled",
12158 "email_queue_enabled"
12159 ],
12160 "properties": {
12161 "email_enabled": {
12162 "type": "boolean"
12163 },
12164 "email_queue_enabled": {
12165 "type": "boolean"
12166 },
12167 "import_enabled": {
12168 "type": "boolean"
12169 },
12170 "search_enabled": {
12171 "type": "boolean"
12172 }
12173 }
12174 },
12175 "GitConfigDto": {
12176 "type": "object",
12177 "required": [
12178 "repo_root",
12179 "objects_root",
12180 "max_blob_size",
12181 "sync_topic",
12182 "sync_concurrency",
12183 "base_url",
12184 "graphql_complexity_limit",
12185 "graphql_introspection"
12186 ],
12187 "properties": {
12188 "base_url": {
12189 "type": "string"
12190 },
12191 "graphql_complexity_limit": {
12192 "type": "integer",
12193 "format": "int64",
12194 "minimum": 0
12195 },
12196 "graphql_introspection": {
12197 "type": "boolean"
12198 },
12199 "max_blob_size": {
12200 "type": "integer",
12201 "format": "int64",
12202 "minimum": 0
12203 },
12204 "objects_root": {
12205 "type": "string"
12206 },
12207 "repo_root": {
12208 "type": "string"
12209 },
12210 "sync_concurrency": {
12211 "type": "integer",
12212 "minimum": 0
12213 },
12214 "sync_topic": {
12215 "type": "string"
12216 }
12217 }
12218 },
12219 "HostEntryDto": {
12220 "type": "object",
12221 "required": [
12222 "name",
12223 "enabled"
12224 ],
12225 "properties": {
12226 "bind_addr": {
12227 "type": [
12228 "string",
12229 "null"
12230 ]
12231 },
12232 "enabled": {
12233 "type": "boolean"
12234 },
12235 "name": {
12236 "type": "string"
12237 },
12238 "port": {
12239 "type": [
12240 "integer",
12241 "null"
12242 ],
12243 "format": "int32",
12244 "minimum": 0
12245 }
12246 }
12247 },
12248 "ImportConfigDto": {
12249 "type": "object",
12250 "required": [
12251 "enabled",
12252 "allowed_hosts",
12253 "allow_insecure_tls",
12254 "max_concurrent_jobs",
12255 "max_repo_bytes",
12256 "max_attempts"
12257 ],
12258 "properties": {
12259 "allow_insecure_tls": {
12260 "type": "boolean"
12261 },
12262 "allowed_hosts": {
12263 "type": "array",
12264 "items": {
12265 "type": "string"
12266 }
12267 },
12268 "enabled": {
12269 "type": "boolean"
12270 },
12271 "max_attempts": {
12272 "type": "integer",
12273 "format": "int32"
12274 },
12275 "max_concurrent_jobs": {
12276 "type": "integer",
12277 "minimum": 0
12278 },
12279 "max_repo_bytes": {
12280 "type": "integer",
12281 "format": "int64",
12282 "minimum": 0
12283 }
12284 }
12285 },
12286 "JobLogDto": {
12287 "type": "object",
12288 "required": [
12289 "source",
12290 "id",
12291 "state",
12292 "created_at"
12293 ],
12294 "properties": {
12295 "created_at": {
12296 "type": "string",
12297 "format": "date-time"
12298 },
12299 "error": {
12300 "type": [
12301 "string",
12302 "null"
12303 ]
12304 },
12305 "id": {
12306 "type": "string",
12307 "format": "uuid"
12308 },
12309 "source": {
12310 "type": "string"
12311 },
12312 "state": {
12313 "type": "string"
12314 },
12315 "updated_at": {
12316 "type": [
12317 "string",
12318 "null"
12319 ],
12320 "format": "date-time"
12321 }
12322 }
12323 },
12324 "JobLogPageDto": {
12325 "type": "object",
12326 "required": [
12327 "items",
12328 "total",
12329 "limit",
12330 "offset"
12331 ],
12332 "properties": {
12333 "items": {
12334 "type": "array",
12335 "items": {
12336 "$ref": "#/components/schemas/JobLogDto"
12337 }
12338 },
12339 "limit": {
12340 "type": "integer",
12341 "format": "int64",
12342 "minimum": 0
12343 },
12344 "offset": {
12345 "type": "integer",
12346 "format": "int64",
12347 "minimum": 0
12348 },
12349 "total": {
12350 "type": "integer",
12351 "format": "int64"
12352 }
12353 }
12354 },
12355 "ListActRunsQuery": {
12356 "type": "object",
12357 "properties": {
12358 "limit": {
12359 "type": [
12360 "integer",
12361 "null"
12362 ],
12363 "format": "int64",
12364 "minimum": 0
12365 },
12366 "offset": {
12367 "type": [
12368 "integer",
12369 "null"
12370 ],
12371 "format": "int64",
12372 "minimum": 0
12373 },
12374 "q": {
12375 "type": [
12376 "string",
12377 "null"
12378 ]
12379 },
12380 "repo_id": {
12381 "type": [
12382 "string",
12383 "null"
12384 ],
12385 "format": "uuid"
12386 },
12387 "state": {
12388 "oneOf": [
12389 {
12390 "type": "null"
12391 },
12392 {
12393 "$ref": "#/components/schemas/WorkflowRunState"
12394 }
12395 ]
12396 }
12397 }
12398 },
12399 "ListReposQuery": {
12400 "type": "object",
12401 "properties": {
12402 "archived": {
12403 "type": [
12404 "boolean",
12405 "null"
12406 ]
12407 },
12408 "limit": {
12409 "type": [
12410 "integer",
12411 "null"
12412 ],
12413 "format": "int64",
12414 "minimum": 0
12415 },
12416 "offset": {
12417 "type": [
12418 "integer",
12419 "null"
12420 ],
12421 "format": "int64",
12422 "minimum": 0
12423 },
12424 "private": {
12425 "type": [
12426 "boolean",
12427 "null"
12428 ]
12429 },
12430 "q": {
12431 "type": [
12432 "string",
12433 "null"
12434 ]
12435 },
12436 "space_id": {
12437 "type": [
12438 "string",
12439 "null"
12440 ],
12441 "format": "uuid"
12442 }
12443 }
12444 },
12445 "NoData": {
12446 "default": null
12447 },
12448 "PageQuery": {
12449 "type": "object",
12450 "properties": {
12451 "limit": {
12452 "type": [
12453 "integer",
12454 "null"
12455 ],
12456 "format": "int64",
12457 "minimum": 0
12458 },
12459 "offset": {
12460 "type": [
12461 "integer",
12462 "null"
12463 ],
12464 "format": "int64",
12465 "minimum": 0
12466 },
12467 "sort": {
12468 "type": [
12469 "string",
12470 "null"
12471 ]
12472 }
12473 }
12474 },
12475 "ProcessInfoDto": {
12476 "type": "object",
12477 "required": [
12478 "version",
12479 "uptime_secs"
12480 ],
12481 "properties": {
12482 "uptime_secs": {
12483 "type": "integer",
12484 "format": "int64",
12485 "minimum": 0
12486 },
12487 "version": {
12488 "type": "string"
12489 }
12490 }
12491 },
12492 "PurgeChatMessagesResp": {
12493 "type": "object",
12494 "required": [
12495 "room_id",
12496 "messages_deleted",
12497 "attachments_deleted",
12498 "threads_deleted"
12499 ],
12500 "properties": {
12501 "attachments_deleted": {
12502 "type": "integer",
12503 "format": "int64",
12504 "minimum": 0
12505 },
12506 "messages_deleted": {
12507 "type": "integer",
12508 "format": "int64",
12509 "minimum": 0
12510 },
12511 "room_id": {
12512 "type": "string",
12513 "format": "uuid"
12514 },
12515 "threads_deleted": {
12516 "type": "integer",
12517 "format": "int64",
12518 "minimum": 0
12519 }
12520 }
12521 },
12522 "QueueActionResp": {
12523 "type": "object",
12524 "required": [
12525 "queue",
12526 "affected"
12527 ],
12528 "properties": {
12529 "affected": {
12530 "type": "integer",
12531 "format": "int64",
12532 "minimum": 0
12533 },
12534 "queue": {
12535 "type": "string"
12536 }
12537 }
12538 },
12539 "QueueSnapshotDto": {
12540 "type": "object",
12541 "required": [
12542 "name",
12543 "kind",
12544 "enabled",
12545 "queued",
12546 "running",
12547 "failed",
12548 "succeeded"
12549 ],
12550 "properties": {
12551 "depth": {
12552 "type": [
12553 "integer",
12554 "null"
12555 ],
12556 "format": "int64"
12557 },
12558 "enabled": {
12559 "type": "boolean"
12560 },
12561 "failed": {
12562 "type": "integer",
12563 "format": "int64"
12564 },
12565 "kind": {
12566 "type": "string"
12567 },
12568 "name": {
12569 "type": "string"
12570 },
12571 "note": {
12572 "type": [
12573 "string",
12574 "null"
12575 ]
12576 },
12577 "queued": {
12578 "type": "integer",
12579 "format": "int64"
12580 },
12581 "running": {
12582 "type": "integer",
12583 "format": "int64"
12584 },
12585 "succeeded": {
12586 "type": "integer",
12587 "format": "int64"
12588 }
12589 }
12590 },
12591 "RedisConfigDto": {
12592 "type": "object",
12593 "required": [
12594 "endpoint",
12595 "mode",
12596 "pool_size"
12597 ],
12598 "properties": {
12599 "database": {
12600 "type": [
12601 "integer",
12602 "null"
12603 ],
12604 "format": "int32",
12605 "minimum": 0
12606 },
12607 "endpoint": {
12608 "type": "string"
12609 },
12610 "mode": {
12611 "type": "string"
12612 },
12613 "pool_size": {
12614 "type": "integer",
12615 "minimum": 0
12616 }
12617 }
12618 },
12619 "RemoveChatMemberResp": {
12620 "type": "object",
12621 "required": [
12622 "room_id",
12623 "user_id",
12624 "removed"
12625 ],
12626 "properties": {
12627 "removed": {
12628 "type": "boolean"
12629 },
12630 "room_id": {
12631 "type": "string",
12632 "format": "uuid"
12633 },
12634 "user_id": {
12635 "type": "string",
12636 "format": "uuid"
12637 }
12638 }
12639 },
12640 "RemoveMemberResp": {
12641 "type": "object",
12642 "required": [
12643 "space_id",
12644 "user_id",
12645 "removed"
12646 ],
12647 "properties": {
12648 "removed": {
12649 "type": "boolean"
12650 },
12651 "space_id": {
12652 "type": "string",
12653 "format": "uuid"
12654 },
12655 "user_id": {
12656 "type": "string",
12657 "format": "uuid"
12658 }
12659 }
12660 },
12661 "ResetAdminPasswordCmd": {
12662 "type": "object",
12663 "required": [
12664 "new_password"
12665 ],
12666 "properties": {
12667 "new_password": {
12668 "type": "string"
12669 }
12670 }
12671 },
12672 "ResetUserPasswordCmd": {
12673 "type": "object",
12674 "required": [
12675 "new_password"
12676 ],
12677 "properties": {
12678 "new_password": {
12679 "type": "string"
12680 }
12681 }
12682 },
12683 "RevokeSessionsResp": {
12684 "type": "object",
12685 "required": [
12686 "revoked"
12687 ],
12688 "properties": {
12689 "revoked": {
12690 "type": "integer",
12691 "format": "int64",
12692 "minimum": 0
12693 }
12694 }
12695 },
12696 "RunnerDto": {
12697 "type": "object",
12698 "required": [
12699 "id",
12700 "name",
12701 "labels",
12702 "capacity",
12703 "capabilities",
12704 "state",
12705 "active_jobs",
12706 "created_at"
12707 ],
12708 "properties": {
12709 "active_jobs": {
12710 "type": "integer",
12711 "format": "int64"
12712 },
12713 "capabilities": {
12714 "type": "array",
12715 "items": {
12716 "type": "string"
12717 }
12718 },
12719 "capacity": {
12720 "type": "integer",
12721 "format": "int32"
12722 },
12723 "created_at": {
12724 "type": "string",
12725 "format": "date-time"
12726 },
12727 "id": {
12728 "type": "string",
12729 "format": "uuid"
12730 },
12731 "labels": {
12732 "type": "array",
12733 "items": {
12734 "type": "string"
12735 }
12736 },
12737 "last_seen_at": {
12738 "type": [
12739 "string",
12740 "null"
12741 ],
12742 "format": "date-time"
12743 },
12744 "name": {
12745 "type": "string"
12746 },
12747 "protocol_version": {
12748 "type": [
12749 "integer",
12750 "null"
12751 ],
12752 "format": "int32"
12753 },
12754 "space_id": {
12755 "type": [
12756 "string",
12757 "null"
12758 ],
12759 "format": "uuid"
12760 },
12761 "state": {
12762 "type": "string"
12763 },
12764 "version": {
12765 "type": [
12766 "string",
12767 "null"
12768 ]
12769 }
12770 }
12771 },
12772 "RunnerPageDto": {
12773 "type": "object",
12774 "required": [
12775 "items",
12776 "total",
12777 "limit",
12778 "offset"
12779 ],
12780 "properties": {
12781 "items": {
12782 "type": "array",
12783 "items": {
12784 "$ref": "#/components/schemas/RunnerDto"
12785 }
12786 },
12787 "limit": {
12788 "type": "integer",
12789 "format": "int64",
12790 "minimum": 0
12791 },
12792 "offset": {
12793 "type": "integer",
12794 "format": "int64",
12795 "minimum": 0
12796 },
12797 "total": {
12798 "type": "integer",
12799 "format": "int64",
12800 "minimum": 0
12801 }
12802 }
12803 },
12804 "RunnerTokenDto": {
12805 "type": "object",
12806 "required": [
12807 "id",
12808 "name",
12809 "single_use",
12810 "created_at"
12811 ],
12812 "properties": {
12813 "created_at": {
12814 "type": "string",
12815 "format": "date-time"
12816 },
12817 "expires_at": {
12818 "type": [
12819 "string",
12820 "null"
12821 ],
12822 "format": "date-time"
12823 },
12824 "id": {
12825 "type": "string",
12826 "format": "uuid"
12827 },
12828 "name": {
12829 "type": "string"
12830 },
12831 "revoked_at": {
12832 "type": [
12833 "string",
12834 "null"
12835 ],
12836 "format": "date-time"
12837 },
12838 "single_use": {
12839 "type": "boolean"
12840 },
12841 "space_id": {
12842 "type": [
12843 "string",
12844 "null"
12845 ],
12846 "format": "uuid"
12847 },
12848 "used_at": {
12849 "type": [
12850 "string",
12851 "null"
12852 ],
12853 "format": "date-time"
12854 }
12855 }
12856 },
12857 "SearchConfigDto": {
12858 "type": "object",
12859 "required": [
12860 "enabled",
12861 "index_prefix",
12862 "max_file_bytes",
12863 "batch_size",
12864 "poll_interval_secs"
12865 ],
12866 "properties": {
12867 "batch_size": {
12868 "type": "integer",
12869 "minimum": 0
12870 },
12871 "enabled": {
12872 "type": "boolean"
12873 },
12874 "index_prefix": {
12875 "type": "string"
12876 },
12877 "max_file_bytes": {
12878 "type": "integer",
12879 "minimum": 0
12880 },
12881 "poll_interval_secs": {
12882 "type": "integer",
12883 "format": "int64",
12884 "minimum": 0
12885 }
12886 }
12887 },
12888 "SessionConfigDto": {
12889 "type": "object",
12890 "required": [
12891 "mode",
12892 "cookie_same_site"
12893 ],
12894 "properties": {
12895 "cookie_domain": {
12896 "type": [
12897 "string",
12898 "null"
12899 ]
12900 },
12901 "cookie_name": {
12902 "type": [
12903 "string",
12904 "null"
12905 ]
12906 },
12907 "cookie_same_site": {
12908 "type": "string"
12909 },
12910 "cookie_secure": {
12911 "type": [
12912 "boolean",
12913 "null"
12914 ]
12915 },
12916 "lifespan_secs": {
12917 "type": [
12918 "integer",
12919 "null"
12920 ],
12921 "format": "int64"
12922 },
12923 "mode": {
12924 "type": "string"
12925 },
12926 "table_name": {
12927 "type": [
12928 "string",
12929 "null"
12930 ]
12931 }
12932 }
12933 },
12934 "StorageCategoryDto": {
12935 "type": "object",
12936 "required": [
12937 "name",
12938 "prefix",
12939 "bytes",
12940 "objects"
12941 ],
12942 "properties": {
12943 "bytes": {
12944 "type": "integer",
12945 "format": "int64",
12946 "minimum": 0
12947 },
12948 "name": {
12949 "type": "string"
12950 },
12951 "objects": {
12952 "type": "integer",
12953 "format": "int64",
12954 "minimum": 0
12955 },
12956 "prefix": {
12957 "type": "string"
12958 }
12959 }
12960 },
12961 "StorageConfigDto": {
12962 "type": "object",
12963 "properties": {
12964 "bucket": {
12965 "type": [
12966 "string",
12967 "null"
12968 ]
12969 },
12970 "path_style": {
12971 "type": [
12972 "boolean",
12973 "null"
12974 ]
12975 },
12976 "region": {
12977 "type": [
12978 "string",
12979 "null"
12980 ]
12981 }
12982 }
12983 },
12984 "StorageObjectDto": {
12985 "type": "object",
12986 "required": [
12987 "key",
12988 "size"
12989 ],
12990 "properties": {
12991 "etag": {
12992 "type": [
12993 "string",
12994 "null"
12995 ]
12996 },
12997 "key": {
12998 "type": "string"
12999 },
13000 "last_modified": {},
13001 "size": {
13002 "type": "integer",
13003 "format": "int64",
13004 "minimum": 0
13005 }
13006 }
13007 },
13008 "StorageObjectPageDto": {
13009 "type": "object",
13010 "required": [
13011 "prefix",
13012 "items"
13013 ],
13014 "properties": {
13015 "items": {
13016 "type": "array",
13017 "items": {
13018 "$ref": "#/components/schemas/StorageObjectDto"
13019 }
13020 },
13021 "next_token": {
13022 "type": [
13023 "string",
13024 "null"
13025 ]
13026 },
13027 "prefix": {
13028 "type": "string"
13029 }
13030 }
13031 },
13032 "StorageOverviewDto": {
13033 "type": "object",
13034 "required": [
13035 "bucket",
13036 "total_bytes",
13037 "total_objects",
13038 "categories",
13039 "generated_at",
13040 "cached"
13041 ],
13042 "properties": {
13043 "bucket": {
13044 "type": "string"
13045 },
13046 "cached": {
13047 "type": "boolean"
13048 },
13049 "categories": {
13050 "type": "array",
13051 "items": {
13052 "$ref": "#/components/schemas/StorageCategoryDto"
13053 }
13054 },
13055 "generated_at": {
13056 "type": "string",
13057 "format": "date-time"
13058 },
13059 "total_bytes": {
13060 "type": "integer",
13061 "format": "int64",
13062 "minimum": 0
13063 },
13064 "total_objects": {
13065 "type": "integer",
13066 "format": "int64",
13067 "minimum": 0
13068 }
13069 }
13070 },
13071 "SuspendUserCmd": {
13072 "type": "object",
13073 "required": [
13074 "suspension_type"
13075 ],
13076 "properties": {
13077 "ends_at": {
13078 "type": [
13079 "string",
13080 "null"
13081 ],
13082 "format": "date-time"
13083 },
13084 "reason": {
13085 "type": [
13086 "string",
13087 "null"
13088 ]
13089 },
13090 "suspension_type": {
13091 "$ref": "#/components/schemas/SuspensionType"
13092 }
13093 }
13094 },
13095 "SuspensionType": {
13096 "type": "string",
13097 "enum": [
13098 "temporary",
13099 "permanent"
13100 ]
13101 },
13102 "SystemConfigDto": {
13103 "type": "object",
13104 "required": [
13105 "version",
13106 "hosts",
13107 "admin",
13108 "features",
13109 "search",
13110 "import",
13111 "git",
13112 "storage",
13113 "database",
13114 "redis",
13115 "session",
13116 "email",
13117 "generated_at"
13118 ],
13119 "properties": {
13120 "admin": {
13121 "$ref": "#/components/schemas/AdminEndpointDto"
13122 },
13123 "database": {
13124 "$ref": "#/components/schemas/DatabaseConfigDto"
13125 },
13126 "email": {
13127 "$ref": "#/components/schemas/EmailConfigDto"
13128 },
13129 "features": {
13130 "$ref": "#/components/schemas/FeaturesDto"
13131 },
13132 "generated_at": {
13133 "type": "string",
13134 "format": "date-time"
13135 },
13136 "git": {
13137 "$ref": "#/components/schemas/GitConfigDto"
13138 },
13139 "hosts": {
13140 "type": "array",
13141 "items": {
13142 "$ref": "#/components/schemas/HostEntryDto"
13143 }
13144 },
13145 "import": {
13146 "$ref": "#/components/schemas/ImportConfigDto"
13147 },
13148 "redis": {
13149 "$ref": "#/components/schemas/RedisConfigDto"
13150 },
13151 "search": {
13152 "$ref": "#/components/schemas/SearchConfigDto"
13153 },
13154 "session": {
13155 "$ref": "#/components/schemas/SessionConfigDto"
13156 },
13157 "storage": {
13158 "$ref": "#/components/schemas/StorageConfigDto"
13159 },
13160 "version": {
13161 "type": "string"
13162 }
13163 }
13164 },
13165 "SystemStatusDto": {
13166 "type": "object",
13167 "required": [
13168 "process",
13169 "dependencies",
13170 "features",
13171 "counts",
13172 "queues",
13173 "recent_failures",
13174 "generated_at"
13175 ],
13176 "properties": {
13177 "counts": {
13178 "$ref": "#/components/schemas/CountsDto"
13179 },
13180 "dependencies": {
13181 "$ref": "#/components/schemas/DependenciesDto"
13182 },
13183 "features": {
13184 "$ref": "#/components/schemas/FeaturesDto"
13185 },
13186 "generated_at": {
13187 "type": "string",
13188 "format": "date-time"
13189 },
13190 "process": {
13191 "$ref": "#/components/schemas/ProcessInfoDto"
13192 },
13193 "queues": {
13194 "type": "array",
13195 "items": {
13196 "$ref": "#/components/schemas/QueueSnapshotDto"
13197 }
13198 },
13199 "recent_failures": {
13200 "type": "array",
13201 "items": {
13202 "$ref": "#/components/schemas/JobLogDto"
13203 }
13204 }
13205 }
13206 },
13207 "TaskActionResp": {
13208 "type": "object",
13209 "required": [
13210 "source",
13211 "id",
13212 "state"
13213 ],
13214 "properties": {
13215 "id": {
13216 "type": "string",
13217 "format": "uuid"
13218 },
13219 "source": {
13220 "type": "string"
13221 },
13222 "state": {
13223 "type": "string"
13224 }
13225 }
13226 },
13227 "TaskDetailDto": {
13228 "type": "object",
13229 "required": [
13230 "task",
13231 "detail"
13232 ],
13233 "properties": {
13234 "detail": {},
13235 "task": {
13236 "$ref": "#/components/schemas/TaskDto"
13237 }
13238 }
13239 },
13240 "TaskDto": {
13241 "type": "object",
13242 "required": [
13243 "id",
13244 "source",
13245 "title",
13246 "state",
13247 "created_at"
13248 ],
13249 "properties": {
13250 "created_at": {
13251 "type": "string",
13252 "format": "date-time"
13253 },
13254 "finished_at": {
13255 "type": [
13256 "string",
13257 "null"
13258 ],
13259 "format": "date-time"
13260 },
13261 "id": {
13262 "type": "string",
13263 "format": "uuid"
13264 },
13265 "last_error": {
13266 "type": [
13267 "string",
13268 "null"
13269 ]
13270 },
13271 "source": {
13272 "type": "string"
13273 },
13274 "state": {
13275 "type": "string"
13276 },
13277 "title": {
13278 "type": "string"
13279 },
13280 "updated_at": {
13281 "type": [
13282 "string",
13283 "null"
13284 ],
13285 "format": "date-time"
13286 }
13287 }
13288 },
13289 "TaskPageDto": {
13290 "type": "object",
13291 "required": [
13292 "items",
13293 "total",
13294 "limit",
13295 "offset"
13296 ],
13297 "properties": {
13298 "items": {
13299 "type": "array",
13300 "items": {
13301 "$ref": "#/components/schemas/TaskDto"
13302 }
13303 },
13304 "limit": {
13305 "type": "integer",
13306 "format": "int64",
13307 "minimum": 0
13308 },
13309 "offset": {
13310 "type": "integer",
13311 "format": "int64",
13312 "minimum": 0
13313 },
13314 "total": {
13315 "type": "integer",
13316 "format": "int64"
13317 }
13318 }
13319 },
13320 "TaskSourceStatDto": {
13321 "type": "object",
13322 "required": [
13323 "source",
13324 "queued",
13325 "running",
13326 "succeeded",
13327 "failed"
13328 ],
13329 "properties": {
13330 "failed": {
13331 "type": "integer",
13332 "format": "int64"
13333 },
13334 "queued": {
13335 "type": "integer",
13336 "format": "int64"
13337 },
13338 "running": {
13339 "type": "integer",
13340 "format": "int64"
13341 },
13342 "source": {
13343 "type": "string"
13344 },
13345 "succeeded": {
13346 "type": "integer",
13347 "format": "int64"
13348 }
13349 }
13350 },
13351 "TaskStatsDto": {
13352 "type": "object",
13353 "required": [
13354 "sources",
13355 "total",
13356 "generated_at"
13357 ],
13358 "properties": {
13359 "generated_at": {
13360 "type": "string",
13361 "format": "date-time"
13362 },
13363 "sources": {
13364 "type": "array",
13365 "items": {
13366 "$ref": "#/components/schemas/TaskSourceStatDto"
13367 }
13368 },
13369 "total": {
13370 "type": "integer",
13371 "format": "int64"
13372 }
13373 }
13374 },
13375 "TransferSpaceCmd": {
13376 "type": "object",
13377 "required": [
13378 "new_owner_id"
13379 ],
13380 "properties": {
13381 "new_owner_id": {
13382 "type": "string",
13383 "format": "uuid"
13384 }
13385 }
13386 },
13387 "TrendPointDto": {
13388 "type": "object",
13389 "required": [
13390 "date",
13391 "users",
13392 "spaces",
13393 "repos"
13394 ],
13395 "properties": {
13396 "date": {
13397 "type": "string"
13398 },
13399 "repos": {
13400 "type": "integer",
13401 "format": "int64"
13402 },
13403 "spaces": {
13404 "type": "integer",
13405 "format": "int64"
13406 },
13407 "users": {
13408 "type": "integer",
13409 "format": "int64"
13410 }
13411 }
13412 },
13413 "UpdateAdminCmd": {
13414 "type": "object",
13415 "properties": {
13416 "display_name": {
13417 "type": [
13418 "string",
13419 "null"
13420 ]
13421 },
13422 "is_active": {
13423 "type": [
13424 "boolean",
13425 "null"
13426 ]
13427 },
13428 "role": {
13429 "oneOf": [
13430 {
13431 "type": "null"
13432 },
13433 {
13434 "$ref": "#/components/schemas/AdminRole"
13435 }
13436 ]
13437 }
13438 }
13439 },
13440 "UpdateChatRoomCmd": {
13441 "type": "object",
13442 "properties": {
13443 "is_active": {
13444 "type": [
13445 "boolean",
13446 "null"
13447 ]
13448 },
13449 "is_private": {
13450 "type": [
13451 "boolean",
13452 "null"
13453 ]
13454 },
13455 "name": {
13456 "type": [
13457 "string",
13458 "null"
13459 ]
13460 },
13461 "nsfw": {
13462 "type": [
13463 "boolean",
13464 "null"
13465 ]
13466 },
13467 "topic": {
13468 "type": [
13469 "string",
13470 "null"
13471 ]
13472 }
13473 }
13474 },
13475 "UpdateRepoCmd": {
13476 "type": "object",
13477 "properties": {
13478 "default_branch": {
13479 "type": [
13480 "string",
13481 "null"
13482 ]
13483 },
13484 "description": {
13485 "type": [
13486 "string",
13487 "null"
13488 ]
13489 },
13490 "enable_lfs": {
13491 "type": [
13492 "boolean",
13493 "null"
13494 ]
13495 },
13496 "is_archived": {
13497 "type": [
13498 "boolean",
13499 "null"
13500 ]
13501 },
13502 "is_private": {
13503 "type": [
13504 "boolean",
13505 "null"
13506 ]
13507 },
13508 "storage_limit_size": {
13509 "type": [
13510 "integer",
13511 "null"
13512 ],
13513 "format": "int64"
13514 }
13515 }
13516 },
13517 "UpdateRunnerCmd": {
13518 "type": "object",
13519 "properties": {
13520 "capacity": {
13521 "type": [
13522 "integer",
13523 "null"
13524 ],
13525 "format": "int32"
13526 },
13527 "enabled": {
13528 "type": [
13529 "boolean",
13530 "null"
13531 ]
13532 },
13533 "labels": {
13534 "type": [
13535 "array",
13536 "null"
13537 ],
13538 "items": {
13539 "type": "string"
13540 }
13541 }
13542 }
13543 },
13544 "UpdateSpaceCmd": {
13545 "type": "object",
13546 "properties": {
13547 "allow_invite": {
13548 "type": [
13549 "boolean",
13550 "null"
13551 ]
13552 },
13553 "allow_join": {
13554 "type": [
13555 "boolean",
13556 "null"
13557 ]
13558 },
13559 "description": {
13560 "type": [
13561 "string",
13562 "null"
13563 ]
13564 },
13565 "is_active": {
13566 "type": [
13567 "boolean",
13568 "null"
13569 ]
13570 },
13571 "is_private": {
13572 "type": [
13573 "boolean",
13574 "null"
13575 ]
13576 },
13577 "member_must_enable_2fa": {
13578 "type": [
13579 "boolean",
13580 "null"
13581 ]
13582 },
13583 "name": {
13584 "type": [
13585 "string",
13586 "null"
13587 ]
13588 }
13589 }
13590 },
13591 "UpdateUserCmd": {
13592 "type": "object",
13593 "properties": {
13594 "display_name": {
13595 "type": [
13596 "string",
13597 "null"
13598 ]
13599 },
13600 "is_active": {
13601 "type": [
13602 "boolean",
13603 "null"
13604 ]
13605 },
13606 "is_restricted": {
13607 "type": [
13608 "boolean",
13609 "null"
13610 ]
13611 },
13612 "prohibit_login": {
13613 "type": [
13614 "boolean",
13615 "null"
13616 ]
13617 }
13618 }
13619 },
13620 "UpsertEnvironmentAdminCmd": {
13621 "type": "object",
13622 "required": [
13623 "name"
13624 ],
13625 "properties": {
13626 "description": {
13627 "type": [
13628 "string",
13629 "null"
13630 ]
13631 },
13632 "name": {
13633 "type": "string"
13634 },
13635 "required_labels": {
13636 "type": "array",
13637 "items": {
13638 "type": "string"
13639 }
13640 }
13641 }
13642 },
13643 "UpsertEnvironmentSecretCmd": {
13644 "type": "object",
13645 "required": [
13646 "name",
13647 "value"
13648 ],
13649 "properties": {
13650 "name": {
13651 "type": "string"
13652 },
13653 "value": {
13654 "type": "string"
13655 }
13656 }
13657 },
13658 "WorkflowRunState": {
13659 "type": "string",
13660 "enum": [
13661 "queued",
13662 "running",
13663 "success",
13664 "failure",
13665 "cancel_requested",
13666 "cancelled"
13667 ]
13668 }
13669 },
13670 "securitySchemes": {
13671 "admin_session_cookie": {
13672 "type": "apiKey",
13673 "in": "cookie",
13674 "name": "gitdata_admin_session"
13675 }
13676 }
13677 },
13678 "security": [
13679 {
13680 "admin_session_cookie": []
13681 }
13682 ]
13683}
No newline at end of file
Deleted admin/package.json
@@ -1,27 +0,0 @@
1{
2 "name": "gitdata-admin",
3 "private": true,
4 "version": "0.0.0",
5 "type": "module",
6 "scripts": {
7 "dev": "vite",
8 "build": "tsc -b && vite build",
9 "preview": "vite preview"
10 },
11 "dependencies": {
12 "@tailwindcss/vite": "^4.3.3",
13 "lucide-react": "^1.39.0",
14 "react": "^19.2.8",
15 "react-dom": "^19.2.8",
16 "react-router-dom": "^7.18.3",
17 "tailwindcss": "^4.3.3"
18 },
19 "devDependencies": {
20 "@types/node": "^24.13.3",
21 "@types/react": "^19.2.18",
22 "@types/react-dom": "^19.2.4",
23 "@vitejs/plugin-react": "^6.1.0",
24 "typescript": "~6.0.2",
25 "vite": "^8.2.2"
26 }
27}
No newline at end of file
Deleted admin/tsconfig.app.json

Diff too large to display.

Deleted admin/tsconfig.json

Diff too large to display.

Deleted admin/tsconfig.node.json

Diff too large to display.

Deleted admin/vite.config.ts

Diff too large to display.

Modified app/adminhost/src/main.rs View app/adminhost/src/main.rs
@@ -1,61 +1,12 @@
1use std::collections::HashMap;
2use std::hash::{Hash, Hasher};
31use std::net::SocketAddr;
4use std::sync::OnceLock;
52
63use admin::AdminService;
74use anyhow::Context;
85use axum::Router;
9use axum::http::{HeaderMap, HeaderValue, Method, StatusCode, Uri, header};
10use axum::response::IntoResponse;
116use config::scoped::{AdminhostConfig, ScopedConfig};
12use session_layer::{SessionLayer, store};
137use tokio::net::TcpListener;
14use tower_http::limit::RequestBodyLimitLayer;
158use tower_http::trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer};
169use tracing::{Level, info, warn};
17
18const MAX_REQUEST_BODY_BYTES: usize = 2 * 1024 * 1024;
19const CACHE_IMMUTABLE: &str = "public, max-age=31536000, immutable";
20const CACHE_NO_CACHE: &str = "no-cache";
21const CACHE_REVALIDATE: &str = "public, max-age=3600, must-revalidate";
22
23fn build_cors_layer(config: &config::admin::AdminConfig) -> tower_http::cors::CorsLayer {
24 use axum::http::HeaderName;
25 use tower_http::cors::{AllowOrigin, CorsLayer};
26
27 if config.cors_allow_origins.is_empty() {
28 return CorsLayer::new();
29 }
30 let origins: Vec<HeaderValue> = config
31 .cors_allow_origins
32 .iter()
33 .filter_map(|origin| origin.parse().ok())
34 .collect();
35 CorsLayer::new()
36 .allow_origin(AllowOrigin::list(origins))
37 .allow_credentials(config.cors_allow_credentials())
38 .allow_methods([
39 Method::GET,
40 Method::POST,
41 Method::PUT,
42 Method::PATCH,
43 Method::DELETE,
44 Method::OPTIONS,
45 ])
46 .allow_headers([
47 header::CONTENT_TYPE,
48 header::AUTHORIZATION,
49 HeaderName::from_static("x-request-id"),
50 ])
51}
52
53fn admin_session_config(cfg: &AdminhostConfig) -> config::session::SessionConfig {
54 let mut session = cfg.session.clone();
55 session.cookie_name = Some(cfg.admin.session_cookie_name().to_string());
56 session.table_name = Some(cfg.admin.session_table_name().to_string());
57 session
58}
5910
6011#[tokio::main]
6112async fn main() -> anyhow::Result<()> {
@@ -74,28 +25,37 @@
7425 info!("dependencies ready");
7526
7627 let health = export::health::router_with(Some(state.database.clone()), state.redis.clone());
77 let session_store = store::connect(state.redis.clone(), &admin_session_config(&cfg))
78 .await
79 .context("connecting admin session store")?;
28 let service = AdminService::new(state);
8029
81 let service = AdminService::new(state);
82 match service.seed_admin_if_empty().await {
83 Ok(Some(admin_id)) => info!(%admin_id, "initial admin account seeded"),
84 Ok(None) => {}
85 Err(error) => warn!(%error, "admin seed failed"),
30 let grpc_addr: SocketAddr = format!("{}:{}", cfg.admin.bind_addr(), cfg.admin.grpc_port())
31 .parse()
32 .with_context(|| {
33 format!(
34 "parsing gRPC bind address {}:{}",
35 cfg.admin.bind_addr(),
36 cfg.admin.grpc_port()
37 )
38 })?;
39 let trusted_keys = cfg.admin.grpc_trusted_keys().to_vec();
40 if trusted_keys.is_empty() {
41 warn!("admin.grpc_trusted_keys is empty; every gRPC call will be rejected");
8642 }
43 let grpc_service = service.clone();
44 tokio::spawn(async move {
45 if let Err(error) = admin_rpc::serve(
46 grpc_service,
47 grpc_addr,
48 trusted_keys,
49 export::telemetry::shutdown_signal(),
50 )
51 .await
52 {
53 warn!(%error, "admin gRPC server failed");
54 }
55 });
56 info!(%grpc_addr, "admin gRPC listening");
8757
88 let api = admin::routes::router(service.clone()).with_state(service);
89 let doc = admin::openapi::api_doc();
90
91 let router: Router = Router::new()
92 .nest("/api/v1", api)
93 .merge(health)
94 .merge(utoipa_swagger_ui::SwaggerUi::new("/api/docs").url("/api/docs/openapi.json", doc))
95 .fallback(spa_fallback)
96 .layer(build_cors_layer(&cfg.admin))
97 .layer(SessionLayer::new(session_store))
98 .layer(RequestBodyLimitLayer::new(MAX_REQUEST_BODY_BYTES))
58 let router: Router = health
9959 .layer(
10060 TraceLayer::new_for_http()
10161 .make_span_with(
@@ -124,101 +84,4 @@
12484
12585 info!("adminhost shut down");
12686 Ok(())
127}
128
129async fn spa_fallback(method: Method, uri: Uri, headers: HeaderMap) -> axum::response::Response {
130 if !matches!(method, Method::GET | Method::HEAD) {
131 return StatusCode::METHOD_NOT_ALLOWED.into_response();
132 }
133 let path = uri.path();
134 if path.starts_with("/api") {
135 return StatusCode::NOT_FOUND.into_response();
136 }
137 let asset = match admin_web::file(path) {
138 Some(asset) => asset,
139 None => {
140 if path.rsplit('/').next().is_some_and(|seg| seg.contains('.')) {
141 return StatusCode::NOT_FOUND.into_response();
142 }
143 admin_web::index()
144 }
145 };
146 serve_asset(asset, &headers)
147}
148
149fn serve_asset(asset: admin_web::Asset, headers: &HeaderMap) -> axum::response::Response {
150 let etag = etags()
151 .get(asset.path)
152 .cloned()
153 .unwrap_or_else(|| etag_of(asset.content));
154 let cache_control = HeaderValue::from_static(cache_control(asset.path));
155
156 if let Some(if_none_match) = headers.get(header::IF_NONE_MATCH)
157 && etag_matches(if_none_match, &etag)
158 {
159 return (
160 StatusCode::NOT_MODIFIED,
161 [(header::CACHE_CONTROL, cache_control), (header::ETAG, etag)],
162 )
163 .into_response();
164 }
165
166 (
167 [
168 (header::CONTENT_TYPE, HeaderValue::from_static(asset.mime)),
169 (header::CACHE_CONTROL, cache_control),
170 (header::ETAG, etag),
171 ],
172 asset.content,
173 )
174 .into_response()
175}
176
177fn etags() -> &'static HashMap<&'static str, HeaderValue> {
178 static ETAGS: OnceLock<HashMap<&'static str, HeaderValue>> = OnceLock::new();
179 ETAGS.get_or_init(|| {
180 let mut map = HashMap::new();
181 let index = admin_web::index();
182 map.insert(index.path, etag_of(index.content));
183 for path in admin_web::paths() {
184 if let Some(asset) = admin_web::file(path) {
185 map.insert(asset.path, etag_of(asset.content));
186 }
187 }
188 map
189 })
190}
191
192fn etag_of(content: &[u8]) -> HeaderValue {
193 let mut hasher = std::collections::hash_map::DefaultHasher::new();
194 content.hash(&mut hasher);
195 hasher.write_u64(content.len() as u64);
196 HeaderValue::from_maybe_shared(format!("W/\"{:016x}\"", hasher.finish()).into_bytes())
197 .expect("valid etag header")
198}
199
200fn cache_control(path: &str) -> &'static str {
201 if path.starts_with("assets/") {
202 CACHE_IMMUTABLE
203 } else if path == "index.html" {
204 CACHE_NO_CACHE
205 } else {
206 CACHE_REVALIDATE
207 }
208}
209
210fn etag_matches(if_none_match: &HeaderValue, etag: &HeaderValue) -> bool {
211 let Ok(value) = if_none_match.to_str() else {
212 return false;
213 };
214 let Ok(target) = etag.to_str() else {
215 return false;
216 };
217 fn strong(value: &str) -> &str {
218 value.strip_prefix("W/").unwrap_or(value)
219 }
220 value
221 .split(',')
222 .map(str::trim)
223 .any(|candidate| candidate == "*" || strong(candidate) == strong(target))
22487}
Modified app/adminhost/Cargo.toml View app/adminhost/Cargo.toml
@@ -6,13 +6,11 @@
66
77[dependencies]
88admin = { workspace = true }
9admin-web = { workspace = true }
9admin-rpc = { workspace = true }
1010config = { workspace = true }
1111export = { workspace = true }
12session-layer = { workspace = true }
1312axum = { workspace = true }
1413anyhow = { workspace = true }
1514tokio = { workspace = true, features = ["signal", "net", "macros", "rt-multi-thread"] }
1615tower-http = { workspace = true }
17tracing = { workspace = true }
18utoipa-swagger-ui = { workspace = true }
No newline at end of file
16tracing = { workspace = true }
No newline at end of file
Modified app/openapi/src/main.rs View app/openapi/src/main.rs
@@ -32,11 +32,6 @@
3232 let doc = webapi::openapi::api_doc();
3333 doc.to_pretty_json().context("serializing web openapi doc")
3434 })?;
35 let admin = generate(|| {
36 let doc = admin::openapi::api_doc();
37 doc.to_pretty_json()
38 .context("serializing admin openapi doc")
39 })?;
4035
4136 let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
4237 let root = crate_dir
@@ -44,19 +39,15 @@
4439 .nth(2)
4540 .ok_or_else(|| anyhow!("workspace root not found above {}", crate_dir.display()))?;
4641 let web_path = root.join("openapi.json");
47 let admin_path = root.join("admin").join("openapi.json");
4842
4943 let check = std::env::args().nth(1).as_deref() == Some("--check");
5044 if check {
5145 check_file(&web_path, &web)?;
52 check_file(&admin_path, &admin)?;
53 println!("openapi.json and admin/openapi.json are up to date");
46 println!("openapi.json is up to date");
5447 return Ok(());
5548 }
5649
5750 std::fs::write(&web_path, web).with_context(|| format!("writing {}", web_path.display()))?;
58 std::fs::write(&admin_path, admin)
59 .with_context(|| format!("writing {}", admin_path.display()))?;
60 println!("wrote {} and {}", web_path.display(), admin_path.display());
51 println!("wrote {}", web_path.display());
6152 Ok(())
6253}
Modified app/openapi/Cargo.toml View app/openapi/Cargo.toml
@@ -5,6 +5,5 @@
55publish = false
66
77[dependencies]
8admin = { workspace = true }
98webapi = { workspace = true }
109anyhow = { workspace = true }
Modified chart/templates/_helpers.tpl View chart/templates/_helpers.tpl
@@ -66,6 +66,8 @@
6666 value: {{ .Values.githost.httpPort | quote }}
6767- name: APP_IM__PORT
6868 value: {{ .Values.im.port | quote }}
69- name: APP_MCP__PORT
70 value: {{ .Values.mcp.port | quote }}
6971- name: APP_HEALTH__ENABLED
7072 value: {{ .Values.health.enabled | quote }}
7173- name: APP_HEALTH__SYNCHOST__PORT
Modified chart/templates/adminhost.yaml View chart/templates/adminhost.yaml
@@ -48,12 +48,18 @@
4848 {{- include "gitdata.envFrom" . | nindent 10 }}
4949 env:
5050 {{- include "gitdata.env" . | nindent 12 }}
51 {{- with .Values.adminhost.grpcTrustedKeys }}
52 - name: APP_ADMIN__GRPC_TRUSTED_KEYS
53 value: {{ join "," . | quote }}
54 {{- end }}
5155 {{- with .Values.adminhost.extraEnv }}
5256 {{- toYaml . | nindent 12 }}
5357 {{- end }}
5458 ports:
5559 - name: http
5660 containerPort: {{ .Values.adminhost.port }}
61 - name: grpc
62 containerPort: {{ .Values.adminhost.grpcPort }}
5763 livenessProbe:
5864 httpGet: { path: /healthz, port: http }
5965 initialDelaySeconds: 10
@@ -86,42 +92,7 @@
8692 - name: http
8793 port: {{ .Values.adminhost.port }}
8894 targetPort: http
89{{- if .Values.adminhost.ingress.enabled }}
90---
91apiVersion: networking.k8s.io/v1
92kind: Ingress
93metadata:
94 name: {{ printf "%s-adminhost" (include "gitdata.fullname" .) }}
95 labels:
96 {{- include "gitdata.labels" . | nindent 4 }}
97 app.kubernetes.io/component: adminhost
98 annotations:
99 {{- with .Values.adminhost.ingress.annotations }}
100 {{- toYaml . | nindent 4 }}
101 {{- end }}
102 {{- if and .Values.adminhost.ingress.tls.enabled .Values.adminhost.ingress.tls.clusterIssuer }}
103 cert-manager.io/cluster-issuer: {{ .Values.adminhost.ingress.tls.clusterIssuer }}
104 {{- end }}
105spec:
106 {{- with .Values.adminhost.ingress.className }}
107 ingressClassName: {{ . }}
108 {{- end }}
109 {{- if .Values.adminhost.ingress.tls.enabled }}
110 tls:
111 - hosts:
112 - {{ .Values.adminhost.ingress.host | quote }}
113 secretName: {{ default (printf "%s-adminhost-tls" (include "gitdata.fullname" .)) .Values.adminhost.ingress.tls.secretName }}
114 {{- end }}
115 rules:
116 - host: {{ .Values.adminhost.ingress.host | quote }}
117 http:
118 paths:
119 - path: /
120 pathType: Prefix
121 backend:
122 service:
123 name: {{ printf "%s-adminhost" (include "gitdata.fullname" .) }}
124 port:
125 name: http
126{{- end }}
95 - name: grpc
96 port: {{ .Values.adminhost.grpcPort }}
97 targetPort: grpc
12798{{- end }}
No newline at end of file
Modified chart/values.yaml View chart/values.yaml
@@ -66,8 +66,6 @@
6666 APP_AUTH__CHAT_JWT_SEC: "" # >=32 字节,chat WS 握手 JWT 密钥
6767 APP_SEARCH__API_KEY: ""
6868 APP_IMPORT__TOKEN_SECRET: "" # >=32 字节,加密导入任务里的源站令牌
69 APP_ADMIN__SEED_USERNAME: "" # 首次启动种子管理员账号(admin_user 为空时创建)
70 APP_ADMIN__SEED_PASSWORD: "" # 种子管理员口令,首次登录强制修改
7169 APP_CI__SECRET_ENCRYPTION_KEY: "" # base64 >=32 字节,CI secrets / webhook 加密密钥
7270 APP_PUSH__VAPID_PRIVATE_KEY: "" # base64url 32 字节 P-256 标量,Web Push 签名私钥
7371
@@ -125,19 +123,12 @@
125123 enabled: true
126124 replicaCount: 1
127125 port: 3004
126 grpcPort: 3014
127 grpcTrustedKeys: []
128128 resources:
129129 requests: { cpu: 100m, memory: 128Mi }
130130 limits: { memory: 512Mi }
131131 extraEnv: []
132 # 管理端独立域名 Ingress(同源提供 SPA + /api/v1/admin)
133 ingress:
134 enabled: false
135 host: admin.gitdata.example.com
136 className: ""
137 tls:
138 enabled: false
139 secretName: ""
140 clusterIssuer: ""
141132
142133githost:
143134 enabled: true
Modified config/default.toml View config/default.toml
@@ -283,10 +283,8 @@
283283enabled = true
284284bind_addr = "0.0.0.0"
285285port = 3004
286seed_username = ""
287seed_password = ""
288session_cookie_name = "gitdata_admin_session"
289session_table_name = "admin_session"
286grpc_port = 3014
287grpc_trusted_keys = []
290288cors_allow_origins = []
291289cors_allow_credentials = false
292290
Deleted lib/admin/src/handlers/act.rs

Diff too large to display.

Deleted lib/admin/src/handlers/admins.rs

Diff too large to display.

Deleted lib/admin/src/handlers/auth.rs

Diff too large to display.

Deleted lib/admin/src/handlers/chat.rs

Diff too large to display.

Deleted lib/admin/src/handlers/logs.rs

Diff too large to display.

Deleted lib/admin/src/handlers/mod.rs

Diff too large to display.

Deleted lib/admin/src/handlers/queues.rs

Diff too large to display.

Deleted lib/admin/src/handlers/repos.rs

Diff too large to display.

Deleted lib/admin/src/handlers/runners.rs

Diff too large to display.

Deleted lib/admin/src/handlers/spaces.rs

Diff too large to display.

Deleted lib/admin/src/handlers/storage.rs

Diff too large to display.

Deleted lib/admin/src/handlers/system.rs

Diff too large to display.

Deleted lib/admin/src/handlers/tasks.rs

Diff too large to display.

Deleted lib/admin/src/handlers/users.rs

Diff too large to display.

Deleted lib/admin/src/services/audit.rs

Diff too large to display.

Deleted lib/admin/src/services/auth.rs

Diff too large to display.

Modified lib/admin/src/services/chat.rs View lib/admin/src/services/chat.rs
@@ -1,7 +1,6 @@
11use std::collections::HashMap;
22
33use export::pagination::Page;
4use models::admin::admin_user::AdminRole;
54use models::chat::attachments::{self, AttachmentStatus};
65use models::chat::message_edits;
76use models::chat::messages::{self, AuthorType, ChatMessageType};
@@ -34,9 +33,8 @@
3433use uuid::Uuid;
3534
3635use crate::errors::AdminError;
37use crate::services::audit::{AuditCtx, AuditRecord};
3836use crate::services::service::AdminService;
39use crate::share::roles::AdminActor;
37use crate::share::roles::{AdminActor, AdminRole};
4038
4139pub const MAX_CHAT_PAGE: u64 = 200;
4240pub const MAX_BULK_CHAT_MESSAGES: usize = 100;
@@ -729,7 +727,6 @@
729727 actor: &AdminActor,
730728 room_id: Uuid,
731729 cmd: UpdateChatRoomCmd,
732 ctx: &AuditCtx,
733730 ) -> Result<AdminChatRoomDetail, AdminError> {
734731 actor.require(AdminRole::Operator)?;
735732 let row = rooms::Entity::find_by_id(room_id)
@@ -784,16 +781,6 @@
784781 }
785782 am.updated_at = Set(OffsetDateTime::now_utc());
786783 am.update(self.db().write()).await?;
787 self.audit(
788 Some(actor),
789 AuditRecord {
790 action: "chat.room_updated",
791 target: Some(("chat_room", room_id.to_string())),
792 detail: serde_json::Value::Object(changed),
793 },
794 ctx,
795 )
796 .await?;
797784 self.publish_room_event(room_id, json!({"kind": "room_updated", "room_id": room_id}))
798785 .await;
799786 self.get_chat_room(room_id).await
@@ -851,7 +838,6 @@
851838 actor: &AdminActor,
852839 room_id: Uuid,
853840 user_id: Uuid,
854 ctx: &AuditCtx,
855841 ) -> Result<RemoveChatMemberResp, AdminError> {
856842 actor.require(AdminRole::Operator)?;
857843 let row = room_members::Entity::find_by_id((room_id, user_id))
@@ -870,16 +856,6 @@
870856 am.left_at = Set(Some(now));
871857 am.updated_at = Set(Some(now));
872858 am.update(self.db().write()).await?;
873 self.audit(
874 Some(actor),
875 AuditRecord {
876 action: "chat.member_removed",
877 target: Some(("chat_room", room_id.to_string())),
878 detail: json!({ "user_id": user_id }),
879 },
880 ctx,
881 )
882 .await?;
883859 self.publish_room_event(
884860 room_id,
885861 json!({"kind": "member_left", "room_id": room_id, "user_id": user_id}),
@@ -1214,7 +1190,6 @@
12141190 &self,
12151191 actor: &AdminActor,
12161192 cmd: BulkDeleteChatMessagesCmd,
1217 ctx: &AuditCtx,
12181193 ) -> Result<BulkDeleteChatMessagesResp, AdminError> {
12191194 actor.require(AdminRole::Operator)?;
12201195 let mut ids = cmd.message_ids.clone();
@@ -1292,20 +1267,6 @@
12921267 )
12931268 .await;
12941269 }
1295 self.audit(
1296 Some(actor),
1297 AuditRecord {
1298 action: "chat.messages_deleted",
1299 target: Some(("chat_message", format!("{} items", succeeded.len()))),
1300 detail: json!({
1301 "succeeded": succeeded,
1302 "failed": failed.iter().map(|item| item.id.to_string()).collect::<Vec<_>>(),
1303 "reason": cmd.reason,
1304 }),
1305 },
1306 ctx,
1307 )
1308 .await?;
13091270 Ok(BulkDeleteChatMessagesResp { succeeded, failed })
13101271 }
13111272
@@ -1313,7 +1274,6 @@
13131274 &self,
13141275 actor: &AdminActor,
13151276 room_id: Uuid,
1316 ctx: &AuditCtx,
13171277 ) -> Result<PurgeChatMessagesResp, AdminError> {
13181278 actor.require(AdminRole::SuperAdmin)?;
13191279 rooms::Entity::find_by_id(room_id)
@@ -1401,20 +1361,6 @@
14011361 )
14021362 .exec(self.db().write())
14031363 .await?;
1404 self.audit(
1405 Some(actor),
1406 AuditRecord {
1407 action: "chat.messages_purged",
1408 target: Some(("chat_room", room_id.to_string())),
1409 detail: json!({
1410 "messages_deleted": messages_deleted,
1411 "attachments_deleted": attachments_deleted,
1412 "threads_deleted": threads_deleted,
1413 }),
1414 },
1415 ctx,
1416 )
1417 .await?;
14181364 Ok(PurgeChatMessagesResp {
14191365 room_id,
14201366 messages_deleted,
@@ -1427,10 +1373,9 @@
14271373 &self,
14281374 actor: &AdminActor,
14291375 room_id: Uuid,
1430 ctx: &AuditCtx,
14311376 ) -> Result<DeleteChatRoomResp, AdminError> {
14321377 actor.require(AdminRole::SuperAdmin)?;
1433 let room = rooms::Entity::find_by_id(room_id)
1378 rooms::Entity::find_by_id(room_id)
14341379 .one(self.db().read())
14351380 .await?
14361381 .ok_or(AdminError::NotFound)?;
@@ -1471,7 +1416,6 @@
14711416 let messages_deleted = message_ids.len() as u64;
14721417 let attachments_deleted = attachment_keys.len() as u64;
14731418 let threads_deleted = thread_ids.len() as u64;
1474 let room_name = room.name.clone();
14751419 self.db()
14761420 .primary()
14771421 .transaction::<_, (), AdminError>(move |txn| {
@@ -1559,22 +1503,6 @@
15591503 tracing::warn!(%error, %key, "admin chat room storage delete failed");
15601504 }
15611505 }
1562 self.audit(
1563 Some(actor),
1564 AuditRecord {
1565 action: "chat.room_deleted",
1566 target: Some(("chat_room", room_id.to_string())),
1567 detail: json!({
1568 "name": room_name,
1569 "messages_deleted": messages_deleted,
1570 "members_deleted": members_deleted,
1571 "attachments_deleted": attachments_deleted,
1572 "threads_deleted": threads_deleted,
1573 }),
1574 },
1575 ctx,
1576 )
1577 .await?;
15781506 Ok(DeleteChatRoomResp {
15791507 room_id,
15801508 messages_deleted,
@@ -1658,7 +1586,6 @@
16581586 &self,
16591587 actor: &AdminActor,
16601588 attachment_id: Uuid,
1661 ctx: &AuditCtx,
16621589 ) -> Result<DeleteChatAttachmentResp, AdminError> {
16631590 actor.require(AdminRole::SuperAdmin)?;
16641591 let row = attachments::Entity::find_by_id(attachment_id)
@@ -1690,20 +1617,6 @@
16901617 }
16911618 }
16921619 }
1693 self.audit(
1694 Some(actor),
1695 AuditRecord {
1696 action: "chat.attachment_deleted",
1697 target: Some(("chat_attachment", attachment_id.to_string())),
1698 detail: json!({
1699 "s3_key": row.s3_key,
1700 "storage_deleted": storage_deleted,
1701 "filename": row.filename,
1702 }),
1703 },
1704 ctx,
1705 )
1706 .await?;
17071620 Ok(DeleteChatAttachmentResp {
17081621 id: attachment_id,
17091622 s3_key: row.s3_key,
Modified lib/admin/src/services/logs.rs View lib/admin/src/services/logs.rs
@@ -1,14 +1,10 @@
1use models::admin::admin_audit_log;
21use models::ai::ai_job;
32use models::chat::webhook_deliveries;
43use models::ci::workflow_run::{self, WorkflowRunState};
54use models::import::job;
65use models::search::index_job;
76use models::system::system_tasks;
8use sea_orm::{
9 ColumnTrait, Condition, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect,
10 Select,
11};
7use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect};
128use serde::{Deserialize, Serialize};
139use time::OffsetDateTime;
1410use utoipa::{IntoParams, ToSchema};
@@ -18,43 +14,6 @@
1814use crate::services::service::AdminService;
1915
2016#[derive(Debug, Clone, Deserialize, IntoParams)]
21pub struct ListAuditQuery {
22 pub admin_id: Option<Uuid>,
23 pub admin_username: Option<String>,
24 pub action: Option<String>,
25 pub target_type: Option<String>,
26 pub target_id: Option<String>,
27 pub q: Option<String>,
28 pub from: Option<OffsetDateTime>,
29 pub to: Option<OffsetDateTime>,
30 pub limit: Option<u64>,
31 pub offset: Option<u64>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
35pub struct AuditLogDto {
36 pub id: Uuid,
37 pub admin_id: Option<Uuid>,
38 pub admin_username: String,
39 pub action: String,
40 pub target_type: Option<String>,
41 pub target_id: Option<String>,
42 pub detail: serde_json::Value,
43 pub ip: Option<String>,
44 pub user_agent: Option<String>,
45 pub request_id: Option<String>,
46 pub created_at: OffsetDateTime,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
50pub struct AuditLogPageDto {
51 pub items: Vec<AuditLogDto>,
52 pub total: i64,
53 pub limit: u64,
54 pub offset: u64,
55}
56
57#[derive(Debug, Clone, Deserialize, IntoParams)]
5817pub struct ListJobLogsQuery {
5918 pub source: Option<String>,
6019 pub from: Option<OffsetDateTime>,
@@ -82,43 +41,6 @@
8241}
8342
8443impl AdminService {
85 pub async fn list_audit_logs(
86 &self,
87 query: ListAuditQuery,
88 ) -> Result<AuditLogPageDto, AdminError> {
89 let limit = query.limit.unwrap_or(50).clamp(1, 100);
90 let offset = query.offset.unwrap_or(0);
91 let select = self.audit_select(&query).await?;
92 let total = select.clone().count(self.db().read()).await? as i64;
93 let rows = select
94 .limit(limit)
95 .offset(offset)
96 .all(self.db().read())
97 .await?;
98 let items = rows
99 .into_iter()
100 .map(|row| AuditLogDto {
101 id: row.id,
102 admin_id: row.admin_id,
103 admin_username: row.admin_username,
104 action: row.action,
105 target_type: row.target_type,
106 target_id: row.target_id,
107 detail: row.detail,
108 ip: row.ip,
109 user_agent: row.user_agent,
110 request_id: row.request_id,
111 created_at: row.created_at,
112 })
113 .collect();
114 Ok(AuditLogPageDto {
115 items,
116 total,
117 limit,
118 offset,
119 })
120 }
121
12244 pub async fn list_job_logs(
12345 &self,
12446 query: ListJobLogsQuery,
@@ -294,103 +216,6 @@
294216}
295217
296218impl AdminService {
297 async fn audit_select(
298 &self,
299 query: &ListAuditQuery,
300 ) -> Result<Select<admin_audit_log::Entity>, AdminError> {
301 let mut select = admin_audit_log::Entity::find();
302 if let Some(admin_id) = query.admin_id {
303 select = select.filter(admin_audit_log::Column::AdminId.eq(admin_id));
304 }
305 if let Some(username) = query
306 .admin_username
307 .as_deref()
308 .map(str::trim)
309 .filter(|value| !value.is_empty())
310 {
311 select = select
312 .filter(admin_audit_log::Column::AdminUsername.eq(username.to_ascii_lowercase()));
313 }
314 if let Some(action) = query
315 .action
316 .as_deref()
317 .map(str::trim)
318 .filter(|value| !value.is_empty())
319 {
320 select = select
321 .filter(admin_audit_log::Column::Action.contains(action.to_ascii_lowercase()));
322 }
323 if let Some(target_type) = query
324 .target_type
325 .as_deref()
326 .map(str::trim)
327 .filter(|value| !value.is_empty())
328 {
329 select = select.filter(admin_audit_log::Column::TargetType.eq(target_type));
330 }
331 if let Some(target_id) = query
332 .target_id
333 .as_deref()
334 .map(str::trim)
335 .filter(|value| !value.is_empty())
336 {
337 select = select.filter(admin_audit_log::Column::TargetId.eq(target_id));
338 }
339 if let Some(q) = query
340 .q
341 .as_deref()
342 .map(str::trim)
343 .filter(|value| !value.is_empty())
344 {
345 let needle = q.to_ascii_lowercase();
346 select = select.filter(
347 Condition::any()
348 .add(admin_audit_log::Column::AdminUsername.contains(needle.clone()))
349 .add(admin_audit_log::Column::Action.contains(needle.clone()))
350 .add(admin_audit_log::Column::TargetId.contains(q))
351 .add(sea_orm::sea_query::Expr::cust_with_values(
352 "detail::text ILIKE $1",
353 [format!("%{needle}%")],
354 )),
355 );
356 }
357 if let Some(from) = query.from {
358 select = select.filter(admin_audit_log::Column::CreatedAt.gte(from));
359 }
360 if let Some(to) = query.to {
361 select = select.filter(admin_audit_log::Column::CreatedAt.lte(to));
362 }
363 Ok(select.order_by_desc(admin_audit_log::Column::CreatedAt))
364 }
365
366 pub async fn export_audit_logs(
367 &self,
368 query: ListAuditQuery,
369 ) -> Result<Vec<AuditLogDto>, AdminError> {
370 let rows = self
371 .audit_select(&query)
372 .await?
373 .limit(10000)
374 .all(self.db().read())
375 .await?;
376 Ok(rows
377 .into_iter()
378 .map(|row| AuditLogDto {
379 id: row.id,
380 admin_id: row.admin_id,
381 admin_username: row.admin_username,
382 action: row.action,
383 target_type: row.target_type,
384 target_id: row.target_id,
385 detail: row.detail,
386 ip: row.ip,
387 user_agent: row.user_agent,
388 request_id: row.request_id,
389 created_at: row.created_at,
390 })
391 .collect())
392 }
393
394219 pub async fn queue_failed_jobs(
395220 &self,
396221 name: &str,
Modified lib/admin/src/services/mod.rs View lib/admin/src/services/mod.rs
@@ -1,8 +1,7 @@
11pub mod act;
2pub mod audit;
3pub mod auth;
42pub mod chat;
53pub mod logs;
4pub mod mcp;
65pub mod queues;
76pub mod repos;
87pub mod runners;
Modified lib/admin/src/services/queues.rs View lib/admin/src/services/queues.rs
@@ -1,4 +1,3 @@
1use models::admin::admin_user::AdminRole;
21use models::ai::ai_job;
32use models::chat::webhook_deliveries::{self, WebhookDeliveryStatus};
43use models::ci::workflow_run::{self, WorkflowRunState};
@@ -9,13 +8,11 @@
98use sea_orm::sea_query::Expr;
109use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter};
1110use serde::{Deserialize, Serialize};
12use serde_json::json;
1311use utoipa::ToSchema;
1412
1513use crate::errors::AdminError;
16use crate::services::audit::{AuditCtx, AuditRecord};
1714use crate::services::service::AdminService;
18use crate::share::roles::AdminActor;
15use crate::share::roles::{AdminActor, AdminRole};
1916
2017/// 读取 Redis 列表长度作为 AI 队列积压深度。
2118async fn ai_depth(topic: &str, service: &AdminService) -> i64 {
@@ -319,7 +316,6 @@
319316 &self,
320317 actor: &AdminActor,
321318 queue: &str,
322 ctx: &AuditCtx,
323319 ) -> Result<QueueActionResp, AdminError> {
324320 actor.require(AdminRole::Operator)?;
325321 let now = time::OffsetDateTime::now_utc();
@@ -470,16 +466,6 @@
470466 )));
471467 }
472468 };
473 self.audit(
474 Some(actor),
475 AuditRecord {
476 action: "queue.requeued",
477 target: Some(("queue", queue.to_string())),
478 detail: json!({ "affected": affected }),
479 },
480 ctx,
481 )
482 .await?;
483469 Ok(QueueActionResp {
484470 queue: queue.to_string(),
485471 affected,
@@ -490,7 +476,6 @@
490476 &self,
491477 actor: &AdminActor,
492478 queue: &str,
493 ctx: &AuditCtx,
494479 ) -> Result<QueueActionResp, AdminError> {
495480 actor.require(AdminRole::SuperAdmin)?;
496481 let affected = match queue {
@@ -584,16 +569,6 @@
584569 )));
585570 }
586571 };
587 self.audit(
588 Some(actor),
589 AuditRecord {
590 action: "queue.purged",
591 target: Some(("queue", queue.to_string())),
592 detail: json!({ "affected": affected }),
593 },
594 ctx,
595 )
596 .await?;
597572 Ok(QueueActionResp {
598573 queue: queue.to_string(),
599574 affected,
Modified lib/admin/src/services/runners.rs View lib/admin/src/services/runners.rs
@@ -433,22 +433,3 @@
433433 Ok(row.space_id)
434434 }
435435}
436
437impl AdminService {
438 pub async fn audit_action(
439 &self,
440 actor: &crate::share::roles::AdminActor,
441 action: &str,
442 detail: serde_json::Value,
443 ctx: &crate::services::audit::AuditCtx,
444 ) {
445 let record = crate::services::audit::AuditRecord {
446 action,
447 target: None,
448 detail,
449 };
450 if let Err(error) = self.audit(Some(actor), record, ctx).await {
451 tracing::warn!(%error, action, "admin audit write failed");
452 }
453 }
454}
Modified lib/admin/src/services/spaces.rs View lib/admin/src/services/spaces.rs
@@ -1,5 +1,4 @@
11use export::pagination::Page;
2use models::admin::admin_user::AdminRole;
32use models::dir::dirs;
43use models::repo::repos;
54use models::room::rooms;
@@ -12,15 +11,13 @@
1211 QuerySelect, Set,
1312};
1413use serde::{Deserialize, Serialize};
15use serde_json::json;
1614use time::OffsetDateTime;
1715use utoipa::{IntoParams, ToSchema};
1816use uuid::Uuid;
1917
2018use crate::errors::AdminError;
21use crate::services::audit::{AuditCtx, AuditRecord};
2219use crate::services::service::AdminService;
23use crate::share::roles::AdminActor;
20use crate::share::roles::{AdminActor, AdminRole};
2421
2522#[derive(Debug, Clone, Deserialize, IntoParams)]
2623pub struct ListSpacesQuery {
@@ -270,7 +267,6 @@
270267 actor: &AdminActor,
271268 id: Uuid,
272269 cmd: UpdateSpaceCmd,
273 ctx: &AuditCtx,
274270 ) -> Result<AdminSpaceDetail, AdminError> {
275271 actor.require(AdminRole::Operator)?;
276272 let row = spaces::Entity::find_by_id(id)
@@ -318,16 +314,6 @@
318314 }
319315 am.updated_at = Set(OffsetDateTime::now_utc());
320316 am.update(self.db().write()).await?;
321 self.audit(
322 Some(actor),
323 AuditRecord {
324 action: "space.updated",
325 target: Some(("space", id.to_string())),
326 detail: json!({ "changed": changed }),
327 },
328 ctx,
329 )
330 .await?;
331317 self.get_space(id).await
332318 }
333319
@@ -336,7 +322,6 @@
336322 actor: &AdminActor,
337323 id: Uuid,
338324 cmd: TransferSpaceCmd,
339 ctx: &AuditCtx,
340325 ) -> Result<AdminSpaceDetail, AdminError> {
341326 actor.require(AdminRole::SuperAdmin)?;
342327 let row = spaces::Entity::find_by_id(id)
@@ -357,30 +342,14 @@
357342 "new owner is already the space owner".to_string(),
358343 ));
359344 }
360 let previous = row.owner_id;
361345 let mut am: spaces::ActiveModel = row.into();
362346 am.owner_id = Set(new_owner.uid);
363347 am.updated_at = Set(OffsetDateTime::now_utc());
364348 am.update(self.db().write()).await?;
365 self.audit(
366 Some(actor),
367 AuditRecord {
368 action: "space.transferred",
369 target: Some(("space", id.to_string())),
370 detail: json!({ "previous_owner_id": previous, "new_owner_id": new_owner.uid, "new_owner_username": new_owner.username }),
371 },
372 ctx,
373 )
374 .await?;
375349 self.get_space(id).await
376350 }
377351
378 pub async fn delete_space(
379 &self,
380 actor: &AdminActor,
381 id: Uuid,
382 ctx: &AuditCtx,
383 ) -> Result<(), AdminError> {
352 pub async fn delete_space(&self, actor: &AdminActor, id: Uuid) -> Result<(), AdminError> {
384353 actor.require(AdminRole::SuperAdmin)?;
385354 let row = spaces::Entity::find_by_id(id)
386355 .one(self.db().read())
@@ -393,16 +362,6 @@
393362 am.is_active = Set(false);
394363 am.updated_at = Set(OffsetDateTime::now_utc());
395364 am.update(self.db().write()).await?;
396 self.audit(
397 Some(actor),
398 AuditRecord {
399 action: "space.deleted",
400 target: Some(("space", id.to_string())),
401 detail: json!({}),
402 },
403 ctx,
404 )
405 .await?;
406365 Ok(())
407366 }
408367
@@ -473,7 +432,6 @@
473432 actor: &AdminActor,
474433 space_id: Uuid,
475434 user_id: Uuid,
476 ctx: &AuditCtx,
477435 ) -> Result<RemoveMemberResp, AdminError> {
478436 actor.require(AdminRole::Operator)?;
479437 let space = spaces::Entity::find_by_id(space_id)
@@ -508,16 +466,6 @@
508466 .exec(self.db().write())
509467 .await?;
510468 }
511 self.audit(
512 Some(actor),
513 AuditRecord {
514 action: "space.member_removed",
515 target: Some(("space", space_id.to_string())),
516 detail: json!({ "user_id": user_id }),
517 },
518 ctx,
519 )
520 .await?;
521469 Ok(RemoveMemberResp {
522470 space_id,
523471 user_id,
@@ -532,7 +480,6 @@
532480 actor: &AdminActor,
533481 space_id: Uuid,
534482 cmd: AddSpaceMemberCmd,
535 ctx: &AuditCtx,
536483 ) -> Result<AddSpaceMemberResp, AdminError> {
537484 actor.require(AdminRole::Operator)?;
538485 let space = spaces::Entity::find_by_id(space_id)
@@ -576,16 +523,6 @@
576523 .filter(spaces::Column::Id.eq(space_id))
577524 .exec(self.db().write())
578525 .await?;
579 self.audit(
580 Some(actor),
581 AuditRecord {
582 action: "space.member_added",
583 target: Some(("space", space_id.to_string())),
584 detail: json!({ "user_id": cmd.user_id, "reactivated": true }),
585 },
586 ctx,
587 )
588 .await?;
589526 Ok(AddSpaceMemberResp {
590527 space_id,
591528 user_id: cmd.user_id,
@@ -622,16 +559,6 @@
622559 .filter(spaces::Column::Id.eq(space_id))
623560 .exec(self.db().write())
624561 .await?;
625 self.audit(
626 Some(actor),
627 AuditRecord {
628 action: "space.member_added",
629 target: Some(("space", space_id.to_string())),
630 detail: json!({ "user_id": cmd.user_id, "reactivated": false }),
631 },
632 ctx,
633 )
634 .await?;
635562 Ok(AddSpaceMemberResp {
636563 space_id,
637564 user_id: cmd.user_id,
Modified lib/admin/src/services/storage.rs View lib/admin/src/services/storage.rs
@@ -1,14 +1,11 @@
11use export::redis;
2use models::admin::admin_user::AdminRole;
32use serde::{Deserialize, Serialize};
4use serde_json::json;
53use time::OffsetDateTime;
64use utoipa::{IntoParams, ToSchema};
75
86use crate::errors::AdminError;
9use crate::services::audit::{AuditCtx, AuditRecord};
107use crate::services::service::AdminService;
11use crate::share::roles::AdminActor;
8use crate::share::roles::{AdminActor, AdminRole};
129
1310const OVERVIEW_CACHE_KEY: &str = "admin:storage:overview";
1411const OVERVIEW_CACHE_TTL_SECS: u64 = 60;
@@ -201,7 +198,6 @@
201198 &self,
202199 actor: &AdminActor,
203200 key: String,
204 ctx: &AuditCtx,
205201 ) -> Result<DeleteObjectResp, AdminError> {
206202 actor.require(AdminRole::SuperAdmin)?;
207203 if key.trim().is_empty() {
@@ -212,16 +208,6 @@
212208 return Err(AdminError::NotFound);
213209 }
214210 self.storage().client().delete(&key).await?;
215 self.audit(
216 Some(actor),
217 AuditRecord {
218 action: "storage.object_deleted",
219 target: Some(("storage_object", key.clone())),
220 detail: json!({ "bucket": self.storage().config().bucket }),
221 },
222 ctx,
223 )
224 .await?;
225211 Ok(DeleteObjectResp { key, deleted: true })
226212 }
227213}
Modified lib/admin/src/services/system.rs View lib/admin/src/services/system.rs
@@ -2,7 +2,6 @@
22
33use std::collections::HashMap;
44
5use models::admin::{admin_audit_log, admin_user};
65use models::chat::messages;
76use models::issue::issues;
87use models::repo::repos;
@@ -10,15 +9,14 @@
109use models::space::spaces;
1110use models::users::{user, user_sessions};
1211use sea_orm::{
13 ColumnTrait, ConnectionTrait, DbBackend, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
14 QuerySelect, Statement,
12 ColumnTrait, ConnectionTrait, DbBackend, EntityTrait, PaginatorTrait, QueryFilter, Statement,
1513};
1614use serde::{Deserialize, Serialize};
1715use time::OffsetDateTime;
1816use utoipa::ToSchema;
1917
2018use crate::errors::AdminError;
21use crate::services::logs::{AuditLogDto, JobLogDto};
19use crate::services::logs::JobLogDto;
2220use crate::services::queues::QueueSnapshotDto;
2321use crate::services::service::AdminService;
2422
@@ -61,7 +59,6 @@
6159 pub rooms: i64,
6260 pub issues: i64,
6361 pub messages: i64,
64 pub admin_users: i64,
6562}
6663
6764#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
@@ -223,7 +220,6 @@
223220 rooms: rooms::Entity::find().count(self.db().read()).await? as i64,
224221 issues: issues::Entity::find().count(self.db().read()).await? as i64,
225222 messages: messages::Entity::find().count(self.db().read()).await? as i64,
226 admin_users: admin_user::Entity::find().count(self.db().read()).await? as i64,
227223 })
228224 }
229225
@@ -286,8 +282,6 @@
286282 pub enabled: bool,
287283 pub bind_addr: String,
288284 pub port: u16,
289 pub session_cookie_name: String,
290 pub session_table_name: String,
291285 pub cors_allow_origins: Vec<String>,
292286 pub cors_allow_credentials: bool,
293287}
@@ -423,8 +417,6 @@
423417 enabled: config.admin.enabled(),
424418 bind_addr: config.admin.bind_addr().to_string(),
425419 port: config.admin.port(),
426 session_cookie_name: config.admin.session_cookie_name().to_string(),
427 session_table_name: config.admin.session_table_name().to_string(),
428420 cors_allow_origins: config.admin.cors_allow_origins.clone(),
429421 cors_allow_credentials: config.admin.cors_allow_credentials(),
430422 },
@@ -523,7 +515,6 @@
523515 pub trend: Vec<TrendPointDto>,
524516 pub active_sessions: i64,
525517 pub restricted_users: i64,
526 pub recent_audit: Vec<AuditLogDto>,
527518 pub recent_failures: Vec<JobLogDto>,
528519 pub queues: Vec<QueueSnapshotDto>,
529520 pub generated_at: OffsetDateTime,
@@ -598,26 +589,6 @@
598589 .filter(user::Column::IsRestricted.eq(true))
599590 .count(self.db().read())
600591 .await? as i64;
601 let recent_audit = admin_audit_log::Entity::find()
602 .order_by_desc(admin_audit_log::Column::CreatedAt)
603 .limit(10)
604 .all(self.db().read())
605 .await?
606 .into_iter()
607 .map(|row| AuditLogDto {
608 id: row.id,
609 admin_id: row.admin_id,
610 admin_username: row.admin_username,
611 action: row.action,
612 target_type: row.target_type,
613 target_id: row.target_id,
614 detail: row.detail,
615 ip: row.ip,
616 user_agent: row.user_agent,
617 request_id: row.request_id,
618 created_at: row.created_at,
619 })
620 .collect();
621592 let recent_failures = self
622593 .list_job_logs(crate::services::logs::ListJobLogsQuery {
623594 source: None,
@@ -633,7 +604,6 @@
633604 trend: buckets,
634605 active_sessions,
635606 restricted_users,
636 recent_audit,
637607 recent_failures,
638608 queues,
639609 generated_at: now,
Modified lib/admin/src/services/tasks.rs View lib/admin/src/services/tasks.rs
@@ -1,4 +1,3 @@
1use models::admin::admin_user::AdminRole;
21use models::ai::ai_job;
32use models::chat::webhook_deliveries::{self, WebhookDeliveryStatus};
43use models::ci::workflow_run::{self, WorkflowRunState};
@@ -17,9 +16,8 @@
1716use uuid::Uuid;
1817
1918use crate::errors::AdminError;
20use crate::services::audit::{AuditCtx, AuditRecord};
2119use crate::services::service::AdminService;
22use crate::share::roles::AdminActor;
20use crate::share::roles::{AdminActor, AdminRole};
2321
2422pub const SOURCE_IMPORT: &str = "import";
2523pub const SOURCE_SEARCH_INDEX: &str = "search_index";
@@ -678,7 +676,6 @@
678676 actor: &AdminActor,
679677 source: &str,
680678 id: Uuid,
681 ctx: &AuditCtx,
682679 ) -> Result<TaskActionResp, AdminError> {
683680 actor.require(AdminRole::Operator)?;
684681 let now = OffsetDateTime::now_utc();
@@ -809,16 +806,6 @@
809806 }
810807 _ => return Err(AdminError::BadRequest("unknown task source".to_string())),
811808 };
812 self.audit(
813 Some(actor),
814 AuditRecord {
815 action: "task.retried",
816 target: Some((source, id.to_string())),
817 detail: json!({ "state": state }),
818 },
819 ctx,
820 )
821 .await?;
822809 Ok(TaskActionResp {
823810 source: source.to_string(),
824811 id,
@@ -831,7 +818,6 @@
831818 actor: &AdminActor,
832819 source: &str,
833820 id: Uuid,
834 ctx: &AuditCtx,
835821 ) -> Result<TaskActionResp, AdminError> {
836822 actor.require(AdminRole::Operator)?;
837823 let now = OffsetDateTime::now_utc();
@@ -976,16 +962,6 @@
976962 }
977963 _ => return Err(AdminError::BadRequest("unknown task source".to_string())),
978964 };
979 self.audit(
980 Some(actor),
981 AuditRecord {
982 action: "task.cancelled",
983 target: Some((source, id.to_string())),
984 detail: json!({ "state": state }),
985 },
986 ctx,
987 )
988 .await?;
989965 Ok(TaskActionResp {
990966 source: source.to_string(),
991967 id,
@@ -997,7 +973,6 @@
997973 actor: &AdminActor,
998974 source: &str,
999975 id: Uuid,
1000 ctx: &AuditCtx,
1001976 ) -> Result<(), AdminError> {
1002977 actor.require(AdminRole::SuperAdmin)?;
1003978 let terminal = match source {
@@ -1110,16 +1085,6 @@
11101085 }
11111086 _ => {}
11121087 }
1113 self.audit(
1114 Some(actor),
1115 AuditRecord {
1116 action: "task.deleted",
1117 target: Some((source, id.to_string())),
1118 detail: json!({}),
1119 },
1120 ctx,
1121 )
1122 .await?;
11231088 Ok(())
11241089 }
11251090}
@@ -1328,7 +1293,6 @@
13281293 actor: &AdminActor,
13291294 source: &str,
13301295 cmd: BulkTaskCmd,
1331 ctx: &AuditCtx,
13321296 ) -> Result<BulkTaskResp, AdminError> {
13331297 actor.require(AdminRole::Operator)?;
13341298 if ![
@@ -1367,9 +1331,9 @@
13671331 let mut failed: Vec<BulkTaskFailure> = Vec::new();
13681332 for id in cmd.ids {
13691333 let outcome = if action == "retry" {
1370 self.retry_task(actor, source, id, ctx).await.map(|_| ())
1334 self.retry_task(actor, source, id).await.map(|_| ())
13711335 } else {
1372 self.cancel_task(actor, source, id, ctx).await.map(|_| ())
1336 self.cancel_task(actor, source, id).await.map(|_| ())
13731337 };
13741338 match outcome {
13751339 Ok(()) => succeeded.push(id),
@@ -1383,21 +1347,6 @@
13831347 }),
13841348 }
13851349 }
1386 self.audit(
1387 Some(actor),
1388 AuditRecord {
1389 action: "task.bulk_action",
1390 target: Some(("task", source.to_string())),
1391 detail: json!({
1392 "action": action,
1393 "source": source,
1394 "succeeded": succeeded.len(),
1395 "failed": failed.len(),
1396 }),
1397 },
1398 ctx,
1399 )
1400 .await?;
14011350 Ok(BulkTaskResp {
14021351 source: source.to_string(),
14031352 action,
Modified lib/admin/src/services/users.rs View lib/admin/src/services/users.rs
@@ -17,16 +17,13 @@
1717 QueryFilter, QueryOrder, QuerySelect, Select, Set, TransactionTrait,
1818};
1919use serde::{Deserialize, Serialize};
20use serde_json::json;
2120use time::OffsetDateTime;
2221use utoipa::{IntoParams, ToSchema};
2322use uuid::Uuid;
2423
2524use crate::errors::AdminError;
26use crate::services::audit::{AuditCtx, AuditRecord};
2725use crate::services::service::AdminService;
28use crate::share::roles::AdminActor;
29use models::admin::admin_user::AdminRole;
26use crate::share::roles::{AdminActor, AdminRole};
3027
3128#[derive(Debug, Clone, Deserialize, IntoParams)]
3229pub struct ListUsersQuery {
@@ -290,7 +287,6 @@
290287 actor: &AdminActor,
291288 uid: Uuid,
292289 cmd: UpdateUserCmd,
293 ctx: &AuditCtx,
294290 ) -> Result<AdminUserSummary, AdminError> {
295291 actor.require(AdminRole::Operator)?;
296292 let row = user::Entity::find_by_id(uid)
@@ -328,16 +324,6 @@
328324 am.updated_at = Set(OffsetDateTime::now_utc());
329325 am.version = Set(row.version + 1);
330326 let row = am.update(self.db().write()).await?;
331 self.audit(
332 Some(actor),
333 AuditRecord {
334 action: "user.updated",
335 target: Some(("user", uid.to_string())),
336 detail: json!({ "changed": changed, "is_active": row.is_active, "is_restricted": row.is_restricted, "prohibit_login": row.prohibit_login }),
337 },
338 ctx,
339 )
340 .await?;
341327 let suspended = self.active_suspension_ids(&[uid]).await?;
342328 Ok(user_summary(&row, suspended.contains(&uid)))
343329 }
@@ -347,7 +333,6 @@
347333 actor: &AdminActor,
348334 uid: Uuid,
349335 cmd: SuspendUserCmd,
350 ctx: &AuditCtx,
351336 ) -> Result<AdminSuspensionDto, AdminError> {
352337 actor.require(AdminRole::Operator)?;
353338 let row = user::Entity::find_by_id(uid)
@@ -393,24 +378,7 @@
393378 am.is_restricted = Set(true);
394379 am.updated_at = Set(now);
395380 am.update(self.db().write()).await?;
396 let revoked = self.revoke_user_sessions(uid).await?;
397
398 self.audit(
399 Some(actor),
400 AuditRecord {
401 action: "user.suspended",
402 target: Some(("user", uid.to_string())),
403 detail: json!({
404 "suspension_id": suspension.id,
405 "suspension_type": format!("{:?}", cmd.suspension_type).to_ascii_lowercase(),
406 "reason": reason,
407 "ends_at": ends_at.map(|value| value.to_string()),
408 "sessions_revoked": revoked,
409 }),
410 },
411 ctx,
412 )
413 .await?;
381 self.revoke_user_sessions(uid).await?;
414382
415383 Ok(AdminSuspensionDto {
416384 id: suspension.id,
@@ -432,7 +400,6 @@
432400 actor: &AdminActor,
433401 uid: Uuid,
434402 suspension_id: Uuid,
435 ctx: &AuditCtx,
436403 ) -> Result<(), AdminError> {
437404 actor.require(AdminRole::Operator)?;
438405 let row = user_suspensions::Entity::find_by_id(suspension_id)
@@ -460,16 +427,6 @@
460427 um.update(self.db().write()).await?;
461428 }
462429
463 self.audit(
464 Some(actor),
465 AuditRecord {
466 action: "user.suspension_revoked",
467 target: Some(("user", uid.to_string())),
468 detail: json!({ "suspension_id": suspension_id }),
469 },
470 ctx,
471 )
472 .await?;
473430 Ok(())
474431 }
475432
@@ -478,7 +435,6 @@
478435 actor: &AdminActor,
479436 uid: Uuid,
480437 cmd: ResetUserPasswordCmd,
481 ctx: &AuditCtx,
482438 ) -> Result<(), AdminError> {
483439 actor.require(AdminRole::Operator)?;
484440 auth::share::crypto::validate_password(
@@ -497,17 +453,7 @@
497453 am.passwd_version = Set(Some(now.unix_timestamp()));
498454 am.updated_at = Set(now);
499455 am.update(self.db().write()).await?;
500 let revoked = self.revoke_user_sessions(uid).await?;
501 self.audit(
502 Some(actor),
503 AuditRecord {
504 action: "user.password_reset",
505 target: Some(("user", uid.to_string())),
506 detail: json!({ "sessions_revoked": revoked }),
507 },
508 ctx,
509 )
510 .await?;
456 self.revoke_user_sessions(uid).await?;
511457 Ok(())
512458 }
513459
@@ -528,24 +474,13 @@
528474 &self,
529475 actor: &AdminActor,
530476 uid: Uuid,
531 ctx: &AuditCtx,
532477 ) -> Result<RevokeSessionsResp, AdminError> {
533478 actor.require(AdminRole::Operator)?;
534 let row = user::Entity::find_by_id(uid)
479 user::Entity::find_by_id(uid)
535480 .one(self.db().read())
536481 .await?
537482 .ok_or(AdminError::NotFound)?;
538483 let revoked = self.revoke_user_sessions(uid).await?;
539 self.audit(
540 Some(actor),
541 AuditRecord {
542 action: "user.sessions_revoked",
543 target: Some(("user", row.uid.to_string())),
544 detail: json!({ "revoked": revoked }),
545 },
546 ctx,
547 )
548 .await?;
549484 Ok(RevokeSessionsResp { revoked })
550485 }
551486
@@ -553,7 +488,6 @@
553488 &self,
554489 actor: &AdminActor,
555490 uid: Uuid,
556 ctx: &AuditCtx,
557491 ) -> Result<DeleteUserResp, AdminError> {
558492 actor.require(AdminRole::SuperAdmin)?;
559493 let Some(user_row) = user::Entity::find_by_id(uid).one(self.db().write()).await? else {
@@ -656,16 +590,6 @@
656590 .exec(self.db().write())
657591 .await?;
658592
659 self.audit(
660 Some(actor),
661 AuditRecord {
662 action: "user.deleted",
663 target: Some(("user", uid.to_string())),
664 detail: json!({ "anonymized": true }),
665 },
666 ctx,
667 )
668 .await?;
669593 Ok(DeleteUserResp {
670594 user_id: uid,
671595 anonymized: true,
@@ -900,7 +824,6 @@
900824 actor: &AdminActor,
901825 uid: Uuid,
902826 session_id: Uuid,
903 ctx: &AuditCtx,
904827 ) -> Result<(), AdminError> {
905828 actor.require(AdminRole::Operator)?;
906829 let row = user_sessions::Entity::find_by_id(session_id)
@@ -916,16 +839,6 @@
916839 am.updated_at = Set(now);
917840 am.update(self.db().write()).await?;
918841 }
919 self.audit(
920 Some(actor),
921 AuditRecord {
922 action: "user.session_revoked",
923 target: Some(("user", uid.to_string())),
924 detail: json!({ "session_id": session_id }),
925 },
926 ctx,
927 )
928 .await?;
929842 Ok(())
930843 }
931844
@@ -967,7 +880,6 @@
967880 &self,
968881 actor: &AdminActor,
969882 uid: Uuid,
970 ctx: &AuditCtx,
971883 ) -> Result<(), AdminError> {
972884 actor.require(AdminRole::SuperAdmin)?;
973885 let row = user::Entity::find_by_id(uid)
@@ -986,16 +898,6 @@
986898 .filter(user_recovery_codes::Column::UserId.eq(uid))
987899 .exec(self.db().write())
988900 .await?;
989 self.audit(
990 Some(actor),
991 AuditRecord {
992 action: "user.two_factor_cleared",
993 target: Some(("user", uid.to_string())),
994 detail: json!({}),
995 },
996 ctx,
997 )
998 .await?;
999901 Ok(())
1000902 }
1001903
@@ -1003,7 +905,6 @@
1003905 &self,
1004906 actor: &AdminActor,
1005907 cmd: CreateUserCmd,
1006 ctx: &AuditCtx,
1007908 ) -> Result<CreateUserResp, AdminError> {
1008909 actor.require(AdminRole::SuperAdmin)?;
1009910 let username = validate_username(cmd.username.trim())?;
@@ -1121,21 +1022,6 @@
11211022 })
11221023 .await?;
11231024
1124 self.audit(
1125 Some(actor),
1126 AuditRecord {
1127 action: "user.created",
1128 target: Some(("user", uid.to_string())),
1129 detail: json!({
1130 "username": username,
1131 "email": email,
1132 "is_active": is_active,
1133 "email_activated": email_activated,
1134 }),
1135 },
1136 ctx,
1137 )
1138 .await?;
11391025 Ok(CreateUserResp {
11401026 uid,
11411027 username,
@@ -1147,7 +1033,6 @@
11471033 &self,
11481034 actor: &AdminActor,
11491035 cmd: BulkUsersCmd,
1150 ctx: &AuditCtx,
11511036 ) -> Result<BulkUsersResp, AdminError> {
11521037 actor.require(AdminRole::Operator)?;
11531038 let action = cmd.action.trim().to_ascii_lowercase();
@@ -1191,7 +1076,6 @@
11911076 is_restricted: None,
11921077 prohibit_login: None,
11931078 },
1194 ctx,
11951079 )
11961080 .await
11971081 .map(|_| ()),
@@ -1205,7 +1089,6 @@
12051089 is_restricted: None,
12061090 prohibit_login: None,
12071091 },
1208 ctx,
12091092 )
12101093 .await
12111094 .map(|_| ()),
@@ -1219,7 +1102,6 @@
12191102 is_restricted: Some(true),
12201103 prohibit_login: None,
12211104 },
1222 ctx,
12231105 )
12241106 .await
12251107 .map(|_| ()),
@@ -1233,12 +1115,11 @@
12331115 is_restricted: Some(false),
12341116 prohibit_login: None,
12351117 },
1236 ctx,
12371118 )
12381119 .await
12391120 .map(|_| ()),
12401121 "revoke_sessions" => self
1241 .admin_revoke_user_sessions(actor, uid, ctx)
1122 .admin_revoke_user_sessions(actor, uid)
12421123 .await
12431124 .map(|_| ()),
12441125 "suspend" => {
@@ -1251,12 +1132,11 @@
12511132 reason: cmd.reason.clone(),
12521133 ends_at: cmd.ends_at,
12531134 },
1254 ctx,
12551135 )
12561136 .await
12571137 .map(|_| ())
12581138 }
1259 _ => self.admin_delete_user(actor, uid, ctx).await.map(|_| ()),
1139 _ => self.admin_delete_user(actor, uid).await.map(|_| ()),
12601140 };
12611141 match outcome {
12621142 Ok(()) => succeeded.push(uid),
@@ -1270,20 +1150,6 @@
12701150 }),
12711151 }
12721152 }
1273 self.audit(
1274 Some(actor),
1275 AuditRecord {
1276 action: "user.bulk_action",
1277 target: None,
1278 detail: json!({
1279 "action": action,
1280 "succeeded": succeeded.len(),
1281 "failed": failed.len(),
1282 }),
1283 },
1284 ctx,
1285 )
1286 .await?;
12871153 Ok(BulkUsersResp {
12881154 action,
12891155 succeeded,
Deleted lib/admin/src/share/guard.rs

Diff too large to display.

Modified lib/admin/src/share/mod.rs View lib/admin/src/share/mod.rs
@@ -1,3 +1 @@
1pub mod guard;
21pub mod roles;
3pub mod session;
Modified lib/admin/src/share/roles.rs View lib/admin/src/share/roles.rs
@@ -1,11 +1,14 @@
1use axum::extract::FromRequestParts;
2use axum::http::request::Parts;
3use models::admin::admin_user::AdminRole;
41use uuid::Uuid;
52
6use crate::errors::AdminApiError;
73use crate::errors::AdminError;
84
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum AdminRole {
7 SuperAdmin,
8 Operator,
9 Viewer,
10}
11
912#[derive(Debug, Clone)]
1013pub struct AdminActor {
1114 pub id: Uuid,
@@ -34,21 +37,5 @@
3437 } else {
3538 Err(AdminError::Forbidden)
3639 }
37 }
38}
39
40#[derive(Debug, Clone)]
41pub struct CurrentAdmin(pub AdminActor);
42
43impl<S: Send + Sync> FromRequestParts<S> for CurrentAdmin {
44 type Rejection = AdminApiError;
45
46 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
47 parts
48 .extensions
49 .get::<AdminActor>()
50 .cloned()
51 .map(CurrentAdmin)
52 .ok_or_else(AdminApiError::unauthorized)
5340 }
5441}
Deleted lib/admin/src/share/session.rs

Diff too large to display.

Modified lib/admin/src/errors.rs View lib/admin/src/errors.rs
@@ -1,9 +1,5 @@
11use std::fmt;
22
3use axum::response::{IntoResponse, Response};
4use export::rest::AppResponseData;
5use export::rest::NoData;
6
73#[derive(Debug)]
84pub enum AdminError {
95 BadRequest(String),
@@ -77,46 +73,66 @@
7773 }
7874}
7975
80export::api_error! {
81 log: "admin",
82 pub enum AdminApiError {
83 BadRequest(400, "request failed validation"),
84 Unauthorized(401, "authentication required"),
85 Forbidden(403, "operation not allowed"),
86 NotFound(404, "resource not found"),
87 Conflict(409, "resource conflict"),
88 RateLimited(429, "rate limit exceeded"),
89 Internal(500, "internal server error"),
90 }
76#[derive(Debug)]
77pub struct AdminApiErrorPayload {
78 pub message: String,
9179}
9280
81#[derive(Debug)]
82pub enum AdminApiError {
83 BadRequest(AdminApiErrorPayload),
84 Unauthorized(AdminApiErrorPayload),
85 Forbidden(AdminApiErrorPayload),
86 NotFound(AdminApiErrorPayload),
87 Conflict(AdminApiErrorPayload),
88 RateLimited(AdminApiErrorPayload),
89 Internal(AdminApiErrorPayload),
90}
91
9392impl AdminApiError {
94 pub fn invalid_credentials() -> Self {
95 Self::build(
96 401,
97 AppResponseData::<NoData>::err(401, "invalid username or password")
98 .with_error_code("invalid_credentials"),
99 )
93 fn payload(message: impl Into<String>) -> AdminApiErrorPayload {
94 AdminApiErrorPayload {
95 message: message.into(),
96 }
97 }
98
99 pub fn bad_request(message: impl AsRef<str>) -> Self {
100 Self::BadRequest(Self::payload(message.as_ref()))
101 }
102
103 pub fn unauthorized() -> Self {
104 Self::Unauthorized(Self::payload("unauthorized"))
100105 }
101106
102 pub fn password_change_required() -> Self {
103 Self::build(
104 403,
105 AppResponseData::<NoData>::err(403, "password change required")
106 .with_error_code("password_change_required"),
107 )
107 pub fn forbidden() -> Self {
108 Self::Forbidden(Self::payload("forbidden"))
108109 }
109}
110110
111pub fn password_change_required_response() -> Response {
112 AdminApiError::password_change_required().into_response()
111 pub fn not_found(message: &'static str) -> Self {
112 Self::NotFound(Self::payload(message))
113 }
114
115 pub fn conflict(message: impl AsRef<str>) -> Self {
116 Self::Conflict(Self::payload(message.as_ref()))
117 }
118
119 pub fn rate_limited(message: impl Into<String>) -> Self {
120 Self::RateLimited(Self::payload(message))
121 }
122
123 pub fn internal(message: impl fmt::Display) -> Self {
124 tracing::warn!(error = %message, "admin handler failed");
125 Self::Internal(Self::payload("internal server error"))
126 }
113127}
114128
115129impl From<AdminError> for AdminApiError {
116130 fn from(error: AdminError) -> Self {
117131 match error {
118132 AdminError::BadRequest(message) => Self::bad_request(message),
119 AdminError::InvalidCredentials => Self::invalid_credentials(),
133 AdminError::InvalidCredentials => {
134 Self::Unauthorized(Self::payload("invalid username or password"))
135 }
120136 AdminError::Unauthorized => Self::unauthorized(),
121137 AdminError::Forbidden => Self::forbidden(),
122138 AdminError::NotFound => Self::not_found("admin resource not found"),
Modified lib/admin/src/lib.rs View lib/admin/src/lib.rs
@@ -1,7 +1,4 @@
11pub mod errors;
2pub mod handlers;
3pub mod openapi;
4pub mod routes;
52pub mod services;
63pub mod share;
74
Deleted lib/admin/src/openapi.rs

Diff too large to display.

Deleted lib/admin/src/routes.rs

Diff too large to display.

Modified lib/admin/Cargo.toml View lib/admin/Cargo.toml
@@ -5,11 +5,11 @@
55
66[dependencies]
77auth = { workspace = true }
8axum = { workspace = true }
98config = { workspace = true }
109export = { workspace = true }
1110git = { workspace = true }
1211imaging = { workspace = true }
12mcp-api = { workspace = true }
1313models = { workspace = true }
1414ci = { workspace = true }
1515redis = { workspace = true }
@@ -17,7 +17,6 @@
1717sea-orm = { workspace = true }
1818serde = { workspace = true, features = ["derive"] }
1919serde_json = { workspace = true }
20session-layer = { workspace = true }
2120storage = { workspace = true }
2221tokio = { workspace = true, features = ["macros", "time"] }
2322time = { workspace = true }
Added lib/admin-rpc/proto/act.proto View lib/admin-rpc/proto/act.proto

Diff too large to display.

Added lib/admin-rpc/proto/admin.proto View lib/admin-rpc/proto/admin.proto

Diff too large to display.

Added lib/admin-rpc/proto/chat.proto View lib/admin-rpc/proto/chat.proto

Diff too large to display.

Added lib/admin-rpc/proto/common.proto View lib/admin-rpc/proto/common.proto

Diff too large to display.

Added lib/admin-rpc/proto/logs.proto View lib/admin-rpc/proto/logs.proto

Diff too large to display.

Added lib/admin-rpc/proto/mcp.proto View lib/admin-rpc/proto/mcp.proto

Diff too large to display.

Added lib/admin-rpc/proto/queues.proto View lib/admin-rpc/proto/queues.proto

Diff too large to display.

Added lib/admin-rpc/proto/repos.proto View lib/admin-rpc/proto/repos.proto

Diff too large to display.

Added lib/admin-rpc/proto/runners.proto View lib/admin-rpc/proto/runners.proto

Diff too large to display.

Added lib/admin-rpc/proto/spaces.proto View lib/admin-rpc/proto/spaces.proto

Diff too large to display.

Added lib/admin-rpc/proto/storage.proto View lib/admin-rpc/proto/storage.proto

Diff too large to display.

Added lib/admin-rpc/proto/system.proto View lib/admin-rpc/proto/system.proto

Diff too large to display.

Added lib/admin-rpc/proto/tasks.proto View lib/admin-rpc/proto/tasks.proto

Diff too large to display.

Added lib/admin-rpc/proto/users.proto View lib/admin-rpc/proto/users.proto

Diff too large to display.

Added lib/admin-rpc/src/convert/act.rs View lib/admin-rpc/src/convert/act.rs

Diff too large to display.

Added lib/admin-rpc/src/convert/chat.rs View lib/admin-rpc/src/convert/chat.rs

Diff too large to display.

Added lib/admin-rpc/src/convert/logs.rs View lib/admin-rpc/src/convert/logs.rs

Diff too large to display.

Added lib/admin-rpc/src/convert/mcp.rs View lib/admin-rpc/src/convert/mcp.rs

Diff too large to display.

Added lib/admin-rpc/src/convert/mod.rs View lib/admin-rpc/src/convert/mod.rs

Diff too large to display.

Added lib/admin-rpc/src/convert/queues.rs View lib/admin-rpc/src/convert/queues.rs

Diff too large to display.

Added lib/admin-rpc/src/convert/repos.rs View lib/admin-rpc/src/convert/repos.rs

Diff too large to display.

Added lib/admin-rpc/src/convert/runners.rs View lib/admin-rpc/src/convert/runners.rs

Diff too large to display.

Added lib/admin-rpc/src/convert/spaces.rs View lib/admin-rpc/src/convert/spaces.rs

Diff too large to display.

Added lib/admin-rpc/src/convert/storage.rs View lib/admin-rpc/src/convert/storage.rs

Diff too large to display.

Added lib/admin-rpc/src/convert/system.rs View lib/admin-rpc/src/convert/system.rs

Diff too large to display.

Added lib/admin-rpc/src/convert/tasks.rs View lib/admin-rpc/src/convert/tasks.rs

Diff too large to display.

Added lib/admin-rpc/src/convert/users.rs View lib/admin-rpc/src/convert/users.rs

Diff too large to display.

Added lib/admin-rpc/src/service/act.rs View lib/admin-rpc/src/service/act.rs

Diff too large to display.

Added lib/admin-rpc/src/service/chat.rs View lib/admin-rpc/src/service/chat.rs

Diff too large to display.

Added lib/admin-rpc/src/service/dispatch.rs View lib/admin-rpc/src/service/dispatch.rs

Diff too large to display.

Added lib/admin-rpc/src/service/logs.rs View lib/admin-rpc/src/service/logs.rs

Diff too large to display.

Added lib/admin-rpc/src/service/mcp.rs View lib/admin-rpc/src/service/mcp.rs

Diff too large to display.

Added lib/admin-rpc/src/service/mod.rs View lib/admin-rpc/src/service/mod.rs

Diff too large to display.

Added lib/admin-rpc/src/service/queues.rs View lib/admin-rpc/src/service/queues.rs

Diff too large to display.

Added lib/admin-rpc/src/service/repos.rs View lib/admin-rpc/src/service/repos.rs

Diff too large to display.

Added lib/admin-rpc/src/service/runners.rs View lib/admin-rpc/src/service/runners.rs

Diff too large to display.

Added lib/admin-rpc/src/service/spaces.rs View lib/admin-rpc/src/service/spaces.rs

Diff too large to display.

Added lib/admin-rpc/src/service/storage.rs View lib/admin-rpc/src/service/storage.rs

Diff too large to display.

Added lib/admin-rpc/src/service/system.rs View lib/admin-rpc/src/service/system.rs

Diff too large to display.

Added lib/admin-rpc/src/service/tasks.rs View lib/admin-rpc/src/service/tasks.rs

Diff too large to display.

Added lib/admin-rpc/src/service/users.rs View lib/admin-rpc/src/service/users.rs

Diff too large to display.

Added lib/admin-rpc/src/auth.rs View lib/admin-rpc/src/auth.rs

Diff too large to display.

Added lib/admin-rpc/src/error.rs View lib/admin-rpc/src/error.rs

Diff too large to display.

Added lib/admin-rpc/src/lib.rs View lib/admin-rpc/src/lib.rs

Diff too large to display.

Added lib/admin-rpc/src/serve.rs View lib/admin-rpc/src/serve.rs

Diff too large to display.

Added lib/admin-rpc/build.rs View lib/admin-rpc/build.rs

Diff too large to display.

Added lib/admin-rpc/Cargo.toml View lib/admin-rpc/Cargo.toml

Diff too large to display.

Modified lib/auth/src/services/access_keys.rs View lib/auth/src/services/access_keys.rs
@@ -258,6 +258,34 @@
258258 if !constant_time_eq(computed.as_bytes(), row.token_hash.as_bytes()) {
259259 return Ok(None);
260260 }
261 let now = OffsetDateTime::now_utc();
262 if let Some(expires_at) = row.expires_at
263 && expires_at <= now
264 {
265 return Ok(None);
266 }
267 let stale = row
268 .last_used_at
269 .map(|t| (now - t).whole_minutes() >= 1)
270 .unwrap_or(true);
271 if stale {
272 let _ = user_access_keys::Entity::update_many()
273 .col_expr(
274 user_access_keys::Column::LastUsedAt,
275 sea_orm::sea_query::Expr::value(now),
276 )
277 .filter(user_access_keys::Column::Id.eq(row.id))
278 .filter(
279 sea_orm::Condition::any()
280 .add(user_access_keys::Column::LastUsedAt.is_null())
281 .add(
282 user_access_keys::Column::LastUsedAt
283 .lt(now - time::Duration::minutes(1)),
284 ),
285 )
286 .exec(self.db().write())
287 .await;
288 }
261289 let user_id = row.user_id;
262290 let scope = row.scope;
263291 Ok(Some(VerifiedAccessKey { user_id, scope }))
Modified lib/ci/src/runners.rs View lib/ci/src/runners.rs
@@ -8,7 +8,9 @@
88use models::ci::workflow_job;
99use models::ci::workflow_run;
1010use models::ci::workflow_step;
11use sea_orm::{ActiveModelTrait, ColumnTrait, Condition, EntityTrait, QueryFilter, QueryOrder, Set};
11use sea_orm::{
12 ActiveModelTrait, ColumnTrait, Condition, EntityTrait, QueryFilter, QueryOrder, Set,
13};
1214use sha2::{Digest, Sha256};
1315use time::OffsetDateTime;
1416use uuid::Uuid;
Modified lib/config/src/admin.rs View lib/config/src/admin.rs
@@ -8,14 +8,35 @@
88 pub enabled: Option<bool>,
99 pub bind_addr: Option<String>,
1010 pub port: Option<u16>,
11 pub seed_username: Option<String>,
12 pub seed_password: Option<String>,
13 pub session_cookie_name: Option<String>,
14 pub session_table_name: Option<String>,
11 pub grpc_port: Option<u16>,
12 #[serde(default, deserialize_with = "string_or_vec")]
13 pub grpc_trusted_keys: Vec<String>,
1514 pub cors_allow_origins: Vec<String>,
1615 pub cors_allow_credentials: Option<bool>,
1716}
1817
18fn string_or_vec<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
19where
20 D: serde::Deserializer<'de>,
21{
22 #[derive(Deserialize)]
23 #[serde(untagged)]
24 enum StringOrVec {
25 String(String),
26 Vec(Vec<String>),
27 }
28
29 Ok(match StringOrVec::deserialize(deserializer)? {
30 StringOrVec::String(value) => value
31 .split(',')
32 .map(str::trim)
33 .filter(|value| !value.is_empty())
34 .map(str::to_string)
35 .collect(),
36 StringOrVec::Vec(values) => values,
37 })
38}
39
1940impl AdminConfig {
2041 pub fn enabled(&self) -> bool {
2142 self.enabled.unwrap_or(true)
@@ -29,31 +50,14 @@
2950 self.port.unwrap_or(3004)
3051 }
3152
32 pub fn seed_username(&self) -> Option<&str> {
33 self.seed_username
34 .as_deref()
35 .map(str::trim)
36 .filter(|value| !value.is_empty())
53 pub fn grpc_port(&self) -> u16 {
54 self.grpc_port.unwrap_or(3014)
3755 }
3856
39 pub fn seed_password(&self) -> Option<&str> {
40 self.seed_password
41 .as_deref()
42 .filter(|value| !value.is_empty())
57 pub fn grpc_trusted_keys(&self) -> &[String] {
58 &self.grpc_trusted_keys
4359 }
4460
45 pub fn session_cookie_name(&self) -> &str {
46 self.session_cookie_name
47 .as_deref()
48 .unwrap_or("gitdata_admin_session")
49 }
50
51 pub fn session_table_name(&self) -> &str {
52 self.session_table_name
53 .as_deref()
54 .unwrap_or("admin_session")
55 }
56
5761 pub fn cors_allow_credentials(&self) -> bool {
5862 self.cors_allow_credentials.unwrap_or(false)
5963 }
@@ -69,34 +73,8 @@
6973 if self.port == Some(0) {
7074 return Err("admin.port must be greater than 0".to_string());
7175 }
72 if self.session_cookie_name().trim().is_empty() {
73 return Err("admin.session_cookie_name must not be empty".to_string());
74 }
75 let table = self.session_table_name();
76 if table.trim().is_empty() {
77 return Err("admin.session_table_name must not be empty".to_string());
78 }
79 if table.contains(['*', '?', '[', ']', '\\']) {
80 return Err(format!(
81 "admin.session_table_name must not contain glob characters: {table:?}"
82 ));
83 }
84 match (self.seed_username(), self.seed_password()) {
85 (Some(username), Some(password)) => {
86 if username.len() < 3 {
87 return Err("admin.seed_username must be at least 3 characters".to_string());
88 }
89 if password.len() < 8 {
90 return Err("admin.seed_password must be at least 8 characters".to_string());
91 }
92 }
93 (Some(_), None) => {
94 return Err("admin.seed_username requires admin.seed_password".to_string());
95 }
96 (None, Some(_)) => {
97 return Err("admin.seed_password requires admin.seed_username".to_string());
98 }
99 (None, None) => {}
76 if self.grpc_port == Some(0) {
77 return Err("admin.grpc_port must be greater than 0".to_string());
10078 }
10179 for origin in &self.cors_allow_origins {
10280 if !origin.starts_with("http://") && !origin.starts_with("https://") {
Modified lib/config/src/lib.rs View lib/config/src/lib.rs
@@ -15,6 +15,7 @@
1515pub mod import;
1616pub mod issues;
1717pub mod livekit;
18pub mod mcp;
1819pub mod pow;
1920pub mod push;
2021pub mod quota;
@@ -51,6 +52,7 @@
5152use crate::import::ImportConfig;
5253use crate::issues::IssueConfig;
5354use crate::livekit::LiveKitConfig;
55use crate::mcp::McpConfig;
5456use crate::pow::PowConfig;
5557use crate::push::PushConfig;
5658use crate::redis::RedisConfig;
@@ -82,6 +84,7 @@
8284 pub space: SpaceConfig,
8385 pub issues: IssueConfig,
8486 pub livekit: LiveKitConfig,
87 pub mcp: McpConfig,
8588 pub git: GitConfig,
8689 pub email: EmailConfig,
8790 pub email_queue: EmailQueueConfig,
@@ -126,6 +129,7 @@
126129 self.ai.validate().map_err(ConfigError::Validation)?;
127130 self.aihost.is_valid().map_err(ConfigError::Validation)?;
128131 self.livekit.is_valid().map_err(ConfigError::Validation)?;
132 self.mcp.is_valid().map_err(ConfigError::Validation)?;
129133 self.push.is_valid().map_err(ConfigError::Validation)?;
130134 Ok(())
131135 }
Modified lib/config/src/scoped.rs View lib/config/src/scoped.rs
@@ -19,6 +19,7 @@
1919use crate::import::ImportConfig;
2020use crate::issues::IssueConfig;
2121use crate::livekit::LiveKitConfig;
22use crate::mcp::McpConfig;
2223use crate::pow::PowConfig;
2324use crate::push::PushConfig;
2425use crate::quota::QuotaConfig;
@@ -317,6 +318,7 @@
317318 pub search: SearchConfig,
318319 pub import: ImportConfig,
319320 pub git: GitConfig,
321 pub ai: AiConfig,
320322 pub admin: AdminSection,
321323}
322324
@@ -333,6 +335,7 @@
333335 search: self.search,
334336 import: self.import,
335337 git: self.git,
338 ai: self.ai,
336339 admin: self.admin,
337340 ..Default::default()
338341 }
@@ -349,6 +352,7 @@
349352 self.session.is_valid()?;
350353 self.search.validate()?;
351354 self.import.validate()?;
355 self.ai.validate()?;
352356 self.admin.is_valid()
353357 }
354358}
@@ -393,3 +397,59 @@
393397 self.health.is_valid()
394398 }
395399}
400
401#[derive(Clone, Debug, Default, Deserialize)]
402#[serde(default)]
403pub struct McpHostConfig {
404 pub database: DatabaseConfig,
405 pub redis: RedisConfig,
406 pub s3: S3Config,
407 pub email: EmailConfig,
408 pub email_queue: EmailQueueConfig,
409 pub push: PushConfig,
410 pub auth: AuthConfig,
411 pub user: UserConfig,
412 pub quota: QuotaConfig,
413 pub space: SpaceConfig,
414 pub issues: IssueConfig,
415 pub git: GitConfig,
416 pub web: WebConfig,
417 pub ai: AiConfig,
418 pub mcp: McpConfig,
419}
420
421impl McpHostConfig {
422 pub fn into_app_config(self) -> AppConfig {
423 AppConfig {
424 database: self.database,
425 redis: self.redis,
426 s3: self.s3,
427 email: self.email,
428 email_queue: self.email_queue,
429 push: self.push,
430 auth: self.auth,
431 user: self.user,
432 quota: self.quota,
433 space: self.space,
434 issues: self.issues,
435 git: self.git,
436 web: self.web,
437 ai: self.ai,
438 mcp: self.mcp,
439 ..Default::default()
440 }
441 }
442}
443
444impl ScopedConfig for McpHostConfig {
445 fn validate_scoped(&self) -> Result<(), String> {
446 self.database.validate()?;
447 self.redis.validate()?;
448 self.auth.validate()?;
449 self.email.is_valid()?;
450 self.email_queue.is_valid()?;
451 self.ai.validate()?;
452 self.mcp.is_valid()?;
453 self.push.is_valid()
454 }
455}
Modified lib/config/Cargo.toml View lib/config/Cargo.toml
@@ -9,5 +9,6 @@
99base64 = { workspace = true }
1010figment = { workspace = true }
1111dotenvy = { workspace = true }
12ipnet = { workspace = true }
1213thiserror = { workspace = true }
1314tracing = { workspace = true }
Added lib/export/src/authz.rs View lib/export/src/authz.rs

Diff too large to display.

Modified lib/export/src/lib.rs View lib/export/src/lib.rs
@@ -1,4 +1,5 @@
11mod api_error;
2pub mod authz;
23pub mod database;
34pub mod error;
45pub mod health;
Modified lib/git/src/service/repos.rs View lib/git/src/service/repos.rs
@@ -308,7 +308,7 @@
308308 cmd: CreateRepoCmd,
309309 ) -> Result<RepoDto, GitError> {
310310 let space = self.lookup_space(&space_name).await?;
311 if space.owner_id != caller {
311 if space.owner_id != caller && !export::authz::is_system_uid(caller) {
312312 return Err(GitError::Forbidden);
313313 }
314314 let name = validate_repo_name(&cmd.name)?;
@@ -555,7 +555,8 @@
555555 };
556556 let rows = sel.all(self.db_read()).await?;
557557
558 let owner_view = caller.is_some_and(|uid| space.owner_id == uid);
558 let owner_view =
559 caller.is_some_and(|uid| space.owner_id == uid || export::authz::is_system_uid(uid));
559560 let mut accessible: std::collections::HashSet<Uuid> = std::collections::HashSet::new();
560561 if let Some(uid) = caller
561562 && !owner_view
@@ -597,7 +598,7 @@
597598 ) -> Result<RepoDto, GitError> {
598599 let ctx = self.context(space_name, repo_name).await?;
599600 let space = self.repo_space(&ctx).await?;
600 if space.owner_id != caller {
601 if space.owner_id != caller && !export::authz::is_system_uid(caller) {
601602 return Err(GitError::Forbidden);
602603 }
603604 let new_branch = match &cmd.default_branch {
@@ -667,7 +668,7 @@
667668 ) -> Result<RepoDto, GitError> {
668669 let ctx = self.context(space_name, repo_name).await?;
669670 let space = self.repo_space(&ctx).await?;
670 if space.owner_id != caller {
671 if space.owner_id != caller && !export::authz::is_system_uid(caller) {
671672 return Err(GitError::Forbidden);
672673 }
673674 if ctx.model.is_archived == archived {
Modified lib/git/src/share/access.rs View lib/git/src/share/access.rs
@@ -235,6 +235,10 @@
235235 want_write: bool,
236236 require_admin: bool,
237237) -> Result<(), GitError> {
238 if export::authz::is_system_uid(uid) {
239 return Ok(());
240 }
241
238242 let repo = &ctx.model;
239243 let db = svc.db_read();
240244
Deleted lib/models/src/admin/admin_audit_log.rs

Diff too large to display.

Deleted lib/models/src/admin/admin_user.rs

Diff too large to display.

Deleted lib/models/src/admin/mod.rs

Diff too large to display.

Modified lib/models/src/lib.rs View lib/models/src/lib.rs
@@ -1,4 +1,3 @@
1pub mod admin;
21pub mod ai;
32pub mod chat;
43pub mod ci;
Modified lib/models/schema_extras.sql View lib/models/schema_extras.sql
@@ -290,13 +290,6 @@
290290 END IF;
291291END $$;
292292
293-- ---------------------------------------------------------------------------
294-- Admin domain (lib/admin): audit log lookups by time, actor and target.
295-- ---------------------------------------------------------------------------
296CREATE INDEX IF NOT EXISTS idx_admin_audit_log_created ON admin_audit_log (created_at DESC);
297CREATE INDEX IF NOT EXISTS idx_admin_audit_log_admin ON admin_audit_log (admin_id, created_at DESC);
298CREATE INDEX IF NOT EXISTS idx_admin_audit_log_target ON admin_audit_log (target_type, target_id);
299
300293CREATE INDEX IF NOT EXISTS idx_ci_runner_state
301294 ON ci_runner (state, last_seen_at DESC);
302295CREATE UNIQUE INDEX IF NOT EXISTS idx_ci_run_event_key ON workflow_run(event_key) WHERE event_key IS NOT NULL;
Modified lib/models/schema_pre_sync.sql View lib/models/schema_pre_sync.sql
@@ -28,6 +28,10 @@
2828-- scheduled messages feature removed; drop the legacy table before sync
2929DROP TABLE IF EXISTS user_scheduled_message;
3030
31-- legacy REST admin retired; drop its tables before sync
32DROP TABLE IF EXISTS admin_audit_log;
33DROP TABLE IF EXISTS admin_user;
34
3135-- rooms.parent_id used to be declared as a self-reference, so sync created
3236-- `fk-rooms-parent_id -> rooms(id)`. The column actually points at a directory
3337-- node (dir.id) — update_room validates the target is a same-space DirKind::Dir
Modified lib/space/src/services/spaces.rs View lib/space/src/services/spaces.rs
@@ -370,7 +370,7 @@
370370 cmd: UpdateSpaceCmd,
371371 ) -> Result<SpaceDto, SpaceError> {
372372 let space = lookup_space_by_name(self, space_name).await?;
373 if space.owner_id != caller {
373 if space.owner_id != caller && !export::authz::is_system_uid(caller) {
374374 return Err(SpaceError::Forbidden);
375375 }
376376
@@ -479,7 +479,7 @@
479479
480480 pub async fn delete_space(&self, caller: Uuid, space_name: &str) -> Result<(), SpaceError> {
481481 let space = lookup_space_by_name(self, space_name).await?;
482 if space.owner_id != caller {
482 if space.owner_id != caller && !export::authz::is_system_uid(caller) {
483483 return Err(SpaceError::Forbidden);
484484 }
485485
@@ -520,10 +520,12 @@
520520 .unwrap_or(0)
521521 .clamp(0, export::pagination::MAX_OFFSET);
522522
523 let select = spaces::Entity::find()
524 .filter(spaces::Column::OwnerId.eq(caller))
523 let mut select = spaces::Entity::find()
525524 .filter(spaces::Column::IsActive.eq(true))
526525 .order_by_desc(spaces::Column::CreatedAt);
526 if !export::authz::is_system_uid(caller) {
527 select = select.filter(spaces::Column::OwnerId.eq(caller));
528 }
527529
528530 let total = select.clone().count(self.db().read()).await? as i64;
529531 let rows = select
@@ -532,19 +534,20 @@
532534 .all(self.db().read())
533535 .await?;
534536
535 let owner_map = self.load_user_briefs(&[caller]).await?;
536 let owner = owner_map.get(&caller).cloned();
537 let owner_ids: Vec<Uuid> = rows.iter().map(|s| s.owner_id).collect();
538 let owner_map = self.load_user_briefs(&owner_ids).await?;
537539
538540 let items = rows
539541 .into_iter()
540542 .map(|s| {
541543 let avatar_url = resolve_space_image_url(&s.lower_name, "avatar", s.avatar_url);
544 let owner = owner_map.get(&s.owner_id).cloned();
542545 SpaceDto {
543546 id: s.id,
544547 lower_name: s.lower_name,
545548 name: s.name,
546549 description: s.description,
547 owner: owner.clone(),
550 owner,
548551 avatar_url,
549552 is_private: s.is_private,
550553 is_active: s.is_active,
Modified lib/space/src/share/authz.rs View lib/space/src/share/authz.rs
@@ -62,6 +62,9 @@
6262 space_id: Uuid,
6363 user_id: Uuid,
6464) -> Result<(), SpaceError> {
65 if export::authz::is_system_uid(user_id) {
66 return Ok(());
67 }
6568 let now = OffsetDateTime::now_utc();
6669 if let Some(ban) = space_bans::Entity::find_by_id((space_id, user_id))
6770 .one(db)
@@ -78,7 +81,7 @@
7881 space: &spaces::Model,
7982 user_id: Uuid,
8083) -> Result<Permissions, SpaceError> {
81 if space.owner_id == user_id {
84 if space.owner_id == user_id || export::authz::is_system_uid(user_id) {
8285 return Ok(Permissions::owner());
8386 }
8487
Modified lib/space/src/share/dir_visibility.rs View lib/space/src/share/dir_visibility.rs
@@ -51,7 +51,7 @@
5151 dir_id: Uuid,
5252 caller: Uuid,
5353) -> Result<bool, SpaceError> {
54 if space.owner_id == caller {
54 if space.owner_id == caller || export::authz::is_system_uid(caller) {
5555 return Ok(true);
5656 }
5757 let target = dirs::Entity::find_by_id(dir_id).one(db).await?;
Modified scripts/dev.ps1 View scripts/dev.ps1
@@ -22,11 +22,11 @@
2222 adminhost = @{ Exe = 'adminhost'; Port = 3004; Health = $true }
2323 webhook = @{ Exe = 'webhook'; Port = 3005; Health = $true }
2424 aihost = @{ Exe = 'aihost'; Port = 3006; Health = $true }
25 mcp = @{ Exe = 'mcp'; Port = 3007; Health = $true }
2526 web = @{ Bun = 'web'; Port = 5173; Health = $false }
26 'web-admin' = @{ Bun = 'admin'; Port = 5174; Health = $false }
2727}
28$Order = @('migrate', 'apihost', 'githost', 'im', 'synchost', 'mails', 'adminhost', 'webhook', 'aihost', 'web', 'web-admin')
29$Backends = @('migrate', 'apihost', 'githost', 'im', 'synchost', 'mails', 'adminhost', 'webhook', 'aihost')
28$Order = @('migrate', 'apihost', 'githost', 'im', 'synchost', 'mails', 'adminhost', 'webhook', 'aihost', 'mcp', 'web')
29$Backends = @('migrate', 'apihost', 'githost', 'im', 'synchost', 'mails', 'adminhost', 'webhook', 'aihost', 'mcp')
3030$Aliases = @{
3131 api = 'apihost'
3232 git = 'githost'
@@ -35,7 +35,6 @@
3535 mail = 'mails'
3636 hook = 'webhook'
3737 ai = 'aihost'
38 admin = 'web-admin'
3938 ui = 'web'
4039}
4140
@@ -48,8 +47,8 @@
4847 adminhost = 'http://127.0.0.1:3004'
4948 webhook = 'http://127.0.0.1:3005'
5049 aihost = 'http://127.0.0.1:3006/healthz'
50 mcp = 'http://127.0.0.1:3007/healthz'
5151 web = 'http://127.0.0.1:5173'
52 'web-admin' = 'http://127.0.0.1:5174'
5352}
5453
5554$Infra = @(
@@ -290,11 +289,11 @@
290289 continue
291290 }
292291 if ($key -eq 'frontends') {
293 foreach ($n in @('web', 'web-admin')) { if (-not $names.Contains($n)) { $names.Add($n) } }
292 foreach ($n in @('web')) { if (-not $names.Contains($n)) { $names.Add($n) } }
294293 continue
295294 }
296295 if (-not $Services.Contains($key)) {
297 throw "未知服务: $part;可用: $($Order -join ', ')(别名: api/git/chat/sync/mail/hook/ai/admin/ui/backends/frontends)"
296 throw "未知服务: $part;可用: $($Order -join ', ')(别名: api/git/chat/sync/mail/hook/ai/ui/backends/frontends)"
298297 }
299298 if (-not $names.Contains($key)) { $names.Add($key) }
300299 }
Modified scripts/dev.sh View scripts/dev.sh
@@ -12,8 +12,8 @@
1212DEV_HOST_KEY="$DEV_DIR/ssh_host_ed25519"
1313COMPOSE_FILE="$ROOT/docker-compose.dev.yml"
1414
15ORDER="apihost githost im synchost mails adminhost webhook aihost web web-admin"
16BACKENDS="apihost githost im synchost mails adminhost webhook aihost"
15ORDER="apihost githost im synchost mails adminhost webhook aihost mcp web"
16BACKENDS="apihost githost im synchost mails adminhost webhook aihost mcp"
1717INFRA="postgresql:5432 redis:6379 s3:9000 meilisearch:7700"
1818
1919if [ -t 1 ]; then
@@ -45,8 +45,8 @@
4545 adminhost) echo 3004 ;;
4646 webhook) echo 3005 ;;
4747 aihost) echo 3006 ;;
48 mcp) echo 3007 ;;
4849 web) echo 5173 ;;
49 web-admin) echo 5174 ;;
5050 *) echo 0 ;;
5151 esac
5252}
@@ -67,7 +67,6 @@
6767 mail) echo mails ;;
6868 hook) echo webhook ;;
6969 ai) echo aihost ;;
70 admin) echo web-admin ;;
7170 ui) echo web ;;
7271 *) echo "$1" ;;
7372 esac
@@ -83,8 +82,8 @@
8382 adminhost) echo "http://127.0.0.1:3004" ;;
8483 webhook) echo "http://127.0.0.1:3005" ;;
8584 aihost) echo "http://127.0.0.1:3006/healthz" ;;
85 mcp) echo "http://127.0.0.1:3007/healthz" ;;
8686 web) echo "http://127.0.0.1:5173" ;;
87 web-admin) echo "http://127.0.0.1:5174" ;;
8887 esac
8988}
9089
@@ -203,7 +202,7 @@
203202 port="$(port_of "$name")"
204203 [ "$port" = "0" ] && return 0
205204 case "$name" in
206 web|web-admin) port_open "$port" ;;
205 web) port_open "$port" ;;
207206 *)
208207 if command -v curl >/dev/null 2>&1; then
209208 # --noproxy '*':有些开发机设置了 http_proxy,否则 localhost 也会被代理
@@ -491,8 +490,8 @@
491490 service_env || return 1
492491
493492 case "$name" in
494 web|web-admin)
495 if [ "$name" = "web" ]; then dir="$ROOT/web"; else dir="$ROOT/admin"; fi
493 web)
494 dir="$ROOT/web"
496495 [ -d "$dir" ] || { fail " 缺少目录 $dir"; return 1; }
497496 ensure_frontend_deps "$dir" "$name" || return 1
498497 # --strictPort:端口被占用时直接失败,避免 vite 悄悄换到别的端口
@@ -591,12 +590,12 @@
591590 case "$key" in
592591 all) out="$out $ORDER" ;;
593592 backends) out="$out $BACKENDS" ;;
594 frontends) out="$out web web-admin" ;;
593 frontends) out="$out web" ;;
595594 *)
596595 if ! contains "$key" $ORDER; then
597596 fail "未知服务: $part"
598597 info "可用: $ORDER"
599 info "别名: api git chat sync mail hook ai admin ui / backends / frontends"
598 info "别名: api git chat sync mail hook ai ui / backends / frontends"
600599 return 1
601600 fi
602601 out="$out $key"
Modified Cargo.lock View Cargo.lock
@@ -19,19 +19,18 @@
1919version = "1.0.0"
2020dependencies = [
2121 "auth",
22 "axum",
2322 "ci",
2423 "config",
2524 "export",
2625 "git",
2726 "imaging",
27 "mcp-api",
2828 "models",
2929 "redis",
3030 "reqwest 0.12.28",
3131 "sea-orm",
3232 "serde",
3333 "serde_json",
34 "session-layer",
3534 "storage",
3635 "time 0.3.55",
3736 "tokio",
@@ -41,24 +40,38 @@
4140]
4241
4342[[package]]
44name = "admin-web"
43name = "admin-rpc"
4544version = "1.0.0"
45dependencies = [
46 "admin",
47 "config",
48 "export",
49 "models",
50 "prost 0.14.4",
51 "serde",
52 "serde_json",
53 "time 0.3.55",
54 "tokio",
55 "tonic",
56 "tonic-prost",
57 "tonic-prost-build",
58 "tracing",
59 "uuid",
60]
4661
4762[[package]]
4863name = "adminhost"
4964version = "1.0.0"
5065dependencies = [
5166 "admin",
52 "admin-web",
67 "admin-rpc",
5368 "anyhow",
5469 "axum",
5570 "config",
5671 "export",
57 "session-layer",
5872 "tokio",
5973 "tower-http 0.7.0",
6074 "tracing",
61 "utoipa-swagger-ui",
6275]
6376
6477[[package]]
@@ -1473,6 +1486,7 @@
14731486 "base64 0.23.1",
14741487 "dotenvy",
14751488 "figment",
1489 "ipnet",
14761490 "sea-orm",
14771491 "serde",
14781492 "thiserror 2.0.20",
@@ -1798,6 +1812,16 @@
17981812]
17991813
18001814[[package]]
1815name = "darling"
1816version = "0.24.1"
1817source = "registry+https://github.com/rust-lang/crates.io-index"
1818checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec"
1819dependencies = [
1820 "darling_core 0.24.1",
1821 "darling_macro 0.24.1",
1822]
1823
1824[[package]]
18011825name = "darling_core"
18021826version = "0.20.11"
18031827source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1825,6 +1849,19 @@
18251849]
18261850
18271851[[package]]
1852name = "darling_core"
1853version = "0.24.1"
1854source = "registry+https://github.com/rust-lang/crates.io-index"
1855checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff"
1856dependencies = [
1857 "ident_case",
1858 "proc-macro2",
1859 "quote",
1860 "strsim",
1861 "syn 3.0.3",
1862]
1863
1864[[package]]
18281865name = "darling_macro"
18291866version = "0.20.11"
18301867source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1847,6 +1884,17 @@
18471884]
18481885
18491886[[package]]
1887name = "darling_macro"
1888version = "0.24.1"
1889source = "registry+https://github.com/rust-lang/crates.io-index"
1890checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
1891dependencies = [
1892 "darling_core 0.24.1",
1893 "quote",
1894 "syn 3.0.3",
1895]
1896
1897[[package]]
18501898name = "dashmap"
18511899version = "6.2.1"
18521900source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2123,6 +2171,12 @@
21232171checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
21242172
21252173[[package]]
2174name = "dyn-clone"
2175version = "1.0.20"
2176source = "registry+https://github.com/rust-lang/crates.io-index"
2177checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
2178
2179[[package]]
21262180name = "ecdsa"
21272181version = "0.16.9"
21282182source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4031,6 +4085,25 @@
40314085]
40324086
40334087[[package]]
4088name = "h2"
4089version = "0.4.19"
4090source = "registry+https://github.com/rust-lang/crates.io-index"
4091checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"
4092dependencies = [
4093 "atomic-waker",
4094 "bytes",
4095 "fnv",
4096 "futures-core",
4097 "futures-sink",
4098 "http 1.5.0",
4099 "indexmap",
4100 "slab",
4101 "tokio",
4102 "tokio-util",
4103 "tracing",
4104]
4105
4106[[package]]
40344107name = "half"
40354108version = "2.7.1"
40364109source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4417,7 +4490,7 @@
44174490 "futures-channel",
44184491 "futures-core",
44194492 "futures-util",
4420 "h2",
4493 "h2 0.3.27",
44214494 "http 0.2.12",
44224495 "http-body 0.4.6",
44234496 "httparse",
@@ -4441,6 +4514,7 @@
44414514 "bytes",
44424515 "futures-channel",
44434516 "futures-core",
4517 "h2 0.4.19",
44444518 "http 1.5.0",
44454519 "http-body 1.1.0",
44464520 "httparse",
@@ -4480,6 +4554,19 @@
44804554]
44814555
44824556[[package]]
4557name = "hyper-timeout"
4558version = "0.5.2"
4559source = "registry+https://github.com/rust-lang/crates.io-index"
4560checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
4561dependencies = [
4562 "hyper 1.11.0",
4563 "hyper-util",
4564 "pin-project-lite",
4565 "tokio",
4566 "tower-service",
4567]
4568
4569[[package]]
44834570name = "hyper-tls"
44844571version = "0.5.0"
44854572source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5507,6 +5594,51 @@
55075594]
55085595
55095596[[package]]
5597name = "mcp"
5598version = "1.0.0"
5599dependencies = [
5600 "anyhow",
5601 "axum",
5602 "config",
5603 "export",
5604 "mcp-api",
5605 "tokio",
5606 "tower-http 0.7.0",
5607 "tracing",
5608]
5609
5610[[package]]
5611name = "mcp-api"
5612version = "1.0.0"
5613dependencies = [
5614 "agent",
5615 "auth",
5616 "axum",
5617 "base64 0.23.1",
5618 "cache",
5619 "config",
5620 "export",
5621 "git",
5622 "gitbaby",
5623 "ipnet",
5624 "issues",
5625 "models",
5626 "pulls",
5627 "rmcp",
5628 "schemars",
5629 "sea-orm",
5630 "serde",
5631 "serde_json",
5632 "sha2 0.11.0",
5633 "space",
5634 "time 0.3.55",
5635 "tokio",
5636 "tracing",
5637 "user",
5638 "uuid",
5639]
5640
5641[[package]]
55105642name = "md-5"
55115643version = "0.11.0"
55125644source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6082,7 +6214,6 @@
60826214name = "openapi"
60836215version = "1.0.0"
60846216dependencies = [
6085 "admin",
60866217 "anyhow",
60876218 "webapi",
60886219]
@@ -6418,6 +6549,12 @@
64186549checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
64196550
64206551[[package]]
6552name = "pastey"
6553version = "0.2.3"
6554source = "registry+https://github.com/rust-lang/crates.io-index"
6555checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4"
6556
6557[[package]]
64216558name = "pbjson"
64226559version = "0.6.0"
64236560source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6436,7 +6573,7 @@
64366573 "heck 0.4.1",
64376574 "itertools 0.11.0",
64386575 "prost 0.12.6",
6439 "prost-types",
6576 "prost-types 0.12.6",
64406577]
64416578
64426579[[package]]
@@ -6450,7 +6587,7 @@
64506587 "pbjson",
64516588 "pbjson-build",
64526589 "prost 0.12.6",
6453 "prost-build",
6590 "prost-build 0.12.6",
64546591 "serde",
64556592]
64566593
@@ -7020,7 +7157,29 @@
70207157 "petgraph",
70217158 "prettyplease",
70227159 "prost 0.12.6",
7023 "prost-types",
7160 "prost-types 0.12.6",
7161 "regex",
7162 "syn 2.0.119",
7163 "tempfile",
7164]
7165
7166[[package]]
7167name = "prost-build"
7168version = "0.14.1"
7169source = "registry+https://github.com/rust-lang/crates.io-index"
7170checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1"
7171dependencies = [
7172 "heck 0.5.0",
7173 "itertools 0.14.0",
7174 "log 0.4.33",
7175 "multimap",
7176 "once_cell",
7177 "petgraph",
7178 "prettyplease",
7179 "prost 0.14.4",
7180 "prost-types 0.14.1",
7181 "pulldown-cmark",
7182 "pulldown-cmark-to-cmark",
70247183 "regex",
70257184 "syn 2.0.119",
70267185 "tempfile",
@@ -7062,6 +7221,15 @@
70627221]
70637222
70647223[[package]]
7224name = "prost-types"
7225version = "0.14.1"
7226source = "registry+https://github.com/rust-lang/crates.io-index"
7227checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72"
7228dependencies = [
7229 "prost 0.14.4",
7230]
7231
7232[[package]]
70657233name = "ptr_meta"
70667234version = "0.1.4"
70677235source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7100,6 +7268,15 @@
71007268checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
71017269
71027270[[package]]
7271name = "pulldown-cmark-to-cmark"
7272version = "21.1.0"
7273source = "registry+https://github.com/rust-lang/crates.io-index"
7274checksum = "8246feae3db61428fd0bb94285c690b460e4517d83152377543ca802357785f1"
7275dependencies = [
7276 "pulldown-cmark",
7277]
7278
7279[[package]]
71037280name = "pulls"
71047281version = "1.0.0"
71057282dependencies = [
@@ -7444,6 +7621,26 @@
74447621]
74457622
74467623[[package]]
7624name = "ref-cast"
7625version = "1.0.27"
7626source = "registry+https://github.com/rust-lang/crates.io-index"
7627checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3"
7628dependencies = [
7629 "ref-cast-impl",
7630]
7631
7632[[package]]
7633name = "ref-cast-impl"
7634version = "1.0.27"
7635source = "registry+https://github.com/rust-lang/crates.io-index"
7636checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a"
7637dependencies = [
7638 "proc-macro2",
7639 "quote",
7640 "syn 3.0.3",
7641]
7642
7643[[package]]
74477644name = "regex"
74487645version = "1.13.1"
74497646source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7620,6 +7817,51 @@
76207817]
76217818
76227819[[package]]
7820name = "rmcp"
7821version = "3.5.0"
7822source = "registry+https://github.com/rust-lang/crates.io-index"
7823checksum = "fae7019994ae0fe4ada40b732f798f3ff26f0f04facb1477f1bf37eb4f18a2d3"
7824dependencies = [
7825 "async-trait",
7826 "base64 0.23.1",
7827 "bytes",
7828 "chrono",
7829 "futures",
7830 "http 1.5.0",
7831 "http-body 1.1.0",
7832 "http-body-util",
7833 "indexmap",
7834 "pastey",
7835 "pin-project-lite",
7836 "rand 0.10.2",
7837 "rmcp-macros",
7838 "schemars",
7839 "serde",
7840 "serde_json",
7841 "sse-stream",
7842 "thiserror 2.0.20",
7843 "tokio",
7844 "tokio-stream",
7845 "tokio-util",
7846 "tower-service",
7847 "tracing",
7848 "uuid",
7849]
7850
7851[[package]]
7852name = "rmcp-macros"
7853version = "3.5.0"
7854source = "registry+https://github.com/rust-lang/crates.io-index"
7855checksum = "c7e66877136e969d5a2d561ee5841b17717a0619aebd64abb47a2a2f46e34683"
7856dependencies = [
7857 "darling 0.24.1",
7858 "proc-macro2",
7859 "quote",
7860 "serde_json",
7861 "syn 3.0.3",
7862]
7863
7864[[package]]
76237865name = "rsa"
76247866version = "0.9.10"
76257867source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -8044,6 +8286,33 @@
80448286]
80458287
80468288[[package]]
8289name = "schemars"
8290version = "1.2.2"
8291source = "registry+https://github.com/rust-lang/crates.io-index"
8292checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a"
8293dependencies = [
8294 "chrono",
8295 "dyn-clone",
8296 "ref-cast",
8297 "schemars_derive",
8298 "serde",
8299 "serde_json",
8300 "uuid",
8301]
8302
8303[[package]]
8304name = "schemars_derive"
8305version = "1.2.2"
8306source = "registry+https://github.com/rust-lang/crates.io-index"
8307checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba"
8308dependencies = [
8309 "proc-macro2",
8310 "quote",
8311 "serde_derive_internals",
8312 "syn 3.0.3",
8313]
8314
8315[[package]]
80478316name = "scopeguard"
80488317version = "1.2.0"
80498318source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -8325,6 +8594,17 @@
83258594]
83268595
83278596[[package]]
8597name = "serde_derive_internals"
8598version = "0.30.0"
8599source = "registry+https://github.com/rust-lang/crates.io-index"
8600checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd"
8601dependencies = [
8602 "proc-macro2",
8603 "quote",
8604 "syn 3.0.3",
8605]
8606
8607[[package]]
83288608name = "serde_json"
83298609version = "1.0.151"
83308610source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -8948,6 +9228,19 @@
89489228]
89499229
89509230[[package]]
9231name = "sse-stream"
9232version = "0.2.6"
9233source = "registry+https://github.com/rust-lang/crates.io-index"
9234checksum = "c25ac7aff0abd1dbc474536e40416e1102c7dd9bfba0b9861c6d357f835dcfb4"
9235dependencies = [
9236 "bytes",
9237 "futures-util",
9238 "http-body 1.1.0",
9239 "http-body-util",
9240 "pin-project-lite",
9241]
9242
9243[[package]]
89519244name = "ssh-cipher"
89529245version = "0.2.0"
89539246source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -9539,6 +9832,74 @@
95399832checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
95409833
95419834[[package]]
9835name = "tonic"
9836version = "0.14.5"
9837source = "registry+https://github.com/rust-lang/crates.io-index"
9838checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec"
9839dependencies = [
9840 "async-trait",
9841 "axum",
9842 "base64 0.22.1",
9843 "bytes",
9844 "h2 0.4.19",
9845 "http 1.5.0",
9846 "http-body 1.1.0",
9847 "http-body-util",
9848 "hyper 1.11.0",
9849 "hyper-timeout",
9850 "hyper-util",
9851 "percent-encoding 2.3.2",
9852 "pin-project",
9853 "socket2 0.6.5",
9854 "sync_wrapper",
9855 "tokio",
9856 "tokio-stream",
9857 "tower",
9858 "tower-layer",
9859 "tower-service",
9860 "tracing",
9861]
9862
9863[[package]]
9864name = "tonic-build"
9865version = "0.14.5"
9866source = "registry+https://github.com/rust-lang/crates.io-index"
9867checksum = "1882ac3bf5ef12877d7ed57aad87e75154c11931c2ba7e6cde5e22d63522c734"
9868dependencies = [
9869 "prettyplease",
9870 "proc-macro2",
9871 "quote",
9872 "syn 2.0.119",
9873]
9874
9875[[package]]
9876name = "tonic-prost"
9877version = "0.14.5"
9878source = "registry+https://github.com/rust-lang/crates.io-index"
9879checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309"
9880dependencies = [
9881 "bytes",
9882 "prost 0.14.4",
9883 "tonic",
9884]
9885
9886[[package]]
9887name = "tonic-prost-build"
9888version = "0.14.5"
9889source = "registry+https://github.com/rust-lang/crates.io-index"
9890checksum = "f3144df636917574672e93d0f56d7edec49f90305749c668df5101751bb8f95a"
9891dependencies = [
9892 "prettyplease",
9893 "proc-macro2",
9894 "prost-build 0.14.1",
9895 "prost-types 0.14.1",
9896 "quote",
9897 "syn 2.0.119",
9898 "tempfile",
9899 "tonic-build",
9900]
9901
9902[[package]]
95429903name = "tower"
95439904version = "0.5.3"
95449905source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -9546,9 +9907,12 @@
95469907dependencies = [
95479908 "futures-core",
95489909 "futures-util",
9910 "indexmap",
95499911 "pin-project-lite",
9912 "slab",
95509913 "sync_wrapper",
95519914 "tokio",
9915 "tokio-util",
95529916 "tower-layer",
95539917 "tower-service",
95549918 "tracing",
Modified Cargo.toml View Cargo.toml
@@ -2,8 +2,7 @@
22members = [
33 "lib/*",
44 "app/*",
5 "web",
6 "admin"
5 "web"
76]
87
98resolver = "3"
@@ -16,9 +15,9 @@
1615
1716[workspace.dependencies]
1817admin = { path = "lib/admin" }
18admin-rpc = { path = "lib/admin-rpc" }
1919agent = { path = "lib/agent" }
2020aihost = { path = "app/aihost" }
21admin-web = { path = "admin" }
2221auth = { path = "lib/auth" }
2322cache = { path = "lib/cache" }
2423chat = { path = "lib/chat" }
@@ -40,6 +39,7 @@
4039imaging = { path = "lib/imaging" }
4140import = { path = "lib/import" }
4241webapi = { path = "lib/webapi" }
42mcp-api = { path = "lib/mcp-api" }
4343pulls = { path = "lib/pulls" }
4444search = { path = "lib/search" }
4545social = { path = "lib/social" }
@@ -128,3 +128,9 @@
128128url = "2"
129129percent-encoding = "2"
130130encoding_rs = "0.8"
131rmcp = { version = "3.5.0", features = ["transport-streamable-http-server"] }
132ipnet = "2"
133tonic = { version = "0.14", default-features = false, features = ["channel", "codegen", "router", "server"] }
134tonic-prost = "0.14"
135prost = "0.14"
136tonic-prost-build = "0.14"
Modified Dockerfile View Dockerfile
@@ -10,6 +10,7 @@
1010 curl \
1111 ca-certificates \
1212 unzip \
13 protobuf-compiler \
1314 && rm -rf /var/lib/apt/lists/*
1415
1516RUN curl -fsSL https://bun.sh/install | bash
@@ -25,16 +26,15 @@
2526COPY lib ./lib
2627COPY app ./app
2728COPY web ./web
28COPY admin ./admin
2929
3030RUN --mount=type=cache,target=/usr/local/cargo/registry \
3131 --mount=type=cache,target=/usr/local/cargo/git \
3232 --mount=type=cache,target=/build/target \
33 rm -rf /build/target/release/build/web-* /build/target/release/build/admin-web-* \
34 /build/target/release/deps/web-* /build/target/release/deps/admin_web-* 2>/dev/null; \
33 rm -rf /build/target/release/build/web-* \
34 /build/target/release/deps/web-* 2>/dev/null; \
3535 cargo build --release --workspace --bins && \
3636 mkdir -p /out && \
37 for b in apihost githost synchost mails migrate openapi im adminhost webhook runner aihost; do \
37 for b in apihost githost synchost mails migrate openapi im adminhost webhook runner aihost mcp; do \
3838 cp "target/release/$b" /out/; \
3939 done
4040
@@ -78,7 +78,7 @@
7878WORKDIR /app
7979
8080COPY --chown=gitdata:gitdata config/default.toml config/default.toml
81COPY --from=builder /out/apihost /out/githost /out/synchost /out/mails /out/migrate /out/openapi /out/im /out/adminhost /out/webhook /out/runner /out/aihost /usr/local/bin/
81COPY --from=builder /out/apihost /out/githost /out/synchost /out/mails /out/migrate /out/openapi /out/im /out/adminhost /out/webhook /out/runner /out/aihost /out/mcp /usr/local/bin/
8282
8383COPY --from=builder /out/docker /usr/local/bin/docker
8484
@@ -89,7 +89,7 @@
8989
9090USER gitdata
9191
92EXPOSE 3000 9418 22 3001 3002 3003 3004 3005 3006
92EXPOSE 3000 9418 22 3001 3002 3003 3004 3005 3006 3007 3014
9393
9494CMD ["apihost"]
9595
Modified justfile View justfile
@@ -48,7 +48,7 @@
4848migrate:
4949 @{{ invoke }}"{{ dev }}" migrate
5050
51# 构建全部后端二进制(web/admin 前端由 build.rs 构建并内嵌)
51# 构建全部后端二进制(web 前端由 build.rs 构建并内嵌)
5252build:
5353 @cargo build --bins
5454
@@ -56,7 +56,7 @@
5656build-release:
5757 @cargo build --release --bins
5858
59# 启动全部服务(后端 + web + admin)
59# 启动全部服务(后端 + web)
6060up *svc='':
6161 @{{ invoke }}"{{ dev }}" up {{ svc }}
6262
@@ -64,7 +64,7 @@
6464up-backend:
6565 @{{ invoke }}"{{ dev }}" up backends
6666
67# 仅启动前端 web(5173) 与 admin(5174)
67# 仅启动前端 web(5173)
6868up-frontend:
6969 @{{ invoke }}"{{ dev }}" up frontends
7070
@@ -99,13 +99,13 @@
9999 @echo " im ws://127.0.0.1:4082"
100100 @echo " synchost http://127.0.0.1:4083/healthz"
101101 @echo " mails http://127.0.0.1:4084/healthz"
102 @echo " adminhost http://127.0.0.1:3004"
102 @echo " adminhost http://127.0.0.1:3004 (gRPC: 127.0.0.1:3014)"
103103 @echo " webhook http://127.0.0.1:3005"
104104 @echo " aihost http://127.0.0.1:3006/healthz"
105 @echo " mcp http://127.0.0.1:3007/v1 (healthz: /healthz)"
105106 @echo " web http://127.0.0.1:5173"
106 @echo " web-admin http://127.0.0.1:5174"
107107
108# 重新生成 openapi.json 与 admin/openapi.json
108# 重新生成 openapi.json
109109openapi:
110110 @cargo run -p openapi
111111
@@ -116,10 +116,6 @@
116116# web 前端单独构建(cargo 构建时会自动执行)
117117web-build:
118118 @bun run --cwd web build
119
120# admin 前端单独构建
121admin-build:
122 @bun run --cwd admin build
123119
124120# web 前端 lint(oxlint + FSD 导航守卫)
125121web-lint: