Skip to content

Commit 73df4dd

Browse files
committed
feat: implement audit logging functionality
- Create AuditManager class with methods to log actions to the audit_log table - Add audit logging calls throughout bot commands: - setup.ts: Log USER_SETUP action - redeem.ts: Log CODE_REDEEMED, CODE_REDEEM_FAILED actions - codes.ts: Log VIEWED_CODES action - makepublic.ts: Log CODE_MADE_PUBLIC action - backfill.ts: Log BACKFILL_STARTED, BACKFILL_COMPLETED actions - inventory.ts: Log VIEWED_INVENTORY action - blacksmith.ts: Log BLACKSMITH_USED action - open.ts: Log CHESTS_OPENED action - Audit logs include discord_id, action name, and contextual details (JSON)
1 parent ee2c710 commit 73df4dd

9 files changed

Lines changed: 148 additions & 0 deletions

File tree

src/bot/commands/backfill.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
ChannelType,
88
} from 'discord.js';
99
import { backfillManager } from '../database/backfillManager';
10+
import { auditManager } from '../database/auditManager';
1011
import { backfillChannelHistory } from '../handlers/backfillHandler';
1112
import logger from '../utils/logger';
1213

@@ -84,6 +85,12 @@ export async function execute(interaction: ChatInputCommandInteraction) {
8485
`[BACKFILL CMD] Operation ${operationId} started for channel ${targetChannel.name}`
8586
);
8687

88+
// Log backfill start
89+
await auditManager.logAction(interaction.user.id, 'BACKFILL_STARTED', {
90+
operationId,
91+
channel: targetChannel.name,
92+
});
93+
8794
// Create progress tracker
8895
let progressMessage = '';
8996
const updateProgress = (message: string) => {
@@ -130,6 +137,14 @@ export async function execute(interaction: ChatInputCommandInteraction) {
130137
logger.info(
131138
`[BACKFILL CMD] Operation ${operationId} completed: found=${stats.codesFound}, redeemed=${stats.codesRedeemed}`
132139
);
140+
141+
// Log backfill completion
142+
await auditManager.logAction(interaction.user.id, 'BACKFILL_COMPLETED', {
143+
operationId,
144+
codesFound: stats.codesFound,
145+
codesRedeemed: stats.codesRedeemed,
146+
errors: stats.errors.length,
147+
});
133148
} catch (error) {
134149
logger.error('[BACKFILL CMD] Command error:', error);
135150

src/bot/commands/blacksmith.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
MessageFlags,
66
} from 'discord.js';
77
import { userManager } from '../database/userManager';
8+
import { auditManager } from '../database/auditManager';
89
import IdleChampionsApi from '../api/idleChampionsApi';
910

1011
enum ContractType {
@@ -153,6 +154,13 @@ export async function execute(interaction: ChatInputCommandInteraction) {
153154
instanceId,
154155
});
155156

157+
// Log action
158+
await auditManager.logAction(interaction.user.id, 'BLACKSMITH_USED', {
159+
contractType: contractName,
160+
heroId,
161+
count,
162+
});
163+
156164
// Build response embed
157165
const embed = new EmbedBuilder()
158166
.setColor(0x00ff00)

src/bot/commands/codes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
MessageFlags,
66
} from 'discord.js';
77
import { codeManager } from '../database/codeManager';
8+
import { auditManager } from '../database/auditManager';
89

910
export const data = new SlashCommandBuilder()
1011
.setName('codes')
@@ -24,6 +25,9 @@ export async function execute(interaction: ChatInputCommandInteraction) {
2425

2526
const count = interaction.options.getNumber('count', false) || 10;
2627

28+
// Log action
29+
await auditManager.logAction(interaction.user.id, 'VIEWED_CODES', { count });
30+
2731
const redeemedCodes = await codeManager.getRedeemedCodeDetails(interaction.user.id, count);
2832

2933
if (redeemedCodes.length === 0) {

src/bot/commands/inventory.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
MessageFlags,
66
} from 'discord.js';
77
import { userManager } from '../database/userManager';
8+
import { auditManager } from '../database/auditManager';
89
import IdleChampionsApi from '../api/idleChampionsApi';
910

1011
export const data = new SlashCommandBuilder()
@@ -212,6 +213,9 @@ export async function execute(interaction: ChatInputCommandInteraction) {
212213
}
213214
}
214215

216+
// Log action
217+
await auditManager.logAction(interaction.user.id, 'VIEWED_INVENTORY', {});
218+
215219
await interaction.editReply({ embeds: [embed] });
216220
} catch (error) {
217221
console.error('[INVENTORY COMMAND] Error:', error);

src/bot/commands/makepublic.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
MessageFlags,
66
} from 'discord.js';
77
import { codeManager } from '../database/codeManager';
8+
import { auditManager } from '../database/auditManager';
89

910
export const data = new SlashCommandBuilder()
1011
.setName('makepublic')
@@ -36,6 +37,9 @@ export async function execute(interaction: ChatInputCommandInteraction) {
3637
// Mark code as public
3738
await codeManager.markCodeAsPublic(code);
3839

40+
// Log action
41+
await auditManager.logAction(interaction.user.id, 'CODE_MADE_PUBLIC', { code });
42+
3943
const embed = new EmbedBuilder()
4044
.setColor(0x00ff00)
4145
.setTitle('✅ Code Shared Successfully')

src/bot/commands/open.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
MessageFlags,
66
} from 'discord.js';
77
import { userManager } from '../database/userManager';
8+
import { auditManager } from '../database/auditManager';
89
import IdleChampionsApi from '../api/idleChampionsApi';
910

1011
enum ChestType {
@@ -158,6 +159,12 @@ export async function execute(interaction: ChatInputCommandInteraction) {
158159
instanceId,
159160
});
160161

162+
// Log action
163+
await auditManager.logAction(interaction.user.id, 'CHESTS_OPENED', {
164+
chestType: chestName,
165+
count,
166+
});
167+
161168
// Build response embed
162169
const embed = new EmbedBuilder()
163170
.setColor(0x00ff00)

src/bot/commands/redeem.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from 'discord.js';
77
import { userManager } from '../database/userManager';
88
import { codeManager } from '../database/codeManager';
9+
import { auditManager } from '../database/auditManager';
910
import IdleChampionsApi from '../api/idleChampionsApi';
1011
import logger from '../utils/logger';
1112

@@ -41,6 +42,10 @@ export async function execute(interaction: ChatInputCommandInteraction) {
4142
const isRedeemed = await codeManager.isCodeRedeemed(code);
4243
if (isRedeemed) {
4344
logger.info(`[REDEEM] Code ${code} already redeemed`);
45+
await auditManager.logAction(interaction.user.id, 'CODE_REDEEM_FAILED', {
46+
code,
47+
reason: 'Already Redeemed',
48+
});
4449
const embed = new EmbedBuilder()
4550
.setColor(0xffaa00)
4651
.setTitle('⚠️ Code Already Redeemed')
@@ -54,6 +59,10 @@ export async function execute(interaction: ChatInputCommandInteraction) {
5459
const isExpired = await codeManager.isCodeExpired(code);
5560
if (isExpired) {
5661
logger.warn(`[REDEEM] Code ${code} is expired - rejecting without API call`);
62+
await auditManager.logAction(interaction.user.id, 'CODE_REDEEM_FAILED', {
63+
code,
64+
reason: 'Code Expired',
65+
});
5766
const embed = new EmbedBuilder()
5867
.setColor(0xff0000)
5968
.setTitle('❌ Code Expired')
@@ -187,6 +196,13 @@ export async function execute(interaction: ChatInputCommandInteraction) {
187196
codeResponse.lootDetail,
188197
shouldBePublic // Private by default, public if second user redeems successfully
189198
);
199+
200+
// Log successful redeem
201+
await auditManager.logAction(interaction.user.id, 'CODE_REDEEMED', {
202+
code,
203+
status: statusName,
204+
autoPublic: shouldBePublic,
205+
});
190206
} else {
191207
// For invalid/already redeemed codes - check if the code was explicitly made public
192208
// If so, switch it back to private (might be a mistake)
@@ -198,6 +214,12 @@ export async function execute(interaction: ChatInputCommandInteraction) {
198214
);
199215
await codeManager.markCodeAsPrivate(code);
200216
}
217+
218+
// Log failed redeem
219+
await auditManager.logAction(interaction.user.id, 'CODE_REDEEM_FAILED', {
220+
code,
221+
reason: statusName,
222+
});
201223
}
202224

203225
// Build response embed

src/bot/commands/setup.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
MessageFlags,
66
} from 'discord.js';
77
import { userManager } from '../database/userManager';
8+
import { auditManager } from '../database/auditManager';
89

910
export const data = new SlashCommandBuilder()
1011
.setName('setup')
@@ -35,6 +36,9 @@ export async function execute(interaction: ChatInputCommandInteraction) {
3536
});
3637
console.log('[SETUP] Credentials saved');
3738

39+
// Log action
40+
await auditManager.logAction(interaction.user.id, 'USER_SETUP', { userId });
41+
3842
const embed = new EmbedBuilder()
3943
.setColor(0x00ff00)
4044
.setTitle('✅ Credentials Saved')

src/bot/database/auditManager.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { db } from './db';
2+
3+
interface AuditLog {
4+
id: number;
5+
discord_id: string | null;
6+
action: string;
7+
details: string | null;
8+
created_at: string;
9+
}
10+
11+
class AuditManager {
12+
/**
13+
* Log an action to the audit log
14+
*/
15+
async logAction(discordId: string | null, action: string, details?: any): Promise<void> {
16+
const detailsStr = details ? JSON.stringify(details) : null;
17+
await db.run(
18+
`INSERT INTO audit_log (discord_id, action, details)
19+
VALUES (?, ?, ?)`,
20+
[discordId, action, detailsStr]
21+
);
22+
}
23+
24+
/**
25+
* Get audit log entries for a specific user
26+
*/
27+
async getUserAuditLog(discordId: string, limit: number = 50): Promise<AuditLog[]> {
28+
return db.all<AuditLog>(
29+
`SELECT id, discord_id, action, details, created_at
30+
FROM audit_log
31+
WHERE discord_id = ?
32+
ORDER BY created_at DESC
33+
LIMIT ?`,
34+
[discordId, limit]
35+
);
36+
}
37+
38+
/**
39+
* Get all audit log entries (admin function)
40+
*/
41+
async getAllAuditLog(limit: number = 100): Promise<AuditLog[]> {
42+
return db.all<AuditLog>(
43+
`SELECT id, discord_id, action, details, created_at
44+
FROM audit_log
45+
ORDER BY created_at DESC
46+
LIMIT ?`,
47+
[limit]
48+
);
49+
}
50+
51+
/**
52+
* Get audit log entries since a specific timestamp
53+
*/
54+
async getAuditLogSince(timestamp: string, limit: number = 100): Promise<AuditLog[]> {
55+
return db.all<AuditLog>(
56+
`SELECT id, discord_id, action, details, created_at
57+
FROM audit_log
58+
WHERE created_at >= ?
59+
ORDER BY created_at DESC
60+
LIMIT ?`,
61+
[timestamp, limit]
62+
);
63+
}
64+
65+
/**
66+
* Get audit log entries for a specific action
67+
*/
68+
async getAuditLogByAction(action: string, limit: number = 50): Promise<AuditLog[]> {
69+
return db.all<AuditLog>(
70+
`SELECT id, discord_id, action, details, created_at
71+
FROM audit_log
72+
WHERE action = ?
73+
ORDER BY created_at DESC
74+
LIMIT ?`,
75+
[action, limit]
76+
);
77+
}
78+
}
79+
80+
export const auditManager = new AuditManager();

0 commit comments

Comments
 (0)