chore: user cannot moderate themselves

This commit is contained in:
Danish Arora 2025-10-03 19:06:11 +05:30
parent f9863121ba
commit 3d3eafd626
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
6 changed files with 66 additions and 57 deletions

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { ArrowUp, ArrowDown, Clock, Shield, UserX } from 'lucide-react'; import { ArrowUp, ArrowDown, Clock, MessageSquareX, UserX } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import type { Comment } from '@opchan/core'; import type { Comment } from '@opchan/core';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@ -174,7 +174,7 @@ const CommentCard: React.FC<CommentCardProps> = ({
className="h-6 w-6 text-cyber-neutral hover:text-orange-500" className="h-6 w-6 text-cyber-neutral hover:text-orange-500"
onClick={() => onModerateComment(comment.id)} onClick={() => onModerateComment(comment.id)}
> >
<Shield className="h-3 w-3" /> <MessageSquareX className="h-3 w-3" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>

View File

@ -24,7 +24,11 @@ export interface LocalDatabaseCache {
posts: PostCache; posts: PostCache;
comments: CommentCache; comments: CommentCache;
votes: VoteCache; votes: VoteCache;
moderations: { [targetId: string]: ModerateMessage }; // Moderations keyed by composite key:
// - post: 'post:<postId>'
// - comment: 'comment:<commentId>'
// - user: '<cellId>:user:<address>'
moderations: { [key: string]: (ModerateMessage & { key?: string }) };
userIdentities: UserIdentityCache; userIdentities: UserIdentityCache;
bookmarks: BookmarkCache; bookmarks: BookmarkCache;
} }
@ -212,13 +216,20 @@ export class LocalDatabase {
} }
case MessageType.MODERATE: { case MessageType.MODERATE: {
const modMsg = message as ModerateMessage; const modMsg = message as ModerateMessage;
if ( // Compose key:
!this.cache.moderations[modMsg.targetId] || // - post: `post:${postId}`
this.cache.moderations[modMsg.targetId]?.timestamp !== // - comment: `comment:${commentId}`
modMsg.timestamp // - user: `${cellId}:user:${address}` (per-cell user moderation)
) { const key =
this.cache.moderations[modMsg.targetId] = modMsg; modMsg.targetType === 'user'
this.put(STORE.MODERATIONS, modMsg); ? `${modMsg.cellId}:user:${modMsg.targetId}`
: `${modMsg.targetType}:${modMsg.targetId}`;
const existing = this.cache.moderations[key];
if (!existing || modMsg.timestamp > existing.timestamp) {
// Store in cache and persist with computed key
this.cache.moderations[key] = { ...(modMsg as ModerateMessage), key };
this.put(STORE.MODERATIONS, { ...(modMsg as ModerateMessage), key });
} }
break; break;
} }
@ -267,7 +278,7 @@ export class LocalDatabase {
PostMessage[], PostMessage[],
CommentMessage[], CommentMessage[],
(VoteMessage & { key: string })[], (VoteMessage & { key: string })[],
ModerateMessage[], (ModerateMessage & { key: string })[],
({ address: string } & UserIdentityCache[string])[], ({ address: string } & UserIdentityCache[string])[],
Bookmark[], Bookmark[],
] = await Promise.all([ ] = await Promise.all([
@ -275,7 +286,7 @@ export class LocalDatabase {
this.getAllFromStore<PostMessage>(STORE.POSTS), this.getAllFromStore<PostMessage>(STORE.POSTS),
this.getAllFromStore<CommentMessage>(STORE.COMMENTS), this.getAllFromStore<CommentMessage>(STORE.COMMENTS),
this.getAllFromStore<VoteMessage & { key: string }>(STORE.VOTES), this.getAllFromStore<VoteMessage & { key: string }>(STORE.VOTES),
this.getAllFromStore<ModerateMessage>(STORE.MODERATIONS), this.getAllFromStore<ModerateMessage & { key: string }>(STORE.MODERATIONS),
this.getAllFromStore<{ address: string } & UserIdentityCache[string]>( this.getAllFromStore<{ address: string } & UserIdentityCache[string]>(
STORE.USER_IDENTITIES STORE.USER_IDENTITIES
), ),
@ -293,7 +304,7 @@ export class LocalDatabase {
}) })
); );
this.cache.moderations = Object.fromEntries( this.cache.moderations = Object.fromEntries(
moderations.map(m => [m.targetId, m]) moderations.map(m => [m.key, m])
); );
this.cache.userIdentities = Object.fromEntries( this.cache.userIdentities = Object.fromEntries(
identities.map(u => { identities.map(u => {

View File

@ -1,5 +1,5 @@
export const DB_NAME = 'opchan-local'; export const DB_NAME = 'opchan-local';
export const DB_VERSION = 4; export const DB_VERSION = 5;
export const STORE = { export const STORE = {
CELLS: 'cells', CELLS: 'cells',
@ -49,10 +49,12 @@ export function openLocalDB(): Promise<IDBDatabase> {
// Votes are keyed by composite key `${targetId}:${author}` // Votes are keyed by composite key `${targetId}:${author}`
db.createObjectStore(STORE.VOTES, { keyPath: 'key' }); db.createObjectStore(STORE.VOTES, { keyPath: 'key' });
} }
if (!db.objectStoreNames.contains(STORE.MODERATIONS)) { // Moderations store: recreate with composite key support
// Moderations keyed by targetId if (db.objectStoreNames.contains(STORE.MODERATIONS)) {
db.createObjectStore(STORE.MODERATIONS, { keyPath: 'targetId' }); db.deleteObjectStore(STORE.MODERATIONS);
} }
// Moderations keyed by computed 'key' (e.g., 'post:postId', 'comment:commentId', 'cellId:user:userAddress')
db.createObjectStore(STORE.MODERATIONS, { keyPath: 'key' });
if (!db.objectStoreNames.contains(STORE.USER_IDENTITIES)) { if (!db.objectStoreNames.contains(STORE.USER_IDENTITIES)) {
// User identities keyed by address // User identities keyed by address
db.createObjectStore(STORE.USER_IDENTITIES, { keyPath: 'address' }); db.createObjectStore(STORE.USER_IDENTITIES, { keyPath: 'address' });

View File

@ -431,6 +431,7 @@ export class ForumActions {
currentUser, currentUser,
isAuthenticated, isAuthenticated,
cellOwner, cellOwner,
commentAuthor,
} = params; } = params;
if (!isAuthenticated || !currentUser) { if (!isAuthenticated || !currentUser) {
@ -446,6 +447,12 @@ export class ForumActions {
error: 'Not authorized. Only the cell admin can moderate comments.', error: 'Not authorized. Only the cell admin can moderate comments.',
}; };
} }
if (currentUser.address === commentAuthor) {
return {
success: false,
error: 'You cannot moderate your own comments.',
};
}
try { try {
const unsignedMod: UnsignedModerateMessage = { const unsignedMod: UnsignedModerateMessage = {
@ -520,6 +527,12 @@ export class ForumActions {
error: 'Not authorized. Only the cell admin can moderate users.', error: 'Not authorized. Only the cell admin can moderate users.',
}; };
} }
if (currentUser.address === userAddress) {
return {
success: false,
error: 'You cannot moderate yourself.',
};
}
try { try {
const unsignedMod: UnsignedModerateMessage = { const unsignedMod: UnsignedModerateMessage = {
@ -647,6 +660,7 @@ export class ForumActions {
currentUser, currentUser,
isAuthenticated, isAuthenticated,
cellOwner, cellOwner,
commentAuthor,
} = params; } = params;
if (!isAuthenticated || !currentUser) { if (!isAuthenticated || !currentUser) {
@ -662,6 +676,12 @@ export class ForumActions {
error: 'Not authorized. Only the cell admin can unmoderate comments.', error: 'Not authorized. Only the cell admin can unmoderate comments.',
}; };
} }
if (currentUser.address === commentAuthor) {
return {
success: false,
error: 'You cannot unmoderate your own comments.',
};
}
try { try {
const unsignedMod: UnsignedModerateMessage = { const unsignedMod: UnsignedModerateMessage = {
@ -736,6 +756,12 @@ export class ForumActions {
error: 'Not authorized. Only the cell admin can unmoderate users.', error: 'Not authorized. Only the cell admin can unmoderate users.',
}; };
} }
if (currentUser.address === userAddress) {
return {
success: false,
error: 'You cannot unmoderate yourself.',
};
}
try { try {
const unsignedMod: UnsignedModerateMessage = { const unsignedMod: UnsignedModerateMessage = {
@ -826,6 +852,7 @@ interface CommentModerationParams extends BaseActionParams {
commentId: string; commentId: string;
reason?: string; reason?: string;
cellOwner: string; cellOwner: string;
commentAuthor: string;
} }
interface UserModerationParams extends BaseActionParams { interface UserModerationParams extends BaseActionParams {

View File

@ -18,27 +18,11 @@ export const transformCell = async (
userVerificationStatus?: UserVerificationStatus, userVerificationStatus?: UserVerificationStatus,
posts?: Post[] posts?: Post[]
): Promise<Cell | null> => { ): Promise<Cell | null> => {
// Message validity already enforced upstream
const transformedCell: Cell = {
id: cellMessage.id,
type: cellMessage.type,
author: cellMessage.author,
name: cellMessage.name,
description: cellMessage.description,
icon: cellMessage.icon || '',
timestamp: cellMessage.timestamp,
signature: cellMessage.signature,
browserPubKey: cellMessage.browserPubKey,
delegationProof: cellMessage.delegationProof,
};
// Calculate relevance score if user verification status and posts are provided
if (userVerificationStatus && posts) { if (userVerificationStatus && posts) {
const relevanceCalculator = new RelevanceCalculator(); const relevanceCalculator = new RelevanceCalculator();
const relevanceResult = relevanceCalculator.calculateCellScore( const relevanceResult = relevanceCalculator.calculateCellScore(
transformedCell, cellMessage,
posts posts
); );
@ -50,14 +34,14 @@ export const transformCell = async (
}); });
return { return {
...transformedCell, ...cellMessage,
relevanceScore: relevanceResult.score, relevanceScore: relevanceResult.score,
activeMemberCount: activeMembers.size, activeMemberCount: activeMembers.size,
relevanceDetails: relevanceResult.details, relevanceDetails: relevanceResult.details,
}; };
} }
return transformedCell; return cellMessage;
}; };
export const transformPost = async ( export const transformPost = async (
@ -97,17 +81,8 @@ export const transformPost = async (
!!userModMsg && userModMsg.action === EModerationAction.MODERATE; !!userModMsg && userModMsg.action === EModerationAction.MODERATE;
const transformedPost: Post = { const transformedPost: Post = {
id: postMessage.id,
type: postMessage.type,
author: postMessage.author,
cellId: postMessage.cellId,
authorAddress: postMessage.author, authorAddress: postMessage.author,
title: postMessage.title, ...postMessage,
content: postMessage.content,
timestamp: postMessage.timestamp,
signature: postMessage.signature,
browserPubKey: postMessage.browserPubKey,
delegationProof: postMessage.delegationProof,
upvotes, upvotes,
downvotes, downvotes,
moderated: isPostModerated || isUserModerated, moderated: isPostModerated || isUserModerated,
@ -223,16 +198,8 @@ export const transformComment = async (
!!userModMsg && userModMsg.action === EModerationAction.MODERATE; !!userModMsg && userModMsg.action === EModerationAction.MODERATE;
const transformedComment: Comment = { const transformedComment: Comment = {
id: commentMessage.id, ...commentMessage,
type: commentMessage.type,
author: commentMessage.author,
postId: commentMessage.postId,
authorAddress: commentMessage.author, authorAddress: commentMessage.author,
content: commentMessage.content,
timestamp: commentMessage.timestamp,
signature: commentMessage.signature,
browserPubKey: commentMessage.browserPubKey,
delegationProof: commentMessage.delegationProof,
upvotes, upvotes,
downvotes, downvotes,
moderated: isCommentModerated || isUserModerated, moderated: isCommentModerated || isUserModerated,

View File

@ -193,8 +193,9 @@ export function useContent() {
const currentUser = session.currentUser; const currentUser = session.currentUser;
const isAuthenticated = Boolean(currentUser); const isAuthenticated = Boolean(currentUser);
const cell = content.cells.find(c => c.id === cellId); const cell = content.cells.find(c => c.id === cellId);
const comment = content.comments.find(c => c.id === commentId);
const res = await client.forumActions.moderateComment( const res = await client.forumActions.moderateComment(
{ cellId, commentId, reason, currentUser, isAuthenticated, cellOwner: cell?.author ?? '' }, { cellId, commentId, reason, currentUser, isAuthenticated, cellOwner: cell?.author ?? '', commentAuthor: comment?.author ?? '' },
() => reflectCache(client) () => reflectCache(client)
); );
reflectCache(client); reflectCache(client);
@ -204,8 +205,9 @@ export function useContent() {
const currentUser = session.currentUser; const currentUser = session.currentUser;
const isAuthenticated = Boolean(currentUser); const isAuthenticated = Boolean(currentUser);
const cell = content.cells.find(c => c.id === cellId); const cell = content.cells.find(c => c.id === cellId);
const comment = content.comments.find(c => c.id === commentId);
const res = await client.forumActions.unmoderateComment( const res = await client.forumActions.unmoderateComment(
{ cellId, commentId, reason, currentUser, isAuthenticated, cellOwner: cell?.author ?? '' }, { cellId, commentId, reason, currentUser, isAuthenticated, cellOwner: cell?.author ?? '', commentAuthor: comment?.author ?? '' },
() => reflectCache(client) () => reflectCache(client)
); );
reflectCache(client); reflectCache(client);