Refactor CRUD Users
This commit is contained in:
parent
179ddf6999
commit
e84a6f52c0
|
@ -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...
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -143,7 +143,7 @@ export default function UserManagement() {
|
|||
user={detailUser}
|
||||
open={isSheetOpen}
|
||||
onOpenChange={setIsSheetOpen}
|
||||
onUserUpdate={() => { }}
|
||||
onUserUpdated={() => { }}
|
||||
/>
|
||||
)}
|
||||
<AddUserDialog
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -14,19 +14,21 @@ 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 {
|
||||
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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 })
|
||||
|
|
|
@ -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 {
|
||||
|
@ -37,3 +37,53 @@ export function useAuthActions() {
|
|||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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,12 +117,17 @@ 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>
|
||||
{!disabled && (
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<DateTimePicker
|
||||
selected={field.value instanceof Date ? field.value : undefined}
|
||||
|
@ -128,8 +137,8 @@ export function FormFieldWrapper({
|
|||
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({
|
|||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export interface ServerActionErrorParams {
|
||||
message: string;
|
||||
code: string;
|
||||
field?: string;
|
||||
}
|
|
@ -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.
|
||||
|
@ -71,3 +73,48 @@ export function generateUsername(email: string): string {
|
|||
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);
|
||||
};
|
|
@ -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
|
||||
]);
|
||||
|
||||
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
)
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue