Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,7 @@
"packages/core/src/compliance/plugin.ts",
"packages/core/src/engagement/chat-commands/plugin.ts",
"packages/core/src/pam/player-management/plugin.ts",
"packages/core/src/pam/player-note/plugin.ts",
"packages/core/src/wallet/plugin.ts"
]
},
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/contracts/adapters/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type DirectAuditAction =
| 'admin.user.updated'
| 'admin.player.updated'
| 'admin.player.removed'
| 'admin.player_note.created'
| 'audit.export'
| 'compliance.kyc.bulk_approve'
| 'wallet.withdrawal.auto_approved'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { call } from '@orpc/server';
import { sql } from 'drizzle-orm';
import type { AdminGuard } from '@openora/core/server';
import { createTestDb, type TestDb } from '@openora/core/testing';
import { makeAdminGuard, makeAuditWriter, testContext } from '../../../testing/mock.js';
import { migrate as migratePlayerNote } from '../migrate.js';
import { createPlayerNoteRouter } from '../router/index.js';
import { playerNote } from '../schema/index.js';
import { PlayerNoteService } from '../service/player-note.service.js';

const CTX = testContext();
const CALLER = '44444444-4444-4444-8444-444444444444';
const PLAYER_ID = '11111111-1111-4111-8111-111111111111';

let db: TestDb;

beforeAll(async () => {
db = await createTestDb([migratePlayerNote]);
});

afterAll(async () => {
await db.drop();
});

beforeEach(async () => {
await db.drizzle.db.execute(sql`TRUNCATE ${playerNote} RESTART IDENTITY`);
});

const guardAllowing = (allow: readonly string[]) =>
makeAdminGuard({ allow, caller: { userId: CALLER, ip: '127.0.0.1', userAgent: 'test' } });

function build(adminGuard: AdminGuard) {
const audit = makeAuditWriter();
return {
router: createPlayerNoteRouter(new PlayerNoteService(db.drizzle), adminGuard, audit),
audit,
};
}

describe('player note router', () => {
it('records adding an internal note in the audit log', async () => {
const { router, audit } = build(guardAllowing(['player-note:create']));

const created = await call(
router.create,
{ playerId: PLAYER_ID, content: 'Contacted player about verification.' },
{ context: CTX },
);

expect(audit.record).toHaveBeenCalledWith({
actorId: CALLER,
actorType: 'admin',
action: 'admin.player_note.created',
resourceType: 'player',
resourceId: PLAYER_ID,
after: { noteId: created.id, content: created.content },
ip: '127.0.0.1',
userAgent: 'test',
});
});

it('does not record an audit entry when permission is denied', async () => {
const { router, audit } = build(guardAllowing([]));

await expect(
call(router.create, { playerId: PLAYER_ID, content: 'Private note' }, { context: CTX }),
).rejects.toBeDefined();

expect(audit.record).not.toHaveBeenCalled();
});
});
8 changes: 7 additions & 1 deletion packages/core/src/pam/player-note/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { DRIZZLE, ADMIN_GUARD } from '@openora/core/server';
import type { CoreTokenCatalog, Plugin } from '@openora/core/server';
import { AUDIT_WRITER } from '@openora/core/contracts';
import { PlayerNoteService } from './service/player-note.service.js';
import { createPlayerNoteRouter } from './router/index.js';

export default {
id: 'player-note',
dependsOn: ['audit'],
register(ctx) {
ctx.routers.add('player-note', (c) =>
createPlayerNoteRouter(new PlayerNoteService(c.get(DRIZZLE)), c.get(ADMIN_GUARD)),
createPlayerNoteRouter(
new PlayerNoteService(c.get(DRIZZLE)),
c.get(ADMIN_GUARD),
c.get(AUDIT_WRITER),
),
);
},
} as const satisfies Plugin<CoreTokenCatalog>;
20 changes: 18 additions & 2 deletions packages/core/src/pam/player-note/router/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { implement } from '@orpc/server';
import { AdminGuard, type OssContext } from '@openora/core/server';
import type { AuditWritePort } from '@openora/core/contracts';
import { playerNoteContract } from '../contract/index.js';
import { PlayerNoteService } from '../service/player-note.service.js';

export function createPlayerNoteRouter(svc: PlayerNoteService, adminGuard: AdminGuard) {
export function createPlayerNoteRouter(
svc: PlayerNoteService,
adminGuard: AdminGuard,
audit: AuditWritePort,
) {
const os = implement(playerNoteContract).$context<OssContext>();

return os.router({
Expand All @@ -20,7 +25,18 @@ export function createPlayerNoteRouter(svc: PlayerNoteService, adminGuard: Admin

create: os.create.handler(async ({ input, context }) => {
const caller = await adminGuard.assert(context, 'player-note', 'create');
return svc.create(input, caller.userId);
const created = await svc.create(input, caller.userId);
await audit.record({
actorId: caller.userId,
actorType: 'admin',
action: 'admin.player_note.created',
resourceType: 'player',
resourceId: input.playerId,
after: { noteId: created.id, content: created.content },
ip: caller.ip,
userAgent: caller.userAgent,
});
return created;
}),
});
}
Loading