refactor add user and invite user

This commit is contained in:
vergiLgood1 2025-03-22 23:20:31 +07:00
parent e95bd8cb23
commit 0af8a9be0b
81 changed files with 2629 additions and 2889 deletions

View File

@ -16,29 +16,27 @@ import {
import { NavPreMain } from "./navigations/nav-pre-main"; import { NavPreMain } from "./navigations/nav-pre-main";
import { navData } from "@/prisma/data/nav"; import { navData } from "@/prisma/data/nav";
import { TeamSwitcher } from "../../../_components/team-switcher"; 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>) { export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const [user, setUser] = React.useState<User | null>(null); const { data: user, isPending, error } = useGetCurrentUserQuery()
const [isLoading, setIsLoading] = React.useState(true);
React.useEffect(() => { // React.useEffect(() => {
async function fetchUser() { // async function fetchUser() {
try { // try {
setIsLoading(true); // setIsLoading(true);
const userData = await getCurrentUser(); // const userData = await getCurrentUser();
setUser(userData.data.user); // setUser(userData.data.user);
} catch (error) { // } catch (error) {
console.error("Failed to fetch user:", error); // console.error("Failed to fetch user:", error);
} finally { // } finally {
setIsLoading(false); // setIsLoading(false);
} // }
} // }
fetchUser(); // fetchUser();
}, []); // }, []);
return ( return (
<Sidebar collapsible="icon" {...props}> <Sidebar collapsible="icon" {...props}>
@ -51,7 +49,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<NavReports reports={navData.reports} /> <NavReports reports={navData.reports} />
</SidebarContent> </SidebarContent>
<SidebarFooter> <SidebarFooter>
<NavUser user={user} /> <NavUser user={user ?? null} />
</SidebarFooter> </SidebarFooter>
<SidebarRail /> <SidebarRail />
</Sidebar> </Sidebar>

View File

@ -24,13 +24,13 @@ import {
useSidebar, useSidebar,
} from "@/app/_components/ui/sidebar"; } from "@/app/_components/ui/sidebar";
import { IconLogout, IconSettings, IconSparkles } from "@tabler/icons-react"; 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 { signOut } from "@/app/(pages)/(auth)/action";
import { SettingsDialog } from "../settings/setting-dialog"; import { SettingsDialog } from "../settings/setting-dialog";
import { useSignOutHandler } from "@/app/(pages)/(auth)/handler"; import { useSignOutHandler } from "@/app/(pages)/(auth)/handler";
import { AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogCancel, AlertDialogAction } from "@/app/_components/ui/alert-dialog"; 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 { isMobile } = useSidebar();
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);

View File

@ -2,7 +2,7 @@
import type React from "react"; 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 { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@ -30,7 +30,6 @@ import { useRef, useState } from "react";
import { ScrollArea } from "@/app/_components/ui/scroll-area"; import { ScrollArea } from "@/app/_components/ui/scroll-area";
import { import {
updateUser, updateUser,
uploadAvatar,
} from "@/app/(pages)/(admin)/dashboard/user-management/action"; } from "@/app/(pages)/(admin)/dashboard/user-management/action";
const profileFormSchema = z.object({ const profileFormSchema = z.object({
@ -41,7 +40,7 @@ const profileFormSchema = z.object({
type ProfileFormValues = z.infer<typeof profileFormSchema>; type ProfileFormValues = z.infer<typeof profileFormSchema>;
interface ProfileSettingsProps { interface ProfileSettingsProps {
user: User | null; user: IUserSchema | null;
} }
export function ProfileSettings({ user }: ProfileSettingsProps) { export function ProfileSettings({ user }: ProfileSettingsProps) {
@ -70,12 +69,12 @@ export function ProfileSettings({ user }: ProfileSettingsProps) {
setIsUploading(true); setIsUploading(true);
// Upload avatar to storage // 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 // Update the form value
form.setValue("avatar", publicUrl); // form.setValue("avatar", publicUrl);
} catch (error) { } catch (error) {
console.error("Error uploading avatar:", error); console.error("Error uploading avatar:", error);
} finally { } finally {

View File

@ -3,15 +3,17 @@ import { createClient } from "@/app/_utils/supabase/server";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
export default async function DashboardPage() { export default async function DashboardPage() {
const supabase = await createClient(); // const supabase = await createClient();
const { // const {
data: { session }, // data: { user },
} = await supabase.auth.getSession(); // } = await supabase.auth.getUser();
if (!session) { // if (!user) {
return redirect("/sign-in"); // return redirect("/sign-in");
} // }
// console.log("user", user);
return ( return (
<> <>
@ -20,7 +22,7 @@ export default async function DashboardPage() {
<div className="grid auto-rows-min gap-4 md:grid-cols-3"> <div className="grid auto-rows-min gap-4 md:grid-cols-3">
<div className="aspect-video rounded-xl bg-muted/50"> <div className="aspect-video rounded-xl bg-muted/50">
<pre className="text-xs font-mono p-3 rounded border overflow-auto"> <pre className="text-xs font-mono p-3 rounded border overflow-auto">
{JSON.stringify(session, null, 2)} {/* {JSON.stringify(user, null, 2)} */}
</pre> </pre>
</div> </div>

View File

@ -1,155 +1,66 @@
import type React from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/app/_components/ui/dialog"
import { Button } from "@/app/_components/ui/button"
import { useState } from "react"; import { Mail, Lock, Loader2 } from "lucide-react"
import { import { useAddUserDialogHandler } from "../handler"
Dialog, import { ReactHookFormField } from "@/app/_components/react-hook-form-field"
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";
interface AddUserDialogProps { interface AddUserDialogProps {
open: boolean; open: boolean
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void
onUserAdded: () => void; onUserAdded: () => void
} }
export function AddUserDialog({ open, onOpenChange, onUserAdded }: AddUserDialogProps) {
export function AddUserDialog({ const {
open, register,
onOpenChange, errors,
onUserAdded, isPending,
}: AddUserDialogProps) { handleSubmit,
const [formData, setFormData] = useState({ handleOpenChange,
email: "", } = useAddUserDialogHandler({ onUserAdded, onOpenChange });
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;
}
};
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-md border-0 "> <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"> <DialogHeader className="flex flex-row items-center justify-between space-y-0 space-x-4 pb-4">
<DialogTitle className="text-xl font-semibold "> <DialogTitle className="text-xl font-semibold">Create a new user</DialogTitle>
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> */}
</DialogHeader> </DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-3"> <div className="space-y-3">
<div className="space-y-2"> <ReactHookFormField
<label htmlFor="email" className="text-sm text-zinc-400"> label="Email address"
Email address icon={Mail}
</label> placeholder="user@example.com"
<div className="relative"> error={errors.email}
<Mail className="absolute left-3 top-2.5 h-5 w-5 text-zinc-500" /> registration={register("email")}
<Input />
id="email"
name="email" <ReactHookFormField
type="email" label="Password"
required icon={Lock}
placeholder="user@example.com" placeholder="••••••••"
value={formData.email} type="password"
onChange={handleInputChange} error={errors.password}
className="pl-10 placeholder:text-zinc-500 " registration={register("password")}
/> />
</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>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center space-x-2"> {/* <div className="flex items-center space-x-2">
<Checkbox <Checkbox
id="email-confirm" id="email_confirm"
checked={formData.emailConfirm} {...register("email_confirm")}
onCheckedChange={(checked) =>
setFormData((prev) => ({
...prev,
emailConfirm: checked as boolean,
}))
}
className="border-zinc-700" className="border-zinc-700"
/> />
<label htmlFor="email-confirm" className="text-sm "> <label htmlFor="email_confirm" className="text-sm">
Auto Confirm User? Auto Confirm User?
</label> </label>
</div> </div> */}
<p className="text-sm text-zinc-500 pl-6"> <p className="text-sm text-zinc-500">
A confirmation email will not be sent when creating a user via A confirmation email will not be sent when creating a user via this form.
this form.
</p> </p>
</div> </div>
<Button type="submit" disabled={isPending} className="w-full "> <Button type="submit" disabled={isPending} className="w-full">
{isPending ? ( {isPending ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
@ -164,3 +75,4 @@ export function AddUserDialog({
</Dialog> </Dialog>
); );
} }

View File

@ -16,6 +16,11 @@ import { Textarea } from "@/app/_components/ui/textarea";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { inviteUser } from "@/app/(pages)/(admin)/dashboard/user-management/action"; import { inviteUser } from "@/app/(pages)/(admin)/dashboard/user-management/action";
import { toast } from "sonner"; 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 { interface InviteUserDialogProps {
open: boolean; open: boolean;
@ -28,52 +33,18 @@ export function InviteUserDialog({
onOpenChange, onOpenChange,
onUserInvited, onUserInvited,
}: InviteUserDialogProps) { }: InviteUserDialogProps) {
const [formData, setFormData] = useState({
email: "",
metadata: "{}",
});
const handleInputChange = ( const {
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement> register,
) => { handleSubmit,
const { name, value } = e.target; reset,
setFormData((prev) => ({ ...prev, [name]: value })); errors,
}; isPending,
handleOpenChange
const { mutate: inviteUserMutation, isPending } = useMutation({ } = useInviteUserHandler({ onUserInvited, onOpenChange });
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;
}
};
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-md"> <DialogContent className="sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle>Invite User</DialogTitle> <DialogTitle>Invite User</DialogTitle>
@ -81,30 +52,26 @@ export function InviteUserDialog({
Send an invitation email to a new user. Send an invitation email to a new user.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4"> <Separator className="" />
<div className="space-y-2"> <form onSubmit={handleSubmit} className="space-y-8">
<Label htmlFor="invite-email">Email *</Label> <ReactHookFormField
<Input label="Email"
id="invite-email" icon={MailIcon}
name="email" placeholder="example@gmail.com"
type="email" error={errors.email}
required registration={register("email")}
value={formData.email} />
onChange={handleInputChange}
placeholder="example@gmail.com"
/>
</div>
<DialogFooter> <DialogFooter>
<Button <Button className="flex w-full" type="submit" disabled={isPending}>
type="button" {
variant="outline" isPending ? (
onClick={() => onOpenChange(false)} <>
> <span className="mr-2">Invite user...</span>
Cancel <Loader2 className="animate-spin" />
</Button> </>
<Button type="submit" disabled={isPending}> ) : "Invite User"
{isPending ? "Sending..." : "Send Invitation"} }
</Button> </Button>
</DialogFooter> </DialogFooter>
</form> </form>

View File

@ -6,7 +6,7 @@ import { useState, useRef } from "react";
import { z } from "zod"; import { z } from "zod";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; 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 { import {
Form, Form,
@ -40,7 +40,7 @@ const profileFormSchema = z.object({
type ProfileFormValues = z.infer<typeof profileFormSchema>; type ProfileFormValues = z.infer<typeof profileFormSchema>;
interface ProfileFormProps { interface ProfileFormProps {
user: User | null; user: IUserSchema | null;
onSuccess?: () => void; onSuccess?: () => void;
} }

View File

@ -35,11 +35,10 @@ import {
import { import {
banUser, banUser,
deleteUser, deleteUser,
sendMagicLink,
sendPasswordRecovery,
unbanUser, unbanUser,
} from "@/app/(pages)/(admin)/dashboard/user-management/action"; } from "@/app/(pages)/(admin)/dashboard/user-management/action";
import { format } from "date-fns"; import { format } from "date-fns";
import { sendMagicLink, sendPasswordRecovery } from "@/app/(pages)/(auth)/action";
interface UserDetailSheetProps { interface UserDetailSheetProps {
open: boolean; open: boolean;
@ -129,7 +128,8 @@ export function UserDetailSheet({
if (user.banned_until) { if (user.banned_until) {
return unbanUser(user.id); return unbanUser(user.id);
} else { } else {
return banUser(user.id); const ban_duration = "7h"; // Example: Ban duration set to 7 days
return banUser(user.id, ban_duration);
} }
}, },
onMutate: () => { onMutate: () => {

View File

@ -5,7 +5,7 @@ import type * as z from "zod"
import { Loader2 } from "lucide-react" 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 // UI Components
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/app/_components/ui/sheet" 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 { useMutation } from "@tanstack/react-query"
import { updateUser } from "../action" import { updateUser } from "../action"
import { toast } from "sonner" 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 { interface UserProfileSheetProps {
open: boolean open: boolean
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void
userData?: User userData?: IUserSchema
onUserUpdated: () => void onUserUpdated: () => void
} }
export function UserProfileSheet({ open, onOpenChange, userData, onUserUpdated }: UserProfileSheetProps) { export function UserProfileSheet({ open, onOpenChange, userData, onUserUpdated }: UserProfileSheetProps) {
// Initialize form with user data // Initialize form with user data
const form = useForm<UserProfileFormValues>({ const form = useForm<UserProfileFormValues>({
resolver: zodResolver(UpdateUserParamsSchema), resolver: zodResolver(UpdateUserSchema),
defaultValues: { defaultValues: {
email: userData?.email || undefined, email: userData?.email || undefined,
encrypted_password: userData?.encrypted_password || undefined, encrypted_password: userData?.encrypted_password || undefined,

View File

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

View File

@ -16,570 +16,63 @@ import {
} from "lucide-react"; } from "lucide-react";
import { Button } from "@/app/_components/ui/button"; import { Button } from "@/app/_components/ui/button";
import { Input } from "@/app/_components/ui/input"; import { Input } from "@/app/_components/ui/input";
import { Badge } from "@/app/_components/ui/badge";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
DropdownMenuCheckboxItem,
} from "@/app/_components/ui/dropdown-menu"; } 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 { DataTable } from "./data-table";
import { InviteUserDialog } from "./invite-user"; import { InviteUserDialog } from "./invite-user";
import { AddUserDialog } from "./add-user-dialog"; import { AddUserDialog } from "./add-user-dialog";
import { UserDetailSheet } from "./sheet"; 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"; import { UserProfileSheet } from "./update-user";
import { filterUsers, useUserManagementHandlers } from "../handler";
type UserFilterOptions = { import { createUserColumns } from "./users-table";
email: string; import { useGetUsersQuery } from "../queries";
phone: string;
lastSignIn: string;
createdAt: string;
status: string[];
};
type UserTableColumn = ColumnDef<User, User>;
export default function UserManagement() { 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 // Use React Query to fetch users
const { const {
data: users = [], data: users = [],
isLoading, isPending,
refetch, refetch,
isPlaceholderData, } = useGetUsersQuery();
} = useQuery<User[]>({
queryKey: ["users"],
queryFn: fetchUsers,
placeholderData: keepPreviousData,
throwOnError: true,
});
// Handle opening the detail sheet // User management handler
const handleUserClick = (user: User) => { const {
setDetailUser(user); searchQuery,
setIsSheetOpen(true); setSearchQuery,
}; detailUser,
updateUser,
// Handle opening the update sheet isSheetOpen,
const handleUserUpdate = (user: User) => { setIsSheetOpen,
setUpdateUser(user); isUpdateOpen,
setIsUpdateOpen(true); setIsUpdateOpen,
}; isAddUserOpen,
setIsAddUserOpen,
// Close detail sheet when update sheet opens isInviteUserOpen,
useEffect(() => { setIsInviteUserOpen,
if (isUpdateOpen) { filters,
setIsSheetOpen(false); setFilters,
} handleUserClick,
}, [isUpdateOpen]); handleUserUpdate,
clearFilters,
// Reset detail user when sheet closes getActiveFilterCount,
useEffect(() => { } = useUserManagementHandlers(refetch)
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]);
// Apply filters to users
const filteredUsers = useMemo(() => { const filteredUsers = useMemo(() => {
return users.filter((user) => { return filterUsers(users, searchQuery, filters)
// Global search }, [users, searchQuery, filters])
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; // Get active filter count
} const activeFilterCount = getActiveFilterCount()
// Email filter // Create table columns
if ( const columns = createUserColumns(filters, setFilters, handleUserUpdate)
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>
),
},
];
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@ -642,7 +135,7 @@ export default function UserManagement() {
<DataTable <DataTable
columns={columns} columns={columns}
data={filteredUsers} data={filteredUsers}
loading={isLoading} loading={isPending}
onRowClick={(user) => handleUserClick(user)} onRowClick={(user) => handleUserClick(user)}
/> />
{detailUser && ( {detailUser && (
@ -653,16 +146,8 @@ export default function UserManagement() {
onUserUpdate={() => refetch()} onUserUpdate={() => refetch()}
/> />
)} )}
<AddUserDialog <AddUserDialog open={isAddUserOpen} onOpenChange={setIsAddUserOpen} onUserAdded={() => refetch()} />
open={isAddUserOpen} <InviteUserDialog open={isInviteUserOpen} onOpenChange={setIsInviteUserOpen} onUserInvited={() => refetch()} />
onOpenChange={setIsAddUserOpen}
onUserAdded={() => refetch()}
/>
<InviteUserDialog
open={isInviteUserOpen}
onOpenChange={setIsInviteUserOpen}
onUserInvited={() => refetch()}
/>
{updateUser && ( {updateUser && (
<UserProfileSheet <UserProfileSheet
open={isUpdateOpen} open={isUpdateOpen}
@ -672,5 +157,5 @@ export default function UserManagement() {
/> />
)} )}
</div> </div>
); )
} }

View File

@ -1,15 +1,22 @@
"use client"; "use client";
import { useQuery } from "@tanstack/react-query";
import { Card, CardContent } from "@/app/_components/ui/card"; import { Card, CardContent } from "@/app/_components/ui/card";
import { Users, UserCheck, UserX } from "lucide-react"; import { Users, UserCheck, UserX } from "lucide-react";
import { fetchUsers } from "@/app/(pages)/(admin)/dashboard/user-management/action"; import { IUserSchema } from "@/src/entities/models/users/users.model";
import { User } from "@/src/entities/models/users/users.model"; import { useGetUsersQuery } from "../queries";
import { useNavigations } from "@/app/_hooks/use-navigations";
import { useEffect, useState } from "react";
import { toast } from "sonner"; 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 totalUsers = users.length;
const activeUsers = users.filter( const activeUsers = users.filter(
(user) => !user.banned_until && user.email_confirmed_at (user) => !user.banned_until && user.email_confirmed_at
@ -21,21 +28,18 @@ function calculateUserStats(users: User[]) {
activeUsers, activeUsers,
inactiveUsers, inactiveUsers,
activePercentage: activePercentage:
totalUsers > 0 ? ((activeUsers / totalUsers) * 100).toFixed(1) : "0.0", totalUsers > 0 ? ((activeUsers / totalUsers) * 100).toFixed(1) : '0.0',
inactivePercentage: inactivePercentage:
totalUsers > 0 ? ((inactiveUsers / totalUsers) * 100).toFixed(1) : "0.0", totalUsers > 0 ? ((inactiveUsers / totalUsers) * 100).toFixed(1) : '0.0',
}; };
} }
export function UserStats() { export function UserStats() {
const { data: users = [], isLoading } = useQuery({ const { data: users, isPending, error } = useGetUsersQuery();
queryKey: ["users"],
queryFn: fetchUsers,
});
const stats = calculateUserStats(users); const stats = calculateUserStats(users);
if (isLoading) { if (isPending) {
return ( return (
<> <>
{[...Array(3)].map((_, i) => ( {[...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 = [ const cards = [
{ {
title: "Total Users", title: 'Total Users',
value: stats.totalUsers, value: stats.totalUsers,
subtitle: "Updated just now", subtitle: 'Updated just now',
icon: Users, icon: Users,
}, },
{ {
title: "Active Users", title: 'Active Users',
value: stats.activeUsers, value: stats.activeUsers,
subtitle: `${stats.activePercentage}% of total users`, subtitle: `${stats.activePercentage}% of total users`,
icon: UserCheck, icon: UserCheck,
}, },
{ {
title: "Inactive Users", title: 'Inactive Users',
value: stats.inactiveUsers, value: stats.inactiveUsers,
subtitle: `${stats.inactivePercentage}% of total users`, subtitle: `${stats.inactivePercentage}% of total users`,
icon: UserX, icon: UserX,
@ -92,4 +107,4 @@ export function UserStats() {
))} ))}
</> </>
); );
} }

View File

@ -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>
),
},
]
}

View File

@ -1,361 +1,498 @@
"use server"; "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 { import {
CreateUserParams, AuthenticationError,
InviteUserParams, UnauthenticatedError,
UpdateUserParams, } from '@/src/entities/errors/auth';
User, import { redirect } from 'next/navigation';
UserResponse, import { IBanDuration, IBanUserSchema, ICredentialsBanUserSchema } from '@/src/entities/models/users/ban-user.model';
} from "@/src/entities/models/users/users.model"; import { ICreateUserSchema } from '@/src/entities/models/users/create-user.model';
import { createClient } from "@/app/_utils/supabase/server"; import { IUpdateUserSchema } from '@/src/entities/models/users/update-user.model';
import { createAdminClient } from "@/app/_utils/supabase/admin"; 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 return { success: true };
export async function fetchUsers(): Promise<User[]> { } catch (err) {
// const { data, error } = await supabase.auth.admin.getUsers(); if (err instanceof InputParseError) {
// return {
// error: err.message,
// };
// if (error) { throw new InputParseError(err.message);
// console.error("Error fetching users:", error); }
// throw new Error(error.message);
// }
// return data.users.map((user) => ({ if (err instanceof UnauthenticatedError) {
// ...user, // return {
// })) as User[]; // 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({ if (err instanceof AuthenticationError) {
include: { // return {
profile: true, // error: 'User not found.',
}, // };
});
if (!users) { throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
throw new Error("Users not found"); }
}
console.log("fetchedUsers"); const crashReporterService = getInjection('ICrashReporterService');
crashReporterService.report(err);
return users; // return {
} // error:
// 'An error happened. The developers have been notified. Please try again later.',
// get current user // };
export async function getCurrentUser(): Promise<UserResponse> { throw new Error('An error happened. The developers have been notified. Please try again later.');
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;
} }
);
// 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 return { success: true };
export async function updateUser( } catch (err) {
userId: string, if (err instanceof InputParseError) {
params: UpdateUserParams // return {
): Promise<UserResponse> { // error: err.message,
const supabase = createAdminClient(); // };
const { data, error } = await supabase.auth.admin.updateUserById(userId, { throw new InputParseError(err.message);
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) { if (err instanceof UnauthenticatedError) {
console.error("Error updating user:", error); // return {
throw new Error(error.message); // 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({ if (err instanceof AuthenticationError) {
where: { // return {
id: userId, // error: 'User not found.',
}, // };
include: {
profile: true,
},
});
if (!user) { throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
throw new Error("User not found"); }
}
const updateUser = await db.users.update({ const crashReporterService = getInjection('ICrashReporterService');
where: { crashReporterService.report(err);
id: userId, // return {
}, // error:
data: { // 'An error happened. The developers have been notified. Please try again later.',
role: params.role || user.role, // };
invited_at: params.invited_at || user.invited_at, throw new Error('An error happened. The developers have been notified. Please try again later.');
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 getCurrentUser() {
export async function inviteUser(params: InviteUserParams): Promise<void> { const instrumentationService = getInjection('IInstrumentationService');
const supabase = createAdminClient(); 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, { if (err instanceof UnauthenticatedError || err instanceof AuthenticationError) {
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`, redirect('/sign-in');
}); }
if (error) { const crashReporterService = getInjection('ICrashReporterService');
console.error("Error inviting user:", error); crashReporterService.report(err);
throw new Error(error.message);
} 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.');
}
}
);
} }

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import { redirect } from "next/navigation"
import { getInjection } from "@/di/container" import { getInjection } from "@/di/container"
import { revalidatePath } from "next/cache" 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 { AuthenticationError, UnauthenticatedError } from "@/src/entities/errors/auth"
import { createClient } from "@/app/_utils/supabase/server" import { createClient } from "@/app/_utils/supabase/server"
@ -18,25 +18,22 @@ export async function signIn(formData: FormData) {
try { try {
const signInController = getInjection("ISignInController") const signInController = getInjection("ISignInController")
await signInController({ email }) return await signInController({ email })
// if (email) { // if (email) {
// redirect(`/verify-otp?email=${encodeURIComponent(email)}`) // redirect(`/verify-otp?email=${encodeURIComponent(email)}`)
// } // }
return { success: true }
} catch (err) { } catch (err) {
if ( if (err instanceof InputParseError) {
err instanceof InputParseError || return { error: err.message }
err instanceof AuthenticationError
) {
return {
error: 'Incorrect credential. Please try again.',
};
} }
if (err instanceof UnauthenticatedError) { if (err instanceof AuthenticationError) {
return { error: "Invalid credential. Please try again." }
}
if (err instanceof UnauthenticatedError || err instanceof NotFoundError) {
return { return {
error: 'User not found. Please tell your admin to create an account for you.', 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.",
}
}
})
}

View File

@ -1,19 +1,18 @@
import { AuthenticationError } from "@/src/entities/errors/auth"; import { AuthenticationError } from "@/src/entities/errors/auth";
import { useState } from "react"; import { useState } from "react";
import { useAuthActions } from "./mutation"; import { useAuthActions } from './queries';
import { useForm } from "react-hook-form"; import { useForm } from 'react-hook-form';
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from '@hookform/resolvers/zod';;
import { defaultSignInPasswordlessValues, SignInFormData, SignInPasswordless, SignInPasswordlessSchema, SignInSchema } from "@/src/entities/models/auth/sign-in.model"; import { toast } from 'sonner';
import { createFormData } from "@/app/_utils/common"; import { useNavigations } from '@/app/_hooks/use-navigations';
import { useFormHandler } from "@/app/_hooks/use-form-handler"; import {
import { toast } from "sonner"; IVerifyOtpSchema,
import { signIn } from "./action"; verifyOtpSchema,
import { useNavigations } from "@/app/_hooks/use-navigations"; } from '@/src/entities/models/auth/verify-otp.model';
import { VerifyOtpFormData, verifyOtpSchema } from "@/src/entities/models/auth/verify-otp.model";
/** /**
* Hook untuk menangani proses sign in * Hook untuk menangani proses sign in
* *
* @returns {Object} Object berisi handler dan state untuk form sign in * @returns {Object} Object berisi handler dan state untuk form sign in
* @example * @example
* const { handleSubmit, isPending, error } = useSignInHandler(); * const { handleSubmit, isPending, error } = useSignInHandler();
@ -32,23 +31,18 @@ export function useSignInHandler() {
setError(undefined); setError(undefined);
const formData = new FormData(event.currentTarget); const formData = new FormData(event.currentTarget);
const email = formData.get("email")?.toString() const email = formData.get('email')?.toString();
try { const res = await signIn.mutateAsync(formData);
await signIn.mutateAsync(formData, {
onSuccess: () => { if (!res?.error) {
toast("An email has been sent to you. Please check your inbox."); toast('An email has been sent to you. Please check your inbox.');
if (email) router.push(`/verify-otp?email=${encodeURIComponent(email)}`); if (email) router.push(`/verify-otp?email=${encodeURIComponent(email)}`);
}, } else {
onError: (error) => { setError(res.error);
setError(error.message);
}
});
} catch (error) {
if (error instanceof Error) {
setError(error.message);
}
} }
}; };
return { return {
@ -58,67 +52,66 @@ export function useSignInHandler() {
error, error,
isPending: signIn.isPending, isPending: signIn.isPending,
errors: !!error || signIn.error, errors: !!error || signIn.error,
clearError: () => setError(undefined) clearError: () => setError(undefined),
}; };
} }
export function useVerifyOtpHandler(email: string) { export function useVerifyOtpHandler(email: string) {
const { router } = useNavigations() const { router } = useNavigations();
const { verifyOtp } = useAuthActions() const { verifyOtp } = useAuthActions();
const [error, setError] = useState<string>() const [error, setError] = useState<string>();
const { const {
register, register,
handleSubmit: hookFormSubmit, handleSubmit: hookFormSubmit,
control, control,
formState: { errors }, formState: { errors },
setValue setValue,
} = useForm<VerifyOtpFormData>({ } = useForm<IVerifyOtpSchema>({
resolver: zodResolver(verifyOtpSchema), resolver: zodResolver(verifyOtpSchema),
defaultValues: { defaultValues: {
email, email,
token: "" token: '',
} },
}) });
const handleOtpChange = (value: string, onChange: (value: string) => void) => { const handleOtpChange = (
onChange(value) value: string,
onChange: (value: string) => void
) => {
onChange(value);
if (value.length === 6) {
handleSubmit();
}
// Clear error when user starts typing // Clear error when user starts typing
if (error) { if (error) {
setError(undefined) setError(undefined);
} }
} };
const handleSubmit = hookFormSubmit(async (data) => { const handleSubmit = hookFormSubmit(async (data) => {
if (verifyOtp.isPending) return if (verifyOtp.isPending) return;
setError(undefined) setError(undefined);
// Create FormData object // Create FormData object
const formData = new FormData() const formData = new FormData();
formData.append("email", data.email) formData.append('email', data.email);
formData.append("token", data.token) formData.append('token', data.token);
try { await verifyOtp.mutateAsync(formData, {
await verifyOtp.mutateAsync(formData, { onSuccess: () => {
onSuccess: () => { toast.success('OTP verified successfully');
toast.success("OTP verified successfully") // Navigate to dashboard on success
// Navigate to dashboard on success router.push('/dashboard');
router.push("/dashboard") },
}, onError: (error) => {
onError: (error) => { setError(error.message);
setError(error.message) },
} });
}) });
} catch (error) {
if (error instanceof Error) {
setError(error.message)
}
}
})
return { return {
register, register,
@ -127,50 +120,42 @@ export function useVerifyOtpHandler(email: string) {
handleOtpChange, handleOtpChange,
errors: { errors: {
...errors, ...errors,
token: error ? { message: error } : errors.token token: error ? { message: error } : errors.token,
}, },
isPending: verifyOtp.isPending, isPending: verifyOtp.isPending,
clearError: () => setError(undefined) clearError: () => setError(undefined),
} };
} }
export function useSignOutHandler() { export function useSignOutHandler() {
const { signOut } = useAuthActions() const { signOut } = useAuthActions();
const { router } = useNavigations() const { router } = useNavigations();
const [error, setError] = useState<string>() const [error, setError] = useState<string>();
const handleSignOut = async () => { const handleSignOut = async () => {
if (signOut.isPending) return if (signOut.isPending) return;
setError(undefined) setError(undefined);
try { await signOut.mutateAsync(undefined, {
await signOut.mutateAsync(undefined, { onSuccess: () => {
onSuccess: () => { toast.success('You have been signed out successfully');
toast.success("You have been signed out successfully") router.push('/sign-in');
router.push("/sign-in") },
}, onError: (error) => {
onError: (error) => { if (error instanceof AuthenticationError) {
if (error instanceof AuthenticationError) { setError(error.message);
setError(error.message) toast.error(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 { return {
handleSignOut, handleSignOut,
error, error,
isPending: signOut.isPending, isPending: signOut.isPending,
errors: !!error || signOut.error, errors: !!error || signOut.error,
clearError: () => setError(undefined) clearError: () => setError(undefined),
} };
} }

View File

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

View File

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

View File

@ -1,71 +1,71 @@
import { hasEnvVars } from "@/app/_utils/supabase/check-env-vars"; // import { hasEnvVars } from "@/app/_utils/supabase/check-env-vars";
import Link from "next/link"; // import Link from "next/link";
import { Badge } from "./ui/badge"; // import { Badge } from "./ui/badge";
import { Button } from "./ui/button"; // import { Button } from "./ui/button";
import { createClient } from "@/app/_utils/supabase/server"; // import { createClient } from "@/app/_utils/supabase/server";
import { signOutAction } from "@/app/(pages)/(auth)/_actions/sign-out"; // import { signOutAction } from "@/app/(pages)/(auth)/_actions/sign-out";
export default async function AuthButton() { // export default async function AuthButton() {
const supabase = await createClient(); // const supabase = await createClient();
const { // const {
data: { user }, // data: { user },
} = await supabase.auth.getUser(); // } = await supabase.auth.getUser();
if (!hasEnvVars) { // if (!hasEnvVars) {
return ( // return (
<> // <>
<div className="flex gap-4 items-center"> // <div className="flex gap-4 items-center">
<div> // <div>
<Badge // <Badge
variant={"default"} // variant={"default"}
className="font-normal pointer-events-none" // className="font-normal pointer-events-none"
> // >
Please update .env.local file with anon key and url // Please update .env.local file with anon key and url
</Badge> // </Badge>
</div> // </div>
<div className="flex gap-2"> // <div className="flex gap-2">
<Button // <Button
asChild // asChild
size="sm" // size="sm"
variant={"outline"} // variant={"outline"}
disabled // disabled
className="opacity-75 cursor-none pointer-events-none" // className="opacity-75 cursor-none pointer-events-none"
> // >
<Link href="/sign-in">Sign in</Link> // <Link href="/sign-in">Sign in</Link>
</Button> // </Button>
<Button // <Button
asChild // asChild
size="sm" // size="sm"
variant={"default"} // variant={"default"}
disabled // disabled
className="opacity-75 cursor-none pointer-events-none" // className="opacity-75 cursor-none pointer-events-none"
> // >
<Link href="/sign-up">Sign up</Link> // <Link href="/sign-up">Sign up</Link>
</Button> // </Button>
</div> // </div>
</div> // </div>
</> // </>
); // );
} // }
return user ? ( // return user ? (
<div className="flex items-center gap-4"> // <div className="flex items-center gap-4">
Hey, {user.email}! // Hey, {user.email}!
<form action={signOutAction}> // <form action={signOutAction}>
<Button type="submit" variant={"outline"}> // <Button type="submit" variant={"outline"}>
Sign out // Sign out
</Button> // </Button>
</form> // </form>
</div> // </div>
) : ( // ) : (
<div className="flex gap-2"> // <div className="flex gap-2">
<Button asChild size="sm" variant={"outline"}> // <Button asChild size="sm" variant={"outline"}>
<Link href="/sign-in">Sign in</Link> // <Link href="/sign-in">Sign in</Link>
</Button> // </Button>
<Button asChild size="sm" variant={"default"}> // <Button asChild size="sm" variant={"default"}>
<Link href="/sign-up">Sign up</Link> // <Link href="/sign-up">Sign up</Link>
</Button> // </Button>
</div> // </div>
); // );
} // }

View File

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

View File

@ -14,7 +14,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
type={type} type={type}
className={cn( 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", "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 className
)} )}
ref={ref} ref={ref}

View File

@ -25,4 +25,8 @@ export class CNumbers {
static readonly MAX_UPLOAD_SIZE_MB = 50; static readonly MAX_UPLOAD_SIZE_MB = 50;
static readonly MIN_PASSWORD_LENGTH = 8; static readonly MIN_PASSWORD_LENGTH = 8;
static readonly MAX_PASSWORD_LENGTH = 128; static readonly MAX_PASSWORD_LENGTH = 128;
// Phone number
static readonly PHONE_MIN_LENGTH = 10;
static readonly PHONE_MAX_LENGTH = 13;
} }

View File

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

View File

@ -84,4 +84,8 @@ export class CTexts {
static readonly SENSITIVE_WORDS = [ static readonly SENSITIVE_WORDS = [
] ]
// Phone number
static readonly PHONE_PREFIX = ['+62', '62', '0']
} }

View File

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

View File

@ -4,88 +4,125 @@
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; /* Latar belakang terang: putih bersih */
--foreground: 240 10% 3.9%; --background: 0 0% 100%; /* #ffffff */
--card: 0 0% 100%; --foreground: 0 0% 10%; /* #1a1a1a, teks hitam pekat */
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%; /* Kartu: sama dengan latar belakang di mode terang */
--popover-foreground: 240 10% 3.9%; --card: 0 0% 100%; /* #ffffff */
--primary: 153 60% 53%; /* Supabase green */ --card-foreground: 0 0% 10%; /* #1a1a1a */
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%; /* Popover: sama dengan latar belakang */
--secondary-foreground: 240 5.9% 10%; --popover: 0 0% 100%; /* #ffffff */
--muted: 240 4.8% 95.9%; --popover-foreground: 0 0% 10%; /* #1a1a1a */
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%; /* Warna utama: hijau Supabase #006239 */
--accent-foreground: 240 5.9% 10%; --primary: 155% 100% 19%; /* #006239 */
--destructive: 0 84.2% 60.2%; --primary-foreground: 0 0% 100%; /* #ffffff untuk kontras pada hijau */
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%; /* Sekunder: abu-abu terang untuk elemen pendukung */
--input: 240 5.9% 90%; --secondary: 0 0% 96%; /* #f5f5f5 */
--ring: 153 60% 53%; /* Matching primary */ --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; --radius: 0.5rem;
--chart-1: 153 60% 53%; /* Supabase green */
--chart-2: 183 65% 50%; /* Chart: gunakan hijau Supabase dan variasi */
--chart-3: 213 70% 47%; --chart-1: 155% 100% 19%; /* #006239 */
--chart-4: 243 75% 44%; --chart-2: 160 60% 45%; /* sedikit lebih gelap */
--chart-5: 273 80% 41%; --chart-3: 165 55% 40%;
--sidebar-background: 0 0% 98%; --chart-4: 170 50% 35%;
--sidebar-foreground: 240 5.3% 26.1%; --chart-5: 175 45% 30%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%; /* Sidebar: mirip dengan kartu di mode terang */
--sidebar-accent: 240 4.8% 95.9%; --sidebar-background: 0 0% 98%; /* #fafafa */
--sidebar-accent-foreground: 240 5.9% 10%; --sidebar-foreground: 0 0% 10%; /* #1a1a1a */
--sidebar-border: 220 13% 91%; --sidebar-primary: 155% 100% 19%; /* #006239 */
--sidebar-ring: 217.2 91.2% 59.8%; --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 { .dark {
--background: 0 0% 9%; /* #171717 */ /* Latar belakang gelap: abu-abu tua mendekati hitam */
--foreground: 210 20% 98%; --background: 0 0% 10%; /* #1a1a1a */
--card: 0 0% 9%; /* #171717 */ --foreground: 0 0% 85%; /* #d9d9d9, teks abu-abu terang */
--card-foreground: 210 20% 98%;
--popover: 0 0% 9%; /* #171717 */ /* Kartu: sama dengan latar belakang di mode gelap */
--popover-foreground: 210 20% 98%; --card: 0 0% 10%; /* #1a1a1a */
--primary: 153 60% 53%; /* Supabase green */ --card-foreground: 0 0% 85%; /* #d9d9d9 */
--primary-foreground: 210 20% 98%;
--secondary: 220 8% 15%; /* Popover: sama dengan latar belakang */
--secondary-foreground: 210 20% 98%; --popover: 0 0% 10%; /* #1a1a1a */
--muted: 220 8% 15%; --popover-foreground: 0 0% 85%; /* #d9d9d9 */
--muted-foreground: 217 10% 64%;
--accent: 220 8% 15%; /* Warna utama: hijau Supabase tetap digunakan */
--accent-foreground: 210 20% 98%; --primary: 155% 100% 19%; /* #006239 */
--destructive: 0 62.8% 30.6%; --primary-foreground: 0 0% 100%; /* #ffffff */
--destructive-foreground: 210 20% 98%;
--border: 220 8% 15%; /* Sekunder: abu-abu gelap untuk elemen pendukung */
--input: 220 8% 15%; --secondary: 0 0% 15%; /* #262626 */
--ring: 153 60% 53%; /* Matching primary */ --secondary-foreground: 0 0% 85%; /* #d9d9d9 */
--chart-1: 153 60% 53%; /* Supabase green */
--chart-2: 183 65% 50%; /* Muted: abu-abu gelap untuk teks pendukung */
--chart-3: 213 70% 47%; --muted: 0 0% 20%; /* #333333 */
--chart-4: 243 75% 44%; --muted-foreground: 0 0% 60%; /* #999999 */
--chart-5: 273 80% 41%;
--sidebar-background: 240 5.9% 10%; /* Aksen: sama dengan sekunder */
--sidebar-foreground: 240 4.8% 95.9%; --accent: 0 0% 15%; /* #262626 */
--sidebar-primary: 224.3 76.3% 48%; --accent-foreground: 0 0% 85%; /* #d9d9d9 */
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%; /* Destructive: merah gelap untuk error */
--sidebar-accent-foreground: 240 4.8% 95.9%; --destructive: 0 62% 30%; /* #802626 */
--sidebar-border: 240 3.7% 15.9%; --destructive-foreground: 0 0% 100%; /* #ffffff */
--sidebar-ring: 217.2 91.2% 59.8%;
/* 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 { @layer base {
* { * {
@apply border-border outline-ring/50; @apply border-border outline-ring/50;
@ -93,4 +130,4 @@
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }

View File

@ -49,3 +49,25 @@ export function createFormData(): FormData {
}); });
return data; 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}`;
}

View File

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

View File

@ -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 { signOutController } from '@/src/interface-adapters/controllers/auth/sign-out.controller';
import { verifyOtpUseCase } from '@/src/application/use-cases/auth/verify-otp.use-case'; import { verifyOtpUseCase } from '@/src/application/use-cases/auth/verify-otp.use-case';
import { verifyOtpController } from '@/src/interface-adapters/controllers/auth/verify-otp.controller'; 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() { export function createAuthenticationModule() {
const authenticationModule = createModule(); const authenticationModule = createModule();
@ -66,6 +70,22 @@ export function createAuthenticationModule() {
DI_SYMBOLS.IAuthenticationService, 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 // Controllers
authenticationModule authenticationModule
@ -90,6 +110,20 @@ export function createAuthenticationModule() {
DI_SYMBOLS.ISignOutUseCase, 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; return authenticationModule;
} }

View File

@ -1,9 +1,7 @@
import { createModule } from '@evyweb/ioctopus'; import { createModule } from '@evyweb/ioctopus';
import { DI_SYMBOLS } from '@/di/types'; import { DI_SYMBOLS } from '@/di/types';
import { UsersRepository } from '@/src/infrastructure/repositories/users.repository'; 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 { getUsersController } from '@/src/interface-adapters/controllers/users/get-users.controller';
import { banUserController } from '@/src/interface-adapters/controllers/users/ban-user.controller'; import { banUserController } from '@/src/interface-adapters/controllers/users/ban-user.controller';
import { banUserUseCase } from '@/src/application/use-cases/users/ban-user.use-case'; 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 { 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 { 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 { 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() { export function createUsersModule() {
@ -169,7 +168,7 @@ export function createUsersModule() {
]); ]);
usersModule usersModule
.bind(DI_SYMBOLS.IGetUserByUserNameController) .bind(DI_SYMBOLS.IGetUserByUsernameController)
.toHigherOrderFunction(getUserByUsernameController, [ .toHigherOrderFunction(getUserByUsernameController, [
DI_SYMBOLS.IInstrumentationService, DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.IGetUserByUserNameUseCase DI_SYMBOLS.IGetUserByUserNameUseCase
@ -179,14 +178,16 @@ export function createUsersModule() {
.bind(DI_SYMBOLS.IInviteUserController) .bind(DI_SYMBOLS.IInviteUserController)
.toHigherOrderFunction(inviteUserController, [ .toHigherOrderFunction(inviteUserController, [
DI_SYMBOLS.IInstrumentationService, DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.IInviteUserUseCase DI_SYMBOLS.IInviteUserUseCase,
DI_SYMBOLS.IGetCurrentUserUseCase
]); ]);
usersModule usersModule
.bind(DI_SYMBOLS.ICreateUserController) .bind(DI_SYMBOLS.ICreateUserController)
.toHigherOrderFunction(createUserController, [ .toHigherOrderFunction(createUserController, [
DI_SYMBOLS.IInstrumentationService, DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.ICreateUserUseCase DI_SYMBOLS.ICreateUserUseCase,
DI_SYMBOLS.IGetCurrentUserUseCase
]); ]);
usersModule usersModule

View File

@ -32,6 +32,11 @@ import { IInviteUserController } from '@/src/interface-adapters/controllers/user
import { IUpdateUserController } from '@/src/interface-adapters/controllers/users/update-user-controller'; import { IUpdateUserController } from '@/src/interface-adapters/controllers/users/update-user-controller';
import { ICreateUserController } from '@/src/interface-adapters/controllers/users/create-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 { 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 = { export const DI_SYMBOLS = {
// Services // Services
@ -48,6 +53,8 @@ export const DI_SYMBOLS = {
ISignUpUseCase: Symbol.for('ISignUpUseCase'), ISignUpUseCase: Symbol.for('ISignUpUseCase'),
IVerifyOtpUseCase: Symbol.for('IVerifyOtpUseCase'), IVerifyOtpUseCase: Symbol.for('IVerifyOtpUseCase'),
ISignOutUseCase: Symbol.for('ISignOutUseCase'), ISignOutUseCase: Symbol.for('ISignOutUseCase'),
ISendMagicLinkUseCase: Symbol.for('ISendMagicLinkUseCase'),
ISendPasswordRecoveryUseCase: Symbol.for('ISendPasswordRecoveryUseCase'),
IBanUserUseCase: Symbol.for('IBanUserUseCase'), IBanUserUseCase: Symbol.for('IBanUserUseCase'),
IUnbanUserUseCase: Symbol.for('IUnbanUserUseCase'), IUnbanUserUseCase: Symbol.for('IUnbanUserUseCase'),
@ -65,6 +72,8 @@ export const DI_SYMBOLS = {
ISignInController: Symbol.for('ISignInController'), ISignInController: Symbol.for('ISignInController'),
ISignOutController: Symbol.for('ISignOutController'), ISignOutController: Symbol.for('ISignOutController'),
IVerifyOtpController: Symbol.for('IVerifyOtpController'), IVerifyOtpController: Symbol.for('IVerifyOtpController'),
ISendMagicLinkController: Symbol.for('ISendMagicLinkController'),
ISendPasswordRecoveryController: Symbol.for('ISendPasswordRecoveryController'),
IBanUserController: Symbol.for('IBanUserController'), IBanUserController: Symbol.for('IBanUserController'),
IUnbanUserController: Symbol.for('IUnbanUserController'), IUnbanUserController: Symbol.for('IUnbanUserController'),
@ -72,7 +81,7 @@ export const DI_SYMBOLS = {
IGetUsersController: Symbol.for('IGetUsersController'), IGetUsersController: Symbol.for('IGetUsersController'),
IGetUserByIdController: Symbol.for('IGetUserByIdController'), IGetUserByIdController: Symbol.for('IGetUserByIdController'),
IGetUserByEmailController: Symbol.for('IGetUserByEmailController'), IGetUserByEmailController: Symbol.for('IGetUserByEmailController'),
IGetUserByUserNameController: Symbol.for('IGetUserByUserNameController'), IGetUserByUsernameController: Symbol.for('IGetUserByUsernameController'),
IInviteUserController: Symbol.for('IInviteUserController'), IInviteUserController: Symbol.for('IInviteUserController'),
ICreateUserController: Symbol.for('ICreateUserController'), ICreateUserController: Symbol.for('ICreateUserController'),
IUpdateUserController: Symbol.for('IUpdateUserController'), IUpdateUserController: Symbol.for('IUpdateUserController'),
@ -94,6 +103,8 @@ export interface DI_RETURN_TYPES {
ISignUpUseCase: ISignUpUseCase; ISignUpUseCase: ISignUpUseCase;
IVerifyOtpUseCase: IVerifyOtpUseCase; IVerifyOtpUseCase: IVerifyOtpUseCase;
ISignOutUseCase: ISignOutUseCase; ISignOutUseCase: ISignOutUseCase;
ISendMagicLinkUseCase: ISendMagicLinkUseCase;
ISendPasswordRecoveryUseCase: ISendPasswordRecoveryUseCase;
IBanUserUseCase: IBanUserUseCase; IBanUserUseCase: IBanUserUseCase;
IUnbanUserUseCase: IUnbanUserUseCase; IUnbanUserUseCase: IUnbanUserUseCase;
@ -111,14 +122,16 @@ export interface DI_RETURN_TYPES {
ISignInController: ISignInController; ISignInController: ISignInController;
IVerifyOtpController: IVerifyOtpController; IVerifyOtpController: IVerifyOtpController;
ISignOutController: ISignOutController; ISignOutController: ISignOutController;
ISendMagicLinkController: ISendMagicLinkController;
ISendPasswordRecoveryController: ISendPasswordRecoveryController;
IBanUserController: IBanUserController; IBanUserController: IBanUserController;
IUnbanUserController: IUnbanUserController; IUnbanUserController: IUnbanUserController;
IGetCurrentUserController: IGetCurrentUserController; IGetCurrentUserController: IGetCurrentUserController;
IGetUsersController: IGetUserByUsernameController; IGetUsersController: IGetUsersController;
IGetUserByIdController: IGetUserByIdController; IGetUserByIdController: IGetUserByIdController;
IGetUserByEmailController: IGetUserByEmailController; IGetUserByEmailController: IGetUserByEmailController;
IGetUserByUserNameController: IGetUserByUsernameController; IGetUserByUsernameController: IGetUserByUsernameController;
IInviteUserController: IInviteUserController; IInviteUserController: IInviteUserController;
ICreateUserController: ICreateUserController; ICreateUserController: ICreateUserController;
IUpdateUserController: IUpdateUserController; IUpdateUserController: IUpdateUserController;

View File

@ -2,24 +2,24 @@ import { PrismaClient } from "@prisma/client";
const prismaClientSingleton = () => { const prismaClientSingleton = () => {
return new PrismaClient({ return new PrismaClient({
log: [ // log: [
{ // {
emit: 'event', // emit: 'event',
level: 'query', // level: 'query',
}, // },
{ // {
emit: 'stdout', // emit: 'stdout',
level: 'error', // level: 'error',
}, // },
{ // {
emit: 'stdout', // emit: 'stdout',
level: 'info', // level: 'info',
}, // },
{ // {
emit: 'stdout', // emit: 'stdout',
level: 'warn', // level: 'warn',
}, // },
], // ],
}) })
}; };
@ -33,8 +33,8 @@ export default db;
if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = db; if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = db;
db.$on('query', (e) => { // db.$on('query', (e) => {
console.log('Query: ' + e.query) // console.log('Query: ' + e.query)
console.log('Params: ' + e.params) // console.log('Params: ' + e.params)
console.log('Duration: ' + e.duration + 'ms') // console.log('Duration: ' + e.duration + 'ms')
}) // })

View File

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

View File

@ -1,740 +1,25 @@
import { createAdminClient } from "@/app/_utils/supabase/admin"; import { createAdminClient } from "@/app/_utils/supabase/admin";
import { createClient } from "@/app/_utils/supabase/client"; 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 { 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 { export interface IUsersRepository {
getUsers(): Promise<User[]>; getUsers(): Promise<IUserSchema[]>;
getCurrentUser(): Promise<User>; getCurrentUser(): Promise<IUserSchema>;
getUserById(id: string): Promise<User | undefined>; getUserById(credential: ICredentialGetUserByIdSchema): Promise<IUserSchema | undefined>;
getUserByUsername(username: string): Promise<User | undefined>; getUserByUsername(credential: ICredentialGetUserByUsernameSchema): Promise<IUserSchema | undefined>;
getUserByEmail(email: string): Promise<User | undefined>; getUserByEmail(credential: ICredentialGetUserByEmailSchema): Promise<IUserSchema | undefined>;
createUser(input: CreateUser, tx?: ITransaction): Promise<User>; createUser(input: ICreateUserSchema, tx?: ITransaction): Promise<IUserSchema>;
inviteUser(email: string, tx?: ITransaction): Promise<User>; inviteUser(credential: ICredentialsInviteUserSchema, tx?: ITransaction): Promise<IUserSchema>;
updateUser(id: string, input: Partial<UpdateUser>, tx?: ITransaction): Promise<User>; updateUser(credential: ICredentialUpdateUserSchema, input: Partial<IUpdateUserSchema>, tx?: ITransaction): Promise<IUserSchema>;
deleteUser(id: string, tx?: ITransaction): Promise<User>; deleteUser(credential: ICredentialsDeleteUserSchema, tx?: ITransaction): Promise<IUserSchema>;
banUser(id: string, ban_duration: string, tx?: ITransaction): Promise<User>; banUser(credential: ICredentialsBanUserSchema, input: IBanUserSchema, tx?: ITransaction): Promise<IUserSchema>;
unbanUser(id: string, tx?: ITransaction): Promise<User>; unbanUser(credential: ICredentialsUnbanUserSchema, tx?: ITransaction): Promise<IUserSchema>;
} }
// 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);
// }
// }

View File

@ -1,18 +1,20 @@
import { AuthResult } from "@/src/entities/models/auth/auth-result.model" 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 { Session } from "@/src/entities/models/auth/session.model"
import { SignInFormData, SignInPasswordless, SignInWithPassword } from "@/src/entities/models/auth/sign-in.model" import { TSignInSchema, ISignInWithPasswordSchema, ISignInPasswordlessSchema } from "@/src/entities/models/auth/sign-in.model"
import { SignUpWithEmail, SignUpWithPhone } from "@/src/entities/models/auth/sign-up.model" import { SignUpWithEmailSchema, SignUpWithPhoneSchema, TSignUpSchema, ISignUpWithEmailSchema, ISignUpWithPhoneSchema } from "@/src/entities/models/auth/sign-up.model"
import { VerifyOtpFormData } from "@/src/entities/models/auth/verify-otp.model" import { IVerifyOtpSchema } from "@/src/entities/models/auth/verify-otp.model"
import { User } from "@/src/entities/models/users/users.model" import { IUserSchema } from "@/src/entities/models/users/users.model"
export interface IAuthenticationService { export interface IAuthenticationService {
signInPasswordless(credentials: SignInPasswordless): Promise<void> signInPasswordless(credentials: ISignInPasswordlessSchema): Promise<void>
signInWithPassword(credentials: SignInWithPassword): Promise<void> SignInWithPasswordSchema(credentials: ISignInWithPasswordSchema): Promise<void>
signUpWithEmail(credentials: SignUpWithEmail): Promise<User> SignUpWithEmailSchema(credentials: ISignUpWithEmailSchema): Promise<IUserSchema>
signUpWithPhone(credentials: SignUpWithPhone): Promise<User> SignUpWithPhoneSchema(credentials: ISignUpWithPhoneSchema): Promise<IUserSchema>
getSession(): Promise<Session | null> getSession(): Promise<Session | null>
signOut(): Promise<void> signOut(): Promise<void>
sendMagicLink(email: string): Promise<void> sendMagicLink(credentials: ISendMagicLinkSchema): Promise<void>
sendPasswordRecovery(email: string): Promise<void> sendPasswordRecovery(credentials: ISendPasswordRecoverySchema): Promise<void>
verifyOtp(credentials: VerifyOtpFormData): Promise<void> verifyOtp(credentials: IVerifyOtpSchema): Promise<void>
} }

View File

@ -1,6 +1,6 @@
import type { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface" import type { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
import { AuthenticationError, UnauthenticatedError } from "@/src/entities/errors/auth"; 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 { IAuthenticationService } from "@/src/application/services/authentication.service.interface";
import { IUsersRepository } from "../../repositories/users.repository.interface"; import { IUsersRepository } from "../../repositories/users.repository.interface";
@ -12,11 +12,11 @@ export const signInUseCase =
authenticationService: IAuthenticationService, authenticationService: IAuthenticationService,
usersRepository: IUsersRepository usersRepository: IUsersRepository
) => ) =>
async (input: SignInPasswordless): Promise<void> => { async (input: ISignInPasswordlessSchema): Promise<void> => {
return instrumentationService.startSpan({ name: "signIn Use Case", op: "function" }, return instrumentationService.startSpan({ name: "signIn Use Case", op: "function" },
async () => { async () => {
const existingUser = await usersRepository.getUserByEmail(input.email) const existingUser = await usersRepository.getUserByEmail({ email: input.email })
if (!existingUser) { if (!existingUser) {
throw new UnauthenticatedError("User not found. Please tell your admin to create an account for you.") throw new UnauthenticatedError("User not found. Please tell your admin to create an account for you.")

View File

@ -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 { IUsersRepository } from "../../repositories/users.repository.interface"
import { IAuthenticationService } from "../../services/authentication.service.interface" import { IAuthenticationService } from "../../services/authentication.service.interface"
import { IInstrumentationService } from "../../services/instrumentation.service.interface" import { IInstrumentationService } from "../../services/instrumentation.service.interface"
import { AuthenticationError } from "@/src/entities/errors/auth" import { AuthenticationError } from "@/src/entities/errors/auth"
import { SignUpFormData } from "@/src/entities/models/auth/sign-up.model" import { ISignUpWithEmailSchema } from "@/src/entities/models/auth/sign-up.model"
import { AuthResult } from "@/src/entities/models/auth/auth-result.model"
export type ISignUpUseCase = ReturnType<typeof signUpUseCase> export type ISignUpUseCase = ReturnType<typeof signUpUseCase>
@ -13,21 +13,21 @@ export const signUpUseCase = (
instrumentationService: IInstrumentationService, instrumentationService: IInstrumentationService,
authenticationService: IAuthenticationService, authenticationService: IAuthenticationService,
usersRepository: IUsersRepository usersRepository: IUsersRepository
) => async (input: SignUpFormData): Promise<User> => { ) => async (input: ISignUpWithEmailSchema): Promise<IUserSchema> => {
return await instrumentationService.startSpan({ name: "signUp Use Case", op: "function" }, return await instrumentationService.startSpan({ name: "signUp Use Case", op: "function" },
async () => { async () => {
const existingUser = await usersRepository.getUserByEmail(input.email) const existingUser = await usersRepository.getUserByEmail({ email: input.email })
if (existingUser) { if (existingUser) {
throw new AuthenticationError("User already exists") throw new AuthenticationError("User already exists")
} }
const newUser = await authenticationService.signUpWithEmail({ const newUser = await authenticationService.SignUpWithEmailSchema({
email: input.email, email: input.email,
password: input.password password: input.password
}) })
await authenticationService.signInWithPassword({ await authenticationService.SignInWithPasswordSchema({
email: input.email, email: input.email,
password: input.password password: input.password
}) })

View File

@ -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 { IUsersRepository } from "../../repositories/users.repository.interface"
import { IAuthenticationService } from "../../services/authentication.service.interface" import { IAuthenticationService } from "../../services/authentication.service.interface"
import { IInstrumentationService } from "../../services/instrumentation.service.interface" import { IInstrumentationService } from "../../services/instrumentation.service.interface"
@ -12,7 +12,7 @@ export const verifyOtpUseCase = (
instrumentationService: IInstrumentationService, instrumentationService: IInstrumentationService,
authenticationService: IAuthenticationService, authenticationService: IAuthenticationService,
usersRepository: IUsersRepository usersRepository: IUsersRepository
) => async (input: VerifyOtpFormData): Promise<void> => { ) => async (input: VerifyOtpSchema): Promise<void> => {
return await instrumentationService.startSpan({ name: "verifyOtp Use Case", op: "function" }, return await instrumentationService.startSpan({ name: "verifyOtp Use Case", op: "function" },
async () => { async () => {
const user = await usersRepository.getUserByEmail(input.email) const user = await usersRepository.getUserByEmail(input.email)

View File

@ -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 { IUsersRepository } from "../../repositories/users.repository.interface"
import { IInstrumentationService } from "../../services/instrumentation.service.interface" import { IInstrumentationService } from "../../services/instrumentation.service.interface"
import { NotFoundError } from "@/src/entities/errors/common" 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 type IBanUserUseCase = ReturnType<typeof banUserUseCase>
export const banUserUseCase = ( export const banUserUseCase = (
instrumentationService: IInstrumentationService, instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository 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" }, return await instrumentationService.startSpan({ name: "banUser Use Case", op: "function" },
async () => { async () => {
const existingUser = await usersRepository.getUserById(id) const existingUser = await usersRepository.getUserById(credential)
if (!existingUser) { if (!existingUser) {
throw new NotFoundError("User not found") 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 return bannedUser
} }

View File

@ -2,8 +2,9 @@ import { AuthenticationError } from "@/src/entities/errors/auth"
import { IUsersRepository } from "../../repositories/users.repository.interface" import { IUsersRepository } from "../../repositories/users.repository.interface"
import { IAuthenticationService } from "../../services/authentication.service.interface" import { IAuthenticationService } from "../../services/authentication.service.interface"
import { IInstrumentationService } from "../../services/instrumentation.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 { InputParseError } from "@/src/entities/errors/common"
import { ICreateUserSchema } from "@/src/entities/models/users/create-user.model"
export type ICreateUserUseCase = ReturnType<typeof createUserUseCase> export type ICreateUserUseCase = ReturnType<typeof createUserUseCase>
@ -11,11 +12,11 @@ export type ICreateUserUseCase = ReturnType<typeof createUserUseCase>
export const createUserUseCase = ( export const createUserUseCase = (
instrumentationService: IInstrumentationService, instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository, usersRepository: IUsersRepository,
) => async (input: CreateUser): Promise<User> => { ) => async (input: ICreateUserSchema): Promise<IUserSchema> => {
return await instrumentationService.startSpan({ name: "createUser Use Case", op: "function" }, return await instrumentationService.startSpan({ name: "createUser Use Case", op: "function" },
async () => { async () => {
const existingUser = await usersRepository.getUserByEmail(input.email) const existingUser = await usersRepository.getUserByEmail({ email: input.email })
if (existingUser) { if (existingUser) {
throw new AuthenticationError("User already exists") throw new AuthenticationError("User already exists")

View File

@ -1,23 +1,24 @@
import { NotFoundError } from "@/src/entities/errors/common" import { NotFoundError } from "@/src/entities/errors/common"
import { IUsersRepository } from "../../repositories/users.repository.interface" import { IUsersRepository } from "../../repositories/users.repository.interface"
import { IInstrumentationService } from "../../services/instrumentation.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 { ICredentialsDeleteUserSchema } from "@/src/entities/models/users/delete-user.model"
export type IDeleteUserUseCase = ReturnType<typeof deleteUserUseCase> export type IDeleteUserUseCase = ReturnType<typeof deleteUserUseCase>
export const deleteUserUseCase = ( export const deleteUserUseCase = (
instrumentationService: IInstrumentationService, instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository usersRepository: IUsersRepository
) => async (id: string): Promise<User> => { ) => async (credential: ICredentialsDeleteUserSchema): Promise<IUserSchema> => {
return await instrumentationService.startSpan({ name: "deleteUser Use Case", op: "function" }, return await instrumentationService.startSpan({ name: "deleteUser Use Case", op: "function" },
async () => { async () => {
const user = await usersRepository.getUserById(id) const user = await usersRepository.getUserById(credential)
if (!user) { if (!user) {
throw new NotFoundError("User not found") throw new NotFoundError("User not found")
} }
const deletedUser = await usersRepository.deleteUser(id) const deletedUser = await usersRepository.deleteUser(credential)
return deletedUser return deletedUser
} }

View File

@ -1,7 +1,7 @@
import { NotFoundError } from "@/src/entities/errors/common" import { NotFoundError } from "@/src/entities/errors/common"
import { IUsersRepository } from "../../repositories/users.repository.interface" import { IUsersRepository } from "../../repositories/users.repository.interface"
import { IInstrumentationService } from "../../services/instrumentation.service.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" import { AuthenticationError } from "@/src/entities/errors/auth"
@ -10,7 +10,7 @@ export type IGetCurrentUserUseCase = ReturnType<typeof getCurrentUserUseCase>
export const getCurrentUserUseCase = ( export const getCurrentUserUseCase = (
instrumentationService: IInstrumentationService, instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository usersRepository: IUsersRepository
) => async (): Promise<User> => { ) => async (): Promise<IUserSchema> => {
return await instrumentationService.startSpan({ name: "getCurrentUser Use Case", op: "function" }, return await instrumentationService.startSpan({ name: "getCurrentUser Use Case", op: "function" },
async () => { async () => {

View File

@ -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 { IUsersRepository } from "../../repositories/users.repository.interface"
import { IInstrumentationService } from "../../services/instrumentation.service.interface" import { IInstrumentationService } from "../../services/instrumentation.service.interface"
import { NotFoundError } from "@/src/entities/errors/common" import { NotFoundError } from "@/src/entities/errors/common"
import { ICredentialGetUserByEmailSchema } from "@/src/entities/models/users/read-user.model"
export type IGetUserByEmailUseCase = ReturnType<typeof getUserByEmailUseCase> export type IGetUserByEmailUseCase = ReturnType<typeof getUserByEmailUseCase>
export const getUserByEmailUseCase = ( export const getUserByEmailUseCase = (
instrumentationService: IInstrumentationService, instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository usersRepository: IUsersRepository
) => async (email: string): Promise<User> => { ) => async (credential: ICredentialGetUserByEmailSchema): Promise<IUserSchema> => {
return await instrumentationService.startSpan({ name: "getUserByEmail Use Case", op: "function" }, return await instrumentationService.startSpan({ name: "getUserByEmail Use Case", op: "function" },
async () => { async () => {
const user = await usersRepository.getUserByEmail(email) const user = await usersRepository.getUserByEmail(credential)
if (!user) { if (!user) {
throw new NotFoundError("User not found") throw new NotFoundError("User not found")

View File

@ -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 { IUsersRepository } from "../../repositories/users.repository.interface"
import { IInstrumentationService } from "../../services/instrumentation.service.interface" import { IInstrumentationService } from "../../services/instrumentation.service.interface"
import { AuthenticationError } from "@/src/entities/errors/auth" import { AuthenticationError } from "@/src/entities/errors/auth"
import { NotFoundError } from "@/src/entities/errors/common" import { NotFoundError } from "@/src/entities/errors/common"
import { ICredentialGetUserByIdSchema } from "@/src/entities/models/users/read-user.model"
export type IGetUserByIdUseCase = ReturnType<typeof getUserByIdUseCase> export type IGetUserByIdUseCase = ReturnType<typeof getUserByIdUseCase>
@ -10,11 +11,11 @@ export type IGetUserByIdUseCase = ReturnType<typeof getUserByIdUseCase>
export const getUserByIdUseCase = ( export const getUserByIdUseCase = (
instrumentationService: IInstrumentationService, instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository usersRepository: IUsersRepository
) => async (id: string): Promise<User> => { ) => async (credential: ICredentialGetUserByIdSchema): Promise<IUserSchema> => {
return await instrumentationService.startSpan({ name: "getUserById Use Case", op: "function" }, return await instrumentationService.startSpan({ name: "getUserById Use Case", op: "function" },
async () => { async () => {
const user = await usersRepository.getUserById(id) const user = await usersRepository.getUserById(credential)
if (!user) { if (!user) {
throw new NotFoundError("User not found") throw new NotFoundError("User not found")

View File

@ -1,18 +1,19 @@
import { NotFoundError } from "@/src/entities/errors/common" import { NotFoundError } from "@/src/entities/errors/common"
import { IInstrumentationService } from "../../services/instrumentation.service.interface" import { IInstrumentationService } from "../../services/instrumentation.service.interface"
import { IUsersRepository } from "../../repositories/users.repository.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 type IGetUserByUsernameUseCase = ReturnType<typeof getUserByUsernameUseCase>
export const getUserByUsernameUseCase = ( export const getUserByUsernameUseCase = (
instrumentationService: IInstrumentationService, instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository usersRepository: IUsersRepository
) => async (username: string): Promise<User> => { ) => async (credential: ICredentialGetUserByUsernameSchema): Promise<IUserSchema> => {
return await instrumentationService.startSpan({ name: "getUserByUsername Use Case", op: "function" }, return await instrumentationService.startSpan({ name: "getUserByUsername Use Case", op: "function" },
async () => { async () => {
const user = await usersRepository.getUserByUsername(username) const user = await usersRepository.getUserByUsername(credential)
if (!user) { if (!user) {
throw new NotFoundError("User not found") throw new NotFoundError("User not found")

View File

@ -8,7 +8,7 @@ export const getUsersUseCase = (
instrumentationService: IInstrumentationService, instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository usersRepository: IUsersRepository
) => async (): Promise<User[]> => { ) => async (): Promise<User[]> => {
return await instrumentationService.startSpan({ name: "getgetUsers Use Case", op: "function" }, return instrumentationService.startSpan({ name: "getUsers Use Case", op: "function" },
async () => { async () => {
const users = await usersRepository.getUsers() const users = await usersRepository.getUsers()

View File

@ -2,7 +2,8 @@ import { AuthenticationError } from "@/src/entities/errors/auth"
import { IUsersRepository } from "../../repositories/users.repository.interface" import { IUsersRepository } from "../../repositories/users.repository.interface"
import { IAuthenticationService } from "../../services/authentication.service.interface" import { IAuthenticationService } from "../../services/authentication.service.interface"
import { IInstrumentationService } from "../../services/instrumentation.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> export type IInviteUserUseCase = ReturnType<typeof inviteUserUseCase>
@ -10,17 +11,16 @@ export type IInviteUserUseCase = ReturnType<typeof inviteUserUseCase>
export const inviteUserUseCase = ( export const inviteUserUseCase = (
instrumentationService: IInstrumentationService, instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository, usersRepository: IUsersRepository,
authenticationService: IAuthenticationService, ) => async (credential: ICredentialsInviteUserSchema): Promise<IUserSchema> => {
) => async (email: string): Promise<User> => {
return await instrumentationService.startSpan({ name: "inviteUser Use Case", op: "function" }, return await instrumentationService.startSpan({ name: "inviteUser Use Case", op: "function" },
async () => { async () => {
const existingUser = await usersRepository.getUserByEmail(email) const existingUser = await usersRepository.getUserByEmail(credential)
if (existingUser) { if (existingUser) {
throw new AuthenticationError("User already exists") throw new AuthenticationError("User already exists")
} }
const invitedUser = await usersRepository.inviteUser(email) const invitedUser = await usersRepository.inviteUser(credential)
if (!invitedUser) { if (!invitedUser) {
throw new AuthenticationError("User not invited") throw new AuthenticationError("User not invited")

View File

@ -1,23 +1,24 @@
import { NotFoundError } from "@/src/entities/errors/common" import { NotFoundError } from "@/src/entities/errors/common"
import { IUsersRepository } from "../../repositories/users.repository.interface" import { IUsersRepository } from "../../repositories/users.repository.interface"
import { IInstrumentationService } from "../../services/instrumentation.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 { ICredentialsUnbanUserSchema } from "@/src/entities/models/users/unban-user.model"
export type IUnbanUserUseCase = ReturnType<typeof unbanUserUseCase> export type IUnbanUserUseCase = ReturnType<typeof unbanUserUseCase>
export const unbanUserUseCase = ( export const unbanUserUseCase = (
instrumentationService: IInstrumentationService, instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository usersRepository: IUsersRepository
) => async (id: string): Promise<User> => { ) => async (credential: ICredentialsUnbanUserSchema): Promise<IUserSchema> => {
return await instrumentationService.startSpan({ name: "unbanUser Use Case", op: "function" }, return await instrumentationService.startSpan({ name: "unbanUser Use Case", op: "function" },
async () => { async () => {
const existingUser = await usersRepository.getUserById(id) const existingUser = await usersRepository.getUserById(credential)
if (!existingUser) { if (!existingUser) {
throw new NotFoundError("User not found") throw new NotFoundError("User not found")
} }
const unbanUser = await usersRepository.unbanUser(id) const unbanUser = await usersRepository.unbanUser(credential)
return unbanUser return unbanUser
} }

View File

@ -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 { IUsersRepository } from "../../repositories/users.repository.interface"
import { IInstrumentationService } from "../../services/instrumentation.service.interface" import { IInstrumentationService } from "../../services/instrumentation.service.interface"
import { NotFoundError } from "@/src/entities/errors/common" 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 type IUpdateUserUseCase = ReturnType<typeof updateUserUseCase>
export const updateUserUseCase = ( export const updateUserUseCase = (
instrumentationService: IInstrumentationService, instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository 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" }, return await instrumentationService.startSpan({ name: "updateUser Use Case", op: "function" },
async () => { async () => {
const existingUser = await usersRepository.getUserById(id) const existingUser = await usersRepository.getUserById(credential)
if (!existingUser) { if (!existingUser) {
throw new NotFoundError("User not found") throw new NotFoundError("User not found")
} }
const updatedUser = await usersRepository.updateUser(id, input) const updatedUser = await usersRepository.updateUser(credential, input)
return updatedUser return updatedUser
} }

View File

@ -1,18 +1,27 @@
export class DatabaseOperationError extends Error { export class DatabaseOperationError extends Error {
constructor(message: string, options?: ErrorOptions) { constructor(message: string, options?: ErrorOptions) {
super(message, options); super(message, options);
}
} }
}
export class NotFoundError extends Error {
constructor(message: string, options?: ErrorOptions) { export class NotFoundError extends Error {
super(message, options); constructor(message: string, options?: ErrorOptions) {
} super(message, options);
} }
}
export class InputParseError extends Error {
constructor(message: string, options?: ErrorOptions) { export class InputParseError extends Error {
super(message, options); 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;
}
}

View File

@ -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: "",
}

View File

@ -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: "",
}

View File

@ -6,40 +6,40 @@ export const SignInSchema = z.object({
.string() .string()
.min(1, { message: "Email is required" }) .min(1, { message: "Email is required" })
.email({ message: "Please enter a valid email address" }), .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(), phone: z.string().optional(),
}); });
// Export the type derived from the schema // 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, email: true,
password: true, password: true,
phone: true phone: true
}) })
export type ISignInWithPasswordSchema = z.infer<typeof SignInWithPasswordSchema>
// Default values for the form // Default values for the form
export const defaultSignInWithPasswordValues: SignInWithPassword = { export const defaulISignInWithPasswordSchemaValues: ISignInWithPasswordSchema = {
email: "", email: "",
password: "", password: "",
phone: "" phone: ""
}; };
export type SignInWithPassword = z.infer<typeof SignInWithPassword>
export const SignInPasswordlessSchema = SignInSchema.pick({ export const SignInPasswordlessSchema = SignInSchema.pick({
email: true, email: true,
}) })
export type ISignInPasswordlessSchema = z.infer<typeof SignInPasswordlessSchema>
// Default values for the form // Default values for the form
export const defaultSignInPasswordlessValues: SignInPasswordless = { export const defaulISignInPasswordlessSchemaValues: ISignInPasswordlessSchema = {
email: "", email: "",
} }
export type SignInPasswordless = z.infer<typeof SignInPasswordlessSchema>
// Define the sign-in response schema using Zod // Define the sign-in response schema using Zod
export const SignInResponseSchema = z.object({ export const SignInResponseSchema = z.object({
success: z.boolean(), success: z.boolean(),

View File

@ -5,32 +5,42 @@ export const SignUpSchema = z.object({
.string() .string()
.min(1, { message: "Email is required" }) .min(1, { message: "Email is required" })
.email({ message: "Please enter a valid email address" }), .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(), 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, email: true,
password: true, password: true,
}) })
export const defaultSignUpWithEmailValues: SignUpWithEmail = { export type ISignUpWithEmailSchema = z.infer<typeof SignUpWithEmailSchema>
export const defaulISignUpWithEmailSchemaValues: ISignUpWithEmailSchema = {
email: "", email: "",
password: "", password: "",
} }
export type SignUpWithEmail = z.infer<typeof SignUpWithEmail> export const SignUpWithPhoneSchema = SignUpSchema.pick({
export const SignUpWithPhone = SignUpSchema.pick({
phone: true, phone: true,
password: true, password: true,
}) })
export const defaultSignUpWithPhoneValues: SignUpWithPhone = { export type ISignUpWithPhoneSchema = z.infer<typeof SignUpWithPhoneSchema>
export const defaulISignUpWithPhoneSchemaValues: ISignUpWithPhoneSchema = {
phone: "", phone: "",
password: "", 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: "",
}

View File

@ -5,9 +5,9 @@ export const verifyOtpSchema = z.object({
token: z.string().length(6, { message: "OTP must be 6 characters long" }), 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: "", email: "",
token: "", token: "",
}; };

View File

@ -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",
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -66,7 +66,7 @@ export const UserSchema = z.object({
.optional(), .optional(),
}); });
export type User = z.infer<typeof UserSchema>; export type IUserSchema = z.infer<typeof UserSchema>;
export const ProfileSchema = z.object({ export const ProfileSchema = z.object({
id: z.string(), id: z.string(),
@ -80,59 +80,30 @@ export const ProfileSchema = z.object({
birth_date: z.string().optional(), birth_date: z.string().optional(),
}); });
export type Profile = z.infer<typeof ProfileSchema>; export type IProfileSchema = z.infer<typeof ProfileSchema>;
export const CreateUserSchema = z.object({ // export type UserFilterOptions = {
email: z.string().email(), // email: string
password: z.string().min(8), // phone: string
phone: z.string().optional(), // lastSignIn: string
email_confirm: z.boolean().optional(), // 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({ export type IUserFilterOptionsSchema = z.infer<typeof UserFilterOptionsSchema>;
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 UserResponse = export type UserResponse =
| { | {
data: { data: {
user: User; user: IUserSchema;
}; };
error: null; error: null;
} }

View File

@ -3,11 +3,18 @@ import { ICrashReporterService } from "@/src/application/services/crash-reporter
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
import { createAdminClient } from "@/app/_utils/supabase/admin"; import { createAdminClient } from "@/app/_utils/supabase/admin";
import { createClient as createServerClient } from "@/app/_utils/supabase/server"; 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 { ITransaction } from "@/src/entities/models/transaction.interface";
import db from "@/prisma/db"; import db from "@/prisma/db";
import { DatabaseOperationError, NotFoundError } from "@/src/entities/errors/common"; import { DatabaseOperationError, NotFoundError } from "@/src/entities/errors/common";
import { AuthenticationError } from "@/src/entities/errors/auth"; 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 { export class UsersRepository implements IUsersRepository {
constructor( constructor(
@ -17,7 +24,7 @@ export class UsersRepository implements IUsersRepository {
private readonly supabaseServer = createServerClient() private readonly supabaseServer = createServerClient()
) { } ) { }
async getUsers(): Promise<User[]> { async getUsers(): Promise<IUserSchema[]> {
return await this.instrumentationService.startSpan({ return await this.instrumentationService.startSpan({
name: "UsersRepository > getUsers", name: "UsersRepository > getUsers",
}, async () => { }, async () => {
@ -40,7 +47,7 @@ export class UsersRepository implements IUsersRepository {
) )
if (!users) { if (!users) {
throw new NotFoundError("Users not found"); return [];
} }
return users; 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({ return await this.instrumentationService.startSpan({
name: "UsersRepository > getUserById", name: "UsersRepository > getUserById",
}, async () => { }, async () => {
try { try {
const query = db.users.findUnique({ const query = db.users.findUnique({
where: { where: {
id, id: credential.id,
}, },
include: { include: {
profile: true, profile: true,
@ -66,7 +73,7 @@ export class UsersRepository implements IUsersRepository {
}) })
const user = await this.instrumentationService.startSpan({ 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", op: "db:query",
attributes: { "system": "prisma" }, attributes: { "system": "prisma" },
}, },
@ -81,7 +88,7 @@ export class UsersRepository implements IUsersRepository {
return { return {
...user, ...user,
id, id: credential.id,
}; };
} catch (err) { } catch (err) {
this.crashReporterService.report(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({ return await this.instrumentationService.startSpan({
name: "UsersRepository > getUserByUsername", name: "UsersRepository > getUserByUsername",
}, async () => { }, async () => {
@ -98,7 +105,7 @@ export class UsersRepository implements IUsersRepository {
const query = db.users.findFirst({ const query = db.users.findFirst({
where: { where: {
profile: { profile: {
username, username: credential.username,
}, },
}, },
include: { include: {
@ -107,7 +114,7 @@ export class UsersRepository implements IUsersRepository {
}) })
const user = await this.instrumentationService.startSpan({ 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", op: "db:query",
attributes: { "system": "prisma" }, 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({ return await this.instrumentationService.startSpan({
name: "UsersRepository > getUserByEmail", name: "UsersRepository > getUserByEmail",
}, async () => { }, async () => {
@ -138,7 +145,7 @@ export class UsersRepository implements IUsersRepository {
const query = db.users.findUnique({ const query = db.users.findUnique({
where: { where: {
email, email: credential.email,
}, },
include: { include: {
profile: true, profile: true,
@ -146,7 +153,7 @@ export class UsersRepository implements IUsersRepository {
}) })
const user = await this.instrumentationService.startSpan({ 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", op: "db:query",
attributes: { "system": "prisma" }, attributes: { "system": "prisma" },
}, },
@ -156,7 +163,7 @@ export class UsersRepository implements IUsersRepository {
) )
if (!user) { if (!user) {
throw new NotFoundError("User not found"); return undefined;
} }
return user; return user;
@ -167,7 +174,7 @@ export class UsersRepository implements IUsersRepository {
}) })
} }
async getCurrentUser(): Promise<User> { async getCurrentUser(): Promise<IUserSchema> {
return await this.instrumentationService.startSpan({ return await this.instrumentationService.startSpan({
name: "UsersRepository > getCurrentUser", name: "UsersRepository > getCurrentUser",
}, async () => { }, async () => {
@ -176,7 +183,7 @@ export class UsersRepository implements IUsersRepository {
const query = supabase.auth.getUser(); 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", name: "UsersRepository > getCurrentUser > supabase.auth.getUser",
op: "db:query", op: "db:query",
attributes: { "system": "supabase.auth" }, attributes: { "system": "supabase.auth" },
@ -190,14 +197,11 @@ export class UsersRepository implements IUsersRepository {
throw new AuthenticationError("Failed to get current user"); throw new AuthenticationError("Failed to get current user");
} }
if (!data) { if (!user) {
throw new NotFoundError("User not found"); throw new NotFoundError("User not found");
} }
return { return user;
...data,
id: data.user.id,
};
} catch (err) { } catch (err) {
this.crashReporterService.report(err); this.crashReporterService.report(err);
throw 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({ return await this.instrumentationService.startSpan({
name: "UsersRepository > createUser", name: "UsersRepository > createUser",
}, async () => { }, async () => {
try { try {
console.log("Create User");
const supabase = this.supabaseAdmin; 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({ const { data: { user } } = await this.instrumentationService.startSpan({
name: "UsersRepository > createUser > supabase.auth.admin.createUser", 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({ return await this.instrumentationService.startSpan({
name: "UsersRepository > inviteUser", name: "UsersRepository > inviteUser",
}, async () => { }, async () => {
try { try {
const supabase = this.supabaseAdmin; 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({ const { data: { user } } = await this.instrumentationService.startSpan({
name: "UsersRepository > inviteUser > supabase.auth.admin.inviteUserByEmail", 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({ return await this.instrumentationService.startSpan({
name: "UsersRepository > updateUser", name: "UsersRepository > updateUser",
}, async () => { }, async () => {
try { try {
const supabase = this.supabaseAdmin; const supabase = this.supabaseAdmin;
const queryUpdateSupabaseUser = supabase.auth.admin.updateUserById(id, { const queryUpdateSupabaseUser = supabase.auth.admin.updateUserById(credential.id, {
email: input.email, email: input.email,
email_confirm: input.email_confirmed_at, email_confirm: input.email_confirmed_at,
password: input.encrypted_password ?? undefined, password: input.encrypted_password ?? undefined,
@ -303,7 +314,7 @@ export class UsersRepository implements IUsersRepository {
const queryGetUser = db.users.findUnique({ const queryGetUser = db.users.findUnique({
where: { where: {
id, id: credential.id,
}, },
include: { include: {
profile: true, profile: true,
@ -326,7 +337,7 @@ export class UsersRepository implements IUsersRepository {
const queryUpdateUser = db.users.update({ const queryUpdateUser = db.users.update({
where: { where: {
id, id: credential.id,
}, },
data: { data: {
role: input.role || user.role, role: input.role || user.role,
@ -369,7 +380,7 @@ export class UsersRepository implements IUsersRepository {
return { return {
...updatedUser, ...updatedUser,
id, id: credential.id,
}; };
} catch (err) { } 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({ return await this.instrumentationService.startSpan({
name: "UsersRepository > deleteUser", name: "UsersRepository > deleteUser",
}, async () => { }, async () => {
try { try {
const supabase = this.supabaseAdmin; 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({ const { data: user, error } = await this.instrumentationService.startSpan({
name: "UsersRepository > deleteUser > supabase.auth.admin.deleteUser", name: "UsersRepository > deleteUser > supabase.auth.admin.deleteUser",
@ -404,7 +415,7 @@ export class UsersRepository implements IUsersRepository {
return { return {
...user, ...user,
id id: credential.id,
}; };
} catch (err) { } 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({ return await this.instrumentationService.startSpan({
name: "UsersRepository > banUser", name: "UsersRepository > banUser",
}, async () => { }, async () => {
try { try {
const supabase = this.supabaseAdmin; const supabase = this.supabaseAdmin;
const query = supabase.auth.admin.updateUserById(id, { const query = supabase.auth.admin.updateUserById(credential.id, {
ban_duration: ban_duration ?? "100h", ban_duration: input.ban_duration ?? "24h",
}) })
const { data: user, error } = await this.instrumentationService.startSpan({ const { data: user, error } = await this.instrumentationService.startSpan({
@ -441,7 +452,7 @@ export class UsersRepository implements IUsersRepository {
return { return {
...user, ...user,
id id: credential.id,
}; };
} catch (err) { } 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({ return await this.instrumentationService.startSpan({
name: "UsersRepository > unbanUser", name: "UsersRepository > unbanUser",
}, async () => { }, async () => {
try { try {
const supabase = this.supabaseAdmin; const supabase = this.supabaseAdmin;
const query = supabase.auth.admin.updateUserById(id, { const query = supabase.auth.admin.updateUserById(credential.id, {
ban_duration: "none", ban_duration: "none",
}) })
@ -479,7 +490,7 @@ export class UsersRepository implements IUsersRepository {
return { return {
...user, ...user,
id id: credential.id,
}; };
} catch (err) { } catch (err) {

View File

@ -6,11 +6,13 @@ import { IAuthenticationService } from "@/src/application/services/authenticatio
import { ICrashReporterService } from "@/src/application/services/crash-reporter.service.interface"; import { ICrashReporterService } from "@/src/application/services/crash-reporter.service.interface";
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
import { AuthenticationError } from "@/src/entities/errors/auth"; 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 { Session } from "@/src/entities/models/auth/session.model";
import { SignInPasswordless, SignInWithPassword } from "@/src/entities/models/auth/sign-in.model"; import { SignInWithPasswordSchema, ISignInPasswordlessSchema, ISignInWithPasswordSchema } from "@/src/entities/models/auth/sign-in.model";
import { SignUpWithEmail, SignUpWithPhone } from "@/src/entities/models/auth/sign-up.model"; import { SignUpWithEmailSchema, SignUpWithPhoneSchema, ISignUpWithEmailSchema, ISignUpWithPhoneSchema } from "@/src/entities/models/auth/sign-up.model";
import { VerifyOtpFormData } from "@/src/entities/models/auth/verify-otp.model"; import { IVerifyOtpSchema } from "@/src/entities/models/auth/verify-otp.model";
import { User } from "@/src/entities/models/users/users.model"; import { IUserSchema } from "@/src/entities/models/users/users.model";
export class AuthenticationService implements IAuthenticationService { export class AuthenticationService implements IAuthenticationService {
constructor( constructor(
@ -21,7 +23,7 @@ export class AuthenticationService implements IAuthenticationService {
private readonly supabaseServer = createClient() private readonly supabaseServer = createClient()
) { } ) { }
async signInPasswordless(credentials: SignInPasswordless): Promise<void> { async signInPasswordless(credentials: ISignInPasswordlessSchema): Promise<void> {
return await this.instrumentationService.startSpan({ return await this.instrumentationService.startSpan({
name: "signInPasswordless Use Case", name: "signInPasswordless Use Case",
}, async () => { }, 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({ return await this.instrumentationService.startSpan({
name: "signInWithPassword Use Case", name: "SignInWithPasswordSchema Use Case",
}, async () => { }, async () => {
try { try {
const supabase = await this.supabaseServer 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({ return await this.instrumentationService.startSpan({
name: "signUpWithEmail Use Case", name: "SignUpWithEmailSchema Use Case",
}, async () => { }, async () => {
try { try {
const supabase = await this.supabaseServer 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."); 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({ return await this.instrumentationService.startSpan({
name: "sendMagicLink Use Case", name: "sendMagicLink Use Case",
}, async () => { }, async () => {
try { try {
const supabase = await this.supabaseServer const supabase = await this.supabaseServer
const magicLink = supabase.auth.signInWithOtp({ email }) const magicLink = supabase.auth.signInWithOtp({ email: credentials.email })
await this.instrumentationService.startSpan({ await this.instrumentationService.startSpan({
name: "supabase.auth.signIn", 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({ return await this.instrumentationService.startSpan({
name: "sendPasswordRecovery Use Case", name: "sendPasswordRecovery Use Case",
}, async () => { }, async () => {
try { try {
const supabase = await this.supabaseServer 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`, 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({ return await this.instrumentationService.startSpan({
name: "verifyOtp Use Case", name: "verifyOtp Use Case",
}, async () => { }, async () => {

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import { InputParseError } from "@/src/entities/errors/common";
// Sign In Controller // Sign In Controller
const signInInputSchema = z.object({ 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> export type ISignInController = ReturnType<typeof signInController>
@ -20,7 +20,7 @@ export const signInController =
const { data, error: inputParseError } = signInInputSchema.safeParse(input) const { data, error: inputParseError } = signInInputSchema.safeParse(input)
if (inputParseError) { if (inputParseError) {
throw new InputParseError("Invalid data", { cause: inputParseError }) throw new InputParseError(inputParseError.errors[0].message)
} }
return await signInUseCase({ return await signInUseCase({

View File

@ -1,27 +1,38 @@
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
import { IBanUserUseCase } from "@/src/application/use-cases/users/ban-user.use-case"; 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 { InputParseError } from "@/src/entities/errors/common";
import { BanDurationSchema, ICredentialsBanUserSchema } from "@/src/entities/models/users/ban-user.model";
import { z } from "zod"; import { z } from "zod";
const inputSchema = z.object({ const inputSchema = z.object({
id: z.string(), ban_duration: BanDurationSchema
ban_duration: z.string()
}) })
export type IBanUserController = ReturnType<typeof banUserController> export type IBanUserController = ReturnType<typeof banUserController>
export const banUserController = ( export const banUserController = (
instrumentationService: IInstrumentationService, 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 () => { 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); const { data, error: inputParseError } = inputSchema.safeParse(input);
if (inputParseError) { if (inputParseError) {
throw new InputParseError("Invalid data", { cause: 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 });
}) })
} }

View File

@ -2,11 +2,24 @@ import { IUsersRepository } from "@/src/application/repositories/users.repositor
import { IAuthenticationService } from "@/src/application/services/authentication.service.interface" import { IAuthenticationService } from "@/src/application/services/authentication.service.interface"
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface" import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
import { ICreateUserUseCase } from "@/src/application/use-cases/users/create-user.use-case" 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 { UnauthenticatedError } from "@/src/entities/errors/auth"
import { InputParseError } from "@/src/entities/errors/common" 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" 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 const inputSchema = CreateUserSchema
export type ICreateUserController = ReturnType<typeof createUserController> export type ICreateUserController = ReturnType<typeof createUserController>
@ -14,20 +27,23 @@ export type ICreateUserController = ReturnType<typeof createUserController>
export const createUserController = ( export const createUserController = (
instrumentationService: IInstrumentationService, instrumentationService: IInstrumentationService,
createUserUseCase: ICreateUserUseCase, createUserUseCase: ICreateUserUseCase,
authenticationService: IAuthenticationService getCurrentUserUseCase: IGetCurrentUserUseCase
) => async (input: Partial<z.infer<typeof inputSchema>>) => { ) => 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) { if (!session) {
throw new UnauthenticatedError("Must be logged in to create a todo") 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) { if (inputParseError) {
throw new InputParseError("Invalid data", { cause: inputParseError }) throw new InputParseError(inputParseError.errors[0].message)
} }
return await createUserUseCase(data); return await createUserUseCase(data);
})
} }

View File

@ -1,7 +1,10 @@
import { IUsersRepository } from "@/src/application/repositories/users.repository.interface"
import { IAuthenticationService } from "@/src/application/services/authentication.service.interface" import { IAuthenticationService } from "@/src/application/services/authentication.service.interface"
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface" import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
import { IDeleteUserUseCase } from "@/src/application/use-cases/users/delete-user.use-case" 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 { UnauthenticatedError } from "@/src/entities/errors/auth"
import { ICredentialsDeleteUserSchema } from "@/src/entities/models/users/delete-user.model"
export type IDeleteUserController = ReturnType<typeof deleteUserController> export type IDeleteUserController = ReturnType<typeof deleteUserController>
@ -9,17 +12,17 @@ export const deleteUserController =
( (
instrumentationService: IInstrumentationService, instrumentationService: IInstrumentationService,
deleteUserUseCase: IDeleteUserUseCase, deleteUserUseCase: IDeleteUserUseCase,
authenticationService: IAuthenticationService getCurrentUserUseCase: IGetCurrentUserUseCase
) => ) =>
async (id: string) => { async (credential: ICredentialsDeleteUserSchema) => {
return await instrumentationService.startSpan({ name: "deleteUser Controller" }, async () => { return await instrumentationService.startSpan({ name: "deleteUser Controller" }, async () => {
const session = await authenticationService.getSession() const session = await getCurrentUserUseCase()
if (!session) { 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);
}) })
} }

View File

@ -22,6 +22,6 @@ export const getUserByEmailController =
throw new InputParseError("Invalid data", { cause: inputParseError }); throw new InputParseError("Invalid data", { cause: inputParseError });
} }
return await getUserByEmailUseCase(data.email); return await getUserByEmailUseCase({ email: data.email });
}) })
} }

View File

@ -21,6 +21,6 @@ export const getUserByIdController = (
throw new InputParseError("Invalid data", { cause: inputParseError }); throw new InputParseError("Invalid data", { cause: inputParseError });
} }
return await getUserByIdUseCase(data.id); return await getUserByIdUseCase({ id: data.id });
}) })
} }

View File

@ -22,6 +22,6 @@ export const getUserByUsernameController =
throw new InputParseError("Invalid data", { cause: inputParseError }); throw new InputParseError("Invalid data", { cause: inputParseError });
} }
return await getUserByUsernameUseCase(data.username); return await getUserByUsernameUseCase({ username: data.username });
}) })
} }

View File

@ -1,15 +1,16 @@
import { IUsersRepository } from "@/src/application/repositories/users.repository.interface" import { IUsersRepository } from "@/src/application/repositories/users.repository.interface"
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.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 = export const getUsersController =
( (
instrumentationService: IInstrumentationService, instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository getUsersUseCase: IGetUsersUseCase
) => ) =>
async () => { async () => {
return await instrumentationService.startSpan({ name: "getgetUsers Controller" }, async () => { return await instrumentationService.startSpan({ name: "geIGetUsers Controller" }, async () => {
return await usersRepository.getUsers(); return await getUsersUseCase()
}) })
} }

View File

@ -1,4 +1,5 @@
import { IInstrumentationService } from "@/src/application/services/instrumentation.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 { IInviteUserUseCase } from "@/src/application/use-cases/users/invite-user.use-case"; import { IInviteUserUseCase } from "@/src/application/use-cases/users/invite-user.use-case";
import { InputParseError } from "@/src/entities/errors/common"; import { InputParseError } from "@/src/entities/errors/common";
import { z } from "zod"; import { z } from "zod";
@ -12,17 +13,25 @@ export type IInviteUserController = ReturnType<typeof inviteUserController>
export const inviteUserController = export const inviteUserController =
( (
instrumentationService: IInstrumentationService, instrumentationService: IInstrumentationService,
inviteUserUseCase: IInviteUserUseCase inviteUserUseCase: IInviteUserUseCase,
getCurrentUserUseCase: IGetCurrentUserUseCase
) => ) =>
async (input: Partial<z.infer<typeof inputSchema>>) => { async (input: Partial<z.infer<typeof inputSchema>>) => {
return await instrumentationService.startSpan({ name: "inviteUser Controller" }, async () => { 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); const { data, error: inputParseError } = inputSchema.safeParse(input);
if (inputParseError) { if (inputParseError) {
throw new InputParseError("Invalid data", { cause: inputParseError }); throw new InputParseError("Invalid data", { cause: inputParseError });
} }
return await inviteUserUseCase(data.email); return await inviteUserUseCase({ email: data.email });
}) })
} }

View File

@ -1,4 +1,5 @@
import { IInstrumentationService } from "@/src/application/services/instrumentation.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 { IUnbanUserUseCase } from "@/src/application/use-cases/users/unban-user.use-case"; import { IUnbanUserUseCase } from "@/src/application/use-cases/users/unban-user.use-case";
import { InputParseError } from "@/src/entities/errors/common"; import { InputParseError } from "@/src/entities/errors/common";
import { z } from "zod"; import { z } from "zod";
@ -11,16 +12,24 @@ export type IUnbanUserController = ReturnType<typeof unbanUserController>
export const unbanUserController = ( export const unbanUserController = (
instrumentationService: IInstrumentationService, instrumentationService: IInstrumentationService,
unbanUserUseCase: IUnbanUserUseCase unbanUserUseCase: IUnbanUserUseCase,
getCurrentUserUseCase: IGetCurrentUserUseCase
) => ) =>
async (input: Partial<z.infer<typeof inputSchema>>) => { async (input: Partial<z.infer<typeof inputSchema>>) => {
return await instrumentationService.startSpan({ name: "unbanUser Controller" }, async () => { 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); const { data, error: inputParseError } = inputSchema.safeParse(input);
if (inputParseError) { if (inputParseError) {
throw new InputParseError("Invalid data", { cause: inputParseError }); throw new InputParseError("Invalid data", { cause: inputParseError });
} }
return await unbanUserUseCase(data.id); return await unbanUserUseCase({ id: data.id });
}) })
} }

View File

@ -1,9 +1,11 @@
import { IAuthenticationService } from "@/src/application/services/authentication.service.interface"; import { IAuthenticationService } from "@/src/application/services/authentication.service.interface";
import { IInstrumentationService } from "@/src/application/services/instrumentation.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 { IUpdateUserUseCase } from "@/src/application/use-cases/users/update-user.use-case";
import { UnauthenticatedError } from "@/src/entities/errors/auth"; import { UnauthenticatedError } from "@/src/entities/errors/auth";
import { InputParseError } from "@/src/entities/errors/common"; 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"; import { z } from "zod";
const inputSchema = UpdateUserSchema const inputSchema = UpdateUserSchema
@ -14,12 +16,12 @@ export const updateUserController =
( (
instrumentationService: IInstrumentationService, instrumentationService: IInstrumentationService,
updateUserUseCase: IUpdateUserUseCase, updateUserUseCase: IUpdateUserUseCase,
authenticationService: IAuthenticationService getCurrentUserUseCase: IGetCurrentUserUseCase
) => ) =>
async (id: string, input: Partial<z.infer<typeof inputSchema>>,) => { async (id: string, input: Partial<z.infer<typeof inputSchema>>,) => {
return await instrumentationService.startSpan({ name: "updateUser Controller" }, async () => { return await instrumentationService.startSpan({ name: "updateUser Controller" }, async () => {
const session = await authenticationService.getSession() const session = await getCurrentUserUseCase()
if (!session) { if (!session) {
throw new UnauthenticatedError("Must be logged in to create a todo") 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 }) throw new InputParseError("Invalid data", { cause: inputParseError })
} }
return await updateUserUseCase(id, data); return await updateUserUseCase({ id }, data);
}) })
} }