feat: Implement Account Lifecycle Tracking

This commit is contained in:
gpt-engineer-app[bot]
2025-10-17 20:26:11 +00:00
parent 09090c29f8
commit 7dcf8156b9
2 changed files with 322 additions and 4 deletions

View File

@@ -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;
}
}
}
}
}
}
}
}