feat: eslint 적용 및 관련 코드 일괄 수정
This commit is contained in:
@@ -7,10 +7,7 @@ import { procedure, router } from './trpc.js';
|
||||
import type { UserSanctions, UserServerRestriction } 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';
|
||||
import { GATEWAY_BUILD_STATUSES, GATEWAY_PROFILE_STATUSES } from './orchestrator/profileRepository.js';
|
||||
|
||||
const zProfileStatus = z.enum(GATEWAY_PROFILE_STATUSES);
|
||||
const zBuildStatus = z.enum(GATEWAY_BUILD_STATUSES);
|
||||
@@ -37,12 +34,9 @@ const ROLE_RESET_SCHEDULE = 'admin.reset.schedule';
|
||||
const ROLE_RESUME_WHEN_STOPPED = 'admin.resume.when-stopped';
|
||||
const ROLE_SURVEY_OPEN = 'admin.survey.open';
|
||||
|
||||
const readSessionToken = (
|
||||
headers: Record<string, string | string[] | undefined>
|
||||
): string | null => {
|
||||
const provided =
|
||||
headers['x-session-token'] ?? headers['authorization'] ?? '';
|
||||
const raw = Array.isArray(provided) ? provided[0] ?? '' : (provided as string);
|
||||
const readSessionToken = (headers: Record<string, string | string[] | undefined>): string | null => {
|
||||
const provided = headers['x-session-token'] ?? headers['authorization'] ?? '';
|
||||
const raw = Array.isArray(provided) ? (provided[0] ?? '') : (provided as string);
|
||||
const token = raw.startsWith('Bearer ') ? raw.slice(7) : raw;
|
||||
const trimmed = token.trim();
|
||||
return trimmed ? trimmed : null;
|
||||
@@ -83,9 +77,7 @@ const resolveAdminAuth = async (ctx: GatewayApiContext): Promise<AdminAuthContex
|
||||
roles.includes(ROLE_SUPERUSER) ||
|
||||
roles.includes(ADMIN_ROLE_SUPERUSER) ||
|
||||
(await isFirstUser(ctx, session.userId));
|
||||
const hasAdminRole =
|
||||
isSuperuser ||
|
||||
roles.some((role) => role === 'admin' || role.startsWith(ADMIN_ROLE_PREFIX));
|
||||
const hasAdminRole = isSuperuser || roles.some((role) => role === 'admin' || role.startsWith(ADMIN_ROLE_PREFIX));
|
||||
if (!hasAdminRole) {
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
@@ -110,11 +102,7 @@ const requireAdminAuth = (ctx: { adminAuth?: AdminAuthContext }): AdminAuthConte
|
||||
return ctx.adminAuth;
|
||||
};
|
||||
|
||||
const roleMatchesScope = (
|
||||
role: string,
|
||||
permission: string,
|
||||
profileName?: string
|
||||
): boolean => {
|
||||
const roleMatchesScope = (role: string, permission: string, profileName?: string): boolean => {
|
||||
if (role === permission || role === `${permission}:*`) {
|
||||
return true;
|
||||
}
|
||||
@@ -124,24 +112,14 @@ const roleMatchesScope = (
|
||||
return false;
|
||||
};
|
||||
|
||||
const hasScopedPermission = (
|
||||
adminAuth: AdminAuthContext,
|
||||
permission: string,
|
||||
profileName?: string
|
||||
): boolean => {
|
||||
const hasScopedPermission = (adminAuth: AdminAuthContext, permission: string, profileName?: string): boolean => {
|
||||
if (adminAuth.isSuperuser) {
|
||||
return true;
|
||||
}
|
||||
return adminAuth.roles.some((role: string) =>
|
||||
roleMatchesScope(role, permission, profileName)
|
||||
);
|
||||
return adminAuth.roles.some((role: string) => roleMatchesScope(role, permission, profileName));
|
||||
};
|
||||
|
||||
const assertPermission = (
|
||||
adminAuth: AdminAuthContext,
|
||||
permission: string,
|
||||
profileName?: string
|
||||
): void => {
|
||||
const assertPermission = (adminAuth: AdminAuthContext, permission: string, profileName?: string): void => {
|
||||
if (hasScopedPermission(adminAuth, permission, profileName)) {
|
||||
return;
|
||||
}
|
||||
@@ -210,15 +188,9 @@ const zSanctionsPatch = z.object({
|
||||
type SanctionsPatch = z.infer<typeof zSanctionsPatch>;
|
||||
|
||||
// 제재 패치 입력을 현재 제재 상태에 병합한다.
|
||||
const applySanctionsPatch = (
|
||||
current: UserSanctions,
|
||||
patch: SanctionsPatch
|
||||
): UserSanctions => {
|
||||
const applySanctionsPatch = (current: UserSanctions, patch: SanctionsPatch): UserSanctions => {
|
||||
const next: UserSanctions = { ...current };
|
||||
const applyField = <K extends keyof UserSanctions>(
|
||||
key: K,
|
||||
value: UserSanctions[K] | null | undefined
|
||||
): void => {
|
||||
const applyField = <K extends keyof UserSanctions>(key: K, value: UserSanctions[K] | null | undefined): void => {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
@@ -242,9 +214,7 @@ const applySanctionsPatch = (
|
||||
delete next.serverRestrictions;
|
||||
} else {
|
||||
const existing = { ...(next.serverRestrictions ?? {}) };
|
||||
for (const [profile, restriction] of Object.entries(
|
||||
patch.serverRestrictions
|
||||
)) {
|
||||
for (const [profile, restriction] of Object.entries(patch.serverRestrictions)) {
|
||||
if (!restriction) {
|
||||
delete existing[profile];
|
||||
} else {
|
||||
@@ -331,14 +301,13 @@ export const adminRouter = router({
|
||||
}),
|
||||
users: router({
|
||||
lookup: userAdminProcedure.input(zUserLookupInput).query(async ({ ctx, input }) => {
|
||||
const user =
|
||||
input.id
|
||||
? await ctx.users.findById(input.id)
|
||||
: input.username
|
||||
? await ctx.users.findByUsername(input.username)
|
||||
: input.email
|
||||
? await ctx.users.findByEmail(input.email)
|
||||
: null;
|
||||
const user = input.id
|
||||
? await ctx.users.findById(input.id)
|
||||
: input.username
|
||||
? await ctx.users.findByUsername(input.username)
|
||||
: input.email
|
||||
? await ctx.users.findByEmail(input.email)
|
||||
: null;
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
@@ -364,10 +333,7 @@ export const adminRouter = router({
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const password = input.newPassword ?? buildAdminPassword();
|
||||
await ctx.users.updatePassword(input.userId, password);
|
||||
await ctx.flushPublisher.publishUserFlush(
|
||||
input.userId,
|
||||
'admin-password-reset'
|
||||
);
|
||||
await ctx.flushPublisher.publishUserFlush(input.userId, 'admin-password-reset');
|
||||
return { password };
|
||||
}),
|
||||
updateRoles: userAdminProcedure
|
||||
@@ -404,10 +370,7 @@ export const adminRouter = router({
|
||||
}
|
||||
const nextRoles = Array.from(roles);
|
||||
await ctx.users.updateRoles(input.userId, nextRoles);
|
||||
await ctx.flushPublisher.publishUserFlush(
|
||||
input.userId,
|
||||
'admin-roles-updated'
|
||||
);
|
||||
await ctx.flushPublisher.publishUserFlush(input.userId, 'admin-roles-updated');
|
||||
return { roles: nextRoles };
|
||||
}),
|
||||
updateSanctions: userAdminProcedure
|
||||
@@ -427,10 +390,7 @@ export const adminRouter = router({
|
||||
}
|
||||
const next = applySanctionsPatch(user.sanctions, input.patch);
|
||||
await ctx.users.updateSanctions(input.userId, next);
|
||||
await ctx.flushPublisher.publishUserFlush(
|
||||
input.userId,
|
||||
'admin-sanctions-updated'
|
||||
);
|
||||
await ctx.flushPublisher.publishUserFlush(input.userId, 'admin-sanctions-updated');
|
||||
return { sanctions: next };
|
||||
}),
|
||||
setServerRestriction: userAdminProcedure
|
||||
@@ -456,10 +416,7 @@ export const adminRouter = router({
|
||||
};
|
||||
const next = applySanctionsPatch(user.sanctions, patch);
|
||||
await ctx.users.updateSanctions(input.userId, next);
|
||||
await ctx.flushPublisher.publishUserFlush(
|
||||
input.userId,
|
||||
'admin-server-restriction'
|
||||
);
|
||||
await ctx.flushPublisher.publishUserFlush(input.userId, 'admin-server-restriction');
|
||||
return { sanctions: next };
|
||||
}),
|
||||
resetProfileIcon: userAdminProcedure
|
||||
@@ -480,10 +437,7 @@ export const adminRouter = router({
|
||||
profileIconResetAt: new Date().toISOString(),
|
||||
});
|
||||
await ctx.users.updateSanctions(input.userId, next);
|
||||
await ctx.flushPublisher.publishUserFlush(
|
||||
input.userId,
|
||||
'admin-profile-icon-reset'
|
||||
);
|
||||
await ctx.flushPublisher.publishUserFlush(input.userId, 'admin-profile-icon-reset');
|
||||
return { profileIconResetAt: next.profileIconResetAt };
|
||||
}),
|
||||
forceDelete: userAdminProcedure
|
||||
@@ -493,10 +447,7 @@ export const adminRouter = router({
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await ctx.flushPublisher.publishUserFlush(
|
||||
input.userId,
|
||||
'admin-force-withdraw'
|
||||
);
|
||||
await ctx.flushPublisher.publishUserFlush(input.userId, 'admin-force-withdraw');
|
||||
await ctx.users.deleteUser(input.userId);
|
||||
return { ok: true };
|
||||
}),
|
||||
@@ -507,9 +458,7 @@ export const adminRouter = router({
|
||||
const runtimeStates = await ctx.orchestrator.listRuntimeStates(
|
||||
profiles.map((profile) => profile.profileName)
|
||||
);
|
||||
const runtimeMap = new Map(
|
||||
runtimeStates.map((state) => [state.profileName, state])
|
||||
);
|
||||
const runtimeMap = new Map(runtimeStates.map((state) => [state.profileName, state]));
|
||||
return profiles.map((profile) => ({
|
||||
...profile,
|
||||
runtime: runtimeMap.get(profile.profileName) ?? {
|
||||
@@ -563,16 +512,11 @@ export const adminRouter = router({
|
||||
message: 'preopenAt and openAt are required for RESERVED status.',
|
||||
});
|
||||
}
|
||||
const result = await ctx.profiles.updateStatus(
|
||||
input.profileName,
|
||||
input.status,
|
||||
{
|
||||
preopenAt: input.preopenAt,
|
||||
openAt: input.openAt,
|
||||
scheduledStartAt:
|
||||
input.status === 'RESERVED' ? input.scheduledStartAt : null,
|
||||
}
|
||||
);
|
||||
const result = await ctx.profiles.updateStatus(input.profileName, input.status, {
|
||||
preopenAt: input.preopenAt,
|
||||
openAt: input.openAt,
|
||||
scheduledStartAt: input.status === 'RESERVED' ? input.scheduledStartAt : null,
|
||||
});
|
||||
if (input.buildCommitSha) {
|
||||
await ctx.profiles.updateBuildStatus(input.profileName, 'IDLE', {
|
||||
commitSha: input.buildCommitSha,
|
||||
@@ -617,10 +561,7 @@ export const adminRouter = router({
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const adminAuth = requireAdminAuth(ctx);
|
||||
if (
|
||||
(input.action === 'ACCELERATE' || input.action === 'DELAY') &&
|
||||
!input.durationMinutes
|
||||
) {
|
||||
if ((input.action === 'ACCELERATE' || input.action === 'DELAY') && !input.durationMinutes) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'durationMinutes is required for acceleration or delay.',
|
||||
@@ -640,24 +581,13 @@ export const adminRouter = router({
|
||||
});
|
||||
}
|
||||
|
||||
const canManageProfiles = hasScopedPermission(
|
||||
adminAuth,
|
||||
ROLE_ADMIN_PROFILES,
|
||||
profile.profileName
|
||||
);
|
||||
const canManageProfiles = hasScopedPermission(adminAuth, ROLE_ADMIN_PROFILES, profile.profileName);
|
||||
const canResume =
|
||||
canManageProfiles ||
|
||||
hasScopedPermission(
|
||||
adminAuth,
|
||||
ROLE_RESUME_WHEN_STOPPED,
|
||||
profile.profileName
|
||||
);
|
||||
canManageProfiles || hasScopedPermission(adminAuth, ROLE_RESUME_WHEN_STOPPED, profile.profileName);
|
||||
const canResetSchedule =
|
||||
canManageProfiles ||
|
||||
hasScopedPermission(adminAuth, ROLE_RESET_SCHEDULE, profile.profileName);
|
||||
canManageProfiles || hasScopedPermission(adminAuth, ROLE_RESET_SCHEDULE, profile.profileName);
|
||||
const canOpenSurvey =
|
||||
canManageProfiles ||
|
||||
hasScopedPermission(adminAuth, ROLE_SURVEY_OPEN, profile.profileName);
|
||||
canManageProfiles || hasScopedPermission(adminAuth, ROLE_SURVEY_OPEN, profile.profileName);
|
||||
|
||||
if (input.action === 'RESUME') {
|
||||
if (profile.status !== 'STOPPED') {
|
||||
@@ -739,15 +669,11 @@ export const adminRouter = router({
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const requestedAt = new Date().toISOString();
|
||||
const result = await ctx.profiles.updateBuildStatus(
|
||||
input.profileName,
|
||||
'QUEUED',
|
||||
{
|
||||
requestedAt,
|
||||
error: null,
|
||||
commitSha: input.commitSha,
|
||||
}
|
||||
);
|
||||
const result = await ctx.profiles.updateBuildStatus(input.profileName, 'QUEUED', {
|
||||
requestedAt,
|
||||
error: null,
|
||||
commitSha: input.commitSha,
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
setBuildStatus: profileAdminProcedure
|
||||
@@ -757,9 +683,7 @@ export const adminRouter = router({
|
||||
status: zBuildStatus,
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) =>
|
||||
ctx.profiles.updateBuildStatus(input.profileName, input.status)
|
||||
),
|
||||
.mutation(async ({ ctx, input }) => ctx.profiles.updateBuildStatus(input.profileName, input.status)),
|
||||
reconcileNow: profileAdminProcedure.mutation(async ({ ctx }) => {
|
||||
await ctx.orchestrator.reconcileNow();
|
||||
return { ok: true };
|
||||
|
||||
@@ -82,10 +82,7 @@ export class InMemoryGatewaySessionService implements GatewaySessionService {
|
||||
this.sessions.delete(sessionToken);
|
||||
}
|
||||
|
||||
async createGameSession(
|
||||
sessionToken: string,
|
||||
profile: string
|
||||
): Promise<GameSessionInfo | null> {
|
||||
async createGameSession(sessionToken: string, profile: string): Promise<GameSessionInfo | null> {
|
||||
const session = await this.getSession(sessionToken);
|
||||
if (!session) {
|
||||
return null;
|
||||
|
||||
@@ -4,9 +4,7 @@ import { createSimplePasswordHasher, type PasswordHasher } from './passwordHashe
|
||||
import type { CreateUserInput, UserRecord, UserRepository } from './userRepository.js';
|
||||
|
||||
// 유저 데이터 저장소를 메모리로 대체한 임시 구현.
|
||||
export const createInMemoryUserRepository = (
|
||||
hasher: PasswordHasher = createSimplePasswordHasher()
|
||||
): UserRepository => {
|
||||
export const createInMemoryUserRepository = (hasher: PasswordHasher = createSimplePasswordHasher()): UserRepository => {
|
||||
const usersByName = new Map<string, UserRecord>();
|
||||
const usersByOauthId = new Map<string, UserRecord>();
|
||||
const usersByEmail = new Map<string, UserRecord>();
|
||||
|
||||
@@ -38,9 +38,7 @@ const parseToken = (payload: Record<string, unknown>): KakaoOAuthToken => {
|
||||
accessToken: String(payload.access_token ?? ''),
|
||||
refreshToken: payload.refresh_token ? String(payload.refresh_token) : undefined,
|
||||
accessTokenExpiresIn: Number(payload.expires_in ?? 0),
|
||||
refreshTokenExpiresIn: payload.refresh_token_expires_in
|
||||
? Number(payload.refresh_token_expires_in)
|
||||
: undefined,
|
||||
refreshTokenExpiresIn: payload.refresh_token_expires_in ? Number(payload.refresh_token_expires_in) : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -141,12 +139,8 @@ export class KakaoOAuthClient {
|
||||
kakaoAccount: {
|
||||
hasEmail: Boolean(kakaoAccount.has_email ?? false),
|
||||
email: kakaoAccount.email ? String(kakaoAccount.email) : undefined,
|
||||
isEmailValid: kakaoAccount.is_email_valid
|
||||
? Boolean(kakaoAccount.is_email_valid)
|
||||
: undefined,
|
||||
isEmailVerified: kakaoAccount.is_email_verified
|
||||
? Boolean(kakaoAccount.is_email_verified)
|
||||
: undefined,
|
||||
isEmailValid: kakaoAccount.is_email_valid ? Boolean(kakaoAccount.is_email_valid) : undefined,
|
||||
isEmailVerified: kakaoAccount.is_email_verified ? Boolean(kakaoAccount.is_email_verified) : undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,6 +8,5 @@ export interface PasswordHasher {
|
||||
// 비밀번호 해싱은 임시 구현이므로 이후 안전한 KDF로 교체한다.
|
||||
export const createSimplePasswordHasher = (): PasswordHasher => ({
|
||||
createSalt: () => randomBytes(16).toString('hex'),
|
||||
hash: (password: string, salt: string) =>
|
||||
createHash('sha256').update(`${salt}:${password}`).digest('hex'),
|
||||
hash: (password: string, salt: string) => createHash('sha256').update(`${salt}:${password}`).digest('hex'),
|
||||
});
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { GatewayPrisma, type GatewayPrismaClient } from '@sammo-ts/infra';
|
||||
|
||||
import { createSimplePasswordHasher, type PasswordHasher } from './passwordHasher.js';
|
||||
import type {
|
||||
CreateUserInput,
|
||||
UserOAuthInfo,
|
||||
UserRecord,
|
||||
UserRepository,
|
||||
UserSanctions,
|
||||
} from './userRepository.js';
|
||||
import type { CreateUserInput, UserOAuthInfo, UserRecord, UserRepository, UserSanctions } from './userRepository.js';
|
||||
|
||||
const readStringArray = (value: unknown): string[] => {
|
||||
if (!Array.isArray(value)) {
|
||||
|
||||
@@ -7,6 +7,5 @@ export interface GatewayRedisKeyBuilder {
|
||||
export const createGatewayRedisKeyBuilder = (prefix: string): GatewayRedisKeyBuilder => ({
|
||||
sessionKey: (sessionToken: string) => `${prefix}:session:${sessionToken}`,
|
||||
sessionGameSetKey: (sessionToken: string) => `${prefix}:session-games:${sessionToken}`,
|
||||
gameSessionKey: (profile: string, gameToken: string) =>
|
||||
`${prefix}:game-session:${profile}:${gameToken}`,
|
||||
gameSessionKey: (profile: string, gameToken: string) => `${prefix}:game-session:${profile}:${gameToken}`,
|
||||
});
|
||||
|
||||
@@ -101,10 +101,7 @@ export class RedisGatewaySessionService implements GatewaySessionService {
|
||||
await this.client.del(key);
|
||||
}
|
||||
|
||||
async createGameSession(
|
||||
sessionToken: string,
|
||||
profile: string
|
||||
): Promise<GameSessionInfo | null> {
|
||||
async createGameSession(sessionToken: string, profile: string): Promise<GameSessionInfo | null> {
|
||||
const session = await this.getSession(sessionToken);
|
||||
if (!session) {
|
||||
return null;
|
||||
|
||||
@@ -69,9 +69,7 @@ const resolveSchemaName = (value: string | undefined): string => {
|
||||
return trimmed ? trimmed : 'public';
|
||||
};
|
||||
|
||||
export const resolveGatewayApiConfigFromEnv = (
|
||||
env: NodeJS.ProcessEnv = process.env
|
||||
): GatewayApiConfig => {
|
||||
export const resolveGatewayApiConfigFromEnv = (env: NodeJS.ProcessEnv = process.env): GatewayApiConfig => {
|
||||
const secret = env.GAME_TOKEN_SECRET ?? env.GATEWAY_TOKEN_SECRET ?? '';
|
||||
if (!secret) {
|
||||
throw new Error('GAME_TOKEN_SECRET is required for gateway token encryption.');
|
||||
@@ -91,17 +89,9 @@ export const resolveGatewayApiConfigFromEnv = (
|
||||
redisKeyPrefix,
|
||||
flushChannel: `${redisKeyPrefix}:flush`,
|
||||
sessionTtlSeconds: parseNumber(env.SESSION_TTL_SECONDS, 60 * 60 * 24 * 7, 'SESSION_TTL_SECONDS'),
|
||||
gameSessionTtlSeconds: parseNumber(
|
||||
env.GAME_SESSION_TTL_SECONDS,
|
||||
60 * 60 * 6,
|
||||
'GAME_SESSION_TTL_SECONDS'
|
||||
),
|
||||
gameSessionTtlSeconds: parseNumber(env.GAME_SESSION_TTL_SECONDS, 60 * 60 * 6, 'GAME_SESSION_TTL_SECONDS'),
|
||||
gameTokenSecret: secret,
|
||||
oauthSessionTtlSeconds: parseNumber(
|
||||
env.OAUTH_SESSION_TTL_SECONDS,
|
||||
10 * 60,
|
||||
'OAUTH_SESSION_TTL_SECONDS'
|
||||
),
|
||||
oauthSessionTtlSeconds: parseNumber(env.OAUTH_SESSION_TTL_SECONDS, 10 * 60, 'OAUTH_SESSION_TTL_SECONDS'),
|
||||
kakaoRestKey,
|
||||
kakaoAdminKey: env.KAKAO_ADMIN_KEY,
|
||||
kakaoRedirectUri,
|
||||
@@ -129,8 +119,7 @@ export const resolveGatewayApiConfigFromEnv = (
|
||||
),
|
||||
workspaceRootHint: env.GATEWAY_WORKSPACE_ROOT ?? process.cwd(),
|
||||
worktreeRoot:
|
||||
env.GATEWAY_WORKTREE_ROOT ??
|
||||
path.resolve(env.GATEWAY_WORKSPACE_ROOT ?? process.cwd(), '.worktrees'),
|
||||
env.GATEWAY_WORKTREE_ROOT ?? path.resolve(env.GATEWAY_WORKSPACE_ROOT ?? process.cwd(), '.worktrees'),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -168,7 +157,6 @@ export const resolveGatewayOrchestratorConfigFromEnv = (
|
||||
),
|
||||
workspaceRootHint: env.GATEWAY_WORKSPACE_ROOT ?? process.cwd(),
|
||||
worktreeRoot:
|
||||
env.GATEWAY_WORKTREE_ROOT ??
|
||||
path.resolve(env.GATEWAY_WORKSPACE_ROOT ?? process.cwd(), '.worktrees'),
|
||||
env.GATEWAY_WORKTREE_ROOT ?? path.resolve(env.GATEWAY_WORKSPACE_ROOT ?? process.cwd(), '.worktrees'),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -61,12 +61,8 @@ export class RepositoryProfileStatusService implements GatewayProfileStatusServi
|
||||
|
||||
async listLobbyProfiles(): Promise<LobbyProfileStatus[]> {
|
||||
const rows = await this.profiles.listProfiles();
|
||||
const runtimeStates = await this.orchestrator.listRuntimeStates(
|
||||
rows.map((profile) => profile.profileName)
|
||||
);
|
||||
const runtimeMap = new Map(
|
||||
runtimeStates.map((state) => [state.profileName, state])
|
||||
);
|
||||
const runtimeStates = await this.orchestrator.listRuntimeStates(rows.map((profile) => profile.profileName));
|
||||
const runtimeMap = new Map(runtimeStates.map((state) => [state.profileName, state]));
|
||||
return rows.map((row) => this.mapProfile(row, runtimeMap));
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,7 @@ import { createGamePostgresConnector, resolvePostgresConfigFromEnv } from '@samm
|
||||
|
||||
import type { BuildRunner } from './buildRunner.js';
|
||||
import type { ProcessManager } from './processManager.js';
|
||||
import type {
|
||||
GatewayProfileRecord,
|
||||
GatewayProfileRepository,
|
||||
GatewayProfileStatus,
|
||||
} from './profileRepository.js';
|
||||
import type { GatewayProfileRecord, GatewayProfileRepository, GatewayProfileStatus } from './profileRepository.js';
|
||||
import type { GitWorkspaceManager } from './workspaceManager.js';
|
||||
|
||||
export interface GatewayProcessConfig {
|
||||
@@ -58,12 +54,7 @@ export const planProfileReconcile = (
|
||||
status: GatewayProfileStatus,
|
||||
runtime: ProfileRuntimeState
|
||||
): { shouldStart: boolean; shouldStop: boolean } => {
|
||||
if (
|
||||
status === 'RUNNING' ||
|
||||
status === 'PREOPEN' ||
|
||||
status === 'PAUSED' ||
|
||||
status === 'COMPLETED'
|
||||
) {
|
||||
if (status === 'RUNNING' || status === 'PREOPEN' || status === 'PAUSED' || status === 'COMPLETED') {
|
||||
return {
|
||||
shouldStart: !(runtime.apiRunning && runtime.daemonRunning),
|
||||
shouldStop: false,
|
||||
@@ -97,8 +88,7 @@ interface GatewayAdminActionResult {
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
|
||||
const normalizeMeta = (value: unknown): Record<string, unknown> =>
|
||||
isRecord(value) ? value : {};
|
||||
const normalizeMeta = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
|
||||
|
||||
const normalizeStatus = (value: unknown): GatewayAdminActionStatus | null => {
|
||||
if (typeof value === 'string') {
|
||||
@@ -108,16 +98,9 @@ const normalizeStatus = (value: unknown): GatewayAdminActionStatus | null => {
|
||||
};
|
||||
|
||||
const buildActionKey = (action: GatewayAdminActionRecord): string =>
|
||||
[
|
||||
action.action ?? '',
|
||||
action.requestedAt ?? '',
|
||||
action.scheduledAt ?? '',
|
||||
action.reason ?? '',
|
||||
].join('|');
|
||||
[action.action ?? '', action.requestedAt ?? '', action.scheduledAt ?? '', action.reason ?? ''].join('|');
|
||||
|
||||
const parseScenarioId = (
|
||||
value: string | number | null | undefined
|
||||
): number | null => {
|
||||
const parseScenarioId = (value: string | number | null | undefined): number | null => {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return Math.floor(value);
|
||||
}
|
||||
@@ -136,8 +119,10 @@ const buildProcessName = (profileName: string, role: 'api' | 'daemon'): string =
|
||||
const buildProcessDefinitions = (
|
||||
profile: GatewayProfileRecord,
|
||||
config: GatewayProcessConfig
|
||||
): { api: { name: string; script: string; cwd: string; env: Record<string, string> };
|
||||
daemon: { name: string; script: string; cwd: string; env: Record<string, string> } } => {
|
||||
): {
|
||||
api: { name: string; script: string; cwd: string; env: Record<string, string> };
|
||||
daemon: { name: string; script: string; cwd: string; env: Record<string, string> };
|
||||
} => {
|
||||
const baseEnv = { ...(config.baseEnv ?? {}) };
|
||||
const apiName = buildProcessName(profile.profileName, 'api');
|
||||
const daemonName = buildProcessName(profile.profileName, 'daemon');
|
||||
@@ -176,10 +161,7 @@ const buildProcessDefinitions = (
|
||||
};
|
||||
};
|
||||
|
||||
const mapRuntimeStates = (
|
||||
profileNames: string[],
|
||||
processNames: Map<string, boolean>
|
||||
): ProfileRuntimeSnapshot[] =>
|
||||
const mapRuntimeStates = (profileNames: string[], processNames: Map<string, boolean>): ProfileRuntimeSnapshot[] =>
|
||||
profileNames.map((profileName) => {
|
||||
const apiName = buildProcessName(profileName, 'api');
|
||||
const daemonName = buildProcessName(profileName, 'daemon');
|
||||
@@ -227,22 +209,10 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
||||
start(): void {
|
||||
void this.reconcileNow();
|
||||
void this.runAdminActionsNow();
|
||||
this.reconcileTimer = setInterval(
|
||||
() => void this.reconcileNow(),
|
||||
this.reconcileIntervalMs
|
||||
);
|
||||
this.scheduleTimer = setInterval(
|
||||
() => void this.runScheduleNow(),
|
||||
this.scheduleIntervalMs
|
||||
);
|
||||
this.buildTimer = setInterval(
|
||||
() => void this.runBuildQueueNow(),
|
||||
this.buildIntervalMs
|
||||
);
|
||||
this.adminActionTimer = setInterval(
|
||||
() => void this.runAdminActionsNow(),
|
||||
this.adminActionIntervalMs
|
||||
);
|
||||
this.reconcileTimer = setInterval(() => void this.reconcileNow(), this.reconcileIntervalMs);
|
||||
this.scheduleTimer = setInterval(() => void this.runScheduleNow(), this.scheduleIntervalMs);
|
||||
this.buildTimer = setInterval(() => void this.runBuildQueueNow(), this.buildIntervalMs);
|
||||
this.adminActionTimer = setInterval(() => void this.runAdminActionsNow(), this.adminActionIntervalMs);
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
@@ -313,8 +283,7 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const queued =
|
||||
profile.buildStatus === 'QUEUED' || profile.buildStatus === 'RUNNING';
|
||||
const queued = profile.buildStatus === 'QUEUED' || profile.buildStatus === 'RUNNING';
|
||||
if (!queued) {
|
||||
await this.repository.updateBuildStatus(profile.profileName, 'QUEUED', {
|
||||
requestedAt: now.toISOString(),
|
||||
@@ -325,11 +294,7 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
||||
}
|
||||
const profiles = await this.repository.listProfiles();
|
||||
for (const profile of profiles) {
|
||||
if (
|
||||
profile.status === 'PREOPEN' &&
|
||||
profile.openAt &&
|
||||
new Date(profile.openAt) <= now
|
||||
) {
|
||||
if (profile.status === 'PREOPEN' && profile.openAt && new Date(profile.openAt) <= now) {
|
||||
await this.repository.updateStatus(profile.profileName, 'RUNNING', {
|
||||
preopenAt: profile.preopenAt ?? null,
|
||||
openAt: profile.openAt ?? null,
|
||||
@@ -363,10 +328,7 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
||||
startedAt,
|
||||
error: null,
|
||||
});
|
||||
const result = await this.runBuildCommands(
|
||||
queued.profileName,
|
||||
queued.buildCommitSha
|
||||
);
|
||||
const result = await this.runBuildCommands(queued.profileName, queued.buildCommitSha);
|
||||
const completedAt = this.now().toISOString();
|
||||
if (result.ok) {
|
||||
await this.repository.updateBuildStatus(queued.profileName, 'SUCCEEDED', {
|
||||
@@ -376,9 +338,7 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
||||
if (queued.status === 'RESERVED') {
|
||||
await this.repository.updateStatus(
|
||||
queued.profileName,
|
||||
queued.openAt && new Date(queued.openAt) <= this.now()
|
||||
? 'RUNNING'
|
||||
: 'PREOPEN',
|
||||
queued.openAt && new Date(queued.openAt) <= this.now() ? 'RUNNING' : 'PREOPEN',
|
||||
{
|
||||
preopenAt: queued.preopenAt ?? null,
|
||||
openAt: queued.openAt ?? null,
|
||||
@@ -418,13 +378,9 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleProfileAdminActions(
|
||||
profile: GatewayProfileRecord
|
||||
): Promise<void> {
|
||||
private async handleProfileAdminActions(profile: GatewayProfileRecord): Promise<void> {
|
||||
const meta = normalizeMeta(profile.meta);
|
||||
const rawActions = Array.isArray(meta.adminActions)
|
||||
? meta.adminActions
|
||||
: [];
|
||||
const rawActions = Array.isArray(meta.adminActions) ? meta.adminActions : [];
|
||||
if (!rawActions.length) {
|
||||
return;
|
||||
}
|
||||
@@ -442,10 +398,7 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
||||
return;
|
||||
}
|
||||
|
||||
const updates = new Map<
|
||||
string,
|
||||
{ status: GatewayAdminActionStatus; detail?: string; handledAt: string }
|
||||
>();
|
||||
const updates = new Map<string, { status: GatewayAdminActionStatus; detail?: string; handledAt: string }>();
|
||||
|
||||
for (const action of pending) {
|
||||
if (action.action !== 'RESET_NOW' && action.action !== 'RESET_SCHEDULED') {
|
||||
@@ -528,9 +481,7 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
||||
return { status: 'FAILED', detail: 'scenarioId is missing' };
|
||||
}
|
||||
const seedTime =
|
||||
action.scheduledAt && action.action === 'RESET_SCHEDULED'
|
||||
? new Date(action.scheduledAt)
|
||||
: this.now();
|
||||
action.scheduledAt && action.action === 'RESET_SCHEDULED' ? new Date(action.scheduledAt) : this.now();
|
||||
const startedAt = this.now().toISOString();
|
||||
await this.repository.updateStatus(profile.profileName, 'STOPPED');
|
||||
await this.stopProfile(profile);
|
||||
@@ -646,10 +597,7 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
||||
async cleanupStaleWorkspaces(): Promise<{ removed: string[]; skipped: string[] }> {
|
||||
const profiles = await this.repository.listProfiles();
|
||||
const cutoff = this.computeCutoffDate(6);
|
||||
const workspaceMap = new Map<
|
||||
string,
|
||||
{ profileNames: string[]; lastUsedAt?: Date; hasActiveBuild: boolean }
|
||||
>();
|
||||
const workspaceMap = new Map<string, { profileNames: string[]; lastUsedAt?: Date; hasActiveBuild: boolean }>();
|
||||
for (const profile of profiles) {
|
||||
const workspace = profile.buildWorkspace;
|
||||
if (!workspace) {
|
||||
@@ -733,8 +681,7 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
||||
const statusMap = new Map<string, boolean>();
|
||||
for (const process of processes) {
|
||||
const status = process.status.toLowerCase();
|
||||
const running =
|
||||
status === 'online' || status === 'launching' || status === 'stopping';
|
||||
const running = status === 'online' || status === 'launching' || status === 'stopping';
|
||||
statusMap.set(process.name, running);
|
||||
}
|
||||
return statusMap;
|
||||
|
||||
@@ -9,9 +9,7 @@ import { resolveWorkspaceRoot } from './workspaceRoot.js';
|
||||
import { GitWorkspaceManager } from './workspaceManager.js';
|
||||
|
||||
export const buildEnvMap = (env: NodeJS.ProcessEnv): Record<string, string> => {
|
||||
const entries = Object.entries(env).filter(
|
||||
(entry): entry is [string, string] => typeof entry[1] === 'string'
|
||||
);
|
||||
const entries = Object.entries(env).filter((entry): entry is [string, string] => typeof entry[1] === 'string');
|
||||
return Object.fromEntries(entries);
|
||||
};
|
||||
|
||||
|
||||
@@ -9,16 +9,10 @@ import { createGatewayOrchestrator } from './orchestratorFactory.js';
|
||||
|
||||
export const runGatewayOrchestrator = async (): Promise<void> => {
|
||||
const config = resolveGatewayOrchestratorConfigFromEnv();
|
||||
const postgres = createGatewayPostgresConnector(
|
||||
resolvePostgresConfigFromEnv({ schema: config.dbSchema })
|
||||
);
|
||||
const postgres = createGatewayPostgresConnector(resolvePostgresConfigFromEnv({ schema: config.dbSchema }));
|
||||
await postgres.connect();
|
||||
|
||||
const { orchestrator } = createGatewayOrchestrator(
|
||||
postgres.prisma as GatewayPrismaClient,
|
||||
config,
|
||||
process.env
|
||||
);
|
||||
const { orchestrator } = createGatewayOrchestrator(postgres.prisma as GatewayPrismaClient, config, process.env);
|
||||
|
||||
const stop = async (reason: string): Promise<void> => {
|
||||
console.info(`[gateway-orchestrator] stopping: ${reason}`);
|
||||
|
||||
@@ -11,13 +11,7 @@ export const GATEWAY_PROFILE_STATUSES = [
|
||||
] as const;
|
||||
export type GatewayProfileStatus = (typeof GATEWAY_PROFILE_STATUSES)[number];
|
||||
|
||||
export const GATEWAY_BUILD_STATUSES = [
|
||||
'IDLE',
|
||||
'QUEUED',
|
||||
'RUNNING',
|
||||
'FAILED',
|
||||
'SUCCEEDED',
|
||||
] as const;
|
||||
export const GATEWAY_BUILD_STATUSES = ['IDLE', 'QUEUED', 'RUNNING', 'FAILED', 'SUCCEEDED'] as const;
|
||||
export type GatewayBuildStatus = (typeof GATEWAY_BUILD_STATUSES)[number];
|
||||
|
||||
export interface GatewayProfileRecord {
|
||||
@@ -81,23 +75,15 @@ export interface GatewayProfileRepository {
|
||||
lastUsedAt?: string | null;
|
||||
}
|
||||
): Promise<GatewayProfileRecord | null>;
|
||||
updateMeta(
|
||||
profileName: string,
|
||||
meta: Record<string, unknown>
|
||||
): Promise<GatewayProfileRecord | null>;
|
||||
updateMeta(profileName: string, meta: Record<string, unknown>): Promise<GatewayProfileRecord | null>;
|
||||
listReservedToStart(now: Date): Promise<GatewayProfileRecord[]>;
|
||||
findQueuedBuild(): Promise<GatewayProfileRecord | null>;
|
||||
updateLastError(profileName: string, lastError: string | null): Promise<void>;
|
||||
updateWorkspaceUsage(
|
||||
profileName: string,
|
||||
workspace: string,
|
||||
lastUsedAt: string
|
||||
): Promise<void>;
|
||||
updateWorkspaceUsage(profileName: string, workspace: string, lastUsedAt: string): Promise<void>;
|
||||
clearWorkspaceUsage(profileNames: string[]): Promise<void>;
|
||||
}
|
||||
|
||||
const toIso = (value: Date | null): string | undefined =>
|
||||
value ? value.toISOString() : undefined;
|
||||
const toIso = (value: Date | null): string | undefined => (value ? value.toISOString() : undefined);
|
||||
|
||||
type GatewayProfileRow = {
|
||||
profileName: string;
|
||||
@@ -145,12 +131,9 @@ const mapProfile = (row: GatewayProfileRow): GatewayProfileRecord => ({
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
});
|
||||
|
||||
const buildProfileName = (profile: string, scenario: string): string =>
|
||||
`${profile}:${scenario}`;
|
||||
const buildProfileName = (profile: string, scenario: string): string => `${profile}:${scenario}`;
|
||||
|
||||
export const createGatewayProfileRepository = (
|
||||
prisma: GatewayPrismaClient
|
||||
): GatewayProfileRepository => ({
|
||||
export const createGatewayProfileRepository = (prisma: GatewayPrismaClient): GatewayProfileRepository => ({
|
||||
async listProfiles(): Promise<GatewayProfileRecord[]> {
|
||||
const rows = await prisma.gatewayProfile.findMany({
|
||||
orderBy: [{ profile: 'asc' }, { scenario: 'asc' }],
|
||||
@@ -175,34 +158,21 @@ export const createGatewayProfileRepository = (
|
||||
status: input.status ?? 'STOPPED',
|
||||
preopenAt: input.preopenAt ? new Date(input.preopenAt) : null,
|
||||
openAt: input.openAt ? new Date(input.openAt) : null,
|
||||
scheduledStartAt: input.scheduledStartAt
|
||||
? new Date(input.scheduledStartAt)
|
||||
: null,
|
||||
scheduledStartAt: input.scheduledStartAt ? new Date(input.scheduledStartAt) : null,
|
||||
buildCommitSha: input.buildCommitSha ?? null,
|
||||
meta: (input.meta ?? {}) as GatewayPrisma.JsonObject,
|
||||
},
|
||||
update: {
|
||||
apiPort: input.apiPort,
|
||||
status: input.status,
|
||||
preopenAt: input.preopenAt
|
||||
? new Date(input.preopenAt)
|
||||
: input.preopenAt === null
|
||||
? null
|
||||
: undefined,
|
||||
openAt: input.openAt
|
||||
? new Date(input.openAt)
|
||||
: input.openAt === null
|
||||
? null
|
||||
: undefined,
|
||||
preopenAt: input.preopenAt ? new Date(input.preopenAt) : input.preopenAt === null ? null : undefined,
|
||||
openAt: input.openAt ? new Date(input.openAt) : input.openAt === null ? null : undefined,
|
||||
scheduledStartAt: input.scheduledStartAt
|
||||
? new Date(input.scheduledStartAt)
|
||||
: input.scheduledStartAt === null
|
||||
? null
|
||||
: undefined,
|
||||
buildCommitSha:
|
||||
input.buildCommitSha === undefined
|
||||
? undefined
|
||||
: input.buildCommitSha,
|
||||
buildCommitSha: input.buildCommitSha === undefined ? undefined : input.buildCommitSha,
|
||||
meta: input.meta ? (input.meta as GatewayPrisma.JsonObject) : undefined,
|
||||
},
|
||||
});
|
||||
@@ -229,11 +199,7 @@ export const createGatewayProfileRepository = (
|
||||
? new Date(schedule.preopenAt)
|
||||
: null,
|
||||
openAt:
|
||||
schedule?.openAt === undefined
|
||||
? undefined
|
||||
: schedule?.openAt
|
||||
? new Date(schedule.openAt)
|
||||
: null,
|
||||
schedule?.openAt === undefined ? undefined : schedule?.openAt ? new Date(schedule.openAt) : null,
|
||||
scheduledStartAt:
|
||||
schedule?.scheduledStartAt === undefined
|
||||
? undefined
|
||||
@@ -262,10 +228,8 @@ export const createGatewayProfileRepository = (
|
||||
where: { profileName },
|
||||
data: {
|
||||
buildStatus: status,
|
||||
buildCommitSha:
|
||||
fields?.commitSha === undefined ? undefined : fields.commitSha,
|
||||
buildWorkspace:
|
||||
fields?.workspace === undefined ? undefined : fields.workspace,
|
||||
buildCommitSha: fields?.commitSha === undefined ? undefined : fields.commitSha,
|
||||
buildWorkspace: fields?.workspace === undefined ? undefined : fields.workspace,
|
||||
buildLastUsedAt:
|
||||
fields?.lastUsedAt === undefined
|
||||
? undefined
|
||||
@@ -279,11 +243,7 @@ export const createGatewayProfileRepository = (
|
||||
? new Date(fields.requestedAt)
|
||||
: null,
|
||||
buildStartedAt:
|
||||
fields?.startedAt === undefined
|
||||
? undefined
|
||||
: fields?.startedAt
|
||||
? new Date(fields.startedAt)
|
||||
: null,
|
||||
fields?.startedAt === undefined ? undefined : fields?.startedAt ? new Date(fields.startedAt) : null,
|
||||
buildCompletedAt:
|
||||
fields?.completedAt === undefined
|
||||
? undefined
|
||||
@@ -295,10 +255,7 @@ export const createGatewayProfileRepository = (
|
||||
});
|
||||
return row ? mapProfile(row) : null;
|
||||
},
|
||||
async updateMeta(
|
||||
profileName: string,
|
||||
meta: Record<string, unknown>
|
||||
): Promise<GatewayProfileRecord | null> {
|
||||
async updateMeta(profileName: string, meta: Record<string, unknown>): Promise<GatewayProfileRecord | null> {
|
||||
const gatewayProfile = prisma.gatewayProfile;
|
||||
const row = await gatewayProfile.update({
|
||||
where: { profileName },
|
||||
@@ -335,11 +292,7 @@ export const createGatewayProfileRepository = (
|
||||
data: { lastError },
|
||||
});
|
||||
},
|
||||
async updateWorkspaceUsage(
|
||||
profileName: string,
|
||||
workspace: string,
|
||||
lastUsedAt: string
|
||||
): Promise<void> {
|
||||
async updateWorkspaceUsage(profileName: string, workspace: string, lastUsedAt: string): Promise<void> {
|
||||
const gatewayProfile = prisma.gatewayProfile;
|
||||
await gatewayProfile.update({
|
||||
where: { profileName },
|
||||
|
||||
@@ -14,11 +14,7 @@ export interface WorkspaceInfo {
|
||||
needsInstall: boolean;
|
||||
}
|
||||
|
||||
const runGit = (
|
||||
args: string[],
|
||||
cwd: string,
|
||||
env?: Record<string, string>
|
||||
): Promise<{ ok: boolean; output: string }> =>
|
||||
const runGit = (args: string[], cwd: string, env?: Record<string, string>): Promise<{ ok: boolean; output: string }> =>
|
||||
new Promise((resolve) => {
|
||||
const child = spawn('git', args, {
|
||||
cwd,
|
||||
@@ -43,8 +39,7 @@ const ensureDir = (dir: string): void => {
|
||||
}
|
||||
};
|
||||
|
||||
const hasInstallMarker = (dir: string): boolean =>
|
||||
fs.existsSync(path.join(dir, 'node_modules', '.pnpm'));
|
||||
const hasInstallMarker = (dir: string): boolean => fs.existsSync(path.join(dir, 'node_modules', '.pnpm'));
|
||||
|
||||
export class GitWorkspaceManager {
|
||||
private readonly repoRoot: string;
|
||||
@@ -63,11 +58,7 @@ export class GitWorkspaceManager {
|
||||
|
||||
const exists = fs.existsSync(workspacePath);
|
||||
if (!exists) {
|
||||
const hasCommit = await runGit(
|
||||
['cat-file', '-e', `${commitSha}^{commit}`],
|
||||
this.repoRoot,
|
||||
this.baseEnv
|
||||
);
|
||||
const hasCommit = await runGit(['cat-file', '-e', `${commitSha}^{commit}`], this.repoRoot, this.baseEnv);
|
||||
if (!hasCommit.ok) {
|
||||
await runGit(['fetch', '--all', '--tags'], this.repoRoot, this.baseEnv);
|
||||
}
|
||||
@@ -97,11 +88,7 @@ export class GitWorkspaceManager {
|
||||
if (!fs.existsSync(resolved)) {
|
||||
return false;
|
||||
}
|
||||
const result = await runGit(
|
||||
['worktree', 'remove', '--force', resolved],
|
||||
this.repoRoot,
|
||||
this.baseEnv
|
||||
);
|
||||
const result = await runGit(['worktree', 'remove', '--force', resolved], this.repoRoot, this.baseEnv);
|
||||
if (!result.ok) {
|
||||
fs.rmSync(resolved, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -6,10 +6,7 @@ const WORKSPACE_MARKERS = ['pnpm-workspace.yaml', 'package.json'];
|
||||
const hasWorkspaceMarker = (dir: string): boolean =>
|
||||
WORKSPACE_MARKERS.some((marker) => fs.existsSync(path.join(dir, marker)));
|
||||
|
||||
export const resolveWorkspaceRoot = (
|
||||
startDir: string,
|
||||
maxDepth = 5
|
||||
): string => {
|
||||
export const resolveWorkspaceRoot = (startDir: string, maxDepth = 5): string => {
|
||||
let current = path.resolve(startDir);
|
||||
for (let depth = 0; depth <= maxDepth; depth += 1) {
|
||||
if (hasWorkspaceMarker(current)) {
|
||||
|
||||
@@ -45,15 +45,15 @@ export const appRouter = router({
|
||||
}),
|
||||
profiles: procedure
|
||||
.input(
|
||||
z.object({
|
||||
sessionToken: z.string().min(1).optional(),
|
||||
}).optional()
|
||||
z
|
||||
.object({
|
||||
sessionToken: z.string().min(1).optional(),
|
||||
})
|
||||
.optional()
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const sessionToken = input?.sessionToken;
|
||||
const session = sessionToken
|
||||
? await ctx.sessions.getSession(sessionToken)
|
||||
: null;
|
||||
const session = sessionToken ? await ctx.sessions.getSession(sessionToken) : null;
|
||||
return ctx.profileStatus.listLobbyProfiles({
|
||||
userId: session?.userId,
|
||||
});
|
||||
@@ -63,10 +63,12 @@ export const appRouter = router({
|
||||
auth: router({
|
||||
kakaoStart: procedure
|
||||
.input(
|
||||
z.object({
|
||||
mode: zOAuthMode.optional(),
|
||||
scopes: z.array(z.string()).optional(),
|
||||
}).optional()
|
||||
z
|
||||
.object({
|
||||
mode: zOAuthMode.optional(),
|
||||
scopes: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional()
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const mode = input?.mode ?? 'login';
|
||||
@@ -96,10 +98,7 @@ export const appRouter = router({
|
||||
}
|
||||
const token = await ctx.kakaoClient.exchangeCode(input.code);
|
||||
const tokenIssuedAt = new Date();
|
||||
const accessTokenValidUntil = addSeconds(
|
||||
tokenIssuedAt,
|
||||
token.accessTokenExpiresIn
|
||||
).toISOString();
|
||||
const accessTokenValidUntil = addSeconds(tokenIssuedAt, token.accessTokenExpiresIn).toISOString();
|
||||
const refreshTokenValidUntil = token.refreshTokenExpiresIn
|
||||
? addSeconds(tokenIssuedAt, token.refreshTokenExpiresIn).toISOString()
|
||||
: undefined;
|
||||
@@ -337,10 +336,7 @@ export const appRouter = router({
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const gameSession = await ctx.sessions.createGameSession(
|
||||
input.sessionToken,
|
||||
input.profile
|
||||
);
|
||||
const gameSession = await ctx.sessions.createGameSession(input.sessionToken, input.profile);
|
||||
if (!gameSession) {
|
||||
throw new TRPCError({
|
||||
code: 'UNAUTHORIZED',
|
||||
|
||||
@@ -22,16 +22,12 @@ import { RepositoryProfileStatusService } from './lobby/profileStatusService.js'
|
||||
|
||||
export const createGatewayApiServer = async () => {
|
||||
const config = resolveGatewayApiConfigFromEnv();
|
||||
const postgres = createGatewayPostgresConnector(
|
||||
resolvePostgresConfigFromEnv({ schema: config.dbSchema })
|
||||
);
|
||||
const postgres = createGatewayPostgresConnector(resolvePostgresConfigFromEnv({ schema: config.dbSchema }));
|
||||
const redis = createRedisConnector(resolveRedisConfigFromEnv());
|
||||
await postgres.connect();
|
||||
await redis.connect();
|
||||
|
||||
const users = createPostgresUserRepository(
|
||||
postgres.prisma as GatewayPrismaClient
|
||||
);
|
||||
const users = createPostgresUserRepository(postgres.prisma as GatewayPrismaClient);
|
||||
const sessions = new RedisGatewaySessionService(redis.client, {
|
||||
keyPrefix: config.redisKeyPrefix,
|
||||
sessionTtlSeconds: config.sessionTtlSeconds,
|
||||
|
||||
Reference in New Issue
Block a user