feat: refresh shared chat and server workflows
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { env } from '../config/env.js';
|
||||
import { getSharedResourceTokenDetailBySharePath } from '../services/shared-resource-token-service.js';
|
||||
import { listServerCommands, restartServerCommand, serverCommandKeys } from '../services/server-command-service.js';
|
||||
import {
|
||||
cancelServerRestartReservation,
|
||||
@@ -16,8 +17,10 @@ const serverCommandParamSchema = z.object({
|
||||
});
|
||||
|
||||
const restartReservationBodySchema = z.object({
|
||||
target: z.enum(['all', 'test', 'work-server']).optional(),
|
||||
autoExecuteDelaySeconds: z.number().int().min(1).max(300).optional(),
|
||||
});
|
||||
const CHAT_SHARE_PATH_PREFIX = '/chat/share/';
|
||||
|
||||
function getImmediateRestartBlockInfo(
|
||||
key: z.infer<typeof serverCommandParamSchema>['key'],
|
||||
@@ -60,6 +63,39 @@ function getRequestAccessToken(request: FastifyRequest) {
|
||||
return Array.isArray(tokenHeader) ? tokenHeader[0]?.trim() ?? '' : String(tokenHeader ?? '').trim();
|
||||
}
|
||||
|
||||
function getRequestChatShareToken(request: FastifyRequest) {
|
||||
const tokenHeader = request.headers['x-chat-share-token'];
|
||||
return Array.isArray(tokenHeader) ? tokenHeader[0]?.trim() ?? '' : String(tokenHeader ?? '').trim();
|
||||
}
|
||||
|
||||
function resolveChatSharePath(token: string) {
|
||||
return `${CHAT_SHARE_PATH_PREFIX}${encodeURIComponent(token)}`;
|
||||
}
|
||||
|
||||
async function resolveSharedServerCommandAccessContext(request: FastifyRequest) {
|
||||
const shareToken = getRequestChatShareToken(request);
|
||||
if (!shareToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const managedResource = await getSharedResourceTokenDetailBySharePath(resolveChatSharePath(shareToken));
|
||||
if (!managedResource || managedResource.token.enabled === false || managedResource.token.revokedAt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const allowedAppIds = managedResource.token.allowedAppIds ?? [];
|
||||
const normalizedAllowedAppIds = new Set(allowedAppIds.map((item) => item.trim().toLowerCase()).filter(Boolean));
|
||||
|
||||
if (!managedResource.token.permissions.includes('manage') || !normalizedAllowedAppIds.has('server-command')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
scope: 'shared' as const,
|
||||
allowedKeys: new Set<string>(['work-server']),
|
||||
};
|
||||
}
|
||||
|
||||
function getRequestClientId(request: FastifyRequest) {
|
||||
const clientIdHeader = request.headers['x-client-id'];
|
||||
return Array.isArray(clientIdHeader) ? clientIdHeader[0]?.trim() ?? '' : String(clientIdHeader ?? '').trim();
|
||||
@@ -78,36 +114,48 @@ function getRequestAppOrigin(request: FastifyRequest) {
|
||||
return origin?.trim() ?? '';
|
||||
}
|
||||
|
||||
function ensureAuthorized(request: FastifyRequest, reply: FastifyReply) {
|
||||
async function resolveServerCommandAccessContext(request: FastifyRequest) {
|
||||
if (getRequestAccessToken(request) === env.SERVER_COMMAND_ACCESS_TOKEN) {
|
||||
return true;
|
||||
return { scope: 'full' as const };
|
||||
}
|
||||
|
||||
return resolveSharedServerCommandAccessContext(request);
|
||||
}
|
||||
|
||||
function sendAccessDenied(reply: FastifyReply) {
|
||||
reply.status(403);
|
||||
void reply.send({
|
||||
message: '권한 토큰이 필요합니다.',
|
||||
message: '권한 토큰 또는 워크서버 재기동 권한이 있는 공유채팅 링크가 필요합니다.',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function registerServerCommandRoutes(app: FastifyInstance) {
|
||||
app.get('/api/server-commands', async (request, reply) => {
|
||||
if (!ensureAuthorized(request, reply)) {
|
||||
const accessContext = await resolveServerCommandAccessContext(request);
|
||||
if (!accessContext) {
|
||||
sendAccessDenied(reply);
|
||||
return;
|
||||
}
|
||||
|
||||
const items = await listServerCommands();
|
||||
return {
|
||||
ok: true,
|
||||
items: await listServerCommands(),
|
||||
items: accessContext.scope === 'full' ? items : items.filter((item) => accessContext.allowedKeys.has(item.key)),
|
||||
};
|
||||
});
|
||||
|
||||
app.post('/api/server-commands/:key/actions/restart', async (request, reply) => {
|
||||
if (!ensureAuthorized(request, reply)) {
|
||||
const accessContext = await resolveServerCommandAccessContext(request);
|
||||
if (!accessContext) {
|
||||
sendAccessDenied(reply);
|
||||
return;
|
||||
}
|
||||
|
||||
const { key } = serverCommandParamSchema.parse(request.params);
|
||||
if (accessContext.scope !== 'full' && !accessContext.allowedKeys.has(key)) {
|
||||
reply.status(403);
|
||||
return { ok: false, message: '현재 공유채팅 링크로는 이 서버를 재기동할 수 없습니다.' };
|
||||
}
|
||||
|
||||
if (key === 'test' || key === 'work-server') {
|
||||
const workloadSummary = await getRestartReservationWorkloadSummary();
|
||||
@@ -160,7 +208,9 @@ export async function registerServerCommandRoutes(app: FastifyInstance) {
|
||||
});
|
||||
|
||||
app.get('/api/server-commands/restart-reservation', async (request, reply) => {
|
||||
if (!ensureAuthorized(request, reply)) {
|
||||
const accessContext = await resolveServerCommandAccessContext(request);
|
||||
if (!accessContext) {
|
||||
sendAccessDenied(reply);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -171,7 +221,9 @@ export async function registerServerCommandRoutes(app: FastifyInstance) {
|
||||
});
|
||||
|
||||
app.put('/api/server-commands/restart-reservation', async (request, reply) => {
|
||||
if (!ensureAuthorized(request, reply)) {
|
||||
const accessContext = await resolveServerCommandAccessContext(request);
|
||||
if (!accessContext) {
|
||||
sendAccessDenied(reply);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -187,9 +239,14 @@ export async function registerServerCommandRoutes(app: FastifyInstance) {
|
||||
|
||||
const parsed = restartReservationBodySchema.parse(payload ?? {});
|
||||
|
||||
if (accessContext.scope !== 'full' && parsed.target !== 'work-server') {
|
||||
return reply.status(403).send({ message: '현재 공유채팅 링크로는 WORK 서버 재기동 예약만 사용할 수 있습니다.' });
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
item: await scheduleServerRestartReservation({
|
||||
target: parsed.target,
|
||||
clientId: getRequestClientId(request),
|
||||
appOrigin: getRequestAppOrigin(request),
|
||||
autoExecuteDelaySeconds: parsed.autoExecuteDelaySeconds,
|
||||
@@ -198,7 +255,9 @@ export async function registerServerCommandRoutes(app: FastifyInstance) {
|
||||
});
|
||||
|
||||
app.post('/api/server-commands/restart-reservation/confirm', async (request, reply) => {
|
||||
if (!ensureAuthorized(request, reply)) {
|
||||
const accessContext = await resolveServerCommandAccessContext(request);
|
||||
if (!accessContext) {
|
||||
sendAccessDenied(reply);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -209,7 +268,9 @@ export async function registerServerCommandRoutes(app: FastifyInstance) {
|
||||
});
|
||||
|
||||
app.delete('/api/server-commands/restart-reservation', async (request, reply) => {
|
||||
if (!ensureAuthorized(request, reply)) {
|
||||
const accessContext = await resolveServerCommandAccessContext(request);
|
||||
if (!accessContext) {
|
||||
sendAccessDenied(reply);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user