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 { navData } from "@/prisma/data/nav";
import { TeamSwitcher } from "../../../_components/team-switcher";
import { useGetCurrentUserQuery } from "../dashboard/user-management/queries";
import { Profile, User } from "@/src/entities/models/users/users.model";
import { getCurrentUser } from "@/app/(pages)/(admin)/dashboard/user-management/action";
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const [user, setUser] = React.useState<User | null>(null);
const [isLoading, setIsLoading] = React.useState(true);
const { data: user, isPending, error } = useGetCurrentUserQuery()
React.useEffect(() => {
async function fetchUser() {
try {
setIsLoading(true);
const userData = await getCurrentUser();
setUser(userData.data.user);
} catch (error) {
console.error("Failed to fetch user:", error);
} finally {
setIsLoading(false);
}
}
// React.useEffect(() => {
// async function fetchUser() {
// try {
// setIsLoading(true);
// const userData = await getCurrentUser();
// setUser(userData.data.user);
// } catch (error) {
// console.error("Failed to fetch user:", error);
// } finally {
// setIsLoading(false);
// }
// }
fetchUser();
}, []);
// fetchUser();
// }, []);
return (
<Sidebar collapsible="icon" {...props}>
@ -51,7 +49,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<NavReports reports={navData.reports} />
</SidebarContent>
<SidebarFooter>
<NavUser user={user} />
<NavUser user={user ?? null} />
</SidebarFooter>
<SidebarRail />
</Sidebar>

View File

@ -24,13 +24,13 @@ import {
useSidebar,
} from "@/app/_components/ui/sidebar";
import { IconLogout, IconSettings, IconSparkles } from "@tabler/icons-react";
import type { User } from "@/src/entities/models/users/users.model";
import type { IUserSchema } from "@/src/entities/models/users/users.model";
// import { signOut } from "@/app/(pages)/(auth)/action";
import { SettingsDialog } from "../settings/setting-dialog";
import { useSignOutHandler } from "@/app/(pages)/(auth)/handler";
import { AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogCancel, AlertDialogAction } from "@/app/_components/ui/alert-dialog";
export function NavUser({ user }: { user: User | null }) {
export function NavUser({ user }: { user: IUserSchema | null }) {
const { isMobile } = useSidebar();
const [isDialogOpen, setIsDialogOpen] = useState(false);

View File

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

View File

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

View File

@ -1,155 +1,66 @@
import type React from "react";
import { useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/app/_components/ui/dialog";
import { Button } from "@/app/_components/ui/button";
import { Input } from "@/app/_components/ui/input";
import { Checkbox } from "@/app/_components/ui/checkbox";
import { createUser } from "@/app/(pages)/(admin)/dashboard/user-management/action";
import { toast } from "sonner";
import { Mail, Lock, Loader2, X } from "lucide-react";
import { useMutation } from "@tanstack/react-query";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/app/_components/ui/dialog"
import { Button } from "@/app/_components/ui/button"
import { Mail, Lock, Loader2 } from "lucide-react"
import { useAddUserDialogHandler } from "../handler"
import { ReactHookFormField } from "@/app/_components/react-hook-form-field"
interface AddUserDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onUserAdded: () => void;
open: boolean
onOpenChange: (open: boolean) => void
onUserAdded: () => void
}
export function AddUserDialog({
open,
onOpenChange,
onUserAdded,
}: AddUserDialogProps) {
const [formData, setFormData] = useState({
email: "",
password: "",
emailConfirm: true,
});
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const { mutate: createUserMutation, isPending } = useMutation({
mutationKey: ["createUser"],
mutationFn: createUser,
onSuccess: () => {
toast.success("User created successfully.");
onUserAdded();
onOpenChange(false);
setFormData({
email: "",
password: "",
emailConfirm: true,
});
},
onError: () => {
toast.error("Failed to create user.");
},
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await createUserMutation({
email: formData.email,
password: formData.password,
email_confirm: formData.emailConfirm,
});
} catch (error) {
toast.error("Failed to create user.");
return;
}
};
export function AddUserDialog({ open, onOpenChange, onUserAdded }: AddUserDialogProps) {
const {
register,
errors,
isPending,
handleSubmit,
handleOpenChange,
} = useAddUserDialogHandler({ onUserAdded, onOpenChange });
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md border-0 ">
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-md border-0">
<DialogHeader className="flex flex-row items-center justify-between space-y-0 space-x-4 pb-4">
<DialogTitle className="text-xl font-semibold ">
Create a new user
</DialogTitle>
{/* <Button
variant="ghost"
size="icon"
className="h-8 w-8 text-zinc-400 hover: hover:bg-zinc-800"
onClick={() => onOpenChange(false)}
>
<X className="h-4 w-4" />
</Button> */}
<DialogTitle className="text-xl font-semibold">Create a new user</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-3">
<div className="space-y-2">
<label htmlFor="email" className="text-sm text-zinc-400">
Email address
</label>
<div className="relative">
<Mail className="absolute left-3 top-2.5 h-5 w-5 text-zinc-500" />
<Input
id="email"
name="email"
type="email"
required
placeholder="user@example.com"
value={formData.email}
onChange={handleInputChange}
className="pl-10 placeholder:text-zinc-500 "
/>
</div>
</div>
<div className="space-y-2">
<label htmlFor="password" className="text-sm text-zinc-400">
User Password
</label>
<div className="relative">
<Lock className="absolute left-3 top-2.5 h-5 w-5 text-zinc-500" />
<Input
id="password"
name="password"
type="password"
required
placeholder="••••••••"
value={formData.password}
onChange={handleInputChange}
className="pl-10 placeholder:text-zinc-500 "
/>
</div>
</div>
<ReactHookFormField
label="Email address"
icon={Mail}
placeholder="user@example.com"
error={errors.email}
registration={register("email")}
/>
<ReactHookFormField
label="Password"
icon={Lock}
placeholder="••••••••"
type="password"
error={errors.password}
registration={register("password")}
/>
</div>
<div className="space-y-2">
<div className="flex items-center space-x-2">
{/* <div className="flex items-center space-x-2">
<Checkbox
id="email-confirm"
checked={formData.emailConfirm}
onCheckedChange={(checked) =>
setFormData((prev) => ({
...prev,
emailConfirm: checked as boolean,
}))
}
id="email_confirm"
{...register("email_confirm")}
className="border-zinc-700"
/>
<label htmlFor="email-confirm" className="text-sm ">
<label htmlFor="email_confirm" className="text-sm">
Auto Confirm User?
</label>
</div>
<p className="text-sm text-zinc-500 pl-6">
A confirmation email will not be sent when creating a user via
this form.
</div> */}
<p className="text-sm text-zinc-500">
A confirmation email will not be sent when creating a user via this form.
</p>
</div>
<Button type="submit" disabled={isPending} className="w-full ">
<Button type="submit" disabled={isPending} className="w-full">
{isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
@ -164,3 +75,4 @@ export function AddUserDialog({
</Dialog>
);
}

View File

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

View File

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

View File

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

View File

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

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";
import { Button } from "@/app/_components/ui/button";
import { Input } from "@/app/_components/ui/input";
import { Badge } from "@/app/_components/ui/badge";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuCheckboxItem,
} from "@/app/_components/ui/dropdown-menu";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { fetchUsers } from "@/app/(pages)/(admin)/dashboard/user-management/action";
import type { User } from "@/src/entities/models/users/users.model";
import { DataTable } from "./data-table";
import { InviteUserDialog } from "./invite-user";
import { AddUserDialog } from "./add-user-dialog";
import { UserDetailSheet } from "./sheet";
import { Avatar } from "@radix-ui/react-avatar";
import Image from "next/image";
import type { ColumnDef, HeaderContext } from "@tanstack/react-table";
import { UserProfileSheet } from "./update-user";
type UserFilterOptions = {
email: string;
phone: string;
lastSignIn: string;
createdAt: string;
status: string[];
};
type UserTableColumn = ColumnDef<User, User>;
import { filterUsers, useUserManagementHandlers } from "../handler";
import { createUserColumns } from "./users-table";
import { useGetUsersQuery } from "../queries";
export default function UserManagement() {
const [searchQuery, setSearchQuery] = useState("");
const [detailUser, setDetailUser] = useState<User | null>(null);
const [updateUser, setUpdateUser] = useState<User | null>(null);
const [isSheetOpen, setIsSheetOpen] = useState(false);
const [isUpdateOpen, setIsUpdateOpen] = useState(false);
const [isAddUserOpen, setIsAddUserOpen] = useState(false);
const [isInviteUserOpen, setIsInviteUserOpen] = useState(false);
// Filter states
const [filters, setFilters] = useState<UserFilterOptions>({
email: "",
phone: "",
lastSignIn: "",
createdAt: "",
status: [],
});
// Use React Query to fetch users
const {
data: users = [],
isLoading,
isPending,
refetch,
isPlaceholderData,
} = useQuery<User[]>({
queryKey: ["users"],
queryFn: fetchUsers,
placeholderData: keepPreviousData,
throwOnError: true,
});
} = useGetUsersQuery();
// Handle opening the detail sheet
const handleUserClick = (user: User) => {
setDetailUser(user);
setIsSheetOpen(true);
};
// Handle opening the update sheet
const handleUserUpdate = (user: User) => {
setUpdateUser(user);
setIsUpdateOpen(true);
};
// Close detail sheet when update sheet opens
useEffect(() => {
if (isUpdateOpen) {
setIsSheetOpen(false);
}
}, [isUpdateOpen]);
// Reset detail user when sheet closes
useEffect(() => {
if (!isSheetOpen) {
// Use a small delay to prevent flickering if another sheet is opening
const timer = setTimeout(() => {
if (!isSheetOpen && !isUpdateOpen) {
setDetailUser(null);
}
}, 300);
return () => clearTimeout(timer);
}
}, [isSheetOpen, isUpdateOpen]);
// Reset update user when update sheet closes
useEffect(() => {
if (!isUpdateOpen) {
// Use a small delay to prevent flickering if another sheet is opening
const timer = setTimeout(() => {
if (!isUpdateOpen) {
setUpdateUser(null);
}
}, 300);
return () => clearTimeout(timer);
}
}, [isUpdateOpen]);
// User management handler
const {
searchQuery,
setSearchQuery,
detailUser,
updateUser,
isSheetOpen,
setIsSheetOpen,
isUpdateOpen,
setIsUpdateOpen,
isAddUserOpen,
setIsAddUserOpen,
isInviteUserOpen,
setIsInviteUserOpen,
filters,
setFilters,
handleUserClick,
handleUserUpdate,
clearFilters,
getActiveFilterCount,
} = useUserManagementHandlers(refetch)
// Apply filters to users
const filteredUsers = useMemo(() => {
return users.filter((user) => {
// Global search
if (searchQuery) {
const query = searchQuery.toLowerCase();
const matchesSearch =
user.email?.toLowerCase().includes(query) ||
user.phone?.toLowerCase().includes(query) ||
user.id.toLowerCase().includes(query);
return filterUsers(users, searchQuery, filters)
}, [users, searchQuery, filters])
if (!matchesSearch) return false;
}
// Get active filter count
const activeFilterCount = getActiveFilterCount()
// Email filter
if (
filters.email &&
!user.email?.toLowerCase().includes(filters.email.toLowerCase())
) {
return false;
}
// Phone filter
if (
filters.phone &&
!user.phone?.toLowerCase().includes(filters.phone.toLowerCase())
) {
return false;
}
// Last sign in filter
if (filters.lastSignIn) {
if (filters.lastSignIn === "never" && user.last_sign_in_at) {
return false;
} else if (filters.lastSignIn === "today") {
const today = new Date();
today.setHours(0, 0, 0, 0);
const signInDate = user.last_sign_in_at
? new Date(user.last_sign_in_at)
: null;
if (!signInDate || signInDate < today) return false;
} else if (filters.lastSignIn === "week") {
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
const signInDate = user.last_sign_in_at
? new Date(user.last_sign_in_at)
: null;
if (!signInDate || signInDate < weekAgo) return false;
} else if (filters.lastSignIn === "month") {
const monthAgo = new Date();
monthAgo.setMonth(monthAgo.getMonth() - 1);
const signInDate = user.last_sign_in_at
? new Date(user.last_sign_in_at)
: null;
if (!signInDate || signInDate < monthAgo) return false;
}
}
// Created at filter
if (filters.createdAt) {
if (filters.createdAt === "today") {
const today = new Date();
today.setHours(0, 0, 0, 0);
const createdAt = user.created_at
? user.created_at
? new Date(user.created_at)
: new Date()
: new Date();
if (createdAt < today) return false;
} else if (filters.createdAt === "week") {
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
const createdAt = user.created_at
? new Date(user.created_at)
: new Date();
if (createdAt < weekAgo) return false;
} else if (filters.createdAt === "month") {
const monthAgo = new Date();
monthAgo.setMonth(monthAgo.getMonth() - 1);
const createdAt = user.created_at
? new Date(user.created_at)
: new Date();
if (createdAt < monthAgo) return false;
}
}
// Status filter
if (filters.status.length > 0) {
const userStatus = user.banned_until
? "banned"
: !user.email_confirmed_at
? "unconfirmed"
: "active";
if (!filters.status.includes(userStatus)) {
return false;
}
}
return true;
});
}, [users, searchQuery, filters]);
const clearFilters = () => {
setFilters({
email: "",
phone: "",
lastSignIn: "",
createdAt: "",
status: [],
});
};
const activeFilterCount = Object.values(filters).filter(
(value) =>
(typeof value === "string" && value !== "") ||
(Array.isArray(value) && value.length > 0)
).length;
const columns: UserTableColumn[] = [
{
id: "email",
header: ({ column }: HeaderContext<User, User>) => (
<div className="flex items-center gap-1">
<span>Email</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<div className="p-2">
<Input
placeholder="Filter by email..."
value={filters.email}
onChange={(e) =>
setFilters({ ...filters, email: e.target.value })
}
className="w-full"
/>
</div>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => setFilters({ ...filters, email: "" })}
>
Clear filter
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
),
cell: ({ row }) => (
<div className="flex items-center gap-2">
<Avatar className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-medium">
{row.original.profile?.avatar ? (
<Image
src={row.original.profile.avatar || "/placeholder.svg"}
alt="Avatar"
className="w-full h-full rounded-full"
width={32}
height={32}
/>
) : (
row.original.email?.[0]?.toUpperCase() || "?"
)}
</Avatar>
<div>
<div className="font-medium">
{row.original.email || "No email"}
</div>
<div className="text-xs text-muted-foreground">
{row.original.id}
</div>
</div>
</div>
),
},
{
id: "phone",
header: ({ column }: HeaderContext<User, User>) => (
<div className="flex items-center gap-1">
<span>Phone</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<div className="p-2">
<Input
placeholder="Filter by phone..."
value={filters.phone}
onChange={(e) =>
setFilters({ ...filters, phone: e.target.value })
}
className="w-full"
/>
</div>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => setFilters({ ...filters, phone: "" })}
>
Clear filter
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
),
cell: ({ row }) => row.original.phone || "-",
},
{
id: "lastSignIn",
header: ({ column }: HeaderContext<User, User>) => (
<div className="flex items-center gap-1">
<span>Last Sign In</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuCheckboxItem
checked={filters.lastSignIn === "today"}
onCheckedChange={() =>
setFilters({
...filters,
lastSignIn: filters.lastSignIn === "today" ? "" : "today",
})
}
>
Today
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={filters.lastSignIn === "week"}
onCheckedChange={() =>
setFilters({
...filters,
lastSignIn: filters.lastSignIn === "week" ? "" : "week",
})
}
>
Last 7 days
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={filters.lastSignIn === "month"}
onCheckedChange={() =>
setFilters({
...filters,
lastSignIn: filters.lastSignIn === "month" ? "" : "month",
})
}
>
Last 30 days
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={filters.lastSignIn === "never"}
onCheckedChange={() =>
setFilters({
...filters,
lastSignIn: filters.lastSignIn === "never" ? "" : "never",
})
}
>
Never
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => setFilters({ ...filters, lastSignIn: "" })}
>
Clear filter
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
),
cell: ({ row }) => {
return row.original.last_sign_in_at
? new Date(row.original.last_sign_in_at).toLocaleString()
: "Never";
},
},
{
id: "createdAt",
header: ({ column }: HeaderContext<User, User>) => (
<div className="flex items-center gap-1">
<span>Created At</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuCheckboxItem
checked={filters.createdAt === "today"}
onCheckedChange={() =>
setFilters({
...filters,
createdAt: filters.createdAt === "today" ? "" : "today",
})
}
>
Today
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={filters.createdAt === "week"}
onCheckedChange={() =>
setFilters({
...filters,
createdAt: filters.createdAt === "week" ? "" : "week",
})
}
>
Last 7 days
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={filters.createdAt === "month"}
onCheckedChange={() =>
setFilters({
...filters,
createdAt: filters.createdAt === "month" ? "" : "month",
})
}
>
Last 30 days
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => setFilters({ ...filters, createdAt: "" })}
>
Clear filter
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
),
cell: ({ row }) => {
return row.original.created_at
? new Date(row.original.created_at).toLocaleString()
: "N/A";
},
},
{
id: "status",
header: ({ column }: HeaderContext<User, User>) => (
<div className="flex items-center gap-1">
<span>Status</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuCheckboxItem
checked={filters.status.includes("active")}
onCheckedChange={() => {
const newStatus = [...filters.status];
if (newStatus.includes("active")) {
newStatus.splice(newStatus.indexOf("active"), 1);
} else {
newStatus.push("active");
}
setFilters({ ...filters, status: newStatus });
}}
>
Active
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={filters.status.includes("unconfirmed")}
onCheckedChange={() => {
const newStatus = [...filters.status];
if (newStatus.includes("unconfirmed")) {
newStatus.splice(newStatus.indexOf("unconfirmed"), 1);
} else {
newStatus.push("unconfirmed");
}
setFilters({ ...filters, status: newStatus });
}}
>
Unconfirmed
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={filters.status.includes("banned")}
onCheckedChange={() => {
const newStatus = [...filters.status];
if (newStatus.includes("banned")) {
newStatus.splice(newStatus.indexOf("banned"), 1);
} else {
newStatus.push("banned");
}
setFilters({ ...filters, status: newStatus });
}}
>
Banned
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => setFilters({ ...filters, status: [] })}
>
Clear filter
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
),
cell: ({ row }) => {
if (row.original.banned_until) {
return <Badge variant="destructive">Banned</Badge>;
}
if (!row.original.email_confirmed_at) {
return <Badge variant="outline">Unconfirmed</Badge>;
}
return <Badge variant="default">Active</Badge>;
},
},
{
id: "actions",
header: "",
cell: ({ row }) => (
<div onClick={(e) => e.stopPropagation()}>
{/* Add this wrapper */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleUserUpdate(row.original)}>
<UserPen className="h-4 w-4 mr-2 text-blue-500" />
Update
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
/* handle delete */
}}
>
<Trash2 className="h-4 w-4 mr-2 text-red-500" />
Delete
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
/* handle ban */
}}
>
<ShieldAlert className="h-4 w-4 mr-2 text-yellow-500" />
{row.original.banned_until != null ? "Unban" : "Ban"}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
),
},
];
// Create table columns
const columns = createUserColumns(filters, setFilters, handleUserUpdate)
return (
<div className="space-y-4">
@ -642,7 +135,7 @@ export default function UserManagement() {
<DataTable
columns={columns}
data={filteredUsers}
loading={isLoading}
loading={isPending}
onRowClick={(user) => handleUserClick(user)}
/>
{detailUser && (
@ -653,16 +146,8 @@ export default function UserManagement() {
onUserUpdate={() => refetch()}
/>
)}
<AddUserDialog
open={isAddUserOpen}
onOpenChange={setIsAddUserOpen}
onUserAdded={() => refetch()}
/>
<InviteUserDialog
open={isInviteUserOpen}
onOpenChange={setIsInviteUserOpen}
onUserInvited={() => refetch()}
/>
<AddUserDialog open={isAddUserOpen} onOpenChange={setIsAddUserOpen} onUserAdded={() => refetch()} />
<InviteUserDialog open={isInviteUserOpen} onOpenChange={setIsInviteUserOpen} onUserInvited={() => refetch()} />
{updateUser && (
<UserProfileSheet
open={isUpdateOpen}
@ -672,5 +157,5 @@ export default function UserManagement() {
/>
)}
</div>
);
)
}

View File

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

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";
import db from "@/prisma/db";
import db from '@/prisma/db';
import { createClient } from '@/app/_utils/supabase/server';
import { createAdminClient } from '@/app/_utils/supabase/admin';
import { getInjection } from '@/di/container';
import { InputParseError, NotFoundError } from '@/src/entities/errors/common';
import {
CreateUserParams,
InviteUserParams,
UpdateUserParams,
User,
UserResponse,
} from "@/src/entities/models/users/users.model";
import { createClient } from "@/app/_utils/supabase/server";
import { createAdminClient } from "@/app/_utils/supabase/admin";
AuthenticationError,
UnauthenticatedError,
} from '@/src/entities/errors/auth';
import { redirect } from 'next/navigation';
import { IBanDuration, IBanUserSchema, ICredentialsBanUserSchema } from '@/src/entities/models/users/ban-user.model';
import { ICreateUserSchema } from '@/src/entities/models/users/create-user.model';
import { IUpdateUserSchema } from '@/src/entities/models/users/update-user.model';
import { ICredentialsInviteUserSchema } from '@/src/entities/models/users/invite-user.model';
// Initialize Supabase client with admin key
export async function banUser(id: string, ban_duration: IBanDuration) {
const instrumentationService = getInjection('IInstrumentationService');
return await instrumentationService.instrumentServerAction(
'banUser',
{ recordResponse: true },
async () => {
try {
const banUserController = getInjection('IBanUserController');
await banUserController({ id }, { ban_duration });
// Fetch all users
export async function fetchUsers(): Promise<User[]> {
// const { data, error } = await supabase.auth.admin.getUsers();
return { success: true };
} catch (err) {
if (err instanceof InputParseError) {
// return {
// error: err.message,
// };
// if (error) {
// console.error("Error fetching users:", error);
// throw new Error(error.message);
// }
throw new InputParseError(err.message);
}
// return data.users.map((user) => ({
// ...user,
// })) as User[];
if (err instanceof UnauthenticatedError) {
// return {
// error: 'Must be logged in to create a user.',
// };
throw new UnauthenticatedError('Must be logged in to ban a user.');
}
const users = await db.users.findMany({
include: {
profile: true,
},
});
if (err instanceof AuthenticationError) {
// return {
// error: 'User not found.',
// };
if (!users) {
throw new Error("Users not found");
}
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
}
console.log("fetchedUsers");
return users;
}
// get current user
export async function getCurrentUser(): Promise<UserResponse> {
const supabase = await createClient();
const {
data: { user },
error,
} = await supabase.auth.getUser();
if (error) {
console.error("Error fetching current user:", error);
throw new Error(error.message);
}
const userDetail = await db.users.findUnique({
where: {
id: user?.id,
},
include: {
profile: true,
},
});
if (!userDetail) {
throw new Error("User not found");
}
return {
data: {
user: userDetail,
},
error: null,
};
}
// Create a new user
export async function createUser(
params: CreateUserParams
): Promise<UserResponse> {
const supabase = createAdminClient();
const { data, error } = await supabase.auth.admin.createUser({
email: params.email,
password: params.password,
phone: params.phone,
email_confirm: params.email_confirm,
});
if (error) {
console.error("Error creating user:", error);
throw new Error(error.message);
}
return {
data: {
user: data.user,
},
error: null,
};
}
export async function uploadAvatar(userId: string, email: string, file: File) {
try {
const supabase = await createClient();
const fileExt = file.name.split(".").pop();
const emailName = email.split("@")[0];
const fileName = `AVR-${emailName}.${fileExt}`;
// Change this line - store directly in the user's folder
const filePath = `${userId}/${fileName}`;
// Upload the avatar to Supabase storage
const { error: uploadError } = await supabase.storage
.from("avatars")
.upload(filePath, file, {
upsert: true,
contentType: file.type,
});
if (uploadError) {
console.error("Error uploading avatar:", uploadError);
throw uploadError;
const crashReporterService = getInjection('ICrashReporterService');
crashReporterService.report(err);
// return {
// error:
// 'An error happened. The developers have been notified. Please try again later.',
// };
throw new Error('An error happened. The developers have been notified. Please try again later.');
}
}
// Get the public URL
const {
data: { publicUrl },
} = supabase.storage.from("avatars").getPublicUrl(filePath);
// Update user profile with the new avatar URL
await db.users.update({
where: {
id: userId,
},
data: {
profile: {
update: {
avatar: publicUrl,
},
},
},
});
return publicUrl;
} catch (error) {
console.error("Error uploading avatar:", error);
throw error;
}
);
}
export async function unbanUser(id: string) {
const instrumentationService = getInjection('IInstrumentationService');
return await instrumentationService.instrumentServerAction(
'unbanUser',
{ recordResponse: true },
async () => {
try {
const unbanUserController = getInjection('IUnbanUserController');
await unbanUserController({ id });
// Update an existing user
export async function updateUser(
userId: string,
params: UpdateUserParams
): Promise<UserResponse> {
const supabase = createAdminClient();
return { success: true };
} catch (err) {
if (err instanceof InputParseError) {
// return {
// error: err.message,
// };
const { data, error } = await supabase.auth.admin.updateUserById(userId, {
email: params.email,
email_confirm: params.email_confirmed_at,
password: params.encrypted_password ?? undefined,
password_hash: params.encrypted_password ?? undefined,
phone: params.phone,
phone_confirm: params.phone_confirmed_at,
role: params.role,
user_metadata: params.user_metadata,
app_metadata: params.app_metadata,
});
throw new InputParseError(err.message);
}
if (error) {
console.error("Error updating user:", error);
throw new Error(error.message);
}
if (err instanceof UnauthenticatedError) {
// return {
// error: 'Must be logged in to create a user.',
// };
throw new UnauthenticatedError('Must be logged in to unban a user.');
}
const user = await db.users.findUnique({
where: {
id: userId,
},
include: {
profile: true,
},
});
if (err instanceof AuthenticationError) {
// return {
// error: 'User not found.',
// };
if (!user) {
throw new Error("User not found");
}
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
}
const updateUser = await db.users.update({
where: {
id: userId,
},
data: {
role: params.role || user.role,
invited_at: params.invited_at || user.invited_at,
confirmed_at: params.confirmed_at || user.confirmed_at,
// recovery_sent_at: params.recovery_sent_at || user.recovery_sent_at,
last_sign_in_at: params.last_sign_in_at || user.last_sign_in_at,
is_anonymous: params.is_anonymous || user.is_anonymous,
created_at: params.created_at || user.created_at,
updated_at: params.updated_at || user.updated_at,
profile: {
update: {
avatar: params.profile?.avatar || user.profile?.avatar,
username: params.profile?.username || user.profile?.username,
first_name: params.profile?.first_name || user.profile?.first_name,
last_name: params.profile?.last_name || user.profile?.last_name,
bio: params.profile?.bio || user.profile?.bio,
address: params.profile?.address || user.profile?.address,
birth_date: params.profile?.birth_date || user.profile?.birth_date,
},
},
},
include: {
profile: true,
},
});
return {
data: {
user: {
...data.user,
role: params.role,
profile: {
user_id: userId,
...updateUser.profile,
},
},
},
error: null,
};
}
// Delete a user
export async function deleteUser(userId: string): Promise<void> {
const supabase = createAdminClient();
const { error } = await supabase.auth.admin.deleteUser(userId);
if (error) {
console.error("Error deleting user:", error);
throw new Error(error.message);
}
}
// Send password recovery email
export async function sendPasswordRecovery(email: string): Promise<void> {
const supabase = createAdminClient();
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/reset-password`,
});
if (error) {
console.error("Error sending password recovery:", error);
throw new Error(error.message);
}
}
// Send magic link
export async function sendMagicLink(email: string): Promise<void> {
const supabase = createAdminClient();
const { error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
},
});
if (error) {
console.error("Error sending magic link:", error);
throw new Error(error.message);
}
}
// Ban a user
export async function banUser(userId: string): Promise<UserResponse> {
const supabase = createAdminClient();
// Ban for 100 years (effectively permanent)
const banUntil = new Date();
banUntil.setFullYear(banUntil.getFullYear() + 100);
const { data, error } = await supabase.auth.admin.updateUserById(userId, {
ban_duration: "100h",
});
if (error) {
console.error("Error banning user:", error);
throw new Error(error.message);
}
return {
data: {
user: data.user,
},
error: null,
};
}
// Unban a user
export async function unbanUser(userId: string): Promise<UserResponse> {
const supabase = createAdminClient();
const { data, error } = await supabase.auth.admin.updateUserById(userId, {
ban_duration: "none",
});
if (error) {
console.error("Error unbanning user:", error);
throw new Error(error.message);
}
const user = await db.users.findUnique({
where: {
id: userId,
},
select: {
banned_until: true,
const crashReporterService = getInjection('ICrashReporterService');
crashReporterService.report(err);
// return {
// error:
// 'An error happened. The developers have been notified. Please try again later.',
// };
throw new Error('An error happened. The developers have been notified. Please try again later.');
}
}
})
if (!user) {
throw new Error("User not found");
}
// const updateUser = await db.users.update({
// where: {
// id: userId,
// },
// data: {
// banned_until: null,
// },
// })
return {
data: {
user: data.user,
},
error: null,
};
);
}
// Invite a user
export async function inviteUser(params: InviteUserParams): Promise<void> {
const supabase = createAdminClient();
export async function getCurrentUser() {
const instrumentationService = getInjection('IInstrumentationService');
return await instrumentationService.instrumentServerAction(
'getCurrentUser',
{ recordResponse: true },
async () => {
try {
const getCurrentUserController = getInjection(
'IGetCurrentUserController'
);
return await getCurrentUserController();
} catch (err) {
const { error } = await supabase.auth.admin.inviteUserByEmail(params.email, {
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
});
if (err instanceof UnauthenticatedError || err instanceof AuthenticationError) {
redirect('/sign-in');
}
if (error) {
console.error("Error inviting user:", error);
throw new Error(error.message);
}
const crashReporterService = getInjection('ICrashReporterService');
crashReporterService.report(err);
throw new Error('An error happened. The developers have been notified. Please try again later.');
}
}
);
}
export async function getUserById(id: string) {
const instrumentationService = getInjection('IInstrumentationService');
return await instrumentationService.instrumentServerAction(
'getUserById',
{ recordResponse: true },
async () => {
try {
const getUserByIdController = getInjection('IGetUserByIdController');
return await getUserByIdController({ id });
} catch (err) {
if (err instanceof InputParseError) {
// return {
// error: err.message,
// };
throw new InputParseError(err.message);
}
if (err instanceof UnauthenticatedError) {
// return {
// error: 'Must be logged in to create a user.',
// };
throw new UnauthenticatedError('Must be logged in to get a user.');
}
if (err instanceof AuthenticationError) {
// return {
// error: 'User not found.',
// };
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
}
const crashReporterService = getInjection('ICrashReporterService');
crashReporterService.report(err);
// return {
// error:
// 'An error happened. The developers have been notified. Please try again later.',
// };
throw new Error('An error happened. The developers have been notified. Please try again later.');
}
}
);
}
export async function getUserByEmail(email: string) {
const instrumentationService = getInjection('IInstrumentationService');
return await instrumentationService.instrumentServerAction(
'getUserByEmail',
{ recordResponse: true },
async () => {
try {
const getUserByEmailController = getInjection(
'IGetUserByEmailController'
);
return await getUserByEmailController({ email });
} catch (err) {
if (err instanceof InputParseError) {
// return {
// error: err.message,
// };
throw new InputParseError(err.message);
}
if (err instanceof UnauthenticatedError) {
// return {
// error: 'Must be logged in to create a user.',
// };
throw new UnauthenticatedError('Must be logged in to get a user.');
}
if (err instanceof AuthenticationError) {
// return {
// error: 'User not found.',
// };
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
}
const crashReporterService = getInjection('ICrashReporterService');
crashReporterService.report(err);
// return {
// error:
// 'An error happened. The developers have been notified. Please try again later.',
// };
throw new Error('An error happened. The developers have been notified. Please try again later.');
}
}
);
}
export async function getUserByUsername(username: string) {
const instrumentationService = getInjection('IInstrumentationService');
return await instrumentationService.instrumentServerAction(
'getUserByUsername',
{ recordResponse: true },
async () => {
try {
const getUserByUsernameController = getInjection(
'IGetUserByUsernameController'
);
return await getUserByUsernameController({ username });
} catch (err) {
if (err instanceof InputParseError) {
// return {
// error: err.message,
// };
throw new InputParseError(err.message);
}
if (err instanceof UnauthenticatedError) {
// return {
// error: 'Must be logged in to create a user.',
// };
throw new UnauthenticatedError('Must be logged in to get a user.');
}
if (err instanceof AuthenticationError) {
// return {
// error: 'User not found.',
// };
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
}
const crashReporterService = getInjection('ICrashReporterService');
crashReporterService.report(err);
// return {
// error:
// 'An error happened. The developers have been notified. Please try again later.',
// };
throw new Error('An error happened. The developers have been notified. Please try again later.');
}
}
);
}
export async function getUsers() {
const instrumentationService = getInjection('IInstrumentationService');
return await instrumentationService.instrumentServerAction(
'getUsers',
{ recordResponse: true },
async () => {
try {
const getUsersController = getInjection('IGetUsersController');
return await getUsersController();
} catch (err) {
if (
err instanceof UnauthenticatedError ||
err instanceof AuthenticationError
) {
redirect('/sign-in');
}
const crashReporterService = getInjection('ICrashReporterService');
crashReporterService.report(err);
throw new Error('An error happened. The developers have been notified. Please try again later.');
}
}
);
}
export async function inviteUser(credentials: ICredentialsInviteUserSchema) {
const instrumentationService = getInjection('IInstrumentationService');
return await instrumentationService.instrumentServerAction(
'inviteUser',
{ recordResponse: true },
async () => {
try {
const inviteUserController = getInjection('IInviteUserController');
await inviteUserController({ email: credentials.email });
return { success: true };
} catch (err) {
if (err instanceof InputParseError) {
// return {
// error: err.message,
// };
throw new InputParseError(err.message);
}
if (err instanceof UnauthenticatedError) {
// return {
// error: 'Must be logged in to create a user.',
// };
throw new UnauthenticatedError('Must be logged in to invite a user.');
}
if (err instanceof AuthenticationError) {
// return {
// error: 'User not found.',
// };
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
}
const crashReporterService = getInjection('ICrashReporterService');
crashReporterService.report(err);
// return {
// error:
// 'An error happened. The developers have been notified. Please try again later.',
// };
throw new Error('An error happened. The developers have been notified. Please try again later.');
}
}
);
}
export async function createUser(data: ICreateUserSchema) {
const instrumentationService = getInjection('IInstrumentationService');
return await instrumentationService.instrumentServerAction(
'createUser',
{ recordResponse: true },
async () => {
try {
const createUserController = getInjection('ICreateUserController');
await createUserController(data);
return { success: true };
} catch (err) {
if (err instanceof InputParseError) {
// return {
// error: err.message,
// };
throw new InputParseError(err.message);
}
if (err instanceof UnauthenticatedError) {
// return {
// error: 'Must be logged in to create a user.',
// };
throw new UnauthenticatedError('Must be logged in to create a user.');
}
if (err instanceof AuthenticationError) {
// return {
// error: 'User not found.',
// };
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
}
const crashReporterService = getInjection('ICrashReporterService');
crashReporterService.report(err);
// return {
// error:
// 'An error happened. The developers have been notified. Please try again later.',
// };
throw new Error('An error happened. The developers have been notified. Please try again later.');
}
}
);
}
export async function updateUser(id: string, data: IUpdateUserSchema) {
const instrumentationService = getInjection('IInstrumentationService');
return await instrumentationService.instrumentServerAction(
'updateUser',
{ recordResponse: true },
async () => {
try {
const updateUserController = getInjection('IUpdateUserController');
await updateUserController(id, data);
return { success: true };
} catch (err) {
if (err instanceof InputParseError) {
// return {
// error: err.message,
// };
throw new InputParseError(err.message);
}
if (err instanceof UnauthenticatedError) {
// return {
// error: 'Must be logged in to create a user.',
// };
throw new UnauthenticatedError('Must be logged in to update a user.');
}
if (err instanceof AuthenticationError) {
// return {
// error: 'User not found.',
// };
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
}
const crashReporterService = getInjection('ICrashReporterService');
crashReporterService.report(err);
// return {
// error:
// 'An error happened. The developers have been notified. Please try again later.',
// };
throw new Error('An error happened. The developers have been notified. Please try again later.');
}
}
);
}
export async function deleteUser(id: string) {
const instrumentationService = getInjection('IInstrumentationService');
return await instrumentationService.instrumentServerAction(
'deleteUser',
{ recordResponse: true },
async () => {
try {
const deleteUserController = getInjection('IDeleteUserController');
await deleteUserController({ id });
return { success: true };
} catch (err) {
if (err instanceof InputParseError) {
// return {
// error: err.message,
// };
throw new InputParseError(err.message);
}
if (err instanceof UnauthenticatedError) {
// return {
// error: 'Must be logged in to create a user.',
// };
throw new UnauthenticatedError('Must be logged in to delete a user.');
}
if (err instanceof AuthenticationError) {
// return {
// error: 'User not found.',
// };
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
}
const crashReporterService = getInjection('ICrashReporterService');
crashReporterService.report(err);
// return {
// error:
// 'An error happened. The developers have been notified. Please try again later.',
// };
throw new Error('An error happened. The developers have been notified. Please try again later.');
}
}
);
}

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 { revalidatePath } from "next/cache"
import { InputParseError } from "@/src/entities/errors/common"
import { InputParseError, NotFoundError } from "@/src/entities/errors/common"
import { AuthenticationError, UnauthenticatedError } from "@/src/entities/errors/auth"
import { createClient } from "@/app/_utils/supabase/server"
@ -18,25 +18,22 @@ export async function signIn(formData: FormData) {
try {
const signInController = getInjection("ISignInController")
await signInController({ email })
return await signInController({ email })
// if (email) {
// redirect(`/verify-otp?email=${encodeURIComponent(email)}`)
// }
return { success: true }
} catch (err) {
if (
err instanceof InputParseError ||
err instanceof AuthenticationError
) {
return {
error: 'Incorrect credential. Please try again.',
};
if (err instanceof InputParseError) {
return { error: err.message }
}
if (err instanceof UnauthenticatedError) {
if (err instanceof AuthenticationError) {
return { error: "Invalid credential. Please try again." }
}
if (err instanceof UnauthenticatedError || err instanceof NotFoundError) {
return {
error: 'User not found. Please tell your admin to create an account for you.',
};
@ -136,3 +133,57 @@ export async function verifyOtp(formData: FormData) {
}
})
}
export async function sendMagicLink(formData: FormData) {
const instrumentationService = getInjection("IInstrumentationService")
return await instrumentationService.instrumentServerAction("sendMagicLink", {
recordResponse: true
}, async () => {
try {
const email = formData.get("email")?.toString()
const sendMagicLinkController = getInjection("ISendMagicLinkController")
await sendMagicLinkController({ email })
return { success: true }
} catch (err) {
if (err instanceof InputParseError) {
return { error: err.message }
}
const crashReporterService = getInjection("ICrashReporterService")
crashReporterService.report(err)
return {
error: "An error occurred during sending magic link. Please try again later.",
}
}
})
}
export async function sendPasswordRecovery(formData: FormData) {
const instrumentationService = getInjection("IInstrumentationService")
return await instrumentationService.instrumentServerAction("sendPasswordRecovery", {
recordResponse: true
}, async () => {
try {
const email = formData.get("email")?.toString()
const sendPasswordRecoveryController = getInjection("ISendPasswordRecoveryController")
await sendPasswordRecoveryController({ email })
return { success: true }
} catch (err) {
if (err instanceof InputParseError) {
return { error: err.message }
}
const crashReporterService = getInjection("ICrashReporterService")
crashReporterService.report(err)
return {
error: "An error occurred during sending password recovery. Please try again later.",
}
}
})
}

View File

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

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

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}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
error && "ring-2 ring-red-500 border-red-500 focus-visible:ring-red-500",
error && "ring-2 ring-red-500 focus-visible:ring-red-500",
className
)}
ref={ref}

View File

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

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

View File

@ -49,3 +49,25 @@ export function createFormData(): FormData {
});
return data;
};
/**
* Generates a unique username based on the provided email address.
*
* The username is created by combining the local part of the email (before the '@' symbol)
* with a randomly generated alphanumeric suffix.
*
* @param email - The email address to generate the username from.
* @returns A string representing the generated username.
*
* @example
* ```typescript
* const username = generateUsername("example@gmail.com");
* console.log(username); // Output: "example.abc123" (random suffix will vary)
* ```
*/
export function generateUsername(email: string): string {
const [localPart] = email.split("@");
const randomSuffix = Math.random().toString(36).substring(2, 8); // Generate a random alphanumeric string
return `${localPart}.${randomSuffix}`;
}

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 { verifyOtpUseCase } from '@/src/application/use-cases/auth/verify-otp.use-case';
import { verifyOtpController } from '@/src/interface-adapters/controllers/auth/verify-otp.controller';
import { sendMagicLinkUseCase } from '@/src/application/use-cases/auth/send-magic-link.use-case';
import { sendPasswordRecoveryUseCase } from '@/src/application/use-cases/auth/send-password-recovery.use-case';
import { sendMagicLinkController } from '@/src/interface-adapters/controllers/auth/send-magic-link.controller';
import { sendPasswordRecoveryController } from '@/src/interface-adapters/controllers/auth/send-password-recovery.controller';
export function createAuthenticationModule() {
const authenticationModule = createModule();
@ -66,6 +70,22 @@ export function createAuthenticationModule() {
DI_SYMBOLS.IAuthenticationService,
]);
authenticationModule
.bind(DI_SYMBOLS.ISendMagicLinkUseCase)
.toHigherOrderFunction(sendMagicLinkUseCase, [
DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.IAuthenticationService,
DI_SYMBOLS.IUsersRepository,
]);
authenticationModule
.bind(DI_SYMBOLS.ISendPasswordRecoveryUseCase)
.toHigherOrderFunction(sendPasswordRecoveryUseCase, [
DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.IAuthenticationService,
DI_SYMBOLS.IUsersRepository,
]);
// Controllers
authenticationModule
@ -90,6 +110,20 @@ export function createAuthenticationModule() {
DI_SYMBOLS.ISignOutUseCase,
]);
authenticationModule
.bind(DI_SYMBOLS.ISendMagicLinkController)
.toHigherOrderFunction(sendMagicLinkController, [
DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.ISendMagicLinkUseCase,
]);
authenticationModule
.bind(DI_SYMBOLS.ISendPasswordRecoveryController)
.toHigherOrderFunction(sendPasswordRecoveryController, [
DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.ISendPasswordRecoveryUseCase,
]);
return authenticationModule;
}

View File

@ -1,9 +1,7 @@
import { createModule } from '@evyweb/ioctopus';
import { DI_SYMBOLS } from '@/di/types';
import { UsersRepository } from '@/src/infrastructure/repositories/users.repository';
import { getUsersUseCase } from '@/src/application/use-cases/users/get-users.use-case';
import { getUsersController } from '@/src/interface-adapters/controllers/users/get-users.controller';
import { banUserController } from '@/src/interface-adapters/controllers/users/ban-user.controller';
import { banUserUseCase } from '@/src/application/use-cases/users/ban-user.use-case';
@ -25,6 +23,7 @@ import { deleteUserUseCase } from '@/src/application/use-cases/users/delete-user
import { getUserByUsernameUseCase } from '@/src/application/use-cases/users/get-user-by-username.use-case';
import { getUserByEmailUseCase } from '@/src/application/use-cases/users/get-user-by-email.use-case';
import { updateUserUseCase } from '@/src/application/use-cases/users/update-user.use-case';
import { getUsersUseCase } from '@/src/application/use-cases/users/get-users.use-case';
export function createUsersModule() {
@ -169,7 +168,7 @@ export function createUsersModule() {
]);
usersModule
.bind(DI_SYMBOLS.IGetUserByUserNameController)
.bind(DI_SYMBOLS.IGetUserByUsernameController)
.toHigherOrderFunction(getUserByUsernameController, [
DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.IGetUserByUserNameUseCase
@ -179,14 +178,16 @@ export function createUsersModule() {
.bind(DI_SYMBOLS.IInviteUserController)
.toHigherOrderFunction(inviteUserController, [
DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.IInviteUserUseCase
DI_SYMBOLS.IInviteUserUseCase,
DI_SYMBOLS.IGetCurrentUserUseCase
]);
usersModule
.bind(DI_SYMBOLS.ICreateUserController)
.toHigherOrderFunction(createUserController, [
DI_SYMBOLS.IInstrumentationService,
DI_SYMBOLS.ICreateUserUseCase
DI_SYMBOLS.ICreateUserUseCase,
DI_SYMBOLS.IGetCurrentUserUseCase
]);
usersModule

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

View File

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

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 { createClient } from "@/app/_utils/supabase/client";
import { CreateUser, InviteUser, UpdateUser, User, UserResponse } from "@/src/entities/models/users/users.model";
import { IUserSchema, UserResponse } from "@/src/entities/models/users/users.model";
import { ITransaction } from "@/src/entities/models/transaction.interface";
import { ICreateUserSchema } from "@/src/entities/models/users/create-user.model";
import { ICredentialUpdateUserSchema, IUpdateUserSchema } from "@/src/entities/models/users/update-user.model";
import { ICredentialsBanUserSchema, IBanUserSchema } from "@/src/entities/models/users/ban-user.model";
import { ICredentialsUnbanUserSchema } from "@/src/entities/models/users/unban-user.model";
import { ICredentialsDeleteUserSchema } from "@/src/entities/models/users/delete-user.model";
import { ICredentialsInviteUserSchema } from "@/src/entities/models/users/invite-user.model";
import { ICredentialGetUserByEmailSchema, ICredentialGetUserByIdSchema, ICredentialGetUserByUsernameSchema, IGetUserByUsernameSchema } from "@/src/entities/models/users/read-user.model";
export interface IUsersRepository {
getUsers(): Promise<User[]>;
getCurrentUser(): Promise<User>;
getUserById(id: string): Promise<User | undefined>;
getUserByUsername(username: string): Promise<User | undefined>;
getUserByEmail(email: string): Promise<User | undefined>;
createUser(input: CreateUser, tx?: ITransaction): Promise<User>;
inviteUser(email: string, tx?: ITransaction): Promise<User>;
updateUser(id: string, input: Partial<UpdateUser>, tx?: ITransaction): Promise<User>;
deleteUser(id: string, tx?: ITransaction): Promise<User>;
banUser(id: string, ban_duration: string, tx?: ITransaction): Promise<User>;
unbanUser(id: string, tx?: ITransaction): Promise<User>;
}
// export class UsersRepository {
// constructor(
// private readonly instrumentationService: IInstrumentationService,
// private readonly crashReporterService: ICrashReporterService,
// private readonly supabaseAdmin = createAdminClient(),
// private readonly supabaseClient = createClient()
// ) { }
// async getUsers(): Promise<User[]> {
// return await this.instrumentationService.startSpan({
// name: "UsersRepository > getUsers",
// op: 'db.query',
// attributes: { 'db.system': 'postgres' },
// }, async () => {
// try {
// const users = await db.users.findMany({
// include: {
// profile: true,
// },
// });
// if (!users) {
// throw new NotFoundError("Users not found");
// }
// return users;
// } catch (err) {
// this.crashReporterService.report(err);
// throw err;
// }
// })
// }
// async getCurrentUser(): Promise<UserResponse> {
// return await this.instrumentationService.startSpan({
// name: "UsersRepository > getCurrentUser",
// op: 'db.query',
// attributes: { 'db.system': 'postgres' },
// }, async () => {
// try {
// const supabase = await this.supabaseClient;
// const {
// data: { user },
// error,
// } = await supabase.auth.getUser();
// if (error) {
// console.error("Error fetching current user:", error);
// throw new AuthenticationError(error.message);
// }
// const userDetail = await db.users.findUnique({
// where: {
// id: user?.id,
// },
// include: {
// profile: true,
// },
// });
// if (!userDetail) {
// throw new NotFoundError("User not found");
// }
// return {
// data: {
// user: userDetail,
// },
// error: null,
// };
// } catch (err) {
// this.crashReporterService.report(err);
// throw err;
// }
// })
// }
// async createUser(params: CreateUser): Promise<UserResponse> {
// return await this.instrumentationService.startSpan({
// name: "UsersRepository > createUser",
// op: 'db.query',
// attributes: { 'db.system': 'postgres' },
// }, async () => {
// try {
// const supabase = this.supabaseAdmin;
// const { data, error } = await supabase.auth.admin.createUser({
// email: params.email,
// password: params.password,
// phone: params.phone,
// email_confirm: params.email_confirm,
// });
// if (error) {
// console.error("Error creating user:", error);
// throw new DatabaseOperationError(error.message);
// }
// return {
// data: {
// user: data.user,
// },
// error: null,
// };
// } catch (err) {
// this.crashReporterService.report(err);
// throw err;
// }
// })
// }
// async inviteUser(params: InviteUser): Promise<void> {
// return await this.instrumentationService.startSpan({
// name: "UsersRepository > inviteUser",
// op: 'db.query',
// attributes: { 'db.system': 'postgres' },
// }, async () => {
// try {
// const supabase = this.supabaseAdmin;
// const { error } = await supabase.auth.admin.inviteUserByEmail(params.email, {
// redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/api/auth/callback`,
// });
// if (error) {
// console.error("Error inviting user:", error);
// throw new DatabaseOperationError(error.message);
// }
// } catch (err) {
// this.crashReporterService.report(err);
// throw err;
// }
// })
// }
// async uploadAvatar(userId: string, email: string, file: File) {
// return await this.instrumentationService.startSpan({
// name: "UsersRepository > uploadAvatar",
// op: 'db.query',
// attributes: { 'db.system': 'postgres' },
// }, async () => {
// try {
// const supabase = await this.supabaseClient;
// const fileExt = file.name.split(".").pop();
// const emailName = email.split("@")[0];
// const fileName = `AVR-${emailName}.${fileExt}`;
// const filePath = `${userId}/${fileName}`;
// const { error: uploadError } = await supabase.storage
// .from("avatars")
// .upload(filePath, file, {
// upsert: true,
// contentType: file.type,
// });
// if (uploadError) {
// console.error("Error uploading avatar:", uploadError);
// throw new DatabaseOperationError(uploadError.message);
// }
// const {
// data: { publicUrl },
// } = supabase.storage.from("avatars").getPublicUrl(filePath);
// await db.users.update({
// where: {
// id: userId,
// },
// data: {
// profile: {
// update: {
// avatar: publicUrl,
// },
// },
// },
// });
// return publicUrl;
// } catch (err) {
// this.crashReporterService.report(err);
// throw err;
// }
// })
// }
// async updateUser(userId: string, params: UpdateUser): Promise<UserResponse> {
// return await this.instrumentationService.startSpan({
// name: "UsersRepository > updateUser",
// op: 'db.query',
// attributes: { 'db.system': 'postgres' },
// }, async () => {
// try {
// const supabase = this.supabaseAdmin;
// const { data, error } = await supabase.auth.admin.updateUserById(userId, {
// email: params.email,
// email_confirm: params.email_confirmed_at,
// password: params.encrypted_password ?? undefined,
// password_hash: params.encrypted_password ?? undefined,
// phone: params.phone,
// phone_confirm: params.phone_confirmed_at,
// role: params.role,
// user_metadata: params.user_metadata,
// app_metadata: params.app_metadata,
// });
// if (error) {
// console.error("Error updating user:", error);
// throw new DatabaseOperationError(error.message);
// }
// const user = await db.users.findUnique({
// where: {
// id: userId,
// },
// include: {
// profile: true,
// },
// });
// if (!user) {
// throw new NotFoundError("User not found");
// }
// const updateUser = await db.users.update({
// where: {
// id: userId,
// },
// data: {
// role: params.role || user.role,
// invited_at: params.invited_at || user.invited_at,
// confirmed_at: params.confirmed_at || user.confirmed_at,
// last_sign_in_at: params.last_sign_in_at || user.last_sign_in_at,
// is_anonymous: params.is_anonymous || user.is_anonymous,
// created_at: params.created_at || user.created_at,
// updated_at: params.updated_at || user.updated_at,
// profile: {
// update: {
// avatar: params.profile?.avatar || user.profile?.avatar,
// username: params.profile?.username || user.profile?.username,
// first_name: params.profile?.first_name || user.profile?.first_name,
// last_name: params.profile?.last_name || user.profile?.last_name,
// bio: params.profile?.bio || user.profile?.bio,
// address: params.profile?.address || user.profile?.address,
// birth_date: params.profile?.birth_date || user.profile?.birth_date,
// },
// },
// },
// include: {
// profile: true,
// },
// });
// return {
// data: {
// user: {
// ...data.user,
// role: params.role,
// profile: {
// user_id: userId,
// ...updateUser.profile,
// },
// },
// },
// error: null,
// };
// } catch (err) {
// this.crashReporterService.report(err);
// throw err;
// }
// })
// }
// async deleteUser(userId: string): Promise<void> {
// return await this.instrumentationService.startSpan({
// name: "UsersRepository > deleteUser",
// op: 'db.query',
// attributes: { 'db.system': 'postgres' },
// }, async () => {
// try {
// const supabase = this.supabaseAdmin;
// const { error } = await supabase.auth.admin.deleteUser(userId);
// if (error) {
// console.error("Error deleting user:", error);
// throw new DatabaseOperationError(error.message);
// }
// } catch (err) {
// this.crashReporterService.report(err);
// throw err;
// }
// })
// }
// async banUser(userId: string): Promise<UserResponse> {
// return await this.instrumentationService.startSpan({
// name: "UsersRepository > banUser",
// op: 'db.query',
// attributes: { 'db.system': 'postgres' },
// }, async () => {
// try {
// const supabase = this.supabaseAdmin;
// const { data, error } = await supabase.auth.admin.updateUserById(userId, {
// ban_duration: "100h",
// });
// if (error) {
// console.error("Error banning user:", error);
// throw new DatabaseOperationError(error.message);
// }
// return {
// data: {
// user: data.user,
// },
// error: null,
// };
// } catch (err) {
// this.crashReporterService.report(err);
// throw err;
// }
// })
// }
// async unbanUser(userId: string): Promise<UserResponse> {
// return await this.instrumentationService.startSpan({
// name: "UsersRepository > unbanUser",
// op: 'db.query',
// attributes: { 'db.system': 'postgres' },
// }, async () => {
// try {
// const supabase = this.supabaseAdmin;
// const { data, error } = await supabase.auth.admin.updateUserById(userId, {
// ban_duration: "none",
// });
// if (error) {
// console.error("Error unbanning user:", error);
// throw new DatabaseOperationError(error.message);
// }
// const user = await db.users.findUnique({
// where: {
// id: userId,
// },
// select: {
// banned_until: true,
// }
// });
// if (!user) {
// throw new NotFoundError("User not found");
// }
// return {
// data: {
// user: data.user,
// },
// error: null,
// };
// } catch (err) {
// this.crashReporterService.report(err);
// throw err;
// }
// })
// }
// }
// export async function fetchUsers(): Promise<User[]> {
// // const { data, error } = await supabase.auth.admin.getUsers();
// // if (error) {
// // console.error("Error fetching users:", error);
// // throw new Error(error.message);
// // }
// // return data.users.map((user) => ({
// // ...user,
// // })) as User[];
// const users = await db.users.findMany({
// include: {
// profile: true,
// },
// });
// if (!users) {
// throw new Error("Users not found");
// }
// console.log("fetchedUsers");
// return users;
// }
// // get current user
// export async function getCurrentUser(): Promise<UserResponse> {
// const supabase = await createClient();
// const {
// data: { user },
// error,
// } = await supabase.auth.getUser();
// if (error) {
// console.error("Error fetching current user:", error);
// throw new Error(error.message);
// }
// const userDetail = await db.users.findUnique({
// where: {
// id: user?.id,
// },
// include: {
// profile: true,
// },
// });
// if (!userDetail) {
// throw new Error("User not found");
// }
// return {
// data: {
// user: userDetail,
// },
// error: null,
// };
// }
// // Create a new user
// export async function createUser(
// params: CreateUser
// ): Promise<UserResponse> {
// const supabase = createAdminClient();
// const { data, error } = await supabase.auth.admin.createUser({
// email: params.email,
// password: params.password,
// phone: params.phone,
// email_confirm: params.email_confirm,
// });
// if (error) {
// console.error("Error creating user:", error);
// throw new Error(error.message);
// }
// return {
// data: {
// user: data.user,
// },
// error: null,
// };
// }
// export async function uploadAvatar(userId: string, email: string, file: File) {
// try {
// const supabase = await createClient();
// const fileExt = file.name.split(".").pop();
// const emailName = email.split("@")[0];
// const fileName = `AVR-${emailName}.${fileExt}`;
// // Change this line - store directly in the user's folder
// const filePath = `${userId}/${fileName}`;
// // Upload the avatar to Supabase storage
// const { error: uploadError } = await supabase.storage
// .from("avatars")
// .upload(filePath, file, {
// upsert: true,
// contentType: file.type,
// });
// if (uploadError) {
// console.error("Error uploading avatar:", uploadError);
// throw uploadError;
// }
// // Get the public URL
// const {
// data: { publicUrl },
// } = supabase.storage.from("avatars").getPublicUrl(filePath);
// // Update user profile with the new avatar URL
// await db.users.update({
// where: {
// id: userId,
// },
// data: {
// profile: {
// update: {
// avatar: publicUrl,
// },
// },
// },
// });
// return publicUrl;
// } catch (error) {
// console.error("Error uploading avatar:", error);
// throw error;
// }
// }
// // Update an existing user
// export async function updateUser(
// userId: string,
// params: UpdateUser
// ): Promise<UserResponse> {
// const supabase = createAdminClient();
// const { data, error } = await supabase.auth.admin.updateUserById(userId, {
// email: params.email,
// email_confirm: params.email_confirmed_at,
// password: params.encrypted_password ?? undefined,
// password_hash: params.encrypted_password ?? undefined,
// phone: params.phone,
// phone_confirm: params.phone_confirmed_at,
// role: params.role,
// user_metadata: params.user_metadata,
// app_metadata: params.app_metadata,
// });
// if (error) {
// console.error("Error updating user:", error);
// throw new Error(error.message);
// }
// const user = await db.users.findUnique({
// where: {
// id: userId,
// },
// include: {
// profile: true,
// },
// });
// if (!user) {
// throw new Error("User not found");
// }
// const updateUser = await db.users.update({
// where: {
// id: userId,
// },
// data: {
// role: params.role || user.role,
// invited_at: params.invited_at || user.invited_at,
// confirmed_at: params.confirmed_at || user.confirmed_at,
// // recovery_sent_at: params.recovery_sent_at || user.recovery_sent_at,
// last_sign_in_at: params.last_sign_in_at || user.last_sign_in_at,
// is_anonymous: params.is_anonymous || user.is_anonymous,
// created_at: params.created_at || user.created_at,
// updated_at: params.updated_at || user.updated_at,
// profile: {
// update: {
// avatar: params.profile?.avatar || user.profile?.avatar,
// username: params.profile?.username || user.profile?.username,
// first_name: params.profile?.first_name || user.profile?.first_name,
// last_name: params.profile?.last_name || user.profile?.last_name,
// bio: params.profile?.bio || user.profile?.bio,
// address: params.profile?.address || user.profile?.address,
// birth_date: params.profile?.birth_date || user.profile?.birth_date,
// },
// },
// },
// include: {
// profile: true,
// },
// });
// return {
// data: {
// user: {
// ...data.user,
// role: params.role,
// profile: {
// user_id: userId,
// ...updateUser.profile,
// },
// },
// },
// error: null,
// };
// }
// // Delete a user
// export async function deleteUser(userId: string): Promise<void> {
// const supabase = createAdminClient();
// const { error } = await supabase.auth.admin.deleteUser(userId);
// if (error) {
// console.error("Error deleting user:", error);
// throw new Error(error.message);
// }
// }
// // Send password recovery email
// export async function sendPasswordRecovery(email: string): Promise<void> {
// const supabase = createAdminClient();
// const { error } = await supabase.auth.resetPasswordForEmail(email, {
// redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/reset-password`,
// });
// if (error) {
// console.error("Error sending password recovery:", error);
// throw new Error(error.message);
// }
// }
// // Send magic link
// export async function sendMagicLink(email: string): Promise<void> {
// const supabase = createAdminClient();
// const { error } = await supabase.auth.signInWithOtp({
// email,
// options: {
// emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
// },
// });
// if (error) {
// console.error("Error sending magic link:", error);
// throw new Error(error.message);
// }
// }
// // Ban a user
// export async function banUser(userId: string): Promise<UserResponse> {
// const supabase = createAdminClient();
// // Ban for 100 years (effectively permanent)
// const banUntil = new Date();
// banUntil.setFullYear(banUntil.getFullYear() + 100);
// const { data, error } = await supabase.auth.admin.updateUserById(userId, {
// ban_duration: "100h",
// });
// if (error) {
// console.error("Error banning user:", error);
// throw new Error(error.message);
// }
// return {
// data: {
// user: data.user,
// },
// error: null,
// };
// }
// // Unban a user
// export async function unbanUser(userId: string): Promise<UserResponse> {
// const supabase = createAdminClient();
// const { data, error } = await supabase.auth.admin.updateUserById(userId, {
// ban_duration: "none",
// });
// if (error) {
// console.error("Error unbanning user:", error);
// throw new Error(error.message);
// }
// const user = await db.users.findUnique({
// where: {
// id: userId,
// },
// select: {
// banned_until: true,
// }
// })
// if (!user) {
// throw new Error("User not found");
// }
// // const updateUser = await db.users.update({
// // where: {
// // id: userId,
// // },
// // data: {
// // banned_until: null,
// // },
// // })
// return {
// data: {
// user: data.user,
// },
// error: null,
// };
// }
// // Invite a user
// export async function inviteUser(params: InviteUser): Promise<void> {
// const supabase = createAdminClient();
// const { error } = await supabase.auth.admin.inviteUserByEmail(params.email, {
// redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
// });
// if (error) {
// console.error("Error inviting user:", error);
// throw new Error(error.message);
// }
// }
getUsers(): Promise<IUserSchema[]>;
getCurrentUser(): Promise<IUserSchema>;
getUserById(credential: ICredentialGetUserByIdSchema): Promise<IUserSchema | undefined>;
getUserByUsername(credential: ICredentialGetUserByUsernameSchema): Promise<IUserSchema | undefined>;
getUserByEmail(credential: ICredentialGetUserByEmailSchema): Promise<IUserSchema | undefined>;
createUser(input: ICreateUserSchema, tx?: ITransaction): Promise<IUserSchema>;
inviteUser(credential: ICredentialsInviteUserSchema, tx?: ITransaction): Promise<IUserSchema>;
updateUser(credential: ICredentialUpdateUserSchema, input: Partial<IUpdateUserSchema>, tx?: ITransaction): Promise<IUserSchema>;
deleteUser(credential: ICredentialsDeleteUserSchema, tx?: ITransaction): Promise<IUserSchema>;
banUser(credential: ICredentialsBanUserSchema, input: IBanUserSchema, tx?: ITransaction): Promise<IUserSchema>;
unbanUser(credential: ICredentialsUnbanUserSchema, tx?: ITransaction): Promise<IUserSchema>;
}

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -1,18 +1,27 @@
export class DatabaseOperationError extends Error {
constructor(message: string, options?: ErrorOptions) {
super(message, options);
}
constructor(message: string, options?: ErrorOptions) {
super(message, options);
}
export class NotFoundError extends Error {
constructor(message: string, options?: ErrorOptions) {
super(message, options);
}
}
export class NotFoundError extends Error {
constructor(message: string, options?: ErrorOptions) {
super(message, options);
}
export class InputParseError extends Error {
constructor(message: string, options?: ErrorOptions) {
super(message, options);
}
}
export class InputParseError extends Error {
constructor(message: string, options?: ErrorOptions) {
super(message, options);
}
}
export class ServerActionError extends Error {
code: string;
constructor(message: string, code: string) {
super(message);
this.name = "ServerActionError";
this.code = code;
}
}

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

View File

@ -5,32 +5,42 @@ export const SignUpSchema = z.object({
.string()
.min(1, { message: "Email is required" })
.email({ message: "Please enter a valid email address" }),
password: z.string().min(1, { message: "Password is required" }),
password: z.string().min(1, { message: "Password is required" }).min(8, { message: "Password must be at least 8 characters" }),
phone: z.string().optional(),
})
export type SignUpFormData = z.infer<typeof SignUpSchema>;
export type TSignUpSchema = z.infer<typeof SignUpSchema>;
export const SignUpWithEmail = SignUpSchema.pick({
export const SignUpWithEmailSchema = SignUpSchema.pick({
email: true,
password: true,
})
export const defaultSignUpWithEmailValues: SignUpWithEmail = {
export type ISignUpWithEmailSchema = z.infer<typeof SignUpWithEmailSchema>
export const defaulISignUpWithEmailSchemaValues: ISignUpWithEmailSchema = {
email: "",
password: "",
}
export type SignUpWithEmail = z.infer<typeof SignUpWithEmail>
export const SignUpWithPhone = SignUpSchema.pick({
export const SignUpWithPhoneSchema = SignUpSchema.pick({
phone: true,
password: true,
})
export const defaultSignUpWithPhoneValues: SignUpWithPhone = {
export type ISignUpWithPhoneSchema = z.infer<typeof SignUpWithPhoneSchema>
export const defaulISignUpWithPhoneSchemaValues: ISignUpWithPhoneSchema = {
phone: "",
password: "",
}
export type SignUpWithPhone = z.infer<typeof SignUpWithPhone>
export const SignUpWithOtpSchema = SignUpSchema.pick({
email: true,
})
export type TSignUpWithOtpSchema = z.infer<typeof SignUpWithOtpSchema>
export const defaultSignUpWithOtpSchemaValues: TSignUpWithOtpSchema = {
email: "",
}

View File

@ -5,9 +5,9 @@ export const verifyOtpSchema = z.object({
token: z.string().length(6, { message: "OTP must be 6 characters long" }),
});
export type VerifyOtpFormData = z.infer<typeof verifyOtpSchema>;
export type IVerifyOtpSchema = z.infer<typeof verifyOtpSchema>;
export const defaultVerifyOtpValues: VerifyOtpFormData = {
export const defaultVerifyOtpValues: IVerifyOtpSchema = {
email: "",
token: "",
};

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(),
});
export type User = z.infer<typeof UserSchema>;
export type IUserSchema = z.infer<typeof UserSchema>;
export const ProfileSchema = z.object({
id: z.string(),
@ -80,59 +80,30 @@ export const ProfileSchema = z.object({
birth_date: z.string().optional(),
});
export type Profile = z.infer<typeof ProfileSchema>;
export type IProfileSchema = z.infer<typeof ProfileSchema>;
export const CreateUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
phone: z.string().optional(),
email_confirm: z.boolean().optional(),
});
// export type UserFilterOptions = {
// email: string
// phone: string
// lastSignIn: string
// createdAt: string
// status: string[]
// }
export type CreateUser = z.infer<typeof CreateUserSchema>;
export const UserFilterOptionsSchema = z.object({
email: z.string(),
phone: z.string(),
lastSignIn: z.string(),
createdAt: z.string(),
status: z.array(z.string()),
})
export const UpdateUserSchema = z.object({
email: z.string().email().optional(),
email_confirmed_at: z.boolean().optional(),
encrypted_password: z.string().optional(),
role: z.enum(["user", "staff", "admin"]).optional(),
phone: z.string().optional(),
phone_confirmed_at: z.boolean().optional(),
invited_at: z.union([z.string(), z.date()]).optional(),
confirmed_at: z.union([z.string(), z.date()]).optional(),
// recovery_sent_at: z.union([z.string(), z.date()]).optional(),
last_sign_in_at: z.union([z.string(), z.date()]).optional(),
created_at: z.union([z.string(), z.date()]).optional(),
updated_at: z.union([z.string(), z.date()]).optional(),
is_anonymous: z.boolean().optional(),
user_metadata: z.record(z.any()).optional(),
app_metadata: z.record(z.any()).optional(),
profile: z
.object({
id: z.string().optional(),
user_id: z.string(),
avatar: z.string().optional(),
username: z.string().optional(),
first_name: z.string().optional(),
last_name: z.string().optional(),
bio: z.string().optional(),
address: z.any().optional(),
birth_date: z.date().optional(),
})
});
export type UpdateUser = z.infer<typeof UpdateUserSchema>;
export const InviteUserSchema = z.object({
email: z.string().email(),
});
export type InviteUser = z.infer<typeof InviteUserSchema>;
export type IUserFilterOptionsSchema = z.infer<typeof UserFilterOptionsSchema>;
export type UserResponse =
| {
data: {
user: User;
user: IUserSchema;
};
error: null;
}

View File

@ -3,11 +3,18 @@ import { ICrashReporterService } from "@/src/application/services/crash-reporter
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
import { createAdminClient } from "@/app/_utils/supabase/admin";
import { createClient as createServerClient } from "@/app/_utils/supabase/server";
import { CreateUser, UpdateUser, User, UserResponse } from "@/src/entities/models/users/users.model";
import { IUserSchema } from "@/src/entities/models/users/users.model";
import { ITransaction } from "@/src/entities/models/transaction.interface";
import db from "@/prisma/db";
import { DatabaseOperationError, NotFoundError } from "@/src/entities/errors/common";
import { AuthenticationError } from "@/src/entities/errors/auth";
import { ICredentialGetUserByEmailSchema, ICredentialGetUserByUsernameSchema, IGetUserByIdSchema } from "@/src/entities/models/users/read-user.model";
import { ICredentialsInviteUserSchema } from "@/src/entities/models/users/invite-user.model";
import { ICredentialUpdateUserSchema, IUpdateUserSchema } from "@/src/entities/models/users/update-user.model";
import { ICredentialsDeleteUserSchema } from "@/src/entities/models/users/delete-user.model";
import { IBanUserSchema, ICredentialsBanUserSchema } from "@/src/entities/models/users/ban-user.model";
import { ICredentialsUnbanUserSchema } from "@/src/entities/models/users/unban-user.model";
import { ICreateUserSchema } from "@/src/entities/models/users/create-user.model";
export class UsersRepository implements IUsersRepository {
constructor(
@ -17,7 +24,7 @@ export class UsersRepository implements IUsersRepository {
private readonly supabaseServer = createServerClient()
) { }
async getUsers(): Promise<User[]> {
async getUsers(): Promise<IUserSchema[]> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > getUsers",
}, async () => {
@ -40,7 +47,7 @@ export class UsersRepository implements IUsersRepository {
)
if (!users) {
throw new NotFoundError("Users not found");
return [];
}
return users;
@ -51,14 +58,14 @@ export class UsersRepository implements IUsersRepository {
})
}
async getUserById(id: string): Promise<User | undefined> {
async getUserById(credential: IGetUserByIdSchema): Promise<IUserSchema | undefined> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > getUserById",
}, async () => {
try {
const query = db.users.findUnique({
where: {
id,
id: credential.id,
},
include: {
profile: true,
@ -66,7 +73,7 @@ export class UsersRepository implements IUsersRepository {
})
const user = await this.instrumentationService.startSpan({
name: `UsersRepository > getUserById > Prisma: db.users.findUnique(${id})`,
name: `UsersRepository > getUserById > Prisma: db.users.findUnique(${credential.id})`,
op: "db:query",
attributes: { "system": "prisma" },
},
@ -81,7 +88,7 @@ export class UsersRepository implements IUsersRepository {
return {
...user,
id,
id: credential.id,
};
} catch (err) {
this.crashReporterService.report(err);
@ -90,7 +97,7 @@ export class UsersRepository implements IUsersRepository {
})
}
async getUserByUsername(username: string): Promise<User | undefined> {
async getUserByUsername(credential: ICredentialGetUserByUsernameSchema): Promise<IUserSchema | undefined> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > getUserByUsername",
}, async () => {
@ -98,7 +105,7 @@ export class UsersRepository implements IUsersRepository {
const query = db.users.findFirst({
where: {
profile: {
username,
username: credential.username,
},
},
include: {
@ -107,7 +114,7 @@ export class UsersRepository implements IUsersRepository {
})
const user = await this.instrumentationService.startSpan({
name: `UsersRepository > getUserByUsername > Prisma: db.users.findFirst(${username})`,
name: `UsersRepository > getUserByUsername > Prisma: db.users.findFirst(${credential.username})`,
op: "db:query",
attributes: { "system": "prisma" },
},
@ -130,7 +137,7 @@ export class UsersRepository implements IUsersRepository {
})
}
async getUserByEmail(email: string): Promise<User | undefined> {
async getUserByEmail(credential: ICredentialGetUserByEmailSchema): Promise<IUserSchema | undefined> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > getUserByEmail",
}, async () => {
@ -138,7 +145,7 @@ export class UsersRepository implements IUsersRepository {
const query = db.users.findUnique({
where: {
email,
email: credential.email,
},
include: {
profile: true,
@ -146,7 +153,7 @@ export class UsersRepository implements IUsersRepository {
})
const user = await this.instrumentationService.startSpan({
name: `UsersRepository > getUserByEmail > Prisma: db.users.findUnique(${email})`,
name: `UsersRepository > getUserByEmail > Prisma: db.users.findUnique(${credential.email})`,
op: "db:query",
attributes: { "system": "prisma" },
},
@ -156,7 +163,7 @@ export class UsersRepository implements IUsersRepository {
)
if (!user) {
throw new NotFoundError("User not found");
return undefined;
}
return user;
@ -167,7 +174,7 @@ export class UsersRepository implements IUsersRepository {
})
}
async getCurrentUser(): Promise<User> {
async getCurrentUser(): Promise<IUserSchema> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > getCurrentUser",
}, async () => {
@ -176,7 +183,7 @@ export class UsersRepository implements IUsersRepository {
const query = supabase.auth.getUser();
const { data, error } = await this.instrumentationService.startSpan({
const { data: { user }, error } = await this.instrumentationService.startSpan({
name: "UsersRepository > getCurrentUser > supabase.auth.getUser",
op: "db:query",
attributes: { "system": "supabase.auth" },
@ -190,14 +197,11 @@ export class UsersRepository implements IUsersRepository {
throw new AuthenticationError("Failed to get current user");
}
if (!data) {
if (!user) {
throw new NotFoundError("User not found");
}
return {
...data,
id: data.user.id,
};
return user;
} catch (err) {
this.crashReporterService.report(err);
throw err;
@ -205,14 +209,21 @@ export class UsersRepository implements IUsersRepository {
})
}
async createUser(input: CreateUser, tx?: ITransaction): Promise<User> {
async createUser(input: ICreateUserSchema, tx?: ITransaction): Promise<IUserSchema> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > createUser",
}, async () => {
try {
console.log("Create User");
const supabase = this.supabaseAdmin;
const query = supabase.auth.admin.createUser(input)
const query = supabase.auth.admin.createUser({
email: input.email,
password: input.password,
email_confirm: true,
})
const { data: { user } } = await this.instrumentationService.startSpan({
name: "UsersRepository > createUser > supabase.auth.admin.createUser",
@ -237,14 +248,14 @@ export class UsersRepository implements IUsersRepository {
})
}
async inviteUser(email: string, tx?: ITransaction): Promise<User> {
async inviteUser(credential: ICredentialsInviteUserSchema, tx?: ITransaction): Promise<IUserSchema> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > inviteUser",
}, async () => {
try {
const supabase = this.supabaseAdmin;
const query = supabase.auth.admin.inviteUserByEmail(email);
const query = supabase.auth.admin.inviteUserByEmail(credential.email);
const { data: { user } } = await this.instrumentationService.startSpan({
name: "UsersRepository > inviteUser > supabase.auth.admin.inviteUserByEmail",
@ -268,14 +279,14 @@ export class UsersRepository implements IUsersRepository {
})
}
async updateUser(id: string, input: Partial<UpdateUser>, tx?: ITransaction): Promise<User> {
async updateUser(credential: ICredentialUpdateUserSchema, input: Partial<IUpdateUserSchema>, tx?: ITransaction): Promise<IUserSchema> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > updateUser",
}, async () => {
try {
const supabase = this.supabaseAdmin;
const queryUpdateSupabaseUser = supabase.auth.admin.updateUserById(id, {
const queryUpdateSupabaseUser = supabase.auth.admin.updateUserById(credential.id, {
email: input.email,
email_confirm: input.email_confirmed_at,
password: input.encrypted_password ?? undefined,
@ -303,7 +314,7 @@ export class UsersRepository implements IUsersRepository {
const queryGetUser = db.users.findUnique({
where: {
id,
id: credential.id,
},
include: {
profile: true,
@ -326,7 +337,7 @@ export class UsersRepository implements IUsersRepository {
const queryUpdateUser = db.users.update({
where: {
id,
id: credential.id,
},
data: {
role: input.role || user.role,
@ -369,7 +380,7 @@ export class UsersRepository implements IUsersRepository {
return {
...updatedUser,
id,
id: credential.id,
};
} catch (err) {
@ -379,14 +390,14 @@ export class UsersRepository implements IUsersRepository {
})
}
async deleteUser(id: string, tx?: ITransaction): Promise<User> {
async deleteUser(credential: ICredentialsDeleteUserSchema, tx?: ITransaction): Promise<IUserSchema> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > deleteUser",
}, async () => {
try {
const supabase = this.supabaseAdmin;
const query = supabase.auth.admin.deleteUser(id);
const query = supabase.auth.admin.deleteUser(credential.id);
const { data: user, error } = await this.instrumentationService.startSpan({
name: "UsersRepository > deleteUser > supabase.auth.admin.deleteUser",
@ -404,7 +415,7 @@ export class UsersRepository implements IUsersRepository {
return {
...user,
id
id: credential.id,
};
} catch (err) {
@ -414,15 +425,15 @@ export class UsersRepository implements IUsersRepository {
})
}
async banUser(id: string, ban_duration: string, tx?: ITransaction): Promise<User> {
async banUser(credential: ICredentialsBanUserSchema, input: IBanUserSchema, tx?: ITransaction): Promise<IUserSchema> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > banUser",
}, async () => {
try {
const supabase = this.supabaseAdmin;
const query = supabase.auth.admin.updateUserById(id, {
ban_duration: ban_duration ?? "100h",
const query = supabase.auth.admin.updateUserById(credential.id, {
ban_duration: input.ban_duration ?? "24h",
})
const { data: user, error } = await this.instrumentationService.startSpan({
@ -441,7 +452,7 @@ export class UsersRepository implements IUsersRepository {
return {
...user,
id
id: credential.id,
};
} catch (err) {
@ -452,14 +463,14 @@ export class UsersRepository implements IUsersRepository {
}
async unbanUser(id: string, tx?: ITransaction): Promise<User> {
async unbanUser(credential: ICredentialsUnbanUserSchema, tx?: ITransaction): Promise<IUserSchema> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > unbanUser",
}, async () => {
try {
const supabase = this.supabaseAdmin;
const query = supabase.auth.admin.updateUserById(id, {
const query = supabase.auth.admin.updateUserById(credential.id, {
ban_duration: "none",
})
@ -479,7 +490,7 @@ export class UsersRepository implements IUsersRepository {
return {
...user,
id
id: credential.id,
};
} catch (err) {

View File

@ -6,11 +6,13 @@ import { IAuthenticationService } from "@/src/application/services/authenticatio
import { ICrashReporterService } from "@/src/application/services/crash-reporter.service.interface";
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
import { AuthenticationError } from "@/src/entities/errors/auth";
import { ISendMagicLinkSchema } from "@/src/entities/models/auth/send-magic-link.model";
import { ISendPasswordRecoverySchema } from "@/src/entities/models/auth/send-password-recovery.model";
import { Session } from "@/src/entities/models/auth/session.model";
import { SignInPasswordless, SignInWithPassword } from "@/src/entities/models/auth/sign-in.model";
import { SignUpWithEmail, SignUpWithPhone } from "@/src/entities/models/auth/sign-up.model";
import { VerifyOtpFormData } from "@/src/entities/models/auth/verify-otp.model";
import { User } from "@/src/entities/models/users/users.model";
import { SignInWithPasswordSchema, ISignInPasswordlessSchema, ISignInWithPasswordSchema } from "@/src/entities/models/auth/sign-in.model";
import { SignUpWithEmailSchema, SignUpWithPhoneSchema, ISignUpWithEmailSchema, ISignUpWithPhoneSchema } from "@/src/entities/models/auth/sign-up.model";
import { IVerifyOtpSchema } from "@/src/entities/models/auth/verify-otp.model";
import { IUserSchema } from "@/src/entities/models/users/users.model";
export class AuthenticationService implements IAuthenticationService {
constructor(
@ -21,7 +23,7 @@ export class AuthenticationService implements IAuthenticationService {
private readonly supabaseServer = createClient()
) { }
async signInPasswordless(credentials: SignInPasswordless): Promise<void> {
async signInPasswordless(credentials: ISignInPasswordlessSchema): Promise<void> {
return await this.instrumentationService.startSpan({
name: "signInPasswordless Use Case",
}, async () => {
@ -49,9 +51,9 @@ export class AuthenticationService implements IAuthenticationService {
})
}
async signInWithPassword(credentials: SignInWithPassword): Promise<void> {
async SignInWithPasswordSchema(credentials: ISignInWithPasswordSchema): Promise<void> {
return await this.instrumentationService.startSpan({
name: "signInWithPassword Use Case",
name: "SignInWithPasswordSchema Use Case",
}, async () => {
try {
const supabase = await this.supabaseServer
@ -77,9 +79,9 @@ export class AuthenticationService implements IAuthenticationService {
})
}
async signUpWithEmail(credentials: SignUpWithEmail): Promise<User> {
async SignUpWithEmailSchema(credentials: ISignUpWithEmailSchema): Promise<IUserSchema> {
return await this.instrumentationService.startSpan({
name: "signUpWithEmail Use Case",
name: "SignUpWithEmailSchema Use Case",
}, async () => {
try {
const supabase = await this.supabaseServer
@ -122,7 +124,7 @@ export class AuthenticationService implements IAuthenticationService {
})
}
async signUpWithPhone(credentials: SignUpWithPhone): Promise<User> {
async SignUpWithPhoneSchema(credentials: ISignUpWithPhoneSchema): Promise<IUserSchema> {
throw new Error("Method not implemented.");
}
@ -190,14 +192,14 @@ export class AuthenticationService implements IAuthenticationService {
})
}
async sendMagicLink(email: string): Promise<void> {
async sendMagicLink(credentials: ISendMagicLinkSchema): Promise<void> {
return await this.instrumentationService.startSpan({
name: "sendMagicLink Use Case",
}, async () => {
try {
const supabase = await this.supabaseServer
const magicLink = supabase.auth.signInWithOtp({ email })
const magicLink = supabase.auth.signInWithOtp({ email: credentials.email })
await this.instrumentationService.startSpan({
name: "supabase.auth.signIn",
@ -216,14 +218,14 @@ export class AuthenticationService implements IAuthenticationService {
})
}
async sendPasswordRecovery(email: string): Promise<void> {
async sendPasswordRecovery(credentials: ISendPasswordRecoverySchema): Promise<void> {
return await this.instrumentationService.startSpan({
name: "sendPasswordRecovery Use Case",
}, async () => {
try {
const supabase = await this.supabaseServer
const passwordRecovery = supabase.auth.resetPasswordForEmail(email, {
const passwordRecovery = supabase.auth.resetPasswordForEmail(credentials.email, {
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/reset-password`,
})
@ -244,7 +246,7 @@ export class AuthenticationService implements IAuthenticationService {
})
}
async verifyOtp(credentials: VerifyOtpFormData): Promise<void> {
async verifyOtp(credentials: IVerifyOtpSchema): Promise<void> {
return await this.instrumentationService.startSpan({
name: "verifyOtp Use Case",
}, async () => {

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
const signInInputSchema = z.object({
email: z.string().email("Please enter a valid email address"),
email: z.string().min(1, "Email is Required").email("Please enter a valid email address"),
})
export type ISignInController = ReturnType<typeof signInController>
@ -20,7 +20,7 @@ export const signInController =
const { data, error: inputParseError } = signInInputSchema.safeParse(input)
if (inputParseError) {
throw new InputParseError("Invalid data", { cause: inputParseError })
throw new InputParseError(inputParseError.errors[0].message)
}
return await signInUseCase({

View File

@ -1,27 +1,38 @@
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
import { IBanUserUseCase } from "@/src/application/use-cases/users/ban-user.use-case";
import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case";
import { InputParseError } from "@/src/entities/errors/common";
import { BanDurationSchema, ICredentialsBanUserSchema } from "@/src/entities/models/users/ban-user.model";
import { z } from "zod";
const inputSchema = z.object({
id: z.string(),
ban_duration: z.string()
ban_duration: BanDurationSchema
})
export type IBanUserController = ReturnType<typeof banUserController>
export const banUserController = (
instrumentationService: IInstrumentationService,
banUserUseCase: IBanUserUseCase
banUserUseCase: IBanUserUseCase,
getCurrentUserUseCase: IGetCurrentUserUseCase
) =>
async (input: Partial<z.infer<typeof inputSchema>>) => {
async (credential: ICredentialsBanUserSchema, input: Partial<z.infer<typeof inputSchema>>) => {
return await instrumentationService.startSpan({ name: "banUser Controller" }, async () => {
const session = await getCurrentUserUseCase();
if (!session) {
throw new InputParseError("Must be logged in to ban a user");
}
const { data, error: inputParseError } = inputSchema.safeParse(input);
if (inputParseError) {
throw new InputParseError("Invalid data", { cause: inputParseError });
}
return await banUserUseCase(data.id, data.ban_duration);
console.log("Controller: Ban User");
return await banUserUseCase({ id: credential.id }, { ban_duration: data.ban_duration });
})
}

View File

@ -2,11 +2,24 @@ import { IUsersRepository } from "@/src/application/repositories/users.repositor
import { IAuthenticationService } from "@/src/application/services/authentication.service.interface"
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
import { ICreateUserUseCase } from "@/src/application/use-cases/users/create-user.use-case"
import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case"
import { UnauthenticatedError } from "@/src/entities/errors/auth"
import { InputParseError } from "@/src/entities/errors/common"
import { CreateUserSchema } from "@/src/entities/models/users/users.model"
import { CreateUserSchema } from "@/src/entities/models/users/create-user.model"
import { z } from "zod"
// const inputSchema = z.object({
// email: z.string().min(1, { message: "Email is required" }).email({ message: "Please enter a valid email address" }),
// password: z
// .string()
// .min(1, { message: "Password is required" })
// .min(8, { message: "Password must be at least 8 characters" })
// .regex(/[A-Z]/, { message: "Password must contain at least one uppercase letter" })
// .regex(/[a-z]/, { message: "Password must contain at least one lowercase letter" })
// .regex(/[0-9]/, { message: "Password must contain at least one number" }),
// email_confirm: z.boolean().optional(),
// })
const inputSchema = CreateUserSchema
export type ICreateUserController = ReturnType<typeof createUserController>
@ -14,20 +27,23 @@ export type ICreateUserController = ReturnType<typeof createUserController>
export const createUserController = (
instrumentationService: IInstrumentationService,
createUserUseCase: ICreateUserUseCase,
authenticationService: IAuthenticationService
getCurrentUserUseCase: IGetCurrentUserUseCase
) => async (input: Partial<z.infer<typeof inputSchema>>) => {
return await instrumentationService.startSpan({ name: "createUser Controller" }, async () => {
const session = await authenticationService.getSession()
const session = await getCurrentUserUseCase()
if (!session) {
throw new UnauthenticatedError("Must be logged in to create a todo")
}
if (!session) {
throw new UnauthenticatedError("Must be logged in to create a user")
}
const { data, error: inputParseError } = inputSchema.safeParse(input)
const { data, error: inputParseError } = inputSchema.safeParse(input)
if (inputParseError) {
throw new InputParseError("Invalid data", { cause: inputParseError })
}
if (inputParseError) {
throw new InputParseError(inputParseError.errors[0].message)
}
return await createUserUseCase(data);
return await createUserUseCase(data);
})
}

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,16 @@
import { IUsersRepository } from "@/src/application/repositories/users.repository.interface"
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
import { IGetUsersUseCase } from "@/src/application/use-cases/users/get-users.use-case"
export type IGetUserController = ReturnType<typeof getUsersController>
export type IGetUsersController = ReturnType<typeof getUsersController>
export const getUsersController =
(
instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository
getUsersUseCase: IGetUsersUseCase
) =>
async () => {
return await instrumentationService.startSpan({ name: "getgetUsers Controller" }, async () => {
return await usersRepository.getUsers();
return await instrumentationService.startSpan({ name: "geIGetUsers Controller" }, async () => {
return await getUsersUseCase()
})
}

View File

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

View File

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

View File

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