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 { ArrowUp, ArrowDown, Clock, Shield, UserX } from 'lucide-react';
import { ArrowUp, ArrowDown, Clock, MessageSquareX, UserX } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import type { Comment } from '@opchan/core';
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"
onClick={() => onModerateComment(comment.id)}
>
<Shield className="h-3 w-3" />
<MessageSquareX className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>

View File

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

View File

@ -1,5 +1,5 @@
export const DB_NAME = 'opchan-local';
export const DB_VERSION = 4;
export const DB_VERSION = 5;
export const STORE = {
CELLS: 'cells',
@ -49,10 +49,12 @@ export function openLocalDB(): Promise<IDBDatabase> {
// Votes are keyed by composite key `${targetId}:${author}`
db.createObjectStore(STORE.VOTES, { keyPath: 'key' });
}
if (!db.objectStoreNames.contains(STORE.MODERATIONS)) {
// Moderations keyed by targetId
db.createObjectStore(STORE.MODERATIONS, { keyPath: 'targetId' });
// Moderations store: recreate with composite key support
if (db.objectStoreNames.contains(STORE.MODERATIONS)) {
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)) {
// User identities keyed by address
db.createObjectStore(STORE.USER_IDENTITIES, { keyPath: 'address' });

View File

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

View File

@ -18,27 +18,11 @@ export const transformCell = async (
userVerificationStatus?: UserVerificationStatus,
posts?: Post[]
): 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) {
const relevanceCalculator = new RelevanceCalculator();
const relevanceResult = relevanceCalculator.calculateCellScore(
transformedCell,
cellMessage,
posts
);
@ -50,14 +34,14 @@ export const transformCell = async (
});
return {
...transformedCell,
...cellMessage,
relevanceScore: relevanceResult.score,
activeMemberCount: activeMembers.size,
relevanceDetails: relevanceResult.details,
};
}
return transformedCell;
return cellMessage;
};
export const transformPost = async (
@ -97,17 +81,8 @@ export const transformPost = async (
!!userModMsg && userModMsg.action === EModerationAction.MODERATE;
const transformedPost: Post = {
id: postMessage.id,
type: postMessage.type,
author: postMessage.author,
cellId: postMessage.cellId,
authorAddress: postMessage.author,
title: postMessage.title,
content: postMessage.content,
timestamp: postMessage.timestamp,
signature: postMessage.signature,
browserPubKey: postMessage.browserPubKey,
delegationProof: postMessage.delegationProof,
...postMessage,
upvotes,
downvotes,
moderated: isPostModerated || isUserModerated,
@ -223,16 +198,8 @@ export const transformComment = async (
!!userModMsg && userModMsg.action === EModerationAction.MODERATE;
const transformedComment: Comment = {
id: commentMessage.id,
type: commentMessage.type,
author: commentMessage.author,
postId: commentMessage.postId,
...commentMessage,
authorAddress: commentMessage.author,
content: commentMessage.content,
timestamp: commentMessage.timestamp,
signature: commentMessage.signature,
browserPubKey: commentMessage.browserPubKey,
delegationProof: commentMessage.delegationProof,
upvotes,
downvotes,
moderated: isCommentModerated || isUserModerated,

View File

@ -193,8 +193,9 @@ export function useContent() {
const currentUser = session.currentUser;
const isAuthenticated = Boolean(currentUser);
const cell = content.cells.find(c => c.id === cellId);
const comment = content.comments.find(c => c.id === commentId);
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);
@ -204,8 +205,9 @@ export function useContent() {
const currentUser = session.currentUser;
const isAuthenticated = Boolean(currentUser);
const cell = content.cells.find(c => c.id === cellId);
const comment = content.comments.find(c => c.id === commentId);
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);