mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 09:31:13 -05:00
feat: Implement complete API optimization plan
This commit is contained in:
@@ -1,153 +1,18 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
|
||||
import { UserX, Trash2 } from 'lucide-react';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { handleError, handleSuccess } from '@/lib/errorHandler';
|
||||
import { logger } from '@/lib/logger';
|
||||
import type { UserBlock } from '@/types/privacy';
|
||||
import { useBlockedUsers } from '@/hooks/privacy/useBlockedUsers';
|
||||
import { useBlockUserMutation } from '@/hooks/privacy/useBlockUserMutation';
|
||||
|
||||
export function BlockedUsers() {
|
||||
const { user } = useAuth();
|
||||
const [blockedUsers, setBlockedUsers] = useState<UserBlock[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { data: blockedUsers = [], isLoading: loading } = useBlockedUsers(user?.id);
|
||||
const { unblockUser, isUnblocking } = useBlockUserMutation();
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
fetchBlockedUsers();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const fetchBlockedUsers = async () => {
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
// First get the blocked user IDs
|
||||
const { data: blocks, error: blocksError } = await supabase
|
||||
.from('user_blocks')
|
||||
.select('id, blocked_id, reason, created_at')
|
||||
.eq('blocker_id', user.id)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
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) {
|
||||
setBlockedUsers([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Then get the profile information for blocked users
|
||||
const blockedIds = blocks.map(b => b.blocked_id);
|
||||
const { data: profiles, error: profilesError } = await supabase
|
||||
.from('profiles')
|
||||
.select('user_id, username, display_name, avatar_url')
|
||||
.in('user_id', blockedIds);
|
||||
|
||||
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
|
||||
const blockedUsersWithProfiles = blocks.map(block => ({
|
||||
...block,
|
||||
blocker_id: user.id,
|
||||
blocked_profile: profiles?.find(p => p.user_id === block.blocked_id)
|
||||
}));
|
||||
|
||||
setBlockedUsers(blockedUsersWithProfiles);
|
||||
|
||||
logger.info('Blocked users fetched successfully', {
|
||||
userId: user.id,
|
||||
action: 'fetch_blocked_users',
|
||||
count: blockedUsersWithProfiles.length
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
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 {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnblock = async (blockId: string, blockedUserId: string, username: string) => {
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('user_blocks')
|
||||
.delete()
|
||||
.eq('id', blockId);
|
||||
|
||||
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: JSON.parse(JSON.stringify({
|
||||
blocked_user_id: blockedUserId,
|
||||
username,
|
||||
timestamp: new Date().toISOString()
|
||||
}))
|
||||
}]);
|
||||
|
||||
setBlockedUsers(prev => prev.filter(block => block.id !== blockId));
|
||||
|
||||
logger.info('User unblocked successfully', {
|
||||
userId: user.id,
|
||||
action: 'unblock_user',
|
||||
targetUserId: blockedUserId
|
||||
});
|
||||
|
||||
handleSuccess('User unblocked', `You have unblocked @${username}`);
|
||||
} catch (error: unknown) {
|
||||
logger.error('Error unblocking user', {
|
||||
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 }
|
||||
});
|
||||
}
|
||||
const handleUnblock = (blockId: string, blockedUserId: string, username: string) => {
|
||||
unblockUser.mutate({ blockId, blockedUserId, username });
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
@@ -211,7 +76,7 @@ export function BlockedUsers() {
|
||||
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<Button variant="outline" size="sm" disabled={isUnblocking}>
|
||||
<Trash2 className="w-4 h-4 mr-1" />
|
||||
Unblock
|
||||
</Button>
|
||||
|
||||
@@ -28,6 +28,7 @@ import { handleError, handleSuccess, AppError } from '@/lib/errorHandler';
|
||||
import { useAvatarUpload } from '@/hooks/useAvatarUpload';
|
||||
import { useUsernameValidation } from '@/hooks/useUsernameValidation';
|
||||
import { useAutoSave } from '@/hooks/useAutoSave';
|
||||
import { useProfileUpdateMutation } from '@/hooks/profile/useProfileUpdateMutation';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
@@ -42,7 +43,7 @@ type ProfileFormData = z.infer<typeof profileSchema>;
|
||||
export function AccountProfileTab() {
|
||||
const { user, pendingEmail, clearPendingEmail } = useAuth();
|
||||
const { data: profile, refreshProfile } = useProfile(user?.id);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const updateProfileMutation = useProfileUpdateMutation();
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [showEmailDialog, setShowEmailDialog] = useState(false);
|
||||
const [showCancelEmailDialog, setShowCancelEmailDialog] = useState(false);
|
||||
@@ -107,47 +108,28 @@ export function AccountProfileTab() {
|
||||
const handleFormSubmit = async (data: ProfileFormData) => {
|
||||
if (!user) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// Use the update_profile RPC function with server-side validation
|
||||
const { data: result, error } = await supabase.rpc('update_profile', {
|
||||
p_username: data.username,
|
||||
p_display_name: data.display_name || null,
|
||||
p_bio: data.bio || null
|
||||
});
|
||||
|
||||
if (error) {
|
||||
// Handle rate limiting error
|
||||
if (error.code === 'P0001') {
|
||||
const resetTime = error.message.match(/Try again at (.+)$/)?.[1];
|
||||
throw new AppError(
|
||||
error.message,
|
||||
'RATE_LIMIT',
|
||||
`Too many profile updates. ${resetTime ? 'Try again at ' + new Date(resetTime).toLocaleTimeString() : 'Please wait a few minutes.'}`
|
||||
);
|
||||
// Update Novu subscriber if username changed (before mutation for optimistic update)
|
||||
const usernameChanged = data.username !== profile?.username;
|
||||
|
||||
updateProfileMutation.mutate({
|
||||
userId: user.id,
|
||||
updates: {
|
||||
username: data.username,
|
||||
display_name: data.display_name || null,
|
||||
bio: data.bio || null
|
||||
}
|
||||
}, {
|
||||
onSuccess: async () => {
|
||||
if (usernameChanged && notificationService.isEnabled()) {
|
||||
await notificationService.updateSubscriber({
|
||||
subscriberId: user.id,
|
||||
email: user.email,
|
||||
firstName: data.username,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
await refreshProfile();
|
||||
}
|
||||
|
||||
// Type the RPC result
|
||||
const rpcResult = result as unknown as { success: boolean; changes_count: number };
|
||||
|
||||
// Update Novu subscriber if username changed
|
||||
if (rpcResult?.changes_count > 0 && notificationService.isEnabled()) {
|
||||
await notificationService.updateSubscriber({
|
||||
subscriberId: user.id,
|
||||
email: user.email,
|
||||
firstName: data.username,
|
||||
});
|
||||
}
|
||||
|
||||
await refreshProfile();
|
||||
handleSuccess('Profile updated', 'Your profile has been successfully updated.');
|
||||
} catch (error: unknown) {
|
||||
handleError(error, { action: 'Update profile', userId: user.id });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = async (data: ProfileFormData) => {
|
||||
@@ -400,17 +382,17 @@ export function AccountProfileTab() {
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={
|
||||
loading ||
|
||||
updateProfileMutation.isPending ||
|
||||
isDeactivated ||
|
||||
isSaving ||
|
||||
usernameValidation.isChecking ||
|
||||
usernameValidation.isAvailable === false
|
||||
}
|
||||
>
|
||||
{loading || isSaving ? 'Saving...' : 'Save Changes'}
|
||||
{updateProfileMutation.isPending || isSaving ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
|
||||
{lastSaved && !loading && !isSaving && (
|
||||
{lastSaved && !updateProfileMutation.isPending && !isSaving && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Last saved {formatDistanceToNow(lastSaved, { addSuffix: true })}
|
||||
</span>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { useEmailChangeMutation } from '@/hooks/security/useEmailChangeMutation';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -52,6 +53,7 @@ interface EmailChangeDialogProps {
|
||||
|
||||
export function EmailChangeDialog({ open, onOpenChange, currentEmail, userId }: EmailChangeDialogProps) {
|
||||
const { theme } = useTheme();
|
||||
const { changeEmail, isChanging } = useEmailChangeMutation();
|
||||
const [step, setStep] = useState<Step>('verification');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [captchaToken, setCaptchaToken] = useState<string>('');
|
||||
@@ -156,63 +158,18 @@ export function EmailChangeDialog({ open, onOpenChange, currentEmail, userId }:
|
||||
throw signInError;
|
||||
}
|
||||
|
||||
// Step 3: Update email address
|
||||
// Supabase will send verification emails to both old and new addresses
|
||||
const { error: updateError } = await supabase.auth.updateUser({
|
||||
email: data.newEmail
|
||||
});
|
||||
|
||||
if (updateError) throw updateError;
|
||||
|
||||
// Step 4: Novu subscriber will be updated automatically after both emails are confirmed
|
||||
// This happens in the useAuth hook when the email change is fully verified
|
||||
|
||||
// Step 5: Log the email change attempt
|
||||
supabase.from('admin_audit_log').insert({
|
||||
admin_user_id: userId,
|
||||
target_user_id: userId,
|
||||
action: 'email_change_initiated',
|
||||
details: {
|
||||
old_email: currentEmail,
|
||||
new_email: data.newEmail,
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
}).then(({ error }) => {
|
||||
if (error) {
|
||||
logger.error('Failed to log email change', {
|
||||
userId,
|
||||
action: 'email_change_audit_log',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Step 6: Send security notifications (non-blocking)
|
||||
if (notificationService.isEnabled()) {
|
||||
notificationService.trigger({
|
||||
workflowId: 'security-alert',
|
||||
subscriberId: userId,
|
||||
payload: {
|
||||
alert_type: 'email_change_initiated',
|
||||
old_email: currentEmail,
|
||||
new_email: data.newEmail,
|
||||
timestamp: new Date().toISOString(),
|
||||
// Step 3: Update email address using mutation hook
|
||||
changeEmail.mutate(
|
||||
{ newEmail: data.newEmail, currentEmail, userId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setStep('success');
|
||||
},
|
||||
onError: (error) => {
|
||||
throw error;
|
||||
}
|
||||
}).catch(error => {
|
||||
logger.error('Failed to send security notification', {
|
||||
userId,
|
||||
action: 'email_change_notification',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handleSuccess(
|
||||
'Email change initiated',
|
||||
'Check both email addresses for confirmation links.'
|
||||
}
|
||||
);
|
||||
|
||||
setStep('success');
|
||||
} catch (error: unknown) {
|
||||
const errorMsg = getErrorMessage(error);
|
||||
logger.error('Email change failed', {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { invokeWithTracking } from '@/lib/edgeFunctionTracking';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { usePasswordUpdateMutation } from '@/hooks/security/usePasswordUpdateMutation';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -45,6 +45,7 @@ function isErrorWithCode(error: unknown): error is Error & ErrorWithCode {
|
||||
|
||||
export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: PasswordUpdateDialogProps) {
|
||||
const { theme } = useTheme();
|
||||
const { updatePassword, isUpdating } = usePasswordUpdateMutation();
|
||||
const [step, setStep] = useState<Step>('password');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [nonce, setNonce] = useState<string>('');
|
||||
@@ -288,62 +289,26 @@ export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: Password
|
||||
|
||||
const updatePasswordWithNonce = async (password: string, nonceValue: string) => {
|
||||
try {
|
||||
// Step 2: Update password
|
||||
const { error: updateError } = await supabase.auth.updateUser({
|
||||
password
|
||||
});
|
||||
|
||||
if (updateError) throw updateError;
|
||||
|
||||
// Step 3: Log audit trail
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (user) {
|
||||
await supabase.from('admin_audit_log').insert({
|
||||
admin_user_id: user.id,
|
||||
target_user_id: user.id,
|
||||
action: 'password_changed',
|
||||
details: {
|
||||
timestamp: new Date().toISOString(),
|
||||
method: hasMFA ? 'password_with_mfa' : 'password_only',
|
||||
user_agent: navigator.userAgent
|
||||
updatePassword.mutate(
|
||||
{ password, hasMFA, userId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setStep('success');
|
||||
form.reset();
|
||||
|
||||
// Auto-close after 2 seconds
|
||||
setTimeout(() => {
|
||||
onOpenChange(false);
|
||||
onSuccess();
|
||||
setStep('password');
|
||||
setTotpCode('');
|
||||
}, 2000);
|
||||
},
|
||||
onError: (error) => {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// Step 4: Send security notification
|
||||
try {
|
||||
await invokeWithTracking(
|
||||
'trigger-notification',
|
||||
{
|
||||
workflowId: 'security-alert',
|
||||
subscriberId: user.id,
|
||||
payload: {
|
||||
alert_type: 'password_changed',
|
||||
timestamp: new Date().toISOString(),
|
||||
device: navigator.userAgent.split(' ')[0]
|
||||
}
|
||||
},
|
||||
user.id
|
||||
);
|
||||
} catch (notifError) {
|
||||
logger.error('Failed to send password change notification', {
|
||||
userId: user!.id,
|
||||
action: 'password_change_notification',
|
||||
error: getErrorMessage(notifError)
|
||||
});
|
||||
// Don't fail the password update if notification fails
|
||||
}
|
||||
}
|
||||
|
||||
setStep('success');
|
||||
form.reset();
|
||||
|
||||
// Auto-close after 2 seconds
|
||||
setTimeout(() => {
|
||||
onOpenChange(false);
|
||||
onSuccess();
|
||||
setStep('password');
|
||||
setTotpCode('');
|
||||
}, 2000);
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -207,12 +207,14 @@ const handleUpdate = () => {
|
||||
## Component Migration Status
|
||||
|
||||
### ✅ Migrated Components
|
||||
- `SecurityTab.tsx` - Using `useSecurityMutations()`
|
||||
- `ReportsQueue.tsx` - Using `useReportActionMutation()`
|
||||
- `PrivacyTab.tsx` - Using `usePrivacyMutations()`
|
||||
- `LocationTab.tsx` - Using `useProfileLocationMutation()`
|
||||
- `AccountProfileTab.tsx` - Using `useProfileUpdateMutation()`
|
||||
- `BlockedUsers.tsx` - Using `useBlockUserMutation()`
|
||||
- `SecurityTab.tsx` - Using `useSecurityMutations()` ✅
|
||||
- `ReportsQueue.tsx` - Using `useReportActionMutation()` ✅
|
||||
- `PrivacyTab.tsx` - Using `usePrivacyMutations()` ✅
|
||||
- `LocationTab.tsx` - Using `useProfileLocationMutation()` ✅
|
||||
- `AccountProfileTab.tsx` - Using `useProfileUpdateMutation()` ✅
|
||||
- `BlockedUsers.tsx` - Using `useBlockUserMutation()` and `useBlockedUsers()` ✅
|
||||
- `PasswordUpdateDialog.tsx` - Using `usePasswordUpdateMutation()` ✅
|
||||
- `EmailChangeDialog.tsx` - Using `useEmailChangeMutation()` ✅
|
||||
|
||||
### 📊 Impact
|
||||
- **100%** of settings mutations now use mutation hooks
|
||||
@@ -220,7 +222,11 @@ const handleUpdate = () => {
|
||||
- **30%** faster perceived load times (optimistic updates)
|
||||
- **10%** fewer API calls (better cache invalidation)
|
||||
- **Zero** manual cache invalidation in components
|
||||
- **Zero** direct Supabase mutations in components
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
When migrating a component:
|
||||
- [ ] Create custom mutation hook in appropriate directory
|
||||
- [ ] Use `useMutation` instead of direct Supabase calls
|
||||
- [ ] Implement `onError` callback with toast notifications
|
||||
@@ -232,27 +238,42 @@ const handleUpdate = () => {
|
||||
- [ ] Test optimistic updates if applicable
|
||||
- [ ] Add audit log creation where appropriate
|
||||
- [ ] Ensure proper type safety with TypeScript
|
||||
- [ ] Consider creating query hooks for data fetching instead of manual `useEffect`
|
||||
|
||||
## Available Mutation Hooks
|
||||
|
||||
### Profile & User Management
|
||||
- **`useProfileUpdateMutation`** - Profile updates (username, display name, bio, avatar)
|
||||
- Modifies: `profiles` table via `update_profile` RPC
|
||||
- Invalidates: profile, profile stats, profile activity, user search (if display name/username changed)
|
||||
- Features: Optimistic updates, automatic rollback
|
||||
- Features: Optimistic updates, automatic rollback, rate limiting, Novu sync
|
||||
|
||||
- **`useProfileLocationMutation`** - Location and personal info updates
|
||||
- Modifies: `profiles` table and `user_preferences` table
|
||||
- Invalidates: profile, profile stats, audit logs
|
||||
- Features: Optimistic updates, automatic rollback
|
||||
- Features: Optimistic updates, automatic rollback, audit logging
|
||||
|
||||
- **`usePrivacyMutations`** - Privacy settings updates
|
||||
- Modifies: `profiles` table and `user_preferences` table
|
||||
- Invalidates: profile, audit logs, user search (privacy affects visibility)
|
||||
- Features: Optimistic updates, automatic rollback
|
||||
- Features: Optimistic updates, automatic rollback, audit logging
|
||||
|
||||
### Security
|
||||
- **`useSecurityMutations`** - Session management
|
||||
- `revokeSession` - Revoke user sessions with automatic redirect for current session
|
||||
- Modifies: User sessions via `revoke_my_session` RPC
|
||||
- Invalidates: sessions list, audit logs
|
||||
|
||||
- **`usePasswordUpdateMutation`** - Password updates
|
||||
- Modifies: User password via Supabase Auth
|
||||
- Invalidates: audit logs
|
||||
- Features: MFA verification, audit logging, security notifications
|
||||
|
||||
- **`useEmailChangeMutation`** - Email address changes
|
||||
- Modifies: User email via Supabase Auth
|
||||
- Invalidates: audit logs
|
||||
- Features: Dual verification emails, audit logging, security notifications
|
||||
|
||||
### Moderation
|
||||
- **`useReportMutation`** - Submit user reports
|
||||
- Invalidates: moderation queue, moderation stats
|
||||
@@ -263,27 +284,27 @@ const handleUpdate = () => {
|
||||
|
||||
### Privacy & Blocking
|
||||
- **`useBlockUserMutation`** - Block/unblock users
|
||||
- Modifies: `user_blocks` table
|
||||
- Invalidates: blocked users list, audit logs
|
||||
- Features: Automatic audit logging
|
||||
|
||||
### Ride Credits
|
||||
- **`useRideCreditsMutation`** - Reorder ride credits
|
||||
- Modifies: User ride credits via `reorder_ride_credit` RPC
|
||||
- Invalidates: ride credits cache
|
||||
- Features: Optimistic drag-drop updates
|
||||
|
||||
### Admin
|
||||
- **`useAuditLogs`** - Query audit logs with pagination and filtering
|
||||
- Features: 2-minute stale time, disabled window focus refetch
|
||||
|
||||
### Profile & User Management
|
||||
- `useProfileUpdateMutation` - Profile updates (username, display name, bio)
|
||||
- `useProfileLocationMutation` - Location and personal info updates
|
||||
- `usePrivacyMutations` - Privacy settings updates
|
||||
## Query Hooks
|
||||
|
||||
### Security
|
||||
- `useSecurityMutations` - Session management (revoke sessions)
|
||||
|
||||
### Moderation
|
||||
- `useReportMutation` - Submit user reports
|
||||
- `useReportActionMutation` - Resolve/dismiss reports
|
||||
|
||||
### Admin
|
||||
- `useAuditLogs` - Query audit logs with pagination
|
||||
### Privacy
|
||||
- **`useBlockedUsers`** - Fetch blocked users for the authenticated user
|
||||
- Queries: `user_blocks` and `profiles` tables
|
||||
- Features: Automatic caching, refetch on window focus, 5-minute stale time
|
||||
- Returns: Array of blocked users with profile information
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,52 +1,72 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { logger } from '@/lib/logger';
|
||||
import type { UserBlock } from '@/types/privacy';
|
||||
|
||||
/**
|
||||
* Hook for querying blocked users
|
||||
* Provides: list of blocked users with profile information
|
||||
* Hook to fetch blocked users for the authenticated user
|
||||
* Provides: automatic caching, refetch on window focus, and loading states
|
||||
*/
|
||||
export function useBlockedUsers() {
|
||||
const { user } = useAuth();
|
||||
|
||||
export function useBlockedUsers(userId?: string) {
|
||||
return useQuery({
|
||||
queryKey: ['blocked-users', user?.id],
|
||||
queryKey: ['blocked-users', userId],
|
||||
queryFn: async () => {
|
||||
if (!user) return [];
|
||||
if (!userId) throw new Error('User ID required');
|
||||
|
||||
// First get the blocked user IDs
|
||||
// Fetch blocked user IDs
|
||||
const { data: blocks, error: blocksError } = await supabase
|
||||
.from('user_blocks')
|
||||
.select('id, blocked_id, reason, created_at')
|
||||
.eq('blocker_id', user.id)
|
||||
.eq('blocker_id', userId)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (blocksError) throw blocksError;
|
||||
if (blocksError) {
|
||||
logger.error('Failed to fetch user blocks', {
|
||||
userId,
|
||||
action: 'fetch_blocked_users',
|
||||
error: blocksError.message,
|
||||
errorCode: blocksError.code
|
||||
});
|
||||
throw blocksError;
|
||||
}
|
||||
|
||||
if (!blocks || blocks.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Then get the profile information for blocked users
|
||||
// Fetch profile information for blocked users
|
||||
const blockedIds = blocks.map(b => b.blocked_id);
|
||||
const { data: profiles, error: profilesError } = await supabase
|
||||
.from('profiles')
|
||||
.select('user_id, username, display_name, avatar_url')
|
||||
.in('user_id', blockedIds);
|
||||
|
||||
if (profilesError) throw profilesError;
|
||||
if (profilesError) {
|
||||
logger.error('Failed to fetch blocked user profiles', {
|
||||
userId,
|
||||
action: 'fetch_blocked_user_profiles',
|
||||
error: profilesError.message,
|
||||
errorCode: profilesError.code
|
||||
});
|
||||
throw profilesError;
|
||||
}
|
||||
|
||||
// Combine the data
|
||||
const blockedUsersWithProfiles: UserBlock[] = blocks.map(block => ({
|
||||
...block,
|
||||
blocker_id: user.id,
|
||||
blocker_id: userId,
|
||||
blocked_profile: profiles?.find(p => p.user_id === block.blocked_id)
|
||||
}));
|
||||
|
||||
logger.info('Blocked users fetched successfully', {
|
||||
userId,
|
||||
action: 'fetch_blocked_users',
|
||||
count: blockedUsersWithProfiles.length
|
||||
});
|
||||
|
||||
return blockedUsersWithProfiles;
|
||||
},
|
||||
enabled: !!user,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
enabled: !!userId,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
50
src/hooks/rides/useRideCreditsMutation.ts
Normal file
50
src/hooks/rides/useRideCreditsMutation.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { toast } from 'sonner';
|
||||
import { getErrorMessage } from '@/lib/errorHandler';
|
||||
import { useQueryInvalidation } from '@/lib/queryInvalidation';
|
||||
|
||||
interface ReorderCreditParams {
|
||||
creditId: string;
|
||||
newPosition: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for ride credits mutations
|
||||
* Provides: reorder ride credits with automatic cache invalidation
|
||||
*/
|
||||
export function useRideCreditsMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
const { invalidateRideDetail } = useQueryInvalidation();
|
||||
|
||||
const reorderCredit = useMutation({
|
||||
mutationFn: async ({ creditId, newPosition }: ReorderCreditParams) => {
|
||||
const { error } = await supabase.rpc('reorder_ride_credit', {
|
||||
p_credit_id: creditId,
|
||||
p_new_position: newPosition
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return { creditId, newPosition };
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
toast.error("Reorder Failed", {
|
||||
description: getErrorMessage(error),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate ride credits queries
|
||||
queryClient.invalidateQueries({ queryKey: ['ride-credits'] });
|
||||
|
||||
toast.success("Order Updated", {
|
||||
description: "Ride credit order has been saved.",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
reorderCredit,
|
||||
isReordering: reorderCredit.isPending,
|
||||
};
|
||||
}
|
||||
83
src/hooks/security/useEmailChangeMutation.ts
Normal file
83
src/hooks/security/useEmailChangeMutation.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { toast } from 'sonner';
|
||||
import { getErrorMessage } from '@/lib/errorHandler';
|
||||
import { useQueryInvalidation } from '@/lib/queryInvalidation';
|
||||
import { notificationService } from '@/lib/notificationService';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
interface EmailChangeParams {
|
||||
newEmail: string;
|
||||
currentEmail: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for email change mutations
|
||||
* Provides: email changes with automatic audit logging and cache invalidation
|
||||
*/
|
||||
export function useEmailChangeMutation() {
|
||||
const { invalidateAuditLogs } = useQueryInvalidation();
|
||||
|
||||
const changeEmail = useMutation({
|
||||
mutationFn: async ({ newEmail, currentEmail, userId }: EmailChangeParams) => {
|
||||
// Update email address
|
||||
const { error: updateError } = await supabase.auth.updateUser({
|
||||
email: newEmail
|
||||
});
|
||||
|
||||
if (updateError) throw updateError;
|
||||
|
||||
// Log the email change attempt
|
||||
await supabase.from('admin_audit_log').insert({
|
||||
admin_user_id: userId,
|
||||
target_user_id: userId,
|
||||
action: 'email_change_initiated',
|
||||
details: {
|
||||
old_email: currentEmail,
|
||||
new_email: newEmail,
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
});
|
||||
|
||||
// Send security notifications (non-blocking)
|
||||
if (notificationService.isEnabled()) {
|
||||
notificationService.trigger({
|
||||
workflowId: 'security-alert',
|
||||
subscriberId: userId,
|
||||
payload: {
|
||||
alert_type: 'email_change_initiated',
|
||||
old_email: currentEmail,
|
||||
new_email: newEmail,
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
}).catch(error => {
|
||||
logger.error('Failed to send security notification', {
|
||||
userId,
|
||||
action: 'email_change_notification',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return { newEmail };
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
toast.error("Update Failed", {
|
||||
description: getErrorMessage(error),
|
||||
});
|
||||
},
|
||||
onSuccess: (_data, { userId }) => {
|
||||
invalidateAuditLogs(userId);
|
||||
|
||||
toast.success("Email Change Initiated", {
|
||||
description: "Check both email addresses for confirmation links.",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
changeEmail,
|
||||
isChanging: changeEmail.isPending,
|
||||
};
|
||||
}
|
||||
87
src/hooks/security/usePasswordUpdateMutation.ts
Normal file
87
src/hooks/security/usePasswordUpdateMutation.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { toast } from 'sonner';
|
||||
import { getErrorMessage } from '@/lib/errorHandler';
|
||||
import { useQueryInvalidation } from '@/lib/queryInvalidation';
|
||||
import { invokeWithTracking } from '@/lib/edgeFunctionTracking';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
interface PasswordUpdateParams {
|
||||
password: string;
|
||||
hasMFA: boolean;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for password update mutations
|
||||
* Provides: password updates with automatic audit logging and cache invalidation
|
||||
*/
|
||||
export function usePasswordUpdateMutation() {
|
||||
const { invalidateAuditLogs } = useQueryInvalidation();
|
||||
|
||||
const updatePassword = useMutation({
|
||||
mutationFn: async ({ password, hasMFA, userId }: PasswordUpdateParams) => {
|
||||
// Update password
|
||||
const { error: updateError } = await supabase.auth.updateUser({
|
||||
password
|
||||
});
|
||||
|
||||
if (updateError) throw updateError;
|
||||
|
||||
// Log audit trail
|
||||
await supabase.from('admin_audit_log').insert({
|
||||
admin_user_id: userId,
|
||||
target_user_id: userId,
|
||||
action: 'password_changed',
|
||||
details: {
|
||||
timestamp: new Date().toISOString(),
|
||||
method: hasMFA ? 'password_with_mfa' : 'password_only',
|
||||
user_agent: navigator.userAgent
|
||||
}
|
||||
});
|
||||
|
||||
// Send security notification (non-blocking)
|
||||
try {
|
||||
await invokeWithTracking(
|
||||
'trigger-notification',
|
||||
{
|
||||
workflowId: 'security-alert',
|
||||
subscriberId: userId,
|
||||
payload: {
|
||||
alert_type: 'password_changed',
|
||||
timestamp: new Date().toISOString(),
|
||||
device: navigator.userAgent.split(' ')[0]
|
||||
}
|
||||
},
|
||||
userId
|
||||
);
|
||||
} catch (notifError) {
|
||||
logger.error('Failed to send password change notification', {
|
||||
userId,
|
||||
action: 'password_change_notification',
|
||||
error: getErrorMessage(notifError)
|
||||
});
|
||||
// Don't fail the password update if notification fails
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
toast.error("Update Failed", {
|
||||
description: getErrorMessage(error),
|
||||
});
|
||||
},
|
||||
onSuccess: (_data, { userId }) => {
|
||||
invalidateAuditLogs(userId);
|
||||
|
||||
toast.success("Password Updated", {
|
||||
description: "Your password has been successfully changed.",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
updatePassword,
|
||||
isUpdating: updatePassword.isPending,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user