refactor add user and invite user
This commit is contained in:
parent
e95bd8cb23
commit
0af8a9be0b
|
@ -16,29 +16,27 @@ import {
|
|||
import { NavPreMain } from "./navigations/nav-pre-main";
|
||||
import { navData } from "@/prisma/data/nav";
|
||||
import { TeamSwitcher } from "../../../_components/team-switcher";
|
||||
import { useGetCurrentUserQuery } from "../dashboard/user-management/queries";
|
||||
|
||||
import { Profile, User } from "@/src/entities/models/users/users.model";
|
||||
import { getCurrentUser } from "@/app/(pages)/(admin)/dashboard/user-management/action";
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const [user, setUser] = React.useState<User | null>(null);
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const { data: user, isPending, error } = useGetCurrentUserQuery()
|
||||
|
||||
React.useEffect(() => {
|
||||
async function fetchUser() {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const userData = await getCurrentUser();
|
||||
setUser(userData.data.user);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
// React.useEffect(() => {
|
||||
// async function fetchUser() {
|
||||
// try {
|
||||
// setIsLoading(true);
|
||||
// const userData = await getCurrentUser();
|
||||
// setUser(userData.data.user);
|
||||
// } catch (error) {
|
||||
// console.error("Failed to fetch user:", error);
|
||||
// } finally {
|
||||
// setIsLoading(false);
|
||||
// }
|
||||
// }
|
||||
|
||||
fetchUser();
|
||||
}, []);
|
||||
// fetchUser();
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<Sidebar collapsible="icon" {...props}>
|
||||
|
@ -51,7 +49,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||
<NavReports reports={navData.reports} />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={user} />
|
||||
<NavUser user={user ?? null} />
|
||||
</SidebarFooter>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
|
|
|
@ -24,13 +24,13 @@ import {
|
|||
useSidebar,
|
||||
} from "@/app/_components/ui/sidebar";
|
||||
import { IconLogout, IconSettings, IconSparkles } from "@tabler/icons-react";
|
||||
import type { User } from "@/src/entities/models/users/users.model";
|
||||
import type { IUserSchema } from "@/src/entities/models/users/users.model";
|
||||
// import { signOut } from "@/app/(pages)/(auth)/action";
|
||||
import { SettingsDialog } from "../settings/setting-dialog";
|
||||
import { useSignOutHandler } from "@/app/(pages)/(auth)/handler";
|
||||
import { AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogCancel, AlertDialogAction } from "@/app/_components/ui/alert-dialog";
|
||||
|
||||
export function NavUser({ user }: { user: User | null }) {
|
||||
export function NavUser({ user }: { user: IUserSchema | null }) {
|
||||
const { isMobile } = useSidebar();
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import type React from "react";
|
||||
|
||||
import type { User } from "@/src/entities/models/users/users.model";
|
||||
import type { IUserSchema } from "@/src/entities/models/users/users.model";
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
@ -30,7 +30,6 @@ import { useRef, useState } from "react";
|
|||
import { ScrollArea } from "@/app/_components/ui/scroll-area";
|
||||
import {
|
||||
updateUser,
|
||||
uploadAvatar,
|
||||
} from "@/app/(pages)/(admin)/dashboard/user-management/action";
|
||||
|
||||
const profileFormSchema = z.object({
|
||||
|
@ -41,7 +40,7 @@ const profileFormSchema = z.object({
|
|||
type ProfileFormValues = z.infer<typeof profileFormSchema>;
|
||||
|
||||
interface ProfileSettingsProps {
|
||||
user: User | null;
|
||||
user: IUserSchema | null;
|
||||
}
|
||||
|
||||
export function ProfileSettings({ user }: ProfileSettingsProps) {
|
||||
|
@ -70,12 +69,12 @@ export function ProfileSettings({ user }: ProfileSettingsProps) {
|
|||
setIsUploading(true);
|
||||
|
||||
// Upload avatar to storage
|
||||
const publicUrl = await uploadAvatar(user.id, user.email, file);
|
||||
// const publicUrl = await uploadAvatar(user.id, user.email, file);
|
||||
|
||||
console.log("publicUrl", publicUrl);
|
||||
// console.log("publicUrl", publicUrl);
|
||||
|
||||
// Update the form value
|
||||
form.setValue("avatar", publicUrl);
|
||||
// form.setValue("avatar", publicUrl);
|
||||
} catch (error) {
|
||||
console.error("Error uploading avatar:", error);
|
||||
} finally {
|
||||
|
|
|
@ -3,15 +3,17 @@ import { createClient } from "@/app/_utils/supabase/server";
|
|||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function DashboardPage() {
|
||||
const supabase = await createClient();
|
||||
// const supabase = await createClient();
|
||||
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
// const {
|
||||
// data: { user },
|
||||
// } = await supabase.auth.getUser();
|
||||
|
||||
if (!session) {
|
||||
return redirect("/sign-in");
|
||||
}
|
||||
// if (!user) {
|
||||
// return redirect("/sign-in");
|
||||
// }
|
||||
|
||||
// console.log("user", user);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -20,7 +22,7 @@ export default async function DashboardPage() {
|
|||
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||
<div className="aspect-video rounded-xl bg-muted/50">
|
||||
<pre className="text-xs font-mono p-3 rounded border overflow-auto">
|
||||
{JSON.stringify(session, null, 2)}
|
||||
{/* {JSON.stringify(user, null, 2)} */}
|
||||
</pre>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -1,155 +1,66 @@
|
|||
import type React from "react";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/app/_components/ui/dialog";
|
||||
import { Button } from "@/app/_components/ui/button";
|
||||
import { Input } from "@/app/_components/ui/input";
|
||||
import { Checkbox } from "@/app/_components/ui/checkbox";
|
||||
import { createUser } from "@/app/(pages)/(admin)/dashboard/user-management/action";
|
||||
import { toast } from "sonner";
|
||||
import { Mail, Lock, Loader2, X } from "lucide-react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/app/_components/ui/dialog"
|
||||
import { Button } from "@/app/_components/ui/button"
|
||||
import { Mail, Lock, Loader2 } from "lucide-react"
|
||||
import { useAddUserDialogHandler } from "../handler"
|
||||
import { ReactHookFormField } from "@/app/_components/react-hook-form-field"
|
||||
|
||||
interface AddUserDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onUserAdded: () => void;
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onUserAdded: () => void
|
||||
}
|
||||
|
||||
export function AddUserDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onUserAdded,
|
||||
}: AddUserDialogProps) {
|
||||
const [formData, setFormData] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
emailConfirm: true,
|
||||
});
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const { mutate: createUserMutation, isPending } = useMutation({
|
||||
mutationKey: ["createUser"],
|
||||
mutationFn: createUser,
|
||||
onSuccess: () => {
|
||||
toast.success("User created successfully.");
|
||||
onUserAdded();
|
||||
onOpenChange(false);
|
||||
setFormData({
|
||||
email: "",
|
||||
password: "",
|
||||
emailConfirm: true,
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Failed to create user.");
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
await createUserMutation({
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
email_confirm: formData.emailConfirm,
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error("Failed to create user.");
|
||||
return;
|
||||
}
|
||||
};
|
||||
export function AddUserDialog({ open, onOpenChange, onUserAdded }: AddUserDialogProps) {
|
||||
const {
|
||||
register,
|
||||
errors,
|
||||
isPending,
|
||||
handleSubmit,
|
||||
handleOpenChange,
|
||||
} = useAddUserDialogHandler({ onUserAdded, onOpenChange });
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md border-0 ">
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="sm:max-w-md border-0">
|
||||
<DialogHeader className="flex flex-row items-center justify-between space-y-0 space-x-4 pb-4">
|
||||
<DialogTitle className="text-xl font-semibold ">
|
||||
Create a new user
|
||||
</DialogTitle>
|
||||
{/* <Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-zinc-400 hover: hover:bg-zinc-800"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button> */}
|
||||
<DialogTitle className="text-xl font-semibold">Create a new user</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="email" className="text-sm text-zinc-400">
|
||||
Email address
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-2.5 h-5 w-5 text-zinc-500" />
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="user@example.com"
|
||||
value={formData.email}
|
||||
onChange={handleInputChange}
|
||||
className="pl-10 placeholder:text-zinc-500 "
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="password" className="text-sm text-zinc-400">
|
||||
User Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-2.5 h-5 w-5 text-zinc-500" />
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
placeholder="••••••••"
|
||||
value={formData.password}
|
||||
onChange={handleInputChange}
|
||||
className="pl-10 placeholder:text-zinc-500 "
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ReactHookFormField
|
||||
label="Email address"
|
||||
icon={Mail}
|
||||
placeholder="user@example.com"
|
||||
error={errors.email}
|
||||
registration={register("email")}
|
||||
/>
|
||||
|
||||
<ReactHookFormField
|
||||
label="Password"
|
||||
icon={Lock}
|
||||
placeholder="••••••••"
|
||||
type="password"
|
||||
error={errors.password}
|
||||
registration={register("password")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* <div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="email-confirm"
|
||||
checked={formData.emailConfirm}
|
||||
onCheckedChange={(checked) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
emailConfirm: checked as boolean,
|
||||
}))
|
||||
}
|
||||
id="email_confirm"
|
||||
{...register("email_confirm")}
|
||||
className="border-zinc-700"
|
||||
/>
|
||||
<label htmlFor="email-confirm" className="text-sm ">
|
||||
<label htmlFor="email_confirm" className="text-sm">
|
||||
Auto Confirm User?
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-sm text-zinc-500 pl-6">
|
||||
A confirmation email will not be sent when creating a user via
|
||||
this form.
|
||||
</div> */}
|
||||
<p className="text-sm text-zinc-500">
|
||||
A confirmation email will not be sent when creating a user via this form.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button type="submit" disabled={isPending} className="w-full ">
|
||||
<Button type="submit" disabled={isPending} className="w-full">
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
|
@ -164,3 +75,4 @@ export function AddUserDialog({
|
|||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,11 @@ import { Textarea } from "@/app/_components/ui/textarea";
|
|||
import { useMutation } from "@tanstack/react-query";
|
||||
import { inviteUser } from "@/app/(pages)/(admin)/dashboard/user-management/action";
|
||||
import { toast } from "sonner";
|
||||
import { useInviteUserHandler } from "../handler";
|
||||
import { ReactHookFormField } from "@/app/_components/react-hook-form-field";
|
||||
import { Loader2, MailIcon } from "lucide-react";
|
||||
import { Separator } from "@/app/_components/ui/separator";
|
||||
|
||||
|
||||
interface InviteUserDialogProps {
|
||||
open: boolean;
|
||||
|
@ -28,52 +33,18 @@ export function InviteUserDialog({
|
|||
onOpenChange,
|
||||
onUserInvited,
|
||||
}: InviteUserDialogProps) {
|
||||
const [formData, setFormData] = useState({
|
||||
email: "",
|
||||
metadata: "{}",
|
||||
});
|
||||
|
||||
const handleInputChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const { mutate: inviteUserMutation, isPending } = useMutation({
|
||||
mutationKey: ["inviteUser"],
|
||||
mutationFn: inviteUser,
|
||||
onSuccess: () => {
|
||||
toast.success("Invitation sent");
|
||||
onUserInvited();
|
||||
onOpenChange(false);
|
||||
setFormData({
|
||||
email: "",
|
||||
metadata: "{}",
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Failed to send invitation");
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
let metadata = {};
|
||||
try {
|
||||
metadata = JSON.parse(formData.metadata);
|
||||
inviteUserMutation({
|
||||
email: formData.email,
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error("Invalid JSON. Please check your metadata format.");
|
||||
return;
|
||||
}
|
||||
};
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
errors,
|
||||
isPending,
|
||||
handleOpenChange
|
||||
} = useInviteUserHandler({ onUserInvited, onOpenChange });
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Invite User</DialogTitle>
|
||||
|
@ -81,30 +52,26 @@ export function InviteUserDialog({
|
|||
Send an invitation email to a new user.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="invite-email">Email *</Label>
|
||||
<Input
|
||||
id="invite-email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={handleInputChange}
|
||||
placeholder="example@gmail.com"
|
||||
/>
|
||||
</div>
|
||||
<Separator className="" />
|
||||
<form onSubmit={handleSubmit} className="space-y-8">
|
||||
<ReactHookFormField
|
||||
label="Email"
|
||||
icon={MailIcon}
|
||||
placeholder="example@gmail.com"
|
||||
error={errors.email}
|
||||
registration={register("email")}
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
{isPending ? "Sending..." : "Send Invitation"}
|
||||
<Button className="flex w-full" type="submit" disabled={isPending}>
|
||||
{
|
||||
isPending ? (
|
||||
<>
|
||||
<span className="mr-2">Invite user...</span>
|
||||
<Loader2 className="animate-spin" />
|
||||
</>
|
||||
) : "Invite User"
|
||||
}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
|
|
@ -6,7 +6,7 @@ import { useState, useRef } from "react";
|
|||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { User } from "@/src/entities/models/users/users.model";
|
||||
import type { IUserSchema } from "@/src/entities/models/users/users.model";
|
||||
|
||||
import {
|
||||
Form,
|
||||
|
@ -40,7 +40,7 @@ const profileFormSchema = z.object({
|
|||
type ProfileFormValues = z.infer<typeof profileFormSchema>;
|
||||
|
||||
interface ProfileFormProps {
|
||||
user: User | null;
|
||||
user: IUserSchema | null;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -35,11 +35,10 @@ import {
|
|||
import {
|
||||
banUser,
|
||||
deleteUser,
|
||||
sendMagicLink,
|
||||
sendPasswordRecovery,
|
||||
unbanUser,
|
||||
} from "@/app/(pages)/(admin)/dashboard/user-management/action";
|
||||
import { format } from "date-fns";
|
||||
import { sendMagicLink, sendPasswordRecovery } from "@/app/(pages)/(auth)/action";
|
||||
|
||||
interface UserDetailSheetProps {
|
||||
open: boolean;
|
||||
|
@ -129,7 +128,8 @@ export function UserDetailSheet({
|
|||
if (user.banned_until) {
|
||||
return unbanUser(user.id);
|
||||
} else {
|
||||
return banUser(user.id);
|
||||
const ban_duration = "7h"; // Example: Ban duration set to 7 days
|
||||
return banUser(user.id, ban_duration);
|
||||
}
|
||||
},
|
||||
onMutate: () => {
|
||||
|
|
|
@ -5,7 +5,7 @@ import type * as z from "zod"
|
|||
|
||||
import { Loader2 } from "lucide-react"
|
||||
|
||||
import { UpdateUserParamsSchema, type User } from "@/src/entities/models/users/users.model"
|
||||
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||
|
||||
// UI Components
|
||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/app/_components/ui/sheet"
|
||||
|
@ -17,20 +17,21 @@ import { FormFieldWrapper } from "@/app/_components/form-wrapper"
|
|||
import { useMutation } from "@tanstack/react-query"
|
||||
import { updateUser } from "../action"
|
||||
import { toast } from "sonner"
|
||||
import { UpdateUserSchema } from "@/src/entities/models/users/update-user.model"
|
||||
|
||||
type UserProfileFormValues = z.infer<typeof UpdateUserParamsSchema>
|
||||
type UserProfileFormValues = z.infer<typeof UpdateUserSchema>
|
||||
|
||||
interface UserProfileSheetProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
userData?: User
|
||||
userData?: IUserSchema
|
||||
onUserUpdated: () => void
|
||||
}
|
||||
|
||||
export function UserProfileSheet({ open, onOpenChange, userData, onUserUpdated }: UserProfileSheetProps) {
|
||||
// Initialize form with user data
|
||||
const form = useForm<UserProfileFormValues>({
|
||||
resolver: zodResolver(UpdateUserParamsSchema),
|
||||
resolver: zodResolver(UpdateUserSchema),
|
||||
defaultValues: {
|
||||
email: userData?.email || undefined,
|
||||
encrypted_password: userData?.encrypted_password || undefined,
|
||||
|
|
|
@ -1,147 +0,0 @@
|
|||
// "use client"
|
||||
|
||||
// import { zodResolver } from "@hookform/resolvers/zod"
|
||||
// import { useForm } from "react-hook-form"
|
||||
// import { z } from "zod"
|
||||
|
||||
// import { Button } from "@/app/_components/ui/button"
|
||||
// import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/app/_components/ui/form"
|
||||
// import { Input } from "@/app/_components/ui/input"
|
||||
// import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select"
|
||||
// import { useState } from "react"
|
||||
// import { User } from "./column"
|
||||
// import { updateUser } from "../../user-management/action"
|
||||
// import { toast } from "@/app/_hooks/use-toast"
|
||||
|
||||
// const userFormSchema = z.object({
|
||||
// email: z.string().email({ message: "Please enter a valid email address" }),
|
||||
// first_name: z.string().nullable(),
|
||||
// last_name: z.string().nullable(),
|
||||
// role: z.enum(["user", "admin", "moderator"]),
|
||||
// })
|
||||
|
||||
// type UserFormValues = z.infer<typeof userFormSchema>
|
||||
|
||||
// interface UserFormProps {
|
||||
// user: User
|
||||
// }
|
||||
|
||||
// export function UserForm({ user }: UserFormProps) {
|
||||
// const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
// const form = useForm<UserFormValues>({
|
||||
// resolver: zodResolver(userFormSchema),
|
||||
// defaultValues: {
|
||||
// email: user.email,
|
||||
// first_name: user.first_name,
|
||||
// last_name: user.last_name,
|
||||
// role: user.role as "user" | "admin" | "moderator",
|
||||
// },
|
||||
// })
|
||||
|
||||
// async function onSubmit(data: UserFormValues) {
|
||||
// try {
|
||||
// setIsSubmitting(true)
|
||||
// await updateUser(user.id, data)
|
||||
// toast({
|
||||
// title: "User updated",
|
||||
// description: "The user" + user.email + " has been updated.",
|
||||
// })
|
||||
// } catch (error) {
|
||||
// toast({
|
||||
// title: "Failed to update user",
|
||||
// description: "An error occurred while updating the user.",
|
||||
// variant: "destructive",
|
||||
// })
|
||||
// console.error(error)
|
||||
// } finally {
|
||||
// setIsSubmitting(false)
|
||||
// }
|
||||
// }
|
||||
|
||||
// return (
|
||||
// <Form {...form}>
|
||||
// <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="email"
|
||||
// render={({ field }) => (
|
||||
// <FormItem>
|
||||
// <FormLabel className="text-white">Email</FormLabel>
|
||||
// <FormControl>
|
||||
// <Input
|
||||
// {...field}
|
||||
// className="bg-[#1c1c1c] border-[#2a2a2a] text-white focus-visible:ring-[#2a2a2a] focus-visible:ring-offset-0"
|
||||
// />
|
||||
// </FormControl>
|
||||
// <FormDescription className="text-gray-400">This is the user's email address.</FormDescription>
|
||||
// <FormMessage />
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
// <div className="grid grid-cols-2 gap-4">
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="first_name"
|
||||
// render={({ field }) => (
|
||||
// <FormItem>
|
||||
// <FormLabel className="text-white">First Name</FormLabel>
|
||||
// <FormControl>
|
||||
// <Input
|
||||
// {...field}
|
||||
// value={field.value || ""}
|
||||
// className="bg-[#1c1c1c] border-[#2a2a2a] text-white focus-visible:ring-[#2a2a2a] focus-visible:ring-offset-0"
|
||||
// />
|
||||
// </FormControl>
|
||||
// <FormMessage />
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="last_name"
|
||||
// render={({ field }) => (
|
||||
// <FormItem>
|
||||
// <FormLabel className="text-white">Last Name</FormLabel>
|
||||
// <FormControl>
|
||||
// <Input
|
||||
// {...field}
|
||||
// value={field.value || ""}
|
||||
// className="bg-[#1c1c1c] border-[#2a2a2a] text-white focus-visible:ring-[#2a2a2a] focus-visible:ring-offset-0"
|
||||
// />
|
||||
// </FormControl>
|
||||
// <FormMessage />
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
// </div>
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="role"
|
||||
// render={({ field }) => (
|
||||
// <FormItem>
|
||||
// <FormLabel className="text-white">Role</FormLabel>
|
||||
// <Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
// <FormControl>
|
||||
// <SelectTrigger className="bg-[#1c1c1c] border-[#2a2a2a] text-white focus:ring-[#2a2a2a] focus:ring-offset-0">
|
||||
// <SelectValue placeholder="Select a role" />
|
||||
// </SelectTrigger>
|
||||
// </FormControl>
|
||||
// <SelectContent className="bg-[#1c1c1c] border-[#2a2a2a] text-white">
|
||||
// <SelectItem value="user">User</SelectItem>
|
||||
// <SelectItem value="admin">Admin</SelectItem>
|
||||
// <SelectItem value="moderator">Moderator</SelectItem>
|
||||
// </SelectContent>
|
||||
// </Select>
|
||||
// <FormDescription className="text-gray-400">The user's role determines their permissions.</FormDescription>
|
||||
// <FormMessage />
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
// <Button type="submit" disabled={isSubmitting} className="bg-emerald-600 hover:bg-emerald-700 text-white">
|
||||
// {isSubmitting ? "Saving..." : "Save changes"}
|
||||
// </Button>
|
||||
// </form>
|
||||
// </Form>
|
||||
// )
|
||||
// }
|
|
@ -16,570 +16,63 @@ import {
|
|||
} from "lucide-react";
|
||||
import { Button } from "@/app/_components/ui/button";
|
||||
import { Input } from "@/app/_components/ui/input";
|
||||
import { Badge } from "@/app/_components/ui/badge";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuCheckboxItem,
|
||||
} from "@/app/_components/ui/dropdown-menu";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { fetchUsers } from "@/app/(pages)/(admin)/dashboard/user-management/action";
|
||||
import type { User } from "@/src/entities/models/users/users.model";
|
||||
|
||||
import { DataTable } from "./data-table";
|
||||
import { InviteUserDialog } from "./invite-user";
|
||||
import { AddUserDialog } from "./add-user-dialog";
|
||||
import { UserDetailSheet } from "./sheet";
|
||||
import { Avatar } from "@radix-ui/react-avatar";
|
||||
import Image from "next/image";
|
||||
import type { ColumnDef, HeaderContext } from "@tanstack/react-table";
|
||||
import { UserProfileSheet } from "./update-user";
|
||||
|
||||
type UserFilterOptions = {
|
||||
email: string;
|
||||
phone: string;
|
||||
lastSignIn: string;
|
||||
createdAt: string;
|
||||
status: string[];
|
||||
};
|
||||
|
||||
type UserTableColumn = ColumnDef<User, User>;
|
||||
import { filterUsers, useUserManagementHandlers } from "../handler";
|
||||
import { createUserColumns } from "./users-table";
|
||||
import { useGetUsersQuery } from "../queries";
|
||||
|
||||
export default function UserManagement() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [detailUser, setDetailUser] = useState<User | null>(null);
|
||||
const [updateUser, setUpdateUser] = useState<User | null>(null);
|
||||
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
||||
const [isUpdateOpen, setIsUpdateOpen] = useState(false);
|
||||
const [isAddUserOpen, setIsAddUserOpen] = useState(false);
|
||||
const [isInviteUserOpen, setIsInviteUserOpen] = useState(false);
|
||||
|
||||
// Filter states
|
||||
const [filters, setFilters] = useState<UserFilterOptions>({
|
||||
email: "",
|
||||
phone: "",
|
||||
lastSignIn: "",
|
||||
createdAt: "",
|
||||
status: [],
|
||||
});
|
||||
|
||||
// Use React Query to fetch users
|
||||
const {
|
||||
data: users = [],
|
||||
isLoading,
|
||||
isPending,
|
||||
refetch,
|
||||
isPlaceholderData,
|
||||
} = useQuery<User[]>({
|
||||
queryKey: ["users"],
|
||||
queryFn: fetchUsers,
|
||||
placeholderData: keepPreviousData,
|
||||
throwOnError: true,
|
||||
});
|
||||
} = useGetUsersQuery();
|
||||
|
||||
// Handle opening the detail sheet
|
||||
const handleUserClick = (user: User) => {
|
||||
setDetailUser(user);
|
||||
setIsSheetOpen(true);
|
||||
};
|
||||
|
||||
// Handle opening the update sheet
|
||||
const handleUserUpdate = (user: User) => {
|
||||
setUpdateUser(user);
|
||||
setIsUpdateOpen(true);
|
||||
};
|
||||
|
||||
// Close detail sheet when update sheet opens
|
||||
useEffect(() => {
|
||||
if (isUpdateOpen) {
|
||||
setIsSheetOpen(false);
|
||||
}
|
||||
}, [isUpdateOpen]);
|
||||
|
||||
// Reset detail user when sheet closes
|
||||
useEffect(() => {
|
||||
if (!isSheetOpen) {
|
||||
// Use a small delay to prevent flickering if another sheet is opening
|
||||
const timer = setTimeout(() => {
|
||||
if (!isSheetOpen && !isUpdateOpen) {
|
||||
setDetailUser(null);
|
||||
}
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isSheetOpen, isUpdateOpen]);
|
||||
|
||||
// Reset update user when update sheet closes
|
||||
useEffect(() => {
|
||||
if (!isUpdateOpen) {
|
||||
// Use a small delay to prevent flickering if another sheet is opening
|
||||
const timer = setTimeout(() => {
|
||||
if (!isUpdateOpen) {
|
||||
setUpdateUser(null);
|
||||
}
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isUpdateOpen]);
|
||||
// User management handler
|
||||
const {
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
detailUser,
|
||||
updateUser,
|
||||
isSheetOpen,
|
||||
setIsSheetOpen,
|
||||
isUpdateOpen,
|
||||
setIsUpdateOpen,
|
||||
isAddUserOpen,
|
||||
setIsAddUserOpen,
|
||||
isInviteUserOpen,
|
||||
setIsInviteUserOpen,
|
||||
filters,
|
||||
setFilters,
|
||||
handleUserClick,
|
||||
handleUserUpdate,
|
||||
clearFilters,
|
||||
getActiveFilterCount,
|
||||
} = useUserManagementHandlers(refetch)
|
||||
|
||||
// Apply filters to users
|
||||
const filteredUsers = useMemo(() => {
|
||||
return users.filter((user) => {
|
||||
// Global search
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
const matchesSearch =
|
||||
user.email?.toLowerCase().includes(query) ||
|
||||
user.phone?.toLowerCase().includes(query) ||
|
||||
user.id.toLowerCase().includes(query);
|
||||
return filterUsers(users, searchQuery, filters)
|
||||
}, [users, searchQuery, filters])
|
||||
|
||||
if (!matchesSearch) return false;
|
||||
}
|
||||
// Get active filter count
|
||||
const activeFilterCount = getActiveFilterCount()
|
||||
|
||||
// Email filter
|
||||
if (
|
||||
filters.email &&
|
||||
!user.email?.toLowerCase().includes(filters.email.toLowerCase())
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Phone filter
|
||||
if (
|
||||
filters.phone &&
|
||||
!user.phone?.toLowerCase().includes(filters.phone.toLowerCase())
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Last sign in filter
|
||||
if (filters.lastSignIn) {
|
||||
if (filters.lastSignIn === "never" && user.last_sign_in_at) {
|
||||
return false;
|
||||
} else if (filters.lastSignIn === "today") {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const signInDate = user.last_sign_in_at
|
||||
? new Date(user.last_sign_in_at)
|
||||
: null;
|
||||
if (!signInDate || signInDate < today) return false;
|
||||
} else if (filters.lastSignIn === "week") {
|
||||
const weekAgo = new Date();
|
||||
weekAgo.setDate(weekAgo.getDate() - 7);
|
||||
const signInDate = user.last_sign_in_at
|
||||
? new Date(user.last_sign_in_at)
|
||||
: null;
|
||||
if (!signInDate || signInDate < weekAgo) return false;
|
||||
} else if (filters.lastSignIn === "month") {
|
||||
const monthAgo = new Date();
|
||||
monthAgo.setMonth(monthAgo.getMonth() - 1);
|
||||
const signInDate = user.last_sign_in_at
|
||||
? new Date(user.last_sign_in_at)
|
||||
: null;
|
||||
if (!signInDate || signInDate < monthAgo) return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Created at filter
|
||||
if (filters.createdAt) {
|
||||
if (filters.createdAt === "today") {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const createdAt = user.created_at
|
||||
? user.created_at
|
||||
? new Date(user.created_at)
|
||||
: new Date()
|
||||
: new Date();
|
||||
if (createdAt < today) return false;
|
||||
} else if (filters.createdAt === "week") {
|
||||
const weekAgo = new Date();
|
||||
weekAgo.setDate(weekAgo.getDate() - 7);
|
||||
const createdAt = user.created_at
|
||||
? new Date(user.created_at)
|
||||
: new Date();
|
||||
if (createdAt < weekAgo) return false;
|
||||
} else if (filters.createdAt === "month") {
|
||||
const monthAgo = new Date();
|
||||
monthAgo.setMonth(monthAgo.getMonth() - 1);
|
||||
const createdAt = user.created_at
|
||||
? new Date(user.created_at)
|
||||
: new Date();
|
||||
if (createdAt < monthAgo) return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if (filters.status.length > 0) {
|
||||
const userStatus = user.banned_until
|
||||
? "banned"
|
||||
: !user.email_confirmed_at
|
||||
? "unconfirmed"
|
||||
: "active";
|
||||
|
||||
if (!filters.status.includes(userStatus)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [users, searchQuery, filters]);
|
||||
|
||||
const clearFilters = () => {
|
||||
setFilters({
|
||||
email: "",
|
||||
phone: "",
|
||||
lastSignIn: "",
|
||||
createdAt: "",
|
||||
status: [],
|
||||
});
|
||||
};
|
||||
|
||||
const activeFilterCount = Object.values(filters).filter(
|
||||
(value) =>
|
||||
(typeof value === "string" && value !== "") ||
|
||||
(Array.isArray(value) && value.length > 0)
|
||||
).length;
|
||||
|
||||
const columns: UserTableColumn[] = [
|
||||
{
|
||||
id: "email",
|
||||
header: ({ column }: HeaderContext<User, User>) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Email</span>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
|
||||
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<div className="p-2">
|
||||
<Input
|
||||
placeholder="Filter by email..."
|
||||
value={filters.email}
|
||||
onChange={(e) =>
|
||||
setFilters({ ...filters, email: e.target.value })
|
||||
}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => setFilters({ ...filters, email: "" })}
|
||||
>
|
||||
Clear filter
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-medium">
|
||||
{row.original.profile?.avatar ? (
|
||||
<Image
|
||||
src={row.original.profile.avatar || "/placeholder.svg"}
|
||||
alt="Avatar"
|
||||
className="w-full h-full rounded-full"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
) : (
|
||||
row.original.email?.[0]?.toUpperCase() || "?"
|
||||
)}
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="font-medium">
|
||||
{row.original.email || "No email"}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{row.original.id}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "phone",
|
||||
header: ({ column }: HeaderContext<User, User>) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Phone</span>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
|
||||
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<div className="p-2">
|
||||
<Input
|
||||
placeholder="Filter by phone..."
|
||||
value={filters.phone}
|
||||
onChange={(e) =>
|
||||
setFilters({ ...filters, phone: e.target.value })
|
||||
}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => setFilters({ ...filters, phone: "" })}
|
||||
>
|
||||
Clear filter
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => row.original.phone || "-",
|
||||
},
|
||||
{
|
||||
id: "lastSignIn",
|
||||
header: ({ column }: HeaderContext<User, User>) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Last Sign In</span>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
|
||||
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={filters.lastSignIn === "today"}
|
||||
onCheckedChange={() =>
|
||||
setFilters({
|
||||
...filters,
|
||||
lastSignIn: filters.lastSignIn === "today" ? "" : "today",
|
||||
})
|
||||
}
|
||||
>
|
||||
Today
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={filters.lastSignIn === "week"}
|
||||
onCheckedChange={() =>
|
||||
setFilters({
|
||||
...filters,
|
||||
lastSignIn: filters.lastSignIn === "week" ? "" : "week",
|
||||
})
|
||||
}
|
||||
>
|
||||
Last 7 days
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={filters.lastSignIn === "month"}
|
||||
onCheckedChange={() =>
|
||||
setFilters({
|
||||
...filters,
|
||||
lastSignIn: filters.lastSignIn === "month" ? "" : "month",
|
||||
})
|
||||
}
|
||||
>
|
||||
Last 30 days
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={filters.lastSignIn === "never"}
|
||||
onCheckedChange={() =>
|
||||
setFilters({
|
||||
...filters,
|
||||
lastSignIn: filters.lastSignIn === "never" ? "" : "never",
|
||||
})
|
||||
}
|
||||
>
|
||||
Never
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => setFilters({ ...filters, lastSignIn: "" })}
|
||||
>
|
||||
Clear filter
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
return row.original.last_sign_in_at
|
||||
? new Date(row.original.last_sign_in_at).toLocaleString()
|
||||
: "Never";
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "createdAt",
|
||||
header: ({ column }: HeaderContext<User, User>) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Created At</span>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
|
||||
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={filters.createdAt === "today"}
|
||||
onCheckedChange={() =>
|
||||
setFilters({
|
||||
...filters,
|
||||
createdAt: filters.createdAt === "today" ? "" : "today",
|
||||
})
|
||||
}
|
||||
>
|
||||
Today
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={filters.createdAt === "week"}
|
||||
onCheckedChange={() =>
|
||||
setFilters({
|
||||
...filters,
|
||||
createdAt: filters.createdAt === "week" ? "" : "week",
|
||||
})
|
||||
}
|
||||
>
|
||||
Last 7 days
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={filters.createdAt === "month"}
|
||||
onCheckedChange={() =>
|
||||
setFilters({
|
||||
...filters,
|
||||
createdAt: filters.createdAt === "month" ? "" : "month",
|
||||
})
|
||||
}
|
||||
>
|
||||
Last 30 days
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => setFilters({ ...filters, createdAt: "" })}
|
||||
>
|
||||
Clear filter
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
return row.original.created_at
|
||||
? new Date(row.original.created_at).toLocaleString()
|
||||
: "N/A";
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
header: ({ column }: HeaderContext<User, User>) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Status</span>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
|
||||
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={filters.status.includes("active")}
|
||||
onCheckedChange={() => {
|
||||
const newStatus = [...filters.status];
|
||||
if (newStatus.includes("active")) {
|
||||
newStatus.splice(newStatus.indexOf("active"), 1);
|
||||
} else {
|
||||
newStatus.push("active");
|
||||
}
|
||||
setFilters({ ...filters, status: newStatus });
|
||||
}}
|
||||
>
|
||||
Active
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={filters.status.includes("unconfirmed")}
|
||||
onCheckedChange={() => {
|
||||
const newStatus = [...filters.status];
|
||||
if (newStatus.includes("unconfirmed")) {
|
||||
newStatus.splice(newStatus.indexOf("unconfirmed"), 1);
|
||||
} else {
|
||||
newStatus.push("unconfirmed");
|
||||
}
|
||||
setFilters({ ...filters, status: newStatus });
|
||||
}}
|
||||
>
|
||||
Unconfirmed
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={filters.status.includes("banned")}
|
||||
onCheckedChange={() => {
|
||||
const newStatus = [...filters.status];
|
||||
if (newStatus.includes("banned")) {
|
||||
newStatus.splice(newStatus.indexOf("banned"), 1);
|
||||
} else {
|
||||
newStatus.push("banned");
|
||||
}
|
||||
setFilters({ ...filters, status: newStatus });
|
||||
}}
|
||||
>
|
||||
Banned
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => setFilters({ ...filters, status: [] })}
|
||||
>
|
||||
Clear filter
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
if (row.original.banned_until) {
|
||||
return <Badge variant="destructive">Banned</Badge>;
|
||||
}
|
||||
if (!row.original.email_confirmed_at) {
|
||||
return <Badge variant="outline">Unconfirmed</Badge>;
|
||||
}
|
||||
return <Badge variant="default">Active</Badge>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "",
|
||||
cell: ({ row }) => (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
{/* Add this wrapper */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => handleUserUpdate(row.original)}>
|
||||
<UserPen className="h-4 w-4 mr-2 text-blue-500" />
|
||||
Update
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
/* handle delete */
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2 text-red-500" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
/* handle ban */
|
||||
}}
|
||||
>
|
||||
<ShieldAlert className="h-4 w-4 mr-2 text-yellow-500" />
|
||||
{row.original.banned_until != null ? "Unban" : "Ban"}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
// Create table columns
|
||||
const columns = createUserColumns(filters, setFilters, handleUserUpdate)
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
@ -642,7 +135,7 @@ export default function UserManagement() {
|
|||
<DataTable
|
||||
columns={columns}
|
||||
data={filteredUsers}
|
||||
loading={isLoading}
|
||||
loading={isPending}
|
||||
onRowClick={(user) => handleUserClick(user)}
|
||||
/>
|
||||
{detailUser && (
|
||||
|
@ -653,16 +146,8 @@ export default function UserManagement() {
|
|||
onUserUpdate={() => refetch()}
|
||||
/>
|
||||
)}
|
||||
<AddUserDialog
|
||||
open={isAddUserOpen}
|
||||
onOpenChange={setIsAddUserOpen}
|
||||
onUserAdded={() => refetch()}
|
||||
/>
|
||||
<InviteUserDialog
|
||||
open={isInviteUserOpen}
|
||||
onOpenChange={setIsInviteUserOpen}
|
||||
onUserInvited={() => refetch()}
|
||||
/>
|
||||
<AddUserDialog open={isAddUserOpen} onOpenChange={setIsAddUserOpen} onUserAdded={() => refetch()} />
|
||||
<InviteUserDialog open={isInviteUserOpen} onOpenChange={setIsInviteUserOpen} onUserInvited={() => refetch()} />
|
||||
{updateUser && (
|
||||
<UserProfileSheet
|
||||
open={isUpdateOpen}
|
||||
|
@ -672,5 +157,5 @@ export default function UserManagement() {
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Card, CardContent } from "@/app/_components/ui/card";
|
||||
import { Users, UserCheck, UserX } from "lucide-react";
|
||||
import { fetchUsers } from "@/app/(pages)/(admin)/dashboard/user-management/action";
|
||||
import { User } from "@/src/entities/models/users/users.model";
|
||||
import { useNavigations } from "@/app/_hooks/use-navigations";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { IUserSchema } from "@/src/entities/models/users/users.model";
|
||||
import { useGetUsersQuery } from "../queries";
|
||||
|
||||
|
||||
function calculateUserStats(users: IUserSchema[] | undefined) {
|
||||
if (!users || !Array.isArray(users)) {
|
||||
return {
|
||||
totalUsers: 0,
|
||||
activeUsers: 0,
|
||||
inactiveUsers: 0,
|
||||
activePercentage: '0.0',
|
||||
inactivePercentage: '0.0',
|
||||
};
|
||||
}
|
||||
|
||||
function calculateUserStats(users: User[]) {
|
||||
const totalUsers = users.length;
|
||||
const activeUsers = users.filter(
|
||||
(user) => !user.banned_until && user.email_confirmed_at
|
||||
|
@ -21,21 +28,18 @@ function calculateUserStats(users: User[]) {
|
|||
activeUsers,
|
||||
inactiveUsers,
|
||||
activePercentage:
|
||||
totalUsers > 0 ? ((activeUsers / totalUsers) * 100).toFixed(1) : "0.0",
|
||||
totalUsers > 0 ? ((activeUsers / totalUsers) * 100).toFixed(1) : '0.0',
|
||||
inactivePercentage:
|
||||
totalUsers > 0 ? ((inactiveUsers / totalUsers) * 100).toFixed(1) : "0.0",
|
||||
totalUsers > 0 ? ((inactiveUsers / totalUsers) * 100).toFixed(1) : '0.0',
|
||||
};
|
||||
}
|
||||
|
||||
export function UserStats() {
|
||||
const { data: users = [], isLoading } = useQuery({
|
||||
queryKey: ["users"],
|
||||
queryFn: fetchUsers,
|
||||
});
|
||||
const { data: users, isPending, error } = useGetUsersQuery();
|
||||
|
||||
const stats = calculateUserStats(users);
|
||||
|
||||
if (isLoading) {
|
||||
if (isPending) {
|
||||
return (
|
||||
<>
|
||||
{[...Array(3)].map((_, i) => (
|
||||
|
@ -53,21 +57,32 @@ export function UserStats() {
|
|||
);
|
||||
}
|
||||
|
||||
// Show error state if there's an error
|
||||
if (error) {
|
||||
return (
|
||||
<Card className="bg-background border-border">
|
||||
<CardContent className="p-6">
|
||||
<div className="text-destructive text-center">Error fetching data</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const cards = [
|
||||
{
|
||||
title: "Total Users",
|
||||
title: 'Total Users',
|
||||
value: stats.totalUsers,
|
||||
subtitle: "Updated just now",
|
||||
subtitle: 'Updated just now',
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
title: "Active Users",
|
||||
title: 'Active Users',
|
||||
value: stats.activeUsers,
|
||||
subtitle: `${stats.activePercentage}% of total users`,
|
||||
icon: UserCheck,
|
||||
},
|
||||
{
|
||||
title: "Inactive Users",
|
||||
title: 'Inactive Users',
|
||||
value: stats.inactiveUsers,
|
||||
subtitle: `${stats.inactivePercentage}% of total users`,
|
||||
icon: UserX,
|
||||
|
@ -92,4 +107,4 @@ export function UserStats() {
|
|||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,338 @@
|
|||
"use client"
|
||||
|
||||
import type { ColumnDef, HeaderContext } from "@tanstack/react-table"
|
||||
import type { IUserSchema, IUserFilterOptionsSchema } from "@/src/entities/models/users/users.model"
|
||||
import { ListFilter, MoreHorizontal, PenIcon as UserPen, Trash2, ShieldAlert } from "lucide-react"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuCheckboxItem,
|
||||
} from "@/app/_components/ui/dropdown-menu"
|
||||
import { Button } from "@/app/_components/ui/button"
|
||||
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"
|
||||
|
||||
export type UserTableColumn = ColumnDef<IUserSchema, IUserSchema>
|
||||
|
||||
export const createUserColumns = (
|
||||
filters: IUserFilterOptionsSchema,
|
||||
setFilters: (filters: IUserFilterOptionsSchema) => void,
|
||||
handleUserUpdate: (user: IUserSchema) => void,
|
||||
): UserTableColumn[] => {
|
||||
return [
|
||||
{
|
||||
id: "email",
|
||||
header: ({ column }: HeaderContext<IUserSchema, IUserSchema>) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Email</span>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
|
||||
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<div className="p-2">
|
||||
<Input
|
||||
placeholder="Filter by email..."
|
||||
value={filters.email}
|
||||
onChange={(e) => setFilters({ ...filters, email: e.target.value })}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => setFilters({ ...filters, email: "" })}>Clear filter</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-medium">
|
||||
{row.original.profile?.avatar ? (
|
||||
<Image
|
||||
src={row.original.profile.avatar || "/placeholder.svg"}
|
||||
alt="Avatar"
|
||||
className="w-full h-full rounded-full"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
) : (
|
||||
row.original.email?.[0]?.toUpperCase() || "?"
|
||||
)}
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="font-medium">{row.original.email || "No email"}</div>
|
||||
<div className="text-xs text-muted-foreground">{row.original.profile?.username}</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "phone",
|
||||
header: ({ column }: HeaderContext<IUserSchema, IUserSchema>) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Phone</span>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
|
||||
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<div className="p-2">
|
||||
<Input
|
||||
placeholder="Filter by phone..."
|
||||
value={filters.phone}
|
||||
onChange={(e) => setFilters({ ...filters, phone: e.target.value })}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => setFilters({ ...filters, phone: "" })}>Clear filter</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => row.original.phone || "-",
|
||||
},
|
||||
{
|
||||
id: "lastSignIn",
|
||||
header: ({ column }: HeaderContext<IUserSchema, IUserSchema>) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Last Sign In</span>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
|
||||
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={filters.lastSignIn === "today"}
|
||||
onCheckedChange={() =>
|
||||
setFilters({
|
||||
...filters,
|
||||
lastSignIn: filters.lastSignIn === "today" ? "" : "today",
|
||||
})
|
||||
}
|
||||
>
|
||||
Today
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={filters.lastSignIn === "week"}
|
||||
onCheckedChange={() =>
|
||||
setFilters({
|
||||
...filters,
|
||||
lastSignIn: filters.lastSignIn === "week" ? "" : "week",
|
||||
})
|
||||
}
|
||||
>
|
||||
Last 7 days
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={filters.lastSignIn === "month"}
|
||||
onCheckedChange={() =>
|
||||
setFilters({
|
||||
...filters,
|
||||
lastSignIn: filters.lastSignIn === "month" ? "" : "month",
|
||||
})
|
||||
}
|
||||
>
|
||||
Last 30 days
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={filters.lastSignIn === "never"}
|
||||
onCheckedChange={() =>
|
||||
setFilters({
|
||||
...filters,
|
||||
lastSignIn: filters.lastSignIn === "never" ? "" : "never",
|
||||
})
|
||||
}
|
||||
>
|
||||
Never
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => setFilters({ ...filters, lastSignIn: "" })}>
|
||||
Clear filter
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
return row.original.last_sign_in_at ? new Date(row.original.last_sign_in_at).toLocaleString() : "Never"
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "createdAt",
|
||||
header: ({ column }: HeaderContext<IUserSchema, IUserSchema>) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Created At</span>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
|
||||
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={filters.createdAt === "today"}
|
||||
onCheckedChange={() =>
|
||||
setFilters({
|
||||
...filters,
|
||||
createdAt: filters.createdAt === "today" ? "" : "today",
|
||||
})
|
||||
}
|
||||
>
|
||||
Today
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={filters.createdAt === "week"}
|
||||
onCheckedChange={() =>
|
||||
setFilters({
|
||||
...filters,
|
||||
createdAt: filters.createdAt === "week" ? "" : "week",
|
||||
})
|
||||
}
|
||||
>
|
||||
Last 7 days
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={filters.createdAt === "month"}
|
||||
onCheckedChange={() =>
|
||||
setFilters({
|
||||
...filters,
|
||||
createdAt: filters.createdAt === "month" ? "" : "month",
|
||||
})
|
||||
}
|
||||
>
|
||||
Last 30 days
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => setFilters({ ...filters, createdAt: "" })}>
|
||||
Clear filter
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
return row.original.created_at ? new Date(row.original.created_at).toLocaleString() : "N/A"
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
header: ({ column }: HeaderContext<IUserSchema, IUserSchema>) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Status</span>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
|
||||
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={filters.status.includes("active")}
|
||||
onCheckedChange={() => {
|
||||
const newStatus = [...filters.status]
|
||||
if (newStatus.includes("active")) {
|
||||
newStatus.splice(newStatus.indexOf("active"), 1)
|
||||
} else {
|
||||
newStatus.push("active")
|
||||
}
|
||||
setFilters({ ...filters, status: newStatus })
|
||||
}}
|
||||
>
|
||||
Active
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={filters.status.includes("unconfirmed")}
|
||||
onCheckedChange={() => {
|
||||
const newStatus = [...filters.status]
|
||||
if (newStatus.includes("unconfirmed")) {
|
||||
newStatus.splice(newStatus.indexOf("unconfirmed"), 1)
|
||||
} else {
|
||||
newStatus.push("unconfirmed")
|
||||
}
|
||||
setFilters({ ...filters, status: newStatus })
|
||||
}}
|
||||
>
|
||||
Unconfirmed
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={filters.status.includes("banned")}
|
||||
onCheckedChange={() => {
|
||||
const newStatus = [...filters.status]
|
||||
if (newStatus.includes("banned")) {
|
||||
newStatus.splice(newStatus.indexOf("banned"), 1)
|
||||
} else {
|
||||
newStatus.push("banned")
|
||||
}
|
||||
setFilters({ ...filters, status: newStatus })
|
||||
}}
|
||||
>
|
||||
Banned
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => setFilters({ ...filters, status: [] })}>Clear filter</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
if (row.original.banned_until) {
|
||||
return <Badge variant="destructive">Banned</Badge>
|
||||
}
|
||||
if (!row.original.email_confirmed_at) {
|
||||
return <Badge variant="outline">Unconfirmed</Badge>
|
||||
}
|
||||
return <Badge variant="default">Active</Badge>
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "",
|
||||
cell: ({ row }) => (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => handleUserUpdate(row.original)}>
|
||||
<UserPen className="h-4 w-4 mr-2 text-blue-500" />
|
||||
Update
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
/* handle delete */
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2 text-red-500" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
/* handle ban */
|
||||
}}
|
||||
>
|
||||
<ShieldAlert className="h-4 w-4 mr-2 text-yellow-500" />
|
||||
{row.original.banned_until != null ? "Unban" : "Ban"}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
@ -1,361 +1,498 @@
|
|||
"use server";
|
||||
|
||||
import db from "@/prisma/db";
|
||||
import db from '@/prisma/db';
|
||||
import { createClient } from '@/app/_utils/supabase/server';
|
||||
import { createAdminClient } from '@/app/_utils/supabase/admin';
|
||||
import { getInjection } from '@/di/container';
|
||||
import { InputParseError, NotFoundError } from '@/src/entities/errors/common';
|
||||
import {
|
||||
CreateUserParams,
|
||||
InviteUserParams,
|
||||
UpdateUserParams,
|
||||
User,
|
||||
UserResponse,
|
||||
} from "@/src/entities/models/users/users.model";
|
||||
import { createClient } from "@/app/_utils/supabase/server";
|
||||
import { createAdminClient } from "@/app/_utils/supabase/admin";
|
||||
AuthenticationError,
|
||||
UnauthenticatedError,
|
||||
} from '@/src/entities/errors/auth';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { IBanDuration, IBanUserSchema, ICredentialsBanUserSchema } from '@/src/entities/models/users/ban-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';
|
||||
|
||||
// Initialize Supabase client with admin key
|
||||
export async function banUser(id: string, ban_duration: IBanDuration) {
|
||||
const instrumentationService = getInjection('IInstrumentationService');
|
||||
return await instrumentationService.instrumentServerAction(
|
||||
'banUser',
|
||||
{ recordResponse: true },
|
||||
async () => {
|
||||
try {
|
||||
const banUserController = getInjection('IBanUserController');
|
||||
await banUserController({ id }, { ban_duration });
|
||||
|
||||
// Fetch all users
|
||||
export async function fetchUsers(): Promise<User[]> {
|
||||
// const { data, error } = await supabase.auth.admin.getUsers();
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
if (err instanceof InputParseError) {
|
||||
// return {
|
||||
// error: err.message,
|
||||
// };
|
||||
|
||||
// if (error) {
|
||||
// console.error("Error fetching users:", error);
|
||||
// throw new Error(error.message);
|
||||
// }
|
||||
throw new InputParseError(err.message);
|
||||
}
|
||||
|
||||
// return data.users.map((user) => ({
|
||||
// ...user,
|
||||
// })) as User[];
|
||||
if (err instanceof UnauthenticatedError) {
|
||||
// return {
|
||||
// error: 'Must be logged in to create a user.',
|
||||
// };
|
||||
throw new UnauthenticatedError('Must be logged in to ban a user.');
|
||||
}
|
||||
|
||||
const users = await db.users.findMany({
|
||||
include: {
|
||||
profile: true,
|
||||
},
|
||||
});
|
||||
if (err instanceof AuthenticationError) {
|
||||
// return {
|
||||
// error: 'User not found.',
|
||||
// };
|
||||
|
||||
if (!users) {
|
||||
throw new Error("Users not found");
|
||||
}
|
||||
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
|
||||
}
|
||||
|
||||
console.log("fetchedUsers");
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
// get current user
|
||||
export async function getCurrentUser(): Promise<UserResponse> {
|
||||
const supabase = await createClient();
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
if (error) {
|
||||
console.error("Error fetching current user:", error);
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
const userDetail = await db.users.findUnique({
|
||||
where: {
|
||||
id: user?.id,
|
||||
},
|
||||
include: {
|
||||
profile: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!userDetail) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
user: userDetail,
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Create a new user
|
||||
export async function createUser(
|
||||
params: CreateUserParams
|
||||
): Promise<UserResponse> {
|
||||
const supabase = createAdminClient();
|
||||
|
||||
const { data, error } = await supabase.auth.admin.createUser({
|
||||
email: params.email,
|
||||
password: params.password,
|
||||
phone: params.phone,
|
||||
email_confirm: params.email_confirm,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("Error creating user:", error);
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
user: data.user,
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function uploadAvatar(userId: string, email: string, file: File) {
|
||||
try {
|
||||
const supabase = await createClient();
|
||||
|
||||
const fileExt = file.name.split(".").pop();
|
||||
const emailName = email.split("@")[0];
|
||||
const fileName = `AVR-${emailName}.${fileExt}`;
|
||||
|
||||
// Change this line - store directly in the user's folder
|
||||
const filePath = `${userId}/${fileName}`;
|
||||
|
||||
// Upload the avatar to Supabase storage
|
||||
const { error: uploadError } = await supabase.storage
|
||||
.from("avatars")
|
||||
.upload(filePath, file, {
|
||||
upsert: true,
|
||||
contentType: file.type,
|
||||
});
|
||||
|
||||
if (uploadError) {
|
||||
console.error("Error uploading avatar:", uploadError);
|
||||
throw uploadError;
|
||||
const crashReporterService = getInjection('ICrashReporterService');
|
||||
crashReporterService.report(err);
|
||||
// return {
|
||||
// error:
|
||||
// 'An error happened. The developers have been notified. Please try again later.',
|
||||
// };
|
||||
throw new Error('An error happened. The developers have been notified. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
// Get the public URL
|
||||
const {
|
||||
data: { publicUrl },
|
||||
} = supabase.storage.from("avatars").getPublicUrl(filePath);
|
||||
|
||||
// Update user profile with the new avatar URL
|
||||
await db.users.update({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
profile: {
|
||||
update: {
|
||||
avatar: publicUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return publicUrl;
|
||||
} catch (error) {
|
||||
console.error("Error uploading avatar:", error);
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function unbanUser(id: string) {
|
||||
const instrumentationService = getInjection('IInstrumentationService');
|
||||
return await instrumentationService.instrumentServerAction(
|
||||
'unbanUser',
|
||||
{ recordResponse: true },
|
||||
async () => {
|
||||
try {
|
||||
const unbanUserController = getInjection('IUnbanUserController');
|
||||
await unbanUserController({ id });
|
||||
|
||||
// Update an existing user
|
||||
export async function updateUser(
|
||||
userId: string,
|
||||
params: UpdateUserParams
|
||||
): Promise<UserResponse> {
|
||||
const supabase = createAdminClient();
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
if (err instanceof InputParseError) {
|
||||
// return {
|
||||
// error: err.message,
|
||||
// };
|
||||
|
||||
const { data, error } = await supabase.auth.admin.updateUserById(userId, {
|
||||
email: params.email,
|
||||
email_confirm: params.email_confirmed_at,
|
||||
password: params.encrypted_password ?? undefined,
|
||||
password_hash: params.encrypted_password ?? undefined,
|
||||
phone: params.phone,
|
||||
phone_confirm: params.phone_confirmed_at,
|
||||
role: params.role,
|
||||
user_metadata: params.user_metadata,
|
||||
app_metadata: params.app_metadata,
|
||||
});
|
||||
throw new InputParseError(err.message);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.error("Error updating user:", error);
|
||||
throw new Error(error.message);
|
||||
}
|
||||
if (err instanceof UnauthenticatedError) {
|
||||
// return {
|
||||
// error: 'Must be logged in to create a user.',
|
||||
// };
|
||||
throw new UnauthenticatedError('Must be logged in to unban a user.');
|
||||
}
|
||||
|
||||
const user = await db.users.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
include: {
|
||||
profile: true,
|
||||
},
|
||||
});
|
||||
if (err instanceof AuthenticationError) {
|
||||
// return {
|
||||
// error: 'User not found.',
|
||||
// };
|
||||
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
|
||||
}
|
||||
|
||||
const updateUser = await db.users.update({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
role: params.role || user.role,
|
||||
invited_at: params.invited_at || user.invited_at,
|
||||
confirmed_at: params.confirmed_at || user.confirmed_at,
|
||||
// recovery_sent_at: params.recovery_sent_at || user.recovery_sent_at,
|
||||
last_sign_in_at: params.last_sign_in_at || user.last_sign_in_at,
|
||||
is_anonymous: params.is_anonymous || user.is_anonymous,
|
||||
created_at: params.created_at || user.created_at,
|
||||
updated_at: params.updated_at || user.updated_at,
|
||||
profile: {
|
||||
update: {
|
||||
avatar: params.profile?.avatar || user.profile?.avatar,
|
||||
username: params.profile?.username || user.profile?.username,
|
||||
first_name: params.profile?.first_name || user.profile?.first_name,
|
||||
last_name: params.profile?.last_name || user.profile?.last_name,
|
||||
bio: params.profile?.bio || user.profile?.bio,
|
||||
address: params.profile?.address || user.profile?.address,
|
||||
birth_date: params.profile?.birth_date || user.profile?.birth_date,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
profile: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
data: {
|
||||
user: {
|
||||
...data.user,
|
||||
role: params.role,
|
||||
profile: {
|
||||
user_id: userId,
|
||||
...updateUser.profile,
|
||||
},
|
||||
},
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Delete a user
|
||||
export async function deleteUser(userId: string): Promise<void> {
|
||||
const supabase = createAdminClient();
|
||||
|
||||
const { error } = await supabase.auth.admin.deleteUser(userId);
|
||||
|
||||
if (error) {
|
||||
console.error("Error deleting user:", error);
|
||||
throw new Error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Send password recovery email
|
||||
export async function sendPasswordRecovery(email: string): Promise<void> {
|
||||
const supabase = createAdminClient();
|
||||
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/reset-password`,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("Error sending password recovery:", error);
|
||||
throw new Error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Send magic link
|
||||
export async function sendMagicLink(email: string): Promise<void> {
|
||||
const supabase = createAdminClient();
|
||||
|
||||
const { error } = await supabase.auth.signInWithOtp({
|
||||
email,
|
||||
options: {
|
||||
emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("Error sending magic link:", error);
|
||||
throw new Error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Ban a user
|
||||
export async function banUser(userId: string): Promise<UserResponse> {
|
||||
const supabase = createAdminClient();
|
||||
|
||||
// Ban for 100 years (effectively permanent)
|
||||
const banUntil = new Date();
|
||||
banUntil.setFullYear(banUntil.getFullYear() + 100);
|
||||
|
||||
const { data, error } = await supabase.auth.admin.updateUserById(userId, {
|
||||
ban_duration: "100h",
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("Error banning user:", error);
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
user: data.user,
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Unban a user
|
||||
export async function unbanUser(userId: string): Promise<UserResponse> {
|
||||
const supabase = createAdminClient();
|
||||
|
||||
const { data, error } = await supabase.auth.admin.updateUserById(userId, {
|
||||
ban_duration: "none",
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("Error unbanning user:", error);
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
const user = await db.users.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
banned_until: true,
|
||||
const crashReporterService = getInjection('ICrashReporterService');
|
||||
crashReporterService.report(err);
|
||||
// return {
|
||||
// error:
|
||||
// 'An error happened. The developers have been notified. Please try again later.',
|
||||
// };
|
||||
throw new Error('An error happened. The developers have been notified. Please try again later.');
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
// const updateUser = await db.users.update({
|
||||
// where: {
|
||||
// id: userId,
|
||||
// },
|
||||
// data: {
|
||||
// banned_until: null,
|
||||
// },
|
||||
// })
|
||||
|
||||
return {
|
||||
data: {
|
||||
user: data.user,
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
);
|
||||
}
|
||||
|
||||
// Invite a user
|
||||
export async function inviteUser(params: InviteUserParams): Promise<void> {
|
||||
const supabase = createAdminClient();
|
||||
export async function getCurrentUser() {
|
||||
const instrumentationService = getInjection('IInstrumentationService');
|
||||
return await instrumentationService.instrumentServerAction(
|
||||
'getCurrentUser',
|
||||
{ recordResponse: true },
|
||||
async () => {
|
||||
try {
|
||||
const getCurrentUserController = getInjection(
|
||||
'IGetCurrentUserController'
|
||||
);
|
||||
return await getCurrentUserController();
|
||||
} catch (err) {
|
||||
|
||||
const { error } = await supabase.auth.admin.inviteUserByEmail(params.email, {
|
||||
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
|
||||
});
|
||||
if (err instanceof UnauthenticatedError || err instanceof AuthenticationError) {
|
||||
redirect('/sign-in');
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.error("Error inviting user:", error);
|
||||
throw new Error(error.message);
|
||||
}
|
||||
const crashReporterService = getInjection('ICrashReporterService');
|
||||
crashReporterService.report(err);
|
||||
|
||||
throw new Error('An error happened. The developers have been notified. Please try again later.');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getUserById(id: string) {
|
||||
const instrumentationService = getInjection('IInstrumentationService');
|
||||
return await instrumentationService.instrumentServerAction(
|
||||
'getUserById',
|
||||
{ recordResponse: true },
|
||||
async () => {
|
||||
try {
|
||||
const getUserByIdController = getInjection('IGetUserByIdController');
|
||||
return await getUserByIdController({ id });
|
||||
|
||||
|
||||
} catch (err) {
|
||||
if (err instanceof InputParseError) {
|
||||
// return {
|
||||
// error: err.message,
|
||||
// };
|
||||
|
||||
throw new InputParseError(err.message);
|
||||
}
|
||||
|
||||
if (err instanceof UnauthenticatedError) {
|
||||
// return {
|
||||
// error: 'Must be logged in to create a user.',
|
||||
// };
|
||||
throw new UnauthenticatedError('Must be logged in to get a user.');
|
||||
}
|
||||
|
||||
if (err instanceof AuthenticationError) {
|
||||
// return {
|
||||
// error: 'User not found.',
|
||||
// };
|
||||
|
||||
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
|
||||
}
|
||||
|
||||
const crashReporterService = getInjection('ICrashReporterService');
|
||||
crashReporterService.report(err);
|
||||
// return {
|
||||
// error:
|
||||
// 'An error happened. The developers have been notified. Please try again later.',
|
||||
// };
|
||||
throw new Error('An error happened. The developers have been notified. Please try again later.');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getUserByEmail(email: string) {
|
||||
const instrumentationService = getInjection('IInstrumentationService');
|
||||
return await instrumentationService.instrumentServerAction(
|
||||
'getUserByEmail',
|
||||
{ recordResponse: true },
|
||||
async () => {
|
||||
try {
|
||||
const getUserByEmailController = getInjection(
|
||||
'IGetUserByEmailController'
|
||||
);
|
||||
return await getUserByEmailController({ email });
|
||||
|
||||
|
||||
} catch (err) {
|
||||
if (err instanceof InputParseError) {
|
||||
// return {
|
||||
// error: err.message,
|
||||
// };
|
||||
|
||||
throw new InputParseError(err.message);
|
||||
}
|
||||
|
||||
if (err instanceof UnauthenticatedError) {
|
||||
// return {
|
||||
// error: 'Must be logged in to create a user.',
|
||||
// };
|
||||
throw new UnauthenticatedError('Must be logged in to get a user.');
|
||||
}
|
||||
|
||||
if (err instanceof AuthenticationError) {
|
||||
// return {
|
||||
// error: 'User not found.',
|
||||
// };
|
||||
|
||||
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
|
||||
}
|
||||
|
||||
const crashReporterService = getInjection('ICrashReporterService');
|
||||
crashReporterService.report(err);
|
||||
// return {
|
||||
// error:
|
||||
// 'An error happened. The developers have been notified. Please try again later.',
|
||||
// };
|
||||
throw new Error('An error happened. The developers have been notified. Please try again later.');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getUserByUsername(username: string) {
|
||||
const instrumentationService = getInjection('IInstrumentationService');
|
||||
return await instrumentationService.instrumentServerAction(
|
||||
'getUserByUsername',
|
||||
{ recordResponse: true },
|
||||
async () => {
|
||||
try {
|
||||
const getUserByUsernameController = getInjection(
|
||||
'IGetUserByUsernameController'
|
||||
);
|
||||
return await getUserByUsernameController({ username });
|
||||
|
||||
|
||||
} catch (err) {
|
||||
|
||||
if (err instanceof InputParseError) {
|
||||
// return {
|
||||
// error: err.message,
|
||||
// };
|
||||
|
||||
throw new InputParseError(err.message);
|
||||
}
|
||||
|
||||
if (err instanceof UnauthenticatedError) {
|
||||
// return {
|
||||
// error: 'Must be logged in to create a user.',
|
||||
// };
|
||||
throw new UnauthenticatedError('Must be logged in to get a user.');
|
||||
}
|
||||
|
||||
if (err instanceof AuthenticationError) {
|
||||
// return {
|
||||
// error: 'User not found.',
|
||||
// };
|
||||
|
||||
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
|
||||
}
|
||||
|
||||
const crashReporterService = getInjection('ICrashReporterService');
|
||||
crashReporterService.report(err);
|
||||
// return {
|
||||
// error:
|
||||
// 'An error happened. The developers have been notified. Please try again later.',
|
||||
// };
|
||||
throw new Error('An error happened. The developers have been notified. Please try again later.');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getUsers() {
|
||||
const instrumentationService = getInjection('IInstrumentationService');
|
||||
return await instrumentationService.instrumentServerAction(
|
||||
'getUsers',
|
||||
{ recordResponse: true },
|
||||
async () => {
|
||||
try {
|
||||
const getUsersController = getInjection('IGetUsersController');
|
||||
return await getUsersController();
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof UnauthenticatedError ||
|
||||
err instanceof AuthenticationError
|
||||
) {
|
||||
redirect('/sign-in');
|
||||
}
|
||||
|
||||
const crashReporterService = getInjection('ICrashReporterService');
|
||||
crashReporterService.report(err);
|
||||
throw new Error('An error happened. The developers have been notified. Please try again later.');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function inviteUser(credentials: ICredentialsInviteUserSchema) {
|
||||
const instrumentationService = getInjection('IInstrumentationService');
|
||||
return await instrumentationService.instrumentServerAction(
|
||||
'inviteUser',
|
||||
{ recordResponse: true },
|
||||
async () => {
|
||||
try {
|
||||
const inviteUserController = getInjection('IInviteUserController');
|
||||
await inviteUserController({ email: credentials.email });
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
|
||||
if (err instanceof InputParseError) {
|
||||
// return {
|
||||
// error: err.message,
|
||||
// };
|
||||
|
||||
throw new InputParseError(err.message);
|
||||
}
|
||||
|
||||
if (err instanceof UnauthenticatedError) {
|
||||
// return {
|
||||
// error: 'Must be logged in to create a user.',
|
||||
// };
|
||||
throw new UnauthenticatedError('Must be logged in to invite a user.');
|
||||
}
|
||||
|
||||
if (err instanceof AuthenticationError) {
|
||||
// return {
|
||||
// error: 'User not found.',
|
||||
// };
|
||||
|
||||
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
|
||||
}
|
||||
|
||||
const crashReporterService = getInjection('ICrashReporterService');
|
||||
crashReporterService.report(err);
|
||||
// return {
|
||||
// error:
|
||||
// 'An error happened. The developers have been notified. Please try again later.',
|
||||
// };
|
||||
throw new Error('An error happened. The developers have been notified. Please try again later.');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function createUser(data: ICreateUserSchema) {
|
||||
const instrumentationService = getInjection('IInstrumentationService');
|
||||
return await instrumentationService.instrumentServerAction(
|
||||
'createUser',
|
||||
{ recordResponse: true },
|
||||
async () => {
|
||||
try {
|
||||
const createUserController = getInjection('ICreateUserController');
|
||||
await createUserController(data);
|
||||
|
||||
return { success: true };
|
||||
|
||||
} catch (err) {
|
||||
|
||||
if (err instanceof InputParseError) {
|
||||
// return {
|
||||
// error: err.message,
|
||||
// };
|
||||
|
||||
throw new InputParseError(err.message);
|
||||
}
|
||||
|
||||
if (err instanceof UnauthenticatedError) {
|
||||
// return {
|
||||
// error: 'Must be logged in to create a user.',
|
||||
// };
|
||||
throw new UnauthenticatedError('Must be logged in to create a user.');
|
||||
}
|
||||
|
||||
if (err instanceof AuthenticationError) {
|
||||
// return {
|
||||
// error: 'User not found.',
|
||||
// };
|
||||
|
||||
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
|
||||
}
|
||||
|
||||
const crashReporterService = getInjection('ICrashReporterService');
|
||||
crashReporterService.report(err);
|
||||
// return {
|
||||
// error:
|
||||
// 'An error happened. The developers have been notified. Please try again later.',
|
||||
// };
|
||||
throw new Error('An error happened. The developers have been notified. Please try again later.');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateUser(id: string, data: IUpdateUserSchema) {
|
||||
const instrumentationService = getInjection('IInstrumentationService');
|
||||
return await instrumentationService.instrumentServerAction(
|
||||
'updateUser',
|
||||
{ recordResponse: true },
|
||||
async () => {
|
||||
try {
|
||||
const updateUserController = getInjection('IUpdateUserController');
|
||||
await updateUserController(id, data);
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
if (err instanceof InputParseError) {
|
||||
// return {
|
||||
// error: err.message,
|
||||
// };
|
||||
|
||||
throw new InputParseError(err.message);
|
||||
}
|
||||
|
||||
if (err instanceof UnauthenticatedError) {
|
||||
// return {
|
||||
// error: 'Must be logged in to create a user.',
|
||||
// };
|
||||
throw new UnauthenticatedError('Must be logged in to update a user.');
|
||||
}
|
||||
|
||||
if (err instanceof AuthenticationError) {
|
||||
// return {
|
||||
// error: 'User not found.',
|
||||
// };
|
||||
|
||||
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
|
||||
}
|
||||
|
||||
const crashReporterService = getInjection('ICrashReporterService');
|
||||
crashReporterService.report(err);
|
||||
// return {
|
||||
// error:
|
||||
// 'An error happened. The developers have been notified. Please try again later.',
|
||||
// };
|
||||
throw new Error('An error happened. The developers have been notified. Please try again later.');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteUser(id: string) {
|
||||
const instrumentationService = getInjection('IInstrumentationService');
|
||||
return await instrumentationService.instrumentServerAction(
|
||||
'deleteUser',
|
||||
{ recordResponse: true },
|
||||
async () => {
|
||||
try {
|
||||
const deleteUserController = getInjection('IDeleteUserController');
|
||||
await deleteUserController({ id });
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
if (err instanceof InputParseError) {
|
||||
// return {
|
||||
// error: err.message,
|
||||
// };
|
||||
|
||||
throw new InputParseError(err.message);
|
||||
}
|
||||
|
||||
if (err instanceof UnauthenticatedError) {
|
||||
// return {
|
||||
// error: 'Must be logged in to create a user.',
|
||||
// };
|
||||
throw new UnauthenticatedError('Must be logged in to delete a user.');
|
||||
}
|
||||
|
||||
if (err instanceof AuthenticationError) {
|
||||
// return {
|
||||
// error: 'User not found.',
|
||||
// };
|
||||
|
||||
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
|
||||
}
|
||||
|
||||
const crashReporterService = getInjection('ICrashReporterService');
|
||||
crashReporterService.report(err);
|
||||
// return {
|
||||
// error:
|
||||
// 'An error happened. The developers have been notified. Please try again later.',
|
||||
// };
|
||||
throw new Error('An error happened. The developers have been notified. Please try again later.');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,308 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { useCreateUserMutation, useInviteUserMutation } from './queries';
|
||||
import { IUserSchema, IUserFilterOptionsSchema } from '@/src/entities/models/users/users.model';
|
||||
import { toast } from 'sonner';
|
||||
import { set } from 'date-fns';
|
||||
import { CreateUserSchema, defaulICreateUserSchemaValues, ICreateUserSchema } from '@/src/entities/models/users/create-user.model';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { defaulIInviteUserSchemaValues, IInviteUserSchema, InviteUserSchema } from '@/src/entities/models/users/invite-user.model';
|
||||
|
||||
export const useAddUserDialogHandler = ({ onUserAdded, onOpenChange }: {
|
||||
onUserAdded: () => void;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) => {
|
||||
const { createUser, isPending } = useCreateUserMutation();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors: errors },
|
||||
setError,
|
||||
getValues,
|
||||
clearErrors,
|
||||
watch,
|
||||
} = useForm<ICreateUserSchema>({
|
||||
resolver: zodResolver(CreateUserSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
email_confirm: true,
|
||||
}
|
||||
});
|
||||
|
||||
const emailConfirm = watch("email_confirm");
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
|
||||
await createUser(data, {
|
||||
onSuccess: () => {
|
||||
toast.success("User created successfully.");
|
||||
onUserAdded();
|
||||
onOpenChange(false);
|
||||
reset();
|
||||
},
|
||||
onError: (error) => {
|
||||
reset();
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
reset();
|
||||
}
|
||||
onOpenChange(open);
|
||||
};
|
||||
|
||||
return {
|
||||
register,
|
||||
handleSubmit: onSubmit,
|
||||
reset,
|
||||
errors,
|
||||
isPending,
|
||||
getValues,
|
||||
clearErrors,
|
||||
emailConfirm,
|
||||
handleOpenChange,
|
||||
};
|
||||
}
|
||||
|
||||
export const useInviteUserHandler = ({ onUserInvited, onOpenChange }: {
|
||||
onUserInvited: () => void;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) => {
|
||||
|
||||
const { inviteUser, isPending } = useInviteUserMutation();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors: errors },
|
||||
setError,
|
||||
getValues,
|
||||
clearErrors,
|
||||
watch,
|
||||
} = useForm<IInviteUserSchema>({
|
||||
resolver: zodResolver(InviteUserSchema),
|
||||
defaultValues: defaulIInviteUserSchemaValues
|
||||
})
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
await inviteUser(data, {
|
||||
onSuccess: () => {
|
||||
toast.success("Invitation sent");
|
||||
onUserInvited();
|
||||
onOpenChange(false);
|
||||
reset();
|
||||
},
|
||||
onError: () => {
|
||||
reset();
|
||||
toast.error("Failed to send invitation");
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
reset();
|
||||
}
|
||||
onOpenChange(open);
|
||||
};
|
||||
|
||||
return {
|
||||
register,
|
||||
handleSubmit: onSubmit,
|
||||
handleOpenChange,
|
||||
reset,
|
||||
getValues,
|
||||
clearErrors,
|
||||
watch,
|
||||
errors,
|
||||
isPending,
|
||||
};
|
||||
}
|
||||
|
||||
export const useUserManagementHandlers = (refetch: () => void) => {
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [detailUser, setDetailUser] = useState<IUserSchema | null>(null)
|
||||
const [updateUser, setUpdateUser] = useState<IUserSchema | null>(null)
|
||||
const [isSheetOpen, setIsSheetOpen] = useState(false)
|
||||
const [isUpdateOpen, setIsUpdateOpen] = useState(false)
|
||||
const [isAddUserOpen, setIsAddUserOpen] = useState(false)
|
||||
const [isInviteUserOpen, setIsInviteUserOpen] = useState(false)
|
||||
|
||||
// Filter states
|
||||
const [filters, setFilters] = useState<IUserFilterOptionsSchema>({
|
||||
email: "",
|
||||
phone: "",
|
||||
lastSignIn: "",
|
||||
createdAt: "",
|
||||
status: [],
|
||||
})
|
||||
|
||||
// Handle opening the detail sheet
|
||||
const handleUserClick = (user: IUserSchema) => {
|
||||
setDetailUser(user)
|
||||
setIsSheetOpen(true)
|
||||
}
|
||||
|
||||
// Handle opening the update sheet
|
||||
const handleUserUpdate = (user: IUserSchema) => {
|
||||
setUpdateUser(user)
|
||||
setIsUpdateOpen(true)
|
||||
}
|
||||
|
||||
// Close detail sheet when update sheet opens
|
||||
useEffect(() => {
|
||||
if (isUpdateOpen) {
|
||||
setIsSheetOpen(false)
|
||||
}
|
||||
}, [isUpdateOpen])
|
||||
|
||||
// Reset detail user when sheet closes
|
||||
useEffect(() => {
|
||||
if (!isSheetOpen) {
|
||||
// Use a small delay to prevent flickering if another sheet is opening
|
||||
const timer = setTimeout(() => {
|
||||
if (!isSheetOpen && !isUpdateOpen) {
|
||||
setDetailUser(null)
|
||||
}
|
||||
}, 300)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [isSheetOpen, isUpdateOpen])
|
||||
|
||||
// Reset update user when update sheet closes
|
||||
useEffect(() => {
|
||||
if (!isUpdateOpen) {
|
||||
// Use a small delay to prevent flickering if another sheet is opening
|
||||
const timer = setTimeout(() => {
|
||||
if (!isUpdateOpen) {
|
||||
setUpdateUser(null)
|
||||
}
|
||||
}, 300)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [isUpdateOpen])
|
||||
|
||||
const clearFilters = () => {
|
||||
setFilters({
|
||||
email: "",
|
||||
phone: "",
|
||||
lastSignIn: "",
|
||||
createdAt: "",
|
||||
status: [],
|
||||
})
|
||||
}
|
||||
|
||||
const getActiveFilterCount = () => {
|
||||
return Object.values(filters).filter(
|
||||
(value) => (typeof value === "string" && value !== "") || (Array.isArray(value) && value.length > 0),
|
||||
).length
|
||||
}
|
||||
|
||||
return {
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
detailUser,
|
||||
updateUser,
|
||||
isSheetOpen,
|
||||
setIsSheetOpen,
|
||||
isUpdateOpen,
|
||||
setIsUpdateOpen,
|
||||
isAddUserOpen,
|
||||
setIsAddUserOpen,
|
||||
isInviteUserOpen,
|
||||
setIsInviteUserOpen,
|
||||
filters,
|
||||
setFilters,
|
||||
handleUserClick,
|
||||
handleUserUpdate,
|
||||
clearFilters,
|
||||
getActiveFilterCount,
|
||||
}
|
||||
}
|
||||
|
||||
export const filterUsers = (users: IUserSchema[], searchQuery: string, filters: IUserFilterOptionsSchema): IUserSchema[] => {
|
||||
return users.filter((user) => {
|
||||
|
||||
// Global search
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase()
|
||||
const matchesSearch =
|
||||
user.email?.toLowerCase().includes(query) ||
|
||||
user.phone?.toLowerCase().includes(query) ||
|
||||
user.id.toLowerCase().includes(query)
|
||||
|
||||
if (!matchesSearch) return false
|
||||
}
|
||||
|
||||
// Email filter
|
||||
if (filters.email && !user.email?.toLowerCase().includes(filters.email.toLowerCase())) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Phone filter
|
||||
if (filters.phone && !user.phone?.toLowerCase().includes(filters.phone.toLowerCase())) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Last sign in filter
|
||||
if (filters.lastSignIn) {
|
||||
if (filters.lastSignIn === "never" && user.last_sign_in_at) {
|
||||
return false
|
||||
} else if (filters.lastSignIn === "today") {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const signInDate = user.last_sign_in_at ? new Date(user.last_sign_in_at) : null
|
||||
if (!signInDate || signInDate < today) return false
|
||||
} else if (filters.lastSignIn === "week") {
|
||||
const weekAgo = new Date()
|
||||
weekAgo.setDate(weekAgo.getDate() - 7)
|
||||
const signInDate = user.last_sign_in_at ? new Date(user.last_sign_in_at) : null
|
||||
if (!signInDate || signInDate < weekAgo) return false
|
||||
} else if (filters.lastSignIn === "month") {
|
||||
const monthAgo = new Date()
|
||||
monthAgo.setMonth(monthAgo.getMonth() - 1)
|
||||
const signInDate = user.last_sign_in_at ? new Date(user.last_sign_in_at) : null
|
||||
if (!signInDate || signInDate < monthAgo) return false
|
||||
}
|
||||
}
|
||||
|
||||
// Created at filter
|
||||
if (filters.createdAt) {
|
||||
if (filters.createdAt === "today") {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const createdAt = user.created_at ? (user.created_at ? new Date(user.created_at) : new Date()) : new Date()
|
||||
if (createdAt < today) return false
|
||||
} else if (filters.createdAt === "week") {
|
||||
const weekAgo = new Date()
|
||||
weekAgo.setDate(weekAgo.getDate() - 7)
|
||||
const createdAt = user.created_at ? new Date(user.created_at) : new Date()
|
||||
if (createdAt < weekAgo) return false
|
||||
} else if (filters.createdAt === "month") {
|
||||
const monthAgo = new Date()
|
||||
monthAgo.setMonth(monthAgo.getMonth() - 1)
|
||||
const createdAt = user.created_at ? new Date(user.created_at) : new Date()
|
||||
if (createdAt < monthAgo) return false
|
||||
}
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if (filters.status.length > 0) {
|
||||
const userStatus = user.banned_until ? "banned" : !user.email_confirmed_at ? "unconfirmed" : "active"
|
||||
|
||||
if (!filters.status.includes(userStatus)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
banUser,
|
||||
getCurrentUser,
|
||||
getUserByEmail,
|
||||
getUserById,
|
||||
getUsers,
|
||||
unbanUser,
|
||||
inviteUser,
|
||||
createUser,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
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 { 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";
|
||||
|
||||
const useUsersAction = () => {
|
||||
|
||||
// For all users (no parameters needed)
|
||||
const getUsersQuery = useQuery<IUserSchema[]>({
|
||||
queryKey: ["users"],
|
||||
queryFn: async () => await getUsers()
|
||||
});
|
||||
|
||||
// Current user query doesn't need parameters
|
||||
const getCurrentUserQuery = useQuery({
|
||||
queryKey: ["user", "current"],
|
||||
queryFn: async () => await getCurrentUser()
|
||||
});
|
||||
|
||||
const getUserByIdQuery = (id: string) => ({
|
||||
queryKey: ["user", id],
|
||||
queryFn: async () => await getUserById(id)
|
||||
});
|
||||
|
||||
const getUserByEmailQuery = (email: string) => ({
|
||||
queryKey: ["user", "email", email],
|
||||
queryFn: async () => await getUserByEmail(email)
|
||||
});
|
||||
|
||||
const getUserByUsernameQuery = (username: string) => ({
|
||||
queryKey: ["user", "username", username],
|
||||
queryFn: async () => await getUserByUsername(username)
|
||||
});
|
||||
|
||||
// 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)
|
||||
});
|
||||
|
||||
const unbanUserMutation = useMutation({
|
||||
mutationKey: ["unbanUser"],
|
||||
mutationFn: async (params: IUnbanUserSchema) => await unbanUser(params.id)
|
||||
});
|
||||
|
||||
// Create functions that return configured hooks
|
||||
const inviteUserMutation = useMutation({
|
||||
mutationKey: ["inviteUser"],
|
||||
mutationFn: async (credential: ICredentialsInviteUserSchema) => await inviteUser(credential)
|
||||
});
|
||||
|
||||
const createUserMutation = useMutation({
|
||||
mutationKey: ["createUser"],
|
||||
mutationFn: async (data: ICreateUserSchema) => await createUser(data)
|
||||
});
|
||||
|
||||
const updateUserMutation = useMutation({
|
||||
mutationKey: ["updateUser"],
|
||||
mutationFn: async (params: { id: string; data: IUpdateUserSchema }) => updateUser(params.id, params.data)
|
||||
});
|
||||
|
||||
const deleteUserMutation = useMutation({
|
||||
mutationKey: ["deleteUser"],
|
||||
mutationFn: async (id: string) => await deleteUser(id)
|
||||
});
|
||||
|
||||
return {
|
||||
getUsers: getUsersQuery,
|
||||
getCurrentUser: getCurrentUserQuery,
|
||||
getUserById: getUserByIdQuery,
|
||||
getUserByEmailQuery,
|
||||
getUserByUsernameQuery,
|
||||
banUser: banUserMutation,
|
||||
unbanUser: unbanUserMutation,
|
||||
inviteUser: inviteUserMutation,
|
||||
createUser: createUserMutation,
|
||||
updateUser: updateUserMutation,
|
||||
deleteUser: deleteUserMutation
|
||||
};
|
||||
}
|
||||
|
||||
export const useGetUsersQuery = () => {
|
||||
const { getUsers } = useUsersAction();
|
||||
|
||||
return {
|
||||
data: getUsers.data,
|
||||
isPending: getUsers.isPending,
|
||||
error: getUsers.error,
|
||||
refetch: getUsers.refetch,
|
||||
};
|
||||
}
|
||||
|
||||
export const useGetCurrentUserQuery = () => {
|
||||
const { getCurrentUser } = useUsersAction();
|
||||
|
||||
return {
|
||||
data: getCurrentUser.data,
|
||||
isPending: getCurrentUser.isPending,
|
||||
error: getCurrentUser.error,
|
||||
refetch: getCurrentUser.refetch,
|
||||
};
|
||||
}
|
||||
|
||||
export const useCreateUserMutation = () => {
|
||||
const { createUser } = useUsersAction();
|
||||
|
||||
return {
|
||||
createUser: createUser.mutateAsync,
|
||||
isPending: createUser.isPending,
|
||||
errors: createUser.error,
|
||||
}
|
||||
}
|
||||
|
||||
export const useInviteUserMutation = () => {
|
||||
const { inviteUser } = useUsersAction();
|
||||
|
||||
return {
|
||||
inviteUser: inviteUser.mutateAsync,
|
||||
isPending: inviteUser.isPending,
|
||||
errors: inviteUser.error,
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import { redirect } from "next/navigation"
|
|||
import { getInjection } from "@/di/container"
|
||||
import { revalidatePath } from "next/cache"
|
||||
|
||||
import { InputParseError } from "@/src/entities/errors/common"
|
||||
import { InputParseError, NotFoundError } from "@/src/entities/errors/common"
|
||||
import { AuthenticationError, UnauthenticatedError } from "@/src/entities/errors/auth"
|
||||
import { createClient } from "@/app/_utils/supabase/server"
|
||||
|
||||
|
@ -18,25 +18,22 @@ export async function signIn(formData: FormData) {
|
|||
|
||||
try {
|
||||
const signInController = getInjection("ISignInController")
|
||||
await signInController({ email })
|
||||
return await signInController({ email })
|
||||
|
||||
// if (email) {
|
||||
// redirect(`/verify-otp?email=${encodeURIComponent(email)}`)
|
||||
// }
|
||||
|
||||
return { success: true }
|
||||
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof InputParseError ||
|
||||
err instanceof AuthenticationError
|
||||
) {
|
||||
return {
|
||||
error: 'Incorrect credential. Please try again.',
|
||||
};
|
||||
if (err instanceof InputParseError) {
|
||||
return { error: err.message }
|
||||
}
|
||||
|
||||
if (err instanceof UnauthenticatedError) {
|
||||
if (err instanceof AuthenticationError) {
|
||||
return { error: "Invalid credential. Please try again." }
|
||||
}
|
||||
|
||||
if (err instanceof UnauthenticatedError || err instanceof NotFoundError) {
|
||||
return {
|
||||
error: 'User not found. Please tell your admin to create an account for you.',
|
||||
};
|
||||
|
@ -136,3 +133,57 @@ export async function verifyOtp(formData: FormData) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function sendMagicLink(formData: FormData) {
|
||||
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 })
|
||||
|
||||
return { success: true }
|
||||
} catch (err) {
|
||||
if (err instanceof InputParseError) {
|
||||
return { error: err.message }
|
||||
}
|
||||
|
||||
const crashReporterService = getInjection("ICrashReporterService")
|
||||
crashReporterService.report(err)
|
||||
|
||||
return {
|
||||
error: "An error occurred during sending magic link. Please try again later.",
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function sendPasswordRecovery(formData: FormData) {
|
||||
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 })
|
||||
|
||||
return { success: true }
|
||||
} catch (err) {
|
||||
if (err instanceof InputParseError) {
|
||||
return { error: err.message }
|
||||
}
|
||||
|
||||
const crashReporterService = getInjection("ICrashReporterService")
|
||||
crashReporterService.report(err)
|
||||
|
||||
return {
|
||||
error: "An error occurred during sending password recovery. Please try again later.",
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,19 +1,18 @@
|
|||
import { AuthenticationError } from "@/src/entities/errors/auth";
|
||||
import { useState } from "react";
|
||||
import { useAuthActions } from "./mutation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { defaultSignInPasswordlessValues, SignInFormData, SignInPasswordless, SignInPasswordlessSchema, SignInSchema } from "@/src/entities/models/auth/sign-in.model";
|
||||
import { createFormData } from "@/app/_utils/common";
|
||||
import { useFormHandler } from "@/app/_hooks/use-form-handler";
|
||||
import { toast } from "sonner";
|
||||
import { signIn } from "./action";
|
||||
import { useNavigations } from "@/app/_hooks/use-navigations";
|
||||
import { VerifyOtpFormData, verifyOtpSchema } from "@/src/entities/models/auth/verify-otp.model";
|
||||
import { useAuthActions } from './queries';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';;
|
||||
import { toast } from 'sonner';
|
||||
import { useNavigations } from '@/app/_hooks/use-navigations';
|
||||
import {
|
||||
IVerifyOtpSchema,
|
||||
verifyOtpSchema,
|
||||
} from '@/src/entities/models/auth/verify-otp.model';
|
||||
|
||||
/**
|
||||
* Hook untuk menangani proses sign in
|
||||
*
|
||||
*
|
||||
* @returns {Object} Object berisi handler dan state untuk form sign in
|
||||
* @example
|
||||
* const { handleSubmit, isPending, error } = useSignInHandler();
|
||||
|
@ -32,23 +31,18 @@ export function useSignInHandler() {
|
|||
setError(undefined);
|
||||
|
||||
const formData = new FormData(event.currentTarget);
|
||||
const email = formData.get("email")?.toString()
|
||||
const email = formData.get('email')?.toString();
|
||||
|
||||
try {
|
||||
await signIn.mutateAsync(formData, {
|
||||
onSuccess: () => {
|
||||
toast("An email has been sent to you. Please check your inbox.");
|
||||
if (email) router.push(`/verify-otp?email=${encodeURIComponent(email)}`);
|
||||
},
|
||||
onError: (error) => {
|
||||
setError(error.message);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
setError(error.message);
|
||||
}
|
||||
const res = await signIn.mutateAsync(formData);
|
||||
|
||||
if (!res?.error) {
|
||||
toast('An email has been sent to you. Please check your inbox.');
|
||||
if (email) router.push(`/verify-otp?email=${encodeURIComponent(email)}`);
|
||||
} else {
|
||||
setError(res.error);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -58,67 +52,66 @@ export function useSignInHandler() {
|
|||
error,
|
||||
isPending: signIn.isPending,
|
||||
errors: !!error || signIn.error,
|
||||
clearError: () => setError(undefined)
|
||||
clearError: () => setError(undefined),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function useVerifyOtpHandler(email: string) {
|
||||
const { router } = useNavigations()
|
||||
const { verifyOtp } = useAuthActions()
|
||||
const [error, setError] = useState<string>()
|
||||
const { router } = useNavigations();
|
||||
const { verifyOtp } = useAuthActions();
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit: hookFormSubmit,
|
||||
control,
|
||||
formState: { errors },
|
||||
setValue
|
||||
} = useForm<VerifyOtpFormData>({
|
||||
setValue,
|
||||
} = useForm<IVerifyOtpSchema>({
|
||||
resolver: zodResolver(verifyOtpSchema),
|
||||
defaultValues: {
|
||||
email,
|
||||
token: ""
|
||||
}
|
||||
})
|
||||
token: '',
|
||||
},
|
||||
});
|
||||
|
||||
const handleOtpChange = (value: string, onChange: (value: string) => void) => {
|
||||
onChange(value)
|
||||
const handleOtpChange = (
|
||||
value: string,
|
||||
onChange: (value: string) => void
|
||||
) => {
|
||||
onChange(value);
|
||||
|
||||
if (value.length === 6) {
|
||||
handleSubmit();
|
||||
}
|
||||
|
||||
// Clear error when user starts typing
|
||||
if (error) {
|
||||
setError(undefined)
|
||||
setError(undefined);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = hookFormSubmit(async (data) => {
|
||||
if (verifyOtp.isPending) return
|
||||
if (verifyOtp.isPending) return;
|
||||
|
||||
setError(undefined)
|
||||
setError(undefined);
|
||||
|
||||
// Create FormData object
|
||||
const formData = new FormData()
|
||||
formData.append("email", data.email)
|
||||
formData.append("token", data.token)
|
||||
const formData = new FormData();
|
||||
formData.append('email', data.email);
|
||||
formData.append('token', data.token);
|
||||
|
||||
try {
|
||||
await verifyOtp.mutateAsync(formData, {
|
||||
onSuccess: () => {
|
||||
toast.success("OTP verified successfully")
|
||||
// Navigate to dashboard on success
|
||||
router.push("/dashboard")
|
||||
},
|
||||
onError: (error) => {
|
||||
setError(error.message)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
setError(error.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
await verifyOtp.mutateAsync(formData, {
|
||||
onSuccess: () => {
|
||||
toast.success('OTP verified successfully');
|
||||
// Navigate to dashboard on success
|
||||
router.push('/dashboard');
|
||||
},
|
||||
onError: (error) => {
|
||||
setError(error.message);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
register,
|
||||
|
@ -127,50 +120,42 @@ export function useVerifyOtpHandler(email: string) {
|
|||
handleOtpChange,
|
||||
errors: {
|
||||
...errors,
|
||||
token: error ? { message: error } : errors.token
|
||||
token: error ? { message: error } : errors.token,
|
||||
},
|
||||
isPending: verifyOtp.isPending,
|
||||
clearError: () => setError(undefined)
|
||||
}
|
||||
clearError: () => setError(undefined),
|
||||
};
|
||||
}
|
||||
|
||||
export function useSignOutHandler() {
|
||||
const { signOut } = useAuthActions()
|
||||
const { router } = useNavigations()
|
||||
const [error, setError] = useState<string>()
|
||||
const { signOut } = useAuthActions();
|
||||
const { router } = useNavigations();
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const handleSignOut = async () => {
|
||||
if (signOut.isPending) return
|
||||
if (signOut.isPending) return;
|
||||
|
||||
setError(undefined)
|
||||
setError(undefined);
|
||||
|
||||
try {
|
||||
await signOut.mutateAsync(undefined, {
|
||||
onSuccess: () => {
|
||||
toast.success("You have been signed out successfully")
|
||||
router.push("/sign-in")
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error instanceof AuthenticationError) {
|
||||
setError(error.message)
|
||||
toast.error(error.message)
|
||||
}
|
||||
await signOut.mutateAsync(undefined, {
|
||||
onSuccess: () => {
|
||||
toast.success('You have been signed out successfully');
|
||||
router.push('/sign-in');
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error instanceof AuthenticationError) {
|
||||
setError(error.message);
|
||||
toast.error(error.message);
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
setError(error.message)
|
||||
toast.error(error.message)
|
||||
// toast.error("An error occurred during sign out. Please try again later.")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
handleSignOut,
|
||||
error,
|
||||
isPending: signOut.isPending,
|
||||
errors: !!error || signOut.error,
|
||||
clearError: () => setError(undefined)
|
||||
}
|
||||
clearError: () => setError(undefined),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
import { queryOptions, useMutation } from '@tanstack/react-query';
|
||||
import { signIn, signOut, verifyOtp } from './action';
|
||||
|
||||
export function useAuthActions() {
|
||||
// Sign In Mutation
|
||||
const signInMutation = useMutation({
|
||||
mutationKey: ["signIn"],
|
||||
mutationFn: async (formData: FormData) => {
|
||||
const response = await signIn(formData);
|
||||
|
||||
// If the server action returns an error, treat it as an error for React Query
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const verifyOtpMutation = useMutation({
|
||||
mutationKey: ["verifyOtp"],
|
||||
mutationFn: async (formData: FormData) => {
|
||||
const response = await verifyOtp(formData);
|
||||
|
||||
// If the server action returns an error, treat it as an error for React Query
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const signOutMutation = useMutation({
|
||||
mutationKey: ["signOut"],
|
||||
mutationFn: async () => {
|
||||
const response = await signOut();
|
||||
|
||||
// If the server action returns an error, treat it as an error for React Query
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
signIn: signInMutation,
|
||||
verifyOtp: verifyOtpMutation,
|
||||
signOut: signOutMutation
|
||||
};
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import { useMutation } from '@tanstack/react-query';
|
||||
import { sendMagicLink, sendPasswordRecovery, signIn, signOut, verifyOtp } from './action';
|
||||
|
||||
export function useAuthActions() {
|
||||
// Sign In Mutation
|
||||
const signInMutation = useMutation({
|
||||
mutationKey: ["signIn"],
|
||||
mutationFn: async (formData: FormData) => await signIn(formData)
|
||||
});
|
||||
|
||||
// Verify OTP Mutation
|
||||
const verifyOtpMutation = useMutation({
|
||||
mutationKey: ["verifyOtp"],
|
||||
mutationFn: async (formData: FormData) => await verifyOtp(formData)
|
||||
});
|
||||
|
||||
const signOutMutation = useMutation({
|
||||
mutationKey: ["signOut"],
|
||||
mutationFn: async () => await signOut()
|
||||
});
|
||||
|
||||
const sendMagicLinkMutation = useMutation({
|
||||
mutationKey: ["sendMagicLink"],
|
||||
mutationFn: async (formData: FormData) => await sendMagicLink(formData)
|
||||
});
|
||||
|
||||
const sendPasswordRecoveryMutation = useMutation({
|
||||
mutationKey: ["sendPasswordRecovery"],
|
||||
mutationFn: async (formData: FormData) => await sendPasswordRecovery(formData)
|
||||
});
|
||||
|
||||
return {
|
||||
signIn: signInMutation,
|
||||
verifyOtp: verifyOtpMutation,
|
||||
signOut: signOutMutation,
|
||||
sendMagicLink: sendMagicLinkMutation,
|
||||
sendPasswordRecovery: sendPasswordRecoveryMutation
|
||||
};
|
||||
}
|
|
@ -1,71 +1,71 @@
|
|||
|
||||
import { hasEnvVars } from "@/app/_utils/supabase/check-env-vars";
|
||||
import Link from "next/link";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { Button } from "./ui/button";
|
||||
import { createClient } from "@/app/_utils/supabase/server";
|
||||
import { signOutAction } from "@/app/(pages)/(auth)/_actions/sign-out";
|
||||
// import { hasEnvVars } from "@/app/_utils/supabase/check-env-vars";
|
||||
// import Link from "next/link";
|
||||
// import { Badge } from "./ui/badge";
|
||||
// import { Button } from "./ui/button";
|
||||
// import { createClient } from "@/app/_utils/supabase/server";
|
||||
// import { signOutAction } from "@/app/(pages)/(auth)/_actions/sign-out";
|
||||
|
||||
export default async function AuthButton() {
|
||||
const supabase = await createClient();
|
||||
// export default async function AuthButton() {
|
||||
// const supabase = await createClient();
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
// const {
|
||||
// data: { user },
|
||||
// } = await supabase.auth.getUser();
|
||||
|
||||
if (!hasEnvVars) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-4 items-center">
|
||||
<div>
|
||||
<Badge
|
||||
variant={"default"}
|
||||
className="font-normal pointer-events-none"
|
||||
>
|
||||
Please update .env.local file with anon key and url
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
variant={"outline"}
|
||||
disabled
|
||||
className="opacity-75 cursor-none pointer-events-none"
|
||||
>
|
||||
<Link href="/sign-in">Sign in</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
variant={"default"}
|
||||
disabled
|
||||
className="opacity-75 cursor-none pointer-events-none"
|
||||
>
|
||||
<Link href="/sign-up">Sign up</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return user ? (
|
||||
<div className="flex items-center gap-4">
|
||||
Hey, {user.email}!
|
||||
<form action={signOutAction}>
|
||||
<Button type="submit" variant={"outline"}>
|
||||
Sign out
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<Button asChild size="sm" variant={"outline"}>
|
||||
<Link href="/sign-in">Sign in</Link>
|
||||
</Button>
|
||||
<Button asChild size="sm" variant={"default"}>
|
||||
<Link href="/sign-up">Sign up</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// if (!hasEnvVars) {
|
||||
// return (
|
||||
// <>
|
||||
// <div className="flex gap-4 items-center">
|
||||
// <div>
|
||||
// <Badge
|
||||
// variant={"default"}
|
||||
// className="font-normal pointer-events-none"
|
||||
// >
|
||||
// Please update .env.local file with anon key and url
|
||||
// </Badge>
|
||||
// </div>
|
||||
// <div className="flex gap-2">
|
||||
// <Button
|
||||
// asChild
|
||||
// size="sm"
|
||||
// variant={"outline"}
|
||||
// disabled
|
||||
// className="opacity-75 cursor-none pointer-events-none"
|
||||
// >
|
||||
// <Link href="/sign-in">Sign in</Link>
|
||||
// </Button>
|
||||
// <Button
|
||||
// asChild
|
||||
// size="sm"
|
||||
// variant={"default"}
|
||||
// disabled
|
||||
// className="opacity-75 cursor-none pointer-events-none"
|
||||
// >
|
||||
// <Link href="/sign-up">Sign up</Link>
|
||||
// </Button>
|
||||
// </div>
|
||||
// </div>
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
// return user ? (
|
||||
// <div className="flex items-center gap-4">
|
||||
// Hey, {user.email}!
|
||||
// <form action={signOutAction}>
|
||||
// <Button type="submit" variant={"outline"}>
|
||||
// Sign out
|
||||
// </Button>
|
||||
// </form>
|
||||
// </div>
|
||||
// ) : (
|
||||
// <div className="flex gap-2">
|
||||
// <Button asChild size="sm" variant={"outline"}>
|
||||
// <Link href="/sign-in">Sign in</Link>
|
||||
// </Button>
|
||||
// <Button asChild size="sm" variant={"default"}>
|
||||
// <Link href="/sign-up">Sign up</Link>
|
||||
// </Button>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import { Input, InputProps } from "@/app/_components/ui/input"
|
||||
import { LucideIcon } from "lucide-react"
|
||||
import { FieldError, UseFormRegisterReturn } from "react-hook-form"
|
||||
|
||||
interface FormFieldProps extends Omit<InputProps, 'error'> {
|
||||
id?: string
|
||||
label: string
|
||||
icon?: LucideIcon
|
||||
error?: FieldError
|
||||
registration: UseFormRegisterReturn
|
||||
}
|
||||
|
||||
export function ReactHookFormField({
|
||||
id,
|
||||
label,
|
||||
icon: Icon,
|
||||
error,
|
||||
registration,
|
||||
className,
|
||||
...props
|
||||
}: FormFieldProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label htmlFor={id} className="text-sm text-zinc-400">
|
||||
{label}
|
||||
</label>
|
||||
<div className="relative space-y-2">
|
||||
{Icon && <Icon className="absolute left-3 top-2.5 h-5 w-5 text-zinc-500" />}
|
||||
<Input
|
||||
id={id}
|
||||
className={`${Icon ? 'pl-10' : ''} placeholder:text-zinc-500 ${className || ''}`}
|
||||
error={!!error}
|
||||
{...registration}
|
||||
{...props}
|
||||
/>
|
||||
{error && <p className="text-sm text-red-500">{error.message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -14,7 +14,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
error && "ring-2 ring-red-500 border-red-500 focus-visible:ring-red-500",
|
||||
error && "ring-2 ring-red-500 focus-visible:ring-red-500",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
|
|
|
@ -25,4 +25,8 @@ export class CNumbers {
|
|||
static readonly MAX_UPLOAD_SIZE_MB = 50;
|
||||
static readonly MIN_PASSWORD_LENGTH = 8;
|
||||
static readonly MAX_PASSWORD_LENGTH = 128;
|
||||
|
||||
// Phone number
|
||||
static readonly PHONE_MIN_LENGTH = 10;
|
||||
static readonly PHONE_MAX_LENGTH = 13;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export class CRegex {
|
||||
static readonly PHONE_REGEX = /^(\+62|62|0)[8][1-9][0-9]{6,11}$/;
|
||||
static readonly BAN_DURATION_REGEX = /^(\d+(ns|us|µs|ms|s|m|h)|none)$/;
|
||||
}
|
|
@ -84,4 +84,8 @@ export class CTexts {
|
|||
static readonly SENSITIVE_WORDS = [
|
||||
|
||||
]
|
||||
|
||||
// Phone number
|
||||
static readonly PHONE_PREFIX = ['+62', '62', '0']
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* Definisikan tipe untuk satuan waktu yang valid
|
||||
* @description Satuan waktu yang valid: "ns", "us", "µs", "ms", "s", "m", "h"
|
||||
*/
|
||||
export type TimeUnit = "ns" | "us" | "µs" | "ms" | "s" | "m" | "h";
|
||||
|
||||
/**
|
||||
* Buat tipe untuk durasi yang valid (1ns, 2ms, 10h, dll.)
|
||||
* @description Format durasi yang valid: `${number}${TimeUnit}` atau "none"
|
||||
*/
|
||||
export type ValidBanDuration = `${number}${TimeUnit}` | "none";
|
|
@ -4,88 +4,125 @@
|
|||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
--primary: 153 60% 53%; /* Supabase green */
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 153 60% 53%; /* Matching primary */
|
||||
/* Latar belakang terang: putih bersih */
|
||||
--background: 0 0% 100%; /* #ffffff */
|
||||
--foreground: 0 0% 10%; /* #1a1a1a, teks hitam pekat */
|
||||
|
||||
/* Kartu: sama dengan latar belakang di mode terang */
|
||||
--card: 0 0% 100%; /* #ffffff */
|
||||
--card-foreground: 0 0% 10%; /* #1a1a1a */
|
||||
|
||||
/* Popover: sama dengan latar belakang */
|
||||
--popover: 0 0% 100%; /* #ffffff */
|
||||
--popover-foreground: 0 0% 10%; /* #1a1a1a */
|
||||
|
||||
/* Warna utama: hijau Supabase #006239 */
|
||||
--primary: 155% 100% 19%; /* #006239 */
|
||||
--primary-foreground: 0 0% 100%; /* #ffffff untuk kontras pada hijau */
|
||||
|
||||
/* Sekunder: abu-abu terang untuk elemen pendukung */
|
||||
--secondary: 0 0% 96%; /* #f5f5f5 */
|
||||
--secondary-foreground: 0 0% 10%; /* #1a1a1a */
|
||||
|
||||
/* Muted: abu-abu untuk teks pendukung */
|
||||
--muted: 0 0% 85%; /* #d9d9d9 */
|
||||
--muted-foreground: 0 0% 40%; /* #666666 */
|
||||
|
||||
/* Aksen: sama dengan sekunder */
|
||||
--accent: 0 0% 96%; /* #f5f5f5 */
|
||||
--accent-foreground: 0 0% 10%; /* #1a1a1a */
|
||||
|
||||
/* Destructive: merah untuk error */
|
||||
--destructive: 0 85% 60%; /* #f44336 */
|
||||
--destructive-foreground: 0 0% 100%; /* #ffffff */
|
||||
|
||||
/* Border dan input: abu-abu netral */
|
||||
--border: 0 0% 80%; /* #cccccc */
|
||||
--input: 0 0% 80%; /* #cccccc */
|
||||
|
||||
/* Ring: sama dengan primary untuk fokus */
|
||||
--ring: 155% 100% 19%; /* #006239 */
|
||||
|
||||
/* Radius: sudut membulat ringan */
|
||||
--radius: 0.5rem;
|
||||
--chart-1: 153 60% 53%; /* Supabase green */
|
||||
--chart-2: 183 65% 50%;
|
||||
--chart-3: 213 70% 47%;
|
||||
--chart-4: 243 75% 44%;
|
||||
--chart-5: 273 80% 41%;
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
--sidebar-border: 220 13% 91%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
|
||||
/* Chart: gunakan hijau Supabase dan variasi */
|
||||
--chart-1: 155% 100% 19%; /* #006239 */
|
||||
--chart-2: 160 60% 45%; /* sedikit lebih gelap */
|
||||
--chart-3: 165 55% 40%;
|
||||
--chart-4: 170 50% 35%;
|
||||
--chart-5: 175 45% 30%;
|
||||
|
||||
/* Sidebar: mirip dengan kartu di mode terang */
|
||||
--sidebar-background: 0 0% 98%; /* #fafafa */
|
||||
--sidebar-foreground: 0 0% 10%; /* #1a1a1a */
|
||||
--sidebar-primary: 155% 100% 19%; /* #006239 */
|
||||
--sidebar-primary-foreground: 0 0% 100%; /* #ffffff */
|
||||
--sidebar-accent: 0 0% 96%; /* #f5f5f5 */
|
||||
--sidebar-accent-foreground: 0 0% 10%; /* #1a1a1a */
|
||||
--sidebar-border: 0 0% 85%; /* #d9d9d9 */
|
||||
--sidebar-ring: 155% 100% 19%; /* #006239 */
|
||||
}
|
||||
|
||||
|
||||
.dark {
|
||||
--background: 0 0% 9%; /* #171717 */
|
||||
--foreground: 210 20% 98%;
|
||||
--card: 0 0% 9%; /* #171717 */
|
||||
--card-foreground: 210 20% 98%;
|
||||
--popover: 0 0% 9%; /* #171717 */
|
||||
--popover-foreground: 210 20% 98%;
|
||||
--primary: 153 60% 53%; /* Supabase green */
|
||||
--primary-foreground: 210 20% 98%;
|
||||
--secondary: 220 8% 15%;
|
||||
--secondary-foreground: 210 20% 98%;
|
||||
--muted: 220 8% 15%;
|
||||
--muted-foreground: 217 10% 64%;
|
||||
--accent: 220 8% 15%;
|
||||
--accent-foreground: 210 20% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 20% 98%;
|
||||
--border: 220 8% 15%;
|
||||
--input: 220 8% 15%;
|
||||
--ring: 153 60% 53%; /* Matching primary */
|
||||
--chart-1: 153 60% 53%; /* Supabase green */
|
||||
--chart-2: 183 65% 50%;
|
||||
--chart-3: 213 70% 47%;
|
||||
--chart-4: 243 75% 44%;
|
||||
--chart-5: 273 80% 41%;
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
/* Latar belakang gelap: abu-abu tua mendekati hitam */
|
||||
--background: 0 0% 10%; /* #1a1a1a */
|
||||
--foreground: 0 0% 85%; /* #d9d9d9, teks abu-abu terang */
|
||||
|
||||
/* Kartu: sama dengan latar belakang di mode gelap */
|
||||
--card: 0 0% 10%; /* #1a1a1a */
|
||||
--card-foreground: 0 0% 85%; /* #d9d9d9 */
|
||||
|
||||
/* Popover: sama dengan latar belakang */
|
||||
--popover: 0 0% 10%; /* #1a1a1a */
|
||||
--popover-foreground: 0 0% 85%; /* #d9d9d9 */
|
||||
|
||||
/* Warna utama: hijau Supabase tetap digunakan */
|
||||
--primary: 155% 100% 19%; /* #006239 */
|
||||
--primary-foreground: 0 0% 100%; /* #ffffff */
|
||||
|
||||
/* Sekunder: abu-abu gelap untuk elemen pendukung */
|
||||
--secondary: 0 0% 15%; /* #262626 */
|
||||
--secondary-foreground: 0 0% 85%; /* #d9d9d9 */
|
||||
|
||||
/* Muted: abu-abu gelap untuk teks pendukung */
|
||||
--muted: 0 0% 20%; /* #333333 */
|
||||
--muted-foreground: 0 0% 60%; /* #999999 */
|
||||
|
||||
/* Aksen: sama dengan sekunder */
|
||||
--accent: 0 0% 15%; /* #262626 */
|
||||
--accent-foreground: 0 0% 85%; /* #d9d9d9 */
|
||||
|
||||
/* Destructive: merah gelap untuk error */
|
||||
--destructive: 0 62% 30%; /* #802626 */
|
||||
--destructive-foreground: 0 0% 100%; /* #ffffff */
|
||||
|
||||
/* Border dan input: abu-abu gelap */
|
||||
--border: 0 0% 20%; /* #333333 */
|
||||
--input: 0 0% 20%; /* #333333 */
|
||||
|
||||
/* Ring: sama dengan primary */
|
||||
--ring: 155% 100% 19%; /* #006239 */
|
||||
|
||||
/* Chart: sama seperti mode terang */
|
||||
--chart-1: 155% 100% 19%; /* #006239 */
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 165 55% 40%;
|
||||
--chart-4: 170 50% 35%;
|
||||
--chart-5: 175 45% 30%;
|
||||
|
||||
/* Sidebar: abu-abu gelap */
|
||||
--sidebar-background: 0 0% 15%; /* #262626 */
|
||||
--sidebar-foreground: 0 0% 85%; /* #d9d9d9 */
|
||||
--sidebar-primary: 155% 100% 19%; /* #006239 */
|
||||
--sidebar-primary-foreground: 0 0% 100%; /* #ffffff */
|
||||
--sidebar-accent: 0 0% 20%; /* #333333 */
|
||||
--sidebar-accent-foreground: 0 0% 85%; /* #d9d9d9 */
|
||||
--sidebar-border: 0 0% 25%; /* #404040 */
|
||||
--sidebar-ring: 155% 100% 19%; /* #006239 */
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
|
@ -93,4 +130,4 @@
|
|||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -49,3 +49,25 @@ export function createFormData(): FormData {
|
|||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Generates a unique username based on the provided email address.
|
||||
*
|
||||
* The username is created by combining the local part of the email (before the '@' symbol)
|
||||
* with a randomly generated alphanumeric suffix.
|
||||
*
|
||||
* @param email - The email address to generate the username from.
|
||||
* @returns A string representing the generated username.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const username = generateUsername("example@gmail.com");
|
||||
* console.log(username); // Output: "example.abc123" (random suffix will vary)
|
||||
* ```
|
||||
*/
|
||||
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}`;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import { CTexts } from "../_lib/const/string";
|
||||
import { CRegex } from "../_lib/const/regex";
|
||||
|
||||
/**
|
||||
* Validates if a given phone number starts with any of the predefined prefixes.
|
||||
*
|
||||
* @param number - The phone number to validate.
|
||||
* @returns A boolean indicating whether the phone number starts with a valid prefix.
|
||||
*/
|
||||
export const phonePrefixValidation = (number: string) => CTexts.PHONE_PREFIX.some(prefix => number.startsWith(prefix));
|
||||
|
||||
|
||||
/**
|
||||
* Validates if a given phone number matches the predefined regex pattern.
|
||||
*
|
||||
* @param number - The phone number to validate.
|
||||
* @returns A boolean indicating whether the phone number matches the regex pattern.
|
||||
*/
|
||||
export const phoneRegexValidation = (number: string) => CRegex.PHONE_REGEX.test(number);
|
|
@ -11,6 +11,10 @@ import { signInController } from '@/src/interface-adapters/controllers/auth/sign
|
|||
import { signOutController } from '@/src/interface-adapters/controllers/auth/sign-out.controller';
|
||||
import { verifyOtpUseCase } from '@/src/application/use-cases/auth/verify-otp.use-case';
|
||||
import { verifyOtpController } from '@/src/interface-adapters/controllers/auth/verify-otp.controller';
|
||||
import { sendMagicLinkUseCase } from '@/src/application/use-cases/auth/send-magic-link.use-case';
|
||||
import { sendPasswordRecoveryUseCase } from '@/src/application/use-cases/auth/send-password-recovery.use-case';
|
||||
import { sendMagicLinkController } from '@/src/interface-adapters/controllers/auth/send-magic-link.controller';
|
||||
import { sendPasswordRecoveryController } from '@/src/interface-adapters/controllers/auth/send-password-recovery.controller';
|
||||
|
||||
export function createAuthenticationModule() {
|
||||
const authenticationModule = createModule();
|
||||
|
@ -66,6 +70,22 @@ export function createAuthenticationModule() {
|
|||
DI_SYMBOLS.IAuthenticationService,
|
||||
]);
|
||||
|
||||
authenticationModule
|
||||
.bind(DI_SYMBOLS.ISendMagicLinkUseCase)
|
||||
.toHigherOrderFunction(sendMagicLinkUseCase, [
|
||||
DI_SYMBOLS.IInstrumentationService,
|
||||
DI_SYMBOLS.IAuthenticationService,
|
||||
DI_SYMBOLS.IUsersRepository,
|
||||
]);
|
||||
|
||||
authenticationModule
|
||||
.bind(DI_SYMBOLS.ISendPasswordRecoveryUseCase)
|
||||
.toHigherOrderFunction(sendPasswordRecoveryUseCase, [
|
||||
DI_SYMBOLS.IInstrumentationService,
|
||||
DI_SYMBOLS.IAuthenticationService,
|
||||
DI_SYMBOLS.IUsersRepository,
|
||||
]);
|
||||
|
||||
|
||||
// Controllers
|
||||
authenticationModule
|
||||
|
@ -90,6 +110,20 @@ export function createAuthenticationModule() {
|
|||
DI_SYMBOLS.ISignOutUseCase,
|
||||
]);
|
||||
|
||||
authenticationModule
|
||||
.bind(DI_SYMBOLS.ISendMagicLinkController)
|
||||
.toHigherOrderFunction(sendMagicLinkController, [
|
||||
DI_SYMBOLS.IInstrumentationService,
|
||||
DI_SYMBOLS.ISendMagicLinkUseCase,
|
||||
]);
|
||||
|
||||
authenticationModule
|
||||
.bind(DI_SYMBOLS.ISendPasswordRecoveryController)
|
||||
.toHigherOrderFunction(sendPasswordRecoveryController, [
|
||||
DI_SYMBOLS.IInstrumentationService,
|
||||
DI_SYMBOLS.ISendPasswordRecoveryUseCase,
|
||||
]);
|
||||
|
||||
|
||||
return authenticationModule;
|
||||
}
|
|
@ -1,9 +1,7 @@
|
|||
import { createModule } from '@evyweb/ioctopus';
|
||||
|
||||
|
||||
import { DI_SYMBOLS } from '@/di/types';
|
||||
import { UsersRepository } from '@/src/infrastructure/repositories/users.repository';
|
||||
import { getUsersUseCase } from '@/src/application/use-cases/users/get-users.use-case';
|
||||
import { getUsersController } from '@/src/interface-adapters/controllers/users/get-users.controller';
|
||||
import { banUserController } from '@/src/interface-adapters/controllers/users/ban-user.controller';
|
||||
import { banUserUseCase } from '@/src/application/use-cases/users/ban-user.use-case';
|
||||
|
@ -25,6 +23,7 @@ import { deleteUserUseCase } from '@/src/application/use-cases/users/delete-user
|
|||
import { getUserByUsernameUseCase } from '@/src/application/use-cases/users/get-user-by-username.use-case';
|
||||
import { getUserByEmailUseCase } from '@/src/application/use-cases/users/get-user-by-email.use-case';
|
||||
import { updateUserUseCase } from '@/src/application/use-cases/users/update-user.use-case';
|
||||
import { getUsersUseCase } from '@/src/application/use-cases/users/get-users.use-case';
|
||||
|
||||
|
||||
export function createUsersModule() {
|
||||
|
@ -169,7 +168,7 @@ export function createUsersModule() {
|
|||
]);
|
||||
|
||||
usersModule
|
||||
.bind(DI_SYMBOLS.IGetUserByUserNameController)
|
||||
.bind(DI_SYMBOLS.IGetUserByUsernameController)
|
||||
.toHigherOrderFunction(getUserByUsernameController, [
|
||||
DI_SYMBOLS.IInstrumentationService,
|
||||
DI_SYMBOLS.IGetUserByUserNameUseCase
|
||||
|
@ -179,14 +178,16 @@ export function createUsersModule() {
|
|||
.bind(DI_SYMBOLS.IInviteUserController)
|
||||
.toHigherOrderFunction(inviteUserController, [
|
||||
DI_SYMBOLS.IInstrumentationService,
|
||||
DI_SYMBOLS.IInviteUserUseCase
|
||||
DI_SYMBOLS.IInviteUserUseCase,
|
||||
DI_SYMBOLS.IGetCurrentUserUseCase
|
||||
]);
|
||||
|
||||
usersModule
|
||||
.bind(DI_SYMBOLS.ICreateUserController)
|
||||
.toHigherOrderFunction(createUserController, [
|
||||
DI_SYMBOLS.IInstrumentationService,
|
||||
DI_SYMBOLS.ICreateUserUseCase
|
||||
DI_SYMBOLS.ICreateUserUseCase,
|
||||
DI_SYMBOLS.IGetCurrentUserUseCase
|
||||
]);
|
||||
|
||||
usersModule
|
||||
|
|
|
@ -32,6 +32,11 @@ import { IInviteUserController } from '@/src/interface-adapters/controllers/user
|
|||
import { IUpdateUserController } from '@/src/interface-adapters/controllers/users/update-user-controller';
|
||||
import { ICreateUserController } from '@/src/interface-adapters/controllers/users/create-user.controller';
|
||||
import { IDeleteUserController } from '@/src/interface-adapters/controllers/users/delete-user.controller';
|
||||
import { IGetUsersController } from '@/src/interface-adapters/controllers/users/get-users.controller';
|
||||
import { ISendMagicLinkUseCase } from '@/src/application/use-cases/auth/send-magic-link.use-case';
|
||||
import { ISendPasswordRecoveryUseCase } from '@/src/application/use-cases/auth/send-password-recovery.use-case';
|
||||
import { ISendMagicLinkController } from '@/src/interface-adapters/controllers/auth/send-magic-link.controller';
|
||||
import { ISendPasswordRecoveryController } from '@/src/interface-adapters/controllers/auth/send-password-recovery.controller';
|
||||
|
||||
export const DI_SYMBOLS = {
|
||||
// Services
|
||||
|
@ -48,6 +53,8 @@ export const DI_SYMBOLS = {
|
|||
ISignUpUseCase: Symbol.for('ISignUpUseCase'),
|
||||
IVerifyOtpUseCase: Symbol.for('IVerifyOtpUseCase'),
|
||||
ISignOutUseCase: Symbol.for('ISignOutUseCase'),
|
||||
ISendMagicLinkUseCase: Symbol.for('ISendMagicLinkUseCase'),
|
||||
ISendPasswordRecoveryUseCase: Symbol.for('ISendPasswordRecoveryUseCase'),
|
||||
|
||||
IBanUserUseCase: Symbol.for('IBanUserUseCase'),
|
||||
IUnbanUserUseCase: Symbol.for('IUnbanUserUseCase'),
|
||||
|
@ -65,6 +72,8 @@ export const DI_SYMBOLS = {
|
|||
ISignInController: Symbol.for('ISignInController'),
|
||||
ISignOutController: Symbol.for('ISignOutController'),
|
||||
IVerifyOtpController: Symbol.for('IVerifyOtpController'),
|
||||
ISendMagicLinkController: Symbol.for('ISendMagicLinkController'),
|
||||
ISendPasswordRecoveryController: Symbol.for('ISendPasswordRecoveryController'),
|
||||
|
||||
IBanUserController: Symbol.for('IBanUserController'),
|
||||
IUnbanUserController: Symbol.for('IUnbanUserController'),
|
||||
|
@ -72,7 +81,7 @@ export const DI_SYMBOLS = {
|
|||
IGetUsersController: Symbol.for('IGetUsersController'),
|
||||
IGetUserByIdController: Symbol.for('IGetUserByIdController'),
|
||||
IGetUserByEmailController: Symbol.for('IGetUserByEmailController'),
|
||||
IGetUserByUserNameController: Symbol.for('IGetUserByUserNameController'),
|
||||
IGetUserByUsernameController: Symbol.for('IGetUserByUsernameController'),
|
||||
IInviteUserController: Symbol.for('IInviteUserController'),
|
||||
ICreateUserController: Symbol.for('ICreateUserController'),
|
||||
IUpdateUserController: Symbol.for('IUpdateUserController'),
|
||||
|
@ -94,6 +103,8 @@ export interface DI_RETURN_TYPES {
|
|||
ISignUpUseCase: ISignUpUseCase;
|
||||
IVerifyOtpUseCase: IVerifyOtpUseCase;
|
||||
ISignOutUseCase: ISignOutUseCase;
|
||||
ISendMagicLinkUseCase: ISendMagicLinkUseCase;
|
||||
ISendPasswordRecoveryUseCase: ISendPasswordRecoveryUseCase;
|
||||
|
||||
IBanUserUseCase: IBanUserUseCase;
|
||||
IUnbanUserUseCase: IUnbanUserUseCase;
|
||||
|
@ -111,14 +122,16 @@ export interface DI_RETURN_TYPES {
|
|||
ISignInController: ISignInController;
|
||||
IVerifyOtpController: IVerifyOtpController;
|
||||
ISignOutController: ISignOutController;
|
||||
ISendMagicLinkController: ISendMagicLinkController;
|
||||
ISendPasswordRecoveryController: ISendPasswordRecoveryController;
|
||||
|
||||
IBanUserController: IBanUserController;
|
||||
IUnbanUserController: IUnbanUserController;
|
||||
IGetCurrentUserController: IGetCurrentUserController;
|
||||
IGetUsersController: IGetUserByUsernameController;
|
||||
IGetUsersController: IGetUsersController;
|
||||
IGetUserByIdController: IGetUserByIdController;
|
||||
IGetUserByEmailController: IGetUserByEmailController;
|
||||
IGetUserByUserNameController: IGetUserByUsernameController;
|
||||
IGetUserByUsernameController: IGetUserByUsernameController;
|
||||
IInviteUserController: IInviteUserController;
|
||||
ICreateUserController: ICreateUserController;
|
||||
IUpdateUserController: IUpdateUserController;
|
||||
|
|
|
@ -2,24 +2,24 @@ import { PrismaClient } from "@prisma/client";
|
|||
|
||||
const prismaClientSingleton = () => {
|
||||
return new PrismaClient({
|
||||
log: [
|
||||
{
|
||||
emit: 'event',
|
||||
level: 'query',
|
||||
},
|
||||
{
|
||||
emit: 'stdout',
|
||||
level: 'error',
|
||||
},
|
||||
{
|
||||
emit: 'stdout',
|
||||
level: 'info',
|
||||
},
|
||||
{
|
||||
emit: 'stdout',
|
||||
level: 'warn',
|
||||
},
|
||||
],
|
||||
// log: [
|
||||
// {
|
||||
// emit: 'event',
|
||||
// level: 'query',
|
||||
// },
|
||||
// {
|
||||
// emit: 'stdout',
|
||||
// level: 'error',
|
||||
// },
|
||||
// {
|
||||
// emit: 'stdout',
|
||||
// level: 'info',
|
||||
// },
|
||||
// {
|
||||
// emit: 'stdout',
|
||||
// level: 'warn',
|
||||
// },
|
||||
// ],
|
||||
})
|
||||
};
|
||||
|
||||
|
@ -33,8 +33,8 @@ export default db;
|
|||
|
||||
if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = db;
|
||||
|
||||
db.$on('query', (e) => {
|
||||
console.log('Query: ' + e.query)
|
||||
console.log('Params: ' + e.params)
|
||||
console.log('Duration: ' + e.duration + 'ms')
|
||||
})
|
||||
// db.$on('query', (e) => {
|
||||
// console.log('Query: ' + e.query)
|
||||
// console.log('Params: ' + e.params)
|
||||
// console.log('Duration: ' + e.duration + 'ms')
|
||||
// })
|
|
@ -1,173 +0,0 @@
|
|||
"use server";
|
||||
|
||||
import { createClient as createServerClient } from "@/app/_utils/supabase/server";
|
||||
import { SignInFormData } from "@/src/entities/models/auth/sign-in.model";
|
||||
import { VerifyOtpFormData } from "@/src/entities/models/auth/verify-otp.model";
|
||||
import { AuthenticationError } from "@/src/entities/errors/auth";
|
||||
import { DatabaseOperationError } from "@/src/entities/errors/common";
|
||||
import { createAdminClient } from "@/app/_utils/supabase/admin";
|
||||
import { IInstrumentationServiceImpl } from "@/src/application/services/instrumentation.service.interface";
|
||||
import { ICrashReporterServiceImpl } from "@/src/application/services/crash-reporter.service.interface";
|
||||
|
||||
let supabaseAdmin = createAdminClient();
|
||||
let supabaseServer = createServerClient();
|
||||
|
||||
// Server actions for authentication
|
||||
export async function signIn({ email }: SignInFormData) {
|
||||
return await IInstrumentationServiceImpl.instrumentServerAction(
|
||||
"auth.signIn",
|
||||
{ email },
|
||||
async () => {
|
||||
try {
|
||||
const supabase = await supabaseServer;
|
||||
const { data, error } = await supabase.auth.signInWithOtp({
|
||||
email,
|
||||
options: {
|
||||
shouldCreateUser: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("Error signing in:", error);
|
||||
throw new AuthenticationError(error.message);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Sign in email sent successfully",
|
||||
data,
|
||||
redirectTo: `/verify-otp?email=${encodeURIComponent(email)}`,
|
||||
};
|
||||
} catch (err) {
|
||||
ICrashReporterServiceImpl.report(err);
|
||||
if (err instanceof AuthenticationError) {
|
||||
throw err;
|
||||
}
|
||||
throw new AuthenticationError("Failed to sign in. Please try again.");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function verifyOtp({ email, token }: VerifyOtpFormData) {
|
||||
return await IInstrumentationServiceImpl.instrumentServerAction(
|
||||
"auth.verifyOtp",
|
||||
{ email },
|
||||
async () => {
|
||||
try {
|
||||
const supabase = await supabaseServer;
|
||||
const { data, error } = await supabase.auth.verifyOtp({
|
||||
email,
|
||||
token,
|
||||
type: "email",
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("Error verifying OTP:", error);
|
||||
throw new AuthenticationError(error.message);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Successfully verified!",
|
||||
data,
|
||||
redirectTo: "/dashboard",
|
||||
};
|
||||
} catch (err) {
|
||||
ICrashReporterServiceImpl.report(err);
|
||||
if (err instanceof AuthenticationError) {
|
||||
throw err;
|
||||
}
|
||||
throw new AuthenticationError("Failed to verify OTP. Please try again.");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function signOut() {
|
||||
return await IInstrumentationServiceImpl.instrumentServerAction(
|
||||
"auth.signOut",
|
||||
{},
|
||||
async () => {
|
||||
try {
|
||||
const supabase = await supabaseServer;
|
||||
const { error } = await supabase.auth.signOut();
|
||||
|
||||
if (error) {
|
||||
console.error("Error signing out:", error);
|
||||
throw new AuthenticationError(error.message);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Sign out successful",
|
||||
redirectTo: "/",
|
||||
};
|
||||
} catch (err) {
|
||||
ICrashReporterServiceImpl.report(err);
|
||||
throw new AuthenticationError("Failed to sign out. Please try again.");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function sendPasswordRecovery(email: string) {
|
||||
return await IInstrumentationServiceImpl.instrumentServerAction(
|
||||
"auth.sendPasswordRecovery",
|
||||
{ email },
|
||||
async () => {
|
||||
try {
|
||||
const supabase = createAdminClient();
|
||||
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/reset-password`,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("Error sending password recovery:", error);
|
||||
throw new DatabaseOperationError(error.message);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Password recovery email sent successfully",
|
||||
};
|
||||
} catch (err) {
|
||||
ICrashReporterServiceImpl.report(err);
|
||||
throw new DatabaseOperationError("Failed to send password recovery email. Please try again.");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function sendMagicLink(email: string) {
|
||||
return await IInstrumentationServiceImpl.instrumentServerAction(
|
||||
"auth.sendMagicLink",
|
||||
{ email },
|
||||
async () => {
|
||||
try {
|
||||
const supabase = createAdminClient();
|
||||
|
||||
const { error } = await supabase.auth.signInWithOtp({
|
||||
email,
|
||||
options: {
|
||||
emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("Error sending magic link:", error);
|
||||
throw new DatabaseOperationError(error.message);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Magic link email sent successfully",
|
||||
};
|
||||
} catch (err) {
|
||||
ICrashReporterServiceImpl.report(err);
|
||||
throw new DatabaseOperationError("Failed to send magic link email. Please try again.");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
|
@ -1,740 +1,25 @@
|
|||
import { createAdminClient } from "@/app/_utils/supabase/admin";
|
||||
import { createClient } from "@/app/_utils/supabase/client";
|
||||
import { CreateUser, InviteUser, UpdateUser, User, UserResponse } from "@/src/entities/models/users/users.model";
|
||||
import { IUserSchema, UserResponse } from "@/src/entities/models/users/users.model";
|
||||
import { ITransaction } from "@/src/entities/models/transaction.interface";
|
||||
import { ICreateUserSchema } from "@/src/entities/models/users/create-user.model";
|
||||
import { ICredentialUpdateUserSchema, IUpdateUserSchema } from "@/src/entities/models/users/update-user.model";
|
||||
import { ICredentialsBanUserSchema, IBanUserSchema } from "@/src/entities/models/users/ban-user.model";
|
||||
import { ICredentialsUnbanUserSchema } from "@/src/entities/models/users/unban-user.model";
|
||||
import { ICredentialsDeleteUserSchema } from "@/src/entities/models/users/delete-user.model";
|
||||
import { ICredentialsInviteUserSchema } from "@/src/entities/models/users/invite-user.model";
|
||||
import { ICredentialGetUserByEmailSchema, ICredentialGetUserByIdSchema, ICredentialGetUserByUsernameSchema, IGetUserByUsernameSchema } from "@/src/entities/models/users/read-user.model";
|
||||
|
||||
export interface IUsersRepository {
|
||||
getUsers(): Promise<User[]>;
|
||||
getCurrentUser(): Promise<User>;
|
||||
getUserById(id: string): Promise<User | undefined>;
|
||||
getUserByUsername(username: string): Promise<User | undefined>;
|
||||
getUserByEmail(email: string): Promise<User | undefined>;
|
||||
createUser(input: CreateUser, tx?: ITransaction): Promise<User>;
|
||||
inviteUser(email: string, tx?: ITransaction): Promise<User>;
|
||||
updateUser(id: string, input: Partial<UpdateUser>, tx?: ITransaction): Promise<User>;
|
||||
deleteUser(id: string, tx?: ITransaction): Promise<User>;
|
||||
banUser(id: string, ban_duration: string, tx?: ITransaction): Promise<User>;
|
||||
unbanUser(id: string, tx?: ITransaction): Promise<User>;
|
||||
}
|
||||
|
||||
// export class UsersRepository {
|
||||
// constructor(
|
||||
// private readonly instrumentationService: IInstrumentationService,
|
||||
// private readonly crashReporterService: ICrashReporterService,
|
||||
// private readonly supabaseAdmin = createAdminClient(),
|
||||
// private readonly supabaseClient = createClient()
|
||||
// ) { }
|
||||
|
||||
// async getUsers(): Promise<User[]> {
|
||||
// return await this.instrumentationService.startSpan({
|
||||
// name: "UsersRepository > getUsers",
|
||||
// op: 'db.query',
|
||||
// attributes: { 'db.system': 'postgres' },
|
||||
// }, async () => {
|
||||
// try {
|
||||
|
||||
// const users = await db.users.findMany({
|
||||
// include: {
|
||||
// profile: true,
|
||||
// },
|
||||
// });
|
||||
|
||||
// if (!users) {
|
||||
// throw new NotFoundError("Users not found");
|
||||
// }
|
||||
|
||||
// return users;
|
||||
|
||||
// } catch (err) {
|
||||
// this.crashReporterService.report(err);
|
||||
// throw err;
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// async getCurrentUser(): Promise<UserResponse> {
|
||||
// return await this.instrumentationService.startSpan({
|
||||
// name: "UsersRepository > getCurrentUser",
|
||||
// op: 'db.query',
|
||||
// attributes: { 'db.system': 'postgres' },
|
||||
// }, async () => {
|
||||
// try {
|
||||
// const supabase = await this.supabaseClient;
|
||||
|
||||
// const {
|
||||
// data: { user },
|
||||
// error,
|
||||
// } = await supabase.auth.getUser();
|
||||
|
||||
// if (error) {
|
||||
// console.error("Error fetching current user:", error);
|
||||
// throw new AuthenticationError(error.message);
|
||||
// }
|
||||
|
||||
// const userDetail = await db.users.findUnique({
|
||||
// where: {
|
||||
// id: user?.id,
|
||||
// },
|
||||
// include: {
|
||||
// profile: true,
|
||||
// },
|
||||
// });
|
||||
|
||||
// if (!userDetail) {
|
||||
// throw new NotFoundError("User not found");
|
||||
// }
|
||||
|
||||
// return {
|
||||
// data: {
|
||||
// user: userDetail,
|
||||
// },
|
||||
// error: null,
|
||||
// };
|
||||
// } catch (err) {
|
||||
// this.crashReporterService.report(err);
|
||||
// throw err;
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// async createUser(params: CreateUser): Promise<UserResponse> {
|
||||
// return await this.instrumentationService.startSpan({
|
||||
// name: "UsersRepository > createUser",
|
||||
// op: 'db.query',
|
||||
// attributes: { 'db.system': 'postgres' },
|
||||
// }, async () => {
|
||||
// try {
|
||||
// const supabase = this.supabaseAdmin;
|
||||
|
||||
// const { data, error } = await supabase.auth.admin.createUser({
|
||||
// email: params.email,
|
||||
// password: params.password,
|
||||
// phone: params.phone,
|
||||
// email_confirm: params.email_confirm,
|
||||
// });
|
||||
|
||||
// if (error) {
|
||||
// console.error("Error creating user:", error);
|
||||
// throw new DatabaseOperationError(error.message);
|
||||
// }
|
||||
|
||||
// return {
|
||||
// data: {
|
||||
// user: data.user,
|
||||
// },
|
||||
// error: null,
|
||||
// };
|
||||
// } catch (err) {
|
||||
// this.crashReporterService.report(err);
|
||||
// throw err;
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// async inviteUser(params: InviteUser): Promise<void> {
|
||||
// return await this.instrumentationService.startSpan({
|
||||
// name: "UsersRepository > inviteUser",
|
||||
// op: 'db.query',
|
||||
// attributes: { 'db.system': 'postgres' },
|
||||
// }, async () => {
|
||||
// try {
|
||||
// const supabase = this.supabaseAdmin;
|
||||
|
||||
// const { error } = await supabase.auth.admin.inviteUserByEmail(params.email, {
|
||||
// redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/api/auth/callback`,
|
||||
// });
|
||||
|
||||
// if (error) {
|
||||
// console.error("Error inviting user:", error);
|
||||
// throw new DatabaseOperationError(error.message);
|
||||
// }
|
||||
// } catch (err) {
|
||||
// this.crashReporterService.report(err);
|
||||
// throw err;
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// async uploadAvatar(userId: string, email: string, file: File) {
|
||||
// return await this.instrumentationService.startSpan({
|
||||
// name: "UsersRepository > uploadAvatar",
|
||||
// op: 'db.query',
|
||||
// attributes: { 'db.system': 'postgres' },
|
||||
// }, async () => {
|
||||
// try {
|
||||
// const supabase = await this.supabaseClient;
|
||||
|
||||
// const fileExt = file.name.split(".").pop();
|
||||
// const emailName = email.split("@")[0];
|
||||
// const fileName = `AVR-${emailName}.${fileExt}`;
|
||||
|
||||
// const filePath = `${userId}/${fileName}`;
|
||||
|
||||
// const { error: uploadError } = await supabase.storage
|
||||
// .from("avatars")
|
||||
// .upload(filePath, file, {
|
||||
// upsert: true,
|
||||
// contentType: file.type,
|
||||
// });
|
||||
|
||||
// if (uploadError) {
|
||||
// console.error("Error uploading avatar:", uploadError);
|
||||
// throw new DatabaseOperationError(uploadError.message);
|
||||
// }
|
||||
|
||||
// const {
|
||||
// data: { publicUrl },
|
||||
// } = supabase.storage.from("avatars").getPublicUrl(filePath);
|
||||
|
||||
// await db.users.update({
|
||||
// where: {
|
||||
// id: userId,
|
||||
// },
|
||||
// data: {
|
||||
// profile: {
|
||||
// update: {
|
||||
// avatar: publicUrl,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
|
||||
// return publicUrl;
|
||||
// } catch (err) {
|
||||
// this.crashReporterService.report(err);
|
||||
// throw err;
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// async updateUser(userId: string, params: UpdateUser): Promise<UserResponse> {
|
||||
// return await this.instrumentationService.startSpan({
|
||||
// name: "UsersRepository > updateUser",
|
||||
// op: 'db.query',
|
||||
// attributes: { 'db.system': 'postgres' },
|
||||
// }, async () => {
|
||||
// try {
|
||||
// const supabase = this.supabaseAdmin;
|
||||
|
||||
// const { data, error } = await supabase.auth.admin.updateUserById(userId, {
|
||||
// email: params.email,
|
||||
// email_confirm: params.email_confirmed_at,
|
||||
// password: params.encrypted_password ?? undefined,
|
||||
// password_hash: params.encrypted_password ?? undefined,
|
||||
// phone: params.phone,
|
||||
// phone_confirm: params.phone_confirmed_at,
|
||||
// role: params.role,
|
||||
// user_metadata: params.user_metadata,
|
||||
// app_metadata: params.app_metadata,
|
||||
// });
|
||||
|
||||
// if (error) {
|
||||
// console.error("Error updating user:", error);
|
||||
// throw new DatabaseOperationError(error.message);
|
||||
// }
|
||||
|
||||
// const user = await db.users.findUnique({
|
||||
// where: {
|
||||
// id: userId,
|
||||
// },
|
||||
// include: {
|
||||
// profile: true,
|
||||
// },
|
||||
// });
|
||||
|
||||
// if (!user) {
|
||||
// throw new NotFoundError("User not found");
|
||||
// }
|
||||
|
||||
// const updateUser = await db.users.update({
|
||||
// where: {
|
||||
// id: userId,
|
||||
// },
|
||||
// data: {
|
||||
// role: params.role || user.role,
|
||||
// invited_at: params.invited_at || user.invited_at,
|
||||
// confirmed_at: params.confirmed_at || user.confirmed_at,
|
||||
// last_sign_in_at: params.last_sign_in_at || user.last_sign_in_at,
|
||||
// is_anonymous: params.is_anonymous || user.is_anonymous,
|
||||
// created_at: params.created_at || user.created_at,
|
||||
// updated_at: params.updated_at || user.updated_at,
|
||||
// profile: {
|
||||
// update: {
|
||||
// avatar: params.profile?.avatar || user.profile?.avatar,
|
||||
// username: params.profile?.username || user.profile?.username,
|
||||
// first_name: params.profile?.first_name || user.profile?.first_name,
|
||||
// last_name: params.profile?.last_name || user.profile?.last_name,
|
||||
// bio: params.profile?.bio || user.profile?.bio,
|
||||
// address: params.profile?.address || user.profile?.address,
|
||||
// birth_date: params.profile?.birth_date || user.profile?.birth_date,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// include: {
|
||||
// profile: true,
|
||||
// },
|
||||
// });
|
||||
|
||||
// return {
|
||||
// data: {
|
||||
// user: {
|
||||
// ...data.user,
|
||||
// role: params.role,
|
||||
// profile: {
|
||||
// user_id: userId,
|
||||
// ...updateUser.profile,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// error: null,
|
||||
// };
|
||||
// } catch (err) {
|
||||
// this.crashReporterService.report(err);
|
||||
// throw err;
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// async deleteUser(userId: string): Promise<void> {
|
||||
// return await this.instrumentationService.startSpan({
|
||||
// name: "UsersRepository > deleteUser",
|
||||
// op: 'db.query',
|
||||
// attributes: { 'db.system': 'postgres' },
|
||||
// }, async () => {
|
||||
// try {
|
||||
// const supabase = this.supabaseAdmin;
|
||||
|
||||
// const { error } = await supabase.auth.admin.deleteUser(userId);
|
||||
|
||||
// if (error) {
|
||||
// console.error("Error deleting user:", error);
|
||||
// throw new DatabaseOperationError(error.message);
|
||||
// }
|
||||
// } catch (err) {
|
||||
// this.crashReporterService.report(err);
|
||||
// throw err;
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// async banUser(userId: string): Promise<UserResponse> {
|
||||
// return await this.instrumentationService.startSpan({
|
||||
// name: "UsersRepository > banUser",
|
||||
// op: 'db.query',
|
||||
// attributes: { 'db.system': 'postgres' },
|
||||
// }, async () => {
|
||||
// try {
|
||||
// const supabase = this.supabaseAdmin;
|
||||
|
||||
// const { data, error } = await supabase.auth.admin.updateUserById(userId, {
|
||||
// ban_duration: "100h",
|
||||
// });
|
||||
|
||||
// if (error) {
|
||||
// console.error("Error banning user:", error);
|
||||
// throw new DatabaseOperationError(error.message);
|
||||
// }
|
||||
|
||||
// return {
|
||||
// data: {
|
||||
// user: data.user,
|
||||
// },
|
||||
// error: null,
|
||||
// };
|
||||
// } catch (err) {
|
||||
// this.crashReporterService.report(err);
|
||||
// throw err;
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// async unbanUser(userId: string): Promise<UserResponse> {
|
||||
// return await this.instrumentationService.startSpan({
|
||||
// name: "UsersRepository > unbanUser",
|
||||
// op: 'db.query',
|
||||
// attributes: { 'db.system': 'postgres' },
|
||||
// }, async () => {
|
||||
// try {
|
||||
// const supabase = this.supabaseAdmin;
|
||||
|
||||
// const { data, error } = await supabase.auth.admin.updateUserById(userId, {
|
||||
// ban_duration: "none",
|
||||
// });
|
||||
|
||||
// if (error) {
|
||||
// console.error("Error unbanning user:", error);
|
||||
// throw new DatabaseOperationError(error.message);
|
||||
// }
|
||||
|
||||
// const user = await db.users.findUnique({
|
||||
// where: {
|
||||
// id: userId,
|
||||
// },
|
||||
// select: {
|
||||
// banned_until: true,
|
||||
// }
|
||||
// });
|
||||
|
||||
// if (!user) {
|
||||
// throw new NotFoundError("User not found");
|
||||
// }
|
||||
|
||||
// return {
|
||||
// data: {
|
||||
// user: data.user,
|
||||
// },
|
||||
// error: null,
|
||||
// };
|
||||
// } catch (err) {
|
||||
// this.crashReporterService.report(err);
|
||||
// throw err;
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
// export async function fetchUsers(): Promise<User[]> {
|
||||
// // const { data, error } = await supabase.auth.admin.getUsers();
|
||||
|
||||
// // if (error) {
|
||||
// // console.error("Error fetching users:", error);
|
||||
// // throw new Error(error.message);
|
||||
// // }
|
||||
|
||||
// // return data.users.map((user) => ({
|
||||
// // ...user,
|
||||
// // })) as User[];
|
||||
|
||||
// const users = await db.users.findMany({
|
||||
// include: {
|
||||
// profile: true,
|
||||
// },
|
||||
// });
|
||||
|
||||
// if (!users) {
|
||||
// throw new Error("Users not found");
|
||||
// }
|
||||
|
||||
// console.log("fetchedUsers");
|
||||
|
||||
// return users;
|
||||
// }
|
||||
|
||||
// // get current user
|
||||
// export async function getCurrentUser(): Promise<UserResponse> {
|
||||
// const supabase = await createClient();
|
||||
|
||||
// const {
|
||||
// data: { user },
|
||||
// error,
|
||||
// } = await supabase.auth.getUser();
|
||||
|
||||
// if (error) {
|
||||
// console.error("Error fetching current user:", error);
|
||||
// throw new Error(error.message);
|
||||
// }
|
||||
|
||||
// const userDetail = await db.users.findUnique({
|
||||
// where: {
|
||||
// id: user?.id,
|
||||
// },
|
||||
// include: {
|
||||
// profile: true,
|
||||
// },
|
||||
// });
|
||||
|
||||
// if (!userDetail) {
|
||||
// throw new Error("User not found");
|
||||
// }
|
||||
|
||||
// return {
|
||||
// data: {
|
||||
// user: userDetail,
|
||||
// },
|
||||
// error: null,
|
||||
// };
|
||||
// }
|
||||
|
||||
// // Create a new user
|
||||
// export async function createUser(
|
||||
// params: CreateUser
|
||||
// ): Promise<UserResponse> {
|
||||
// const supabase = createAdminClient();
|
||||
|
||||
// const { data, error } = await supabase.auth.admin.createUser({
|
||||
// email: params.email,
|
||||
// password: params.password,
|
||||
// phone: params.phone,
|
||||
// email_confirm: params.email_confirm,
|
||||
// });
|
||||
|
||||
// if (error) {
|
||||
// console.error("Error creating user:", error);
|
||||
// throw new Error(error.message);
|
||||
// }
|
||||
|
||||
// return {
|
||||
// data: {
|
||||
// user: data.user,
|
||||
// },
|
||||
// error: null,
|
||||
// };
|
||||
// }
|
||||
|
||||
// export async function uploadAvatar(userId: string, email: string, file: File) {
|
||||
// try {
|
||||
// const supabase = await createClient();
|
||||
|
||||
// const fileExt = file.name.split(".").pop();
|
||||
// const emailName = email.split("@")[0];
|
||||
// const fileName = `AVR-${emailName}.${fileExt}`;
|
||||
|
||||
// // Change this line - store directly in the user's folder
|
||||
// const filePath = `${userId}/${fileName}`;
|
||||
|
||||
// // Upload the avatar to Supabase storage
|
||||
// const { error: uploadError } = await supabase.storage
|
||||
// .from("avatars")
|
||||
// .upload(filePath, file, {
|
||||
// upsert: true,
|
||||
// contentType: file.type,
|
||||
// });
|
||||
|
||||
// if (uploadError) {
|
||||
// console.error("Error uploading avatar:", uploadError);
|
||||
// throw uploadError;
|
||||
// }
|
||||
|
||||
// // Get the public URL
|
||||
// const {
|
||||
// data: { publicUrl },
|
||||
// } = supabase.storage.from("avatars").getPublicUrl(filePath);
|
||||
|
||||
// // Update user profile with the new avatar URL
|
||||
// await db.users.update({
|
||||
// where: {
|
||||
// id: userId,
|
||||
// },
|
||||
// data: {
|
||||
// profile: {
|
||||
// update: {
|
||||
// avatar: publicUrl,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
|
||||
// return publicUrl;
|
||||
// } catch (error) {
|
||||
// console.error("Error uploading avatar:", error);
|
||||
// throw error;
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// // Update an existing user
|
||||
// export async function updateUser(
|
||||
// userId: string,
|
||||
// params: UpdateUser
|
||||
// ): Promise<UserResponse> {
|
||||
// const supabase = createAdminClient();
|
||||
|
||||
// const { data, error } = await supabase.auth.admin.updateUserById(userId, {
|
||||
// email: params.email,
|
||||
// email_confirm: params.email_confirmed_at,
|
||||
// password: params.encrypted_password ?? undefined,
|
||||
// password_hash: params.encrypted_password ?? undefined,
|
||||
// phone: params.phone,
|
||||
// phone_confirm: params.phone_confirmed_at,
|
||||
// role: params.role,
|
||||
// user_metadata: params.user_metadata,
|
||||
// app_metadata: params.app_metadata,
|
||||
// });
|
||||
|
||||
// if (error) {
|
||||
// console.error("Error updating user:", error);
|
||||
// throw new Error(error.message);
|
||||
// }
|
||||
|
||||
// const user = await db.users.findUnique({
|
||||
// where: {
|
||||
// id: userId,
|
||||
// },
|
||||
// include: {
|
||||
// profile: true,
|
||||
// },
|
||||
// });
|
||||
|
||||
// if (!user) {
|
||||
// throw new Error("User not found");
|
||||
// }
|
||||
|
||||
// const updateUser = await db.users.update({
|
||||
// where: {
|
||||
// id: userId,
|
||||
// },
|
||||
// data: {
|
||||
// role: params.role || user.role,
|
||||
// invited_at: params.invited_at || user.invited_at,
|
||||
// confirmed_at: params.confirmed_at || user.confirmed_at,
|
||||
// // recovery_sent_at: params.recovery_sent_at || user.recovery_sent_at,
|
||||
// last_sign_in_at: params.last_sign_in_at || user.last_sign_in_at,
|
||||
// is_anonymous: params.is_anonymous || user.is_anonymous,
|
||||
// created_at: params.created_at || user.created_at,
|
||||
// updated_at: params.updated_at || user.updated_at,
|
||||
// profile: {
|
||||
// update: {
|
||||
// avatar: params.profile?.avatar || user.profile?.avatar,
|
||||
// username: params.profile?.username || user.profile?.username,
|
||||
// first_name: params.profile?.first_name || user.profile?.first_name,
|
||||
// last_name: params.profile?.last_name || user.profile?.last_name,
|
||||
// bio: params.profile?.bio || user.profile?.bio,
|
||||
// address: params.profile?.address || user.profile?.address,
|
||||
// birth_date: params.profile?.birth_date || user.profile?.birth_date,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// include: {
|
||||
// profile: true,
|
||||
// },
|
||||
// });
|
||||
|
||||
// return {
|
||||
// data: {
|
||||
// user: {
|
||||
// ...data.user,
|
||||
// role: params.role,
|
||||
// profile: {
|
||||
// user_id: userId,
|
||||
// ...updateUser.profile,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// error: null,
|
||||
// };
|
||||
// }
|
||||
|
||||
// // Delete a user
|
||||
// export async function deleteUser(userId: string): Promise<void> {
|
||||
// const supabase = createAdminClient();
|
||||
|
||||
// const { error } = await supabase.auth.admin.deleteUser(userId);
|
||||
|
||||
// if (error) {
|
||||
// console.error("Error deleting user:", error);
|
||||
// throw new Error(error.message);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Send password recovery email
|
||||
// export async function sendPasswordRecovery(email: string): Promise<void> {
|
||||
// const supabase = createAdminClient();
|
||||
|
||||
// const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
// redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/reset-password`,
|
||||
// });
|
||||
|
||||
// if (error) {
|
||||
// console.error("Error sending password recovery:", error);
|
||||
// throw new Error(error.message);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Send magic link
|
||||
// export async function sendMagicLink(email: string): Promise<void> {
|
||||
// const supabase = createAdminClient();
|
||||
|
||||
// const { error } = await supabase.auth.signInWithOtp({
|
||||
// email,
|
||||
// options: {
|
||||
// emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
|
||||
// },
|
||||
// });
|
||||
|
||||
// if (error) {
|
||||
// console.error("Error sending magic link:", error);
|
||||
// throw new Error(error.message);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Ban a user
|
||||
// export async function banUser(userId: string): Promise<UserResponse> {
|
||||
// const supabase = createAdminClient();
|
||||
|
||||
// // Ban for 100 years (effectively permanent)
|
||||
// const banUntil = new Date();
|
||||
// banUntil.setFullYear(banUntil.getFullYear() + 100);
|
||||
|
||||
// const { data, error } = await supabase.auth.admin.updateUserById(userId, {
|
||||
// ban_duration: "100h",
|
||||
// });
|
||||
|
||||
// if (error) {
|
||||
// console.error("Error banning user:", error);
|
||||
// throw new Error(error.message);
|
||||
// }
|
||||
|
||||
// return {
|
||||
// data: {
|
||||
// user: data.user,
|
||||
// },
|
||||
// error: null,
|
||||
// };
|
||||
// }
|
||||
|
||||
// // Unban a user
|
||||
// export async function unbanUser(userId: string): Promise<UserResponse> {
|
||||
// const supabase = createAdminClient();
|
||||
|
||||
// const { data, error } = await supabase.auth.admin.updateUserById(userId, {
|
||||
// ban_duration: "none",
|
||||
// });
|
||||
|
||||
// if (error) {
|
||||
// console.error("Error unbanning user:", error);
|
||||
// throw new Error(error.message);
|
||||
// }
|
||||
|
||||
// const user = await db.users.findUnique({
|
||||
// where: {
|
||||
// id: userId,
|
||||
// },
|
||||
// select: {
|
||||
// banned_until: true,
|
||||
// }
|
||||
// })
|
||||
|
||||
// if (!user) {
|
||||
// throw new Error("User not found");
|
||||
// }
|
||||
|
||||
// // const updateUser = await db.users.update({
|
||||
// // where: {
|
||||
// // id: userId,
|
||||
// // },
|
||||
// // data: {
|
||||
// // banned_until: null,
|
||||
// // },
|
||||
// // })
|
||||
|
||||
// return {
|
||||
// data: {
|
||||
// user: data.user,
|
||||
// },
|
||||
// error: null,
|
||||
// };
|
||||
// }
|
||||
|
||||
// // Invite a user
|
||||
// export async function inviteUser(params: InviteUser): Promise<void> {
|
||||
// const supabase = createAdminClient();
|
||||
|
||||
// const { error } = await supabase.auth.admin.inviteUserByEmail(params.email, {
|
||||
// redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
|
||||
// });
|
||||
|
||||
// if (error) {
|
||||
// console.error("Error inviting user:", error);
|
||||
// throw new Error(error.message);
|
||||
// }
|
||||
// }
|
||||
getUsers(): Promise<IUserSchema[]>;
|
||||
getCurrentUser(): Promise<IUserSchema>;
|
||||
getUserById(credential: ICredentialGetUserByIdSchema): Promise<IUserSchema | undefined>;
|
||||
getUserByUsername(credential: ICredentialGetUserByUsernameSchema): Promise<IUserSchema | undefined>;
|
||||
getUserByEmail(credential: ICredentialGetUserByEmailSchema): Promise<IUserSchema | undefined>;
|
||||
createUser(input: ICreateUserSchema, tx?: ITransaction): Promise<IUserSchema>;
|
||||
inviteUser(credential: ICredentialsInviteUserSchema, tx?: ITransaction): Promise<IUserSchema>;
|
||||
updateUser(credential: ICredentialUpdateUserSchema, input: Partial<IUpdateUserSchema>, tx?: ITransaction): Promise<IUserSchema>;
|
||||
deleteUser(credential: ICredentialsDeleteUserSchema, tx?: ITransaction): Promise<IUserSchema>;
|
||||
banUser(credential: ICredentialsBanUserSchema, input: IBanUserSchema, tx?: ITransaction): Promise<IUserSchema>;
|
||||
unbanUser(credential: ICredentialsUnbanUserSchema, tx?: ITransaction): Promise<IUserSchema>;
|
||||
}
|
|
@ -1,18 +1,20 @@
|
|||
import { AuthResult } from "@/src/entities/models/auth/auth-result.model"
|
||||
import { ISendMagicLinkSchema } from "@/src/entities/models/auth/send-magic-link.model"
|
||||
import { ISendPasswordRecoverySchema } from "@/src/entities/models/auth/send-password-recovery.model"
|
||||
import { Session } from "@/src/entities/models/auth/session.model"
|
||||
import { SignInFormData, SignInPasswordless, SignInWithPassword } from "@/src/entities/models/auth/sign-in.model"
|
||||
import { SignUpWithEmail, SignUpWithPhone } from "@/src/entities/models/auth/sign-up.model"
|
||||
import { VerifyOtpFormData } from "@/src/entities/models/auth/verify-otp.model"
|
||||
import { User } from "@/src/entities/models/users/users.model"
|
||||
import { TSignInSchema, ISignInWithPasswordSchema, ISignInPasswordlessSchema } from "@/src/entities/models/auth/sign-in.model"
|
||||
import { SignUpWithEmailSchema, SignUpWithPhoneSchema, TSignUpSchema, ISignUpWithEmailSchema, ISignUpWithPhoneSchema } from "@/src/entities/models/auth/sign-up.model"
|
||||
import { IVerifyOtpSchema } from "@/src/entities/models/auth/verify-otp.model"
|
||||
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||
|
||||
export interface IAuthenticationService {
|
||||
signInPasswordless(credentials: SignInPasswordless): Promise<void>
|
||||
signInWithPassword(credentials: SignInWithPassword): Promise<void>
|
||||
signUpWithEmail(credentials: SignUpWithEmail): Promise<User>
|
||||
signUpWithPhone(credentials: SignUpWithPhone): Promise<User>
|
||||
signInPasswordless(credentials: ISignInPasswordlessSchema): Promise<void>
|
||||
SignInWithPasswordSchema(credentials: ISignInWithPasswordSchema): Promise<void>
|
||||
SignUpWithEmailSchema(credentials: ISignUpWithEmailSchema): Promise<IUserSchema>
|
||||
SignUpWithPhoneSchema(credentials: ISignUpWithPhoneSchema): Promise<IUserSchema>
|
||||
getSession(): Promise<Session | null>
|
||||
signOut(): Promise<void>
|
||||
sendMagicLink(email: string): Promise<void>
|
||||
sendPasswordRecovery(email: string): Promise<void>
|
||||
verifyOtp(credentials: VerifyOtpFormData): Promise<void>
|
||||
sendMagicLink(credentials: ISendMagicLinkSchema): Promise<void>
|
||||
sendPasswordRecovery(credentials: ISendPasswordRecoverySchema): Promise<void>
|
||||
verifyOtp(credentials: IVerifyOtpSchema): Promise<void>
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import type { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
|
||||
import { AuthenticationError, UnauthenticatedError } from "@/src/entities/errors/auth";
|
||||
import { type SignInFormData, SignInPasswordless, SignInSchema } from "@/src/entities/models/auth/sign-in.model"
|
||||
import { type TSignInSchema, ISignInPasswordlessSchema, SignInSchema } from "@/src/entities/models/auth/sign-in.model"
|
||||
import { IAuthenticationService } from "@/src/application/services/authentication.service.interface";
|
||||
import { IUsersRepository } from "../../repositories/users.repository.interface";
|
||||
|
||||
|
@ -12,11 +12,11 @@ export const signInUseCase =
|
|||
authenticationService: IAuthenticationService,
|
||||
usersRepository: IUsersRepository
|
||||
) =>
|
||||
async (input: SignInPasswordless): Promise<void> => {
|
||||
async (input: ISignInPasswordlessSchema): Promise<void> => {
|
||||
return instrumentationService.startSpan({ name: "signIn Use Case", op: "function" },
|
||||
async () => {
|
||||
|
||||
const existingUser = await usersRepository.getUserByEmail(input.email)
|
||||
const existingUser = await usersRepository.getUserByEmail({ email: input.email })
|
||||
|
||||
if (!existingUser) {
|
||||
throw new UnauthenticatedError("User not found. Please tell your admin to create an account for you.")
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { CreateUser, User } from "@/src/entities/models/users/users.model"
|
||||
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||
import { IAuthenticationService } from "../../services/authentication.service.interface"
|
||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||
import { AuthenticationError } from "@/src/entities/errors/auth"
|
||||
import { SignUpFormData } from "@/src/entities/models/auth/sign-up.model"
|
||||
import { AuthResult } from "@/src/entities/models/auth/auth-result.model"
|
||||
import { ISignUpWithEmailSchema } from "@/src/entities/models/auth/sign-up.model"
|
||||
|
||||
|
||||
|
||||
export type ISignUpUseCase = ReturnType<typeof signUpUseCase>
|
||||
|
@ -13,21 +13,21 @@ export const signUpUseCase = (
|
|||
instrumentationService: IInstrumentationService,
|
||||
authenticationService: IAuthenticationService,
|
||||
usersRepository: IUsersRepository
|
||||
) => async (input: SignUpFormData): Promise<User> => {
|
||||
) => async (input: ISignUpWithEmailSchema): Promise<IUserSchema> => {
|
||||
return await instrumentationService.startSpan({ name: "signUp Use Case", op: "function" },
|
||||
async () => {
|
||||
const existingUser = await usersRepository.getUserByEmail(input.email)
|
||||
const existingUser = await usersRepository.getUserByEmail({ email: input.email })
|
||||
|
||||
if (existingUser) {
|
||||
throw new AuthenticationError("User already exists")
|
||||
}
|
||||
|
||||
const newUser = await authenticationService.signUpWithEmail({
|
||||
const newUser = await authenticationService.SignUpWithEmailSchema({
|
||||
email: input.email,
|
||||
password: input.password
|
||||
})
|
||||
|
||||
await authenticationService.signInWithPassword({
|
||||
await authenticationService.SignInWithPasswordSchema({
|
||||
email: input.email,
|
||||
password: input.password
|
||||
})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { VerifyOtpFormData } from "@/src/entities/models/auth/verify-otp.model"
|
||||
import { VerifyOtpSchema } from "@/src/entities/models/auth/verify-otp.model"
|
||||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||
import { IAuthenticationService } from "../../services/authentication.service.interface"
|
||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||
|
@ -12,7 +12,7 @@ export const verifyOtpUseCase = (
|
|||
instrumentationService: IInstrumentationService,
|
||||
authenticationService: IAuthenticationService,
|
||||
usersRepository: IUsersRepository
|
||||
) => async (input: VerifyOtpFormData): Promise<void> => {
|
||||
) => async (input: VerifyOtpSchema): Promise<void> => {
|
||||
return await instrumentationService.startSpan({ name: "verifyOtp Use Case", op: "function" },
|
||||
async () => {
|
||||
const user = await usersRepository.getUserByEmail(input.email)
|
||||
|
|
|
@ -1,23 +1,26 @@
|
|||
import { User } from "@/src/entities/models/users/users.model"
|
||||
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||
import { NotFoundError } from "@/src/entities/errors/common"
|
||||
import { IBanUserSchema, ICredentialsBanUserSchema } from "@/src/entities/models/users/ban-user.model"
|
||||
|
||||
export type IBanUserUseCase = ReturnType<typeof banUserUseCase>
|
||||
|
||||
export const banUserUseCase = (
|
||||
instrumentationService: IInstrumentationService,
|
||||
usersRepository: IUsersRepository
|
||||
) => async (id: string, ban_duration: string): Promise<User> => {
|
||||
) => async (credential: ICredentialsBanUserSchema, input: IBanUserSchema): Promise<IUserSchema> => {
|
||||
return await instrumentationService.startSpan({ name: "banUser Use Case", op: "function" },
|
||||
async () => {
|
||||
const existingUser = await usersRepository.getUserById(id)
|
||||
const existingUser = await usersRepository.getUserById(credential)
|
||||
|
||||
if (!existingUser) {
|
||||
throw new NotFoundError("User not found")
|
||||
}
|
||||
|
||||
const bannedUser = await usersRepository.banUser(id, ban_duration)
|
||||
const bannedUser = await usersRepository.banUser(credential, input)
|
||||
|
||||
console.log("Use Case: Ban User")
|
||||
|
||||
return bannedUser
|
||||
}
|
||||
|
|
|
@ -2,8 +2,9 @@ import { AuthenticationError } from "@/src/entities/errors/auth"
|
|||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||
import { IAuthenticationService } from "../../services/authentication.service.interface"
|
||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||
import { CreateUser, User } from "@/src/entities/models/users/users.model"
|
||||
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||
import { InputParseError } from "@/src/entities/errors/common"
|
||||
import { ICreateUserSchema } from "@/src/entities/models/users/create-user.model"
|
||||
|
||||
|
||||
export type ICreateUserUseCase = ReturnType<typeof createUserUseCase>
|
||||
|
@ -11,11 +12,11 @@ export type ICreateUserUseCase = ReturnType<typeof createUserUseCase>
|
|||
export const createUserUseCase = (
|
||||
instrumentationService: IInstrumentationService,
|
||||
usersRepository: IUsersRepository,
|
||||
) => async (input: CreateUser): Promise<User> => {
|
||||
) => async (input: ICreateUserSchema): Promise<IUserSchema> => {
|
||||
return await instrumentationService.startSpan({ name: "createUser Use Case", op: "function" },
|
||||
async () => {
|
||||
|
||||
const existingUser = await usersRepository.getUserByEmail(input.email)
|
||||
const existingUser = await usersRepository.getUserByEmail({ email: input.email })
|
||||
|
||||
if (existingUser) {
|
||||
throw new AuthenticationError("User already exists")
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
import { NotFoundError } from "@/src/entities/errors/common"
|
||||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||
import { User } from "@/src/entities/models/users/users.model"
|
||||
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||
import { ICredentialsDeleteUserSchema } from "@/src/entities/models/users/delete-user.model"
|
||||
|
||||
export type IDeleteUserUseCase = ReturnType<typeof deleteUserUseCase>
|
||||
|
||||
export const deleteUserUseCase = (
|
||||
instrumentationService: IInstrumentationService,
|
||||
usersRepository: IUsersRepository
|
||||
) => async (id: string): Promise<User> => {
|
||||
) => async (credential: ICredentialsDeleteUserSchema): Promise<IUserSchema> => {
|
||||
return await instrumentationService.startSpan({ name: "deleteUser Use Case", op: "function" },
|
||||
async () => {
|
||||
const user = await usersRepository.getUserById(id)
|
||||
const user = await usersRepository.getUserById(credential)
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundError("User not found")
|
||||
}
|
||||
|
||||
const deletedUser = await usersRepository.deleteUser(id)
|
||||
const deletedUser = await usersRepository.deleteUser(credential)
|
||||
|
||||
return deletedUser
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { NotFoundError } from "@/src/entities/errors/common"
|
||||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||
import { User, UserResponse } from "@/src/entities/models/users/users.model"
|
||||
import { IUserSchema, UserResponse } from "@/src/entities/models/users/users.model"
|
||||
import { AuthenticationError } from "@/src/entities/errors/auth"
|
||||
|
||||
|
||||
|
@ -10,7 +10,7 @@ export type IGetCurrentUserUseCase = ReturnType<typeof getCurrentUserUseCase>
|
|||
export const getCurrentUserUseCase = (
|
||||
instrumentationService: IInstrumentationService,
|
||||
usersRepository: IUsersRepository
|
||||
) => async (): Promise<User> => {
|
||||
) => async (): Promise<IUserSchema> => {
|
||||
return await instrumentationService.startSpan({ name: "getCurrentUser Use Case", op: "function" },
|
||||
async () => {
|
||||
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import { User } from "@/src/entities/models/users/users.model"
|
||||
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||
import { NotFoundError } from "@/src/entities/errors/common"
|
||||
import { ICredentialGetUserByEmailSchema } from "@/src/entities/models/users/read-user.model"
|
||||
|
||||
export type IGetUserByEmailUseCase = ReturnType<typeof getUserByEmailUseCase>
|
||||
|
||||
export const getUserByEmailUseCase = (
|
||||
instrumentationService: IInstrumentationService,
|
||||
usersRepository: IUsersRepository
|
||||
) => async (email: string): Promise<User> => {
|
||||
) => async (credential: ICredentialGetUserByEmailSchema): Promise<IUserSchema> => {
|
||||
return await instrumentationService.startSpan({ name: "getUserByEmail Use Case", op: "function" },
|
||||
async () => {
|
||||
|
||||
const user = await usersRepository.getUserByEmail(email)
|
||||
const user = await usersRepository.getUserByEmail(credential)
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundError("User not found")
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { User } from "@/src/entities/models/users/users.model"
|
||||
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||
import { AuthenticationError } from "@/src/entities/errors/auth"
|
||||
import { NotFoundError } from "@/src/entities/errors/common"
|
||||
import { ICredentialGetUserByIdSchema } from "@/src/entities/models/users/read-user.model"
|
||||
|
||||
|
||||
export type IGetUserByIdUseCase = ReturnType<typeof getUserByIdUseCase>
|
||||
|
@ -10,11 +11,11 @@ export type IGetUserByIdUseCase = ReturnType<typeof getUserByIdUseCase>
|
|||
export const getUserByIdUseCase = (
|
||||
instrumentationService: IInstrumentationService,
|
||||
usersRepository: IUsersRepository
|
||||
) => async (id: string): Promise<User> => {
|
||||
) => async (credential: ICredentialGetUserByIdSchema): Promise<IUserSchema> => {
|
||||
return await instrumentationService.startSpan({ name: "getUserById Use Case", op: "function" },
|
||||
async () => {
|
||||
|
||||
const user = await usersRepository.getUserById(id)
|
||||
const user = await usersRepository.getUserById(credential)
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundError("User not found")
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import { NotFoundError } from "@/src/entities/errors/common"
|
||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||
import { User } from "@/src/entities/models/users/users.model"
|
||||
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||
import { ICredentialGetUserByUsernameSchema } from "@/src/entities/models/users/read-user.model"
|
||||
|
||||
export type IGetUserByUsernameUseCase = ReturnType<typeof getUserByUsernameUseCase>
|
||||
|
||||
export const getUserByUsernameUseCase = (
|
||||
instrumentationService: IInstrumentationService,
|
||||
usersRepository: IUsersRepository
|
||||
) => async (username: string): Promise<User> => {
|
||||
) => async (credential: ICredentialGetUserByUsernameSchema): Promise<IUserSchema> => {
|
||||
return await instrumentationService.startSpan({ name: "getUserByUsername Use Case", op: "function" },
|
||||
async () => {
|
||||
|
||||
const user = await usersRepository.getUserByUsername(username)
|
||||
const user = await usersRepository.getUserByUsername(credential)
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundError("User not found")
|
||||
|
|
|
@ -8,7 +8,7 @@ export const getUsersUseCase = (
|
|||
instrumentationService: IInstrumentationService,
|
||||
usersRepository: IUsersRepository
|
||||
) => async (): Promise<User[]> => {
|
||||
return await instrumentationService.startSpan({ name: "getgetUsers Use Case", op: "function" },
|
||||
return instrumentationService.startSpan({ name: "getUsers Use Case", op: "function" },
|
||||
async () => {
|
||||
const users = await usersRepository.getUsers()
|
||||
|
||||
|
|
|
@ -2,7 +2,8 @@ import { AuthenticationError } from "@/src/entities/errors/auth"
|
|||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||
import { IAuthenticationService } from "../../services/authentication.service.interface"
|
||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||
import { User } from "@/src/entities/models/users/users.model"
|
||||
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||
import { ICredentialsInviteUserSchema } from "@/src/entities/models/users/invite-user.model"
|
||||
|
||||
|
||||
export type IInviteUserUseCase = ReturnType<typeof inviteUserUseCase>
|
||||
|
@ -10,17 +11,16 @@ export type IInviteUserUseCase = ReturnType<typeof inviteUserUseCase>
|
|||
export const inviteUserUseCase = (
|
||||
instrumentationService: IInstrumentationService,
|
||||
usersRepository: IUsersRepository,
|
||||
authenticationService: IAuthenticationService,
|
||||
) => async (email: string): Promise<User> => {
|
||||
) => async (credential: ICredentialsInviteUserSchema): Promise<IUserSchema> => {
|
||||
return await instrumentationService.startSpan({ name: "inviteUser Use Case", op: "function" },
|
||||
async () => {
|
||||
const existingUser = await usersRepository.getUserByEmail(email)
|
||||
const existingUser = await usersRepository.getUserByEmail(credential)
|
||||
|
||||
if (existingUser) {
|
||||
throw new AuthenticationError("User already exists")
|
||||
}
|
||||
|
||||
const invitedUser = await usersRepository.inviteUser(email)
|
||||
const invitedUser = await usersRepository.inviteUser(credential)
|
||||
|
||||
if (!invitedUser) {
|
||||
throw new AuthenticationError("User not invited")
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
import { NotFoundError } from "@/src/entities/errors/common"
|
||||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||
import { User } from "@/src/entities/models/users/users.model"
|
||||
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||
import { ICredentialsUnbanUserSchema } from "@/src/entities/models/users/unban-user.model"
|
||||
|
||||
export type IUnbanUserUseCase = ReturnType<typeof unbanUserUseCase>
|
||||
|
||||
export const unbanUserUseCase = (
|
||||
instrumentationService: IInstrumentationService,
|
||||
usersRepository: IUsersRepository
|
||||
) => async (id: string): Promise<User> => {
|
||||
) => async (credential: ICredentialsUnbanUserSchema): Promise<IUserSchema> => {
|
||||
return await instrumentationService.startSpan({ name: "unbanUser Use Case", op: "function" },
|
||||
async () => {
|
||||
const existingUser = await usersRepository.getUserById(id)
|
||||
const existingUser = await usersRepository.getUserById(credential)
|
||||
|
||||
if (!existingUser) {
|
||||
throw new NotFoundError("User not found")
|
||||
}
|
||||
|
||||
const unbanUser = await usersRepository.unbanUser(id)
|
||||
const unbanUser = await usersRepository.unbanUser(credential)
|
||||
|
||||
return unbanUser
|
||||
}
|
||||
|
|
|
@ -1,24 +1,25 @@
|
|||
import { UpdateUser, User, UserResponse } from "@/src/entities/models/users/users.model"
|
||||
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||
import { NotFoundError } from "@/src/entities/errors/common"
|
||||
import { ICredentialUpdateUserSchema, IUpdateUserSchema } from "@/src/entities/models/users/update-user.model"
|
||||
|
||||
export type IUpdateUserUseCase = ReturnType<typeof updateUserUseCase>
|
||||
|
||||
export const updateUserUseCase = (
|
||||
instrumentationService: IInstrumentationService,
|
||||
usersRepository: IUsersRepository
|
||||
) => async (id: string, input: UpdateUser): Promise<User> => {
|
||||
) => async (credential: ICredentialUpdateUserSchema, input: IUpdateUserSchema): Promise<IUserSchema> => {
|
||||
return await instrumentationService.startSpan({ name: "updateUser Use Case", op: "function" },
|
||||
async () => {
|
||||
|
||||
const existingUser = await usersRepository.getUserById(id)
|
||||
const existingUser = await usersRepository.getUserById(credential)
|
||||
|
||||
if (!existingUser) {
|
||||
throw new NotFoundError("User not found")
|
||||
}
|
||||
|
||||
const updatedUser = await usersRepository.updateUser(id, input)
|
||||
const updatedUser = await usersRepository.updateUser(credential, input)
|
||||
|
||||
return updatedUser
|
||||
}
|
||||
|
|
|
@ -1,18 +1,27 @@
|
|||
export class DatabaseOperationError extends Error {
|
||||
constructor(message: string, options?: ErrorOptions) {
|
||||
super(message, options);
|
||||
}
|
||||
constructor(message: string, options?: ErrorOptions) {
|
||||
super(message, options);
|
||||
}
|
||||
|
||||
export class NotFoundError extends Error {
|
||||
constructor(message: string, options?: ErrorOptions) {
|
||||
super(message, options);
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends Error {
|
||||
constructor(message: string, options?: ErrorOptions) {
|
||||
super(message, options);
|
||||
}
|
||||
|
||||
export class InputParseError extends Error {
|
||||
constructor(message: string, options?: ErrorOptions) {
|
||||
super(message, options);
|
||||
}
|
||||
}
|
||||
|
||||
export class InputParseError extends Error {
|
||||
constructor(message: string, options?: ErrorOptions) {
|
||||
super(message, options);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class ServerActionError extends Error {
|
||||
code: string;
|
||||
|
||||
constructor(message: string, code: string) {
|
||||
super(message);
|
||||
this.name = "ServerActionError";
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const SendMagicLinkSchema = z.object({
|
||||
email: z.string().min(1, { message: "Email is required" }).email({ message: "Please enter a valid email address" }),
|
||||
})
|
||||
|
||||
export type ISendMagicLinkSchema = z.infer<typeof SendMagicLinkSchema>
|
||||
|
||||
export const defaulISendMagicLinkSchemaValues: ISendMagicLinkSchema = {
|
||||
email: "",
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const SendPasswordRecoverySchema = z.object({
|
||||
email: z.string().min(1, { message: "Email is required" }).email({ message: "Please enter a valid email address" }),
|
||||
})
|
||||
|
||||
export type ISendPasswordRecoverySchema = z.infer<typeof SendPasswordRecoverySchema>
|
||||
|
||||
export const defaulISendPasswordRecoverySchemaValues: ISendPasswordRecoverySchema = {
|
||||
email: "",
|
||||
}
|
|
@ -6,40 +6,40 @@ export const SignInSchema = z.object({
|
|||
.string()
|
||||
.min(1, { message: "Email is required" })
|
||||
.email({ message: "Please enter a valid email address" }),
|
||||
password: z.string().min(1, { message: "Password is required" }),
|
||||
password: z.string().min(1, { message: "Password is required" }).min(8, { message: "Password must be at least 8 characters" }),
|
||||
phone: z.string().optional(),
|
||||
});
|
||||
|
||||
// Export the type derived from the schema
|
||||
export type SignInFormData = z.infer<typeof SignInSchema>;
|
||||
export type TSignInSchema = z.infer<typeof SignInSchema>;
|
||||
|
||||
export const SignInWithPassword = SignInSchema.pick({
|
||||
export const SignInWithPasswordSchema = SignInSchema.pick({
|
||||
email: true,
|
||||
password: true,
|
||||
phone: true
|
||||
})
|
||||
|
||||
|
||||
export type ISignInWithPasswordSchema = z.infer<typeof SignInWithPasswordSchema>
|
||||
|
||||
// Default values for the form
|
||||
export const defaultSignInWithPasswordValues: SignInWithPassword = {
|
||||
export const defaulISignInWithPasswordSchemaValues: ISignInWithPasswordSchema = {
|
||||
email: "",
|
||||
password: "",
|
||||
phone: ""
|
||||
};
|
||||
|
||||
export type SignInWithPassword = z.infer<typeof SignInWithPassword>
|
||||
|
||||
export const SignInPasswordlessSchema = SignInSchema.pick({
|
||||
email: true,
|
||||
})
|
||||
|
||||
export type ISignInPasswordlessSchema = z.infer<typeof SignInPasswordlessSchema>
|
||||
|
||||
// Default values for the form
|
||||
export const defaultSignInPasswordlessValues: SignInPasswordless = {
|
||||
export const defaulISignInPasswordlessSchemaValues: ISignInPasswordlessSchema = {
|
||||
email: "",
|
||||
}
|
||||
|
||||
export type SignInPasswordless = z.infer<typeof SignInPasswordlessSchema>
|
||||
|
||||
|
||||
// Define the sign-in response schema using Zod
|
||||
export const SignInResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
|
|
|
@ -5,32 +5,42 @@ export const SignUpSchema = z.object({
|
|||
.string()
|
||||
.min(1, { message: "Email is required" })
|
||||
.email({ message: "Please enter a valid email address" }),
|
||||
password: z.string().min(1, { message: "Password is required" }),
|
||||
password: z.string().min(1, { message: "Password is required" }).min(8, { message: "Password must be at least 8 characters" }),
|
||||
phone: z.string().optional(),
|
||||
})
|
||||
|
||||
export type SignUpFormData = z.infer<typeof SignUpSchema>;
|
||||
export type TSignUpSchema = z.infer<typeof SignUpSchema>;
|
||||
|
||||
export const SignUpWithEmail = SignUpSchema.pick({
|
||||
export const SignUpWithEmailSchema = SignUpSchema.pick({
|
||||
email: true,
|
||||
password: true,
|
||||
})
|
||||
|
||||
export const defaultSignUpWithEmailValues: SignUpWithEmail = {
|
||||
export type ISignUpWithEmailSchema = z.infer<typeof SignUpWithEmailSchema>
|
||||
|
||||
export const defaulISignUpWithEmailSchemaValues: ISignUpWithEmailSchema = {
|
||||
email: "",
|
||||
password: "",
|
||||
}
|
||||
|
||||
export type SignUpWithEmail = z.infer<typeof SignUpWithEmail>
|
||||
|
||||
export const SignUpWithPhone = SignUpSchema.pick({
|
||||
export const SignUpWithPhoneSchema = SignUpSchema.pick({
|
||||
phone: true,
|
||||
password: true,
|
||||
})
|
||||
|
||||
export const defaultSignUpWithPhoneValues: SignUpWithPhone = {
|
||||
export type ISignUpWithPhoneSchema = z.infer<typeof SignUpWithPhoneSchema>
|
||||
|
||||
export const defaulISignUpWithPhoneSchemaValues: ISignUpWithPhoneSchema = {
|
||||
phone: "",
|
||||
password: "",
|
||||
}
|
||||
|
||||
export type SignUpWithPhone = z.infer<typeof SignUpWithPhone>
|
||||
export const SignUpWithOtpSchema = SignUpSchema.pick({
|
||||
email: true,
|
||||
})
|
||||
|
||||
export type TSignUpWithOtpSchema = z.infer<typeof SignUpWithOtpSchema>
|
||||
|
||||
export const defaultSignUpWithOtpSchemaValues: TSignUpWithOtpSchema = {
|
||||
email: "",
|
||||
}
|
|
@ -5,9 +5,9 @@ export const verifyOtpSchema = z.object({
|
|||
token: z.string().length(6, { message: "OTP must be 6 characters long" }),
|
||||
});
|
||||
|
||||
export type VerifyOtpFormData = z.infer<typeof verifyOtpSchema>;
|
||||
export type IVerifyOtpSchema = z.infer<typeof verifyOtpSchema>;
|
||||
|
||||
export const defaultVerifyOtpValues: VerifyOtpFormData = {
|
||||
export const defaultVerifyOtpValues: IVerifyOtpSchema = {
|
||||
email: "",
|
||||
token: "",
|
||||
};
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
// Schema Zod untuk validasi runtime
|
||||
import { CRegex } from "@/app/_lib/const/regex";
|
||||
import { ValidBanDuration } from "@/app/_lib/types/ban-duration";
|
||||
import { z } from "zod";
|
||||
|
||||
export const BanDurationSchema = z.custom<ValidBanDuration>(
|
||||
(value) => typeof value === "string" && CRegex.BAN_DURATION_REGEX.test(value),
|
||||
{ message: "Invalid ban duration format." }
|
||||
);
|
||||
|
||||
// Tipe untuk digunakan di kode
|
||||
export type IBanDuration = z.infer<typeof BanDurationSchema>;
|
||||
|
||||
export const BanUserCredentialsSchema = z.object({
|
||||
id: z.string(),
|
||||
})
|
||||
|
||||
export type ICredentialsBanUserSchema = z.infer<typeof BanUserCredentialsSchema>
|
||||
|
||||
// Schema utama untuk user
|
||||
export const BanUserSchema = z.object({
|
||||
ban_duration: BanDurationSchema,
|
||||
});
|
||||
|
||||
export type IBanUserSchema = z.infer<typeof BanUserSchema>;
|
||||
|
||||
// Nilai default
|
||||
export const defaulIBanUserSchemaValues: IBanUserSchema = {
|
||||
ban_duration: "none",
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
import { CNumbers } from "@/app/_lib/const/number";
|
||||
import { CTexts } from "@/app/_lib/const/string";
|
||||
import { phonePrefixValidation, phoneRegexValidation } from "@/app/_utils/validation";
|
||||
import { z } from "zod";
|
||||
|
||||
export const CreateUserSchema = z.object({
|
||||
email: z.string().min(1, "Email is required").email(),
|
||||
password: z.string().min(1, { message: "Password is required" }).min(8, { message: "Password must be at least 8 characters" }),
|
||||
phone: z.string()
|
||||
.refine(phonePrefixValidation, {
|
||||
message: `Phone number must start with one of the following: ${CTexts.PHONE_PREFIX.join(', ')}.`,
|
||||
})
|
||||
.refine(phoneRegexValidation, {
|
||||
message: `Phone number must have a length between ${CNumbers.PHONE_MIN_LENGTH} and ${CNumbers.PHONE_MAX_LENGTH} digits without the country code.`,
|
||||
})
|
||||
.optional(),
|
||||
email_confirm: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type ICreateUserSchema = z.infer<typeof CreateUserSchema>;
|
||||
|
||||
export const defaulICreateUserSchemaValues: ICreateUserSchema = {
|
||||
email: "",
|
||||
password: "",
|
||||
phone: "",
|
||||
email_confirm: true,
|
||||
}
|
||||
|
||||
export const CredentialCreateUserSchema = CreateUserSchema.pick({
|
||||
email: true,
|
||||
})
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { z } from "zod"
|
||||
|
||||
export const DeleteUserSchema = z.object({
|
||||
id: z.string(),
|
||||
})
|
||||
|
||||
export type IDeleteUserSchema = z.infer<typeof DeleteUserSchema>
|
||||
|
||||
export const defaulIDeleteUserSchemaValues: IDeleteUserSchema = {
|
||||
id: "",
|
||||
}
|
||||
|
||||
export const DeleteUserCredentialsSchema = DeleteUserSchema.pick({ id: true })
|
||||
|
||||
export type ICredentialsDeleteUserSchema = z.infer<typeof DeleteUserCredentialsSchema>
|
|
@ -0,0 +1,15 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const InviteUserSchema = z.object({
|
||||
email: z.string().min(1, "Email is required").email(),
|
||||
});
|
||||
|
||||
export type IInviteUserSchema = z.infer<typeof InviteUserSchema>;
|
||||
|
||||
export const defaulIInviteUserSchemaValues: IInviteUserSchema = {
|
||||
email: "",
|
||||
}
|
||||
|
||||
export const InviteUserCredentialsSchema = InviteUserSchema.pick({ email: true })
|
||||
|
||||
export type ICredentialsInviteUserSchema = z.infer<typeof InviteUserCredentialsSchema>
|
|
@ -0,0 +1,61 @@
|
|||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Schema untuk mendapatkan user berdasarkan ID
|
||||
* @typedef {Object} IGetUserByIdSchema
|
||||
* @property {string} id - ID pengguna yang akan dicari
|
||||
*/
|
||||
export const GetUserByIdSchema = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
export type IGetUserByIdSchema = z.infer<typeof GetUserByIdSchema>;
|
||||
|
||||
export const defaulIGetUserByIdSchemaValues: IGetUserByIdSchema = {
|
||||
id: "",
|
||||
};
|
||||
|
||||
/**
|
||||
* Schema credential untuk mendapatkan user berdasarkan ID
|
||||
* Mengambil hanya properti 'id' dari GetUserByIdSchema
|
||||
*/
|
||||
export const ICredentialGetUserByIdSchema = GetUserByIdSchema.pick({ id: true });
|
||||
|
||||
export type ICredentialGetUserByIdSchema = z.infer<typeof ICredentialGetUserByIdSchema>;
|
||||
|
||||
export const GetUserByEmailSchema = z.object({
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Tipe inferensi dari GetUserByEmailSchema
|
||||
* @type {z.infer<typeof GetUserByEmailSchema>}
|
||||
*/
|
||||
export type IGetUserByEmailSchema = z.infer<typeof GetUserByEmailSchema>;
|
||||
|
||||
export const defaulIGetUserByEmailSchemaValues: IGetUserByEmailSchema = {
|
||||
email: "",
|
||||
};
|
||||
|
||||
export const ICredentialGetUserByEmailSchema = GetUserByEmailSchema.pick({ email: true });
|
||||
|
||||
export type ICredentialGetUserByEmailSchema = z.infer<typeof ICredentialGetUserByEmailSchema>;
|
||||
|
||||
/**
|
||||
* Schema untuk mendapatkan user berdasarkan username
|
||||
* @typedef {Object} IGetUserByUsernameSchema
|
||||
* @property {string} username - Nama pengguna yang akan dicari
|
||||
*/
|
||||
export const GetUserByUsernameSchema = z.object({
|
||||
username: z.string(),
|
||||
});
|
||||
|
||||
export type IGetUserByUsernameSchema = z.infer<typeof GetUserByUsernameSchema>;
|
||||
|
||||
export const defaulIGetUserByUsernameSchemaValues: IGetUserByUsernameSchema = {
|
||||
username: "",
|
||||
};
|
||||
|
||||
export const ICredentialGetUserByUsernameSchema = GetUserByUsernameSchema.pick({ username: true });
|
||||
|
||||
export type ICredentialGetUserByUsernameSchema = z.infer<typeof ICredentialGetUserByUsernameSchema>;
|
|
@ -0,0 +1,15 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const UnbanUserSchema = z.object({
|
||||
id: z.string(),
|
||||
})
|
||||
|
||||
export type IUnbanUserSchema = z.infer<typeof UnbanUserSchema>;
|
||||
|
||||
export const defaulIUnbanUserSchemaValues: IUnbanUserSchema = {
|
||||
id: "",
|
||||
}
|
||||
|
||||
export const UnbanUserCredentialsSchema = UnbanUserSchema.pick({ id: true })
|
||||
|
||||
export type ICredentialsUnbanUserSchema = z.infer<typeof UnbanUserCredentialsSchema>
|
|
@ -0,0 +1,63 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const UpdateUserSchema = z.object({
|
||||
email: z.string().email().optional(),
|
||||
email_confirmed_at: z.boolean().optional(),
|
||||
encrypted_password: z.string().optional(),
|
||||
role: z.enum(["user", "staff", "admin"]).optional(),
|
||||
phone: z.string().optional(),
|
||||
phone_confirmed_at: z.boolean().optional(),
|
||||
invited_at: z.union([z.string(), z.date()]).optional(),
|
||||
confirmed_at: z.union([z.string(), z.date()]).optional(),
|
||||
// recovery_sent_at: z.union([z.string(), z.date()]).optional(),
|
||||
last_sign_in_at: z.union([z.string(), z.date()]).optional(),
|
||||
created_at: z.union([z.string(), z.date()]).optional(),
|
||||
updated_at: z.union([z.string(), z.date()]).optional(),
|
||||
is_anonymous: z.boolean().optional(),
|
||||
user_metadata: z.record(z.any()).optional(),
|
||||
app_metadata: z.record(z.any()).optional(),
|
||||
profile: z
|
||||
.object({
|
||||
avatar: z.string().optional(),
|
||||
username: z.string().optional(),
|
||||
first_name: z.string().optional(),
|
||||
last_name: z.string().optional(),
|
||||
bio: z.string().optional(),
|
||||
address: z.any().optional(),
|
||||
birth_date: z.date().optional(),
|
||||
})
|
||||
});
|
||||
|
||||
export type IUpdateUserSchema = z.infer<typeof UpdateUserSchema>;
|
||||
|
||||
export const defaulIUpdateUserSchemaValues: IUpdateUserSchema = {
|
||||
email: "",
|
||||
email_confirmed_at: false,
|
||||
encrypted_password: "",
|
||||
role: "user",
|
||||
phone: "",
|
||||
phone_confirmed_at: false,
|
||||
invited_at: "",
|
||||
confirmed_at: "",
|
||||
last_sign_in_at: "",
|
||||
created_at: "",
|
||||
updated_at: "",
|
||||
is_anonymous: false,
|
||||
user_metadata: {},
|
||||
app_metadata: {},
|
||||
profile: {
|
||||
avatar: "",
|
||||
username: "",
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
bio: "",
|
||||
address: "",
|
||||
birth_date: new Date(),
|
||||
}
|
||||
}
|
||||
|
||||
export const CredentialUpdateUserSchema = z.object({
|
||||
id: z.string(),
|
||||
})
|
||||
|
||||
export type ICredentialUpdateUserSchema = z.infer<typeof CredentialUpdateUserSchema>
|
|
@ -66,7 +66,7 @@ export const UserSchema = z.object({
|
|||
.optional(),
|
||||
});
|
||||
|
||||
export type User = z.infer<typeof UserSchema>;
|
||||
export type IUserSchema = z.infer<typeof UserSchema>;
|
||||
|
||||
export const ProfileSchema = z.object({
|
||||
id: z.string(),
|
||||
|
@ -80,59 +80,30 @@ export const ProfileSchema = z.object({
|
|||
birth_date: z.string().optional(),
|
||||
});
|
||||
|
||||
export type Profile = z.infer<typeof ProfileSchema>;
|
||||
export type IProfileSchema = z.infer<typeof ProfileSchema>;
|
||||
|
||||
export const CreateUserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
phone: z.string().optional(),
|
||||
email_confirm: z.boolean().optional(),
|
||||
});
|
||||
// export type UserFilterOptions = {
|
||||
// email: string
|
||||
// phone: string
|
||||
// lastSignIn: string
|
||||
// createdAt: string
|
||||
// status: string[]
|
||||
// }
|
||||
|
||||
export type CreateUser = z.infer<typeof CreateUserSchema>;
|
||||
export const UserFilterOptionsSchema = z.object({
|
||||
email: z.string(),
|
||||
phone: z.string(),
|
||||
lastSignIn: z.string(),
|
||||
createdAt: z.string(),
|
||||
status: z.array(z.string()),
|
||||
})
|
||||
|
||||
export const UpdateUserSchema = z.object({
|
||||
email: z.string().email().optional(),
|
||||
email_confirmed_at: z.boolean().optional(),
|
||||
encrypted_password: z.string().optional(),
|
||||
role: z.enum(["user", "staff", "admin"]).optional(),
|
||||
phone: z.string().optional(),
|
||||
phone_confirmed_at: z.boolean().optional(),
|
||||
invited_at: z.union([z.string(), z.date()]).optional(),
|
||||
confirmed_at: z.union([z.string(), z.date()]).optional(),
|
||||
// recovery_sent_at: z.union([z.string(), z.date()]).optional(),
|
||||
last_sign_in_at: z.union([z.string(), z.date()]).optional(),
|
||||
created_at: z.union([z.string(), z.date()]).optional(),
|
||||
updated_at: z.union([z.string(), z.date()]).optional(),
|
||||
is_anonymous: z.boolean().optional(),
|
||||
user_metadata: z.record(z.any()).optional(),
|
||||
app_metadata: z.record(z.any()).optional(),
|
||||
profile: z
|
||||
.object({
|
||||
id: z.string().optional(),
|
||||
user_id: z.string(),
|
||||
avatar: z.string().optional(),
|
||||
username: z.string().optional(),
|
||||
first_name: z.string().optional(),
|
||||
last_name: z.string().optional(),
|
||||
bio: z.string().optional(),
|
||||
address: z.any().optional(),
|
||||
birth_date: z.date().optional(),
|
||||
})
|
||||
});
|
||||
|
||||
export type UpdateUser = z.infer<typeof UpdateUserSchema>;
|
||||
|
||||
export const InviteUserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
export type InviteUser = z.infer<typeof InviteUserSchema>;
|
||||
export type IUserFilterOptionsSchema = z.infer<typeof UserFilterOptionsSchema>;
|
||||
|
||||
export type UserResponse =
|
||||
| {
|
||||
data: {
|
||||
user: User;
|
||||
user: IUserSchema;
|
||||
};
|
||||
error: null;
|
||||
}
|
||||
|
|
|
@ -3,11 +3,18 @@ import { ICrashReporterService } from "@/src/application/services/crash-reporter
|
|||
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
|
||||
import { createAdminClient } from "@/app/_utils/supabase/admin";
|
||||
import { createClient as createServerClient } from "@/app/_utils/supabase/server";
|
||||
import { CreateUser, UpdateUser, User, UserResponse } from "@/src/entities/models/users/users.model";
|
||||
import { IUserSchema } from "@/src/entities/models/users/users.model";
|
||||
import { ITransaction } from "@/src/entities/models/transaction.interface";
|
||||
import db from "@/prisma/db";
|
||||
import { DatabaseOperationError, NotFoundError } from "@/src/entities/errors/common";
|
||||
import { AuthenticationError } from "@/src/entities/errors/auth";
|
||||
import { ICredentialGetUserByEmailSchema, ICredentialGetUserByUsernameSchema, IGetUserByIdSchema } from "@/src/entities/models/users/read-user.model";
|
||||
import { ICredentialsInviteUserSchema } from "@/src/entities/models/users/invite-user.model";
|
||||
import { ICredentialUpdateUserSchema, IUpdateUserSchema } from "@/src/entities/models/users/update-user.model";
|
||||
import { ICredentialsDeleteUserSchema } from "@/src/entities/models/users/delete-user.model";
|
||||
import { IBanUserSchema, ICredentialsBanUserSchema } from "@/src/entities/models/users/ban-user.model";
|
||||
import { ICredentialsUnbanUserSchema } from "@/src/entities/models/users/unban-user.model";
|
||||
import { ICreateUserSchema } from "@/src/entities/models/users/create-user.model";
|
||||
|
||||
export class UsersRepository implements IUsersRepository {
|
||||
constructor(
|
||||
|
@ -17,7 +24,7 @@ export class UsersRepository implements IUsersRepository {
|
|||
private readonly supabaseServer = createServerClient()
|
||||
) { }
|
||||
|
||||
async getUsers(): Promise<User[]> {
|
||||
async getUsers(): Promise<IUserSchema[]> {
|
||||
return await this.instrumentationService.startSpan({
|
||||
name: "UsersRepository > getUsers",
|
||||
}, async () => {
|
||||
|
@ -40,7 +47,7 @@ export class UsersRepository implements IUsersRepository {
|
|||
)
|
||||
|
||||
if (!users) {
|
||||
throw new NotFoundError("Users not found");
|
||||
return [];
|
||||
}
|
||||
|
||||
return users;
|
||||
|
@ -51,14 +58,14 @@ export class UsersRepository implements IUsersRepository {
|
|||
})
|
||||
}
|
||||
|
||||
async getUserById(id: string): Promise<User | undefined> {
|
||||
async getUserById(credential: IGetUserByIdSchema): Promise<IUserSchema | undefined> {
|
||||
return await this.instrumentationService.startSpan({
|
||||
name: "UsersRepository > getUserById",
|
||||
}, async () => {
|
||||
try {
|
||||
const query = db.users.findUnique({
|
||||
where: {
|
||||
id,
|
||||
id: credential.id,
|
||||
},
|
||||
include: {
|
||||
profile: true,
|
||||
|
@ -66,7 +73,7 @@ export class UsersRepository implements IUsersRepository {
|
|||
})
|
||||
|
||||
const user = await this.instrumentationService.startSpan({
|
||||
name: `UsersRepository > getUserById > Prisma: db.users.findUnique(${id})`,
|
||||
name: `UsersRepository > getUserById > Prisma: db.users.findUnique(${credential.id})`,
|
||||
op: "db:query",
|
||||
attributes: { "system": "prisma" },
|
||||
},
|
||||
|
@ -81,7 +88,7 @@ export class UsersRepository implements IUsersRepository {
|
|||
|
||||
return {
|
||||
...user,
|
||||
id,
|
||||
id: credential.id,
|
||||
};
|
||||
} catch (err) {
|
||||
this.crashReporterService.report(err);
|
||||
|
@ -90,7 +97,7 @@ export class UsersRepository implements IUsersRepository {
|
|||
})
|
||||
}
|
||||
|
||||
async getUserByUsername(username: string): Promise<User | undefined> {
|
||||
async getUserByUsername(credential: ICredentialGetUserByUsernameSchema): Promise<IUserSchema | undefined> {
|
||||
return await this.instrumentationService.startSpan({
|
||||
name: "UsersRepository > getUserByUsername",
|
||||
}, async () => {
|
||||
|
@ -98,7 +105,7 @@ export class UsersRepository implements IUsersRepository {
|
|||
const query = db.users.findFirst({
|
||||
where: {
|
||||
profile: {
|
||||
username,
|
||||
username: credential.username,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
|
@ -107,7 +114,7 @@ export class UsersRepository implements IUsersRepository {
|
|||
})
|
||||
|
||||
const user = await this.instrumentationService.startSpan({
|
||||
name: `UsersRepository > getUserByUsername > Prisma: db.users.findFirst(${username})`,
|
||||
name: `UsersRepository > getUserByUsername > Prisma: db.users.findFirst(${credential.username})`,
|
||||
op: "db:query",
|
||||
attributes: { "system": "prisma" },
|
||||
},
|
||||
|
@ -130,7 +137,7 @@ export class UsersRepository implements IUsersRepository {
|
|||
})
|
||||
}
|
||||
|
||||
async getUserByEmail(email: string): Promise<User | undefined> {
|
||||
async getUserByEmail(credential: ICredentialGetUserByEmailSchema): Promise<IUserSchema | undefined> {
|
||||
return await this.instrumentationService.startSpan({
|
||||
name: "UsersRepository > getUserByEmail",
|
||||
}, async () => {
|
||||
|
@ -138,7 +145,7 @@ export class UsersRepository implements IUsersRepository {
|
|||
|
||||
const query = db.users.findUnique({
|
||||
where: {
|
||||
email,
|
||||
email: credential.email,
|
||||
},
|
||||
include: {
|
||||
profile: true,
|
||||
|
@ -146,7 +153,7 @@ export class UsersRepository implements IUsersRepository {
|
|||
})
|
||||
|
||||
const user = await this.instrumentationService.startSpan({
|
||||
name: `UsersRepository > getUserByEmail > Prisma: db.users.findUnique(${email})`,
|
||||
name: `UsersRepository > getUserByEmail > Prisma: db.users.findUnique(${credential.email})`,
|
||||
op: "db:query",
|
||||
attributes: { "system": "prisma" },
|
||||
},
|
||||
|
@ -156,7 +163,7 @@ export class UsersRepository implements IUsersRepository {
|
|||
)
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundError("User not found");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return user;
|
||||
|
@ -167,7 +174,7 @@ export class UsersRepository implements IUsersRepository {
|
|||
})
|
||||
}
|
||||
|
||||
async getCurrentUser(): Promise<User> {
|
||||
async getCurrentUser(): Promise<IUserSchema> {
|
||||
return await this.instrumentationService.startSpan({
|
||||
name: "UsersRepository > getCurrentUser",
|
||||
}, async () => {
|
||||
|
@ -176,7 +183,7 @@ export class UsersRepository implements IUsersRepository {
|
|||
|
||||
const query = supabase.auth.getUser();
|
||||
|
||||
const { data, error } = await this.instrumentationService.startSpan({
|
||||
const { data: { user }, error } = await this.instrumentationService.startSpan({
|
||||
name: "UsersRepository > getCurrentUser > supabase.auth.getUser",
|
||||
op: "db:query",
|
||||
attributes: { "system": "supabase.auth" },
|
||||
|
@ -190,14 +197,11 @@ export class UsersRepository implements IUsersRepository {
|
|||
throw new AuthenticationError("Failed to get current user");
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
if (!user) {
|
||||
throw new NotFoundError("User not found");
|
||||
}
|
||||
|
||||
return {
|
||||
...data,
|
||||
id: data.user.id,
|
||||
};
|
||||
return user;
|
||||
} catch (err) {
|
||||
this.crashReporterService.report(err);
|
||||
throw err;
|
||||
|
@ -205,14 +209,21 @@ export class UsersRepository implements IUsersRepository {
|
|||
})
|
||||
}
|
||||
|
||||
async createUser(input: CreateUser, tx?: ITransaction): Promise<User> {
|
||||
async createUser(input: ICreateUserSchema, tx?: ITransaction): Promise<IUserSchema> {
|
||||
return await this.instrumentationService.startSpan({
|
||||
name: "UsersRepository > createUser",
|
||||
}, async () => {
|
||||
try {
|
||||
|
||||
console.log("Create User");
|
||||
|
||||
const supabase = this.supabaseAdmin;
|
||||
|
||||
const query = supabase.auth.admin.createUser(input)
|
||||
const query = supabase.auth.admin.createUser({
|
||||
email: input.email,
|
||||
password: input.password,
|
||||
email_confirm: true,
|
||||
})
|
||||
|
||||
const { data: { user } } = await this.instrumentationService.startSpan({
|
||||
name: "UsersRepository > createUser > supabase.auth.admin.createUser",
|
||||
|
@ -237,14 +248,14 @@ export class UsersRepository implements IUsersRepository {
|
|||
})
|
||||
}
|
||||
|
||||
async inviteUser(email: string, tx?: ITransaction): Promise<User> {
|
||||
async inviteUser(credential: ICredentialsInviteUserSchema, tx?: ITransaction): Promise<IUserSchema> {
|
||||
return await this.instrumentationService.startSpan({
|
||||
name: "UsersRepository > inviteUser",
|
||||
}, async () => {
|
||||
try {
|
||||
const supabase = this.supabaseAdmin;
|
||||
|
||||
const query = supabase.auth.admin.inviteUserByEmail(email);
|
||||
const query = supabase.auth.admin.inviteUserByEmail(credential.email);
|
||||
|
||||
const { data: { user } } = await this.instrumentationService.startSpan({
|
||||
name: "UsersRepository > inviteUser > supabase.auth.admin.inviteUserByEmail",
|
||||
|
@ -268,14 +279,14 @@ export class UsersRepository implements IUsersRepository {
|
|||
})
|
||||
}
|
||||
|
||||
async updateUser(id: string, input: Partial<UpdateUser>, tx?: ITransaction): Promise<User> {
|
||||
async updateUser(credential: ICredentialUpdateUserSchema, input: Partial<IUpdateUserSchema>, tx?: ITransaction): Promise<IUserSchema> {
|
||||
return await this.instrumentationService.startSpan({
|
||||
name: "UsersRepository > updateUser",
|
||||
}, async () => {
|
||||
try {
|
||||
const supabase = this.supabaseAdmin;
|
||||
|
||||
const queryUpdateSupabaseUser = supabase.auth.admin.updateUserById(id, {
|
||||
const queryUpdateSupabaseUser = supabase.auth.admin.updateUserById(credential.id, {
|
||||
email: input.email,
|
||||
email_confirm: input.email_confirmed_at,
|
||||
password: input.encrypted_password ?? undefined,
|
||||
|
@ -303,7 +314,7 @@ export class UsersRepository implements IUsersRepository {
|
|||
|
||||
const queryGetUser = db.users.findUnique({
|
||||
where: {
|
||||
id,
|
||||
id: credential.id,
|
||||
},
|
||||
include: {
|
||||
profile: true,
|
||||
|
@ -326,7 +337,7 @@ export class UsersRepository implements IUsersRepository {
|
|||
|
||||
const queryUpdateUser = db.users.update({
|
||||
where: {
|
||||
id,
|
||||
id: credential.id,
|
||||
},
|
||||
data: {
|
||||
role: input.role || user.role,
|
||||
|
@ -369,7 +380,7 @@ export class UsersRepository implements IUsersRepository {
|
|||
|
||||
return {
|
||||
...updatedUser,
|
||||
id,
|
||||
id: credential.id,
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
|
@ -379,14 +390,14 @@ export class UsersRepository implements IUsersRepository {
|
|||
})
|
||||
}
|
||||
|
||||
async deleteUser(id: string, tx?: ITransaction): Promise<User> {
|
||||
async deleteUser(credential: ICredentialsDeleteUserSchema, tx?: ITransaction): Promise<IUserSchema> {
|
||||
return await this.instrumentationService.startSpan({
|
||||
name: "UsersRepository > deleteUser",
|
||||
}, async () => {
|
||||
try {
|
||||
const supabase = this.supabaseAdmin;
|
||||
|
||||
const query = supabase.auth.admin.deleteUser(id);
|
||||
const query = supabase.auth.admin.deleteUser(credential.id);
|
||||
|
||||
const { data: user, error } = await this.instrumentationService.startSpan({
|
||||
name: "UsersRepository > deleteUser > supabase.auth.admin.deleteUser",
|
||||
|
@ -404,7 +415,7 @@ export class UsersRepository implements IUsersRepository {
|
|||
|
||||
return {
|
||||
...user,
|
||||
id
|
||||
id: credential.id,
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
|
@ -414,15 +425,15 @@ export class UsersRepository implements IUsersRepository {
|
|||
})
|
||||
}
|
||||
|
||||
async banUser(id: string, ban_duration: string, tx?: ITransaction): Promise<User> {
|
||||
async banUser(credential: ICredentialsBanUserSchema, input: IBanUserSchema, tx?: ITransaction): Promise<IUserSchema> {
|
||||
return await this.instrumentationService.startSpan({
|
||||
name: "UsersRepository > banUser",
|
||||
}, async () => {
|
||||
try {
|
||||
const supabase = this.supabaseAdmin;
|
||||
|
||||
const query = supabase.auth.admin.updateUserById(id, {
|
||||
ban_duration: ban_duration ?? "100h",
|
||||
const query = supabase.auth.admin.updateUserById(credential.id, {
|
||||
ban_duration: input.ban_duration ?? "24h",
|
||||
})
|
||||
|
||||
const { data: user, error } = await this.instrumentationService.startSpan({
|
||||
|
@ -441,7 +452,7 @@ export class UsersRepository implements IUsersRepository {
|
|||
|
||||
return {
|
||||
...user,
|
||||
id
|
||||
id: credential.id,
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
|
@ -452,14 +463,14 @@ export class UsersRepository implements IUsersRepository {
|
|||
}
|
||||
|
||||
|
||||
async unbanUser(id: string, tx?: ITransaction): Promise<User> {
|
||||
async unbanUser(credential: ICredentialsUnbanUserSchema, tx?: ITransaction): Promise<IUserSchema> {
|
||||
return await this.instrumentationService.startSpan({
|
||||
name: "UsersRepository > unbanUser",
|
||||
}, async () => {
|
||||
try {
|
||||
const supabase = this.supabaseAdmin;
|
||||
|
||||
const query = supabase.auth.admin.updateUserById(id, {
|
||||
const query = supabase.auth.admin.updateUserById(credential.id, {
|
||||
ban_duration: "none",
|
||||
})
|
||||
|
||||
|
@ -479,7 +490,7 @@ export class UsersRepository implements IUsersRepository {
|
|||
|
||||
return {
|
||||
...user,
|
||||
id
|
||||
id: credential.id,
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
|
|
|
@ -6,11 +6,13 @@ import { IAuthenticationService } from "@/src/application/services/authenticatio
|
|||
import { ICrashReporterService } from "@/src/application/services/crash-reporter.service.interface";
|
||||
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
|
||||
import { AuthenticationError } from "@/src/entities/errors/auth";
|
||||
import { ISendMagicLinkSchema } from "@/src/entities/models/auth/send-magic-link.model";
|
||||
import { ISendPasswordRecoverySchema } from "@/src/entities/models/auth/send-password-recovery.model";
|
||||
import { Session } from "@/src/entities/models/auth/session.model";
|
||||
import { SignInPasswordless, SignInWithPassword } from "@/src/entities/models/auth/sign-in.model";
|
||||
import { SignUpWithEmail, SignUpWithPhone } from "@/src/entities/models/auth/sign-up.model";
|
||||
import { VerifyOtpFormData } from "@/src/entities/models/auth/verify-otp.model";
|
||||
import { User } from "@/src/entities/models/users/users.model";
|
||||
import { SignInWithPasswordSchema, ISignInPasswordlessSchema, ISignInWithPasswordSchema } from "@/src/entities/models/auth/sign-in.model";
|
||||
import { SignUpWithEmailSchema, SignUpWithPhoneSchema, ISignUpWithEmailSchema, ISignUpWithPhoneSchema } from "@/src/entities/models/auth/sign-up.model";
|
||||
import { IVerifyOtpSchema } from "@/src/entities/models/auth/verify-otp.model";
|
||||
import { IUserSchema } from "@/src/entities/models/users/users.model";
|
||||
|
||||
export class AuthenticationService implements IAuthenticationService {
|
||||
constructor(
|
||||
|
@ -21,7 +23,7 @@ export class AuthenticationService implements IAuthenticationService {
|
|||
private readonly supabaseServer = createClient()
|
||||
) { }
|
||||
|
||||
async signInPasswordless(credentials: SignInPasswordless): Promise<void> {
|
||||
async signInPasswordless(credentials: ISignInPasswordlessSchema): Promise<void> {
|
||||
return await this.instrumentationService.startSpan({
|
||||
name: "signInPasswordless Use Case",
|
||||
}, async () => {
|
||||
|
@ -49,9 +51,9 @@ export class AuthenticationService implements IAuthenticationService {
|
|||
})
|
||||
}
|
||||
|
||||
async signInWithPassword(credentials: SignInWithPassword): Promise<void> {
|
||||
async SignInWithPasswordSchema(credentials: ISignInWithPasswordSchema): Promise<void> {
|
||||
return await this.instrumentationService.startSpan({
|
||||
name: "signInWithPassword Use Case",
|
||||
name: "SignInWithPasswordSchema Use Case",
|
||||
}, async () => {
|
||||
try {
|
||||
const supabase = await this.supabaseServer
|
||||
|
@ -77,9 +79,9 @@ export class AuthenticationService implements IAuthenticationService {
|
|||
})
|
||||
}
|
||||
|
||||
async signUpWithEmail(credentials: SignUpWithEmail): Promise<User> {
|
||||
async SignUpWithEmailSchema(credentials: ISignUpWithEmailSchema): Promise<IUserSchema> {
|
||||
return await this.instrumentationService.startSpan({
|
||||
name: "signUpWithEmail Use Case",
|
||||
name: "SignUpWithEmailSchema Use Case",
|
||||
}, async () => {
|
||||
try {
|
||||
const supabase = await this.supabaseServer
|
||||
|
@ -122,7 +124,7 @@ export class AuthenticationService implements IAuthenticationService {
|
|||
})
|
||||
}
|
||||
|
||||
async signUpWithPhone(credentials: SignUpWithPhone): Promise<User> {
|
||||
async SignUpWithPhoneSchema(credentials: ISignUpWithPhoneSchema): Promise<IUserSchema> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
|
@ -190,14 +192,14 @@ export class AuthenticationService implements IAuthenticationService {
|
|||
})
|
||||
}
|
||||
|
||||
async sendMagicLink(email: string): Promise<void> {
|
||||
async sendMagicLink(credentials: ISendMagicLinkSchema): Promise<void> {
|
||||
return await this.instrumentationService.startSpan({
|
||||
name: "sendMagicLink Use Case",
|
||||
}, async () => {
|
||||
try {
|
||||
const supabase = await this.supabaseServer
|
||||
|
||||
const magicLink = supabase.auth.signInWithOtp({ email })
|
||||
const magicLink = supabase.auth.signInWithOtp({ email: credentials.email })
|
||||
|
||||
await this.instrumentationService.startSpan({
|
||||
name: "supabase.auth.signIn",
|
||||
|
@ -216,14 +218,14 @@ export class AuthenticationService implements IAuthenticationService {
|
|||
})
|
||||
}
|
||||
|
||||
async sendPasswordRecovery(email: string): Promise<void> {
|
||||
async sendPasswordRecovery(credentials: ISendPasswordRecoverySchema): Promise<void> {
|
||||
return await this.instrumentationService.startSpan({
|
||||
name: "sendPasswordRecovery Use Case",
|
||||
}, async () => {
|
||||
try {
|
||||
const supabase = await this.supabaseServer
|
||||
|
||||
const passwordRecovery = supabase.auth.resetPasswordForEmail(email, {
|
||||
const passwordRecovery = supabase.auth.resetPasswordForEmail(credentials.email, {
|
||||
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/reset-password`,
|
||||
})
|
||||
|
||||
|
@ -244,7 +246,7 @@ export class AuthenticationService implements IAuthenticationService {
|
|||
})
|
||||
}
|
||||
|
||||
async verifyOtp(credentials: VerifyOtpFormData): Promise<void> {
|
||||
async verifyOtp(credentials: IVerifyOtpSchema): Promise<void> {
|
||||
return await this.instrumentationService.startSpan({
|
||||
name: "verifyOtp Use Case",
|
||||
}, async () => {
|
||||
|
|
|
@ -1,123 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import { SignInFormData } from '@/src/entities/models/auth/sign-in.model';
|
||||
import { VerifyOtpFormData } from '@/src/entities/models/auth/verify-otp.model';
|
||||
import { useNavigations } from '@/app/_hooks/use-navigations';
|
||||
import { AuthenticationError } from '@/src/entities/errors/auth';
|
||||
import * as authRepository from '@/src/application/repositories/authentication.repository';
|
||||
|
||||
export function useAuthActions() {
|
||||
const { router } = useNavigations();
|
||||
|
||||
// Sign In Mutation
|
||||
const signInMutation = useMutation({
|
||||
mutationFn: async (data: SignInFormData) => {
|
||||
return await authRepository.signIn(data);
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
toast.success(result.message);
|
||||
if (result.redirectTo && result.success) {
|
||||
router.push(result.redirectTo);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error instanceof AuthenticationError) {
|
||||
toast.error(`Authentication failed: ${error.message}`);
|
||||
} else {
|
||||
toast.error('Failed to sign in. Please try again later.');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Verify OTP Mutation
|
||||
const verifyOtpMutation = useMutation({
|
||||
mutationFn: async (data: VerifyOtpFormData) => {
|
||||
return await authRepository.verifyOtp(data);
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
toast.success(result.message);
|
||||
if (result.redirectTo) {
|
||||
router.push(result.redirectTo);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error instanceof AuthenticationError) {
|
||||
toast.error(`Verification failed: ${error.message}`);
|
||||
} else {
|
||||
toast.error('Failed to verify OTP. Please try again.');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Sign Out Mutation
|
||||
const signOutMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return await authRepository.signOut();
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
toast.success(result.message);
|
||||
if (result.redirectTo) {
|
||||
router.push(result.redirectTo);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error('Failed to sign out. Please try again.');
|
||||
}
|
||||
});
|
||||
|
||||
// Password Recovery Mutation
|
||||
const passwordRecoveryMutation = useMutation({
|
||||
mutationFn: async (email: string) => {
|
||||
return await authRepository.sendPasswordRecovery(email);
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
toast.success(result.message);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error('Failed to send password recovery email. Please try again.');
|
||||
}
|
||||
});
|
||||
|
||||
// Magic Link Mutation
|
||||
const magicLinkMutation = useMutation({
|
||||
mutationFn: async (email: string) => {
|
||||
return await authRepository.sendMagicLink(email);
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
toast.success(result.message);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error('Failed to send magic link email. Please try again.');
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
signIn: {
|
||||
mutate: signInMutation.mutateAsync,
|
||||
isPending: signInMutation.isPending,
|
||||
error: signInMutation.error,
|
||||
},
|
||||
verifyOtp: {
|
||||
mutate: verifyOtpMutation.mutateAsync,
|
||||
isPending: verifyOtpMutation.isPending,
|
||||
error: verifyOtpMutation.error,
|
||||
},
|
||||
signOut: {
|
||||
mutate: signOutMutation.mutateAsync,
|
||||
isPending: signOutMutation.isPending,
|
||||
error: signOutMutation.error,
|
||||
},
|
||||
passwordRecovery: {
|
||||
mutate: passwordRecoveryMutation.mutateAsync,
|
||||
isPending: passwordRecoveryMutation.isPending,
|
||||
error: passwordRecoveryMutation.error,
|
||||
},
|
||||
magicLink: {
|
||||
mutate: magicLinkMutation.mutateAsync,
|
||||
isPending: magicLinkMutation.isPending,
|
||||
error: magicLinkMutation.error,
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
|
||||
import { ISendMagicLinkUseCase } from "@/src/application/use-cases/auth/send-magic-link.use-case"
|
||||
|
||||
import { z } from "zod"
|
||||
|
||||
import { InputParseError } from "@/src/entities/errors/common"
|
||||
|
||||
const sendMagicLinkInputSchema = z.object({
|
||||
email: z.string().email("Please enter a valid email address"),
|
||||
})
|
||||
|
||||
export type ISendMagicLinkController = ReturnType<typeof sendMagicLinkController>
|
||||
|
||||
export const sendMagicLinkController =
|
||||
(
|
||||
instrumentationService: IInstrumentationService,
|
||||
sendMagicLinkUseCase: ISendMagicLinkUseCase
|
||||
) =>
|
||||
async (input: Partial<z.infer<typeof sendMagicLinkInputSchema>>) => {
|
||||
return await instrumentationService.startSpan({ name: "sendMagicLink Controller" },
|
||||
async () => {
|
||||
const { data, error: inputParseError } = sendMagicLinkInputSchema.safeParse(input)
|
||||
|
||||
if (inputParseError) {
|
||||
throw new InputParseError("Invalid data", { cause: inputParseError })
|
||||
}
|
||||
|
||||
return await sendMagicLinkUseCase({
|
||||
email: data.email
|
||||
})
|
||||
})
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
|
||||
import { ISendPasswordRecoveryUseCase } from "@/src/application/use-cases/auth/send-password-recovery.use-case"
|
||||
import { InputParseError } from "@/src/entities/errors/common"
|
||||
import { z } from "zod"
|
||||
|
||||
const sendPasswordRecoveryInputSchema = z.object({
|
||||
email: z.string().email("Please enter a valid email address"),
|
||||
})
|
||||
|
||||
export type ISendPasswordRecoveryController = ReturnType<typeof sendPasswordRecoveryController>
|
||||
|
||||
export const sendPasswordRecoveryController =
|
||||
(
|
||||
instrumentationService: IInstrumentationService,
|
||||
sendPasswordRecoveryUseCase: ISendPasswordRecoveryUseCase
|
||||
) =>
|
||||
async (input: Partial<z.infer<typeof sendPasswordRecoveryInputSchema>>) => {
|
||||
return await instrumentationService.startSpan({ name: "sendPasswordRecovery Controller" },
|
||||
async () => {
|
||||
const { data, error: inputParseError } = sendPasswordRecoveryInputSchema.safeParse(input)
|
||||
|
||||
if (inputParseError) {
|
||||
throw new InputParseError("Invalid data", { cause: inputParseError })
|
||||
}
|
||||
|
||||
return await sendPasswordRecoveryUseCase({
|
||||
email: data.email
|
||||
})
|
||||
})
|
||||
}
|
|
@ -5,7 +5,7 @@ import { InputParseError } from "@/src/entities/errors/common";
|
|||
|
||||
// Sign In Controller
|
||||
const signInInputSchema = z.object({
|
||||
email: z.string().email("Please enter a valid email address"),
|
||||
email: z.string().min(1, "Email is Required").email("Please enter a valid email address"),
|
||||
})
|
||||
|
||||
export type ISignInController = ReturnType<typeof signInController>
|
||||
|
@ -20,7 +20,7 @@ export const signInController =
|
|||
const { data, error: inputParseError } = signInInputSchema.safeParse(input)
|
||||
|
||||
if (inputParseError) {
|
||||
throw new InputParseError("Invalid data", { cause: inputParseError })
|
||||
throw new InputParseError(inputParseError.errors[0].message)
|
||||
}
|
||||
|
||||
return await signInUseCase({
|
||||
|
|
|
@ -1,27 +1,38 @@
|
|||
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
|
||||
import { IBanUserUseCase } from "@/src/application/use-cases/users/ban-user.use-case";
|
||||
import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case";
|
||||
import { InputParseError } from "@/src/entities/errors/common";
|
||||
import { BanDurationSchema, ICredentialsBanUserSchema } from "@/src/entities/models/users/ban-user.model";
|
||||
import { z } from "zod";
|
||||
|
||||
const inputSchema = z.object({
|
||||
id: z.string(),
|
||||
ban_duration: z.string()
|
||||
ban_duration: BanDurationSchema
|
||||
})
|
||||
|
||||
export type IBanUserController = ReturnType<typeof banUserController>
|
||||
|
||||
export const banUserController = (
|
||||
instrumentationService: IInstrumentationService,
|
||||
banUserUseCase: IBanUserUseCase
|
||||
banUserUseCase: IBanUserUseCase,
|
||||
getCurrentUserUseCase: IGetCurrentUserUseCase
|
||||
) =>
|
||||
async (input: Partial<z.infer<typeof inputSchema>>) => {
|
||||
async (credential: ICredentialsBanUserSchema, input: Partial<z.infer<typeof inputSchema>>) => {
|
||||
return await instrumentationService.startSpan({ name: "banUser Controller" }, async () => {
|
||||
|
||||
const session = await getCurrentUserUseCase();
|
||||
|
||||
if (!session) {
|
||||
throw new InputParseError("Must be logged in to ban a user");
|
||||
}
|
||||
|
||||
const { data, error: inputParseError } = inputSchema.safeParse(input);
|
||||
|
||||
if (inputParseError) {
|
||||
throw new InputParseError("Invalid data", { cause: inputParseError });
|
||||
}
|
||||
|
||||
return await banUserUseCase(data.id, data.ban_duration);
|
||||
console.log("Controller: Ban User");
|
||||
|
||||
return await banUserUseCase({ id: credential.id }, { ban_duration: data.ban_duration });
|
||||
})
|
||||
}
|
|
@ -2,11 +2,24 @@ import { IUsersRepository } from "@/src/application/repositories/users.repositor
|
|||
import { IAuthenticationService } from "@/src/application/services/authentication.service.interface"
|
||||
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
|
||||
import { ICreateUserUseCase } from "@/src/application/use-cases/users/create-user.use-case"
|
||||
import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case"
|
||||
import { UnauthenticatedError } from "@/src/entities/errors/auth"
|
||||
import { InputParseError } from "@/src/entities/errors/common"
|
||||
import { CreateUserSchema } from "@/src/entities/models/users/users.model"
|
||||
import { CreateUserSchema } from "@/src/entities/models/users/create-user.model"
|
||||
import { z } from "zod"
|
||||
|
||||
// const inputSchema = z.object({
|
||||
// email: z.string().min(1, { message: "Email is required" }).email({ message: "Please enter a valid email address" }),
|
||||
// password: z
|
||||
// .string()
|
||||
// .min(1, { message: "Password is required" })
|
||||
// .min(8, { message: "Password must be at least 8 characters" })
|
||||
// .regex(/[A-Z]/, { message: "Password must contain at least one uppercase letter" })
|
||||
// .regex(/[a-z]/, { message: "Password must contain at least one lowercase letter" })
|
||||
// .regex(/[0-9]/, { message: "Password must contain at least one number" }),
|
||||
// email_confirm: z.boolean().optional(),
|
||||
// })
|
||||
|
||||
const inputSchema = CreateUserSchema
|
||||
|
||||
export type ICreateUserController = ReturnType<typeof createUserController>
|
||||
|
@ -14,20 +27,23 @@ export type ICreateUserController = ReturnType<typeof createUserController>
|
|||
export const createUserController = (
|
||||
instrumentationService: IInstrumentationService,
|
||||
createUserUseCase: ICreateUserUseCase,
|
||||
authenticationService: IAuthenticationService
|
||||
getCurrentUserUseCase: IGetCurrentUserUseCase
|
||||
) => async (input: Partial<z.infer<typeof inputSchema>>) => {
|
||||
return await instrumentationService.startSpan({ name: "createUser Controller" }, async () => {
|
||||
|
||||
const session = await authenticationService.getSession()
|
||||
const session = await getCurrentUserUseCase()
|
||||
|
||||
if (!session) {
|
||||
throw new UnauthenticatedError("Must be logged in to create a todo")
|
||||
}
|
||||
if (!session) {
|
||||
throw new UnauthenticatedError("Must be logged in to create a user")
|
||||
}
|
||||
|
||||
const { data, error: inputParseError } = inputSchema.safeParse(input)
|
||||
const { data, error: inputParseError } = inputSchema.safeParse(input)
|
||||
|
||||
if (inputParseError) {
|
||||
throw new InputParseError("Invalid data", { cause: inputParseError })
|
||||
}
|
||||
if (inputParseError) {
|
||||
throw new InputParseError(inputParseError.errors[0].message)
|
||||
}
|
||||
|
||||
return await createUserUseCase(data);
|
||||
return await createUserUseCase(data);
|
||||
|
||||
})
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
import { IUsersRepository } from "@/src/application/repositories/users.repository.interface"
|
||||
import { IAuthenticationService } from "@/src/application/services/authentication.service.interface"
|
||||
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
|
||||
import { IDeleteUserUseCase } from "@/src/application/use-cases/users/delete-user.use-case"
|
||||
import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case"
|
||||
import { UnauthenticatedError } from "@/src/entities/errors/auth"
|
||||
import { ICredentialsDeleteUserSchema } from "@/src/entities/models/users/delete-user.model"
|
||||
|
||||
export type IDeleteUserController = ReturnType<typeof deleteUserController>
|
||||
|
||||
|
@ -9,17 +12,17 @@ export const deleteUserController =
|
|||
(
|
||||
instrumentationService: IInstrumentationService,
|
||||
deleteUserUseCase: IDeleteUserUseCase,
|
||||
authenticationService: IAuthenticationService
|
||||
getCurrentUserUseCase: IGetCurrentUserUseCase
|
||||
) =>
|
||||
async (id: string) => {
|
||||
async (credential: ICredentialsDeleteUserSchema) => {
|
||||
return await instrumentationService.startSpan({ name: "deleteUser Controller" }, async () => {
|
||||
|
||||
const session = await authenticationService.getSession()
|
||||
const session = await getCurrentUserUseCase()
|
||||
|
||||
if (!session) {
|
||||
throw new UnauthenticatedError("Must be logged in to create a todo")
|
||||
throw new UnauthenticatedError("Must be logged in to create a user")
|
||||
}
|
||||
|
||||
return await deleteUserUseCase(id);
|
||||
return await deleteUserUseCase(credential);
|
||||
})
|
||||
}
|
|
@ -22,6 +22,6 @@ export const getUserByEmailController =
|
|||
throw new InputParseError("Invalid data", { cause: inputParseError });
|
||||
}
|
||||
|
||||
return await getUserByEmailUseCase(data.email);
|
||||
return await getUserByEmailUseCase({ email: data.email });
|
||||
})
|
||||
}
|
|
@ -21,6 +21,6 @@ export const getUserByIdController = (
|
|||
throw new InputParseError("Invalid data", { cause: inputParseError });
|
||||
}
|
||||
|
||||
return await getUserByIdUseCase(data.id);
|
||||
return await getUserByIdUseCase({ id: data.id });
|
||||
})
|
||||
}
|
|
@ -22,6 +22,6 @@ export const getUserByUsernameController =
|
|||
throw new InputParseError("Invalid data", { cause: inputParseError });
|
||||
}
|
||||
|
||||
return await getUserByUsernameUseCase(data.username);
|
||||
return await getUserByUsernameUseCase({ username: data.username });
|
||||
})
|
||||
}
|
|
@ -1,15 +1,16 @@
|
|||
import { IUsersRepository } from "@/src/application/repositories/users.repository.interface"
|
||||
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
|
||||
import { IGetUsersUseCase } from "@/src/application/use-cases/users/get-users.use-case"
|
||||
|
||||
export type IGetUserController = ReturnType<typeof getUsersController>
|
||||
export type IGetUsersController = ReturnType<typeof getUsersController>
|
||||
|
||||
export const getUsersController =
|
||||
(
|
||||
instrumentationService: IInstrumentationService,
|
||||
usersRepository: IUsersRepository
|
||||
getUsersUseCase: IGetUsersUseCase
|
||||
) =>
|
||||
async () => {
|
||||
return await instrumentationService.startSpan({ name: "getgetUsers Controller" }, async () => {
|
||||
return await usersRepository.getUsers();
|
||||
return await instrumentationService.startSpan({ name: "geIGetUsers Controller" }, async () => {
|
||||
return await getUsersUseCase()
|
||||
})
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
|
||||
import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case";
|
||||
import { IInviteUserUseCase } from "@/src/application/use-cases/users/invite-user.use-case";
|
||||
import { InputParseError } from "@/src/entities/errors/common";
|
||||
import { z } from "zod";
|
||||
|
@ -12,17 +13,25 @@ export type IInviteUserController = ReturnType<typeof inviteUserController>
|
|||
export const inviteUserController =
|
||||
(
|
||||
instrumentationService: IInstrumentationService,
|
||||
inviteUserUseCase: IInviteUserUseCase
|
||||
inviteUserUseCase: IInviteUserUseCase,
|
||||
getCurrentUserUseCase: IGetCurrentUserUseCase
|
||||
) =>
|
||||
async (input: Partial<z.infer<typeof inputSchema>>) => {
|
||||
return await instrumentationService.startSpan({ name: "inviteUser Controller" }, async () => {
|
||||
|
||||
const session = await getCurrentUserUseCase();
|
||||
|
||||
if (!session) {
|
||||
throw new InputParseError("Must be logged in to invite a user");
|
||||
}
|
||||
|
||||
const { data, error: inputParseError } = inputSchema.safeParse(input);
|
||||
|
||||
if (inputParseError) {
|
||||
throw new InputParseError("Invalid data", { cause: inputParseError });
|
||||
}
|
||||
|
||||
return await inviteUserUseCase(data.email);
|
||||
return await inviteUserUseCase({ email: data.email });
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
|
||||
import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case";
|
||||
import { IUnbanUserUseCase } from "@/src/application/use-cases/users/unban-user.use-case";
|
||||
import { InputParseError } from "@/src/entities/errors/common";
|
||||
import { z } from "zod";
|
||||
|
@ -11,16 +12,24 @@ export type IUnbanUserController = ReturnType<typeof unbanUserController>
|
|||
|
||||
export const unbanUserController = (
|
||||
instrumentationService: IInstrumentationService,
|
||||
unbanUserUseCase: IUnbanUserUseCase
|
||||
unbanUserUseCase: IUnbanUserUseCase,
|
||||
getCurrentUserUseCase: IGetCurrentUserUseCase
|
||||
) =>
|
||||
async (input: Partial<z.infer<typeof inputSchema>>) => {
|
||||
return await instrumentationService.startSpan({ name: "unbanUser Controller" }, async () => {
|
||||
|
||||
const session = await getCurrentUserUseCase();
|
||||
|
||||
if (!session) {
|
||||
throw new InputParseError("Must be logged in to unban a user");
|
||||
}
|
||||
|
||||
const { data, error: inputParseError } = inputSchema.safeParse(input);
|
||||
|
||||
if (inputParseError) {
|
||||
throw new InputParseError("Invalid data", { cause: inputParseError });
|
||||
}
|
||||
|
||||
return await unbanUserUseCase(data.id);
|
||||
return await unbanUserUseCase({ id: data.id });
|
||||
})
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
import { IAuthenticationService } from "@/src/application/services/authentication.service.interface";
|
||||
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
|
||||
import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case";
|
||||
import { IUpdateUserUseCase } from "@/src/application/use-cases/users/update-user.use-case";
|
||||
import { UnauthenticatedError } from "@/src/entities/errors/auth";
|
||||
import { InputParseError } from "@/src/entities/errors/common";
|
||||
import { UpdateUser, UpdateUserSchema } from "@/src/entities/models/users/users.model";
|
||||
import { UpdateUserSchema } from "@/src/entities/models/users/update-user.model";
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
const inputSchema = UpdateUserSchema
|
||||
|
@ -14,12 +16,12 @@ export const updateUserController =
|
|||
(
|
||||
instrumentationService: IInstrumentationService,
|
||||
updateUserUseCase: IUpdateUserUseCase,
|
||||
authenticationService: IAuthenticationService
|
||||
getCurrentUserUseCase: IGetCurrentUserUseCase
|
||||
) =>
|
||||
async (id: string, input: Partial<z.infer<typeof inputSchema>>,) => {
|
||||
return await instrumentationService.startSpan({ name: "updateUser Controller" }, async () => {
|
||||
|
||||
const session = await authenticationService.getSession()
|
||||
const session = await getCurrentUserUseCase()
|
||||
|
||||
if (!session) {
|
||||
throw new UnauthenticatedError("Must be logged in to create a todo")
|
||||
|
@ -31,6 +33,6 @@ export const updateUserController =
|
|||
throw new InputParseError("Invalid data", { cause: inputParseError })
|
||||
}
|
||||
|
||||
return await updateUserUseCase(id, data);
|
||||
return await updateUserUseCase({ id }, data);
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue