diff --git a/src/components/admin/SystemActivityLog.tsx b/src/components/admin/SystemActivityLog.tsx index 37a10c33..fa24be3c 100644 --- a/src/components/admin/SystemActivityLog.tsx +++ b/src/components/admin/SystemActivityLog.tsx @@ -21,7 +21,11 @@ import { CheckCircle, ChevronDown, ChevronUp, - Trash2 + Trash2, + UserPlus, + UserX, + Ban, + UserCheck } from 'lucide-react'; import { formatDistanceToNow } from 'date-fns'; import { @@ -33,7 +37,8 @@ import { SubmissionReviewDetails, ReportResolutionDetails, ReviewModerationDetails, - PhotoApprovalDetails + PhotoApprovalDetails, + AccountLifecycleDetails } from '@/lib/systemActivityService'; export interface SystemActivityLogRef { @@ -82,6 +87,42 @@ const activityTypeConfig = { bgColor: 'bg-teal-500/10', label: 'Photo Approval', }, + account_created: { + icon: UserPlus, + color: 'text-green-600', + bgColor: 'bg-green-600/10', + label: 'Account Created', + }, + account_deletion_requested: { + icon: UserX, + color: 'text-orange-600', + bgColor: 'bg-orange-600/10', + label: 'Deletion Requested', + }, + account_deletion_confirmed: { + icon: UserX, + color: 'text-red-600', + bgColor: 'bg-red-600/10', + label: 'Deletion Confirmed', + }, + account_deletion_cancelled: { + icon: UserCheck, + color: 'text-blue-600', + bgColor: 'bg-blue-600/10', + label: 'Deletion Cancelled', + }, + user_banned: { + icon: Ban, + color: 'text-red-700', + bgColor: 'bg-red-700/10', + label: 'User Banned', + }, + user_unbanned: { + icon: UserCheck, + color: 'text-green-700', + bgColor: 'bg-green-700/10', + label: 'User Unbanned', + }, }; export const SystemActivityLog = forwardRef( @@ -322,6 +363,123 @@ export const SystemActivityLog = forwardRef +
+ New Account + {details.username && ( + @{details.username} + )} +
+ + ); + } + + case 'account_deletion_requested': { + const details = activity.details as AccountLifecycleDetails; + return ( +
+
+ Deletion Requested + {details.scheduled_date && ( + + Scheduled: {new Date(details.scheduled_date).toLocaleDateString()} + + )} +
+ {isExpanded && details.request_id && ( +

+ Request ID: {details.request_id} +

+ )} +
+ ); + } + + case 'account_deletion_confirmed': { + const details = activity.details as AccountLifecycleDetails; + return ( +
+
+ Deletion Confirmed + {details.scheduled_date && ( + + Will be deleted: {new Date(details.scheduled_date).toLocaleDateString()} + + )} +
+
+ + + Account will be permanently deleted after 7 days + +
+
+ ); + } + + case 'account_deletion_cancelled': { + const details = activity.details as AccountLifecycleDetails; + return ( +
+
+ Deletion Cancelled +
+ {isExpanded && details.reason && ( +
+ + + Reason: {details.reason} + +
+ )} +
+ ); + } + + case 'user_banned': { + const details = activity.details as AccountLifecycleDetails; + return ( +
+
+ Banned + {details.username && ( + @{details.username} + )} +
+ {isExpanded && details.reason && ( +
+ + + Reason: {details.reason} + +
+ )} +
+ ); + } + + case 'user_unbanned': { + const details = activity.details as AccountLifecycleDetails; + return ( +
+
+ Unbanned + {details.username && ( + @{details.username} + )} +
+ {isExpanded && details.reason && ( +

+ Reason: {details.reason} +

+ )} +
+ ); + } + default: return null; } diff --git a/src/lib/systemActivityService.ts b/src/lib/systemActivityService.ts index ce357d9a..d15aebee 100644 --- a/src/lib/systemActivityService.ts +++ b/src/lib/systemActivityService.ts @@ -7,7 +7,13 @@ export type ActivityType = | 'submission_review' | 'report_resolution' | 'review_moderation' - | 'photo_approval'; + | 'photo_approval' + | 'account_created' + | 'account_deletion_requested' + | 'account_deletion_confirmed' + | 'account_deletion_cancelled' + | 'user_banned' + | 'user_unbanned'; export interface ActivityActor { id: string; @@ -69,13 +75,23 @@ export interface PhotoApprovalDetails { entity_name?: string; } +export interface AccountLifecycleDetails { + user_id: string; + username?: string; + action: 'created' | 'deletion_requested' | 'deletion_confirmed' | 'deletion_cancelled' | 'banned' | 'unbanned'; + reason?: string; + scheduled_date?: string; + request_id?: string; +} + export type ActivityDetails = | EntityChangeDetails | AdminActionDetails | SubmissionReviewDetails | ReportResolutionDetails | ReviewModerationDetails - | PhotoApprovalDetails; + | PhotoApprovalDetails + | AccountLifecycleDetails; export interface SystemActivity { id: string; @@ -441,6 +457,121 @@ export async function fetchSystemActivities( } } + // Fetch account lifecycle events + // 1. Account deletions + const { data: deletionRequests, error: deletionsError } = await supabase + .from('account_deletion_requests') + .select('id, user_id, status, requested_at, completed_at, cancelled_at, scheduled_deletion_at, cancellation_reason') + .order('requested_at', { ascending: false }) + .limit(limit); + + if (!deletionsError && deletionRequests) { + for (const request of deletionRequests) { + // Deletion requested + activities.push({ + id: `${request.id}-requested`, + type: 'account_deletion_requested', + timestamp: request.requested_at, + actor_id: request.user_id, + action: 'requested account deletion', + details: { + user_id: request.user_id, + action: 'deletion_requested', + scheduled_date: request.scheduled_deletion_at, + request_id: request.id, + } as AccountLifecycleDetails, + }); + + // Deletion confirmed + if (request.status === 'confirmed' || request.status === 'completed') { + activities.push({ + id: `${request.id}-confirmed`, + type: 'account_deletion_confirmed', + timestamp: request.completed_at || request.requested_at, + actor_id: request.user_id, + action: 'confirmed account deletion', + details: { + user_id: request.user_id, + action: 'deletion_confirmed', + scheduled_date: request.scheduled_deletion_at, + request_id: request.id, + } as AccountLifecycleDetails, + }); + } + + // Deletion cancelled + if (request.status === 'cancelled' && request.cancelled_at) { + activities.push({ + id: `${request.id}-cancelled`, + type: 'account_deletion_cancelled', + timestamp: request.cancelled_at, + actor_id: request.user_id, + action: 'cancelled account deletion', + details: { + user_id: request.user_id, + action: 'deletion_cancelled', + reason: request.cancellation_reason || undefined, + request_id: request.id, + } as AccountLifecycleDetails, + }); + } + } + } + + // 2. New account creations (recent 7 days) + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); + + const { data: newAccounts, error: accountsError } = await supabase + .from('profiles') + .select('user_id, username, display_name, created_at') + .gte('created_at', sevenDaysAgo.toISOString()) + .order('created_at', { ascending: false }) + .limit(Math.ceil(limit / 2)); + + if (!accountsError && newAccounts) { + for (const account of newAccounts) { + activities.push({ + id: `account-${account.user_id}`, + type: 'account_created', + timestamp: account.created_at, + actor_id: account.user_id, + action: 'created account', + details: { + user_id: account.user_id, + username: account.username, + action: 'created', + } as AccountLifecycleDetails, + }); + } + } + + // 3. User bans/unbans from admin audit log + const { data: banActions, error: banError } = await supabase + .from('admin_audit_log') + .select('id, admin_user_id, target_user_id, action, details, created_at') + .in('action', ['user_banned', 'user_unbanned']) + .order('created_at', { ascending: false }) + .limit(limit); + + if (!banError && banActions) { + for (const action of banActions) { + const activityType = action.action === 'user_banned' ? 'user_banned' : 'user_unbanned'; + activities.push({ + id: action.id, + type: activityType, + timestamp: action.created_at, + actor_id: action.admin_user_id, + action: action.action === 'user_banned' ? 'banned user' : 'unbanned user', + details: { + user_id: action.target_user_id, + action: action.action === 'user_banned' ? 'banned' : 'unbanned', + reason: typeof action.details === 'object' && action.details && 'reason' in action.details ? String(action.details.reason) : undefined, + } as AccountLifecycleDetails, + }); + } + } + // Sort all activities by timestamp (newest first) activities.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); @@ -510,6 +641,35 @@ export async function fetchSystemActivities( } } } + + // Enrich account lifecycle target users (for bans/unbans) + const accountUserIds = filteredActivities + .filter(a => ['user_banned', 'user_unbanned', 'account_deletion_requested', 'account_deletion_confirmed', 'account_deletion_cancelled'].includes(a.type)) + .map(a => (a.details as AccountLifecycleDetails).user_id) + .filter(Boolean) as string[]; + + if (accountUserIds.length > 0) { + const { data: accountProfiles } = await supabase + .from('profiles') + .select('user_id, username') + .in('user_id', accountUserIds); + + if (accountProfiles) { + const accountProfileMap = new Map(accountProfiles.map(p => [p.user_id, p])); + + for (const activity of filteredActivities) { + if (['user_banned', 'user_unbanned', 'account_deletion_requested', 'account_deletion_confirmed', 'account_deletion_cancelled'].includes(activity.type)) { + const details = activity.details as AccountLifecycleDetails; + if (details.user_id && !details.username) { + const accountProfile = accountProfileMap.get(details.user_id); + if (accountProfile) { + details.username = accountProfile.username; + } + } + } + } + } + } } }