feat: make general creation daemon-atomic
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user