mirror of
https://github.com/SrIzan10/hctv.git
synced 2026-06-06 00:56:56 +00:00
feat(admin): show deleted message content on audit logs
This commit is contained in:
@@ -280,23 +280,32 @@ async function logModerationEvent(payload: {
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteMessageFromHistory(targetUsername: string, msgId: string): Promise<boolean> {
|
||||
async function deleteMessageFromHistory(
|
||||
targetUsername: string,
|
||||
msgId: string
|
||||
): Promise<{ deleted: boolean; messageContent?: string }> {
|
||||
const channelKey = `chat:history:${targetUsername}`;
|
||||
const history = await redis.zrange(channelKey, 0, -1);
|
||||
|
||||
for (const entry of history) {
|
||||
try {
|
||||
const parsed = JSON.parse(entry) as { msgId?: string };
|
||||
const parsed = JSON.parse(entry) as { msgId?: string; message?: string };
|
||||
if (parsed.msgId === msgId) {
|
||||
await redis.zrem(channelKey, entry);
|
||||
return true;
|
||||
return {
|
||||
deleted: true,
|
||||
messageContent:
|
||||
typeof parsed.message === 'string' && parsed.message.length > 0
|
||||
? parsed.message
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return { deleted: false };
|
||||
}
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
@@ -26,7 +26,10 @@ type ModerationContext = {
|
||||
};
|
||||
|
||||
type DeleteMessageDeps = {
|
||||
deleteMessageFromHistory: (targetUsername: string, msgId: string) => Promise<boolean>;
|
||||
deleteMessageFromHistory: (
|
||||
targetUsername: string,
|
||||
msgId: string
|
||||
) => Promise<{ deleted: boolean; messageContent?: string }>;
|
||||
logModerationEvent: (payload: {
|
||||
action: ChatModerationAction;
|
||||
channelId: string;
|
||||
@@ -258,8 +261,8 @@ export async function handleDeleteMessageCommand(
|
||||
return;
|
||||
}
|
||||
|
||||
const deleted = await deps.deleteMessageFromHistory(context.targetUsername, msgId);
|
||||
if (!deleted) {
|
||||
const deletedMessage = await deps.deleteMessageFromHistory(context.targetUsername, msgId);
|
||||
if (!deletedMessage.deleted) {
|
||||
sendModerationError(socket, 'NOT_FOUND', 'Message not found.');
|
||||
return;
|
||||
}
|
||||
@@ -269,7 +272,10 @@ export async function handleDeleteMessageCommand(
|
||||
channelId: context.channelId,
|
||||
moderatorId: context.chatUser.moderatorUserId,
|
||||
reason: 'Message deleted by moderator',
|
||||
details: { msgId },
|
||||
details: {
|
||||
msgId,
|
||||
messageContent: deletedMessage.messageContent,
|
||||
},
|
||||
});
|
||||
recordChatModerationAction('message_deleted');
|
||||
|
||||
|
||||
@@ -1019,6 +1019,20 @@ export default function AdminPanelClient({ currentUser }: AdminPanelClientProps)
|
||||
{log.reason}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{log.deletedMessageContent && (
|
||||
<div className="mt-2 flex items-start gap-2 rounded-md border border-border bg-muted/40 px-3 py-2">
|
||||
<MessageSquare className="mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Deleted message
|
||||
</p>
|
||||
<p className="mt-1 break-words text-xs leading-relaxed text-foreground/90">
|
||||
{log.deletedMessageContent}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1184,10 +1198,10 @@ export default function AdminPanelClient({ currentUser }: AdminPanelClientProps)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'settings' && (
|
||||
<div>
|
||||
)}
|
||||
|
||||
{activeTab === 'settings' && (
|
||||
<div>
|
||||
<SectionHeader
|
||||
icon={<Settings className="h-4 w-4" />}
|
||||
title="Platform Settings"
|
||||
@@ -1205,7 +1219,8 @@ export default function AdminPanelClient({ currentUser }: AdminPanelClientProps)
|
||||
ID Verification Bypass
|
||||
</h3>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground leading-relaxed">
|
||||
Allow existing users to bypass HCA verification and let them access the platform.
|
||||
Allow existing users to bypass HCA verification and let them access the
|
||||
platform.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1231,16 +1246,15 @@ export default function AdminPanelClient({ currentUser }: AdminPanelClientProps)
|
||||
Start searching to manage users
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Type an email or username above to find users and toggle their verification bypass
|
||||
Type an email or username above to find users and toggle their
|
||||
verification bypass
|
||||
</p>
|
||||
</div>
|
||||
) : users.length === 0 ? (
|
||||
<div className="rounded-lg border-2 border-dashed border-border bg-muted/30 p-6 text-center">
|
||||
<XCircle className="mx-auto h-8 w-8 text-muted-foreground/50 mb-2" />
|
||||
<p className="text-sm font-medium text-foreground mb-1">No users found</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Try a different search term
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">Try a different search term</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -1292,7 +1306,8 @@ export default function AdminPanelClient({ currentUser }: AdminPanelClientProps)
|
||||
onClick={() => handleToggleBypassVerification(user.id)}
|
||||
className={cn(
|
||||
'h-7 text-xs gap-1 shrink-0 font-semibold transition-all duration-200',
|
||||
user.bypassVerification && 'border-primary/30 hover:bg-primary/10 hover:border-primary/50'
|
||||
user.bypassVerification &&
|
||||
'border-primary/30 hover:bg-primary/10 hover:border-primary/50'
|
||||
)}
|
||||
>
|
||||
{user.bypassVerification ? (
|
||||
@@ -1327,7 +1342,8 @@ export default function AdminPanelClient({ currentUser }: AdminPanelClientProps)
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-bold tracking-tight">Session Management</h3>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground mb-3 leading-relaxed">
|
||||
Force logout all other sessions except your current one. Useful for security maintenance.
|
||||
Force logout all other sessions except your current one. Useful for
|
||||
security maintenance.
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -1536,6 +1552,7 @@ interface AuditLog {
|
||||
reason: string | null;
|
||||
details?: unknown;
|
||||
channelName?: string;
|
||||
deletedMessageContent?: string | null;
|
||||
}
|
||||
|
||||
interface ChatReport {
|
||||
|
||||
@@ -2,6 +2,15 @@ import { validateRequest } from '@/lib/auth/validate';
|
||||
import { prisma } from '@hctv/db';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
function getDeletedMessageContent(details: unknown): string | null {
|
||||
if (!details || typeof details !== 'object' || Array.isArray(details)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const messageContent = (details as { messageContent?: unknown }).messageContent;
|
||||
return typeof messageContent === 'string' && messageContent.length > 0 ? messageContent : null;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { user } = await validateRequest();
|
||||
if (!user?.isAdmin) {
|
||||
@@ -136,6 +145,7 @@ export async function GET(request: NextRequest) {
|
||||
(log.targetUserId ? (targetUserMap.get(log.targetUserId) ?? log.targetUserId) : null),
|
||||
reason: log.reason,
|
||||
details: log.details,
|
||||
deletedMessageContent: getDeletedMessageContent(log.details),
|
||||
actorMeta: {
|
||||
isPlatformAdmin: log.actor.isAdmin,
|
||||
isChannelModerator: channelModSet.has(log.actorId),
|
||||
@@ -157,6 +167,7 @@ export async function GET(request: NextRequest) {
|
||||
target: log.targetUser?.personalChannel?.name ?? log.channel.name,
|
||||
reason: log.reason,
|
||||
details: log.details,
|
||||
deletedMessageContent: getDeletedMessageContent(log.details),
|
||||
channelName: log.channel.name,
|
||||
actorMeta: {
|
||||
isPlatformAdmin: log.moderator.isAdmin,
|
||||
|
||||
@@ -292,6 +292,7 @@ export async function POST(request: NextRequest) {
|
||||
details: {
|
||||
reportId,
|
||||
msgId: report.reportedMessageId,
|
||||
messageContent: report.reportedMessage,
|
||||
} as any,
|
||||
},
|
||||
});
|
||||
@@ -307,6 +308,7 @@ export async function POST(request: NextRequest) {
|
||||
reportId,
|
||||
enforcement: 'DELETE_REPORTED_MESSAGE',
|
||||
msgId: report.reportedMessageId,
|
||||
messageContent: report.reportedMessage,
|
||||
} as any,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -200,7 +200,7 @@ export default function StreamPlayer() {
|
||||
{(process.env.NODE_ENV === 'development' || userInfo?.isLive) && (
|
||||
<MediaChromeButton onClick={() => triggerRecovery('manual_reload')}>
|
||||
<span className="flex h-4 w-4 items-center justify-center">
|
||||
<RefreshCw className="h-5 w-5 shrink-0" strokeWidth={2.5} />
|
||||
<RefreshCw className={cn("h-5 w-5 shrink-0", isRecovering && "animate-spin")} strokeWidth={2.5} />
|
||||
</span>
|
||||
<span slot="tooltip-content">Retry stream</span>
|
||||
</MediaChromeButton>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"docker:chat": "dotenvx run -f .env.docker -- docker buildx build --platform linux/amd64 -f apps/chat/Dockerfile . --secret id=TURBO_TOKEN,env=TURBO_TOKEN --secret id=TURBO_TEAM,env=TURBO_TEAM --no-cache",
|
||||
"act": "act --secret-file .env.ci",
|
||||
"db:migrate": "pnpm --filter=@hctv/db db:migrate",
|
||||
"db:studio": "pnpm --filter=@hctv/db db:studio",
|
||||
"ui:add": "pnpm --filter=@hctv/web ui:add",
|
||||
"prisma": "pnpm --filter=@hctv/db prisma",
|
||||
"r:rtmp": "docker compose -f dev/docker-compose.yml restart nginx-rtmp -t 0",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:deploy": "prisma migrate deploy",
|
||||
"db:populate-verification": "tsx src/populateHackClubVerification.ts",
|
||||
"db:studio": "prisma studio",
|
||||
"build": "prisma generate && tsc --build",
|
||||
"dev": "tsc --watch --preserveWatchOutput"
|
||||
},
|
||||
@@ -27,4 +28,4 @@
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user