feat: Implement complete API optimization plan

This commit is contained in:
gpt-engineer-app[bot]
2025-10-31 12:28:24 +00:00
parent 631ce9c89e
commit ca9aa757ae
9 changed files with 363 additions and 333 deletions

View File

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

View File

@@ -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
});
// Update Novu subscriber if username changed (before mutation for optimistic update)
const usernameChanged = data.username !== profile?.username;
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.'}`
);
updateProfileMutation.mutate({
userId: user.id,
updates: {
username: data.username,
display_name: data.display_name || null,
bio: data.bio || null
}
throw error;
}
// 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()) {
}, {
onSuccess: async () => {
if (usernameChanged && 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>

View File

@@ -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(),
}
}).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.'
);
// Step 3: Update email address using mutation hook
changeEmail.mutate(
{ newEmail: data.newEmail, currentEmail, userId },
{
onSuccess: () => {
setStep('success');
},
onError: (error) => {
throw error;
}
}
);
} catch (error: unknown) {
const errorMsg = getErrorMessage(error);
logger.error('Email change failed', {

View File

@@ -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,52 +289,10 @@ 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
}
});
// Step 4: Send security notification
try {
await invokeWithTracking(
'trigger-notification',
updatePassword.mutate(
{ password, hasMFA, userId },
{
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
}
}
onSuccess: () => {
setStep('success');
form.reset();
@@ -344,6 +303,12 @@ export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: Password
setStep('password');
setTotpCode('');
}, 2000);
},
onError: (error) => {
throw error;
}
}
);
} catch (error: unknown) {
throw error;
}

View File

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

View File

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

View 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,
};
}

View 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,
};
}

View 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,
};
}