Refactor CRUD Users

This commit is contained in:
vergiLgood1 2025-03-23 09:43:15 +07:00
parent 179ddf6999
commit e84a6f52c0
21 changed files with 891 additions and 280 deletions

View File

@ -39,122 +39,134 @@ import {
} from "@/app/(pages)/(admin)/dashboard/user-management/action";
import { format } from "date-fns";
import { sendMagicLink, sendPasswordRecovery } from "@/app/(pages)/(auth)/action";
import { useUserDetailSheetHandlers } from "../handler";
import { IUserSchema } from "@/src/entities/models/users/users.model";
import { formatDate } from "@/app/_utils/common";
interface UserDetailSheetProps {
open: boolean;
user: IUserSchema;
onOpenChange: (open: boolean) => void;
user: any;
onUserUpdate: () => void;
onUserUpdated: () => void;
}
export function UserDetailSheet({
open,
onOpenChange,
user,
onUserUpdate,
onUserUpdated,
}: UserDetailSheetProps) {
const [isDeleting, setIsDeleting] = useState(false);
const [isLoading, setIsLoading] = useState({
deleteUser: false,
sendPasswordRecovery: false,
sendMagicLink: false,
toggleBan: false,
});
// const [isDeleting, setIsDeleting] = useState(false);
// const [isLoading, setIsLoading] = useState({
// deleteUser: false,
// sendPasswordRecovery: false,
// sendMagicLink: false,
// toggleBan: false,
// });
const deleteUserMutation = useMutation({
mutationFn: () => deleteUser(user.id),
onMutate: () => {
setIsLoading((prev) => ({ ...prev, deleteUser: true }));
setIsDeleting(true);
},
onSuccess: () => {
toast.success("User deleted successfully");
onUserUpdate();
onOpenChange(false);
},
onError: () => {
toast.error("Failed to delete user");
},
onSettled: () => {
setIsLoading((prev) => ({ ...prev, deleteUser: false }));
setIsDeleting(false);
},
});
// const deleteUserMutation = useMutation({
// mutationFn: () => deleteUser(user.id),
// onMutate: () => {
// setIsLoading((prev) => ({ ...prev, deleteUser: true }));
// setIsDeleting(true);
// },
// onSuccess: () => {
// toast.success("User deleted successfully");
// onUserUpdate();
// onOpenChange(false);
// },
// onError: () => {
// toast.error("Failed to delete user");
// },
// onSettled: () => {
// setIsLoading((prev) => ({ ...prev, deleteUser: false }));
// setIsDeleting(false);
// },
// });
const sendPasswordRecoveryMutation = useMutation({
mutationFn: () => {
if (!user.email) {
throw new Error("User does not have an email address");
}
return sendPasswordRecovery(user.email);
},
onMutate: () => {
setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: true }));
},
onSuccess: () => {
toast.success("Password recovery email sent");
},
onError: () => {
toast.error("Failed to send password recovery email");
},
onSettled: () => {
setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: false }));
},
});
// const sendPasswordRecoveryMutation = useMutation({
// mutationFn: () => {
// if (!user.email) {
// throw new Error("User does not have an email address");
// }
// return sendPasswordRecovery(user.email);
// },
// onMutate: () => {
// setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: true }));
// },
// onSuccess: () => {
// toast.success("Password recovery email sent");
// },
// onError: () => {
// toast.error("Failed to send password recovery email");
// },
// onSettled: () => {
// setIsLoading((prev) => ({ ...prev, sendPasswordRecovery: false }));
// },
// });
const sendMagicLinkMutation = useMutation({
mutationFn: () => {
if (!user.email) {
throw new Error("User does not have an email address");
}
return sendMagicLink(user.email);
},
onMutate: () => {
setIsLoading((prev) => ({ ...prev, sendMagicLink: true }));
},
onSuccess: () => {
toast.success("Magic link sent successfully");
},
onError: () => {
toast.error("Failed to send magic link");
},
onSettled: () => {
setIsLoading((prev) => ({ ...prev, sendMagicLink: false }));
},
});
// const sendMagicLinkMutation = useMutation({
// mutationFn: () => {
// if (!user.email) {
// throw new Error("User does not have an email address");
// }
// return sendMagicLink(user.email);
// },
// onMutate: () => {
// setIsLoading((prev) => ({ ...prev, sendMagicLink: true }));
// },
// onSuccess: () => {
// toast.success("Magic link sent successfully");
// },
// onError: () => {
// toast.error("Failed to send magic link");
// },
// onSettled: () => {
// setIsLoading((prev) => ({ ...prev, sendMagicLink: false }));
// },
// });
const toggleBanMutation = useMutation({
mutationFn: () => {
if (user.banned_until) {
return unbanUser(user.id);
} else {
const ban_duration = "7h"; // Example: Ban duration set to 7 days
return banUser(user.id, ban_duration);
}
},
onMutate: () => {
setIsLoading((prev) => ({ ...prev, toggleBan: true }));
},
onSuccess: () => {
toast.success("User ban status updated");
onUserUpdate();
},
onError: () => {
toast.error("Failed to update user ban status");
},
onSettled: () => {
setIsLoading((prev) => ({ ...prev, toggleBan: false }));
},
});
// const toggleBanMutation = useMutation({
// mutationFn: () => {
// if (user.banned_until) {
// return unbanUser(user.id);
// } else {
// const ban_duration = "7h"; // Example: Ban duration set to 7 days
// return banUser({ id: user.id, ban_duration });
// }
// },
// onMutate: () => {
// setIsLoading((prev) => ({ ...prev, toggleBan: true }));
// },
// onSuccess: () => {
// toast.success("User ban status updated");
// onUserUpdate();
// },
// onError: () => {
// toast.error("Failed to update user ban status");
// },
// onSettled: () => {
// setIsLoading((prev) => ({ ...prev, toggleBan: false }));
// },
// });
const handleCopyItem = (item: string) => {
navigator.clipboard.writeText(item);
toast.success("Copied to clipboard");
};
// const handleCopyItem = (item: string) => {
// navigator.clipboard.writeText(item);
// toast.success("Copied to clipboard");
// };
const formatDate = (date: string | undefined | null) => {
return date ? format(new Date(date), "PPpp") : "-";
};
const {
handleDeleteUser,
handleSendPasswordRecovery,
handleSendMagicLink,
handleCopyItem,
handleToggleBan,
isBanPending,
isUnbanPending,
isDeletePending,
isSendPasswordRecoveryPending,
isSendMagicLinkPending,
} = useUserDetailSheetHandlers({ open, user, onUserUpdated, onOpenChange });
return (
<Sheet open={open} onOpenChange={onOpenChange}>
@ -166,7 +178,7 @@ export function UserDetailSheet({
variant="ghost"
size="icon"
className="h-4 w-4"
onClick={() => handleCopyItem(user.email)}
onClick={() => handleCopyItem(user.email ?? "")}
>
<Copy className="h-4 w-4" />
</Button>
@ -203,15 +215,13 @@ export function UserDetailSheet({
<div className="flex justify-between items-center py-1">
<span className="text-muted-foreground">Created at</span>
<span>{new Date(user.created_at).toLocaleString()}</span>
<span>{formatDate(user.created_at)}</span>
</div>
<div className="flex justify-between items-center py-1">
<span className="text-muted-foreground">Updated at</span>
<span>
{new Date(
user.updated_at || user.created_at
).toLocaleString()}
{formatDate(user.updated_at)}
</span>
</div>
@ -224,7 +234,7 @@ export function UserDetailSheet({
<span className="text-muted-foreground">
Confirmation sent at
</span>
<span>{formatDate(user.email_confirmation_sent_at)}</span>
<span>{formatDate(user.email_confirmed_at)}</span>
</div>
<div className="flex justify-between items-center py-1">
@ -284,10 +294,10 @@ export function UserDetailSheet({
<Button
variant="outline"
size="sm"
onClick={() => sendPasswordRecoveryMutation.mutate()}
disabled={isLoading.sendPasswordRecovery || !user.email}
onClick={handleSendPasswordRecovery}
disabled={isSendPasswordRecoveryPending}
>
{isLoading.sendPasswordRecovery ? (
{isSendPasswordRecoveryPending ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Sending...
@ -313,10 +323,10 @@ export function UserDetailSheet({
<Button
variant="outline"
size="sm"
onClick={() => sendMagicLinkMutation.mutate()}
disabled={isLoading.sendMagicLink || !user.email}
onClick={handleSendMagicLink}
disabled={isSendMagicLinkPending}
>
{isLoading.sendMagicLink ? (
{isSendMagicLinkPending ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Sending...
@ -354,10 +364,10 @@ export function UserDetailSheet({
<Button
variant={user.banned_until ? "outline" : "outline"}
size="sm"
onClick={() => toggleBanMutation.mutate()}
disabled={isLoading.toggleBan}
onClick={handleToggleBan}
disabled={isBanPending || isUnbanPending}
>
{isLoading.toggleBan ? (
{isBanPending || isUnbanPending ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
{user.banned_until ? "Unbanning..." : "Banning..."}
@ -383,9 +393,9 @@ export function UserDetailSheet({
<Button
variant="destructive"
size="sm"
disabled={isDeleting}
disabled={isDeletePending}
>
{isDeleting ? (
{isDeletePending ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Deleting...
@ -412,11 +422,11 @@ export function UserDetailSheet({
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => deleteUserMutation.mutate()}
onClick={handleDeleteUser}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
disabled={isDeleting}
disabled={isDeletePending}
>
{isDeleting ? (
{isDeletePending ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Deleting...

View File

@ -18,78 +18,24 @@ import { useMutation } from "@tanstack/react-query"
import { updateUser } from "../action"
import { toast } from "sonner"
import { UpdateUserSchema } from "@/src/entities/models/users/update-user.model"
import { useUserProfileSheetHandlers } from "../handler"
type UserProfileFormValues = z.infer<typeof UpdateUserSchema>
interface UserProfileSheetProps {
open: boolean
userData: IUserSchema
onOpenChange: (open: boolean) => void
userData?: IUserSchema
onUserUpdated: () => void
}
export function UserProfileSheet({ open, onOpenChange, userData, onUserUpdated }: UserProfileSheetProps) {
// Initialize form with user data
const form = useForm<UserProfileFormValues>({
resolver: zodResolver(UpdateUserSchema),
defaultValues: {
email: userData?.email || undefined,
encrypted_password: userData?.encrypted_password || undefined,
role: (userData?.role as "user" | "staff" | "admin") || "user",
phone: userData?.phone || undefined,
invited_at: userData?.invited_at || undefined,
confirmed_at: userData?.confirmed_at || undefined,
// recovery_sent_at: userData?.recovery_sent_at || undefined,
last_sign_in_at: userData?.last_sign_in_at || undefined,
created_at: userData?.created_at || undefined,
updated_at: userData?.updated_at || undefined,
is_anonymous: userData?.is_anonymous || false,
profile: {
id: userData?.profile?.id || undefined,
user_id: userData?.profile?.user_id || undefined,
avatar: userData?.profile?.avatar || undefined,
username: userData?.profile?.username || undefined,
first_name: userData?.profile?.first_name || undefined,
last_name: userData?.profile?.last_name || undefined,
bio: userData?.profile?.bio || undefined,
address: userData?.profile?.address || {
street: "",
city: "",
state: "",
country: "",
postal_code: "",
},
birth_date: userData?.profile?.birth_date ? new Date(userData.profile.birth_date) : undefined,
},
},
})
const { mutate: updateUserMutation, isPending } = useMutation({
mutationKey: ["updateUser"],
mutationFn: (data: UserProfileFormValues) => {
if (!userData?.id) {
throw new Error("User ID is required")
}
return updateUser(userData.id, data)
},
onError: (error) => {
toast("Failed to update user")
onOpenChange(false)
},
onSuccess: () => {
toast("User updated")
onUserUpdated()
onOpenChange(false)
},
})
async function onSubmit(data: UserProfileFormValues) {
try {
await updateUserMutation(data)
} catch (error) {
console.error("Error saving user profile:", error)
}
}
const {
form,
handleUpdateUser,
isUpdatePending,
} = useUserProfileSheetHandlers({ open, userData, onOpenChange, onUserUpdated })
return (
<Sheet open={open} onOpenChange={onOpenChange}>
@ -100,7 +46,7 @@ export function UserProfileSheet({ open, onOpenChange, userData, onUserUpdated }
</SheetHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<form onSubmit={form.handleSubmit(handleUpdateUser)} className="space-y-6">
{/* User Information Section */}
<FormSection
title="User Information"
@ -113,6 +59,7 @@ export function UserProfileSheet({ open, onOpenChange, userData, onUserUpdated }
control={form.control}
placeholder="email@example.com"
rows={4}
disabled={true}
/>
<FormFieldWrapper
name="phone"
@ -120,6 +67,8 @@ export function UserProfileSheet({ open, onOpenChange, userData, onUserUpdated }
type="tel"
control={form.control}
placeholder="+1234567890"
disabled={true}
/>
<FormFieldWrapper
name="role"
@ -265,14 +214,14 @@ export function UserProfileSheet({ open, onOpenChange, userData, onUserUpdated }
type="button"
variant="outline"
size="xs"
onClick={() => !isPending && onOpenChange(false)}
disabled={isPending}
onClick={() => !isUpdatePending && onOpenChange(false)}
disabled={isUpdatePending}
>
Cancel
</Button>
<Button size="xs" type="submit" disabled={isPending}>
{isPending && <Loader2 className="mr-1 h-4 w-4 animate-spin" />}
{isPending ? "Saving..." : "Save"}
<Button size="xs" type="submit" disabled={isUpdatePending}>
{isUpdatePending && <Loader2 className="mr-1 h-4 w-4 animate-spin" />}
{isUpdatePending ? "Saving..." : "Save"}
</Button>
</div>
</form>

View File

@ -143,7 +143,7 @@ export default function UserManagement() {
user={detailUser}
open={isSheetOpen}
onOpenChange={setIsSheetOpen}
onUserUpdate={() => { }}
onUserUpdated={() => { }}
/>
)}
<AddUserDialog

View File

@ -16,6 +16,7 @@ import { Input } from "@/app/_components/ui/input"
import { Avatar } from "@/app/_components/ui/avatar"
import Image from "next/image"
import { Badge } from "@/app/_components/ui/badge"
import { useUserDetailSheetHandlers, useUsersHandlers } from "../handler"
export type UserTableColumn = ColumnDef<IUserSchema, IUserSchema>
@ -24,6 +25,13 @@ export const createUserColumns = (
setFilters: (filters: IUserFilterOptionsSchema) => void,
handleUserUpdate: (user: IUserSchema) => void,
): UserTableColumn[] => {
const {
deleteUser,
banUser,
unbanUser,
} = useUsersHandlers();
return [
{
id: "email",
@ -313,16 +321,18 @@ export const createUserColumns = (
Update
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
/* handle delete */
}}
onClick={() => deleteUser(row.original.id)}
>
<Trash2 className="h-4 w-4 mr-2 text-destructive" />
Delete
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
/* handle ban */
if (row.original.banned_until != null) {
unbanUser(row.original.id)
} else {
banUser(row.original.id)
}
}}
>
<ShieldAlert className="h-4 w-4 mr-2 text-yellow-500" />

View File

@ -14,24 +14,26 @@ import { IBanDuration, IBanUserSchema, ICredentialsBanUserSchema } from '@/src/e
import { ICreateUserSchema } from '@/src/entities/models/users/create-user.model';
import { IUpdateUserSchema } from '@/src/entities/models/users/update-user.model';
import { ICredentialsInviteUserSchema } from '@/src/entities/models/users/invite-user.model';
import { ICredentialGetUserByEmailSchema } from '@/src/entities/models/users/read-user.model';
import { ICredentialGetUserByEmailSchema, ICredentialGetUserByIdSchema, ICredentialGetUserByUsernameSchema } from '@/src/entities/models/users/read-user.model';
import { ICredentialsUnbanUserSchema } from '@/src/entities/models/users/unban-user.model';
export async function banUser(id: string, ban_duration: IBanDuration) {
export async function banUser(credential: ICredentialsBanUserSchema, data: IBanUserSchema) {
const instrumentationService = getInjection('IInstrumentationService');
return await instrumentationService.instrumentServerAction(
'banUser',
'unbanUser',
{ recordResponse: true },
async () => {
try {
const banUserController = getInjection('IBanUserController');
await banUserController({ id }, { ban_duration });
await banUserController({ id: credential.id }, { ban_duration: data.ban_duration });
return { success: true };
} catch (err) {
if (err instanceof InputParseError) {
// return {
// error: err.message,
// };
// return {
// error: err.message,
// };
throw new InputParseError(err.message);
}
@ -40,7 +42,7 @@ export async function banUser(id: string, ban_duration: IBanDuration) {
// return {
// error: 'Must be logged in to create a user.',
// };
throw new UnauthenticatedError('Must be logged in to ban a user.');
throw new UnauthenticatedError('Must be logged in to unban a user.');
}
if (err instanceof AuthenticationError) {
@ -63,7 +65,8 @@ export async function banUser(id: string, ban_duration: IBanDuration) {
);
}
export async function unbanUser(id: string) {
export async function unbanUser(credential: ICredentialsUnbanUserSchema) {
const instrumentationService = getInjection('IInstrumentationService');
return await instrumentationService.instrumentServerAction(
'unbanUser',
@ -71,7 +74,7 @@ export async function unbanUser(id: string) {
async () => {
try {
const unbanUserController = getInjection('IUnbanUserController');
await unbanUserController({ id });
await unbanUserController(credential);
return { success: true };
} catch (err) {
@ -134,7 +137,7 @@ export async function getCurrentUser() {
);
}
export async function getUserById(id: string) {
export async function getUserById(credential: ICredentialGetUserByIdSchema) {
const instrumentationService = getInjection('IInstrumentationService');
return await instrumentationService.instrumentServerAction(
'getUserById',
@ -142,7 +145,7 @@ export async function getUserById(id: string) {
async () => {
try {
const getUserByIdController = getInjection('IGetUserByIdController');
return await getUserByIdController({ id });
return await getUserByIdController(credential);
} catch (err) {
@ -230,7 +233,7 @@ export async function getUserByEmail(credential: ICredentialGetUserByEmailSchema
);
}
export async function getUserByUsername(username: string) {
export async function getUserByUsername(credential: ICredentialGetUserByUsernameSchema) {
const instrumentationService = getInjection('IInstrumentationService');
return await instrumentationService.instrumentServerAction(
'getUserByUsername',
@ -240,7 +243,7 @@ export async function getUserByUsername(username: string) {
const getUserByUsernameController = getInjection(
'IGetUserByUsernameController'
);
return await getUserByUsernameController({ username });
return await getUserByUsernameController(credential);
} catch (err) {

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { useCreateUserMutation, useInviteUserMutation } from './queries';
import { useBanUserMutation, useCreateUserMutation, useDeleteUserMutation, useInviteUserMutation, useUnbanUserMutation, useUpdateUserMutation } from './queries';
import { IUserSchema, IUserFilterOptionsSchema } from '@/src/entities/models/users/users.model';
import { toast } from 'sonner';
import { set } from 'date-fns';
@ -8,6 +8,226 @@ import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { defaulIInviteUserSchemaValues, IInviteUserSchema, InviteUserSchema } from '@/src/entities/models/users/invite-user.model';
import { useQueryClient } from '@tanstack/react-query';
import { useSendMagicLinkMutation, useSendPasswordRecoveryMutation } from '@/app/(pages)/(auth)/queries';
import { ValidBanDuration } from '@/app/_lib/types/ban-duration';
import { IUpdateUserSchema, UpdateUserSchema } from '@/src/entities/models/users/update-user.model';
export const useUsersHandlers = () => {
const queryClient = useQueryClient();
// Core mutations
const { updateUser, isPending: isUpdatePending, errors: isUpdateError } = useUpdateUserMutation();
const { deleteUser, isPending: isDeletePending } = useDeleteUserMutation();
const { sendPasswordRecovery, isPending: isSendPasswordRecoveryPending } = useSendPasswordRecoveryMutation();
const { sendMagicLink, isPending: isSendMagicLinkPending } = useSendMagicLinkMutation();
const { banUser, isPending: isBanPending } = useBanUserMutation();
const { unbanUser, isPending: isUnbanPending } = useUnbanUserMutation();
/**
* update a user by ID
*/
const handleUpdateUser = async (userId: string, data: IUpdateUserSchema, options?: {
onSuccess?: () => void,
onError?: (error: unknown) => void
}) => {
await updateUser({ id: userId, data }, {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
toast.success("User updated successfully");
options?.onSuccess?.();
},
onError: (error) => {
toast.error("Failed to update user");
options?.onError?.(error);
},
});
}
/**
* Deletes a user by ID
*/
const handleDeleteUser = async (userId: string, options?: {
onSuccess?: () => void,
onError?: (error: unknown) => void
}) => {
await deleteUser(userId, {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
toast.success("User deleted successfully");
options?.onSuccess?.();
},
onError: (error) => {
toast.error("Failed to delete user");
options?.onError?.(error);
},
});
};
/**
* Sends a password recovery email to the user
*/
const handleSendPasswordRecovery = async (email: string, options?: {
onSuccess?: () => void,
onError?: (error: unknown) => void
}) => {
if (!email) {
toast.error("No email address provided");
options?.onError?.(new Error("No email address provided"));
return;
}
await sendPasswordRecovery(email, {
onSuccess: () => {
toast.success("Recovery email sent");
options?.onSuccess?.();
},
onError: (error) => {
toast.error("Failed to send recovery email");
options?.onError?.(error);
},
});
};
/**
* Sends a magic link to the user's email
*/
const handleSendMagicLink = async (email: string, options?: {
onSuccess?: () => void,
onError?: (error: unknown) => void
}) => {
if (!email) {
toast.error("No email address provided");
options?.onError?.(new Error("No email address provided"));
return;
}
await sendMagicLink(email, {
onSuccess: () => {
toast.success("Magic link sent");
options?.onSuccess?.();
},
onError: (error) => {
toast.error("Failed to send magic link");
options?.onError?.(error);
},
});
};
/**
* Bans a user for the specified duration
*/
const handleBanUser = async (userId: string, banDuration: ValidBanDuration = "24h", options?: {
onSuccess?: () => void,
onError?: (error: unknown) => void
}) => {
await banUser({ credential: { id: userId }, data: { ban_duration: banDuration } }, {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
toast.success("User banned successfully");
options?.onSuccess?.();
},
onError: (error) => {
toast.error("Failed to ban user");
options?.onError?.(error);
},
});
};
/**
* Unbans a user
*/
const handleUnbanUser = async (userId: string, options?: {
onSuccess?: () => void,
onError?: (error: unknown) => void
}) => {
await unbanUser({ id: userId }, {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
toast.success("User unbanned successfully");
options?.onSuccess?.();
},
onError: (error) => {
toast.error("Failed to unban user");
options?.onError?.(error);
},
});
};
/**
* Toggles a user's ban status
*/
const handleToggleBan = async (user: { id: string, banned_until?: ValidBanDuration }, options?: {
onSuccess?: () => void,
onError?: (error: unknown) => void,
banDuration?: ValidBanDuration
}) => {
if (user.banned_until) {
await handleUnbanUser(user.id, options);
} else {
await handleBanUser(user.id, options?.banDuration, options);
}
};
/**
* Copies text to clipboard
*/
const handleCopyItem = (item: string, options?: {
onSuccess?: () => void,
onError?: (error: unknown) => void
}) => {
if (!navigator.clipboard) {
const error = new Error("Clipboard not supported");
toast.error("Clipboard not supported");
options?.onError?.(error);
return;
}
if (!item) {
const error = new Error("Nothing to copy");
toast.error("Nothing to copy");
options?.onError?.(error);
return;
}
navigator.clipboard.writeText(item)
.then(() => {
toast.success("Copied to clipboard");
options?.onSuccess?.();
})
.catch((error) => {
toast.error("Failed to copy to clipboard");
options?.onError?.(error);
});
};
return {
// Action handlers
updateUser: handleUpdateUser,
deleteUser: handleDeleteUser,
sendPasswordRecovery: handleSendPasswordRecovery,
sendMagicLink: handleSendMagicLink,
banUser: handleBanUser,
unbanUser: handleUnbanUser,
toggleBan: handleToggleBan,
copyToClipboard: handleCopyItem,
// Loading states
isUpdatePending,
isDeletePending,
isSendPasswordRecoveryPending,
isSendMagicLinkPending,
isBanPending,
isUnbanPending,
// Errors
isUpdateError,
};
};
// Specific handler for the component
export const useAddUserDialogHandler = ({ onUserAdded, onOpenChange }: {
onUserAdded: () => void;
@ -139,6 +359,150 @@ export const useInviteUserHandler = ({ onUserInvited, onOpenChange }: {
};
}
export const useUserDetailSheetHandlers = ({ open, user, onUserUpdated, onOpenChange }: {
open: boolean;
user: IUserSchema;
onUserUpdated: () => void;
onOpenChange: (open: boolean) => void;
}) => {
const {
deleteUser,
sendPasswordRecovery,
sendMagicLink,
banUser,
unbanUser,
toggleBan,
copyToClipboard,
isDeletePending,
isSendPasswordRecoveryPending,
isSendMagicLinkPending,
isBanPending,
isUnbanPending,
} = useUsersHandlers();
const handleDeleteUser = async () => {
await deleteUser(user.id, {
onSuccess: () => {
onOpenChange(false);
}
});
};
const handleSendPasswordRecovery = async () => {
if (!user.email) {
toast.error("User has no email address");
return;
}
await sendPasswordRecovery(user.email);
};
const handleSendMagicLink = async () => {
if (!user.email) {
toast.error("User has no email address");
return;
}
await sendMagicLink(user.email);
};
const handleBanUser = async () => {
await banUser(user.id, "24h", {
onSuccess: onUserUpdated
});
};
const handleUnbanUser = async () => {
await unbanUser(user.id, {
onSuccess: onUserUpdated
});
};
const handleToggleBan = async () => {
await toggleBan({ id: user.id }, {
onSuccess: onUserUpdated
});
};
return {
handleDeleteUser,
handleSendPasswordRecovery,
handleSendMagicLink,
handleBanUser,
handleUnbanUser,
handleToggleBan,
handleCopyItem: copyToClipboard,
isDeletePending,
isSendPasswordRecoveryPending,
isSendMagicLinkPending,
isBanPending,
isUnbanPending,
};
};
export const useUserProfileSheetHandlers = ({ open, onOpenChange, userData, onUserUpdated }: {
open: boolean;
userData: IUserSchema;
onOpenChange: (open: boolean) => void;
onUserUpdated: () => void;
}) => {
const { updateUser, isUpdatePending, isUpdateError } = useUsersHandlers();
// Initialize form with user data
const form = useForm<IUpdateUserSchema>({
resolver: zodResolver(UpdateUserSchema),
defaultValues: {
email: userData?.email || undefined,
encrypted_password: userData?.encrypted_password || undefined,
role: (userData?.role as "user" | "staff" | "admin") || "user",
phone: userData?.phone || undefined,
invited_at: userData?.invited_at || undefined,
confirmed_at: userData?.confirmed_at || undefined,
// recovery_sent_at: userData?.recovery_sent_at || undefined,
last_sign_in_at: userData?.last_sign_in_at || undefined,
created_at: userData?.created_at || undefined,
updated_at: userData?.updated_at || undefined,
is_anonymous: userData?.is_anonymous || false,
profile: {
// id: userData?.profile?.id || undefined,
// user_id: userData?.profile?.user_id || undefined,
avatar: userData?.profile?.avatar || undefined,
username: userData?.profile?.username || undefined,
first_name: userData?.profile?.first_name || undefined,
last_name: userData?.profile?.last_name || undefined,
bio: userData?.profile?.bio || undefined,
address: userData?.profile?.address || {
street: "",
city: "",
state: "",
country: "",
postal_code: "",
},
birth_date: userData?.profile?.birth_date ? new Date(userData.profile.birth_date) : undefined,
},
},
})
const handleUpdateUser = async () => {
await updateUser(userData.id, form.getValues(), {
onSuccess: () => {
onUserUpdated();
onOpenChange(false);
},
onError: () => {
onOpenChange(false);
},
});
}
return {
handleUpdateUser,
form,
isUpdatePending,
};
}
export const useUserManagementHandlers = (refetch: () => void) => {
const [searchQuery, setSearchQuery] = useState("")
const [detailUser, setDetailUser] = useState<IUserSchema | null>(null)

View File

@ -13,11 +13,12 @@ import {
getUserByUsername
} from "./action";
import { IUserSchema } from "@/src/entities/models/users/users.model";
import { IBanUserSchema, ICredentialsBanUserSchema } from "@/src/entities/models/users/ban-user.model";
import { IUnbanUserSchema } from "@/src/entities/models/users/unban-user.model";
import { IBanDuration, IBanUserSchema, ICredentialsBanUserSchema } from "@/src/entities/models/users/ban-user.model";
import { ICredentialsUnbanUserSchema, IUnbanUserSchema } from "@/src/entities/models/users/unban-user.model";
import { ICreateUserSchema } from "@/src/entities/models/users/create-user.model";
import { IUpdateUserSchema } from "@/src/entities/models/users/update-user.model";
import { ICredentialsInviteUserSchema } from "@/src/entities/models/users/invite-user.model";
import { ICredentialGetUserByEmailSchema, ICredentialGetUserByIdSchema, ICredentialGetUserByUsernameSchema, IGetUserByEmailSchema, IGetUserByIdSchema, IGetUserByUsernameSchema } from "@/src/entities/models/users/read-user.model";
const useUsersAction = () => {
@ -28,35 +29,35 @@ const useUsersAction = () => {
});
// Current user query doesn't need parameters
const getCurrentUserQuery = useQuery({
const getCurrentUserQuery = useQuery<IUserSchema>({
queryKey: ["user", "current"],
queryFn: async () => await getCurrentUser()
});
const getUserByIdQuery = (id: string) => ({
queryKey: ["user", id],
queryFn: async () => await getUserById(id)
const getUserByIdQuery = (credential: ICredentialGetUserByIdSchema) => useQuery<IUserSchema>({
queryKey: ["user", "id", credential.id],
queryFn: async () => await getUserById(credential)
});
const getUserByEmailQuery = (email: string) => ({
queryKey: ["user", "email", email],
queryFn: async () => await getUserByEmail({ email })
const getUserByEmailQuery = (credential: IGetUserByEmailSchema) => useQuery<IUserSchema>({
queryKey: ["user", "email", credential.email],
queryFn: async () => await getUserByEmail(credential)
});
const getUserByUsernameQuery = (username: string) => ({
queryKey: ["user", "username", username],
queryFn: async () => await getUserByUsername(username)
const getUserByUsernameQuery = (credential: IGetUserByUsernameSchema) => useQuery<IUserSchema>({
queryKey: ["user", "username", credential.username],
queryFn: async () => await getUserByUsername(credential)
});
// Mutations that don't need dynamic parameters
const banUserMutation = useMutation({
mutationKey: ["banUser"],
mutationFn: async ({ credential, params }: { credential: ICredentialsBanUserSchema; params: IBanUserSchema }) => await banUser(credential.id, params.ban_duration)
mutationFn: async ({ credential, data }: { credential: ICredentialsBanUserSchema; data: IBanUserSchema }) => await banUser(credential, data)
});
const unbanUserMutation = useMutation({
mutationKey: ["unbanUser"],
mutationFn: async (params: IUnbanUserSchema) => await unbanUser(params.id)
mutationFn: async (credential: ICredentialsUnbanUserSchema) => await unbanUser(credential)
});
// Create functions that return configured hooks
@ -117,6 +118,39 @@ export const useGetCurrentUserQuery = () => {
};
}
export const useGetUserByIdQuery = (credential: ICredentialGetUserByIdSchema) => {
const { getUserById } = useUsersAction();
return {
data: getUserById(credential).data,
isPending: getUserById(credential).isPending,
error: getUserById(credential).error,
refetch: getUserById(credential).refetch,
};
}
export const useGetUserByEmailQuery = (credential: ICredentialGetUserByEmailSchema) => {
const { getUserByEmailQuery } = useUsersAction();
return {
data: getUserByEmailQuery(credential).data,
isPending: getUserByEmailQuery(credential).isPending,
error: getUserByEmailQuery(credential).error,
refetch: getUserByEmailQuery(credential).refetch,
};
}
export const useGetUserByUsernameQuery = (credential: ICredentialGetUserByUsernameSchema) => {
const { getUserByUsernameQuery } = useUsersAction();
return {
data: getUserByUsernameQuery(credential).data,
isPending: getUserByUsernameQuery(credential).isPending,
error: getUserByUsernameQuery(credential).error,
refetch: getUserByUsernameQuery(credential).refetch,
};
}
export const useCreateUserMutation = () => {
const { createUser } = useUsersAction();
@ -136,3 +170,43 @@ export const useInviteUserMutation = () => {
errors: inviteUser.error,
}
}
export const useUpdateUserMutation = () => {
const { updateUser } = useUsersAction();
return {
updateUser: updateUser.mutateAsync,
isPending: updateUser.isPending,
errors: updateUser.error,
}
}
export const useBanUserMutation = () => {
const { banUser } = useUsersAction();
return {
banUser: banUser.mutateAsync,
isPending: banUser.isPending,
errors: banUser.error,
}
}
export const useUnbanUserMutation = () => {
const { unbanUser } = useUsersAction();
return {
unbanUser: unbanUser.mutateAsync,
isPending: unbanUser.isPending,
errors: unbanUser.error,
}
}
export const useDeleteUserMutation = () => {
const { deleteUser } = useUsersAction();
return {
deleteUser: deleteUser.mutateAsync,
isPending: deleteUser.isPending,
errors: deleteUser.error,
}
}

View File

@ -134,13 +134,12 @@ export async function verifyOtp(formData: FormData) {
})
}
export async function sendMagicLink(formData: FormData) {
export async function sendMagicLink(email: string) {
const instrumentationService = getInjection("IInstrumentationService")
return await instrumentationService.instrumentServerAction("sendMagicLink", {
recordResponse: true
}, async () => {
try {
const email = formData.get("email")?.toString()
const sendMagicLinkController = getInjection("ISendMagicLinkController")
await sendMagicLinkController({ email })
@ -161,13 +160,12 @@ export async function sendMagicLink(formData: FormData) {
})
}
export async function sendPasswordRecovery(formData: FormData) {
export async function sendPasswordRecovery(email: string) {
const instrumentationService = getInjection("IInstrumentationService")
return await instrumentationService.instrumentServerAction("sendPasswordRecovery", {
recordResponse: true
}, async () => {
try {
const email = formData.get("email")?.toString()
const sendPasswordRecoveryController = getInjection("ISendPasswordRecoveryController")
await sendPasswordRecoveryController({ email })

View File

@ -21,12 +21,12 @@ export function useAuthActions() {
const sendMagicLinkMutation = useMutation({
mutationKey: ["sendMagicLink"],
mutationFn: async (formData: FormData) => await sendMagicLink(formData)
mutationFn: async (email: string) => await sendMagicLink(email)
});
const sendPasswordRecoveryMutation = useMutation({
mutationKey: ["sendPasswordRecovery"],
mutationFn: async (formData: FormData) => await sendPasswordRecovery(formData)
mutationFn: async (email: string) => await sendPasswordRecovery(email)
});
return {
@ -36,4 +36,54 @@ export function useAuthActions() {
sendMagicLink: sendMagicLinkMutation,
sendPasswordRecovery: sendPasswordRecoveryMutation
};
}
}
export const useSignInMutation = () => {
const { signIn } = useAuthActions();
return {
signIn: signIn.mutateAsync,
isPending: signIn.isPending,
error: signIn.error,
};
}
export const useVerifyOtpMutation = () => {
const { verifyOtp } = useAuthActions();
return {
verifyOtp: verifyOtp.mutateAsync,
isPending: verifyOtp.isPending,
error: verifyOtp.error,
}
}
export const useSignOutMutation = () => {
const { signOut } = useAuthActions();
return {
signOut: signOut.mutateAsync,
isPending: signOut.isPending
}
}
export const useSendMagicLinkMutation = () => {
const { sendMagicLink } = useAuthActions();
return {
sendMagicLink: sendMagicLink.mutateAsync,
isPending: sendMagicLink.isPending,
error: sendMagicLink.error,
}
}
export const useSendPasswordRecoveryMutation = () => {
const { sendPasswordRecovery } = useAuthActions();
return {
sendPasswordRecovery: sendPasswordRecovery.mutateAsync,
isPending: sendPasswordRecovery.isPending,
error: sendPasswordRecovery.error,
}
}

View File

@ -54,7 +54,7 @@ export default function RootLayout({
</nav> */}
<div className="flex flex-col max-w-full p-0">
{children}
<Toaster theme="system" richColors position="top-right" />
<Toaster theme="system" position="top-right" />
</div>
{/* <footer className="w-full flex items-center justify-center border-t mx-auto text-center text-xs gap-8 py-16 mt-auto">

View File

@ -31,6 +31,7 @@ interface FormFieldProps {
booleanType?: "switch" | "checkbox" | "select"
fromYear?: number
toYear?: number
disabled?: boolean // Ganti readonly menjadi disabled
}
export function FormFieldWrapper({
@ -47,6 +48,7 @@ export function FormFieldWrapper({
booleanType = "switch",
fromYear = 1900,
toYear = new Date().getFullYear(),
disabled = false, // Default disabled adalah false
}: FormFieldProps) {
// Default boolean options for select
const booleanOptions = [
@ -60,11 +62,12 @@ export function FormFieldWrapper({
name={name}
render={({ field }) => (
<FormItem
className={
className={cn(
isBoolean && booleanType === "switch"
? "flex items-start justify-between rounded-lg border p-4"
: "flex justify-between items-start gap-4"
}
: "flex justify-between items-start gap-4",
disabled && "opacity-50 cursor-not-allowed" // Tambahkan gaya khusus untuk disabled
)}
>
<div className={isBoolean && booleanType === "switch" ? "space-y-1" : "w-1/3"}>
<FormLabel className={isBoolean && booleanType === "switch" ? "text-base" : ""}>{label}</FormLabel>
@ -75,13 +78,14 @@ export function FormFieldWrapper({
<FormControl>
{isBoolean ? (
booleanType === "switch" ? (
<Switch checked={field.value} onCheckedChange={field.onChange} />
<Switch checked={field.value} onCheckedChange={field.onChange} disabled={disabled} />
) : booleanType === "checkbox" ? (
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
<Checkbox checked={field.value} onCheckedChange={field.onChange} disabled={disabled} />
) : (
<Select
onValueChange={(value) => field.onChange(value === "true")}
defaultValue={String(field.value)}
disabled={disabled}
>
<SelectTrigger>
<SelectValue placeholder={placeholder || `Select ${label.toLowerCase()}`} />
@ -96,7 +100,7 @@ export function FormFieldWrapper({
</Select>
)
) : type.includes("select") && options ? (
<Select onValueChange={field.onChange} defaultValue={field.value}>
<Select onValueChange={field.onChange} defaultValue={field.value} disabled={disabled}>
<SelectTrigger>
<SelectValue placeholder={placeholder || `Select ${label.toLowerCase()}`} />
</SelectTrigger>
@ -113,23 +117,28 @@ export function FormFieldWrapper({
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn("w-full pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
className={cn(
"w-full pl-3 text-left font-normal",
!field.value && "text-muted-foreground"
)}
disabled={disabled}
>
{field.value ? format(field.value, "MM/dd/yyyy hh:mm:ss a") : <span>Pick a date and time</span>}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<DateTimePicker
selected={field.value instanceof Date ? field.value : undefined}
onSelect={field.onChange}
disabled={(date) => date > new Date() || date < new Date(`${fromYear}-01-01`)}
fromYear={fromYear}
toYear={toYear}
showTimePicker
/>
</PopoverContent>
{!disabled && (
<PopoverContent className="w-auto p-0" align="start">
<DateTimePicker
selected={field.value instanceof Date ? field.value : undefined}
onSelect={field.onChange}
disabled={(date) => date > new Date() || date < new Date(`${fromYear}-01-01`)}
fromYear={fromYear}
toYear={toYear}
showTimePicker
/>
</PopoverContent>
)}
</Popover>
) : rows > 1 ? (
<Textarea
@ -138,6 +147,7 @@ export function FormFieldWrapper({
rows={rows}
value={field.value ?? ""}
placeholder={placeholder}
disabled={disabled}
/>
) : (
<Input
@ -145,6 +155,7 @@ export function FormFieldWrapper({
type={type.includes("URL") ? "url" : type === "string" ? "text" : type}
placeholder={placeholder}
value={field.value ?? ""}
disabled={disabled}
/>
)}
</FormControl>
@ -155,4 +166,3 @@ export function FormFieldWrapper({
/>
)
}

View File

@ -730,7 +730,7 @@ const SidebarMenuSubButton = React.forwardRef<
className
)}
{...props}
/>
/>
);
});
SidebarMenuSubButton.displayName = "SidebarMenuSubButton";

View File

@ -0,0 +1,74 @@
import { Locale } from "date-fns";
/**
* Available date format patterns for the formatDate function.
* See date-fns format documentation for more details on each pattern.
*/
export type DateFormatPattern =
// Year formats
| "yyyy" // 2025
| "yy" // 25
| "y" // 2025
// Month formats
| "MMMM" // March
| "MMM" // Mar
| "MM" // 03
| "M" // 3
// Day formats
| "dd" // 01-31
| "d" // 1-31
| "Do" // 1st, 2nd, etc.
| "EEEE" // Monday, Tuesday, etc.
| "EEE" // Mon, Tue, etc.
| "EE" // Mo, Tu, etc.
| "E" // M, T, etc.
// Hour formats
| "HH" // 00-23
| "H" // 0-23
| "hh" // 01-12
| "h" // 1-12
// Minute and second formats
| "mm" // 00-59
| "m" // 0-59
| "ss" // 00-59
| "s" // 0-59
// AM/PM
| "a" // am/pm
| "aa" // AM/PM
// Combined common formats
| "PPpp" // Mar 23, 2025, 12:00 AM
| "Pp" // 03/23/2025, 12:00 AM
| "PP" // Mar 23, 2025
| "p" // 12:00 AM
// Custom combined patterns (allow any string)
| string;
/**
* Custom formatting options for dates.
*/
export interface DateFormatOptions {
/**
* The format pattern to use when formatting the date.
* @default "PPpp"
*/
format?: DateFormatPattern;
/**
* The value to display when the date is null or undefined.
* @default "-"
*/
fallback?: string;
/**
* The locale to use for formatting.
* @default undefined (uses system locale)
*/
locale?: Locale;
}

View File

@ -0,0 +1,5 @@
export interface ServerActionErrorParams {
message: string;
code: string;
field?: string;
}

View File

@ -1,4 +1,6 @@
import { format } from "date-fns";
import { redirect } from "next/navigation";
import { DateFormatOptions, DateFormatPattern } from "../_lib/types/date-format.interface";
/**
* Redirects to a specified path with an encoded message as a query parameter.
@ -70,4 +72,49 @@ export function generateUsername(email: string): string {
const [localPart] = email.split("@");
const randomSuffix = Math.random().toString(36).substring(2, 8); // Generate a random alphanumeric string
return `${localPart}.${randomSuffix}`;
}
}
/**
* Formats a date string to a human-readable format with type safety.
* @param date - The date string to format.
* @param options - Formatting options or a format string.
* @returns The formatted date string.
* @example
* // Using default format
* formatDate("2025-03-23")
*
* // Using a custom format string
* formatDate("2025-03-23", "yyyy-MM-dd")
*
* // Using formatting options
* formatDate("2025-03-23", { format: "MMMM do, yyyy", fallback: "Not available" })
*/
export const formatDate = (
date: string | Date | undefined | null,
options: DateFormatOptions | DateFormatPattern = { format: "PPpp" }
): string => {
if (!date) {
return typeof options === "string"
? "-"
: (options.fallback || "-");
}
const dateObj = date instanceof Date ? date : new Date(date);
// Handle invalid dates
if (isNaN(dateObj.getTime())) {
return typeof options === "string"
? "-"
: (options.fallback || "-");
}
if (typeof options === "string") {
return format(dateObj, options);
}
const { format: formatPattern = "PPpp", locale } = options;
return locale
? format(dateObj, formatPattern, { locale })
: format(dateObj, formatPattern);
};

View File

@ -51,14 +51,14 @@ export function createUsersModule() {
.bind(DI_SYMBOLS.IBanUserUseCase)
.toHigherOrderFunction(banUserUseCase, [
DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.IUsersRepository
DI_SYMBOLS.IUsersRepository,
]);
usersModule
.bind(DI_SYMBOLS.IUnbanUserUseCase)
.toHigherOrderFunction(unbanUserUseCase, [
DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.IUsersRepository
DI_SYMBOLS.IUsersRepository,
]);
usersModule
@ -129,14 +129,16 @@ export function createUsersModule() {
.bind(DI_SYMBOLS.IBanUserController)
.toHigherOrderFunction(banUserController, [
DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.IBanUserUseCase
DI_SYMBOLS.IBanUserUseCase,
DI_SYMBOLS.IGetCurrentUserUseCase
]);
usersModule
.bind(DI_SYMBOLS.IUnbanUserController)
.toHigherOrderFunction(unbanUserController, [
DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.IUnbanUserUseCase
DI_SYMBOLS.IUnbanUserUseCase,
DI_SYMBOLS.IGetCurrentUserUseCase
]);
usersModule
@ -194,14 +196,16 @@ export function createUsersModule() {
.bind(DI_SYMBOLS.IUpdateUserController)
.toHigherOrderFunction(updateUserController, [
DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.IUpdateUserUseCase
DI_SYMBOLS.IUpdateUserUseCase,
DI_SYMBOLS.IGetCurrentUserUseCase
]);
usersModule
.bind(DI_SYMBOLS.IDeleteUserController)
.toHigherOrderFunction(deleteUserController, [
DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.IDeleteUserUseCase
DI_SYMBOLS.IDeleteUserUseCase,
DI_SYMBOLS.IGetCurrentUserUseCase
]);

View File

@ -13,13 +13,13 @@ export const sendMagicLinkUseCase = (
) => async (input: { email: string }): Promise<void> => {
return await instrumentationService.startSpan({ name: "sendMagicLink Use Case", op: "function" },
async () => {
const user = await usersRepository.getUserByEmail(input.email)
const user = await usersRepository.getUserByEmail({ email: input.email })
if (!user) {
throw new NotFoundError("User not found")
}
await authenticationService.sendMagicLink(input.email)
await authenticationService.sendMagicLink({ email: input.email })
}
)
}

View File

@ -13,13 +13,13 @@ export const sendPasswordRecoveryUseCase = (
) => async (input: { email: string }): Promise<void> => {
return await instrumentationService.startSpan({ name: "sendPasswordRecovery Use Case", op: "function" },
async () => {
const user = await usersRepository.getUserByEmail(input.email)
const user = await usersRepository.getUserByEmail({ email: input.email })
if (!user) {
throw new NotFoundError("User not found")
}
await authenticationService.sendPasswordRecovery(input.email)
await authenticationService.sendPasswordRecovery({ email: input.email })
}
)
}

View File

@ -1,3 +1,5 @@
import { ServerActionErrorParams } from "@/app/_lib/types/error-server-action.interface";
export class DatabaseOperationError extends Error {
constructor(message: string, options?: ErrorOptions) {
super(message, options);
@ -16,12 +18,15 @@ export class InputParseError extends Error {
}
}
export class ServerActionError extends Error {
code: string;
field?: string;
constructor(message: string, code: string) {
constructor({ message, code, field }: ServerActionErrorParams) {
super(message);
this.name = "ServerActionError";
this.code = code;
this.field = field;
}
}

View File

@ -30,31 +30,39 @@ export const UpdateUserSchema = z.object({
export type IUpdateUserSchema = z.infer<typeof UpdateUserSchema>;
export const defaulIUpdateUserSchemaValues: IUpdateUserSchema = {
email: "",
email_confirmed_at: false,
encrypted_password: "",
export const defaultValueUpdateUserSchema: IUpdateUserSchema = {
email: undefined,
email_confirmed_at: undefined,
encrypted_password: undefined,
role: "user",
phone: "",
phone_confirmed_at: false,
invited_at: "",
confirmed_at: "",
last_sign_in_at: "",
created_at: "",
updated_at: "",
phone: undefined,
phone_confirmed_at: undefined,
invited_at: undefined,
confirmed_at: undefined,
last_sign_in_at: undefined,
created_at: undefined,
updated_at: undefined,
is_anonymous: false,
user_metadata: {},
app_metadata: {},
profile: {
avatar: "",
username: "",
first_name: "",
last_name: "",
bio: "",
address: "",
birth_date: new Date(),
avatar: undefined,
username: undefined,
first_name: undefined,
last_name: undefined,
bio: undefined,
address: {
street: "",
city: "",
state: "",
country: "",
postal_code: "",
},
birth_date: undefined,
}
}
};
export const CredentialUpdateUserSchema = z.object({
id: z.string(),

View File

@ -20,7 +20,7 @@ export const deleteUserController =
const session = await getCurrentUserUseCase()
if (!session) {
throw new UnauthenticatedError("Must be logged in to create a user")
throw new UnauthenticatedError("Must be logged in to delete a user")
}
return await deleteUserUseCase(credential);