feat: complete user-facing message and account APIs
This commit is contained in:
@@ -26,6 +26,8 @@ GATEWAY_REDIS_PREFIX=sammo:gateway
|
||||
GATEWAY_DB_SCHEMA=public
|
||||
GATEWAY_WORKSPACE_ROOT=/path/to/core2026
|
||||
GATEWAY_WORKTREE_ROOT=/path/to/core2026/.worktrees
|
||||
GATEWAY_USER_ICON_DIR=uploads/user-icons
|
||||
GATEWAY_USER_ICON_PUBLIC_URL=http://localhost:13000/user-icons
|
||||
SESSION_TTL_SECONDS=604800
|
||||
GAME_SESSION_TTL_SECONDS=21600
|
||||
OAUTH_SESSION_TTL_SECONDS=600
|
||||
|
||||
@@ -164,3 +164,4 @@ docker-compose.override.yml
|
||||
docs/image-storage.md
|
||||
playwright-report/
|
||||
test-results/
|
||||
uploads/
|
||||
|
||||
@@ -23,6 +23,14 @@ interface MessageRow {
|
||||
message: unknown;
|
||||
}
|
||||
|
||||
export interface StoredMessage {
|
||||
id: number;
|
||||
mailbox: number;
|
||||
msgType: MessageType;
|
||||
time: Date;
|
||||
payload: MessagePayload;
|
||||
}
|
||||
|
||||
const parsePayload = (value: unknown): MessagePayload => {
|
||||
if (typeof value === 'string') {
|
||||
return JSON.parse(value) as MessagePayload;
|
||||
@@ -113,3 +121,30 @@ export const fetchOldMessagesFromMailbox = async (params: {
|
||||
|
||||
return rows.map(toMessageView);
|
||||
};
|
||||
|
||||
export const fetchMessageById = async (db: DatabaseClient, id: number): Promise<StoredMessage | null> => {
|
||||
const rows = await db.$queryRaw<MessageRow[]>`
|
||||
SELECT id, mailbox, type, src, dest, time, valid_until, message
|
||||
FROM message
|
||||
WHERE id = ${id} AND valid_until > NOW()
|
||||
LIMIT 1
|
||||
`;
|
||||
const row = rows[0];
|
||||
if (!row) return null;
|
||||
return {
|
||||
id: row.id,
|
||||
mailbox: row.mailbox,
|
||||
msgType: row.type,
|
||||
time: new Date(row.time),
|
||||
payload: parsePayload(row.message),
|
||||
};
|
||||
};
|
||||
|
||||
export const invalidateMessages = async (db: DatabaseClient, ids: number[]): Promise<void> => {
|
||||
const uniqueIds = Array.from(new Set(ids.filter((id) => Number.isInteger(id) && id > 0)));
|
||||
if (uniqueIds.length === 0) return;
|
||||
await db.message.updateMany({
|
||||
where: { id: { in: uniqueIds } },
|
||||
data: { validUntil: new Date() },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -14,11 +14,14 @@ import { buildNationTarget, buildTargetFromGeneral, resolveNationInfo } from '..
|
||||
import {
|
||||
fetchMessagesFromMailbox,
|
||||
fetchOldMessagesFromMailbox,
|
||||
fetchMessageById,
|
||||
invalidateMessages,
|
||||
insertMessage,
|
||||
type MessageView,
|
||||
} from '../../messages/store.js';
|
||||
import { publishRealtimeEvent } from '../../realtime/publisher.js';
|
||||
import { getOwnedGeneral } from '../shared/general.js';
|
||||
import { resolveNationPermission } from '../nation/shared.js';
|
||||
|
||||
const zMessageType = z.enum(['private', 'public', 'national', 'diplomacy']);
|
||||
|
||||
@@ -42,36 +45,39 @@ export const messagesRouter = router({
|
||||
diplomacy: MESSAGE_MAILBOX_NATIONAL_BASE + nationId,
|
||||
} satisfies Record<MessageType, number>;
|
||||
|
||||
const [privateMessages, publicMessages, nationalMessages, diplomacyMessages] = await Promise.all([
|
||||
fetchMessagesFromMailbox({
|
||||
db: ctx.db,
|
||||
mailbox: mailboxes.private,
|
||||
msgType: 'private',
|
||||
limit: 15,
|
||||
fromSeq: sequence,
|
||||
}),
|
||||
fetchMessagesFromMailbox({
|
||||
db: ctx.db,
|
||||
mailbox: mailboxes.public,
|
||||
msgType: 'public',
|
||||
limit: 15,
|
||||
fromSeq: sequence,
|
||||
}),
|
||||
fetchMessagesFromMailbox({
|
||||
db: ctx.db,
|
||||
mailbox: mailboxes.national,
|
||||
msgType: 'national',
|
||||
limit: 15,
|
||||
fromSeq: sequence,
|
||||
}),
|
||||
fetchMessagesFromMailbox({
|
||||
db: ctx.db,
|
||||
mailbox: mailboxes.diplomacy,
|
||||
msgType: 'diplomacy',
|
||||
limit: 15,
|
||||
fromSeq: sequence,
|
||||
}),
|
||||
]);
|
||||
const [privateMessages, publicMessages, nationalMessages, diplomacyMessages, readState] = await Promise.all(
|
||||
[
|
||||
fetchMessagesFromMailbox({
|
||||
db: ctx.db,
|
||||
mailbox: mailboxes.private,
|
||||
msgType: 'private',
|
||||
limit: 15,
|
||||
fromSeq: sequence,
|
||||
}),
|
||||
fetchMessagesFromMailbox({
|
||||
db: ctx.db,
|
||||
mailbox: mailboxes.public,
|
||||
msgType: 'public',
|
||||
limit: 15,
|
||||
fromSeq: sequence,
|
||||
}),
|
||||
fetchMessagesFromMailbox({
|
||||
db: ctx.db,
|
||||
mailbox: mailboxes.national,
|
||||
msgType: 'national',
|
||||
limit: 15,
|
||||
fromSeq: sequence,
|
||||
}),
|
||||
fetchMessagesFromMailbox({
|
||||
db: ctx.db,
|
||||
mailbox: mailboxes.diplomacy,
|
||||
msgType: 'diplomacy',
|
||||
limit: 15,
|
||||
fromSeq: sequence,
|
||||
}),
|
||||
ctx.db.messageReadState.findUnique({ where: { generalId: general.id } }),
|
||||
]
|
||||
);
|
||||
|
||||
const messageBuckets: Record<MessageType, MessageView[]> = {
|
||||
private: privateMessages,
|
||||
@@ -117,11 +123,118 @@ export const messagesRouter = router({
|
||||
nationId: nationId,
|
||||
generalName: general.name,
|
||||
latestRead: {
|
||||
diplomacy: 0,
|
||||
private: 0,
|
||||
diplomacy: readState?.latestDiplomacyMessage ?? 0,
|
||||
private: readState?.latestPrivateMessage ?? 0,
|
||||
},
|
||||
};
|
||||
}),
|
||||
getContacts: authedProcedure
|
||||
.input(z.object({ generalId: z.number().int().positive() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
await getOwnedGeneral(ctx, input.generalId);
|
||||
const [nations, generals] = await Promise.all([
|
||||
ctx.db.nation.findMany({
|
||||
select: { id: true, name: true, color: true, meta: true },
|
||||
orderBy: { id: 'asc' },
|
||||
}),
|
||||
ctx.db.general.findMany({
|
||||
where: { npcState: { lt: 2 } },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
nationId: true,
|
||||
officerLevel: true,
|
||||
npcState: true,
|
||||
meta: true,
|
||||
penalty: true,
|
||||
},
|
||||
orderBy: { id: 'asc' },
|
||||
}),
|
||||
]);
|
||||
const nationMeta = new Map(nations.map((nation) => [nation.id, nation.meta]));
|
||||
const grouped = new Map<number, Array<[number, string, number]>>();
|
||||
for (const general of generals) {
|
||||
let flags = 0;
|
||||
if (general.officerLevel === 12) flags |= 1;
|
||||
if (general.npcState === 1) flags |= 2;
|
||||
if (resolveNationPermission(general, nationMeta.get(general.nationId) ?? {}, false) === 4) flags |= 4;
|
||||
const list = grouped.get(general.nationId) ?? [];
|
||||
list.push([general.id, general.name, flags]);
|
||||
grouped.set(general.nationId, list);
|
||||
}
|
||||
const nationList = [
|
||||
{ id: 0, name: '재야', color: '#000000', meta: {} },
|
||||
...nations.filter((nation) => nation.id !== 0),
|
||||
];
|
||||
return {
|
||||
nation: nationList.map((nation) => ({
|
||||
mailbox: MESSAGE_MAILBOX_NATIONAL_BASE + nation.id,
|
||||
name: nation.name,
|
||||
color: nation.color,
|
||||
general: grouped.get(nation.id) ?? [],
|
||||
})),
|
||||
};
|
||||
}),
|
||||
readLatest: authedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
generalId: z.number().int().positive(),
|
||||
type: z.enum(['private', 'diplomacy']),
|
||||
messageId: z.number().int().positive(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const general = await getOwnedGeneral(ctx, input.generalId);
|
||||
const privateValue = input.type === 'private' ? input.messageId : 0;
|
||||
const diplomacyValue = input.type === 'diplomacy' ? input.messageId : 0;
|
||||
await ctx.db.$executeRaw`
|
||||
INSERT INTO message_read_state (
|
||||
general_id,
|
||||
latest_private_message,
|
||||
latest_diplomacy_message,
|
||||
updated_at
|
||||
)
|
||||
VALUES (${general.id}, ${privateValue}, ${diplomacyValue}, NOW())
|
||||
ON CONFLICT (general_id) DO UPDATE SET
|
||||
latest_private_message = GREATEST(
|
||||
message_read_state.latest_private_message,
|
||||
EXCLUDED.latest_private_message
|
||||
),
|
||||
latest_diplomacy_message = GREATEST(
|
||||
message_read_state.latest_diplomacy_message,
|
||||
EXCLUDED.latest_diplomacy_message
|
||||
),
|
||||
updated_at = NOW()
|
||||
`;
|
||||
return { ok: true };
|
||||
}),
|
||||
delete: authedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
generalId: z.number().int().positive(),
|
||||
messageId: z.number().int().positive(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const general = await getOwnedGeneral(ctx, input.generalId);
|
||||
const message = await fetchMessageById(ctx.db, input.messageId);
|
||||
if (!message) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: '메시지가 없습니다.' });
|
||||
}
|
||||
if (message.payload.src.generalId !== general.id) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: '본인의 메시지만 삭제할 수 있습니다.' });
|
||||
}
|
||||
if (message.msgType === 'diplomacy' || message.payload.option?.deletable === false) {
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: '삭제할 수 없는 메시지입니다.' });
|
||||
}
|
||||
if (Date.now() - message.time.getTime() > 5 * 60 * 1000) {
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: '5분 이내의 메시지만 삭제할 수 있습니다.' });
|
||||
}
|
||||
const receiverMessageId = message.payload.option?.receiverMessageID;
|
||||
const ids = [message.id, ...(typeof receiverMessageId === 'number' ? [receiverMessageId] : [])];
|
||||
await invalidateMessages(ctx.db, ids);
|
||||
return { ok: true, deletedIds: ids };
|
||||
}),
|
||||
getOld: authedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { GameSessionTokenPayload } from '@sammo-ts/common/auth/gameToken';
|
||||
import { appRouter } from '../src/router.js';
|
||||
import type { GameApiContext, GeneralRow } from '../src/context.js';
|
||||
|
||||
const general = {
|
||||
id: 7,
|
||||
userId: 'user-7',
|
||||
name: '보낸이',
|
||||
nationId: 1,
|
||||
officerLevel: 5,
|
||||
npcState: 0,
|
||||
meta: {},
|
||||
penalty: {},
|
||||
} as GeneralRow;
|
||||
|
||||
const auth: GameSessionTokenPayload = {
|
||||
version: 1,
|
||||
profile: 'che:default',
|
||||
issuedAt: '2026-01-01T00:00:00.000Z',
|
||||
expiresAt: '2027-01-01T00:00:00.000Z',
|
||||
sessionId: 'session-7',
|
||||
user: {
|
||||
id: 'user-7',
|
||||
username: 'tester',
|
||||
displayName: 'Tester',
|
||||
roles: ['user'],
|
||||
},
|
||||
sanctions: {},
|
||||
};
|
||||
|
||||
const buildContext = (overrides: Record<string, unknown> = {}) => {
|
||||
const executeRaw = vi.fn(async () => 1);
|
||||
const updateMany = vi.fn(async () => ({ count: 1 }));
|
||||
const db = {
|
||||
general: {
|
||||
findUnique: vi.fn(async () => general),
|
||||
findMany: vi.fn(async () => []),
|
||||
},
|
||||
nation: {
|
||||
findMany: vi.fn(async () => []),
|
||||
findUnique: vi.fn(async () => null),
|
||||
},
|
||||
messageReadState: {
|
||||
findUnique: vi.fn(async () => ({
|
||||
generalId: general.id,
|
||||
latestPrivateMessage: 11,
|
||||
latestDiplomacyMessage: 13,
|
||||
updatedAt: new Date(),
|
||||
})),
|
||||
},
|
||||
message: { updateMany },
|
||||
$queryRaw: vi.fn(async () => []),
|
||||
$executeRaw: executeRaw,
|
||||
...overrides,
|
||||
};
|
||||
const context = {
|
||||
db,
|
||||
auth,
|
||||
profile: { id: 'che', scenario: 'default', name: 'che:default' },
|
||||
redis: {},
|
||||
turnDaemon: {},
|
||||
battleSim: {},
|
||||
uploadDir: 'uploads',
|
||||
uploadPath: '/uploads',
|
||||
uploadPublicUrl: null,
|
||||
accessTokenStore: {},
|
||||
flushStore: {},
|
||||
gameTokenSecret: 'test-secret',
|
||||
} as unknown as GameApiContext;
|
||||
return { caller: appRouter.createCaller(context), db, executeRaw, updateMany };
|
||||
};
|
||||
|
||||
describe('messages router missing-flow compatibility', () => {
|
||||
it('returns persisted latest-read positions with recent messages', async () => {
|
||||
const { caller } = buildContext();
|
||||
const result = await caller.messages.getRecent({ generalId: general.id });
|
||||
|
||||
expect(result.latestRead).toEqual({ private: 11, diplomacy: 13 });
|
||||
});
|
||||
|
||||
it('persists latest-read updates through the monotonic upsert', async () => {
|
||||
const { caller, executeRaw } = buildContext();
|
||||
|
||||
await caller.messages.readLatest({
|
||||
generalId: general.id,
|
||||
type: 'private',
|
||||
messageId: 17,
|
||||
});
|
||||
|
||||
expect(executeRaw).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('invalidates a recent owned message and its receiver copy', async () => {
|
||||
const queryRaw = vi.fn(async () => [
|
||||
{
|
||||
id: 21,
|
||||
mailbox: general.id,
|
||||
type: 'private',
|
||||
src: general.id,
|
||||
dest: 8,
|
||||
time: new Date(),
|
||||
valid_until: new Date('9999-12-31T00:00:00Z'),
|
||||
message: {
|
||||
src: {
|
||||
generalId: general.id,
|
||||
generalName: general.name,
|
||||
nationId: 1,
|
||||
nationName: '위',
|
||||
color: '#fff',
|
||||
icon: '',
|
||||
},
|
||||
dest: {
|
||||
generalId: 8,
|
||||
generalName: '받는이',
|
||||
nationId: 2,
|
||||
nationName: '촉',
|
||||
color: '#000',
|
||||
icon: '',
|
||||
},
|
||||
text: '삭제할 메시지',
|
||||
option: { receiverMessageID: 22 },
|
||||
},
|
||||
},
|
||||
]);
|
||||
const { caller, updateMany } = buildContext({ $queryRaw: queryRaw });
|
||||
|
||||
const result = await caller.messages.delete({ generalId: general.id, messageId: 21 });
|
||||
|
||||
expect(result.deletedIds).toEqual([21, 22]);
|
||||
expect(updateMany).toHaveBeenCalledWith({
|
||||
where: { id: { in: [21, 22] } },
|
||||
data: { validUntil: expect.any(Date) },
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects deleting another general message', async () => {
|
||||
const queryRaw = vi.fn(async () => [
|
||||
{
|
||||
id: 23,
|
||||
mailbox: general.id,
|
||||
type: 'private',
|
||||
src: 99,
|
||||
dest: general.id,
|
||||
time: new Date(),
|
||||
valid_until: new Date('9999-12-31T00:00:00Z'),
|
||||
message: {
|
||||
src: { generalId: 99, generalName: '타인', nationId: 1, nationName: '위', color: '#fff', icon: '' },
|
||||
dest: {
|
||||
generalId: general.id,
|
||||
generalName: general.name,
|
||||
nationId: 1,
|
||||
nationName: '위',
|
||||
color: '#fff',
|
||||
icon: '',
|
||||
},
|
||||
text: '타인 메시지',
|
||||
option: {},
|
||||
},
|
||||
},
|
||||
]);
|
||||
const { caller } = buildContext({ $queryRaw: queryRaw });
|
||||
|
||||
await expect(caller.messages.delete({ generalId: general.id, messageId: 23 })).rejects.toMatchObject({
|
||||
code: 'FORBIDDEN',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -492,6 +492,7 @@ export const createDatabaseTurnHooks = async (
|
||||
name: nation.name,
|
||||
color: nation.color,
|
||||
capitalCityId: nation.capitalCityId,
|
||||
chiefGeneralId: nation.chiefGeneralId,
|
||||
gold: nation.gold,
|
||||
rice: nation.rice,
|
||||
level: nation.level,
|
||||
|
||||
@@ -257,7 +257,7 @@ const mapNationRow = (row: TurnEngineNationRow): Nation => ({
|
||||
name: row.name,
|
||||
color: row.color,
|
||||
capitalCityId: row.capitalCityId,
|
||||
chiefGeneralId: null,
|
||||
chiefGeneralId: row.chiefGeneralId,
|
||||
gold: row.gold,
|
||||
rice: row.rice,
|
||||
power: 0,
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^11.2.0",
|
||||
"@fastify/static": "^9.0.0",
|
||||
"@prisma/client": "^7.2.0",
|
||||
"@sammo-ts/common": "workspace:*",
|
||||
"@sammo-ts/game-engine": "workspace:*",
|
||||
@@ -38,6 +39,7 @@
|
||||
"fastify": "^5.6.2",
|
||||
"pm2": "^5.4.3",
|
||||
"redis": "^5.10.0",
|
||||
"sharp": "^0.34.4",
|
||||
"zod": "^4.3.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import sharp from 'sharp';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { GatewayApiContext } from '../context.js';
|
||||
import { procedure, router } from '../trpc.js';
|
||||
import type { UserRecord, UserSanctions } from '../auth/userRepository.js';
|
||||
|
||||
const zSessionToken = z.string().min(1);
|
||||
const zPassword = z.string().min(6).max(128);
|
||||
const MAX_ICON_BYTES = 50 * 1024;
|
||||
const ALLOWED_ICON_FORMATS = new Set(['avif', 'webp', 'jpeg', 'png', 'gif']);
|
||||
|
||||
const requireSessionUser = async (ctx: GatewayApiContext, sessionToken: string): Promise<UserRecord> => {
|
||||
const session = await ctx.sessions.getSession(sessionToken);
|
||||
if (!session) {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Session is not valid.' });
|
||||
}
|
||||
const user = await ctx.users.findById(session.userId);
|
||||
if (!user) {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'User no longer exists.' });
|
||||
}
|
||||
return user;
|
||||
};
|
||||
|
||||
const decodeImage = (input: string): Buffer => {
|
||||
const match = input.match(/^data:[^;]+;base64,(.+)$/);
|
||||
const encoded = match?.[1] ?? input;
|
||||
const buffer = Buffer.from(encoded, 'base64');
|
||||
if (buffer.length === 0 || buffer.length > MAX_ICON_BYTES) {
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: '아이콘은 50KB 이하여야 합니다.' });
|
||||
}
|
||||
return buffer;
|
||||
};
|
||||
|
||||
const sameUtcDate = (left: Date, right: Date): boolean =>
|
||||
left.getUTCFullYear() === right.getUTCFullYear() &&
|
||||
left.getUTCMonth() === right.getUTCMonth() &&
|
||||
left.getUTCDate() === right.getUTCDate();
|
||||
|
||||
const assertIconChangeAvailable = (user: UserRecord, now: Date): void => {
|
||||
if (user.iconUpdatedAt && sameUtcDate(new Date(user.iconUpdatedAt), now)) {
|
||||
throw new TRPCError({ code: 'TOO_MANY_REQUESTS', message: '아이콘은 하루에 한 번만 변경할 수 있습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
const hasActiveSanction = (sanctions: UserSanctions, now: Date): boolean => {
|
||||
const dates = [sanctions.bannedUntil, sanctions.mutedUntil, sanctions.suspendedUntil];
|
||||
for (const value of dates) {
|
||||
if (value && new Date(value) > now) return true;
|
||||
}
|
||||
return Object.values(sanctions.serverRestrictions ?? {}).some((restriction) =>
|
||||
Boolean(restriction.until && new Date(restriction.until) > now)
|
||||
);
|
||||
};
|
||||
|
||||
const buildIconUrl = (ctx: GatewayApiContext, user: UserRecord): string | null => {
|
||||
if (user.imageServer !== 1 || user.picture === 'default.jpg') return null;
|
||||
return `${ctx.userIconPublicUrl.replace(/\/$/, '')}/${encodeURIComponent(user.picture)}`;
|
||||
};
|
||||
|
||||
export const accountRouter = router({
|
||||
get: procedure.input(z.object({ sessionToken: zSessionToken })).query(async ({ ctx, input }) => {
|
||||
const user = await requireSessionUser(ctx, input.sessionToken);
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
displayName: user.displayName,
|
||||
roles: user.roles,
|
||||
oauthType: user.oauthType,
|
||||
createdAt: user.createdAt,
|
||||
iconUrl: buildIconUrl(ctx, user),
|
||||
thirdPartyUse: user.thirdPartyUse,
|
||||
deleteAfter: user.deleteAfter ?? null,
|
||||
};
|
||||
}),
|
||||
changePassword: procedure
|
||||
.input(
|
||||
z.object({
|
||||
sessionToken: zSessionToken,
|
||||
currentPassword: zPassword,
|
||||
newPassword: zPassword,
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const user = await requireSessionUser(ctx, input.sessionToken);
|
||||
if (!(await ctx.users.verifyPassword(user, input.currentPassword))) {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED', message: '현재 비밀번호가 일치하지 않습니다.' });
|
||||
}
|
||||
await ctx.users.updatePassword(user.id, input.newPassword);
|
||||
await ctx.flushPublisher.publishUserFlush(user.id, 'password-changed');
|
||||
return { ok: true };
|
||||
}),
|
||||
scheduleDeletion: procedure
|
||||
.input(z.object({ sessionToken: zSessionToken, currentPassword: zPassword }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const user = await requireSessionUser(ctx, input.sessionToken);
|
||||
if (!(await ctx.users.verifyPassword(user, input.currentPassword))) {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED', message: '현재 비밀번호가 일치하지 않습니다.' });
|
||||
}
|
||||
if (user.deleteAfter) {
|
||||
throw new TRPCError({ code: 'CONFLICT', message: '이미 탈퇴 처리되어 있습니다.' });
|
||||
}
|
||||
const now = new Date();
|
||||
if (hasActiveSanction(user.sanctions, now)) {
|
||||
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: '징계가 남아 있어 탈퇴할 수 없습니다.' });
|
||||
}
|
||||
const deleteAfter = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
|
||||
await ctx.users.scheduleDeletion(user.id, deleteAfter);
|
||||
await ctx.sessions.revokeSession(input.sessionToken, { revokeGames: true });
|
||||
await ctx.flushPublisher.publishUserFlush(user.id, 'account-deletion-scheduled');
|
||||
return { ok: true, deleteAfter: deleteAfter.toISOString() };
|
||||
}),
|
||||
disallowThirdPartyUse: procedure
|
||||
.input(z.object({ sessionToken: zSessionToken }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const user = await requireSessionUser(ctx, input.sessionToken);
|
||||
await ctx.users.setThirdPartyUse(user.id, false);
|
||||
return { ok: true };
|
||||
}),
|
||||
changeIcon: procedure
|
||||
.input(
|
||||
z.object({
|
||||
sessionToken: zSessionToken,
|
||||
imageData: z.string().min(1).max(100_000),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const user = await requireSessionUser(ctx, input.sessionToken);
|
||||
const now = new Date();
|
||||
assertIconChangeAvailable(user, now);
|
||||
const buffer = decodeImage(input.imageData);
|
||||
const metadata = await sharp(buffer, { animated: true }).metadata();
|
||||
if (!metadata.format || !ALLOWED_ICON_FORMATS.has(metadata.format)) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'avif, webp, jpg, gif, png 아이콘만 사용할 수 있습니다.',
|
||||
});
|
||||
}
|
||||
if (!metadata.width || metadata.width < 64 || metadata.width > 128 || metadata.height !== metadata.width) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: '아이콘은 64x64~128x128 범위의 정사각형이어야 합니다.',
|
||||
});
|
||||
}
|
||||
const extension = metadata.format === 'jpeg' ? 'jpg' : metadata.format;
|
||||
const filename = `${randomBytes(8).toString('hex')}.${extension}`;
|
||||
await fs.mkdir(ctx.userIconDir, { recursive: true });
|
||||
await fs.writeFile(path.join(ctx.userIconDir, filename), buffer, { flag: 'wx' });
|
||||
await ctx.users.updateIcon(user.id, filename, 1, now);
|
||||
return {
|
||||
ok: true,
|
||||
iconUrl: `${ctx.userIconPublicUrl.replace(/\/$/, '')}/${filename}`,
|
||||
};
|
||||
}),
|
||||
deleteIcon: procedure.input(z.object({ sessionToken: zSessionToken })).mutation(async ({ ctx, input }) => {
|
||||
const user = await requireSessionUser(ctx, input.sessionToken);
|
||||
const now = new Date();
|
||||
assertIconChangeAvailable(user, now);
|
||||
await ctx.users.updateIcon(user.id, 'default.jpg', 0, now);
|
||||
return { ok: true, iconUrl: null };
|
||||
}),
|
||||
});
|
||||
@@ -43,6 +43,9 @@ export const createInMemoryUserRepository = (hasher: PasswordHasher = createSimp
|
||||
oauthId: input.oauth?.id,
|
||||
email: input.oauth?.email,
|
||||
oauthInfo: input.oauth?.info,
|
||||
picture: 'default.jpg',
|
||||
imageServer: 0,
|
||||
thirdPartyUse: true,
|
||||
passwordSalt: salt,
|
||||
passwordHash: hasher.hash(input.password, salt),
|
||||
createdAt: new Date().toISOString(),
|
||||
@@ -97,6 +100,35 @@ export const createInMemoryUserRepository = (hasher: PasswordHasher = createSimp
|
||||
}
|
||||
throw new Error('User not found.');
|
||||
},
|
||||
async updateIcon(userId: string, picture: string, imageServer: number, updatedAt: Date): Promise<void> {
|
||||
for (const user of usersByName.values()) {
|
||||
if (user.id === userId) {
|
||||
user.picture = picture;
|
||||
user.imageServer = imageServer;
|
||||
user.iconUpdatedAt = updatedAt.toISOString();
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new Error('User not found.');
|
||||
},
|
||||
async setThirdPartyUse(userId: string, allowed: boolean): Promise<void> {
|
||||
for (const user of usersByName.values()) {
|
||||
if (user.id === userId) {
|
||||
user.thirdPartyUse = allowed;
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new Error('User not found.');
|
||||
},
|
||||
async scheduleDeletion(userId: string, deleteAfter: Date): Promise<void> {
|
||||
for (const user of usersByName.values()) {
|
||||
if (user.id === userId) {
|
||||
user.deleteAfter = deleteAfter.toISOString();
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new Error('User not found.');
|
||||
},
|
||||
async deleteUser(userId: string): Promise<void> {
|
||||
for (const [username, user] of usersByName.entries()) {
|
||||
if (user.id === userId) {
|
||||
|
||||
@@ -29,6 +29,11 @@ const mapUser = (row: {
|
||||
oauthId: string | null;
|
||||
email: string | null;
|
||||
oauthInfo: GatewayPrisma.JsonValue;
|
||||
picture: string;
|
||||
imageServer: number;
|
||||
iconUpdatedAt: Date | null;
|
||||
thirdPartyUse: boolean;
|
||||
deleteAfter: Date | null;
|
||||
createdAt: Date;
|
||||
}): UserRecord => ({
|
||||
id: row.id,
|
||||
@@ -40,6 +45,11 @@ const mapUser = (row: {
|
||||
oauthId: row.oauthId ?? undefined,
|
||||
email: row.email ?? undefined,
|
||||
oauthInfo: readObject<UserOAuthInfo>(row.oauthInfo, {}),
|
||||
picture: row.picture,
|
||||
imageServer: row.imageServer,
|
||||
iconUpdatedAt: row.iconUpdatedAt?.toISOString(),
|
||||
thirdPartyUse: row.thirdPartyUse,
|
||||
deleteAfter: row.deleteAfter?.toISOString(),
|
||||
passwordHash: row.passwordHash,
|
||||
passwordSalt: row.passwordSalt,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
@@ -139,6 +149,28 @@ export const createPostgresUserRepository = (
|
||||
},
|
||||
});
|
||||
},
|
||||
async updateIcon(userId: string, picture: string, imageServer: number, updatedAt: Date): Promise<void> {
|
||||
await prisma.appUser.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
picture,
|
||||
imageServer,
|
||||
iconUpdatedAt: updatedAt,
|
||||
},
|
||||
});
|
||||
},
|
||||
async setThirdPartyUse(userId: string, allowed: boolean): Promise<void> {
|
||||
await prisma.appUser.update({
|
||||
where: { id: userId },
|
||||
data: { thirdPartyUse: allowed },
|
||||
});
|
||||
},
|
||||
async scheduleDeletion(userId: string, deleteAfter: Date): Promise<void> {
|
||||
await prisma.appUser.update({
|
||||
where: { id: userId },
|
||||
data: { deleteAfter },
|
||||
});
|
||||
},
|
||||
async deleteUser(userId: string): Promise<void> {
|
||||
await prisma.appUser.delete({
|
||||
where: { id: userId },
|
||||
|
||||
@@ -8,6 +8,11 @@ export interface UserRecord {
|
||||
oauthId?: string;
|
||||
email?: string;
|
||||
oauthInfo?: UserOAuthInfo;
|
||||
picture: string;
|
||||
imageServer: number;
|
||||
iconUpdatedAt?: string;
|
||||
thirdPartyUse: boolean;
|
||||
deleteAfter?: string;
|
||||
passwordHash: string;
|
||||
passwordSalt: string;
|
||||
createdAt: string;
|
||||
@@ -18,6 +23,7 @@ export interface PublicUser {
|
||||
username: string;
|
||||
displayName: string;
|
||||
roles: string[];
|
||||
picture: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
@@ -44,6 +50,7 @@ export const toPublicUser = (user: UserRecord): PublicUser => ({
|
||||
username: user.username,
|
||||
displayName: user.displayName,
|
||||
roles: user.roles,
|
||||
picture: user.picture,
|
||||
createdAt: user.createdAt,
|
||||
});
|
||||
|
||||
@@ -70,6 +77,9 @@ export interface UserRepository {
|
||||
updateOAuthInfo(userId: string, oauthInfo: UserOAuthInfo): Promise<void>;
|
||||
updateRoles(userId: string, roles: string[]): Promise<void>;
|
||||
updateSanctions(userId: string, sanctions: UserSanctions): Promise<void>;
|
||||
updateIcon(userId: string, picture: string, imageServer: number, updatedAt: Date): Promise<void>;
|
||||
setThirdPartyUse(userId: string, allowed: boolean): Promise<void>;
|
||||
scheduleDeletion(userId: string, deleteAfter: Date): Promise<void>;
|
||||
deleteUser(userId: string): Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface GatewayApiConfig {
|
||||
kakaoAdminKey?: string;
|
||||
kakaoRedirectUri: string;
|
||||
publicBaseUrl: string;
|
||||
userIconDir: string;
|
||||
userIconPublicUrl: string;
|
||||
adminLocalAccountEnabled: boolean;
|
||||
orchestratorEnabled: boolean;
|
||||
orchestratorReconcileIntervalMs: number;
|
||||
@@ -81,6 +83,8 @@ export const resolveGatewayApiConfigFromEnv = (env: NodeJS.ProcessEnv = process.
|
||||
kakaoAdminKey: env.KAKAO_ADMIN_KEY,
|
||||
kakaoRedirectUri,
|
||||
publicBaseUrl,
|
||||
userIconDir: env.GATEWAY_USER_ICON_DIR ?? 'uploads/user-icons',
|
||||
userIconPublicUrl: env.GATEWAY_USER_ICON_PUBLIC_URL ?? `${publicBaseUrl.replace(/\/$/, '')}/user-icons`,
|
||||
adminLocalAccountEnabled: parseBooleanWithFallback(env.GATEWAY_ADMIN_LOCAL_ACCOUNT_ENABLED, false),
|
||||
orchestratorEnabled: parseBooleanWithFallback(env.GATEWAY_ORCHESTRATOR_ENABLED, false),
|
||||
orchestratorReconcileIntervalMs: parseNumberWithFallback(
|
||||
|
||||
@@ -18,6 +18,8 @@ export interface GatewayApiContext {
|
||||
kakaoClient: KakaoOAuthClient;
|
||||
oauthSessions: OAuthSessionStore;
|
||||
publicBaseUrl: string;
|
||||
userIconDir: string;
|
||||
userIconPublicUrl: string;
|
||||
adminLocalAccountEnabled: boolean;
|
||||
profiles: GatewayProfileRepository;
|
||||
orchestrator: GatewayOrchestratorHandle;
|
||||
@@ -36,6 +38,8 @@ export const createGatewayApiContext = (options: {
|
||||
kakaoClient: KakaoOAuthClient;
|
||||
oauthSessions: OAuthSessionStore;
|
||||
publicBaseUrl: string;
|
||||
userIconDir?: string;
|
||||
userIconPublicUrl?: string;
|
||||
adminLocalAccountEnabled: boolean;
|
||||
profiles: GatewayProfileRepository;
|
||||
orchestrator: GatewayOrchestratorHandle;
|
||||
@@ -51,6 +55,8 @@ export const createGatewayApiContext = (options: {
|
||||
kakaoClient: options.kakaoClient,
|
||||
oauthSessions: options.oauthSessions,
|
||||
publicBaseUrl: options.publicBaseUrl,
|
||||
userIconDir: options.userIconDir ?? 'uploads/user-icons',
|
||||
userIconPublicUrl: options.userIconPublicUrl ?? `${options.publicBaseUrl.replace(/\/$/, '')}/user-icons`,
|
||||
adminLocalAccountEnabled: options.adminLocalAccountEnabled,
|
||||
profiles: options.profiles,
|
||||
orchestrator: options.orchestrator,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { procedure, router } from './trpc.js';
|
||||
import { toPublicUser } from './auth/userRepository.js';
|
||||
import type { UserOAuthInfo } from './auth/userRepository.js';
|
||||
import { adminRouter } from './adminRouter.js';
|
||||
import { accountRouter } from './account/router.js';
|
||||
|
||||
const zUsername = z.string().min(2).max(32);
|
||||
const zPassword = z.string().min(6).max(128);
|
||||
@@ -61,6 +62,7 @@ export const appRouter = router({
|
||||
}),
|
||||
}),
|
||||
admin: adminRouter,
|
||||
account: accountRouter,
|
||||
auth: router({
|
||||
bootstrapLocal: procedure
|
||||
.input(
|
||||
@@ -330,6 +332,12 @@ export const appRouter = router({
|
||||
message: 'Invalid username or password.',
|
||||
});
|
||||
}
|
||||
if (user.deleteAfter) {
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
message: 'Account deletion is pending.',
|
||||
});
|
||||
}
|
||||
const ok = await ctx.users.verifyPassword(user, input.password);
|
||||
if (!ok) {
|
||||
throw new TRPCError({
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import fastify, { type FastifyRequest } from 'fastify';
|
||||
import cors from '@fastify/cors';
|
||||
import fastifyStatic from '@fastify/static';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';
|
||||
import {
|
||||
createGatewayPostgresConnector,
|
||||
@@ -60,6 +63,12 @@ export const createGatewayApiServer = async () => {
|
||||
origin: true,
|
||||
credentials: true,
|
||||
});
|
||||
await fs.mkdir(path.resolve(process.cwd(), config.userIconDir), { recursive: true });
|
||||
await app.register(fastifyStatic, {
|
||||
root: path.resolve(process.cwd(), config.userIconDir),
|
||||
prefix: '/user-icons/',
|
||||
decorateReply: false,
|
||||
});
|
||||
|
||||
await app.register(fastifyTRPCPlugin, {
|
||||
prefix: config.trpcPath,
|
||||
@@ -75,6 +84,8 @@ export const createGatewayApiServer = async () => {
|
||||
kakaoClient,
|
||||
oauthSessions,
|
||||
publicBaseUrl: config.publicBaseUrl,
|
||||
userIconDir: path.resolve(process.cwd(), config.userIconDir),
|
||||
userIconPublicUrl: config.userIconPublicUrl,
|
||||
adminLocalAccountEnabled: config.adminLocalAccountEnabled,
|
||||
profiles,
|
||||
orchestrator,
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import sharp from 'sharp';
|
||||
|
||||
import { InMemoryGatewaySessionService } from '../src/auth/inMemorySessionService.js';
|
||||
import { createInMemoryUserRepository } from '../src/auth/inMemoryUserRepository.js';
|
||||
@@ -10,7 +14,7 @@ import { appRouter } from '../src/router.js';
|
||||
import type { GatewayPrismaClient } from '@sammo-ts/infra';
|
||||
import { decryptGameSessionToken } from '@sammo-ts/common/auth/gameToken';
|
||||
|
||||
const buildCaller = () => {
|
||||
const buildCaller = (options: { userIconDir?: string } = {}) => {
|
||||
const users = createInMemoryUserRepository();
|
||||
const sessions = new InMemoryGatewaySessionService({
|
||||
sessionTtlSeconds: 3600,
|
||||
@@ -83,6 +87,8 @@ const buildCaller = () => {
|
||||
kakaoClient: kakaoClient as unknown as KakaoOAuthClient,
|
||||
oauthSessions,
|
||||
publicBaseUrl: 'http://localhost',
|
||||
userIconDir: options.userIconDir,
|
||||
userIconPublicUrl: 'http://localhost/user-icons',
|
||||
adminLocalAccountEnabled: false,
|
||||
profiles,
|
||||
orchestrator,
|
||||
@@ -95,7 +101,7 @@ const buildCaller = () => {
|
||||
} as unknown as GatewayPrismaClient,
|
||||
})
|
||||
);
|
||||
return { caller, oauthSessions };
|
||||
return { caller, oauthSessions, users, sessions };
|
||||
};
|
||||
|
||||
describe('gateway auth flow', () => {
|
||||
@@ -167,3 +173,99 @@ describe('gateway auth flow', () => {
|
||||
expect(validated?.user.username).toBe('tester');
|
||||
});
|
||||
});
|
||||
|
||||
describe('account self service', () => {
|
||||
it('changes only the authenticated user password after verifying the current password', async () => {
|
||||
const { caller, users, sessions } = buildCaller();
|
||||
const user = await users.createUser({
|
||||
username: 'self-service',
|
||||
password: 'current-password',
|
||||
});
|
||||
const session = await sessions.createSession(user);
|
||||
|
||||
await expect(
|
||||
caller.account.changePassword({
|
||||
sessionToken: session.sessionToken,
|
||||
currentPassword: 'wrong-password',
|
||||
newPassword: 'next-password',
|
||||
})
|
||||
).rejects.toMatchObject({ code: 'UNAUTHORIZED' });
|
||||
|
||||
await caller.account.changePassword({
|
||||
sessionToken: session.sessionToken,
|
||||
currentPassword: 'current-password',
|
||||
newPassword: 'next-password',
|
||||
});
|
||||
|
||||
const refreshed = await users.findById(user.id);
|
||||
expect(refreshed && (await users.verifyPassword(refreshed, 'next-password'))).toBe(true);
|
||||
});
|
||||
|
||||
it('revokes the session and schedules deletion after 30 days', async () => {
|
||||
const { caller, users, sessions } = buildCaller();
|
||||
const user = await users.createUser({
|
||||
username: 'delete-self',
|
||||
password: 'current-password',
|
||||
});
|
||||
const session = await sessions.createSession(user);
|
||||
|
||||
const result = await caller.account.scheduleDeletion({
|
||||
sessionToken: session.sessionToken,
|
||||
currentPassword: 'current-password',
|
||||
});
|
||||
|
||||
expect(new Date(result.deleteAfter).getTime()).toBeGreaterThan(Date.now() + 29 * 24 * 60 * 60 * 1000);
|
||||
expect((await users.findById(user.id))?.deleteAfter).toBe(result.deleteAfter);
|
||||
expect(await sessions.getSession(session.sessionToken)).toBeNull();
|
||||
});
|
||||
|
||||
it('revokes third-party use consent without allowing it to be re-enabled', async () => {
|
||||
const { caller, users, sessions } = buildCaller();
|
||||
const user = await users.createUser({
|
||||
username: 'privacy-self',
|
||||
password: 'current-password',
|
||||
});
|
||||
const session = await sessions.createSession(user);
|
||||
|
||||
await caller.account.disallowThirdPartyUse({ sessionToken: session.sessionToken });
|
||||
|
||||
expect((await users.findById(user.id))?.thirdPartyUse).toBe(false);
|
||||
});
|
||||
|
||||
it('validates and stores a legacy-sized account icon with a daily change limit', async () => {
|
||||
const iconDir = await fs.mkdtemp(path.join(os.tmpdir(), 'sammo-account-icon-'));
|
||||
try {
|
||||
const { caller, users, sessions } = buildCaller({ userIconDir: iconDir });
|
||||
const user = await users.createUser({
|
||||
username: 'icon-self',
|
||||
password: 'current-password',
|
||||
});
|
||||
const session = await sessions.createSession(user);
|
||||
const png = await sharp({
|
||||
create: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
channels: 4,
|
||||
background: '#334455',
|
||||
},
|
||||
})
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
const result = await caller.account.changeIcon({
|
||||
sessionToken: session.sessionToken,
|
||||
imageData: `data:image/png;base64,${png.toString('base64')}`,
|
||||
});
|
||||
const updated = await users.findById(user.id);
|
||||
|
||||
expect(result.iconUrl).toMatch(/^http:\/\/localhost\/user-icons\/[a-f0-9]{16}\.png$/);
|
||||
expect(updated?.imageServer).toBe(1);
|
||||
expect(await fs.stat(path.join(iconDir, updated?.picture ?? 'missing'))).toBeTruthy();
|
||||
await expect(caller.account.deleteIcon({ sessionToken: session.sessionToken })).rejects.toMatchObject({
|
||||
code: 'TOO_MANY_REQUESTS',
|
||||
});
|
||||
} finally {
|
||||
await fs.rm(iconDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+454
-431
@@ -1,47 +1,47 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
output = "./generated/game"
|
||||
engineType = "binary"
|
||||
provider = "prisma-client-js"
|
||||
output = "./generated/game"
|
||||
engineType = "binary"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
enum LogScope {
|
||||
SYSTEM
|
||||
NATION
|
||||
GENERAL
|
||||
USER
|
||||
SYSTEM
|
||||
NATION
|
||||
GENERAL
|
||||
USER
|
||||
}
|
||||
|
||||
enum LogCategory {
|
||||
HISTORY
|
||||
SUMMARY
|
||||
ACTION
|
||||
BATTLE_BRIEF
|
||||
BATTLE_DETAIL
|
||||
USER
|
||||
HISTORY
|
||||
SUMMARY
|
||||
ACTION
|
||||
BATTLE_BRIEF
|
||||
BATTLE_DETAIL
|
||||
USER
|
||||
}
|
||||
|
||||
enum AuctionStatus {
|
||||
OPEN
|
||||
FINALIZING
|
||||
FINISHED
|
||||
CANCELED
|
||||
OPEN
|
||||
FINALIZING
|
||||
FINISHED
|
||||
CANCELED
|
||||
}
|
||||
|
||||
enum AuctionType {
|
||||
BUY_RICE
|
||||
SELL_RICE
|
||||
UNIQUE_ITEM
|
||||
BUY_RICE
|
||||
SELL_RICE
|
||||
UNIQUE_ITEM
|
||||
}
|
||||
|
||||
enum DiplomacyLetterState {
|
||||
PROPOSED
|
||||
ACTIVATED
|
||||
CANCELLED
|
||||
REPLACED
|
||||
PROPOSED
|
||||
ACTIVATED
|
||||
CANCELLED
|
||||
REPLACED
|
||||
}
|
||||
|
||||
enum InputEventStatus {
|
||||
@@ -78,538 +78,561 @@ model InputEvent {
|
||||
}
|
||||
|
||||
model WorldState {
|
||||
id Int @id @default(autoincrement())
|
||||
scenarioCode String @map("scenario_code")
|
||||
currentYear Int @map("current_year")
|
||||
currentMonth Int @map("current_month")
|
||||
tickSeconds Int @map("tick_seconds")
|
||||
config Json @default(dbgenerated("'{}'::jsonb"))
|
||||
meta Json @default(dbgenerated("'{}'::jsonb"))
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
id Int @id @default(autoincrement())
|
||||
scenarioCode String @map("scenario_code")
|
||||
currentYear Int @map("current_year")
|
||||
currentMonth Int @map("current_month")
|
||||
tickSeconds Int @map("tick_seconds")
|
||||
config Json @default(dbgenerated("'{}'::jsonb"))
|
||||
meta Json @default(dbgenerated("'{}'::jsonb"))
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@map("world_state")
|
||||
@@map("world_state")
|
||||
}
|
||||
|
||||
model Nation {
|
||||
id Int @id
|
||||
name String
|
||||
color String
|
||||
capitalCityId Int? @map("capital_city_id")
|
||||
gold Int @default(0)
|
||||
rice Int @default(0)
|
||||
tech Float @default(0)
|
||||
level Int @default(0)
|
||||
typeCode String @default("che_중립") @map("type_code")
|
||||
meta Json @default(dbgenerated("'{}'::jsonb"))
|
||||
id Int @id
|
||||
name String
|
||||
color String
|
||||
capitalCityId Int? @map("capital_city_id")
|
||||
chiefGeneralId Int? @map("chief_general_id")
|
||||
gold Int @default(0)
|
||||
rice Int @default(0)
|
||||
tech Float @default(0)
|
||||
level Int @default(0)
|
||||
typeCode String @default("che_중립") @map("type_code")
|
||||
meta Json @default(dbgenerated("'{}'::jsonb"))
|
||||
|
||||
@@map("nation")
|
||||
@@map("nation")
|
||||
}
|
||||
|
||||
model City {
|
||||
id Int @id
|
||||
name String
|
||||
level Int
|
||||
nationId Int @default(0) @map("nation_id")
|
||||
supplyState Int @default(1) @map("supply_state")
|
||||
frontState Int @default(0) @map("front_state")
|
||||
population Int @map("pop")
|
||||
populationMax Int @map("pop_max")
|
||||
agriculture Int @map("agri")
|
||||
agricultureMax Int @map("agri_max")
|
||||
commerce Int @map("comm")
|
||||
commerceMax Int @map("comm_max")
|
||||
security Int @map("secu")
|
||||
securityMax Int @map("secu_max")
|
||||
trust Int @default(0)
|
||||
trade Int @default(100)
|
||||
defence Int @map("def")
|
||||
defenceMax Int @map("def_max")
|
||||
wall Int @map("wall")
|
||||
wallMax Int @map("wall_max")
|
||||
region Int
|
||||
conflict Json @default(dbgenerated("'{}'::jsonb"))
|
||||
meta Json @default(dbgenerated("'{}'::jsonb"))
|
||||
id Int @id
|
||||
name String
|
||||
level Int
|
||||
nationId Int @default(0) @map("nation_id")
|
||||
supplyState Int @default(1) @map("supply_state")
|
||||
frontState Int @default(0) @map("front_state")
|
||||
population Int @map("pop")
|
||||
populationMax Int @map("pop_max")
|
||||
agriculture Int @map("agri")
|
||||
agricultureMax Int @map("agri_max")
|
||||
commerce Int @map("comm")
|
||||
commerceMax Int @map("comm_max")
|
||||
security Int @map("secu")
|
||||
securityMax Int @map("secu_max")
|
||||
trust Int @default(0)
|
||||
trade Int @default(100)
|
||||
defence Int @map("def")
|
||||
defenceMax Int @map("def_max")
|
||||
wall Int @map("wall")
|
||||
wallMax Int @map("wall_max")
|
||||
region Int
|
||||
conflict Json @default(dbgenerated("'{}'::jsonb"))
|
||||
meta Json @default(dbgenerated("'{}'::jsonb"))
|
||||
|
||||
@@map("city")
|
||||
@@map("city")
|
||||
}
|
||||
|
||||
model General {
|
||||
id Int @id
|
||||
userId String? @map("user_id")
|
||||
name String
|
||||
nationId Int @default(0) @map("nation_id")
|
||||
cityId Int @default(0) @map("city_id")
|
||||
troopId Int @default(0) @map("troop_id")
|
||||
npcState Int @default(0) @map("npc_state")
|
||||
affinity Int?
|
||||
bornYear Int @default(180) @map("born_year")
|
||||
deadYear Int @default(300) @map("dead_year")
|
||||
picture String?
|
||||
imageServer Int @default(0) @map("image_server")
|
||||
leadership Int @default(50)
|
||||
strength Int @default(50)
|
||||
intel Int @default(50)
|
||||
injury Int @default(0)
|
||||
experience Int @default(0)
|
||||
dedication Int @default(0)
|
||||
officerLevel Int @default(0) @map("officer_level")
|
||||
gold Int @default(1000)
|
||||
rice Int @default(1000)
|
||||
crew Int @default(0)
|
||||
crewTypeId Int @default(0) @map("crew_type_id")
|
||||
train Int @default(0)
|
||||
atmos Int @default(0)
|
||||
weaponCode String @default("None") @map("weapon_code")
|
||||
bookCode String @default("None") @map("book_code")
|
||||
horseCode String @default("None") @map("horse_code")
|
||||
itemCode String @default("None") @map("item_code")
|
||||
turnTime DateTime @map("turn_time")
|
||||
recentWarTime DateTime? @map("recent_war_time")
|
||||
age Int @default(20)
|
||||
startAge Int @default(20) @map("start_age")
|
||||
personalCode String @default("None") @map("personal_code")
|
||||
specialCode String @default("None") @map("special_code")
|
||||
special2Code String @default("None") @map("special2_code")
|
||||
lastTurn Json @default(dbgenerated("'{}'::jsonb")) @map("last_turn")
|
||||
meta Json @default(dbgenerated("'{}'::jsonb"))
|
||||
penalty Json @default(dbgenerated("'{}'::jsonb"))
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @map("updated_at")
|
||||
id Int @id
|
||||
userId String? @map("user_id")
|
||||
name String
|
||||
nationId Int @default(0) @map("nation_id")
|
||||
cityId Int @default(0) @map("city_id")
|
||||
troopId Int @default(0) @map("troop_id")
|
||||
npcState Int @default(0) @map("npc_state")
|
||||
affinity Int?
|
||||
bornYear Int @default(180) @map("born_year")
|
||||
deadYear Int @default(300) @map("dead_year")
|
||||
picture String?
|
||||
imageServer Int @default(0) @map("image_server")
|
||||
leadership Int @default(50)
|
||||
strength Int @default(50)
|
||||
intel Int @default(50)
|
||||
injury Int @default(0)
|
||||
experience Int @default(0)
|
||||
dedication Int @default(0)
|
||||
officerLevel Int @default(0) @map("officer_level")
|
||||
gold Int @default(1000)
|
||||
rice Int @default(1000)
|
||||
crew Int @default(0)
|
||||
crewTypeId Int @default(0) @map("crew_type_id")
|
||||
train Int @default(0)
|
||||
atmos Int @default(0)
|
||||
weaponCode String @default("None") @map("weapon_code")
|
||||
bookCode String @default("None") @map("book_code")
|
||||
horseCode String @default("None") @map("horse_code")
|
||||
itemCode String @default("None") @map("item_code")
|
||||
turnTime DateTime @map("turn_time")
|
||||
recentWarTime DateTime? @map("recent_war_time")
|
||||
age Int @default(20)
|
||||
startAge Int @default(20) @map("start_age")
|
||||
personalCode String @default("None") @map("personal_code")
|
||||
specialCode String @default("None") @map("special_code")
|
||||
special2Code String @default("None") @map("special2_code")
|
||||
lastTurn Json @default(dbgenerated("'{}'::jsonb")) @map("last_turn")
|
||||
meta Json @default(dbgenerated("'{}'::jsonb"))
|
||||
penalty Json @default(dbgenerated("'{}'::jsonb"))
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @map("updated_at")
|
||||
|
||||
@@map("general")
|
||||
@@map("general")
|
||||
}
|
||||
|
||||
model GeneralAccessLog {
|
||||
id Int @id @default(autoincrement())
|
||||
generalId Int @unique @map("general_id")
|
||||
userId String? @map("user_id")
|
||||
lastRefresh DateTime? @map("last_refresh")
|
||||
refresh Int @default(0)
|
||||
refreshTotal Int @default(0) @map("refresh_total")
|
||||
refreshScore Int @default(0) @map("refresh_score")
|
||||
refreshScoreTotal Int @default(0) @map("refresh_score_total")
|
||||
id Int @id @default(autoincrement())
|
||||
generalId Int @unique @map("general_id")
|
||||
userId String? @map("user_id")
|
||||
lastRefresh DateTime? @map("last_refresh")
|
||||
refresh Int @default(0)
|
||||
refreshTotal Int @default(0) @map("refresh_total")
|
||||
refreshScore Int @default(0) @map("refresh_score")
|
||||
refreshScoreTotal Int @default(0) @map("refresh_score_total")
|
||||
|
||||
@@index([userId])
|
||||
@@map("general_access_log")
|
||||
@@index([userId])
|
||||
@@map("general_access_log")
|
||||
}
|
||||
|
||||
model MessageReadState {
|
||||
generalId Int @id @map("general_id")
|
||||
latestPrivateMessage Int @default(0) @map("latest_private_message")
|
||||
latestDiplomacyMessage Int @default(0) @map("latest_diplomacy_message")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@map("message_read_state")
|
||||
}
|
||||
|
||||
model Message {
|
||||
id Int @id @default(autoincrement())
|
||||
mailbox Int
|
||||
type String
|
||||
src Int
|
||||
dest Int
|
||||
time DateTime
|
||||
validUntil DateTime @map("valid_until")
|
||||
message Json
|
||||
|
||||
@@map("message")
|
||||
}
|
||||
|
||||
model RankData {
|
||||
id Int @id @default(autoincrement())
|
||||
nationId Int @default(0) @map("nation_id")
|
||||
generalId Int @map("general_id")
|
||||
type String @db.VarChar(20)
|
||||
value Int @default(0)
|
||||
id Int @id @default(autoincrement())
|
||||
nationId Int @default(0) @map("nation_id")
|
||||
generalId Int @map("general_id")
|
||||
type String @db.VarChar(20)
|
||||
value Int @default(0)
|
||||
|
||||
@@unique([generalId, type])
|
||||
@@index([type, value], name: "by_type")
|
||||
@@index([nationId, type, value], name: "by_nation")
|
||||
@@map("rank_data")
|
||||
@@unique([generalId, type])
|
||||
@@index([type, value], name: "by_type")
|
||||
@@index([nationId, type, value], name: "by_nation")
|
||||
@@map("rank_data")
|
||||
}
|
||||
|
||||
model HallOfFame {
|
||||
id Int @id @default(autoincrement())
|
||||
serverId String @map("server_id")
|
||||
season Int
|
||||
scenario Int
|
||||
generalNo Int @map("general_no")
|
||||
type String @db.VarChar(20)
|
||||
value Float
|
||||
owner String? @map("owner")
|
||||
aux Json @default(dbgenerated("'{}'::jsonb"))
|
||||
id Int @id @default(autoincrement())
|
||||
serverId String @map("server_id")
|
||||
season Int
|
||||
scenario Int
|
||||
generalNo Int @map("general_no")
|
||||
type String @db.VarChar(20)
|
||||
value Float
|
||||
owner String? @map("owner")
|
||||
aux Json @default(dbgenerated("'{}'::jsonb"))
|
||||
|
||||
@@unique([serverId, type, generalNo])
|
||||
@@unique([owner, serverId, type], name: "hall_owner")
|
||||
@@index([serverId, type, value], name: "server_show")
|
||||
@@index([season, scenario, type, value], name: "scenario")
|
||||
@@map("hall")
|
||||
@@unique([serverId, type, generalNo])
|
||||
@@unique([owner, serverId, type], name: "hall_owner")
|
||||
@@index([serverId, type, value], name: "server_show")
|
||||
@@index([season, scenario, type, value], name: "scenario")
|
||||
@@map("hall")
|
||||
}
|
||||
|
||||
model GameHistory {
|
||||
id Int @id @default(autoincrement())
|
||||
serverId String @map("server_id")
|
||||
date DateTime
|
||||
winnerNation Int? @map("winner_nation")
|
||||
map String? @map("map")
|
||||
season Int
|
||||
scenario Int
|
||||
scenarioName String @map("scenario_name")
|
||||
env Json @default(dbgenerated("'{}'::jsonb"))
|
||||
id Int @id @default(autoincrement())
|
||||
serverId String @map("server_id")
|
||||
date DateTime
|
||||
winnerNation Int? @map("winner_nation")
|
||||
map String? @map("map")
|
||||
season Int
|
||||
scenario Int
|
||||
scenarioName String @map("scenario_name")
|
||||
env Json @default(dbgenerated("'{}'::jsonb"))
|
||||
|
||||
@@unique([serverId])
|
||||
@@index([date])
|
||||
@@map("ng_games")
|
||||
@@unique([serverId])
|
||||
@@index([date])
|
||||
@@map("ng_games")
|
||||
}
|
||||
|
||||
model OldNation {
|
||||
id Int @id @default(autoincrement())
|
||||
serverId String @map("server_id")
|
||||
nation Int @default(0)
|
||||
data Json @default(dbgenerated("'{}'::jsonb"))
|
||||
date DateTime @default(now())
|
||||
id Int @id @default(autoincrement())
|
||||
serverId String @map("server_id")
|
||||
nation Int @default(0)
|
||||
data Json @default(dbgenerated("'{}'::jsonb"))
|
||||
date DateTime @default(now())
|
||||
|
||||
@@unique([serverId, nation])
|
||||
@@index([serverId, nation], name: "by_server")
|
||||
@@map("ng_old_nations")
|
||||
@@unique([serverId, nation])
|
||||
@@index([serverId, nation], name: "by_server")
|
||||
@@map("ng_old_nations")
|
||||
}
|
||||
|
||||
model OldGeneral {
|
||||
id Int @id @default(autoincrement())
|
||||
serverId String @map("server_id")
|
||||
generalNo Int @map("general_no")
|
||||
owner String? @map("owner")
|
||||
name String
|
||||
lastYearMonth Int @map("last_yearmonth")
|
||||
turnTime DateTime @map("turntime")
|
||||
data Json @default(dbgenerated("'{}'::jsonb"))
|
||||
id Int @id @default(autoincrement())
|
||||
serverId String @map("server_id")
|
||||
generalNo Int @map("general_no")
|
||||
owner String? @map("owner")
|
||||
name String
|
||||
lastYearMonth Int @map("last_yearmonth")
|
||||
turnTime DateTime @map("turntime")
|
||||
data Json @default(dbgenerated("'{}'::jsonb"))
|
||||
|
||||
@@unique([serverId, generalNo], name: "by_no")
|
||||
@@index([serverId, name], name: "by_name")
|
||||
@@index([owner, serverId], name: "owner")
|
||||
@@map("ng_old_generals")
|
||||
@@unique([serverId, generalNo], name: "by_no")
|
||||
@@index([serverId, name], name: "by_name")
|
||||
@@index([owner, serverId], name: "owner")
|
||||
@@map("ng_old_generals")
|
||||
}
|
||||
|
||||
model Emperor {
|
||||
id Int @id @default(autoincrement()) @map("no")
|
||||
serverId String? @map("server_id")
|
||||
phase String? @default("")
|
||||
nationCount String? @map("nation_count")
|
||||
nationName String? @map("nation_name")
|
||||
nationHist String? @map("nation_hist")
|
||||
genCount String? @map("gen_count")
|
||||
personalHist String? @map("personal_hist")
|
||||
specialHist String? @map("special_hist")
|
||||
name String? @default("")
|
||||
type String? @default("")
|
||||
color String? @default("")
|
||||
year Int? @default(0)
|
||||
month Int? @default(0)
|
||||
power Int? @default(0)
|
||||
gennum Int? @default(0)
|
||||
citynum Int? @default(0)
|
||||
pop String? @default("0")
|
||||
poprate String? @default("")
|
||||
gold Int? @default(0)
|
||||
rice Int? @default(0)
|
||||
l12name String? @default("")
|
||||
l12pic String? @default("")
|
||||
l11name String? @default("")
|
||||
l11pic String? @default("")
|
||||
l10name String? @default("")
|
||||
l10pic String? @default("")
|
||||
l9name String? @default("")
|
||||
l9pic String? @default("")
|
||||
l8name String? @default("")
|
||||
l8pic String? @default("")
|
||||
l7name String? @default("")
|
||||
l7pic String? @default("")
|
||||
l6name String? @default("")
|
||||
l6pic String? @default("")
|
||||
l5name String? @default("")
|
||||
l5pic String? @default("")
|
||||
tiger String? @default("")
|
||||
eagle String? @default("")
|
||||
gen String? @default("")
|
||||
history Json @default(dbgenerated("'{}'::jsonb"))
|
||||
aux Json @default(dbgenerated("'{}'::jsonb"))
|
||||
id Int @id @default(autoincrement()) @map("no")
|
||||
serverId String? @map("server_id")
|
||||
phase String? @default("")
|
||||
nationCount String? @map("nation_count")
|
||||
nationName String? @map("nation_name")
|
||||
nationHist String? @map("nation_hist")
|
||||
genCount String? @map("gen_count")
|
||||
personalHist String? @map("personal_hist")
|
||||
specialHist String? @map("special_hist")
|
||||
name String? @default("")
|
||||
type String? @default("")
|
||||
color String? @default("")
|
||||
year Int? @default(0)
|
||||
month Int? @default(0)
|
||||
power Int? @default(0)
|
||||
gennum Int? @default(0)
|
||||
citynum Int? @default(0)
|
||||
pop String? @default("0")
|
||||
poprate String? @default("")
|
||||
gold Int? @default(0)
|
||||
rice Int? @default(0)
|
||||
l12name String? @default("")
|
||||
l12pic String? @default("")
|
||||
l11name String? @default("")
|
||||
l11pic String? @default("")
|
||||
l10name String? @default("")
|
||||
l10pic String? @default("")
|
||||
l9name String? @default("")
|
||||
l9pic String? @default("")
|
||||
l8name String? @default("")
|
||||
l8pic String? @default("")
|
||||
l7name String? @default("")
|
||||
l7pic String? @default("")
|
||||
l6name String? @default("")
|
||||
l6pic String? @default("")
|
||||
l5name String? @default("")
|
||||
l5pic String? @default("")
|
||||
tiger String? @default("")
|
||||
eagle String? @default("")
|
||||
gen String? @default("")
|
||||
history Json @default(dbgenerated("'{}'::jsonb"))
|
||||
aux Json @default(dbgenerated("'{}'::jsonb"))
|
||||
|
||||
@@map("emperior")
|
||||
@@map("emperior")
|
||||
}
|
||||
|
||||
model Troop {
|
||||
troopLeaderId Int @id @map("troop_leader")
|
||||
nationId Int @map("nation")
|
||||
name String
|
||||
troopLeaderId Int @id @map("troop_leader")
|
||||
nationId Int @map("nation")
|
||||
name String
|
||||
|
||||
@@map("troop")
|
||||
@@map("troop")
|
||||
}
|
||||
|
||||
model GeneralTurn {
|
||||
id Int @id @default(autoincrement())
|
||||
generalId Int @map("general_id")
|
||||
turnIdx Int @map("turn_idx")
|
||||
actionCode String @map("action_code")
|
||||
arg Json @default(dbgenerated("'{}'::jsonb"))
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
id Int @id @default(autoincrement())
|
||||
generalId Int @map("general_id")
|
||||
turnIdx Int @map("turn_idx")
|
||||
actionCode String @map("action_code")
|
||||
arg Json @default(dbgenerated("'{}'::jsonb"))
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@unique([generalId, turnIdx])
|
||||
@@map("general_turn")
|
||||
@@unique([generalId, turnIdx])
|
||||
@@map("general_turn")
|
||||
}
|
||||
|
||||
model NationTurn {
|
||||
id Int @id @default(autoincrement())
|
||||
nationId Int @map("nation_id")
|
||||
officerLevel Int @map("officer_level")
|
||||
turnIdx Int @map("turn_idx")
|
||||
actionCode String @map("action_code")
|
||||
arg Json @default(dbgenerated("'{}'::jsonb"))
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
id Int @id @default(autoincrement())
|
||||
nationId Int @map("nation_id")
|
||||
officerLevel Int @map("officer_level")
|
||||
turnIdx Int @map("turn_idx")
|
||||
actionCode String @map("action_code")
|
||||
arg Json @default(dbgenerated("'{}'::jsonb"))
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@unique([nationId, officerLevel, turnIdx])
|
||||
@@map("nation_turn")
|
||||
@@unique([nationId, officerLevel, turnIdx])
|
||||
@@map("nation_turn")
|
||||
}
|
||||
|
||||
model Diplomacy {
|
||||
id Int @id @default(autoincrement())
|
||||
srcNationId Int @map("src_nation_id")
|
||||
destNationId Int @map("dest_nation_id")
|
||||
stateCode Int @map("state_code")
|
||||
term Int @default(0)
|
||||
isDead Boolean @default(false) @map("is_dead")
|
||||
isShowing Boolean @default(true) @map("is_showing")
|
||||
meta Json @default(dbgenerated("'{}'::jsonb"))
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
id Int @id @default(autoincrement())
|
||||
srcNationId Int @map("src_nation_id")
|
||||
destNationId Int @map("dest_nation_id")
|
||||
stateCode Int @map("state_code")
|
||||
term Int @default(0)
|
||||
isDead Boolean @default(false) @map("is_dead")
|
||||
isShowing Boolean @default(true) @map("is_showing")
|
||||
meta Json @default(dbgenerated("'{}'::jsonb"))
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@unique([srcNationId, destNationId])
|
||||
@@map("diplomacy")
|
||||
@@unique([srcNationId, destNationId])
|
||||
@@map("diplomacy")
|
||||
}
|
||||
|
||||
model DiplomacyLetter {
|
||||
id Int @id @default(autoincrement())
|
||||
srcNationId Int @map("src_nation_id")
|
||||
destNationId Int @map("dest_nation_id")
|
||||
prevId Int? @map("prev_id")
|
||||
state DiplomacyLetterState @default(PROPOSED)
|
||||
textBrief String @map("text_brief")
|
||||
textDetail String @map("text_detail")
|
||||
date DateTime @default(now()) @map("date")
|
||||
srcSignerId Int @map("src_signer")
|
||||
destSignerId Int? @map("dest_signer")
|
||||
aux Json @default(dbgenerated("'{}'::jsonb"))
|
||||
id Int @id @default(autoincrement())
|
||||
srcNationId Int @map("src_nation_id")
|
||||
destNationId Int @map("dest_nation_id")
|
||||
prevId Int? @map("prev_id")
|
||||
state DiplomacyLetterState @default(PROPOSED)
|
||||
textBrief String @map("text_brief")
|
||||
textDetail String @map("text_detail")
|
||||
date DateTime @default(now()) @map("date")
|
||||
srcSignerId Int @map("src_signer")
|
||||
destSignerId Int? @map("dest_signer")
|
||||
aux Json @default(dbgenerated("'{}'::jsonb"))
|
||||
|
||||
@@index([srcNationId, destNationId])
|
||||
@@index([destNationId, srcNationId])
|
||||
@@index([state, date])
|
||||
@@map("diplomacy_letter")
|
||||
@@index([srcNationId, destNationId])
|
||||
@@index([destNationId, srcNationId])
|
||||
@@index([state, date])
|
||||
@@map("diplomacy_letter")
|
||||
}
|
||||
|
||||
model YearbookHistory {
|
||||
id Int @id @default(autoincrement())
|
||||
profileName String @map("profile_name")
|
||||
year Int
|
||||
month Int
|
||||
map Json
|
||||
nations Json
|
||||
hash String @default("")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
id Int @id @default(autoincrement())
|
||||
profileName String @map("profile_name")
|
||||
year Int
|
||||
month Int
|
||||
map Json
|
||||
nations Json
|
||||
hash String @default("")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@unique([profileName, year, month])
|
||||
@@index([profileName, year, month])
|
||||
@@map("yearbook_history")
|
||||
@@unique([profileName, year, month])
|
||||
@@index([profileName, year, month])
|
||||
@@map("yearbook_history")
|
||||
}
|
||||
|
||||
model Event {
|
||||
id Int @id @default(autoincrement())
|
||||
targetCode String @map("target_code")
|
||||
priority Int @default(0)
|
||||
condition Json @default(dbgenerated("'{}'::jsonb"))
|
||||
action Json @default(dbgenerated("'{}'::jsonb"))
|
||||
meta Json @default(dbgenerated("'{}'::jsonb"))
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
id Int @id @default(autoincrement())
|
||||
targetCode String @map("target_code")
|
||||
priority Int @default(0)
|
||||
condition Json @default(dbgenerated("'{}'::jsonb"))
|
||||
action Json @default(dbgenerated("'{}'::jsonb"))
|
||||
meta Json @default(dbgenerated("'{}'::jsonb"))
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@map("event")
|
||||
@@map("event")
|
||||
}
|
||||
|
||||
model LogEntry {
|
||||
id Int @id @default(autoincrement())
|
||||
scope LogScope
|
||||
category LogCategory
|
||||
subType String? @map("sub_type")
|
||||
year Int
|
||||
month Int
|
||||
text String
|
||||
generalId Int? @map("general_id")
|
||||
nationId Int? @map("nation_id")
|
||||
userId Int? @map("user_id")
|
||||
meta Json @default(dbgenerated("'{}'::jsonb"))
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
id Int @id @default(autoincrement())
|
||||
scope LogScope
|
||||
category LogCategory
|
||||
subType String? @map("sub_type")
|
||||
year Int
|
||||
month Int
|
||||
text String
|
||||
generalId Int? @map("general_id")
|
||||
nationId Int? @map("nation_id")
|
||||
userId Int? @map("user_id")
|
||||
meta Json @default(dbgenerated("'{}'::jsonb"))
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@index([scope, category, id])
|
||||
@@index([generalId, category, id])
|
||||
@@index([nationId, category, id])
|
||||
@@index([userId, category, id])
|
||||
@@map("log_entry")
|
||||
@@index([scope, category, id])
|
||||
@@index([generalId, category, id])
|
||||
@@index([nationId, category, id])
|
||||
@@index([userId, category, id])
|
||||
@@map("log_entry")
|
||||
}
|
||||
|
||||
model ErrorLog {
|
||||
id Int @id @default(autoincrement())
|
||||
category String
|
||||
source String? @map("source")
|
||||
message String
|
||||
trace String?
|
||||
context Json @default(dbgenerated("'{}'::jsonb"))
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
id Int @id @default(autoincrement())
|
||||
category String
|
||||
source String? @map("source")
|
||||
message String
|
||||
trace String?
|
||||
context Json @default(dbgenerated("'{}'::jsonb"))
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@index([category, id])
|
||||
@@map("error_log")
|
||||
@@index([category, id])
|
||||
@@map("error_log")
|
||||
}
|
||||
|
||||
model InheritancePoint {
|
||||
id Int @id @default(autoincrement())
|
||||
userId String @map("user_id")
|
||||
key String
|
||||
value Float @default(0)
|
||||
aux Json @default(dbgenerated("'{}'::jsonb"))
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
id Int @id @default(autoincrement())
|
||||
userId String @map("user_id")
|
||||
key String
|
||||
value Float @default(0)
|
||||
aux Json @default(dbgenerated("'{}'::jsonb"))
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@unique([userId, key])
|
||||
@@index([userId])
|
||||
@@map("inheritance_point")
|
||||
@@unique([userId, key])
|
||||
@@index([userId])
|
||||
@@map("inheritance_point")
|
||||
}
|
||||
|
||||
model InheritanceLog {
|
||||
id Int @id @default(autoincrement())
|
||||
userId String @map("user_id")
|
||||
year Int
|
||||
month Int
|
||||
text String
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
id Int @id @default(autoincrement())
|
||||
userId String @map("user_id")
|
||||
year Int
|
||||
month Int
|
||||
text String
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@index([userId, id])
|
||||
@@map("inheritance_log")
|
||||
@@index([userId, id])
|
||||
@@map("inheritance_log")
|
||||
}
|
||||
|
||||
model InheritanceResult {
|
||||
id Int @id @default(autoincrement())
|
||||
serverId String @map("server_id")
|
||||
owner String @map("owner")
|
||||
generalId Int @map("general_id")
|
||||
year Int
|
||||
month Int
|
||||
value Json @default(dbgenerated("'{}'::jsonb"))
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
id Int @id @default(autoincrement())
|
||||
serverId String @map("server_id")
|
||||
owner String @map("owner")
|
||||
generalId Int @map("general_id")
|
||||
year Int
|
||||
month Int
|
||||
value Json @default(dbgenerated("'{}'::jsonb"))
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@index([serverId, owner])
|
||||
@@map("inheritance_result")
|
||||
@@index([serverId, owner])
|
||||
@@map("inheritance_result")
|
||||
}
|
||||
|
||||
model Auction {
|
||||
id Int @id @default(autoincrement())
|
||||
type AuctionType
|
||||
targetCode String? @map("target_code")
|
||||
hostGeneralId Int @map("host_general_id")
|
||||
hostName String? @map("host_name")
|
||||
detail Json @default(dbgenerated("'{}'::jsonb"))
|
||||
status AuctionStatus @default(OPEN)
|
||||
closeAt DateTime @map("close_at")
|
||||
latestEventId String @default("") @map("latest_event_id")
|
||||
latestEventAt DateTime @default(now()) @map("latest_event_at")
|
||||
finalizingAt DateTime? @map("finalizing_at")
|
||||
finishedAt DateTime? @map("finished_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
id Int @id @default(autoincrement())
|
||||
type AuctionType
|
||||
targetCode String? @map("target_code")
|
||||
hostGeneralId Int @map("host_general_id")
|
||||
hostName String? @map("host_name")
|
||||
detail Json @default(dbgenerated("'{}'::jsonb"))
|
||||
status AuctionStatus @default(OPEN)
|
||||
closeAt DateTime @map("close_at")
|
||||
latestEventId String @default("") @map("latest_event_id")
|
||||
latestEventAt DateTime @default(now()) @map("latest_event_at")
|
||||
finalizingAt DateTime? @map("finalizing_at")
|
||||
finishedAt DateTime? @map("finished_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
bids AuctionBid[]
|
||||
bids AuctionBid[]
|
||||
|
||||
@@index([status, closeAt])
|
||||
@@map("auction")
|
||||
@@index([status, closeAt])
|
||||
@@map("auction")
|
||||
}
|
||||
|
||||
model AuctionBid {
|
||||
id Int @id @default(autoincrement())
|
||||
auctionId Int @map("auction_id")
|
||||
generalId Int @map("general_id")
|
||||
amount Int
|
||||
eventId String @map("event_id")
|
||||
eventAt DateTime @map("event_at")
|
||||
meta Json @default(dbgenerated("'{}'::jsonb"))
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
id Int @id @default(autoincrement())
|
||||
auctionId Int @map("auction_id")
|
||||
generalId Int @map("general_id")
|
||||
amount Int
|
||||
eventId String @map("event_id")
|
||||
eventAt DateTime @map("event_at")
|
||||
meta Json @default(dbgenerated("'{}'::jsonb"))
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
auction Auction @relation(fields: [auctionId], references: [id], onDelete: Cascade)
|
||||
auction Auction @relation(fields: [auctionId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([auctionId, amount])
|
||||
@@index([auctionId, eventAt])
|
||||
@@map("auction_bid")
|
||||
@@index([auctionId, amount])
|
||||
@@index([auctionId, eventAt])
|
||||
@@map("auction_bid")
|
||||
}
|
||||
|
||||
model InheritanceUserState {
|
||||
userId String @id @map("user_id")
|
||||
meta Json @default(dbgenerated("'{}'::jsonb"))
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
userId String @id @map("user_id")
|
||||
meta Json @default(dbgenerated("'{}'::jsonb"))
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@map("inheritance_user_state")
|
||||
@@map("inheritance_user_state")
|
||||
}
|
||||
|
||||
model BoardPost {
|
||||
id Int @id @default(autoincrement())
|
||||
nationId Int @map("nation_id")
|
||||
isSecret Boolean @default(false) @map("is_secret")
|
||||
authorGeneralId Int @map("author_general_id")
|
||||
authorName String @map("author_name")
|
||||
title String
|
||||
contentHtml String @map("content_html")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
id Int @id @default(autoincrement())
|
||||
nationId Int @map("nation_id")
|
||||
isSecret Boolean @default(false) @map("is_secret")
|
||||
authorGeneralId Int @map("author_general_id")
|
||||
authorName String @map("author_name")
|
||||
title String
|
||||
contentHtml String @map("content_html")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
comments BoardComment[]
|
||||
comments BoardComment[]
|
||||
|
||||
@@index([nationId, isSecret, createdAt])
|
||||
@@map("board_post")
|
||||
@@index([nationId, isSecret, createdAt])
|
||||
@@map("board_post")
|
||||
}
|
||||
|
||||
model BoardComment {
|
||||
id Int @id @default(autoincrement())
|
||||
postId Int @map("post_id")
|
||||
nationId Int @map("nation_id")
|
||||
isSecret Boolean @default(false) @map("is_secret")
|
||||
authorGeneralId Int @map("author_general_id")
|
||||
authorName String @map("author_name")
|
||||
contentText String @map("content_text")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
id Int @id @default(autoincrement())
|
||||
postId Int @map("post_id")
|
||||
nationId Int @map("nation_id")
|
||||
isSecret Boolean @default(false) @map("is_secret")
|
||||
authorGeneralId Int @map("author_general_id")
|
||||
authorName String @map("author_name")
|
||||
contentText String @map("content_text")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
post BoardPost @relation(fields: [postId], references: [id], onDelete: Cascade)
|
||||
post BoardPost @relation(fields: [postId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([postId, createdAt])
|
||||
@@map("board_comment")
|
||||
@@index([postId, createdAt])
|
||||
@@map("board_comment")
|
||||
}
|
||||
|
||||
model VotePoll {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
body String @default("")
|
||||
options Json
|
||||
multipleOptions Int @default(1) @map("multiple_options")
|
||||
revealMode String @map("reveal_mode")
|
||||
openerGeneralId Int @map("opener_general_id")
|
||||
openerName String @map("opener_name")
|
||||
startAt DateTime @default(now()) @map("start_at")
|
||||
endAt DateTime? @map("end_at")
|
||||
closedAt DateTime? @map("closed_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
body String @default("")
|
||||
options Json
|
||||
multipleOptions Int @default(1) @map("multiple_options")
|
||||
revealMode String @map("reveal_mode")
|
||||
openerGeneralId Int @map("opener_general_id")
|
||||
openerName String @map("opener_name")
|
||||
startAt DateTime @default(now()) @map("start_at")
|
||||
endAt DateTime? @map("end_at")
|
||||
closedAt DateTime? @map("closed_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
votes Vote[]
|
||||
comments VoteComment[]
|
||||
votes Vote[]
|
||||
comments VoteComment[]
|
||||
|
||||
@@map("vote_poll")
|
||||
@@map("vote_poll")
|
||||
}
|
||||
|
||||
model Vote {
|
||||
id Int @id @default(autoincrement())
|
||||
voteId Int @map("vote_id")
|
||||
generalId Int @map("general_id")
|
||||
nationId Int @map("nation_id")
|
||||
selection Json
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
id Int @id @default(autoincrement())
|
||||
voteId Int @map("vote_id")
|
||||
generalId Int @map("general_id")
|
||||
nationId Int @map("nation_id")
|
||||
selection Json
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
poll VotePoll @relation(fields: [voteId], references: [id], onDelete: Cascade)
|
||||
poll VotePoll @relation(fields: [voteId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([voteId, generalId])
|
||||
@@index([voteId])
|
||||
@@map("vote")
|
||||
@@unique([voteId, generalId])
|
||||
@@index([voteId])
|
||||
@@map("vote")
|
||||
}
|
||||
|
||||
model VoteComment {
|
||||
id Int @id @default(autoincrement())
|
||||
voteId Int @map("vote_id")
|
||||
generalId Int @map("general_id")
|
||||
nationId Int @map("nation_id")
|
||||
generalName String @map("general_name")
|
||||
nationName String @map("nation_name")
|
||||
text String
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
id Int @id @default(autoincrement())
|
||||
voteId Int @map("vote_id")
|
||||
generalId Int @map("general_id")
|
||||
nationId Int @map("nation_id")
|
||||
generalName String @map("general_name")
|
||||
nationName String @map("nation_name")
|
||||
text String
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
poll VotePoll @relation(fields: [voteId], references: [id], onDelete: Cascade)
|
||||
poll VotePoll @relation(fields: [voteId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([voteId, createdAt])
|
||||
@@map("vote_comment")
|
||||
@@index([voteId, createdAt])
|
||||
@@map("vote_comment")
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE "app_user"
|
||||
ADD COLUMN "picture" TEXT NOT NULL DEFAULT 'default.jpg',
|
||||
ADD COLUMN "image_server" INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN "icon_updated_at" TIMESTAMP(3),
|
||||
ADD COLUMN "third_party_use" BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
ADD COLUMN "delete_after" TIMESTAMP(3);
|
||||
@@ -1,84 +1,89 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
output = "./generated/gateway"
|
||||
engineType = "binary"
|
||||
provider = "prisma-client-js"
|
||||
output = "./generated/gateway"
|
||||
engineType = "binary"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
enum OAuthType {
|
||||
NONE
|
||||
KAKAO
|
||||
NONE
|
||||
KAKAO
|
||||
}
|
||||
|
||||
enum GatewayProfileStatus {
|
||||
RESERVED
|
||||
PREOPEN
|
||||
RUNNING
|
||||
PAUSED
|
||||
COMPLETED
|
||||
STOPPED
|
||||
DISABLED
|
||||
RESERVED
|
||||
PREOPEN
|
||||
RUNNING
|
||||
PAUSED
|
||||
COMPLETED
|
||||
STOPPED
|
||||
DISABLED
|
||||
}
|
||||
|
||||
enum GatewayBuildStatus {
|
||||
IDLE
|
||||
QUEUED
|
||||
RUNNING
|
||||
FAILED
|
||||
SUCCEEDED
|
||||
IDLE
|
||||
QUEUED
|
||||
RUNNING
|
||||
FAILED
|
||||
SUCCEEDED
|
||||
}
|
||||
|
||||
model AppUser {
|
||||
id String @id @default(uuid())
|
||||
loginId String @unique @map("login_id")
|
||||
displayName String @map("display_name")
|
||||
passwordHash String @map("password_hash")
|
||||
passwordSalt String @map("password_salt")
|
||||
roles Json @default(dbgenerated("'[]'::jsonb"))
|
||||
sanctions Json @default(dbgenerated("'{}'::jsonb"))
|
||||
oauthType OAuthType @default(NONE) @map("oauth_type")
|
||||
oauthId String? @unique @map("oauth_id")
|
||||
email String? @unique
|
||||
oauthInfo Json @default(dbgenerated("'{}'::jsonb")) @map("oauth_info")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
lastLoginAt DateTime? @map("last_login_at")
|
||||
id String @id @default(uuid())
|
||||
loginId String @unique @map("login_id")
|
||||
displayName String @map("display_name")
|
||||
passwordHash String @map("password_hash")
|
||||
passwordSalt String @map("password_salt")
|
||||
roles Json @default(dbgenerated("'[]'::jsonb"))
|
||||
sanctions Json @default(dbgenerated("'{}'::jsonb"))
|
||||
oauthType OAuthType @default(NONE) @map("oauth_type")
|
||||
oauthId String? @unique @map("oauth_id")
|
||||
email String? @unique
|
||||
oauthInfo Json @default(dbgenerated("'{}'::jsonb")) @map("oauth_info")
|
||||
picture String @default("default.jpg")
|
||||
imageServer Int @default(0) @map("image_server")
|
||||
iconUpdatedAt DateTime? @map("icon_updated_at")
|
||||
thirdPartyUse Boolean @default(true) @map("third_party_use")
|
||||
deleteAfter DateTime? @map("delete_after")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
lastLoginAt DateTime? @map("last_login_at")
|
||||
|
||||
@@map("app_user")
|
||||
@@map("app_user")
|
||||
}
|
||||
|
||||
model GatewayProfile {
|
||||
profileName String @id @map("profile_name")
|
||||
profile String
|
||||
scenario String
|
||||
apiPort Int @map("api_port")
|
||||
status GatewayProfileStatus
|
||||
buildStatus GatewayBuildStatus @default(IDLE) @map("build_status")
|
||||
buildCommitSha String? @map("build_commit_sha")
|
||||
buildWorkspace String? @map("build_workspace")
|
||||
buildLastUsedAt DateTime? @map("build_last_used_at")
|
||||
preopenAt DateTime? @map("preopen_at")
|
||||
openAt DateTime? @map("open_at")
|
||||
scheduledStartAt DateTime? @map("scheduled_start_at")
|
||||
buildRequestedAt DateTime? @map("build_requested_at")
|
||||
buildStartedAt DateTime? @map("build_started_at")
|
||||
buildCompletedAt DateTime? @map("build_completed_at")
|
||||
buildError String? @map("build_error")
|
||||
lastError String? @map("last_error")
|
||||
meta Json @default(dbgenerated("'{}'::jsonb"))
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
profileName String @id @map("profile_name")
|
||||
profile String
|
||||
scenario String
|
||||
apiPort Int @map("api_port")
|
||||
status GatewayProfileStatus
|
||||
buildStatus GatewayBuildStatus @default(IDLE) @map("build_status")
|
||||
buildCommitSha String? @map("build_commit_sha")
|
||||
buildWorkspace String? @map("build_workspace")
|
||||
buildLastUsedAt DateTime? @map("build_last_used_at")
|
||||
preopenAt DateTime? @map("preopen_at")
|
||||
openAt DateTime? @map("open_at")
|
||||
scheduledStartAt DateTime? @map("scheduled_start_at")
|
||||
buildRequestedAt DateTime? @map("build_requested_at")
|
||||
buildStartedAt DateTime? @map("build_started_at")
|
||||
buildCompletedAt DateTime? @map("build_completed_at")
|
||||
buildError String? @map("build_error")
|
||||
lastError String? @map("last_error")
|
||||
meta Json @default(dbgenerated("'{}'::jsonb"))
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@unique([profile, scenario])
|
||||
@@map("gateway_profile")
|
||||
@@unique([profile, scenario])
|
||||
@@map("gateway_profile")
|
||||
}
|
||||
|
||||
model SystemSetting {
|
||||
id Int @id @default(1) @map("no")
|
||||
notice String @default("") @map("notice")
|
||||
id Int @id @default(1) @map("no")
|
||||
notice String @default("") @map("notice")
|
||||
|
||||
@@map("system")
|
||||
@@map("system")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE "message_read_state" (
|
||||
"general_id" INTEGER PRIMARY KEY,
|
||||
"latest_private_message" INTEGER NOT NULL DEFAULT 0,
|
||||
"latest_diplomacy_message" INTEGER NOT NULL DEFAULT 0,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "nation"
|
||||
ADD COLUMN "chief_general_id" INTEGER;
|
||||
@@ -7,6 +7,8 @@ export interface DatabaseClient {
|
||||
worldState: GamePrisma.WorldStateDelegate;
|
||||
general: GamePrisma.GeneralDelegate;
|
||||
generalAccessLog: GamePrisma.GeneralAccessLogDelegate;
|
||||
messageReadState: GamePrisma.MessageReadStateDelegate;
|
||||
message: GamePrisma.MessageDelegate;
|
||||
city: GamePrisma.CityDelegate;
|
||||
nation: GamePrisma.NationDelegate;
|
||||
diplomacy: GamePrisma.DiplomacyDelegate;
|
||||
|
||||
@@ -84,6 +84,7 @@ export interface TurnEngineNationRow {
|
||||
name: string;
|
||||
color: string;
|
||||
capitalCityId: number | null;
|
||||
chiefGeneralId: number | null;
|
||||
gold: number;
|
||||
rice: number;
|
||||
tech: number;
|
||||
@@ -287,6 +288,7 @@ export interface TurnEngineNationCreateManyInput {
|
||||
name: string;
|
||||
color: string;
|
||||
capitalCityId: number | null;
|
||||
chiefGeneralId: number | null;
|
||||
gold: number;
|
||||
rice: number;
|
||||
tech: number;
|
||||
|
||||
Generated
+6
@@ -233,6 +233,9 @@ importers:
|
||||
'@fastify/cors':
|
||||
specifier: ^11.2.0
|
||||
version: 11.2.0
|
||||
'@fastify/static':
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.0
|
||||
'@prisma/client':
|
||||
specifier: ^7.2.0
|
||||
version: 7.2.0(prisma@7.2.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2))(typescript@6.0.2)
|
||||
@@ -266,6 +269,9 @@ importers:
|
||||
redis:
|
||||
specifier: ^5.10.0
|
||||
version: 5.10.0
|
||||
sharp:
|
||||
specifier: ^0.34.4
|
||||
version: 0.34.5
|
||||
zod:
|
||||
specifier: ^4.3.5
|
||||
version: 4.3.5
|
||||
|
||||
@@ -295,7 +295,7 @@ describe('auction integration flow', () => {
|
||||
|
||||
const adminGatewayToken = await gatewayClient.auth.issueGameSession.mutate({
|
||||
sessionToken: adminSessionRef.value ?? '',
|
||||
profile: 'che:2',
|
||||
profile: 'che:908',
|
||||
});
|
||||
const adminAccess = await gameClient.auth.exchangeGatewayToken.mutate({
|
||||
gatewayToken: adminGatewayToken.gameToken,
|
||||
|
||||
Reference in New Issue
Block a user