feat: 로컬 계정 생성 기능 추가; 환경 설정에 따라 계정 생성 가능 여부 제어
This commit is contained in:
@@ -6,6 +6,7 @@ import { z } from 'zod';
|
||||
import { procedure, router } from './trpc.js';
|
||||
import { listScenarioPreviews, resolveGitCommitSha } from './scenario/scenarioCatalog.js';
|
||||
import type { UserSanctions, UserServerRestriction } from './auth/userRepository.js';
|
||||
import { toPublicUser } from './auth/userRepository.js';
|
||||
import type { AdminAuthContext } from './adminAuth.js';
|
||||
import type { GatewayApiContext } from './context.js';
|
||||
import { GATEWAY_BUILD_STATUSES, GATEWAY_PROFILE_STATUSES } from './orchestrator/profileRepository.js';
|
||||
@@ -41,6 +42,7 @@ const ADMIN_ROLE_PREFIX = 'admin.';
|
||||
const ADMIN_ROLE_SUPERUSER = 'admin.superuser';
|
||||
const ROLE_SUPERUSER = 'superuser';
|
||||
const ROLE_ADMIN_USERS = 'admin.users.manage';
|
||||
const ROLE_ADMIN_USERS_CREATE = 'admin.users.create';
|
||||
const ROLE_ADMIN_PROFILES = 'admin.profiles.manage';
|
||||
const ROLE_ADMIN_NOTICE = 'admin.notice.manage';
|
||||
const ROLE_RESET_SCHEDULE = 'admin.reset.schedule';
|
||||
@@ -142,6 +144,20 @@ const assertPermission = (adminAuth: AdminAuthContext, permission: string, profi
|
||||
});
|
||||
};
|
||||
|
||||
const canCreateLocalUser = (adminAuth: AdminAuthContext): boolean =>
|
||||
hasScopedPermission(adminAuth, ROLE_ADMIN_USERS_CREATE) || hasScopedPermission(adminAuth, ROLE_ADMIN_USERS);
|
||||
|
||||
// 로컬 계정 임의 생성은 환경 설정이 켜져 있을 때만 허용한다.
|
||||
const assertLocalAccountEnabled = (ctx: GatewayApiContext): void => {
|
||||
if (ctx.adminLocalAccountEnabled) {
|
||||
return;
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
message: 'Local account provisioning is disabled.',
|
||||
});
|
||||
};
|
||||
|
||||
const adminProcedure = procedure.use(async ({ ctx, next }) => {
|
||||
const adminAuth = await resolveAdminAuth(ctx as GatewayApiContext);
|
||||
return next({
|
||||
@@ -164,6 +180,17 @@ const userAdminProcedure = adminProcedure.use(({ ctx, next }) => {
|
||||
return next();
|
||||
});
|
||||
|
||||
const userCreateProcedure = adminProcedure.use(({ ctx, next }) => {
|
||||
const adminAuth = requireAdminAuth(ctx);
|
||||
if (!canCreateLocalUser(adminAuth)) {
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
message: 'Permission denied.',
|
||||
});
|
||||
}
|
||||
return next();
|
||||
});
|
||||
|
||||
const profileAdminProcedure = adminProcedure.use(({ ctx, next }) => {
|
||||
const adminAuth = requireAdminAuth(ctx);
|
||||
assertPermission(adminAuth, ROLE_ADMIN_PROFILES);
|
||||
@@ -198,6 +225,12 @@ const zSanctionsPatch = z.object({
|
||||
serverRestrictions: z.record(z.string(), zServerRestriction.nullable()).nullable().optional(),
|
||||
});
|
||||
|
||||
const zLocalAccountInput = z.object({
|
||||
username: z.string().min(2).max(32),
|
||||
password: z.string().min(6).max(128),
|
||||
displayName: z.string().min(2).max(40).optional(),
|
||||
});
|
||||
|
||||
const zInstallAutorun = z.object({
|
||||
limitMinutes: z.number().int().min(0).max(43200),
|
||||
options: z.array(z.enum(AUTORUN_USER_OPTIONS)),
|
||||
@@ -339,6 +372,34 @@ export const adminRouter = router({
|
||||
}),
|
||||
}),
|
||||
users: router({
|
||||
getLocalAccountStatus: adminProcedure.query(({ ctx }) => ({
|
||||
enabled: (ctx as GatewayApiContext).adminLocalAccountEnabled,
|
||||
})),
|
||||
createLocal: userCreateProcedure.input(zLocalAccountInput).mutation(async ({ ctx, input }) => {
|
||||
const gatewayCtx = ctx as GatewayApiContext;
|
||||
assertLocalAccountEnabled(gatewayCtx);
|
||||
const existing = await gatewayCtx.users.findByUsername(input.username);
|
||||
if (existing) {
|
||||
throw new TRPCError({
|
||||
code: 'CONFLICT',
|
||||
message: 'Username already exists.',
|
||||
});
|
||||
}
|
||||
try {
|
||||
const created = await gatewayCtx.users.createUser({
|
||||
username: input.username,
|
||||
password: input.password,
|
||||
displayName: input.displayName,
|
||||
});
|
||||
return { user: toPublicUser(created) };
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: 'CONFLICT',
|
||||
message: 'Username already exists.',
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
lookup: userAdminProcedure.input(zUserLookupInput).query(async ({ ctx, input }) => {
|
||||
const user = input.id
|
||||
? await ctx.users.findById(input.id)
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface GatewayApiConfig {
|
||||
kakaoAdminKey?: string;
|
||||
kakaoRedirectUri: string;
|
||||
publicBaseUrl: string;
|
||||
adminLocalAccountEnabled: boolean;
|
||||
orchestratorEnabled: boolean;
|
||||
orchestratorReconcileIntervalMs: number;
|
||||
orchestratorScheduleIntervalMs: number;
|
||||
@@ -80,6 +81,7 @@ export const resolveGatewayApiConfigFromEnv = (env: NodeJS.ProcessEnv = process.
|
||||
kakaoAdminKey: env.KAKAO_ADMIN_KEY,
|
||||
kakaoRedirectUri,
|
||||
publicBaseUrl,
|
||||
adminLocalAccountEnabled: parseBooleanWithFallback(env.GATEWAY_ADMIN_LOCAL_ACCOUNT_ENABLED, false),
|
||||
orchestratorEnabled: parseBooleanWithFallback(env.GATEWAY_ORCHESTRATOR_ENABLED, false),
|
||||
orchestratorReconcileIntervalMs: parseNumberWithFallback(
|
||||
env.GATEWAY_ORCHESTRATOR_RECONCILE_MS,
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface GatewayApiContext {
|
||||
kakaoClient: KakaoOAuthClient;
|
||||
oauthSessions: OAuthSessionStore;
|
||||
publicBaseUrl: string;
|
||||
adminLocalAccountEnabled: boolean;
|
||||
profiles: GatewayProfileRepository;
|
||||
orchestrator: GatewayOrchestratorHandle;
|
||||
profileStatus: GatewayProfileStatusService;
|
||||
@@ -35,6 +36,7 @@ export const createGatewayApiContext = (options: {
|
||||
kakaoClient: KakaoOAuthClient;
|
||||
oauthSessions: OAuthSessionStore;
|
||||
publicBaseUrl: string;
|
||||
adminLocalAccountEnabled: boolean;
|
||||
profiles: GatewayProfileRepository;
|
||||
orchestrator: GatewayOrchestratorHandle;
|
||||
profileStatus: GatewayProfileStatusService;
|
||||
@@ -49,6 +51,7 @@ export const createGatewayApiContext = (options: {
|
||||
kakaoClient: options.kakaoClient,
|
||||
oauthSessions: options.oauthSessions,
|
||||
publicBaseUrl: options.publicBaseUrl,
|
||||
adminLocalAccountEnabled: options.adminLocalAccountEnabled,
|
||||
profiles: options.profiles,
|
||||
orchestrator: options.orchestrator,
|
||||
profileStatus: options.profileStatus,
|
||||
|
||||
@@ -75,6 +75,7 @@ export const createGatewayApiServer = async () => {
|
||||
kakaoClient,
|
||||
oauthSessions,
|
||||
publicBaseUrl: config.publicBaseUrl,
|
||||
adminLocalAccountEnabled: config.adminLocalAccountEnabled,
|
||||
profiles,
|
||||
orchestrator,
|
||||
profileStatus,
|
||||
|
||||
Reference in New Issue
Block a user