feat: make general creation daemon-atomic

This commit is contained in:
2026-07-31 00:33:03 +00:00
parent 115218ded8
commit c6a2a0da93
34 changed files with 2227 additions and 655 deletions
@@ -20,9 +20,13 @@ const readObject = <T extends object>(value: unknown, fallback: T): T => {
const readLegacyMemberNo = (value: unknown): number | undefined => {
const legacyData = readObject<Record<string, unknown>>(value, {});
const memberNo = legacyData.memberNo;
return typeof memberNo === 'number' && Number.isSafeInteger(memberNo) && memberNo > 0
? memberNo
: undefined;
return typeof memberNo === 'number' && Number.isSafeInteger(memberNo) && memberNo > 0 ? memberNo : undefined;
};
const readLegacyGrade = (value: unknown): number | undefined => {
const legacyData = readObject<Record<string, unknown>>(value, {});
const grade = legacyData.grade;
return typeof grade === 'number' && Number.isSafeInteger(grade) ? grade : undefined;
};
const mapUser = (row: {
@@ -71,6 +75,7 @@ const mapUser = (row: {
passwordSalt: row.passwordSalt,
createdAt: row.createdAt.toISOString(),
legacyMemberNo: readLegacyMemberNo(row.legacyData),
legacyGrade: readLegacyGrade(row.legacyData),
});
export const createPostgresUserRepository = (
@@ -21,6 +21,7 @@ export interface UserRecord {
passwordSalt: string;
createdAt: string;
legacyMemberNo?: number;
legacyGrade?: number;
}
export interface PublicUser {
@@ -43,6 +44,7 @@ export interface UserSanctions {
notes?: string;
profileIconResetAt?: string;
serverRestrictions?: Record<string, UserServerRestriction>;
legacyPenalty?: Record<string, unknown>;
}
export interface UserServerRestriction {
+11 -13
View File
@@ -13,12 +13,7 @@ import type { UserOAuthInfo } from './auth/userRepository.js';
import { adminRouter } from './adminRouter.js';
import { accountRouter } from './account/router.js';
import { resolveLocalAccountProfilePolicy } from './auth/localAccountPolicy.js';
import {
openPassword,
zDisplayName,
zPasswordEnvelope,
zRegistrationUsername,
} from './auth/registrationInput.js';
import { openPassword, zDisplayName, zPasswordEnvelope, zRegistrationUsername } from './auth/registrationInput.js';
const zUsername = z
.string()
@@ -667,14 +662,17 @@ export const appRouter = router({
expiresAt: addSeconds(now, ctx.gameSessionTtlSeconds).toISOString(),
sessionId: gameSession.gameToken,
user: {
id: gameSession.userId,
username: gameSession.username,
displayName: gameSession.displayName,
roles: gameSession.roles,
createdAt: gameSession.createdAt,
legacyMemberNo: gameSession.legacyMemberNo,
id: user.id,
username: user.username,
displayName: user.displayName,
picture: user.picture,
imageServer: user.imageServer,
canUseGeneralPicture: user.legacyGrade === undefined || user.legacyGrade >= 1,
roles: user.roles,
createdAt: user.createdAt,
legacyMemberNo: user.legacyMemberNo,
},
sanctions: gameSession.sanctions,
sanctions: user.sanctions,
identity: {
kakaoVerified: localAccountPolicy.kakaoVerified,
canCreateGeneral: localAccountPolicy.canCreateGeneral,
+49 -5
View File
@@ -79,7 +79,8 @@ const buildCaller = (options: { userIconDir?: string; localAccountGraceDays?: nu
];
const profiles = {
listProfiles: async () => profileRows,
getProfile: async (profileName: string) => profileRows.find((profile) => profile.profileName === profileName) ?? null,
getProfile: async (profileName: string) =>
profileRows.find((profile) => profile.profileName === profileName) ?? null,
upsertProfile: async () => {
throw new Error('not used');
},
@@ -544,6 +545,49 @@ describe('gateway auth flow', () => {
expect(validated?.user).not.toHaveProperty('legacyMemberNo');
});
it('issues each game token from the latest user roles, sanctions, and icon', async () => {
const { caller, users, sessions } = buildCaller();
const user = await users.createUser({
username: 'fresh-game-identity',
password: 'secretpass',
});
user.legacyGrade = 0;
const session = await sessions.createSession(user);
await users.updateRoles(user.id, ['user', 'latest-role']);
await users.updateSanctions(user.id, {
warningCount: 2,
legacyPenalty: {
any: {
chat: { expire: 4_102_444_800, value: 1 },
},
},
});
await users.updateIcon(user.id, 'latest-owner.webp', 3, new Date('2026-07-30T12:00:00.000Z'));
const issued = await caller.auth.issueGameSession({
sessionToken: session.sessionToken,
profile: 'che:default',
});
const payload = decryptGameSessionToken(issued.gameToken, 'test-secret');
expect(payload?.user).toMatchObject({
id: user.id,
roles: ['user', 'latest-role'],
picture: 'latest-owner.webp',
imageServer: 3,
canUseGeneralPicture: false,
});
expect(payload?.sanctions).toMatchObject({
warningCount: 2,
legacyPenalty: {
any: {
chat: { expire: 4_102_444_800, value: 1 },
},
},
});
});
it('revokes the gateway session and every linked game session on logout', async () => {
const { caller, users, sessions } = buildCaller();
const user = await users.createUser({
@@ -573,10 +617,10 @@ describe('account self service', () => {
const session = await sessions.createSession(user);
await expect(
caller.account.changePassword({
sessionToken: session.sessionToken,
currentCredential: sealPassword('wrong-password'),
newCredential: sealPassword('next-password'),
caller.account.changePassword({
sessionToken: session.sessionToken,
currentCredential: sealPassword('wrong-password'),
newCredential: sealPassword('next-password'),
})
).rejects.toMatchObject({ code: 'UNAUTHORIZED' });