fix: address code review findings for analytics and security

This commit is contained in:
syntaxbullet
2026-01-08 21:39:01 +01:00
parent 11e07a0068
commit 6763e3c543
6 changed files with 114 additions and 26 deletions

View File

@@ -13,7 +13,13 @@ import {
bigserial,
check
} from 'drizzle-orm/pg-core';
import { relations, sql } from 'drizzle-orm';
import { relations, sql, type InferSelectModel } from 'drizzle-orm';
export type User = InferSelectModel<typeof users>;
export type Transaction = InferSelectModel<typeof transactions>;
export type ModerationCase = InferSelectModel<typeof moderationCases>;
export type Item = InferSelectModel<typeof items>;
export type Inventory = InferSelectModel<typeof inventory>;
// --- TABLES ---

View File

@@ -227,5 +227,66 @@ describe("dashboardService", () => {
expect(otherHour?.transactions).toBe(0);
expect(otherHour?.commands).toBe(0);
});
test("should return 24 hours of zeros if database is empty", async () => {
mockSelect.mockImplementationOnce(() => ({
// @ts-ignore
from: mock(() => ({
where: mock(() => ({
groupBy: mock(() => ({
orderBy: mock(() => Promise.resolve([]))
}))
}))
}))
}));
const activity = await dashboardService.getActivityAggregation();
expect(activity).toHaveLength(24);
expect(activity.every(a => a.transactions === 0 && a.commands === 0)).toBe(true);
});
test("should return 24 hours of zeros if database returns rows with null hours", async () => {
mockSelect.mockImplementationOnce(() => ({
// @ts-ignore
from: mock(() => ({
where: mock(() => ({
groupBy: mock(() => ({
orderBy: mock(() => Promise.resolve([{ hour: null, transactions: "10", commands: "5" }]))
}))
}))
}))
}));
const activity = await dashboardService.getActivityAggregation();
expect(activity).toHaveLength(24);
expect(activity.every(a => a.transactions === 0 && a.commands === 0)).toBe(true);
});
test("should correctly map hours regardless of input sort order", async () => {
const now = new Date();
now.setHours(now.getHours(), 0, 0, 0);
const hourAgo = new Date(now.getTime() - 60 * 60 * 1000);
mockSelect.mockImplementationOnce(() => ({
// @ts-ignore
from: mock(() => ({
where: mock(() => ({
groupBy: mock(() => ({
orderBy: mock(() => Promise.resolve([
{ hour: now.toISOString(), transactions: "10", commands: "5" },
{ hour: hourAgo.toISOString(), transactions: "20", commands: "10" }
]))
}))
}))
}))
}));
const activity = await dashboardService.getActivityAggregation();
const current = activity.find(a => a.hour === now.toISOString());
const past = activity.find(a => a.hour === hourAgo.toISOString());
expect(current?.transactions).toBe(10);
expect(past?.transactions).toBe(20);
});
});
});

View File

@@ -1,6 +1,6 @@
import { DrizzleClient } from "@shared/db/DrizzleClient";
import { users, transactions, moderationCases, inventory } from "@db/schema";
import { desc, sql, and, gte } from "drizzle-orm";
import { users, transactions, moderationCases, inventory, type User } from "@db/schema";
import { desc, sql, gte } from "drizzle-orm";
import type { RecentEvent, ActivityData } from "./dashboard.types";
import { TransactionType } from "@shared/lib/constants";
@@ -39,18 +39,18 @@ export const dashboardService = {
const allUsers = await DrizzleClient.select().from(users);
const totalWealth = allUsers.reduce(
(acc: bigint, u: any) => acc + (u.balance || 0n),
(acc: bigint, u: User) => acc + (u.balance || 0n),
0n
);
const avgLevel = allUsers.length > 0
? Math.round(
allUsers.reduce((acc: number, u: any) => acc + (u.level || 1), 0) / allUsers.length
allUsers.reduce((acc: number, u: User) => acc + (u.level || 1), 0) / allUsers.length
)
: 1;
const topStreak = allUsers.reduce(
(max: number, u: any) => Math.max(max, u.dailyStreak || 0),
(max: number, u: User) => Math.max(max, u.dailyStreak || 0),
0
);
@@ -83,7 +83,7 @@ export const dashboardService = {
},
});
return recentTx.map((tx: any) => ({
return recentTx.map((tx) => ({
type: 'info' as const,
message: `${tx.user?.username || 'Unknown'}: ${tx.description || 'Transaction'}`,
timestamp: tx.createdAt || new Date(),
@@ -103,11 +103,11 @@ export const dashboardService = {
where: gte(moderationCases.createdAt, oneDayAgo),
});
return recentCases.map((modCase: any) => ({
return recentCases.map((modCase) => ({
type: modCase.type === 'warn' || modCase.type === 'ban' ? 'error' : 'info',
message: `${modCase.type.toUpperCase()}: ${modCase.username} - ${modCase.reason}`,
timestamp: modCase.createdAt || new Date(),
icon: getModerationIcon(modCase.type),
icon: getModerationIcon(modCase.type as string),
}));
},