feat(admin): manage server updates and local configuration
ci / api / api (push) Successful in 52s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m9s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m35s
release / release (версия и заметки) (push) Successful in 5s
release / android (APK в релиз) (push) Successful in 4m6s
ci / db / db (push) Successful in 1m15s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m19s
ci / ts / ts (packages + web) (push) Successful in 2m34s
release / desktop (Linux и Windows в релиз) (push) Successful in 14m27s
release / prune (старые файлы и прогоны) (push) Successful in 6s
ci / api / api (push) Successful in 52s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m9s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m35s
release / release (версия и заметки) (push) Successful in 5s
release / android (APK в релиз) (push) Successful in 4m6s
ci / db / db (push) Successful in 1m15s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m19s
ci / ts / ts (packages + web) (push) Successful in 2m34s
release / desktop (Linux и Windows в релиз) (push) Successful in 14m27s
release / prune (старые файлы и прогоны) (push) Successful in 6s
This commit is contained in:
1 parent
e33abc948c
commit
a29a8b18ad
49 files changed
+3604
-353
No files matched your search
@@ -79,12 +79,27 @@ export function fakeApi(initial: FakeUser[]) {
|
||||
const playlistOwners = new Map<string, string>();
|
||||
const playlists = new Map<string, components['schemas']['Playlist']>();
|
||||
|
||||
let managementSources = [{ id: 'origin', name: 'Текущий репозиторий', url: 'https://git.example.org/pipu/music.git' }];
|
||||
let managementJob: components['schemas']['ServerJob'] | null = null;
|
||||
let configRevision = 1;
|
||||
let configPending = false;
|
||||
const configValues: Record<string, string> = {};
|
||||
const configEntries: components['schemas']['ServerConfigEntry'][] = [
|
||||
{ key: 'LOG_LEVEL', group: 'shared', type: 'string', secret: false, readonly: false, effective: 'info', env: 'info', local: null, default_value: 'info', source: 'env' },
|
||||
{ key: 'TRAIN_HOUR', group: 'recommender', type: 'integer', secret: false, readonly: false, effective: '3', env: null, local: null, default_value: '3', source: 'default' },
|
||||
{ key: 'GITEA_TOKEN', group: 'api', type: 'string', secret: true, readonly: false, effective: '••••••••', env: null, local: '••••••••', default_value: '', source: 'local' },
|
||||
];
|
||||
const serverConfig = () => ({ revision: String(configRevision), restart_required: configPending, entries: configEntries.map((entry) => {
|
||||
const local = configValues[entry.key] ?? entry.local;
|
||||
return { ...entry, local, effective: entry.env ?? local ?? entry.default_value, source: entry.env !== null ? 'env' : local !== null ? 'local' : 'default' };
|
||||
}) });
|
||||
const me = (u: FakeUser): Me => ({
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
display_name: u.display_name,
|
||||
role: u.role,
|
||||
reviews_uploads: u.reviews_uploads,
|
||||
manages_updates: u.role === 'admin' || !!u.manages_updates,
|
||||
locale: u.locale,
|
||||
ui_prefs: prefs.get(u.id) ?? {},
|
||||
must_change_password: u.must_change_password,
|
||||
@@ -107,6 +122,25 @@ export function fakeApi(initial: FakeUser[]) {
|
||||
calls.push({ method: req.method, path, body });
|
||||
const route = `${req.method} ${path}`;
|
||||
const user = current ? users.get(current) : undefined;
|
||||
if (path.startsWith('/api/v1/admin/server/')) {
|
||||
if (!user) return problem(401, 'auth.unauthorized');
|
||||
const configs = path.includes('/config');
|
||||
if (configs ? user.role !== 'admin' : user.role !== 'admin' && !user.manages_updates) return problem(403, 'auth.forbidden');
|
||||
if (route === 'GET /api/v1/admin/server/updates') return json({ sources: managementSources, versions: ['v1.10.0', 'v1.9.0', 'v2.0.0-rc.1'], current_version: '1.9.0', source_error: '', job: managementJob });
|
||||
if (route === 'PUT /api/v1/admin/server/update-sources') { managementSources = body?.sources as typeof managementSources; return json({ sources: managementSources }); }
|
||||
if (route === 'GET /api/v1/admin/server/config') return json(serverConfig());
|
||||
if (route === 'PATCH /api/v1/admin/server/config') {
|
||||
if (body?.revision !== String(configRevision)) return problem(409, 'management.config_conflict');
|
||||
for (const [key, value] of Object.entries(body?.values as Record<string, string | null>)) {
|
||||
if (value === null) delete configValues[key]; else configValues[key] = value;
|
||||
}
|
||||
configRevision++; configPending = true; return json(serverConfig());
|
||||
}
|
||||
if (route === 'POST /api/v1/admin/server/updates' || route === 'POST /api/v1/admin/server/config/apply') {
|
||||
managementJob = { id: 'job-1', kind: configs ? 'config' : 'update', state: 'running', target: configs ? 'config.local' : body?.channel === 'version' ? String(body.version) : body?.channel === 'main' ? 'main' : 'v1.10.0', started_at: '2026-10-22T09:00:00Z', finished_at: '', log: '→ build and restart' };
|
||||
return json(managementJob, 202);
|
||||
}
|
||||
}
|
||||
|
||||
switch (route) {
|
||||
case 'GET /api/v1/server-info':
|
||||
@@ -323,6 +357,10 @@ export function fakeApi(initial: FakeUser[]) {
|
||||
return problem(422, 'auth.username_taken', { errors: [{ field: 'username', code: 'auth.username_taken' }] });
|
||||
target.username = username;
|
||||
}
|
||||
if (typeof body?.manages_updates === 'boolean') {
|
||||
if (user.role !== 'admin') return problem(403, 'auth.forbidden');
|
||||
target.manages_updates = body.manages_updates;
|
||||
}
|
||||
if (typeof body?.reviews_uploads === 'boolean') {
|
||||
if (user.role !== 'admin') return problem(403, 'auth.forbidden');
|
||||
target.reviews_uploads = body.reviews_uploads;
|
||||
|
||||
Reference in new issue
Block a user