feat: Implement privacy modernization plan

This commit is contained in:
gpt-engineer-app[bot]
2025-10-14 19:45:39 +00:00
parent 95972a0b22
commit 5313d8e66c
4 changed files with 428 additions and 145 deletions

View File

@@ -1,29 +1,17 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
import { UserX, Trash2 } from 'lucide-react'; import { UserX, Trash2 } from 'lucide-react';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { useAuth } from '@/hooks/useAuth'; import { useAuth } from '@/hooks/useAuth';
import { useToast } from '@/hooks/use-toast'; import { handleError, handleSuccess } from '@/lib/errorHandler';
import { logger } from '@/lib/logger';
interface BlockedUser { import type { UserBlock } from '@/types/privacy';
id: string;
blocked_id: string;
reason?: string;
created_at: string;
blocked_profile?: {
username: string;
display_name?: string;
avatar_url?: string;
};
}
export function BlockedUsers() { export function BlockedUsers() {
const { user } = useAuth(); const { user } = useAuth();
const { toast } = useToast(); const [blockedUsers, setBlockedUsers] = useState<UserBlock[]>([]);
const [blockedUsers, setBlockedUsers] = useState<BlockedUser[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
@@ -43,7 +31,15 @@ export function BlockedUsers() {
.eq('blocker_id', user.id) .eq('blocker_id', user.id)
.order('created_at', { ascending: false }); .order('created_at', { ascending: false });
if (blocksError) throw blocksError; if (blocksError) {
logger.error('Failed to fetch user blocks', {
userId: user.id,
action: 'fetch_blocked_users',
error: blocksError.message,
errorCode: blocksError.code
});
throw blocksError;
}
if (!blocks || blocks.length === 0) { if (!blocks || blocks.length === 0) {
setBlockedUsers([]); setBlockedUsers([]);
@@ -57,46 +53,99 @@ export function BlockedUsers() {
.select('user_id, username, display_name, avatar_url') .select('user_id, username, display_name, avatar_url')
.in('user_id', blockedIds); .in('user_id', blockedIds);
if (profilesError) throw profilesError; if (profilesError) {
logger.error('Failed to fetch blocked user profiles', {
userId: user.id,
action: 'fetch_blocked_user_profiles',
error: profilesError.message,
errorCode: profilesError.code
});
throw profilesError;
}
// Combine the data // Combine the data
const blockedUsersWithProfiles = blocks.map(block => ({ const blockedUsersWithProfiles = blocks.map(block => ({
...block, ...block,
blocker_id: user.id,
blocked_profile: profiles?.find(p => p.user_id === block.blocked_id) blocked_profile: profiles?.find(p => p.user_id === block.blocked_id)
})); }));
setBlockedUsers(blockedUsersWithProfiles); setBlockedUsers(blockedUsersWithProfiles);
} catch (error: any) {
console.error('Error fetching blocked users:', error); logger.info('Blocked users fetched successfully', {
toast({ userId: user.id,
title: 'Error', action: 'fetch_blocked_users',
description: 'Failed to load blocked users', count: blockedUsersWithProfiles.length
variant: 'destructive' });
} catch (error) {
logger.error('Error fetching blocked users', {
userId: user.id,
action: 'fetch_blocked_users',
error: error instanceof Error ? error.message : String(error)
});
handleError(error, {
action: 'Load blocked users',
userId: user.id
}); });
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const handleUnblock = async (blockId: string, username: string) => { const handleUnblock = async (blockId: string, blockedUserId: string, username: string) => {
if (!user) return;
try { try {
const { error } = await supabase const { error } = await supabase
.from('user_blocks') .from('user_blocks')
.delete() .delete()
.eq('id', blockId); .eq('id', blockId);
if (error) throw error; if (error) {
logger.error('Failed to unblock user', {
userId: user.id,
action: 'unblock_user',
targetUserId: blockedUserId,
error: error.message,
errorCode: error.code
});
throw error;
}
// Log to audit trail
await supabase.from('profile_audit_log').insert({
user_id: user.id,
changed_by: user.id,
action: 'user_unblocked',
changes: {
blocked_user_id: blockedUserId,
username,
timestamp: new Date().toISOString()
}
});
setBlockedUsers(prev => prev.filter(block => block.id !== blockId)); setBlockedUsers(prev => prev.filter(block => block.id !== blockId));
toast({
title: 'User unblocked', logger.info('User unblocked successfully', {
description: `You have unblocked @${username}` userId: user.id,
action: 'unblock_user',
targetUserId: blockedUserId
}); });
} catch (error: any) {
toast({ handleSuccess('User unblocked', `You have unblocked @${username}`);
title: 'Error', } catch (error) {
description: 'Failed to unblock user', logger.error('Error unblocking user', {
variant: 'destructive' userId: user.id,
action: 'unblock_user',
targetUserId: blockedUserId,
error: error instanceof Error ? error.message : String(error)
});
handleError(error, {
action: 'Unblock user',
userId: user.id,
metadata: { targetUsername: username }
}); });
} }
}; };
@@ -178,7 +227,11 @@ export function BlockedUsers() {
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
onClick={() => handleUnblock(block.id, block.blocked_profile?.username || 'user')} onClick={() => handleUnblock(
block.id,
block.blocked_id,
block.blocked_profile?.username || 'user'
)}
> >
Unblock Unblock
</AlertDialogAction> </AlertDialogAction>

View File

@@ -6,156 +6,227 @@ import { Switch } from '@/components/ui/switch';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { useToast } from '@/hooks/use-toast'; import { handleError, handleSuccess, AppError } from '@/lib/errorHandler';
import { logger } from '@/lib/logger';
import { useAuth } from '@/hooks/useAuth'; import { useAuth } from '@/hooks/useAuth';
import { useProfile } from '@/hooks/useProfile'; import { useProfile } from '@/hooks/useProfile';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { Eye, UserX, Shield, Search } from 'lucide-react'; import { Eye, UserX, Shield, Search } from 'lucide-react';
import { BlockedUsers } from '@/components/privacy/BlockedUsers'; import { BlockedUsers } from '@/components/privacy/BlockedUsers';
interface PrivacySettings { import type { PrivacySettings, PrivacyFormData } from '@/types/privacy';
activity_visibility: 'public' | 'private'; import { privacyFormSchema, privacySettingsSchema, DEFAULT_PRIVACY_SETTINGS } from '@/lib/privacyValidation';
search_visibility: boolean; import { z } from 'zod';
show_location: boolean;
show_age: boolean;
show_avatar: boolean;
show_bio: boolean;
show_activity_stats: boolean;
show_home_park: boolean;
}
interface ProfilePrivacy {
privacy_level: 'public' | 'private';
show_pronouns: boolean;
}
export function PrivacyTab() { export function PrivacyTab() {
const { user } = useAuth(); const { user } = useAuth();
const { data: profile, refreshProfile } = useProfile(user?.id); const { data: profile, refreshProfile } = useProfile(user?.id);
const { toast } = useToast();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [preferences, setPreferences] = useState<PrivacySettings | null>(null); const [preferences, setPreferences] = useState<PrivacySettings | null>(null);
const form = useForm<ProfilePrivacy & PrivacySettings>({
const form = useForm<PrivacyFormData>({
defaultValues: { defaultValues: {
privacy_level: (profile?.privacy_level === 'friends' ? 'public' : profile?.privacy_level) || 'public', privacy_level: profile?.privacy_level || 'public',
show_pronouns: profile?.show_pronouns || false, show_pronouns: profile?.show_pronouns || false,
activity_visibility: 'public', ...DEFAULT_PRIVACY_SETTINGS
search_visibility: true,
show_location: false,
show_age: false,
show_avatar: true,
show_bio: true,
show_activity_stats: true,
show_home_park: false
} }
}); });
useEffect(() => { useEffect(() => {
fetchPreferences(); fetchPreferences();
}, [user]); }, [user]);
const fetchPreferences = async () => { const fetchPreferences = async () => {
if (!user) return; if (!user) return;
try { try {
const { const { data, error } = await supabase
data, .from('user_preferences')
error .select('privacy_settings')
} = await supabase.from('user_preferences').select('privacy_settings').eq('user_id', user.id).maybeSingle(); .eq('user_id', user.id)
.maybeSingle();
if (error && error.code !== 'PGRST116') { if (error && error.code !== 'PGRST116') {
console.error('Error fetching preferences:', error); logger.error('Failed to fetch privacy preferences', {
return; userId: user.id,
action: 'fetch_privacy_preferences',
error: error.message,
errorCode: error.code
});
throw error;
} }
if (data?.privacy_settings) { if (data?.privacy_settings) {
const privacySettings = data.privacy_settings as any; // Validate the data before using it
setPreferences(privacySettings); const validatedSettings = privacySettingsSchema.parse(data.privacy_settings);
setPreferences(validatedSettings);
form.reset({ form.reset({
privacy_level: profile?.privacy_level === 'friends' ? 'public' : profile?.privacy_level || 'public', privacy_level: profile?.privacy_level || 'public',
show_pronouns: profile?.show_pronouns || false, show_pronouns: profile?.show_pronouns || false,
...privacySettings ...validatedSettings
}); });
} else { } else {
// Initialize preferences if they don't exist // Initialize preferences if they don't exist
await initializePreferences(); await initializePreferences();
} }
} catch (error) { } catch (error) {
console.error('Error fetching preferences:', error); logger.error('Error fetching privacy preferences', {
userId: user.id,
action: 'fetch_privacy_preferences',
error: error instanceof Error ? error.message : String(error)
});
handleError(error, {
action: 'Load privacy settings',
userId: user.id
});
} }
}; };
const initializePreferences = async () => { const initializePreferences = async () => {
if (!user) return; if (!user) return;
const defaultSettings: PrivacySettings = {
activity_visibility: 'public',
search_visibility: true,
show_location: false,
show_age: false,
show_avatar: true,
show_bio: true,
show_activity_stats: true,
show_home_park: false
};
try { try {
const { const { error } = await supabase
error .from('user_preferences')
} = await supabase.from('user_preferences').insert([{ .insert([{
user_id: user.id, user_id: user.id,
privacy_settings: defaultSettings as any privacy_settings: DEFAULT_PRIVACY_SETTINGS
}]); }]);
if (error) throw error;
setPreferences(defaultSettings); if (error) {
logger.error('Failed to initialize privacy preferences', {
userId: user.id,
action: 'initialize_privacy_preferences',
error: error.message
});
throw error;
}
setPreferences(DEFAULT_PRIVACY_SETTINGS);
form.reset({ form.reset({
privacy_level: (profile?.privacy_level === 'friends' ? 'public' : profile?.privacy_level) || 'public', privacy_level: profile?.privacy_level || 'public',
show_pronouns: profile?.show_pronouns || false, show_pronouns: profile?.show_pronouns || false,
...defaultSettings ...DEFAULT_PRIVACY_SETTINGS
}); });
} catch (error) { } catch (error) {
console.error('Error initializing preferences:', error); logger.error('Error initializing privacy preferences', {
userId: user.id,
action: 'initialize_privacy_preferences',
error: error instanceof Error ? error.message : String(error)
});
handleError(error, {
action: 'Initialize privacy settings',
userId: user.id
});
} }
}; };
const onSubmit = async (data: any) => {
const onSubmit = async (data: PrivacyFormData) => {
if (!user) return; if (!user) return;
setLoading(true); setLoading(true);
try { try {
// Validate the form data
const validated = privacyFormSchema.parse(data);
// Update profile privacy settings // Update profile privacy settings
const { const { error: profileError } = await supabase
error: profileError .from('profiles')
} = await supabase.from('profiles').update({ .update({
privacy_level: data.privacy_level, privacy_level: validated.privacy_level,
show_pronouns: data.show_pronouns, show_pronouns: validated.show_pronouns,
updated_at: new Date().toISOString() updated_at: new Date().toISOString()
}).eq('user_id', user.id); })
if (profileError) throw profileError; .eq('user_id', user.id);
if (profileError) {
logger.error('Failed to update profile privacy', {
userId: user.id,
action: 'update_profile_privacy',
error: profileError.message,
errorCode: profileError.code
});
throw profileError;
}
// Extract privacy settings (exclude profile fields)
const { privacy_level, show_pronouns, ...privacySettings } = validated;
// Update user preferences // Update user preferences
const privacySettings: PrivacySettings = { const { error: prefsError } = await supabase
activity_visibility: data.activity_visibility, .from('user_preferences')
search_visibility: data.search_visibility, .upsert([{
show_location: data.show_location,
show_age: data.show_age,
show_avatar: data.show_avatar,
show_bio: data.show_bio,
show_activity_stats: data.show_activity_stats,
show_home_park: data.show_home_park
};
const {
error: prefsError
} = await supabase.from('user_preferences').upsert([{
user_id: user.id, user_id: user.id,
privacy_settings: privacySettings as any, privacy_settings: privacySettings,
updated_at: new Date().toISOString() updated_at: new Date().toISOString()
}]); }]);
if (prefsError) throw prefsError;
if (prefsError) {
logger.error('Failed to update privacy preferences', {
userId: user.id,
action: 'update_privacy_preferences',
error: prefsError.message,
errorCode: prefsError.code
});
throw prefsError;
}
// Log to audit trail
await supabase.from('profile_audit_log').insert({
user_id: user.id,
changed_by: user.id,
action: 'privacy_settings_updated',
changes: {
previous: preferences,
updated: privacySettings,
timestamp: new Date().toISOString()
}
});
await refreshProfile(); await refreshProfile();
setPreferences(privacySettings); setPreferences(privacySettings);
toast({
title: 'Privacy settings updated', logger.info('Privacy settings updated successfully', {
description: 'Your privacy preferences have been successfully saved.' userId: user.id,
action: 'update_privacy_settings'
}); });
} catch (error: any) {
toast({ handleSuccess(
title: 'Error', 'Privacy settings updated',
description: error.message || 'Failed to update privacy settings', 'Your privacy preferences have been successfully saved.'
variant: 'destructive' );
} catch (error) {
logger.error('Failed to update privacy settings', {
userId: user.id,
action: 'update_privacy_settings',
error: error instanceof Error ? error.message : String(error)
}); });
if (error instanceof z.ZodError) {
handleError(
new AppError(
'Invalid privacy settings',
'VALIDATION_ERROR',
error.errors.map(e => e.message).join(', ')
),
{ action: 'Validate privacy settings', userId: user.id }
);
} else {
handleError(error, {
action: 'Update privacy settings',
userId: user.id
});
}
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
return <div className="space-y-8">
return (
<div className="space-y-8">
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
{/* Profile Visibility */} {/* Profile Visibility */}
<div className="space-y-4"> <div className="space-y-4">
@@ -173,7 +244,10 @@ export function PrivacyTab() {
<CardContent className="space-y-6"> <CardContent className="space-y-6">
<div className="space-y-3"> <div className="space-y-3">
<Label htmlFor="privacy_level">Profile Privacy</Label> <Label htmlFor="privacy_level">Profile Privacy</Label>
<Select value={form.watch('privacy_level')} onValueChange={(value: 'public' | 'private') => form.setValue('privacy_level', value)}> <Select
value={form.watch('privacy_level')}
onValueChange={(value: 'public' | 'private') => form.setValue('privacy_level', value)}
>
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
@@ -195,7 +269,10 @@ export function PrivacyTab() {
Display your preferred pronouns on your profile Display your preferred pronouns on your profile
</p> </p>
</div> </div>
<Switch checked={form.watch('show_pronouns')} onCheckedChange={checked => form.setValue('show_pronouns', checked)} /> <Switch
checked={form.watch('show_pronouns')}
onCheckedChange={checked => form.setValue('show_pronouns', checked)}
/>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -205,7 +282,10 @@ export function PrivacyTab() {
Display your location on your profile Display your location on your profile
</p> </p>
</div> </div>
<Switch checked={form.watch('show_location')} onCheckedChange={checked => form.setValue('show_location', checked)} /> <Switch
checked={form.watch('show_location')}
onCheckedChange={checked => form.setValue('show_location', checked)}
/>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -215,7 +295,10 @@ export function PrivacyTab() {
Display your birth date on your profile Display your birth date on your profile
</p> </p>
</div> </div>
<Switch checked={form.watch('show_age')} onCheckedChange={checked => form.setValue('show_age', checked)} /> <Switch
checked={form.watch('show_age')}
onCheckedChange={checked => form.setValue('show_age', checked)}
/>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -225,7 +308,10 @@ export function PrivacyTab() {
Display your profile picture Display your profile picture
</p> </p>
</div> </div>
<Switch checked={form.watch('show_avatar')} onCheckedChange={checked => form.setValue('show_avatar', checked)} /> <Switch
checked={form.watch('show_avatar')}
onCheckedChange={checked => form.setValue('show_avatar', checked)}
/>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -235,7 +321,10 @@ export function PrivacyTab() {
Display your profile bio/description Display your profile bio/description
</p> </p>
</div> </div>
<Switch checked={form.watch('show_bio')} onCheckedChange={checked => form.setValue('show_bio', checked)} /> <Switch
checked={form.watch('show_bio')}
onCheckedChange={checked => form.setValue('show_bio', checked)}
/>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -245,7 +334,10 @@ export function PrivacyTab() {
Display your ride counts and park visits Display your ride counts and park visits
</p> </p>
</div> </div>
<Switch checked={form.watch('show_activity_stats')} onCheckedChange={checked => form.setValue('show_activity_stats', checked)} /> <Switch
checked={form.watch('show_activity_stats')}
onCheckedChange={checked => form.setValue('show_activity_stats', checked)}
/>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -255,7 +347,10 @@ export function PrivacyTab() {
Display your home park preference Display your home park preference
</p> </p>
</div> </div>
<Switch checked={form.watch('show_home_park')} onCheckedChange={checked => form.setValue('show_home_park', checked)} /> <Switch
checked={form.watch('show_home_park')}
onCheckedChange={checked => form.setValue('show_home_park', checked)}
/>
</div> </div>
</CardContent> </CardContent>
@@ -280,7 +375,10 @@ export function PrivacyTab() {
<CardContent className="space-y-6"> <CardContent className="space-y-6">
<div className="space-y-3"> <div className="space-y-3">
<Label htmlFor="activity_visibility">Activity Visibility</Label> <Label htmlFor="activity_visibility">Activity Visibility</Label>
<Select value={form.watch('activity_visibility')} onValueChange={(value: 'public' | 'private') => form.setValue('activity_visibility', value)}> <Select
value={form.watch('activity_visibility')}
onValueChange={(value: 'public' | 'private') => form.setValue('activity_visibility', value)}
>
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
@@ -324,7 +422,10 @@ export function PrivacyTab() {
Allow your profile to appear in search results Allow your profile to appear in search results
</p> </p>
</div> </div>
<Switch checked={form.watch('search_visibility')} onCheckedChange={checked => form.setValue('search_visibility', checked)} /> <Switch
checked={form.watch('search_visibility')}
onCheckedChange={checked => form.setValue('search_visibility', checked)}
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -358,5 +459,6 @@ export function PrivacyTab() {
</Button> </Button>
</div> </div>
</form> </form>
</div>; </div>
);
} }

View File

@@ -0,0 +1,70 @@
/**
* Privacy Settings Validation
*
* Provides Zod schemas for runtime validation of privacy settings.
*
* Usage:
* ```typescript
* const validated = privacyFormSchema.parse(userInput);
* ```
*
* Security:
* - All user inputs must be validated before database writes
* - Prevents injection attacks and data corruption
* - Ensures data integrity with type-safe validation
*/
import { z } from 'zod';
/**
* Schema for privacy settings in user_preferences
*/
export const privacySettingsSchema = z.object({
activity_visibility: z.enum(['public', 'private'], {
errorMap: () => ({ message: 'Activity visibility must be public or private' })
}),
search_visibility: z.boolean(),
show_location: z.boolean(),
show_age: z.boolean(),
show_avatar: z.boolean(),
show_bio: z.boolean(),
show_activity_stats: z.boolean(),
show_home_park: z.boolean()
});
/**
* Schema for profile privacy settings
*/
export const profilePrivacySchema = z.object({
privacy_level: z.enum(['public', 'private'], {
errorMap: () => ({ message: 'Privacy level must be public or private' })
}),
show_pronouns: z.boolean()
});
/**
* Combined schema for privacy form
*/
export const privacyFormSchema = privacySettingsSchema.merge(profilePrivacySchema);
/**
* Schema for blocking a user
*/
export const blockUserSchema = z.object({
blocked_id: z.string().uuid('Invalid user ID'),
reason: z.string().max(500, 'Reason must be 500 characters or less').optional()
});
/**
* Default privacy settings for new users
*/
export const DEFAULT_PRIVACY_SETTINGS = {
activity_visibility: 'public' as const,
search_visibility: true,
show_location: false,
show_age: false,
show_avatar: true,
show_bio: true,
show_activity_stats: true,
show_home_park: false
};

58
src/types/privacy.ts Normal file
View File

@@ -0,0 +1,58 @@
/**
* Privacy Types
*
* Centralized type definitions for user privacy settings.
*
* Storage:
* - ProfilePrivacySettings: stored in profiles table
* - PrivacySettings: stored in user_preferences.privacy_settings (JSONB)
*
* Security:
* - All privacy settings are validated with Zod before database writes
* - Changes are logged to profile_audit_log for compliance
* - RLS policies ensure users can only modify their own settings
*/
/**
* Privacy settings stored in user_preferences.privacy_settings
*/
export interface PrivacySettings {
activity_visibility: 'public' | 'private';
search_visibility: boolean;
show_location: boolean;
show_age: boolean;
show_avatar: boolean;
show_bio: boolean;
show_activity_stats: boolean;
show_home_park: boolean;
}
/**
* Profile-level privacy settings
*/
export interface ProfilePrivacySettings {
privacy_level: 'public' | 'private';
show_pronouns: boolean;
}
/**
* Combined form data for privacy tab
*/
export interface PrivacyFormData extends ProfilePrivacySettings, PrivacySettings {}
/**
* User block information
*/
export interface UserBlock {
id: string;
blocker_id: string;
blocked_id: string;
reason?: string;
created_at: string;
blocked_profile?: {
user_id: string;
username: string;
display_name?: string;
avatar_url?: string;
};
}