refactor add user and invite user
This commit is contained in:
parent
e95bd8cb23
commit
0af8a9be0b
|
@ -16,29 +16,27 @@ import {
|
||||||
import { NavPreMain } from "./navigations/nav-pre-main";
|
import { NavPreMain } from "./navigations/nav-pre-main";
|
||||||
import { navData } from "@/prisma/data/nav";
|
import { navData } from "@/prisma/data/nav";
|
||||||
import { TeamSwitcher } from "../../../_components/team-switcher";
|
import { TeamSwitcher } from "../../../_components/team-switcher";
|
||||||
|
import { useGetCurrentUserQuery } from "../dashboard/user-management/queries";
|
||||||
|
|
||||||
import { Profile, User } from "@/src/entities/models/users/users.model";
|
|
||||||
import { getCurrentUser } from "@/app/(pages)/(admin)/dashboard/user-management/action";
|
|
||||||
|
|
||||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
const [user, setUser] = React.useState<User | null>(null);
|
const { data: user, isPending, error } = useGetCurrentUserQuery()
|
||||||
const [isLoading, setIsLoading] = React.useState(true);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
// React.useEffect(() => {
|
||||||
async function fetchUser() {
|
// async function fetchUser() {
|
||||||
try {
|
// try {
|
||||||
setIsLoading(true);
|
// setIsLoading(true);
|
||||||
const userData = await getCurrentUser();
|
// const userData = await getCurrentUser();
|
||||||
setUser(userData.data.user);
|
// setUser(userData.data.user);
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.error("Failed to fetch user:", error);
|
// console.error("Failed to fetch user:", error);
|
||||||
} finally {
|
// } finally {
|
||||||
setIsLoading(false);
|
// setIsLoading(false);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
fetchUser();
|
// fetchUser();
|
||||||
}, []);
|
// }, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon" {...props}>
|
<Sidebar collapsible="icon" {...props}>
|
||||||
|
@ -51,7 +49,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
<NavReports reports={navData.reports} />
|
<NavReports reports={navData.reports} />
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<NavUser user={user} />
|
<NavUser user={user ?? null} />
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
<SidebarRail />
|
<SidebarRail />
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
|
|
@ -24,13 +24,13 @@ import {
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from "@/app/_components/ui/sidebar";
|
} from "@/app/_components/ui/sidebar";
|
||||||
import { IconLogout, IconSettings, IconSparkles } from "@tabler/icons-react";
|
import { IconLogout, IconSettings, IconSparkles } from "@tabler/icons-react";
|
||||||
import type { User } from "@/src/entities/models/users/users.model";
|
import type { IUserSchema } from "@/src/entities/models/users/users.model";
|
||||||
// import { signOut } from "@/app/(pages)/(auth)/action";
|
// import { signOut } from "@/app/(pages)/(auth)/action";
|
||||||
import { SettingsDialog } from "../settings/setting-dialog";
|
import { SettingsDialog } from "../settings/setting-dialog";
|
||||||
import { useSignOutHandler } from "@/app/(pages)/(auth)/handler";
|
import { useSignOutHandler } from "@/app/(pages)/(auth)/handler";
|
||||||
import { AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogCancel, AlertDialogAction } from "@/app/_components/ui/alert-dialog";
|
import { AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogCancel, AlertDialogAction } from "@/app/_components/ui/alert-dialog";
|
||||||
|
|
||||||
export function NavUser({ user }: { user: User | null }) {
|
export function NavUser({ user }: { user: IUserSchema | null }) {
|
||||||
const { isMobile } = useSidebar();
|
const { isMobile } = useSidebar();
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
import type { User } from "@/src/entities/models/users/users.model";
|
import type { IUserSchema } from "@/src/entities/models/users/users.model";
|
||||||
|
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
@ -30,7 +30,6 @@ import { useRef, useState } from "react";
|
||||||
import { ScrollArea } from "@/app/_components/ui/scroll-area";
|
import { ScrollArea } from "@/app/_components/ui/scroll-area";
|
||||||
import {
|
import {
|
||||||
updateUser,
|
updateUser,
|
||||||
uploadAvatar,
|
|
||||||
} from "@/app/(pages)/(admin)/dashboard/user-management/action";
|
} from "@/app/(pages)/(admin)/dashboard/user-management/action";
|
||||||
|
|
||||||
const profileFormSchema = z.object({
|
const profileFormSchema = z.object({
|
||||||
|
@ -41,7 +40,7 @@ const profileFormSchema = z.object({
|
||||||
type ProfileFormValues = z.infer<typeof profileFormSchema>;
|
type ProfileFormValues = z.infer<typeof profileFormSchema>;
|
||||||
|
|
||||||
interface ProfileSettingsProps {
|
interface ProfileSettingsProps {
|
||||||
user: User | null;
|
user: IUserSchema | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProfileSettings({ user }: ProfileSettingsProps) {
|
export function ProfileSettings({ user }: ProfileSettingsProps) {
|
||||||
|
@ -70,12 +69,12 @@ export function ProfileSettings({ user }: ProfileSettingsProps) {
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
|
|
||||||
// Upload avatar to storage
|
// Upload avatar to storage
|
||||||
const publicUrl = await uploadAvatar(user.id, user.email, file);
|
// const publicUrl = await uploadAvatar(user.id, user.email, file);
|
||||||
|
|
||||||
console.log("publicUrl", publicUrl);
|
// console.log("publicUrl", publicUrl);
|
||||||
|
|
||||||
// Update the form value
|
// Update the form value
|
||||||
form.setValue("avatar", publicUrl);
|
// form.setValue("avatar", publicUrl);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error uploading avatar:", error);
|
console.error("Error uploading avatar:", error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -3,15 +3,17 @@ import { createClient } from "@/app/_utils/supabase/server";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export default async function DashboardPage() {
|
export default async function DashboardPage() {
|
||||||
const supabase = await createClient();
|
// const supabase = await createClient();
|
||||||
|
|
||||||
const {
|
// const {
|
||||||
data: { session },
|
// data: { user },
|
||||||
} = await supabase.auth.getSession();
|
// } = await supabase.auth.getUser();
|
||||||
|
|
||||||
if (!session) {
|
// if (!user) {
|
||||||
return redirect("/sign-in");
|
// return redirect("/sign-in");
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
// console.log("user", user);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -20,7 +22,7 @@ export default async function DashboardPage() {
|
||||||
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
|
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||||
<div className="aspect-video rounded-xl bg-muted/50">
|
<div className="aspect-video rounded-xl bg-muted/50">
|
||||||
<pre className="text-xs font-mono p-3 rounded border overflow-auto">
|
<pre className="text-xs font-mono p-3 rounded border overflow-auto">
|
||||||
{JSON.stringify(session, null, 2)}
|
{/* {JSON.stringify(user, null, 2)} */}
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,155 +1,66 @@
|
||||||
import type React from "react";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/app/_components/ui/dialog"
|
||||||
|
import { Button } from "@/app/_components/ui/button"
|
||||||
import { useState } from "react";
|
import { Mail, Lock, Loader2 } from "lucide-react"
|
||||||
import {
|
import { useAddUserDialogHandler } from "../handler"
|
||||||
Dialog,
|
import { ReactHookFormField } from "@/app/_components/react-hook-form-field"
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/app/_components/ui/dialog";
|
|
||||||
import { Button } from "@/app/_components/ui/button";
|
|
||||||
import { Input } from "@/app/_components/ui/input";
|
|
||||||
import { Checkbox } from "@/app/_components/ui/checkbox";
|
|
||||||
import { createUser } from "@/app/(pages)/(admin)/dashboard/user-management/action";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Mail, Lock, Loader2, X } from "lucide-react";
|
|
||||||
import { useMutation } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
interface AddUserDialogProps {
|
interface AddUserDialogProps {
|
||||||
open: boolean;
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void
|
||||||
onUserAdded: () => void;
|
onUserAdded: () => void
|
||||||
}
|
}
|
||||||
|
export function AddUserDialog({ open, onOpenChange, onUserAdded }: AddUserDialogProps) {
|
||||||
export function AddUserDialog({
|
const {
|
||||||
open,
|
register,
|
||||||
onOpenChange,
|
errors,
|
||||||
onUserAdded,
|
isPending,
|
||||||
}: AddUserDialogProps) {
|
handleSubmit,
|
||||||
const [formData, setFormData] = useState({
|
handleOpenChange,
|
||||||
email: "",
|
} = useAddUserDialogHandler({ onUserAdded, onOpenChange });
|
||||||
password: "",
|
|
||||||
emailConfirm: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const { name, value } = e.target;
|
|
||||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const { mutate: createUserMutation, isPending } = useMutation({
|
|
||||||
mutationKey: ["createUser"],
|
|
||||||
mutationFn: createUser,
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("User created successfully.");
|
|
||||||
onUserAdded();
|
|
||||||
onOpenChange(false);
|
|
||||||
setFormData({
|
|
||||||
email: "",
|
|
||||||
password: "",
|
|
||||||
emailConfirm: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
toast.error("Failed to create user.");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await createUserMutation({
|
|
||||||
email: formData.email,
|
|
||||||
password: formData.password,
|
|
||||||
email_confirm: formData.emailConfirm,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
toast.error("Failed to create user.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
<DialogContent className="sm:max-w-md border-0 ">
|
<DialogContent className="sm:max-w-md border-0">
|
||||||
<DialogHeader className="flex flex-row items-center justify-between space-y-0 space-x-4 pb-4">
|
<DialogHeader className="flex flex-row items-center justify-between space-y-0 space-x-4 pb-4">
|
||||||
<DialogTitle className="text-xl font-semibold ">
|
<DialogTitle className="text-xl font-semibold">Create a new user</DialogTitle>
|
||||||
Create a new user
|
|
||||||
</DialogTitle>
|
|
||||||
{/* <Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8 text-zinc-400 hover: hover:bg-zinc-800"
|
|
||||||
onClick={() => onOpenChange(false)}
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button> */}
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="space-y-2">
|
<ReactHookFormField
|
||||||
<label htmlFor="email" className="text-sm text-zinc-400">
|
label="Email address"
|
||||||
Email address
|
icon={Mail}
|
||||||
</label>
|
placeholder="user@example.com"
|
||||||
<div className="relative">
|
error={errors.email}
|
||||||
<Mail className="absolute left-3 top-2.5 h-5 w-5 text-zinc-500" />
|
registration={register("email")}
|
||||||
<Input
|
/>
|
||||||
id="email"
|
|
||||||
name="email"
|
<ReactHookFormField
|
||||||
type="email"
|
label="Password"
|
||||||
required
|
icon={Lock}
|
||||||
placeholder="user@example.com"
|
placeholder="••••••••"
|
||||||
value={formData.email}
|
type="password"
|
||||||
onChange={handleInputChange}
|
error={errors.password}
|
||||||
className="pl-10 placeholder:text-zinc-500 "
|
registration={register("password")}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="password" className="text-sm text-zinc-400">
|
|
||||||
User Password
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<Lock className="absolute left-3 top-2.5 h-5 w-5 text-zinc-500" />
|
|
||||||
<Input
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
placeholder="••••••••"
|
|
||||||
value={formData.password}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
className="pl-10 placeholder:text-zinc-500 "
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center space-x-2">
|
{/* <div className="flex items-center space-x-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="email-confirm"
|
id="email_confirm"
|
||||||
checked={formData.emailConfirm}
|
{...register("email_confirm")}
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
emailConfirm: checked as boolean,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
className="border-zinc-700"
|
className="border-zinc-700"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="email-confirm" className="text-sm ">
|
<label htmlFor="email_confirm" className="text-sm">
|
||||||
Auto Confirm User?
|
Auto Confirm User?
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div> */}
|
||||||
<p className="text-sm text-zinc-500 pl-6">
|
<p className="text-sm text-zinc-500">
|
||||||
A confirmation email will not be sent when creating a user via
|
A confirmation email will not be sent when creating a user via this form.
|
||||||
this form.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button type="submit" disabled={isPending} className="w-full ">
|
<Button type="submit" disabled={isPending} className="w-full">
|
||||||
{isPending ? (
|
{isPending ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
@ -164,3 +75,4 @@ export function AddUserDialog({
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,11 @@ import { Textarea } from "@/app/_components/ui/textarea";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { inviteUser } from "@/app/(pages)/(admin)/dashboard/user-management/action";
|
import { inviteUser } from "@/app/(pages)/(admin)/dashboard/user-management/action";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { useInviteUserHandler } from "../handler";
|
||||||
|
import { ReactHookFormField } from "@/app/_components/react-hook-form-field";
|
||||||
|
import { Loader2, MailIcon } from "lucide-react";
|
||||||
|
import { Separator } from "@/app/_components/ui/separator";
|
||||||
|
|
||||||
|
|
||||||
interface InviteUserDialogProps {
|
interface InviteUserDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
@ -28,52 +33,18 @@ export function InviteUserDialog({
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onUserInvited,
|
onUserInvited,
|
||||||
}: InviteUserDialogProps) {
|
}: InviteUserDialogProps) {
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
email: "",
|
|
||||||
metadata: "{}",
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleInputChange = (
|
const {
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
register,
|
||||||
) => {
|
handleSubmit,
|
||||||
const { name, value } = e.target;
|
reset,
|
||||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
errors,
|
||||||
};
|
isPending,
|
||||||
|
handleOpenChange
|
||||||
const { mutate: inviteUserMutation, isPending } = useMutation({
|
} = useInviteUserHandler({ onUserInvited, onOpenChange });
|
||||||
mutationKey: ["inviteUser"],
|
|
||||||
mutationFn: inviteUser,
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("Invitation sent");
|
|
||||||
onUserInvited();
|
|
||||||
onOpenChange(false);
|
|
||||||
setFormData({
|
|
||||||
email: "",
|
|
||||||
metadata: "{}",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
toast.error("Failed to send invitation");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
let metadata = {};
|
|
||||||
try {
|
|
||||||
metadata = JSON.parse(formData.metadata);
|
|
||||||
inviteUserMutation({
|
|
||||||
email: formData.email,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
toast.error("Invalid JSON. Please check your metadata format.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Invite User</DialogTitle>
|
<DialogTitle>Invite User</DialogTitle>
|
||||||
|
@ -81,30 +52,26 @@ export function InviteUserDialog({
|
||||||
Send an invitation email to a new user.
|
Send an invitation email to a new user.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<Separator className="" />
|
||||||
<div className="space-y-2">
|
<form onSubmit={handleSubmit} className="space-y-8">
|
||||||
<Label htmlFor="invite-email">Email *</Label>
|
<ReactHookFormField
|
||||||
<Input
|
label="Email"
|
||||||
id="invite-email"
|
icon={MailIcon}
|
||||||
name="email"
|
placeholder="example@gmail.com"
|
||||||
type="email"
|
error={errors.email}
|
||||||
required
|
registration={register("email")}
|
||||||
value={formData.email}
|
/>
|
||||||
onChange={handleInputChange}
|
|
||||||
placeholder="example@gmail.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button className="flex w-full" type="submit" disabled={isPending}>
|
||||||
type="button"
|
{
|
||||||
variant="outline"
|
isPending ? (
|
||||||
onClick={() => onOpenChange(false)}
|
<>
|
||||||
>
|
<span className="mr-2">Invite user...</span>
|
||||||
Cancel
|
<Loader2 className="animate-spin" />
|
||||||
</Button>
|
</>
|
||||||
<Button type="submit" disabled={isPending}>
|
) : "Invite User"
|
||||||
{isPending ? "Sending..." : "Send Invitation"}
|
}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { useState, useRef } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import type { User } from "@/src/entities/models/users/users.model";
|
import type { IUserSchema } from "@/src/entities/models/users/users.model";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
|
@ -40,7 +40,7 @@ const profileFormSchema = z.object({
|
||||||
type ProfileFormValues = z.infer<typeof profileFormSchema>;
|
type ProfileFormValues = z.infer<typeof profileFormSchema>;
|
||||||
|
|
||||||
interface ProfileFormProps {
|
interface ProfileFormProps {
|
||||||
user: User | null;
|
user: IUserSchema | null;
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,11 +35,10 @@ import {
|
||||||
import {
|
import {
|
||||||
banUser,
|
banUser,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
sendMagicLink,
|
|
||||||
sendPasswordRecovery,
|
|
||||||
unbanUser,
|
unbanUser,
|
||||||
} from "@/app/(pages)/(admin)/dashboard/user-management/action";
|
} from "@/app/(pages)/(admin)/dashboard/user-management/action";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
import { sendMagicLink, sendPasswordRecovery } from "@/app/(pages)/(auth)/action";
|
||||||
|
|
||||||
interface UserDetailSheetProps {
|
interface UserDetailSheetProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
@ -129,7 +128,8 @@ export function UserDetailSheet({
|
||||||
if (user.banned_until) {
|
if (user.banned_until) {
|
||||||
return unbanUser(user.id);
|
return unbanUser(user.id);
|
||||||
} else {
|
} else {
|
||||||
return banUser(user.id);
|
const ban_duration = "7h"; // Example: Ban duration set to 7 days
|
||||||
|
return banUser(user.id, ban_duration);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onMutate: () => {
|
onMutate: () => {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import type * as z from "zod"
|
||||||
|
|
||||||
import { Loader2 } from "lucide-react"
|
import { Loader2 } from "lucide-react"
|
||||||
|
|
||||||
import { UpdateUserParamsSchema, type User } from "@/src/entities/models/users/users.model"
|
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||||
|
|
||||||
// UI Components
|
// UI Components
|
||||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/app/_components/ui/sheet"
|
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/app/_components/ui/sheet"
|
||||||
|
@ -17,20 +17,21 @@ import { FormFieldWrapper } from "@/app/_components/form-wrapper"
|
||||||
import { useMutation } from "@tanstack/react-query"
|
import { useMutation } from "@tanstack/react-query"
|
||||||
import { updateUser } from "../action"
|
import { updateUser } from "../action"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
import { UpdateUserSchema } from "@/src/entities/models/users/update-user.model"
|
||||||
|
|
||||||
type UserProfileFormValues = z.infer<typeof UpdateUserParamsSchema>
|
type UserProfileFormValues = z.infer<typeof UpdateUserSchema>
|
||||||
|
|
||||||
interface UserProfileSheetProps {
|
interface UserProfileSheetProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
userData?: User
|
userData?: IUserSchema
|
||||||
onUserUpdated: () => void
|
onUserUpdated: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserProfileSheet({ open, onOpenChange, userData, onUserUpdated }: UserProfileSheetProps) {
|
export function UserProfileSheet({ open, onOpenChange, userData, onUserUpdated }: UserProfileSheetProps) {
|
||||||
// Initialize form with user data
|
// Initialize form with user data
|
||||||
const form = useForm<UserProfileFormValues>({
|
const form = useForm<UserProfileFormValues>({
|
||||||
resolver: zodResolver(UpdateUserParamsSchema),
|
resolver: zodResolver(UpdateUserSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: userData?.email || undefined,
|
email: userData?.email || undefined,
|
||||||
encrypted_password: userData?.encrypted_password || undefined,
|
encrypted_password: userData?.encrypted_password || undefined,
|
||||||
|
|
|
@ -1,147 +0,0 @@
|
||||||
// "use client"
|
|
||||||
|
|
||||||
// import { zodResolver } from "@hookform/resolvers/zod"
|
|
||||||
// import { useForm } from "react-hook-form"
|
|
||||||
// import { z } from "zod"
|
|
||||||
|
|
||||||
// import { Button } from "@/app/_components/ui/button"
|
|
||||||
// import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/app/_components/ui/form"
|
|
||||||
// import { Input } from "@/app/_components/ui/input"
|
|
||||||
// import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select"
|
|
||||||
// import { useState } from "react"
|
|
||||||
// import { User } from "./column"
|
|
||||||
// import { updateUser } from "../../user-management/action"
|
|
||||||
// import { toast } from "@/app/_hooks/use-toast"
|
|
||||||
|
|
||||||
// const userFormSchema = z.object({
|
|
||||||
// email: z.string().email({ message: "Please enter a valid email address" }),
|
|
||||||
// first_name: z.string().nullable(),
|
|
||||||
// last_name: z.string().nullable(),
|
|
||||||
// role: z.enum(["user", "admin", "moderator"]),
|
|
||||||
// })
|
|
||||||
|
|
||||||
// type UserFormValues = z.infer<typeof userFormSchema>
|
|
||||||
|
|
||||||
// interface UserFormProps {
|
|
||||||
// user: User
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export function UserForm({ user }: UserFormProps) {
|
|
||||||
// const [isSubmitting, setIsSubmitting] = useState(false)
|
|
||||||
|
|
||||||
// const form = useForm<UserFormValues>({
|
|
||||||
// resolver: zodResolver(userFormSchema),
|
|
||||||
// defaultValues: {
|
|
||||||
// email: user.email,
|
|
||||||
// first_name: user.first_name,
|
|
||||||
// last_name: user.last_name,
|
|
||||||
// role: user.role as "user" | "admin" | "moderator",
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
|
|
||||||
// async function onSubmit(data: UserFormValues) {
|
|
||||||
// try {
|
|
||||||
// setIsSubmitting(true)
|
|
||||||
// await updateUser(user.id, data)
|
|
||||||
// toast({
|
|
||||||
// title: "User updated",
|
|
||||||
// description: "The user" + user.email + " has been updated.",
|
|
||||||
// })
|
|
||||||
// } catch (error) {
|
|
||||||
// toast({
|
|
||||||
// title: "Failed to update user",
|
|
||||||
// description: "An error occurred while updating the user.",
|
|
||||||
// variant: "destructive",
|
|
||||||
// })
|
|
||||||
// console.error(error)
|
|
||||||
// } finally {
|
|
||||||
// setIsSubmitting(false)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <Form {...form}>
|
|
||||||
// <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
||||||
// <FormField
|
|
||||||
// control={form.control}
|
|
||||||
// name="email"
|
|
||||||
// render={({ field }) => (
|
|
||||||
// <FormItem>
|
|
||||||
// <FormLabel className="text-white">Email</FormLabel>
|
|
||||||
// <FormControl>
|
|
||||||
// <Input
|
|
||||||
// {...field}
|
|
||||||
// className="bg-[#1c1c1c] border-[#2a2a2a] text-white focus-visible:ring-[#2a2a2a] focus-visible:ring-offset-0"
|
|
||||||
// />
|
|
||||||
// </FormControl>
|
|
||||||
// <FormDescription className="text-gray-400">This is the user's email address.</FormDescription>
|
|
||||||
// <FormMessage />
|
|
||||||
// </FormItem>
|
|
||||||
// )}
|
|
||||||
// />
|
|
||||||
// <div className="grid grid-cols-2 gap-4">
|
|
||||||
// <FormField
|
|
||||||
// control={form.control}
|
|
||||||
// name="first_name"
|
|
||||||
// render={({ field }) => (
|
|
||||||
// <FormItem>
|
|
||||||
// <FormLabel className="text-white">First Name</FormLabel>
|
|
||||||
// <FormControl>
|
|
||||||
// <Input
|
|
||||||
// {...field}
|
|
||||||
// value={field.value || ""}
|
|
||||||
// className="bg-[#1c1c1c] border-[#2a2a2a] text-white focus-visible:ring-[#2a2a2a] focus-visible:ring-offset-0"
|
|
||||||
// />
|
|
||||||
// </FormControl>
|
|
||||||
// <FormMessage />
|
|
||||||
// </FormItem>
|
|
||||||
// )}
|
|
||||||
// />
|
|
||||||
// <FormField
|
|
||||||
// control={form.control}
|
|
||||||
// name="last_name"
|
|
||||||
// render={({ field }) => (
|
|
||||||
// <FormItem>
|
|
||||||
// <FormLabel className="text-white">Last Name</FormLabel>
|
|
||||||
// <FormControl>
|
|
||||||
// <Input
|
|
||||||
// {...field}
|
|
||||||
// value={field.value || ""}
|
|
||||||
// className="bg-[#1c1c1c] border-[#2a2a2a] text-white focus-visible:ring-[#2a2a2a] focus-visible:ring-offset-0"
|
|
||||||
// />
|
|
||||||
// </FormControl>
|
|
||||||
// <FormMessage />
|
|
||||||
// </FormItem>
|
|
||||||
// )}
|
|
||||||
// />
|
|
||||||
// </div>
|
|
||||||
// <FormField
|
|
||||||
// control={form.control}
|
|
||||||
// name="role"
|
|
||||||
// render={({ field }) => (
|
|
||||||
// <FormItem>
|
|
||||||
// <FormLabel className="text-white">Role</FormLabel>
|
|
||||||
// <Select onValueChange={field.onChange} defaultValue={field.value}>
|
|
||||||
// <FormControl>
|
|
||||||
// <SelectTrigger className="bg-[#1c1c1c] border-[#2a2a2a] text-white focus:ring-[#2a2a2a] focus:ring-offset-0">
|
|
||||||
// <SelectValue placeholder="Select a role" />
|
|
||||||
// </SelectTrigger>
|
|
||||||
// </FormControl>
|
|
||||||
// <SelectContent className="bg-[#1c1c1c] border-[#2a2a2a] text-white">
|
|
||||||
// <SelectItem value="user">User</SelectItem>
|
|
||||||
// <SelectItem value="admin">Admin</SelectItem>
|
|
||||||
// <SelectItem value="moderator">Moderator</SelectItem>
|
|
||||||
// </SelectContent>
|
|
||||||
// </Select>
|
|
||||||
// <FormDescription className="text-gray-400">The user's role determines their permissions.</FormDescription>
|
|
||||||
// <FormMessage />
|
|
||||||
// </FormItem>
|
|
||||||
// )}
|
|
||||||
// />
|
|
||||||
// <Button type="submit" disabled={isSubmitting} className="bg-emerald-600 hover:bg-emerald-700 text-white">
|
|
||||||
// {isSubmitting ? "Saving..." : "Save changes"}
|
|
||||||
// </Button>
|
|
||||||
// </form>
|
|
||||||
// </Form>
|
|
||||||
// )
|
|
||||||
// }
|
|
|
@ -16,570 +16,63 @@ import {
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/app/_components/ui/button";
|
import { Button } from "@/app/_components/ui/button";
|
||||||
import { Input } from "@/app/_components/ui/input";
|
import { Input } from "@/app/_components/ui/input";
|
||||||
import { Badge } from "@/app/_components/ui/badge";
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
} from "@/app/_components/ui/dropdown-menu";
|
} from "@/app/_components/ui/dropdown-menu";
|
||||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
|
||||||
import { fetchUsers } from "@/app/(pages)/(admin)/dashboard/user-management/action";
|
|
||||||
import type { User } from "@/src/entities/models/users/users.model";
|
|
||||||
import { DataTable } from "./data-table";
|
import { DataTable } from "./data-table";
|
||||||
import { InviteUserDialog } from "./invite-user";
|
import { InviteUserDialog } from "./invite-user";
|
||||||
import { AddUserDialog } from "./add-user-dialog";
|
import { AddUserDialog } from "./add-user-dialog";
|
||||||
import { UserDetailSheet } from "./sheet";
|
import { UserDetailSheet } from "./sheet";
|
||||||
import { Avatar } from "@radix-ui/react-avatar";
|
|
||||||
import Image from "next/image";
|
|
||||||
import type { ColumnDef, HeaderContext } from "@tanstack/react-table";
|
|
||||||
import { UserProfileSheet } from "./update-user";
|
import { UserProfileSheet } from "./update-user";
|
||||||
|
import { filterUsers, useUserManagementHandlers } from "../handler";
|
||||||
type UserFilterOptions = {
|
import { createUserColumns } from "./users-table";
|
||||||
email: string;
|
import { useGetUsersQuery } from "../queries";
|
||||||
phone: string;
|
|
||||||
lastSignIn: string;
|
|
||||||
createdAt: string;
|
|
||||||
status: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type UserTableColumn = ColumnDef<User, User>;
|
|
||||||
|
|
||||||
export default function UserManagement() {
|
export default function UserManagement() {
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
|
||||||
const [detailUser, setDetailUser] = useState<User | null>(null);
|
|
||||||
const [updateUser, setUpdateUser] = useState<User | null>(null);
|
|
||||||
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
|
||||||
const [isUpdateOpen, setIsUpdateOpen] = useState(false);
|
|
||||||
const [isAddUserOpen, setIsAddUserOpen] = useState(false);
|
|
||||||
const [isInviteUserOpen, setIsInviteUserOpen] = useState(false);
|
|
||||||
|
|
||||||
// Filter states
|
|
||||||
const [filters, setFilters] = useState<UserFilterOptions>({
|
|
||||||
email: "",
|
|
||||||
phone: "",
|
|
||||||
lastSignIn: "",
|
|
||||||
createdAt: "",
|
|
||||||
status: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use React Query to fetch users
|
// Use React Query to fetch users
|
||||||
const {
|
const {
|
||||||
data: users = [],
|
data: users = [],
|
||||||
isLoading,
|
isPending,
|
||||||
refetch,
|
refetch,
|
||||||
isPlaceholderData,
|
} = useGetUsersQuery();
|
||||||
} = useQuery<User[]>({
|
|
||||||
queryKey: ["users"],
|
|
||||||
queryFn: fetchUsers,
|
|
||||||
placeholderData: keepPreviousData,
|
|
||||||
throwOnError: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle opening the detail sheet
|
// User management handler
|
||||||
const handleUserClick = (user: User) => {
|
const {
|
||||||
setDetailUser(user);
|
searchQuery,
|
||||||
setIsSheetOpen(true);
|
setSearchQuery,
|
||||||
};
|
detailUser,
|
||||||
|
updateUser,
|
||||||
// Handle opening the update sheet
|
isSheetOpen,
|
||||||
const handleUserUpdate = (user: User) => {
|
setIsSheetOpen,
|
||||||
setUpdateUser(user);
|
isUpdateOpen,
|
||||||
setIsUpdateOpen(true);
|
setIsUpdateOpen,
|
||||||
};
|
isAddUserOpen,
|
||||||
|
setIsAddUserOpen,
|
||||||
// Close detail sheet when update sheet opens
|
isInviteUserOpen,
|
||||||
useEffect(() => {
|
setIsInviteUserOpen,
|
||||||
if (isUpdateOpen) {
|
filters,
|
||||||
setIsSheetOpen(false);
|
setFilters,
|
||||||
}
|
handleUserClick,
|
||||||
}, [isUpdateOpen]);
|
handleUserUpdate,
|
||||||
|
clearFilters,
|
||||||
// Reset detail user when sheet closes
|
getActiveFilterCount,
|
||||||
useEffect(() => {
|
} = useUserManagementHandlers(refetch)
|
||||||
if (!isSheetOpen) {
|
|
||||||
// Use a small delay to prevent flickering if another sheet is opening
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
if (!isSheetOpen && !isUpdateOpen) {
|
|
||||||
setDetailUser(null);
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}, [isSheetOpen, isUpdateOpen]);
|
|
||||||
|
|
||||||
// Reset update user when update sheet closes
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isUpdateOpen) {
|
|
||||||
// Use a small delay to prevent flickering if another sheet is opening
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
if (!isUpdateOpen) {
|
|
||||||
setUpdateUser(null);
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}, [isUpdateOpen]);
|
|
||||||
|
|
||||||
|
// Apply filters to users
|
||||||
const filteredUsers = useMemo(() => {
|
const filteredUsers = useMemo(() => {
|
||||||
return users.filter((user) => {
|
return filterUsers(users, searchQuery, filters)
|
||||||
// Global search
|
}, [users, searchQuery, filters])
|
||||||
if (searchQuery) {
|
|
||||||
const query = searchQuery.toLowerCase();
|
|
||||||
const matchesSearch =
|
|
||||||
user.email?.toLowerCase().includes(query) ||
|
|
||||||
user.phone?.toLowerCase().includes(query) ||
|
|
||||||
user.id.toLowerCase().includes(query);
|
|
||||||
|
|
||||||
if (!matchesSearch) return false;
|
// Get active filter count
|
||||||
}
|
const activeFilterCount = getActiveFilterCount()
|
||||||
|
|
||||||
// Email filter
|
// Create table columns
|
||||||
if (
|
const columns = createUserColumns(filters, setFilters, handleUserUpdate)
|
||||||
filters.email &&
|
|
||||||
!user.email?.toLowerCase().includes(filters.email.toLowerCase())
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phone filter
|
|
||||||
if (
|
|
||||||
filters.phone &&
|
|
||||||
!user.phone?.toLowerCase().includes(filters.phone.toLowerCase())
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last sign in filter
|
|
||||||
if (filters.lastSignIn) {
|
|
||||||
if (filters.lastSignIn === "never" && user.last_sign_in_at) {
|
|
||||||
return false;
|
|
||||||
} else if (filters.lastSignIn === "today") {
|
|
||||||
const today = new Date();
|
|
||||||
today.setHours(0, 0, 0, 0);
|
|
||||||
const signInDate = user.last_sign_in_at
|
|
||||||
? new Date(user.last_sign_in_at)
|
|
||||||
: null;
|
|
||||||
if (!signInDate || signInDate < today) return false;
|
|
||||||
} else if (filters.lastSignIn === "week") {
|
|
||||||
const weekAgo = new Date();
|
|
||||||
weekAgo.setDate(weekAgo.getDate() - 7);
|
|
||||||
const signInDate = user.last_sign_in_at
|
|
||||||
? new Date(user.last_sign_in_at)
|
|
||||||
: null;
|
|
||||||
if (!signInDate || signInDate < weekAgo) return false;
|
|
||||||
} else if (filters.lastSignIn === "month") {
|
|
||||||
const monthAgo = new Date();
|
|
||||||
monthAgo.setMonth(monthAgo.getMonth() - 1);
|
|
||||||
const signInDate = user.last_sign_in_at
|
|
||||||
? new Date(user.last_sign_in_at)
|
|
||||||
: null;
|
|
||||||
if (!signInDate || signInDate < monthAgo) return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Created at filter
|
|
||||||
if (filters.createdAt) {
|
|
||||||
if (filters.createdAt === "today") {
|
|
||||||
const today = new Date();
|
|
||||||
today.setHours(0, 0, 0, 0);
|
|
||||||
const createdAt = user.created_at
|
|
||||||
? user.created_at
|
|
||||||
? new Date(user.created_at)
|
|
||||||
: new Date()
|
|
||||||
: new Date();
|
|
||||||
if (createdAt < today) return false;
|
|
||||||
} else if (filters.createdAt === "week") {
|
|
||||||
const weekAgo = new Date();
|
|
||||||
weekAgo.setDate(weekAgo.getDate() - 7);
|
|
||||||
const createdAt = user.created_at
|
|
||||||
? new Date(user.created_at)
|
|
||||||
: new Date();
|
|
||||||
if (createdAt < weekAgo) return false;
|
|
||||||
} else if (filters.createdAt === "month") {
|
|
||||||
const monthAgo = new Date();
|
|
||||||
monthAgo.setMonth(monthAgo.getMonth() - 1);
|
|
||||||
const createdAt = user.created_at
|
|
||||||
? new Date(user.created_at)
|
|
||||||
: new Date();
|
|
||||||
if (createdAt < monthAgo) return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status filter
|
|
||||||
if (filters.status.length > 0) {
|
|
||||||
const userStatus = user.banned_until
|
|
||||||
? "banned"
|
|
||||||
: !user.email_confirmed_at
|
|
||||||
? "unconfirmed"
|
|
||||||
: "active";
|
|
||||||
|
|
||||||
if (!filters.status.includes(userStatus)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}, [users, searchQuery, filters]);
|
|
||||||
|
|
||||||
const clearFilters = () => {
|
|
||||||
setFilters({
|
|
||||||
email: "",
|
|
||||||
phone: "",
|
|
||||||
lastSignIn: "",
|
|
||||||
createdAt: "",
|
|
||||||
status: [],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const activeFilterCount = Object.values(filters).filter(
|
|
||||||
(value) =>
|
|
||||||
(typeof value === "string" && value !== "") ||
|
|
||||||
(Array.isArray(value) && value.length > 0)
|
|
||||||
).length;
|
|
||||||
|
|
||||||
const columns: UserTableColumn[] = [
|
|
||||||
{
|
|
||||||
id: "email",
|
|
||||||
header: ({ column }: HeaderContext<User, User>) => (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<span>Email</span>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
|
|
||||||
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="start">
|
|
||||||
<div className="p-2">
|
|
||||||
<Input
|
|
||||||
placeholder="Filter by email..."
|
|
||||||
value={filters.email}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFilters({ ...filters, email: e.target.value })
|
|
||||||
}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => setFilters({ ...filters, email: "" })}
|
|
||||||
>
|
|
||||||
Clear filter
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Avatar className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-medium">
|
|
||||||
{row.original.profile?.avatar ? (
|
|
||||||
<Image
|
|
||||||
src={row.original.profile.avatar || "/placeholder.svg"}
|
|
||||||
alt="Avatar"
|
|
||||||
className="w-full h-full rounded-full"
|
|
||||||
width={32}
|
|
||||||
height={32}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
row.original.email?.[0]?.toUpperCase() || "?"
|
|
||||||
)}
|
|
||||||
</Avatar>
|
|
||||||
<div>
|
|
||||||
<div className="font-medium">
|
|
||||||
{row.original.email || "No email"}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-muted-foreground">
|
|
||||||
{row.original.id}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "phone",
|
|
||||||
header: ({ column }: HeaderContext<User, User>) => (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<span>Phone</span>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
|
|
||||||
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="start">
|
|
||||||
<div className="p-2">
|
|
||||||
<Input
|
|
||||||
placeholder="Filter by phone..."
|
|
||||||
value={filters.phone}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFilters({ ...filters, phone: e.target.value })
|
|
||||||
}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => setFilters({ ...filters, phone: "" })}
|
|
||||||
>
|
|
||||||
Clear filter
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => row.original.phone || "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "lastSignIn",
|
|
||||||
header: ({ column }: HeaderContext<User, User>) => (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<span>Last Sign In</span>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
|
|
||||||
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="start">
|
|
||||||
<DropdownMenuCheckboxItem
|
|
||||||
checked={filters.lastSignIn === "today"}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
setFilters({
|
|
||||||
...filters,
|
|
||||||
lastSignIn: filters.lastSignIn === "today" ? "" : "today",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Today
|
|
||||||
</DropdownMenuCheckboxItem>
|
|
||||||
<DropdownMenuCheckboxItem
|
|
||||||
checked={filters.lastSignIn === "week"}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
setFilters({
|
|
||||||
...filters,
|
|
||||||
lastSignIn: filters.lastSignIn === "week" ? "" : "week",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Last 7 days
|
|
||||||
</DropdownMenuCheckboxItem>
|
|
||||||
<DropdownMenuCheckboxItem
|
|
||||||
checked={filters.lastSignIn === "month"}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
setFilters({
|
|
||||||
...filters,
|
|
||||||
lastSignIn: filters.lastSignIn === "month" ? "" : "month",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Last 30 days
|
|
||||||
</DropdownMenuCheckboxItem>
|
|
||||||
<DropdownMenuCheckboxItem
|
|
||||||
checked={filters.lastSignIn === "never"}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
setFilters({
|
|
||||||
...filters,
|
|
||||||
lastSignIn: filters.lastSignIn === "never" ? "" : "never",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Never
|
|
||||||
</DropdownMenuCheckboxItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => setFilters({ ...filters, lastSignIn: "" })}
|
|
||||||
>
|
|
||||||
Clear filter
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => {
|
|
||||||
return row.original.last_sign_in_at
|
|
||||||
? new Date(row.original.last_sign_in_at).toLocaleString()
|
|
||||||
: "Never";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "createdAt",
|
|
||||||
header: ({ column }: HeaderContext<User, User>) => (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<span>Created At</span>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
|
|
||||||
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="start">
|
|
||||||
<DropdownMenuCheckboxItem
|
|
||||||
checked={filters.createdAt === "today"}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
setFilters({
|
|
||||||
...filters,
|
|
||||||
createdAt: filters.createdAt === "today" ? "" : "today",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Today
|
|
||||||
</DropdownMenuCheckboxItem>
|
|
||||||
<DropdownMenuCheckboxItem
|
|
||||||
checked={filters.createdAt === "week"}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
setFilters({
|
|
||||||
...filters,
|
|
||||||
createdAt: filters.createdAt === "week" ? "" : "week",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Last 7 days
|
|
||||||
</DropdownMenuCheckboxItem>
|
|
||||||
<DropdownMenuCheckboxItem
|
|
||||||
checked={filters.createdAt === "month"}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
setFilters({
|
|
||||||
...filters,
|
|
||||||
createdAt: filters.createdAt === "month" ? "" : "month",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Last 30 days
|
|
||||||
</DropdownMenuCheckboxItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => setFilters({ ...filters, createdAt: "" })}
|
|
||||||
>
|
|
||||||
Clear filter
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => {
|
|
||||||
return row.original.created_at
|
|
||||||
? new Date(row.original.created_at).toLocaleString()
|
|
||||||
: "N/A";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "status",
|
|
||||||
header: ({ column }: HeaderContext<User, User>) => (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<span>Status</span>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
|
|
||||||
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="start">
|
|
||||||
<DropdownMenuCheckboxItem
|
|
||||||
checked={filters.status.includes("active")}
|
|
||||||
onCheckedChange={() => {
|
|
||||||
const newStatus = [...filters.status];
|
|
||||||
if (newStatus.includes("active")) {
|
|
||||||
newStatus.splice(newStatus.indexOf("active"), 1);
|
|
||||||
} else {
|
|
||||||
newStatus.push("active");
|
|
||||||
}
|
|
||||||
setFilters({ ...filters, status: newStatus });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Active
|
|
||||||
</DropdownMenuCheckboxItem>
|
|
||||||
<DropdownMenuCheckboxItem
|
|
||||||
checked={filters.status.includes("unconfirmed")}
|
|
||||||
onCheckedChange={() => {
|
|
||||||
const newStatus = [...filters.status];
|
|
||||||
if (newStatus.includes("unconfirmed")) {
|
|
||||||
newStatus.splice(newStatus.indexOf("unconfirmed"), 1);
|
|
||||||
} else {
|
|
||||||
newStatus.push("unconfirmed");
|
|
||||||
}
|
|
||||||
setFilters({ ...filters, status: newStatus });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Unconfirmed
|
|
||||||
</DropdownMenuCheckboxItem>
|
|
||||||
<DropdownMenuCheckboxItem
|
|
||||||
checked={filters.status.includes("banned")}
|
|
||||||
onCheckedChange={() => {
|
|
||||||
const newStatus = [...filters.status];
|
|
||||||
if (newStatus.includes("banned")) {
|
|
||||||
newStatus.splice(newStatus.indexOf("banned"), 1);
|
|
||||||
} else {
|
|
||||||
newStatus.push("banned");
|
|
||||||
}
|
|
||||||
setFilters({ ...filters, status: newStatus });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Banned
|
|
||||||
</DropdownMenuCheckboxItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => setFilters({ ...filters, status: [] })}
|
|
||||||
>
|
|
||||||
Clear filter
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => {
|
|
||||||
if (row.original.banned_until) {
|
|
||||||
return <Badge variant="destructive">Banned</Badge>;
|
|
||||||
}
|
|
||||||
if (!row.original.email_confirmed_at) {
|
|
||||||
return <Badge variant="outline">Unconfirmed</Badge>;
|
|
||||||
}
|
|
||||||
return <Badge variant="default">Active</Badge>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "actions",
|
|
||||||
header: "",
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div onClick={(e) => e.stopPropagation()}>
|
|
||||||
{/* Add this wrapper */}
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon">
|
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem onClick={() => handleUserUpdate(row.original)}>
|
|
||||||
<UserPen className="h-4 w-4 mr-2 text-blue-500" />
|
|
||||||
Update
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
/* handle delete */
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4 mr-2 text-red-500" />
|
|
||||||
Delete
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
/* handle ban */
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ShieldAlert className="h-4 w-4 mr-2 text-yellow-500" />
|
|
||||||
{row.original.banned_until != null ? "Unban" : "Ban"}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
@ -642,7 +135,7 @@ export default function UserManagement() {
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={filteredUsers}
|
data={filteredUsers}
|
||||||
loading={isLoading}
|
loading={isPending}
|
||||||
onRowClick={(user) => handleUserClick(user)}
|
onRowClick={(user) => handleUserClick(user)}
|
||||||
/>
|
/>
|
||||||
{detailUser && (
|
{detailUser && (
|
||||||
|
@ -653,16 +146,8 @@ export default function UserManagement() {
|
||||||
onUserUpdate={() => refetch()}
|
onUserUpdate={() => refetch()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<AddUserDialog
|
<AddUserDialog open={isAddUserOpen} onOpenChange={setIsAddUserOpen} onUserAdded={() => refetch()} />
|
||||||
open={isAddUserOpen}
|
<InviteUserDialog open={isInviteUserOpen} onOpenChange={setIsInviteUserOpen} onUserInvited={() => refetch()} />
|
||||||
onOpenChange={setIsAddUserOpen}
|
|
||||||
onUserAdded={() => refetch()}
|
|
||||||
/>
|
|
||||||
<InviteUserDialog
|
|
||||||
open={isInviteUserOpen}
|
|
||||||
onOpenChange={setIsInviteUserOpen}
|
|
||||||
onUserInvited={() => refetch()}
|
|
||||||
/>
|
|
||||||
{updateUser && (
|
{updateUser && (
|
||||||
<UserProfileSheet
|
<UserProfileSheet
|
||||||
open={isUpdateOpen}
|
open={isUpdateOpen}
|
||||||
|
@ -672,5 +157,5 @@ export default function UserManagement() {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,22 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { Card, CardContent } from "@/app/_components/ui/card";
|
import { Card, CardContent } from "@/app/_components/ui/card";
|
||||||
import { Users, UserCheck, UserX } from "lucide-react";
|
import { Users, UserCheck, UserX } from "lucide-react";
|
||||||
import { fetchUsers } from "@/app/(pages)/(admin)/dashboard/user-management/action";
|
import { IUserSchema } from "@/src/entities/models/users/users.model";
|
||||||
import { User } from "@/src/entities/models/users/users.model";
|
import { useGetUsersQuery } from "../queries";
|
||||||
import { useNavigations } from "@/app/_hooks/use-navigations";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
function calculateUserStats(users: IUserSchema[] | undefined) {
|
||||||
|
if (!users || !Array.isArray(users)) {
|
||||||
|
return {
|
||||||
|
totalUsers: 0,
|
||||||
|
activeUsers: 0,
|
||||||
|
inactiveUsers: 0,
|
||||||
|
activePercentage: '0.0',
|
||||||
|
inactivePercentage: '0.0',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function calculateUserStats(users: User[]) {
|
|
||||||
const totalUsers = users.length;
|
const totalUsers = users.length;
|
||||||
const activeUsers = users.filter(
|
const activeUsers = users.filter(
|
||||||
(user) => !user.banned_until && user.email_confirmed_at
|
(user) => !user.banned_until && user.email_confirmed_at
|
||||||
|
@ -21,21 +28,18 @@ function calculateUserStats(users: User[]) {
|
||||||
activeUsers,
|
activeUsers,
|
||||||
inactiveUsers,
|
inactiveUsers,
|
||||||
activePercentage:
|
activePercentage:
|
||||||
totalUsers > 0 ? ((activeUsers / totalUsers) * 100).toFixed(1) : "0.0",
|
totalUsers > 0 ? ((activeUsers / totalUsers) * 100).toFixed(1) : '0.0',
|
||||||
inactivePercentage:
|
inactivePercentage:
|
||||||
totalUsers > 0 ? ((inactiveUsers / totalUsers) * 100).toFixed(1) : "0.0",
|
totalUsers > 0 ? ((inactiveUsers / totalUsers) * 100).toFixed(1) : '0.0',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserStats() {
|
export function UserStats() {
|
||||||
const { data: users = [], isLoading } = useQuery({
|
const { data: users, isPending, error } = useGetUsersQuery();
|
||||||
queryKey: ["users"],
|
|
||||||
queryFn: fetchUsers,
|
|
||||||
});
|
|
||||||
|
|
||||||
const stats = calculateUserStats(users);
|
const stats = calculateUserStats(users);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isPending) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{[...Array(3)].map((_, i) => (
|
{[...Array(3)].map((_, i) => (
|
||||||
|
@ -53,21 +57,32 @@ export function UserStats() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show error state if there's an error
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Card className="bg-background border-border">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="text-destructive text-center">Error fetching data</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const cards = [
|
const cards = [
|
||||||
{
|
{
|
||||||
title: "Total Users",
|
title: 'Total Users',
|
||||||
value: stats.totalUsers,
|
value: stats.totalUsers,
|
||||||
subtitle: "Updated just now",
|
subtitle: 'Updated just now',
|
||||||
icon: Users,
|
icon: Users,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Active Users",
|
title: 'Active Users',
|
||||||
value: stats.activeUsers,
|
value: stats.activeUsers,
|
||||||
subtitle: `${stats.activePercentage}% of total users`,
|
subtitle: `${stats.activePercentage}% of total users`,
|
||||||
icon: UserCheck,
|
icon: UserCheck,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Inactive Users",
|
title: 'Inactive Users',
|
||||||
value: stats.inactiveUsers,
|
value: stats.inactiveUsers,
|
||||||
subtitle: `${stats.inactivePercentage}% of total users`,
|
subtitle: `${stats.inactivePercentage}% of total users`,
|
||||||
icon: UserX,
|
icon: UserX,
|
||||||
|
@ -92,4 +107,4 @@ export function UserStats() {
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -0,0 +1,338 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import type { ColumnDef, HeaderContext } from "@tanstack/react-table"
|
||||||
|
import type { IUserSchema, IUserFilterOptionsSchema } from "@/src/entities/models/users/users.model"
|
||||||
|
import { ListFilter, MoreHorizontal, PenIcon as UserPen, Trash2, ShieldAlert } from "lucide-react"
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
} from "@/app/_components/ui/dropdown-menu"
|
||||||
|
import { Button } from "@/app/_components/ui/button"
|
||||||
|
import { Input } from "@/app/_components/ui/input"
|
||||||
|
import { Avatar } from "@/app/_components/ui/avatar"
|
||||||
|
import Image from "next/image"
|
||||||
|
import { Badge } from "@/app/_components/ui/badge"
|
||||||
|
|
||||||
|
export type UserTableColumn = ColumnDef<IUserSchema, IUserSchema>
|
||||||
|
|
||||||
|
export const createUserColumns = (
|
||||||
|
filters: IUserFilterOptionsSchema,
|
||||||
|
setFilters: (filters: IUserFilterOptionsSchema) => void,
|
||||||
|
handleUserUpdate: (user: IUserSchema) => void,
|
||||||
|
): UserTableColumn[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "email",
|
||||||
|
header: ({ column }: HeaderContext<IUserSchema, IUserSchema>) => (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span>Email</span>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
|
||||||
|
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start">
|
||||||
|
<div className="p-2">
|
||||||
|
<Input
|
||||||
|
placeholder="Filter by email..."
|
||||||
|
value={filters.email}
|
||||||
|
onChange={(e) => setFilters({ ...filters, email: e.target.value })}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onClick={() => setFilters({ ...filters, email: "" })}>Clear filter</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Avatar className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-medium">
|
||||||
|
{row.original.profile?.avatar ? (
|
||||||
|
<Image
|
||||||
|
src={row.original.profile.avatar || "/placeholder.svg"}
|
||||||
|
alt="Avatar"
|
||||||
|
className="w-full h-full rounded-full"
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
row.original.email?.[0]?.toUpperCase() || "?"
|
||||||
|
)}
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{row.original.email || "No email"}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">{row.original.profile?.username}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "phone",
|
||||||
|
header: ({ column }: HeaderContext<IUserSchema, IUserSchema>) => (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span>Phone</span>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
|
||||||
|
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start">
|
||||||
|
<div className="p-2">
|
||||||
|
<Input
|
||||||
|
placeholder="Filter by phone..."
|
||||||
|
value={filters.phone}
|
||||||
|
onChange={(e) => setFilters({ ...filters, phone: e.target.value })}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onClick={() => setFilters({ ...filters, phone: "" })}>Clear filter</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => row.original.phone || "-",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "lastSignIn",
|
||||||
|
header: ({ column }: HeaderContext<IUserSchema, IUserSchema>) => (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span>Last Sign In</span>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
|
||||||
|
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start">
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
checked={filters.lastSignIn === "today"}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
setFilters({
|
||||||
|
...filters,
|
||||||
|
lastSignIn: filters.lastSignIn === "today" ? "" : "today",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Today
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
checked={filters.lastSignIn === "week"}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
setFilters({
|
||||||
|
...filters,
|
||||||
|
lastSignIn: filters.lastSignIn === "week" ? "" : "week",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Last 7 days
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
checked={filters.lastSignIn === "month"}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
setFilters({
|
||||||
|
...filters,
|
||||||
|
lastSignIn: filters.lastSignIn === "month" ? "" : "month",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Last 30 days
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
checked={filters.lastSignIn === "never"}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
setFilters({
|
||||||
|
...filters,
|
||||||
|
lastSignIn: filters.lastSignIn === "never" ? "" : "never",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Never
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onClick={() => setFilters({ ...filters, lastSignIn: "" })}>
|
||||||
|
Clear filter
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return row.original.last_sign_in_at ? new Date(row.original.last_sign_in_at).toLocaleString() : "Never"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "createdAt",
|
||||||
|
header: ({ column }: HeaderContext<IUserSchema, IUserSchema>) => (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span>Created At</span>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
|
||||||
|
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start">
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
checked={filters.createdAt === "today"}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
setFilters({
|
||||||
|
...filters,
|
||||||
|
createdAt: filters.createdAt === "today" ? "" : "today",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Today
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
checked={filters.createdAt === "week"}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
setFilters({
|
||||||
|
...filters,
|
||||||
|
createdAt: filters.createdAt === "week" ? "" : "week",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Last 7 days
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
checked={filters.createdAt === "month"}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
setFilters({
|
||||||
|
...filters,
|
||||||
|
createdAt: filters.createdAt === "month" ? "" : "month",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Last 30 days
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onClick={() => setFilters({ ...filters, createdAt: "" })}>
|
||||||
|
Clear filter
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return row.original.created_at ? new Date(row.original.created_at).toLocaleString() : "N/A"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "status",
|
||||||
|
header: ({ column }: HeaderContext<IUserSchema, IUserSchema>) => (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span>Status</span>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" className="h-6 w-6 p-0 ml-1">
|
||||||
|
<ListFilter className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start">
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
checked={filters.status.includes("active")}
|
||||||
|
onCheckedChange={() => {
|
||||||
|
const newStatus = [...filters.status]
|
||||||
|
if (newStatus.includes("active")) {
|
||||||
|
newStatus.splice(newStatus.indexOf("active"), 1)
|
||||||
|
} else {
|
||||||
|
newStatus.push("active")
|
||||||
|
}
|
||||||
|
setFilters({ ...filters, status: newStatus })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Active
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
checked={filters.status.includes("unconfirmed")}
|
||||||
|
onCheckedChange={() => {
|
||||||
|
const newStatus = [...filters.status]
|
||||||
|
if (newStatus.includes("unconfirmed")) {
|
||||||
|
newStatus.splice(newStatus.indexOf("unconfirmed"), 1)
|
||||||
|
} else {
|
||||||
|
newStatus.push("unconfirmed")
|
||||||
|
}
|
||||||
|
setFilters({ ...filters, status: newStatus })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Unconfirmed
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
checked={filters.status.includes("banned")}
|
||||||
|
onCheckedChange={() => {
|
||||||
|
const newStatus = [...filters.status]
|
||||||
|
if (newStatus.includes("banned")) {
|
||||||
|
newStatus.splice(newStatus.indexOf("banned"), 1)
|
||||||
|
} else {
|
||||||
|
newStatus.push("banned")
|
||||||
|
}
|
||||||
|
setFilters({ ...filters, status: newStatus })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Banned
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onClick={() => setFilters({ ...filters, status: [] })}>Clear filter</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
if (row.original.banned_until) {
|
||||||
|
return <Badge variant="destructive">Banned</Badge>
|
||||||
|
}
|
||||||
|
if (!row.original.email_confirmed_at) {
|
||||||
|
return <Badge variant="outline">Unconfirmed</Badge>
|
||||||
|
}
|
||||||
|
return <Badge variant="default">Active</Badge>
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
header: "",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={() => handleUserUpdate(row.original)}>
|
||||||
|
<UserPen className="h-4 w-4 mr-2 text-blue-500" />
|
||||||
|
Update
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
/* handle delete */
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2 text-red-500" />
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
/* handle ban */
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ShieldAlert className="h-4 w-4 mr-2 text-yellow-500" />
|
||||||
|
{row.original.banned_until != null ? "Unban" : "Ban"}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
|
@ -1,361 +1,498 @@
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import db from "@/prisma/db";
|
import db from '@/prisma/db';
|
||||||
|
import { createClient } from '@/app/_utils/supabase/server';
|
||||||
|
import { createAdminClient } from '@/app/_utils/supabase/admin';
|
||||||
|
import { getInjection } from '@/di/container';
|
||||||
|
import { InputParseError, NotFoundError } from '@/src/entities/errors/common';
|
||||||
import {
|
import {
|
||||||
CreateUserParams,
|
AuthenticationError,
|
||||||
InviteUserParams,
|
UnauthenticatedError,
|
||||||
UpdateUserParams,
|
} from '@/src/entities/errors/auth';
|
||||||
User,
|
import { redirect } from 'next/navigation';
|
||||||
UserResponse,
|
import { IBanDuration, IBanUserSchema, ICredentialsBanUserSchema } from '@/src/entities/models/users/ban-user.model';
|
||||||
} from "@/src/entities/models/users/users.model";
|
import { ICreateUserSchema } from '@/src/entities/models/users/create-user.model';
|
||||||
import { createClient } from "@/app/_utils/supabase/server";
|
import { IUpdateUserSchema } from '@/src/entities/models/users/update-user.model';
|
||||||
import { createAdminClient } from "@/app/_utils/supabase/admin";
|
import { ICredentialsInviteUserSchema } from '@/src/entities/models/users/invite-user.model';
|
||||||
|
|
||||||
// Initialize Supabase client with admin key
|
export async function banUser(id: string, ban_duration: IBanDuration) {
|
||||||
|
const instrumentationService = getInjection('IInstrumentationService');
|
||||||
|
return await instrumentationService.instrumentServerAction(
|
||||||
|
'banUser',
|
||||||
|
{ recordResponse: true },
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const banUserController = getInjection('IBanUserController');
|
||||||
|
await banUserController({ id }, { ban_duration });
|
||||||
|
|
||||||
// Fetch all users
|
return { success: true };
|
||||||
export async function fetchUsers(): Promise<User[]> {
|
} catch (err) {
|
||||||
// const { data, error } = await supabase.auth.admin.getUsers();
|
if (err instanceof InputParseError) {
|
||||||
|
// return {
|
||||||
|
// error: err.message,
|
||||||
|
// };
|
||||||
|
|
||||||
// if (error) {
|
throw new InputParseError(err.message);
|
||||||
// console.error("Error fetching users:", error);
|
}
|
||||||
// throw new Error(error.message);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return data.users.map((user) => ({
|
if (err instanceof UnauthenticatedError) {
|
||||||
// ...user,
|
// return {
|
||||||
// })) as User[];
|
// error: 'Must be logged in to create a user.',
|
||||||
|
// };
|
||||||
|
throw new UnauthenticatedError('Must be logged in to ban a user.');
|
||||||
|
}
|
||||||
|
|
||||||
const users = await db.users.findMany({
|
if (err instanceof AuthenticationError) {
|
||||||
include: {
|
// return {
|
||||||
profile: true,
|
// error: 'User not found.',
|
||||||
},
|
// };
|
||||||
});
|
|
||||||
|
|
||||||
if (!users) {
|
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
|
||||||
throw new Error("Users not found");
|
}
|
||||||
}
|
|
||||||
|
|
||||||
console.log("fetchedUsers");
|
const crashReporterService = getInjection('ICrashReporterService');
|
||||||
|
crashReporterService.report(err);
|
||||||
return users;
|
// return {
|
||||||
}
|
// error:
|
||||||
|
// 'An error happened. The developers have been notified. Please try again later.',
|
||||||
// get current user
|
// };
|
||||||
export async function getCurrentUser(): Promise<UserResponse> {
|
throw new Error('An error happened. The developers have been notified. Please try again later.');
|
||||||
const supabase = await createClient();
|
}
|
||||||
|
|
||||||
const {
|
|
||||||
data: { user },
|
|
||||||
error,
|
|
||||||
} = await supabase.auth.getUser();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error("Error fetching current user:", error);
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const userDetail = await db.users.findUnique({
|
|
||||||
where: {
|
|
||||||
id: user?.id,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
profile: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userDetail) {
|
|
||||||
throw new Error("User not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: {
|
|
||||||
user: userDetail,
|
|
||||||
},
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new user
|
|
||||||
export async function createUser(
|
|
||||||
params: CreateUserParams
|
|
||||||
): Promise<UserResponse> {
|
|
||||||
const supabase = createAdminClient();
|
|
||||||
|
|
||||||
const { data, error } = await supabase.auth.admin.createUser({
|
|
||||||
email: params.email,
|
|
||||||
password: params.password,
|
|
||||||
phone: params.phone,
|
|
||||||
email_confirm: params.email_confirm,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error("Error creating user:", error);
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: {
|
|
||||||
user: data.user,
|
|
||||||
},
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function uploadAvatar(userId: string, email: string, file: File) {
|
|
||||||
try {
|
|
||||||
const supabase = await createClient();
|
|
||||||
|
|
||||||
const fileExt = file.name.split(".").pop();
|
|
||||||
const emailName = email.split("@")[0];
|
|
||||||
const fileName = `AVR-${emailName}.${fileExt}`;
|
|
||||||
|
|
||||||
// Change this line - store directly in the user's folder
|
|
||||||
const filePath = `${userId}/${fileName}`;
|
|
||||||
|
|
||||||
// Upload the avatar to Supabase storage
|
|
||||||
const { error: uploadError } = await supabase.storage
|
|
||||||
.from("avatars")
|
|
||||||
.upload(filePath, file, {
|
|
||||||
upsert: true,
|
|
||||||
contentType: file.type,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (uploadError) {
|
|
||||||
console.error("Error uploading avatar:", uploadError);
|
|
||||||
throw uploadError;
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
// Get the public URL
|
|
||||||
const {
|
|
||||||
data: { publicUrl },
|
|
||||||
} = supabase.storage.from("avatars").getPublicUrl(filePath);
|
|
||||||
|
|
||||||
// Update user profile with the new avatar URL
|
|
||||||
await db.users.update({
|
|
||||||
where: {
|
|
||||||
id: userId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
profile: {
|
|
||||||
update: {
|
|
||||||
avatar: publicUrl,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return publicUrl;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error uploading avatar:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function unbanUser(id: string) {
|
||||||
|
const instrumentationService = getInjection('IInstrumentationService');
|
||||||
|
return await instrumentationService.instrumentServerAction(
|
||||||
|
'unbanUser',
|
||||||
|
{ recordResponse: true },
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const unbanUserController = getInjection('IUnbanUserController');
|
||||||
|
await unbanUserController({ id });
|
||||||
|
|
||||||
// Update an existing user
|
return { success: true };
|
||||||
export async function updateUser(
|
} catch (err) {
|
||||||
userId: string,
|
if (err instanceof InputParseError) {
|
||||||
params: UpdateUserParams
|
// return {
|
||||||
): Promise<UserResponse> {
|
// error: err.message,
|
||||||
const supabase = createAdminClient();
|
// };
|
||||||
|
|
||||||
const { data, error } = await supabase.auth.admin.updateUserById(userId, {
|
throw new InputParseError(err.message);
|
||||||
email: params.email,
|
}
|
||||||
email_confirm: params.email_confirmed_at,
|
|
||||||
password: params.encrypted_password ?? undefined,
|
|
||||||
password_hash: params.encrypted_password ?? undefined,
|
|
||||||
phone: params.phone,
|
|
||||||
phone_confirm: params.phone_confirmed_at,
|
|
||||||
role: params.role,
|
|
||||||
user_metadata: params.user_metadata,
|
|
||||||
app_metadata: params.app_metadata,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
if (err instanceof UnauthenticatedError) {
|
||||||
console.error("Error updating user:", error);
|
// return {
|
||||||
throw new Error(error.message);
|
// error: 'Must be logged in to create a user.',
|
||||||
}
|
// };
|
||||||
|
throw new UnauthenticatedError('Must be logged in to unban a user.');
|
||||||
|
}
|
||||||
|
|
||||||
const user = await db.users.findUnique({
|
if (err instanceof AuthenticationError) {
|
||||||
where: {
|
// return {
|
||||||
id: userId,
|
// error: 'User not found.',
|
||||||
},
|
// };
|
||||||
include: {
|
|
||||||
profile: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
|
||||||
throw new Error("User not found");
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const updateUser = await db.users.update({
|
const crashReporterService = getInjection('ICrashReporterService');
|
||||||
where: {
|
crashReporterService.report(err);
|
||||||
id: userId,
|
// return {
|
||||||
},
|
// error:
|
||||||
data: {
|
// 'An error happened. The developers have been notified. Please try again later.',
|
||||||
role: params.role || user.role,
|
// };
|
||||||
invited_at: params.invited_at || user.invited_at,
|
throw new Error('An error happened. The developers have been notified. Please try again later.');
|
||||||
confirmed_at: params.confirmed_at || user.confirmed_at,
|
}
|
||||||
// recovery_sent_at: params.recovery_sent_at || user.recovery_sent_at,
|
|
||||||
last_sign_in_at: params.last_sign_in_at || user.last_sign_in_at,
|
|
||||||
is_anonymous: params.is_anonymous || user.is_anonymous,
|
|
||||||
created_at: params.created_at || user.created_at,
|
|
||||||
updated_at: params.updated_at || user.updated_at,
|
|
||||||
profile: {
|
|
||||||
update: {
|
|
||||||
avatar: params.profile?.avatar || user.profile?.avatar,
|
|
||||||
username: params.profile?.username || user.profile?.username,
|
|
||||||
first_name: params.profile?.first_name || user.profile?.first_name,
|
|
||||||
last_name: params.profile?.last_name || user.profile?.last_name,
|
|
||||||
bio: params.profile?.bio || user.profile?.bio,
|
|
||||||
address: params.profile?.address || user.profile?.address,
|
|
||||||
birth_date: params.profile?.birth_date || user.profile?.birth_date,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
profile: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: {
|
|
||||||
user: {
|
|
||||||
...data.user,
|
|
||||||
role: params.role,
|
|
||||||
profile: {
|
|
||||||
user_id: userId,
|
|
||||||
...updateUser.profile,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete a user
|
|
||||||
export async function deleteUser(userId: string): Promise<void> {
|
|
||||||
const supabase = createAdminClient();
|
|
||||||
|
|
||||||
const { error } = await supabase.auth.admin.deleteUser(userId);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error("Error deleting user:", error);
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send password recovery email
|
|
||||||
export async function sendPasswordRecovery(email: string): Promise<void> {
|
|
||||||
const supabase = createAdminClient();
|
|
||||||
|
|
||||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
|
||||||
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/reset-password`,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error("Error sending password recovery:", error);
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send magic link
|
|
||||||
export async function sendMagicLink(email: string): Promise<void> {
|
|
||||||
const supabase = createAdminClient();
|
|
||||||
|
|
||||||
const { error } = await supabase.auth.signInWithOtp({
|
|
||||||
email,
|
|
||||||
options: {
|
|
||||||
emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error("Error sending magic link:", error);
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ban a user
|
|
||||||
export async function banUser(userId: string): Promise<UserResponse> {
|
|
||||||
const supabase = createAdminClient();
|
|
||||||
|
|
||||||
// Ban for 100 years (effectively permanent)
|
|
||||||
const banUntil = new Date();
|
|
||||||
banUntil.setFullYear(banUntil.getFullYear() + 100);
|
|
||||||
|
|
||||||
const { data, error } = await supabase.auth.admin.updateUserById(userId, {
|
|
||||||
ban_duration: "100h",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error("Error banning user:", error);
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: {
|
|
||||||
user: data.user,
|
|
||||||
},
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unban a user
|
|
||||||
export async function unbanUser(userId: string): Promise<UserResponse> {
|
|
||||||
const supabase = createAdminClient();
|
|
||||||
|
|
||||||
const { data, error } = await supabase.auth.admin.updateUserById(userId, {
|
|
||||||
ban_duration: "none",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error("Error unbanning user:", error);
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await db.users.findUnique({
|
|
||||||
where: {
|
|
||||||
id: userId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
banned_until: true,
|
|
||||||
}
|
}
|
||||||
})
|
);
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new Error("User not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
// const updateUser = await db.users.update({
|
|
||||||
// where: {
|
|
||||||
// id: userId,
|
|
||||||
// },
|
|
||||||
// data: {
|
|
||||||
// banned_until: null,
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: {
|
|
||||||
user: data.user,
|
|
||||||
},
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invite a user
|
export async function getCurrentUser() {
|
||||||
export async function inviteUser(params: InviteUserParams): Promise<void> {
|
const instrumentationService = getInjection('IInstrumentationService');
|
||||||
const supabase = createAdminClient();
|
return await instrumentationService.instrumentServerAction(
|
||||||
|
'getCurrentUser',
|
||||||
|
{ recordResponse: true },
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const getCurrentUserController = getInjection(
|
||||||
|
'IGetCurrentUserController'
|
||||||
|
);
|
||||||
|
return await getCurrentUserController();
|
||||||
|
} catch (err) {
|
||||||
|
|
||||||
const { error } = await supabase.auth.admin.inviteUserByEmail(params.email, {
|
if (err instanceof UnauthenticatedError || err instanceof AuthenticationError) {
|
||||||
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
|
redirect('/sign-in');
|
||||||
});
|
}
|
||||||
|
|
||||||
if (error) {
|
const crashReporterService = getInjection('ICrashReporterService');
|
||||||
console.error("Error inviting user:", error);
|
crashReporterService.report(err);
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
throw new Error('An error happened. The developers have been notified. Please try again later.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserById(id: string) {
|
||||||
|
const instrumentationService = getInjection('IInstrumentationService');
|
||||||
|
return await instrumentationService.instrumentServerAction(
|
||||||
|
'getUserById',
|
||||||
|
{ recordResponse: true },
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const getUserByIdController = getInjection('IGetUserByIdController');
|
||||||
|
return await getUserByIdController({ id });
|
||||||
|
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof InputParseError) {
|
||||||
|
// return {
|
||||||
|
// error: err.message,
|
||||||
|
// };
|
||||||
|
|
||||||
|
throw new InputParseError(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof UnauthenticatedError) {
|
||||||
|
// return {
|
||||||
|
// error: 'Must be logged in to create a user.',
|
||||||
|
// };
|
||||||
|
throw new UnauthenticatedError('Must be logged in to get a user.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof AuthenticationError) {
|
||||||
|
// return {
|
||||||
|
// error: 'User not found.',
|
||||||
|
// };
|
||||||
|
|
||||||
|
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const crashReporterService = getInjection('ICrashReporterService');
|
||||||
|
crashReporterService.report(err);
|
||||||
|
// return {
|
||||||
|
// error:
|
||||||
|
// 'An error happened. The developers have been notified. Please try again later.',
|
||||||
|
// };
|
||||||
|
throw new Error('An error happened. The developers have been notified. Please try again later.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserByEmail(email: string) {
|
||||||
|
const instrumentationService = getInjection('IInstrumentationService');
|
||||||
|
return await instrumentationService.instrumentServerAction(
|
||||||
|
'getUserByEmail',
|
||||||
|
{ recordResponse: true },
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const getUserByEmailController = getInjection(
|
||||||
|
'IGetUserByEmailController'
|
||||||
|
);
|
||||||
|
return await getUserByEmailController({ email });
|
||||||
|
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof InputParseError) {
|
||||||
|
// return {
|
||||||
|
// error: err.message,
|
||||||
|
// };
|
||||||
|
|
||||||
|
throw new InputParseError(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof UnauthenticatedError) {
|
||||||
|
// return {
|
||||||
|
// error: 'Must be logged in to create a user.',
|
||||||
|
// };
|
||||||
|
throw new UnauthenticatedError('Must be logged in to get a user.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof AuthenticationError) {
|
||||||
|
// return {
|
||||||
|
// error: 'User not found.',
|
||||||
|
// };
|
||||||
|
|
||||||
|
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const crashReporterService = getInjection('ICrashReporterService');
|
||||||
|
crashReporterService.report(err);
|
||||||
|
// return {
|
||||||
|
// error:
|
||||||
|
// 'An error happened. The developers have been notified. Please try again later.',
|
||||||
|
// };
|
||||||
|
throw new Error('An error happened. The developers have been notified. Please try again later.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserByUsername(username: string) {
|
||||||
|
const instrumentationService = getInjection('IInstrumentationService');
|
||||||
|
return await instrumentationService.instrumentServerAction(
|
||||||
|
'getUserByUsername',
|
||||||
|
{ recordResponse: true },
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const getUserByUsernameController = getInjection(
|
||||||
|
'IGetUserByUsernameController'
|
||||||
|
);
|
||||||
|
return await getUserByUsernameController({ username });
|
||||||
|
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
|
||||||
|
if (err instanceof InputParseError) {
|
||||||
|
// return {
|
||||||
|
// error: err.message,
|
||||||
|
// };
|
||||||
|
|
||||||
|
throw new InputParseError(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof UnauthenticatedError) {
|
||||||
|
// return {
|
||||||
|
// error: 'Must be logged in to create a user.',
|
||||||
|
// };
|
||||||
|
throw new UnauthenticatedError('Must be logged in to get a user.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof AuthenticationError) {
|
||||||
|
// return {
|
||||||
|
// error: 'User not found.',
|
||||||
|
// };
|
||||||
|
|
||||||
|
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const crashReporterService = getInjection('ICrashReporterService');
|
||||||
|
crashReporterService.report(err);
|
||||||
|
// return {
|
||||||
|
// error:
|
||||||
|
// 'An error happened. The developers have been notified. Please try again later.',
|
||||||
|
// };
|
||||||
|
throw new Error('An error happened. The developers have been notified. Please try again later.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUsers() {
|
||||||
|
const instrumentationService = getInjection('IInstrumentationService');
|
||||||
|
return await instrumentationService.instrumentServerAction(
|
||||||
|
'getUsers',
|
||||||
|
{ recordResponse: true },
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const getUsersController = getInjection('IGetUsersController');
|
||||||
|
return await getUsersController();
|
||||||
|
} catch (err) {
|
||||||
|
if (
|
||||||
|
err instanceof UnauthenticatedError ||
|
||||||
|
err instanceof AuthenticationError
|
||||||
|
) {
|
||||||
|
redirect('/sign-in');
|
||||||
|
}
|
||||||
|
|
||||||
|
const crashReporterService = getInjection('ICrashReporterService');
|
||||||
|
crashReporterService.report(err);
|
||||||
|
throw new Error('An error happened. The developers have been notified. Please try again later.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function inviteUser(credentials: ICredentialsInviteUserSchema) {
|
||||||
|
const instrumentationService = getInjection('IInstrumentationService');
|
||||||
|
return await instrumentationService.instrumentServerAction(
|
||||||
|
'inviteUser',
|
||||||
|
{ recordResponse: true },
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const inviteUserController = getInjection('IInviteUserController');
|
||||||
|
await inviteUserController({ email: credentials.email });
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (err) {
|
||||||
|
|
||||||
|
if (err instanceof InputParseError) {
|
||||||
|
// return {
|
||||||
|
// error: err.message,
|
||||||
|
// };
|
||||||
|
|
||||||
|
throw new InputParseError(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof UnauthenticatedError) {
|
||||||
|
// return {
|
||||||
|
// error: 'Must be logged in to create a user.',
|
||||||
|
// };
|
||||||
|
throw new UnauthenticatedError('Must be logged in to invite a user.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof AuthenticationError) {
|
||||||
|
// return {
|
||||||
|
// error: 'User not found.',
|
||||||
|
// };
|
||||||
|
|
||||||
|
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const crashReporterService = getInjection('ICrashReporterService');
|
||||||
|
crashReporterService.report(err);
|
||||||
|
// return {
|
||||||
|
// error:
|
||||||
|
// 'An error happened. The developers have been notified. Please try again later.',
|
||||||
|
// };
|
||||||
|
throw new Error('An error happened. The developers have been notified. Please try again later.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createUser(data: ICreateUserSchema) {
|
||||||
|
const instrumentationService = getInjection('IInstrumentationService');
|
||||||
|
return await instrumentationService.instrumentServerAction(
|
||||||
|
'createUser',
|
||||||
|
{ recordResponse: true },
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const createUserController = getInjection('ICreateUserController');
|
||||||
|
await createUserController(data);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
|
||||||
|
if (err instanceof InputParseError) {
|
||||||
|
// return {
|
||||||
|
// error: err.message,
|
||||||
|
// };
|
||||||
|
|
||||||
|
throw new InputParseError(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof UnauthenticatedError) {
|
||||||
|
// return {
|
||||||
|
// error: 'Must be logged in to create a user.',
|
||||||
|
// };
|
||||||
|
throw new UnauthenticatedError('Must be logged in to create a user.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof AuthenticationError) {
|
||||||
|
// return {
|
||||||
|
// error: 'User not found.',
|
||||||
|
// };
|
||||||
|
|
||||||
|
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const crashReporterService = getInjection('ICrashReporterService');
|
||||||
|
crashReporterService.report(err);
|
||||||
|
// return {
|
||||||
|
// error:
|
||||||
|
// 'An error happened. The developers have been notified. Please try again later.',
|
||||||
|
// };
|
||||||
|
throw new Error('An error happened. The developers have been notified. Please try again later.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateUser(id: string, data: IUpdateUserSchema) {
|
||||||
|
const instrumentationService = getInjection('IInstrumentationService');
|
||||||
|
return await instrumentationService.instrumentServerAction(
|
||||||
|
'updateUser',
|
||||||
|
{ recordResponse: true },
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const updateUserController = getInjection('IUpdateUserController');
|
||||||
|
await updateUserController(id, data);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof InputParseError) {
|
||||||
|
// return {
|
||||||
|
// error: err.message,
|
||||||
|
// };
|
||||||
|
|
||||||
|
throw new InputParseError(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof UnauthenticatedError) {
|
||||||
|
// return {
|
||||||
|
// error: 'Must be logged in to create a user.',
|
||||||
|
// };
|
||||||
|
throw new UnauthenticatedError('Must be logged in to update a user.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof AuthenticationError) {
|
||||||
|
// return {
|
||||||
|
// error: 'User not found.',
|
||||||
|
// };
|
||||||
|
|
||||||
|
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const crashReporterService = getInjection('ICrashReporterService');
|
||||||
|
crashReporterService.report(err);
|
||||||
|
// return {
|
||||||
|
// error:
|
||||||
|
// 'An error happened. The developers have been notified. Please try again later.',
|
||||||
|
// };
|
||||||
|
throw new Error('An error happened. The developers have been notified. Please try again later.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteUser(id: string) {
|
||||||
|
const instrumentationService = getInjection('IInstrumentationService');
|
||||||
|
return await instrumentationService.instrumentServerAction(
|
||||||
|
'deleteUser',
|
||||||
|
{ recordResponse: true },
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const deleteUserController = getInjection('IDeleteUserController');
|
||||||
|
await deleteUserController({ id });
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof InputParseError) {
|
||||||
|
// return {
|
||||||
|
// error: err.message,
|
||||||
|
// };
|
||||||
|
|
||||||
|
throw new InputParseError(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof UnauthenticatedError) {
|
||||||
|
// return {
|
||||||
|
// error: 'Must be logged in to create a user.',
|
||||||
|
// };
|
||||||
|
throw new UnauthenticatedError('Must be logged in to delete a user.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof AuthenticationError) {
|
||||||
|
// return {
|
||||||
|
// error: 'User not found.',
|
||||||
|
// };
|
||||||
|
|
||||||
|
throw new AuthenticationError('There was an error with the credentials. Please try again or contact support.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const crashReporterService = getInjection('ICrashReporterService');
|
||||||
|
crashReporterService.report(err);
|
||||||
|
// return {
|
||||||
|
// error:
|
||||||
|
// 'An error happened. The developers have been notified. Please try again later.',
|
||||||
|
// };
|
||||||
|
throw new Error('An error happened. The developers have been notified. Please try again later.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,308 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useCreateUserMutation, useInviteUserMutation } from './queries';
|
||||||
|
import { IUserSchema, IUserFilterOptionsSchema } from '@/src/entities/models/users/users.model';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { set } from 'date-fns';
|
||||||
|
import { CreateUserSchema, defaulICreateUserSchemaValues, ICreateUserSchema } from '@/src/entities/models/users/create-user.model';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { defaulIInviteUserSchemaValues, IInviteUserSchema, InviteUserSchema } from '@/src/entities/models/users/invite-user.model';
|
||||||
|
|
||||||
|
export const useAddUserDialogHandler = ({ onUserAdded, onOpenChange }: {
|
||||||
|
onUserAdded: () => void;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
}) => {
|
||||||
|
const { createUser, isPending } = useCreateUserMutation();
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { errors: errors },
|
||||||
|
setError,
|
||||||
|
getValues,
|
||||||
|
clearErrors,
|
||||||
|
watch,
|
||||||
|
} = useForm<ICreateUserSchema>({
|
||||||
|
resolver: zodResolver(CreateUserSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
email_confirm: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emailConfirm = watch("email_confirm");
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit(async (data) => {
|
||||||
|
|
||||||
|
await createUser(data, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("User created successfully.");
|
||||||
|
onUserAdded();
|
||||||
|
onOpenChange(false);
|
||||||
|
reset();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
reset();
|
||||||
|
toast.error(error.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleOpenChange = (open: boolean) => {
|
||||||
|
if (!open) {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
onOpenChange(open);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
register,
|
||||||
|
handleSubmit: onSubmit,
|
||||||
|
reset,
|
||||||
|
errors,
|
||||||
|
isPending,
|
||||||
|
getValues,
|
||||||
|
clearErrors,
|
||||||
|
emailConfirm,
|
||||||
|
handleOpenChange,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useInviteUserHandler = ({ onUserInvited, onOpenChange }: {
|
||||||
|
onUserInvited: () => void;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const { inviteUser, isPending } = useInviteUserMutation();
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { errors: errors },
|
||||||
|
setError,
|
||||||
|
getValues,
|
||||||
|
clearErrors,
|
||||||
|
watch,
|
||||||
|
} = useForm<IInviteUserSchema>({
|
||||||
|
resolver: zodResolver(InviteUserSchema),
|
||||||
|
defaultValues: defaulIInviteUserSchemaValues
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit(async (data) => {
|
||||||
|
await inviteUser(data, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Invitation sent");
|
||||||
|
onUserInvited();
|
||||||
|
onOpenChange(false);
|
||||||
|
reset();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
reset();
|
||||||
|
toast.error("Failed to send invitation");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleOpenChange = (open: boolean) => {
|
||||||
|
if (!open) {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
onOpenChange(open);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
register,
|
||||||
|
handleSubmit: onSubmit,
|
||||||
|
handleOpenChange,
|
||||||
|
reset,
|
||||||
|
getValues,
|
||||||
|
clearErrors,
|
||||||
|
watch,
|
||||||
|
errors,
|
||||||
|
isPending,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUserManagementHandlers = (refetch: () => void) => {
|
||||||
|
const [searchQuery, setSearchQuery] = useState("")
|
||||||
|
const [detailUser, setDetailUser] = useState<IUserSchema | null>(null)
|
||||||
|
const [updateUser, setUpdateUser] = useState<IUserSchema | null>(null)
|
||||||
|
const [isSheetOpen, setIsSheetOpen] = useState(false)
|
||||||
|
const [isUpdateOpen, setIsUpdateOpen] = useState(false)
|
||||||
|
const [isAddUserOpen, setIsAddUserOpen] = useState(false)
|
||||||
|
const [isInviteUserOpen, setIsInviteUserOpen] = useState(false)
|
||||||
|
|
||||||
|
// Filter states
|
||||||
|
const [filters, setFilters] = useState<IUserFilterOptionsSchema>({
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
lastSignIn: "",
|
||||||
|
createdAt: "",
|
||||||
|
status: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle opening the detail sheet
|
||||||
|
const handleUserClick = (user: IUserSchema) => {
|
||||||
|
setDetailUser(user)
|
||||||
|
setIsSheetOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle opening the update sheet
|
||||||
|
const handleUserUpdate = (user: IUserSchema) => {
|
||||||
|
setUpdateUser(user)
|
||||||
|
setIsUpdateOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close detail sheet when update sheet opens
|
||||||
|
useEffect(() => {
|
||||||
|
if (isUpdateOpen) {
|
||||||
|
setIsSheetOpen(false)
|
||||||
|
}
|
||||||
|
}, [isUpdateOpen])
|
||||||
|
|
||||||
|
// Reset detail user when sheet closes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSheetOpen) {
|
||||||
|
// Use a small delay to prevent flickering if another sheet is opening
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (!isSheetOpen && !isUpdateOpen) {
|
||||||
|
setDetailUser(null)
|
||||||
|
}
|
||||||
|
}, 300)
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}
|
||||||
|
}, [isSheetOpen, isUpdateOpen])
|
||||||
|
|
||||||
|
// Reset update user when update sheet closes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isUpdateOpen) {
|
||||||
|
// Use a small delay to prevent flickering if another sheet is opening
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (!isUpdateOpen) {
|
||||||
|
setUpdateUser(null)
|
||||||
|
}
|
||||||
|
}, 300)
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}
|
||||||
|
}, [isUpdateOpen])
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
|
setFilters({
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
lastSignIn: "",
|
||||||
|
createdAt: "",
|
||||||
|
status: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getActiveFilterCount = () => {
|
||||||
|
return Object.values(filters).filter(
|
||||||
|
(value) => (typeof value === "string" && value !== "") || (Array.isArray(value) && value.length > 0),
|
||||||
|
).length
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchQuery,
|
||||||
|
setSearchQuery,
|
||||||
|
detailUser,
|
||||||
|
updateUser,
|
||||||
|
isSheetOpen,
|
||||||
|
setIsSheetOpen,
|
||||||
|
isUpdateOpen,
|
||||||
|
setIsUpdateOpen,
|
||||||
|
isAddUserOpen,
|
||||||
|
setIsAddUserOpen,
|
||||||
|
isInviteUserOpen,
|
||||||
|
setIsInviteUserOpen,
|
||||||
|
filters,
|
||||||
|
setFilters,
|
||||||
|
handleUserClick,
|
||||||
|
handleUserUpdate,
|
||||||
|
clearFilters,
|
||||||
|
getActiveFilterCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const filterUsers = (users: IUserSchema[], searchQuery: string, filters: IUserFilterOptionsSchema): IUserSchema[] => {
|
||||||
|
return users.filter((user) => {
|
||||||
|
|
||||||
|
// Global search
|
||||||
|
if (searchQuery) {
|
||||||
|
const query = searchQuery.toLowerCase()
|
||||||
|
const matchesSearch =
|
||||||
|
user.email?.toLowerCase().includes(query) ||
|
||||||
|
user.phone?.toLowerCase().includes(query) ||
|
||||||
|
user.id.toLowerCase().includes(query)
|
||||||
|
|
||||||
|
if (!matchesSearch) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email filter
|
||||||
|
if (filters.email && !user.email?.toLowerCase().includes(filters.email.toLowerCase())) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phone filter
|
||||||
|
if (filters.phone && !user.phone?.toLowerCase().includes(filters.phone.toLowerCase())) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last sign in filter
|
||||||
|
if (filters.lastSignIn) {
|
||||||
|
if (filters.lastSignIn === "never" && user.last_sign_in_at) {
|
||||||
|
return false
|
||||||
|
} else if (filters.lastSignIn === "today") {
|
||||||
|
const today = new Date()
|
||||||
|
today.setHours(0, 0, 0, 0)
|
||||||
|
const signInDate = user.last_sign_in_at ? new Date(user.last_sign_in_at) : null
|
||||||
|
if (!signInDate || signInDate < today) return false
|
||||||
|
} else if (filters.lastSignIn === "week") {
|
||||||
|
const weekAgo = new Date()
|
||||||
|
weekAgo.setDate(weekAgo.getDate() - 7)
|
||||||
|
const signInDate = user.last_sign_in_at ? new Date(user.last_sign_in_at) : null
|
||||||
|
if (!signInDate || signInDate < weekAgo) return false
|
||||||
|
} else if (filters.lastSignIn === "month") {
|
||||||
|
const monthAgo = new Date()
|
||||||
|
monthAgo.setMonth(monthAgo.getMonth() - 1)
|
||||||
|
const signInDate = user.last_sign_in_at ? new Date(user.last_sign_in_at) : null
|
||||||
|
if (!signInDate || signInDate < monthAgo) return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Created at filter
|
||||||
|
if (filters.createdAt) {
|
||||||
|
if (filters.createdAt === "today") {
|
||||||
|
const today = new Date()
|
||||||
|
today.setHours(0, 0, 0, 0)
|
||||||
|
const createdAt = user.created_at ? (user.created_at ? new Date(user.created_at) : new Date()) : new Date()
|
||||||
|
if (createdAt < today) return false
|
||||||
|
} else if (filters.createdAt === "week") {
|
||||||
|
const weekAgo = new Date()
|
||||||
|
weekAgo.setDate(weekAgo.getDate() - 7)
|
||||||
|
const createdAt = user.created_at ? new Date(user.created_at) : new Date()
|
||||||
|
if (createdAt < weekAgo) return false
|
||||||
|
} else if (filters.createdAt === "month") {
|
||||||
|
const monthAgo = new Date()
|
||||||
|
monthAgo.setMonth(monthAgo.getMonth() - 1)
|
||||||
|
const createdAt = user.created_at ? new Date(user.created_at) : new Date()
|
||||||
|
if (createdAt < monthAgo) return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status filter
|
||||||
|
if (filters.status.length > 0) {
|
||||||
|
const userStatus = user.banned_until ? "banned" : !user.email_confirmed_at ? "unconfirmed" : "active"
|
||||||
|
|
||||||
|
if (!filters.status.includes(userStatus)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
banUser,
|
||||||
|
getCurrentUser,
|
||||||
|
getUserByEmail,
|
||||||
|
getUserById,
|
||||||
|
getUsers,
|
||||||
|
unbanUser,
|
||||||
|
inviteUser,
|
||||||
|
createUser,
|
||||||
|
updateUser,
|
||||||
|
deleteUser,
|
||||||
|
getUserByUsername
|
||||||
|
} from "./action";
|
||||||
|
import { IUserSchema } from "@/src/entities/models/users/users.model";
|
||||||
|
import { IBanUserSchema, ICredentialsBanUserSchema } from "@/src/entities/models/users/ban-user.model";
|
||||||
|
import { IUnbanUserSchema } from "@/src/entities/models/users/unban-user.model";
|
||||||
|
import { ICreateUserSchema } from "@/src/entities/models/users/create-user.model";
|
||||||
|
import { IUpdateUserSchema } from "@/src/entities/models/users/update-user.model";
|
||||||
|
import { ICredentialsInviteUserSchema } from "@/src/entities/models/users/invite-user.model";
|
||||||
|
|
||||||
|
const useUsersAction = () => {
|
||||||
|
|
||||||
|
// For all users (no parameters needed)
|
||||||
|
const getUsersQuery = useQuery<IUserSchema[]>({
|
||||||
|
queryKey: ["users"],
|
||||||
|
queryFn: async () => await getUsers()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Current user query doesn't need parameters
|
||||||
|
const getCurrentUserQuery = useQuery({
|
||||||
|
queryKey: ["user", "current"],
|
||||||
|
queryFn: async () => await getCurrentUser()
|
||||||
|
});
|
||||||
|
|
||||||
|
const getUserByIdQuery = (id: string) => ({
|
||||||
|
queryKey: ["user", id],
|
||||||
|
queryFn: async () => await getUserById(id)
|
||||||
|
});
|
||||||
|
|
||||||
|
const getUserByEmailQuery = (email: string) => ({
|
||||||
|
queryKey: ["user", "email", email],
|
||||||
|
queryFn: async () => await getUserByEmail(email)
|
||||||
|
});
|
||||||
|
|
||||||
|
const getUserByUsernameQuery = (username: string) => ({
|
||||||
|
queryKey: ["user", "username", username],
|
||||||
|
queryFn: async () => await getUserByUsername(username)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mutations that don't need dynamic parameters
|
||||||
|
const banUserMutation = useMutation({
|
||||||
|
mutationKey: ["banUser"],
|
||||||
|
mutationFn: async ({ credential, params }: { credential: ICredentialsBanUserSchema; params: IBanUserSchema }) => await banUser(credential.id, params.ban_duration)
|
||||||
|
});
|
||||||
|
|
||||||
|
const unbanUserMutation = useMutation({
|
||||||
|
mutationKey: ["unbanUser"],
|
||||||
|
mutationFn: async (params: IUnbanUserSchema) => await unbanUser(params.id)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create functions that return configured hooks
|
||||||
|
const inviteUserMutation = useMutation({
|
||||||
|
mutationKey: ["inviteUser"],
|
||||||
|
mutationFn: async (credential: ICredentialsInviteUserSchema) => await inviteUser(credential)
|
||||||
|
});
|
||||||
|
|
||||||
|
const createUserMutation = useMutation({
|
||||||
|
mutationKey: ["createUser"],
|
||||||
|
mutationFn: async (data: ICreateUserSchema) => await createUser(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateUserMutation = useMutation({
|
||||||
|
mutationKey: ["updateUser"],
|
||||||
|
mutationFn: async (params: { id: string; data: IUpdateUserSchema }) => updateUser(params.id, params.data)
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteUserMutation = useMutation({
|
||||||
|
mutationKey: ["deleteUser"],
|
||||||
|
mutationFn: async (id: string) => await deleteUser(id)
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
getUsers: getUsersQuery,
|
||||||
|
getCurrentUser: getCurrentUserQuery,
|
||||||
|
getUserById: getUserByIdQuery,
|
||||||
|
getUserByEmailQuery,
|
||||||
|
getUserByUsernameQuery,
|
||||||
|
banUser: banUserMutation,
|
||||||
|
unbanUser: unbanUserMutation,
|
||||||
|
inviteUser: inviteUserMutation,
|
||||||
|
createUser: createUserMutation,
|
||||||
|
updateUser: updateUserMutation,
|
||||||
|
deleteUser: deleteUserMutation
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetUsersQuery = () => {
|
||||||
|
const { getUsers } = useUsersAction();
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: getUsers.data,
|
||||||
|
isPending: getUsers.isPending,
|
||||||
|
error: getUsers.error,
|
||||||
|
refetch: getUsers.refetch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetCurrentUserQuery = () => {
|
||||||
|
const { getCurrentUser } = useUsersAction();
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: getCurrentUser.data,
|
||||||
|
isPending: getCurrentUser.isPending,
|
||||||
|
error: getCurrentUser.error,
|
||||||
|
refetch: getCurrentUser.refetch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCreateUserMutation = () => {
|
||||||
|
const { createUser } = useUsersAction();
|
||||||
|
|
||||||
|
return {
|
||||||
|
createUser: createUser.mutateAsync,
|
||||||
|
isPending: createUser.isPending,
|
||||||
|
errors: createUser.error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useInviteUserMutation = () => {
|
||||||
|
const { inviteUser } = useUsersAction();
|
||||||
|
|
||||||
|
return {
|
||||||
|
inviteUser: inviteUser.mutateAsync,
|
||||||
|
isPending: inviteUser.isPending,
|
||||||
|
errors: inviteUser.error,
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import { redirect } from "next/navigation"
|
||||||
import { getInjection } from "@/di/container"
|
import { getInjection } from "@/di/container"
|
||||||
import { revalidatePath } from "next/cache"
|
import { revalidatePath } from "next/cache"
|
||||||
|
|
||||||
import { InputParseError } from "@/src/entities/errors/common"
|
import { InputParseError, NotFoundError } from "@/src/entities/errors/common"
|
||||||
import { AuthenticationError, UnauthenticatedError } from "@/src/entities/errors/auth"
|
import { AuthenticationError, UnauthenticatedError } from "@/src/entities/errors/auth"
|
||||||
import { createClient } from "@/app/_utils/supabase/server"
|
import { createClient } from "@/app/_utils/supabase/server"
|
||||||
|
|
||||||
|
@ -18,25 +18,22 @@ export async function signIn(formData: FormData) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const signInController = getInjection("ISignInController")
|
const signInController = getInjection("ISignInController")
|
||||||
await signInController({ email })
|
return await signInController({ email })
|
||||||
|
|
||||||
// if (email) {
|
// if (email) {
|
||||||
// redirect(`/verify-otp?email=${encodeURIComponent(email)}`)
|
// redirect(`/verify-otp?email=${encodeURIComponent(email)}`)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
return { success: true }
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (
|
if (err instanceof InputParseError) {
|
||||||
err instanceof InputParseError ||
|
return { error: err.message }
|
||||||
err instanceof AuthenticationError
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
error: 'Incorrect credential. Please try again.',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err instanceof UnauthenticatedError) {
|
if (err instanceof AuthenticationError) {
|
||||||
|
return { error: "Invalid credential. Please try again." }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err instanceof UnauthenticatedError || err instanceof NotFoundError) {
|
||||||
return {
|
return {
|
||||||
error: 'User not found. Please tell your admin to create an account for you.',
|
error: 'User not found. Please tell your admin to create an account for you.',
|
||||||
};
|
};
|
||||||
|
@ -136,3 +133,57 @@ export async function verifyOtp(formData: FormData) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function sendMagicLink(formData: FormData) {
|
||||||
|
const instrumentationService = getInjection("IInstrumentationService")
|
||||||
|
return await instrumentationService.instrumentServerAction("sendMagicLink", {
|
||||||
|
recordResponse: true
|
||||||
|
}, async () => {
|
||||||
|
try {
|
||||||
|
const email = formData.get("email")?.toString()
|
||||||
|
|
||||||
|
const sendMagicLinkController = getInjection("ISendMagicLinkController")
|
||||||
|
await sendMagicLinkController({ email })
|
||||||
|
|
||||||
|
return { success: true }
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof InputParseError) {
|
||||||
|
return { error: err.message }
|
||||||
|
}
|
||||||
|
|
||||||
|
const crashReporterService = getInjection("ICrashReporterService")
|
||||||
|
crashReporterService.report(err)
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: "An error occurred during sending magic link. Please try again later.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendPasswordRecovery(formData: FormData) {
|
||||||
|
const instrumentationService = getInjection("IInstrumentationService")
|
||||||
|
return await instrumentationService.instrumentServerAction("sendPasswordRecovery", {
|
||||||
|
recordResponse: true
|
||||||
|
}, async () => {
|
||||||
|
try {
|
||||||
|
const email = formData.get("email")?.toString()
|
||||||
|
|
||||||
|
const sendPasswordRecoveryController = getInjection("ISendPasswordRecoveryController")
|
||||||
|
await sendPasswordRecoveryController({ email })
|
||||||
|
|
||||||
|
return { success: true }
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof InputParseError) {
|
||||||
|
return { error: err.message }
|
||||||
|
}
|
||||||
|
|
||||||
|
const crashReporterService = getInjection("ICrashReporterService")
|
||||||
|
crashReporterService.report(err)
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: "An error occurred during sending password recovery. Please try again later.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,19 +1,18 @@
|
||||||
import { AuthenticationError } from "@/src/entities/errors/auth";
|
import { AuthenticationError } from "@/src/entities/errors/auth";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useAuthActions } from "./mutation";
|
import { useAuthActions } from './queries';
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from '@hookform/resolvers/zod';;
|
||||||
import { defaultSignInPasswordlessValues, SignInFormData, SignInPasswordless, SignInPasswordlessSchema, SignInSchema } from "@/src/entities/models/auth/sign-in.model";
|
import { toast } from 'sonner';
|
||||||
import { createFormData } from "@/app/_utils/common";
|
import { useNavigations } from '@/app/_hooks/use-navigations';
|
||||||
import { useFormHandler } from "@/app/_hooks/use-form-handler";
|
import {
|
||||||
import { toast } from "sonner";
|
IVerifyOtpSchema,
|
||||||
import { signIn } from "./action";
|
verifyOtpSchema,
|
||||||
import { useNavigations } from "@/app/_hooks/use-navigations";
|
} from '@/src/entities/models/auth/verify-otp.model';
|
||||||
import { VerifyOtpFormData, verifyOtpSchema } from "@/src/entities/models/auth/verify-otp.model";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook untuk menangani proses sign in
|
* Hook untuk menangani proses sign in
|
||||||
*
|
*
|
||||||
* @returns {Object} Object berisi handler dan state untuk form sign in
|
* @returns {Object} Object berisi handler dan state untuk form sign in
|
||||||
* @example
|
* @example
|
||||||
* const { handleSubmit, isPending, error } = useSignInHandler();
|
* const { handleSubmit, isPending, error } = useSignInHandler();
|
||||||
|
@ -32,23 +31,18 @@ export function useSignInHandler() {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
|
|
||||||
const formData = new FormData(event.currentTarget);
|
const formData = new FormData(event.currentTarget);
|
||||||
const email = formData.get("email")?.toString()
|
const email = formData.get('email')?.toString();
|
||||||
|
|
||||||
try {
|
const res = await signIn.mutateAsync(formData);
|
||||||
await signIn.mutateAsync(formData, {
|
|
||||||
onSuccess: () => {
|
if (!res?.error) {
|
||||||
toast("An email has been sent to you. Please check your inbox.");
|
toast('An email has been sent to you. Please check your inbox.');
|
||||||
if (email) router.push(`/verify-otp?email=${encodeURIComponent(email)}`);
|
if (email) router.push(`/verify-otp?email=${encodeURIComponent(email)}`);
|
||||||
},
|
} else {
|
||||||
onError: (error) => {
|
setError(res.error);
|
||||||
setError(error.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
setError(error.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -58,67 +52,66 @@ export function useSignInHandler() {
|
||||||
error,
|
error,
|
||||||
isPending: signIn.isPending,
|
isPending: signIn.isPending,
|
||||||
errors: !!error || signIn.error,
|
errors: !!error || signIn.error,
|
||||||
clearError: () => setError(undefined)
|
clearError: () => setError(undefined),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function useVerifyOtpHandler(email: string) {
|
export function useVerifyOtpHandler(email: string) {
|
||||||
const { router } = useNavigations()
|
const { router } = useNavigations();
|
||||||
const { verifyOtp } = useAuthActions()
|
const { verifyOtp } = useAuthActions();
|
||||||
const [error, setError] = useState<string>()
|
const [error, setError] = useState<string>();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit: hookFormSubmit,
|
handleSubmit: hookFormSubmit,
|
||||||
control,
|
control,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
setValue
|
setValue,
|
||||||
} = useForm<VerifyOtpFormData>({
|
} = useForm<IVerifyOtpSchema>({
|
||||||
resolver: zodResolver(verifyOtpSchema),
|
resolver: zodResolver(verifyOtpSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email,
|
email,
|
||||||
token: ""
|
token: '',
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const handleOtpChange = (value: string, onChange: (value: string) => void) => {
|
const handleOtpChange = (
|
||||||
onChange(value)
|
value: string,
|
||||||
|
onChange: (value: string) => void
|
||||||
|
) => {
|
||||||
|
onChange(value);
|
||||||
|
|
||||||
|
if (value.length === 6) {
|
||||||
|
handleSubmit();
|
||||||
|
}
|
||||||
|
|
||||||
// Clear error when user starts typing
|
// Clear error when user starts typing
|
||||||
if (error) {
|
if (error) {
|
||||||
setError(undefined)
|
setError(undefined);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleSubmit = hookFormSubmit(async (data) => {
|
const handleSubmit = hookFormSubmit(async (data) => {
|
||||||
if (verifyOtp.isPending) return
|
if (verifyOtp.isPending) return;
|
||||||
|
|
||||||
setError(undefined)
|
setError(undefined);
|
||||||
|
|
||||||
// Create FormData object
|
// Create FormData object
|
||||||
const formData = new FormData()
|
const formData = new FormData();
|
||||||
formData.append("email", data.email)
|
formData.append('email', data.email);
|
||||||
formData.append("token", data.token)
|
formData.append('token', data.token);
|
||||||
|
|
||||||
try {
|
await verifyOtp.mutateAsync(formData, {
|
||||||
await verifyOtp.mutateAsync(formData, {
|
onSuccess: () => {
|
||||||
onSuccess: () => {
|
toast.success('OTP verified successfully');
|
||||||
toast.success("OTP verified successfully")
|
// Navigate to dashboard on success
|
||||||
// Navigate to dashboard on success
|
router.push('/dashboard');
|
||||||
router.push("/dashboard")
|
},
|
||||||
},
|
onError: (error) => {
|
||||||
onError: (error) => {
|
setError(error.message);
|
||||||
setError(error.message)
|
},
|
||||||
}
|
});
|
||||||
})
|
});
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
setError(error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
register,
|
register,
|
||||||
|
@ -127,50 +120,42 @@ export function useVerifyOtpHandler(email: string) {
|
||||||
handleOtpChange,
|
handleOtpChange,
|
||||||
errors: {
|
errors: {
|
||||||
...errors,
|
...errors,
|
||||||
token: error ? { message: error } : errors.token
|
token: error ? { message: error } : errors.token,
|
||||||
},
|
},
|
||||||
isPending: verifyOtp.isPending,
|
isPending: verifyOtp.isPending,
|
||||||
clearError: () => setError(undefined)
|
clearError: () => setError(undefined),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSignOutHandler() {
|
export function useSignOutHandler() {
|
||||||
const { signOut } = useAuthActions()
|
const { signOut } = useAuthActions();
|
||||||
const { router } = useNavigations()
|
const { router } = useNavigations();
|
||||||
const [error, setError] = useState<string>()
|
const [error, setError] = useState<string>();
|
||||||
|
|
||||||
const handleSignOut = async () => {
|
const handleSignOut = async () => {
|
||||||
if (signOut.isPending) return
|
if (signOut.isPending) return;
|
||||||
|
|
||||||
setError(undefined)
|
setError(undefined);
|
||||||
|
|
||||||
try {
|
await signOut.mutateAsync(undefined, {
|
||||||
await signOut.mutateAsync(undefined, {
|
onSuccess: () => {
|
||||||
onSuccess: () => {
|
toast.success('You have been signed out successfully');
|
||||||
toast.success("You have been signed out successfully")
|
router.push('/sign-in');
|
||||||
router.push("/sign-in")
|
},
|
||||||
},
|
onError: (error) => {
|
||||||
onError: (error) => {
|
if (error instanceof AuthenticationError) {
|
||||||
if (error instanceof AuthenticationError) {
|
setError(error.message);
|
||||||
setError(error.message)
|
toast.error(error.message);
|
||||||
toast.error(error.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
} catch (error) {
|
});
|
||||||
if (error instanceof Error) {
|
};
|
||||||
setError(error.message)
|
|
||||||
toast.error(error.message)
|
|
||||||
// toast.error("An error occurred during sign out. Please try again later.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleSignOut,
|
handleSignOut,
|
||||||
error,
|
error,
|
||||||
isPending: signOut.isPending,
|
isPending: signOut.isPending,
|
||||||
errors: !!error || signOut.error,
|
errors: !!error || signOut.error,
|
||||||
clearError: () => setError(undefined)
|
clearError: () => setError(undefined),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
import { queryOptions, useMutation } from '@tanstack/react-query';
|
|
||||||
import { signIn, signOut, verifyOtp } from './action';
|
|
||||||
|
|
||||||
export function useAuthActions() {
|
|
||||||
// Sign In Mutation
|
|
||||||
const signInMutation = useMutation({
|
|
||||||
mutationKey: ["signIn"],
|
|
||||||
mutationFn: async (formData: FormData) => {
|
|
||||||
const response = await signIn(formData);
|
|
||||||
|
|
||||||
// If the server action returns an error, treat it as an error for React Query
|
|
||||||
if (response?.error) {
|
|
||||||
throw new Error(response.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const verifyOtpMutation = useMutation({
|
|
||||||
mutationKey: ["verifyOtp"],
|
|
||||||
mutationFn: async (formData: FormData) => {
|
|
||||||
const response = await verifyOtp(formData);
|
|
||||||
|
|
||||||
// If the server action returns an error, treat it as an error for React Query
|
|
||||||
if (response?.error) {
|
|
||||||
throw new Error(response.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const signOutMutation = useMutation({
|
|
||||||
mutationKey: ["signOut"],
|
|
||||||
mutationFn: async () => {
|
|
||||||
const response = await signOut();
|
|
||||||
|
|
||||||
// If the server action returns an error, treat it as an error for React Query
|
|
||||||
if (response?.error) {
|
|
||||||
throw new Error(response.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
signIn: signInMutation,
|
|
||||||
verifyOtp: verifyOtpMutation,
|
|
||||||
signOut: signOutMutation
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import { sendMagicLink, sendPasswordRecovery, signIn, signOut, verifyOtp } from './action';
|
||||||
|
|
||||||
|
export function useAuthActions() {
|
||||||
|
// Sign In Mutation
|
||||||
|
const signInMutation = useMutation({
|
||||||
|
mutationKey: ["signIn"],
|
||||||
|
mutationFn: async (formData: FormData) => await signIn(formData)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify OTP Mutation
|
||||||
|
const verifyOtpMutation = useMutation({
|
||||||
|
mutationKey: ["verifyOtp"],
|
||||||
|
mutationFn: async (formData: FormData) => await verifyOtp(formData)
|
||||||
|
});
|
||||||
|
|
||||||
|
const signOutMutation = useMutation({
|
||||||
|
mutationKey: ["signOut"],
|
||||||
|
mutationFn: async () => await signOut()
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendMagicLinkMutation = useMutation({
|
||||||
|
mutationKey: ["sendMagicLink"],
|
||||||
|
mutationFn: async (formData: FormData) => await sendMagicLink(formData)
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendPasswordRecoveryMutation = useMutation({
|
||||||
|
mutationKey: ["sendPasswordRecovery"],
|
||||||
|
mutationFn: async (formData: FormData) => await sendPasswordRecovery(formData)
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
signIn: signInMutation,
|
||||||
|
verifyOtp: verifyOtpMutation,
|
||||||
|
signOut: signOutMutation,
|
||||||
|
sendMagicLink: sendMagicLinkMutation,
|
||||||
|
sendPasswordRecovery: sendPasswordRecoveryMutation
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,71 +1,71 @@
|
||||||
|
|
||||||
import { hasEnvVars } from "@/app/_utils/supabase/check-env-vars";
|
// import { hasEnvVars } from "@/app/_utils/supabase/check-env-vars";
|
||||||
import Link from "next/link";
|
// import Link from "next/link";
|
||||||
import { Badge } from "./ui/badge";
|
// import { Badge } from "./ui/badge";
|
||||||
import { Button } from "./ui/button";
|
// import { Button } from "./ui/button";
|
||||||
import { createClient } from "@/app/_utils/supabase/server";
|
// import { createClient } from "@/app/_utils/supabase/server";
|
||||||
import { signOutAction } from "@/app/(pages)/(auth)/_actions/sign-out";
|
// import { signOutAction } from "@/app/(pages)/(auth)/_actions/sign-out";
|
||||||
|
|
||||||
export default async function AuthButton() {
|
// export default async function AuthButton() {
|
||||||
const supabase = await createClient();
|
// const supabase = await createClient();
|
||||||
|
|
||||||
const {
|
// const {
|
||||||
data: { user },
|
// data: { user },
|
||||||
} = await supabase.auth.getUser();
|
// } = await supabase.auth.getUser();
|
||||||
|
|
||||||
if (!hasEnvVars) {
|
// if (!hasEnvVars) {
|
||||||
return (
|
// return (
|
||||||
<>
|
// <>
|
||||||
<div className="flex gap-4 items-center">
|
// <div className="flex gap-4 items-center">
|
||||||
<div>
|
// <div>
|
||||||
<Badge
|
// <Badge
|
||||||
variant={"default"}
|
// variant={"default"}
|
||||||
className="font-normal pointer-events-none"
|
// className="font-normal pointer-events-none"
|
||||||
>
|
// >
|
||||||
Please update .env.local file with anon key and url
|
// Please update .env.local file with anon key and url
|
||||||
</Badge>
|
// </Badge>
|
||||||
</div>
|
// </div>
|
||||||
<div className="flex gap-2">
|
// <div className="flex gap-2">
|
||||||
<Button
|
// <Button
|
||||||
asChild
|
// asChild
|
||||||
size="sm"
|
// size="sm"
|
||||||
variant={"outline"}
|
// variant={"outline"}
|
||||||
disabled
|
// disabled
|
||||||
className="opacity-75 cursor-none pointer-events-none"
|
// className="opacity-75 cursor-none pointer-events-none"
|
||||||
>
|
// >
|
||||||
<Link href="/sign-in">Sign in</Link>
|
// <Link href="/sign-in">Sign in</Link>
|
||||||
</Button>
|
// </Button>
|
||||||
<Button
|
// <Button
|
||||||
asChild
|
// asChild
|
||||||
size="sm"
|
// size="sm"
|
||||||
variant={"default"}
|
// variant={"default"}
|
||||||
disabled
|
// disabled
|
||||||
className="opacity-75 cursor-none pointer-events-none"
|
// className="opacity-75 cursor-none pointer-events-none"
|
||||||
>
|
// >
|
||||||
<Link href="/sign-up">Sign up</Link>
|
// <Link href="/sign-up">Sign up</Link>
|
||||||
</Button>
|
// </Button>
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
</>
|
// </>
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
return user ? (
|
// return user ? (
|
||||||
<div className="flex items-center gap-4">
|
// <div className="flex items-center gap-4">
|
||||||
Hey, {user.email}!
|
// Hey, {user.email}!
|
||||||
<form action={signOutAction}>
|
// <form action={signOutAction}>
|
||||||
<Button type="submit" variant={"outline"}>
|
// <Button type="submit" variant={"outline"}>
|
||||||
Sign out
|
// Sign out
|
||||||
</Button>
|
// </Button>
|
||||||
</form>
|
// </form>
|
||||||
</div>
|
// </div>
|
||||||
) : (
|
// ) : (
|
||||||
<div className="flex gap-2">
|
// <div className="flex gap-2">
|
||||||
<Button asChild size="sm" variant={"outline"}>
|
// <Button asChild size="sm" variant={"outline"}>
|
||||||
<Link href="/sign-in">Sign in</Link>
|
// <Link href="/sign-in">Sign in</Link>
|
||||||
</Button>
|
// </Button>
|
||||||
<Button asChild size="sm" variant={"default"}>
|
// <Button asChild size="sm" variant={"default"}>
|
||||||
<Link href="/sign-up">Sign up</Link>
|
// <Link href="/sign-up">Sign up</Link>
|
||||||
</Button>
|
// </Button>
|
||||||
</div>
|
// </div>
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { Input, InputProps } from "@/app/_components/ui/input"
|
||||||
|
import { LucideIcon } from "lucide-react"
|
||||||
|
import { FieldError, UseFormRegisterReturn } from "react-hook-form"
|
||||||
|
|
||||||
|
interface FormFieldProps extends Omit<InputProps, 'error'> {
|
||||||
|
id?: string
|
||||||
|
label: string
|
||||||
|
icon?: LucideIcon
|
||||||
|
error?: FieldError
|
||||||
|
registration: UseFormRegisterReturn
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReactHookFormField({
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
icon: Icon,
|
||||||
|
error,
|
||||||
|
registration,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: FormFieldProps) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor={id} className="text-sm text-zinc-400">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<div className="relative space-y-2">
|
||||||
|
{Icon && <Icon className="absolute left-3 top-2.5 h-5 w-5 text-zinc-500" />}
|
||||||
|
<Input
|
||||||
|
id={id}
|
||||||
|
className={`${Icon ? 'pl-10' : ''} placeholder:text-zinc-500 ${className || ''}`}
|
||||||
|
error={!!error}
|
||||||
|
{...registration}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{error && <p className="text-sm text-red-500">{error.message}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
error && "ring-2 ring-red-500 border-red-500 focus-visible:ring-red-500",
|
error && "ring-2 ring-red-500 focus-visible:ring-red-500",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
|
@ -25,4 +25,8 @@ export class CNumbers {
|
||||||
static readonly MAX_UPLOAD_SIZE_MB = 50;
|
static readonly MAX_UPLOAD_SIZE_MB = 50;
|
||||||
static readonly MIN_PASSWORD_LENGTH = 8;
|
static readonly MIN_PASSWORD_LENGTH = 8;
|
||||||
static readonly MAX_PASSWORD_LENGTH = 128;
|
static readonly MAX_PASSWORD_LENGTH = 128;
|
||||||
|
|
||||||
|
// Phone number
|
||||||
|
static readonly PHONE_MIN_LENGTH = 10;
|
||||||
|
static readonly PHONE_MAX_LENGTH = 13;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export class CRegex {
|
||||||
|
static readonly PHONE_REGEX = /^(\+62|62|0)[8][1-9][0-9]{6,11}$/;
|
||||||
|
static readonly BAN_DURATION_REGEX = /^(\d+(ns|us|µs|ms|s|m|h)|none)$/;
|
||||||
|
}
|
|
@ -84,4 +84,8 @@ export class CTexts {
|
||||||
static readonly SENSITIVE_WORDS = [
|
static readonly SENSITIVE_WORDS = [
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Phone number
|
||||||
|
static readonly PHONE_PREFIX = ['+62', '62', '0']
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
/**
|
||||||
|
* Definisikan tipe untuk satuan waktu yang valid
|
||||||
|
* @description Satuan waktu yang valid: "ns", "us", "µs", "ms", "s", "m", "h"
|
||||||
|
*/
|
||||||
|
export type TimeUnit = "ns" | "us" | "µs" | "ms" | "s" | "m" | "h";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buat tipe untuk durasi yang valid (1ns, 2ms, 10h, dll.)
|
||||||
|
* @description Format durasi yang valid: `${number}${TimeUnit}` atau "none"
|
||||||
|
*/
|
||||||
|
export type ValidBanDuration = `${number}${TimeUnit}` | "none";
|
|
@ -4,88 +4,125 @@
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
/* Latar belakang terang: putih bersih */
|
||||||
--foreground: 240 10% 3.9%;
|
--background: 0 0% 100%; /* #ffffff */
|
||||||
--card: 0 0% 100%;
|
--foreground: 0 0% 10%; /* #1a1a1a, teks hitam pekat */
|
||||||
--card-foreground: 240 10% 3.9%;
|
|
||||||
--popover: 0 0% 100%;
|
/* Kartu: sama dengan latar belakang di mode terang */
|
||||||
--popover-foreground: 240 10% 3.9%;
|
--card: 0 0% 100%; /* #ffffff */
|
||||||
--primary: 153 60% 53%; /* Supabase green */
|
--card-foreground: 0 0% 10%; /* #1a1a1a */
|
||||||
--primary-foreground: 0 0% 98%;
|
|
||||||
--secondary: 240 4.8% 95.9%;
|
/* Popover: sama dengan latar belakang */
|
||||||
--secondary-foreground: 240 5.9% 10%;
|
--popover: 0 0% 100%; /* #ffffff */
|
||||||
--muted: 240 4.8% 95.9%;
|
--popover-foreground: 0 0% 10%; /* #1a1a1a */
|
||||||
--muted-foreground: 240 3.8% 46.1%;
|
|
||||||
--accent: 240 4.8% 95.9%;
|
/* Warna utama: hijau Supabase #006239 */
|
||||||
--accent-foreground: 240 5.9% 10%;
|
--primary: 155% 100% 19%; /* #006239 */
|
||||||
--destructive: 0 84.2% 60.2%;
|
--primary-foreground: 0 0% 100%; /* #ffffff untuk kontras pada hijau */
|
||||||
--destructive-foreground: 0 0% 98%;
|
|
||||||
--border: 240 5.9% 90%;
|
/* Sekunder: abu-abu terang untuk elemen pendukung */
|
||||||
--input: 240 5.9% 90%;
|
--secondary: 0 0% 96%; /* #f5f5f5 */
|
||||||
--ring: 153 60% 53%; /* Matching primary */
|
--secondary-foreground: 0 0% 10%; /* #1a1a1a */
|
||||||
|
|
||||||
|
/* Muted: abu-abu untuk teks pendukung */
|
||||||
|
--muted: 0 0% 85%; /* #d9d9d9 */
|
||||||
|
--muted-foreground: 0 0% 40%; /* #666666 */
|
||||||
|
|
||||||
|
/* Aksen: sama dengan sekunder */
|
||||||
|
--accent: 0 0% 96%; /* #f5f5f5 */
|
||||||
|
--accent-foreground: 0 0% 10%; /* #1a1a1a */
|
||||||
|
|
||||||
|
/* Destructive: merah untuk error */
|
||||||
|
--destructive: 0 85% 60%; /* #f44336 */
|
||||||
|
--destructive-foreground: 0 0% 100%; /* #ffffff */
|
||||||
|
|
||||||
|
/* Border dan input: abu-abu netral */
|
||||||
|
--border: 0 0% 80%; /* #cccccc */
|
||||||
|
--input: 0 0% 80%; /* #cccccc */
|
||||||
|
|
||||||
|
/* Ring: sama dengan primary untuk fokus */
|
||||||
|
--ring: 155% 100% 19%; /* #006239 */
|
||||||
|
|
||||||
|
/* Radius: sudut membulat ringan */
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
--chart-1: 153 60% 53%; /* Supabase green */
|
|
||||||
--chart-2: 183 65% 50%;
|
/* Chart: gunakan hijau Supabase dan variasi */
|
||||||
--chart-3: 213 70% 47%;
|
--chart-1: 155% 100% 19%; /* #006239 */
|
||||||
--chart-4: 243 75% 44%;
|
--chart-2: 160 60% 45%; /* sedikit lebih gelap */
|
||||||
--chart-5: 273 80% 41%;
|
--chart-3: 165 55% 40%;
|
||||||
--sidebar-background: 0 0% 98%;
|
--chart-4: 170 50% 35%;
|
||||||
--sidebar-foreground: 240 5.3% 26.1%;
|
--chart-5: 175 45% 30%;
|
||||||
--sidebar-primary: 240 5.9% 10%;
|
|
||||||
--sidebar-primary-foreground: 0 0% 98%;
|
/* Sidebar: mirip dengan kartu di mode terang */
|
||||||
--sidebar-accent: 240 4.8% 95.9%;
|
--sidebar-background: 0 0% 98%; /* #fafafa */
|
||||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
--sidebar-foreground: 0 0% 10%; /* #1a1a1a */
|
||||||
--sidebar-border: 220 13% 91%;
|
--sidebar-primary: 155% 100% 19%; /* #006239 */
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
--sidebar-primary-foreground: 0 0% 100%; /* #ffffff */
|
||||||
|
--sidebar-accent: 0 0% 96%; /* #f5f5f5 */
|
||||||
|
--sidebar-accent-foreground: 0 0% 10%; /* #1a1a1a */
|
||||||
|
--sidebar-border: 0 0% 85%; /* #d9d9d9 */
|
||||||
|
--sidebar-ring: 155% 100% 19%; /* #006239 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 0 0% 9%; /* #171717 */
|
/* Latar belakang gelap: abu-abu tua mendekati hitam */
|
||||||
--foreground: 210 20% 98%;
|
--background: 0 0% 10%; /* #1a1a1a */
|
||||||
--card: 0 0% 9%; /* #171717 */
|
--foreground: 0 0% 85%; /* #d9d9d9, teks abu-abu terang */
|
||||||
--card-foreground: 210 20% 98%;
|
|
||||||
--popover: 0 0% 9%; /* #171717 */
|
/* Kartu: sama dengan latar belakang di mode gelap */
|
||||||
--popover-foreground: 210 20% 98%;
|
--card: 0 0% 10%; /* #1a1a1a */
|
||||||
--primary: 153 60% 53%; /* Supabase green */
|
--card-foreground: 0 0% 85%; /* #d9d9d9 */
|
||||||
--primary-foreground: 210 20% 98%;
|
|
||||||
--secondary: 220 8% 15%;
|
/* Popover: sama dengan latar belakang */
|
||||||
--secondary-foreground: 210 20% 98%;
|
--popover: 0 0% 10%; /* #1a1a1a */
|
||||||
--muted: 220 8% 15%;
|
--popover-foreground: 0 0% 85%; /* #d9d9d9 */
|
||||||
--muted-foreground: 217 10% 64%;
|
|
||||||
--accent: 220 8% 15%;
|
/* Warna utama: hijau Supabase tetap digunakan */
|
||||||
--accent-foreground: 210 20% 98%;
|
--primary: 155% 100% 19%; /* #006239 */
|
||||||
--destructive: 0 62.8% 30.6%;
|
--primary-foreground: 0 0% 100%; /* #ffffff */
|
||||||
--destructive-foreground: 210 20% 98%;
|
|
||||||
--border: 220 8% 15%;
|
/* Sekunder: abu-abu gelap untuk elemen pendukung */
|
||||||
--input: 220 8% 15%;
|
--secondary: 0 0% 15%; /* #262626 */
|
||||||
--ring: 153 60% 53%; /* Matching primary */
|
--secondary-foreground: 0 0% 85%; /* #d9d9d9 */
|
||||||
--chart-1: 153 60% 53%; /* Supabase green */
|
|
||||||
--chart-2: 183 65% 50%;
|
/* Muted: abu-abu gelap untuk teks pendukung */
|
||||||
--chart-3: 213 70% 47%;
|
--muted: 0 0% 20%; /* #333333 */
|
||||||
--chart-4: 243 75% 44%;
|
--muted-foreground: 0 0% 60%; /* #999999 */
|
||||||
--chart-5: 273 80% 41%;
|
|
||||||
--sidebar-background: 240 5.9% 10%;
|
/* Aksen: sama dengan sekunder */
|
||||||
--sidebar-foreground: 240 4.8% 95.9%;
|
--accent: 0 0% 15%; /* #262626 */
|
||||||
--sidebar-primary: 224.3 76.3% 48%;
|
--accent-foreground: 0 0% 85%; /* #d9d9d9 */
|
||||||
--sidebar-primary-foreground: 0 0% 100%;
|
|
||||||
--sidebar-accent: 240 3.7% 15.9%;
|
/* Destructive: merah gelap untuk error */
|
||||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
--destructive: 0 62% 30%; /* #802626 */
|
||||||
--sidebar-border: 240 3.7% 15.9%;
|
--destructive-foreground: 0 0% 100%; /* #ffffff */
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
|
||||||
|
/* Border dan input: abu-abu gelap */
|
||||||
|
--border: 0 0% 20%; /* #333333 */
|
||||||
|
--input: 0 0% 20%; /* #333333 */
|
||||||
|
|
||||||
|
/* Ring: sama dengan primary */
|
||||||
|
--ring: 155% 100% 19%; /* #006239 */
|
||||||
|
|
||||||
|
/* Chart: sama seperti mode terang */
|
||||||
|
--chart-1: 155% 100% 19%; /* #006239 */
|
||||||
|
--chart-2: 160 60% 45%;
|
||||||
|
--chart-3: 165 55% 40%;
|
||||||
|
--chart-4: 170 50% 35%;
|
||||||
|
--chart-5: 175 45% 30%;
|
||||||
|
|
||||||
|
/* Sidebar: abu-abu gelap */
|
||||||
|
--sidebar-background: 0 0% 15%; /* #262626 */
|
||||||
|
--sidebar-foreground: 0 0% 85%; /* #d9d9d9 */
|
||||||
|
--sidebar-primary: 155% 100% 19%; /* #006239 */
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%; /* #ffffff */
|
||||||
|
--sidebar-accent: 0 0% 20%; /* #333333 */
|
||||||
|
--sidebar-accent-foreground: 0 0% 85%; /* #d9d9d9 */
|
||||||
|
--sidebar-border: 0 0% 25%; /* #404040 */
|
||||||
|
--sidebar-ring: 155% 100% 19%; /* #006239 */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
|
||||||
* {
|
|
||||||
@apply border-border;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
@apply bg-background text-foreground;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
|
@ -93,4 +130,4 @@
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -49,3 +49,25 @@ export function createFormData(): FormData {
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a unique username based on the provided email address.
|
||||||
|
*
|
||||||
|
* The username is created by combining the local part of the email (before the '@' symbol)
|
||||||
|
* with a randomly generated alphanumeric suffix.
|
||||||
|
*
|
||||||
|
* @param email - The email address to generate the username from.
|
||||||
|
* @returns A string representing the generated username.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const username = generateUsername("example@gmail.com");
|
||||||
|
* console.log(username); // Output: "example.abc123" (random suffix will vary)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function generateUsername(email: string): string {
|
||||||
|
const [localPart] = email.split("@");
|
||||||
|
const randomSuffix = Math.random().toString(36).substring(2, 8); // Generate a random alphanumeric string
|
||||||
|
return `${localPart}.${randomSuffix}`;
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { CTexts } from "../_lib/const/string";
|
||||||
|
import { CRegex } from "../_lib/const/regex";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates if a given phone number starts with any of the predefined prefixes.
|
||||||
|
*
|
||||||
|
* @param number - The phone number to validate.
|
||||||
|
* @returns A boolean indicating whether the phone number starts with a valid prefix.
|
||||||
|
*/
|
||||||
|
export const phonePrefixValidation = (number: string) => CTexts.PHONE_PREFIX.some(prefix => number.startsWith(prefix));
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates if a given phone number matches the predefined regex pattern.
|
||||||
|
*
|
||||||
|
* @param number - The phone number to validate.
|
||||||
|
* @returns A boolean indicating whether the phone number matches the regex pattern.
|
||||||
|
*/
|
||||||
|
export const phoneRegexValidation = (number: string) => CRegex.PHONE_REGEX.test(number);
|
|
@ -11,6 +11,10 @@ import { signInController } from '@/src/interface-adapters/controllers/auth/sign
|
||||||
import { signOutController } from '@/src/interface-adapters/controllers/auth/sign-out.controller';
|
import { signOutController } from '@/src/interface-adapters/controllers/auth/sign-out.controller';
|
||||||
import { verifyOtpUseCase } from '@/src/application/use-cases/auth/verify-otp.use-case';
|
import { verifyOtpUseCase } from '@/src/application/use-cases/auth/verify-otp.use-case';
|
||||||
import { verifyOtpController } from '@/src/interface-adapters/controllers/auth/verify-otp.controller';
|
import { verifyOtpController } from '@/src/interface-adapters/controllers/auth/verify-otp.controller';
|
||||||
|
import { sendMagicLinkUseCase } from '@/src/application/use-cases/auth/send-magic-link.use-case';
|
||||||
|
import { sendPasswordRecoveryUseCase } from '@/src/application/use-cases/auth/send-password-recovery.use-case';
|
||||||
|
import { sendMagicLinkController } from '@/src/interface-adapters/controllers/auth/send-magic-link.controller';
|
||||||
|
import { sendPasswordRecoveryController } from '@/src/interface-adapters/controllers/auth/send-password-recovery.controller';
|
||||||
|
|
||||||
export function createAuthenticationModule() {
|
export function createAuthenticationModule() {
|
||||||
const authenticationModule = createModule();
|
const authenticationModule = createModule();
|
||||||
|
@ -66,6 +70,22 @@ export function createAuthenticationModule() {
|
||||||
DI_SYMBOLS.IAuthenticationService,
|
DI_SYMBOLS.IAuthenticationService,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
authenticationModule
|
||||||
|
.bind(DI_SYMBOLS.ISendMagicLinkUseCase)
|
||||||
|
.toHigherOrderFunction(sendMagicLinkUseCase, [
|
||||||
|
DI_SYMBOLS.IInstrumentationService,
|
||||||
|
DI_SYMBOLS.IAuthenticationService,
|
||||||
|
DI_SYMBOLS.IUsersRepository,
|
||||||
|
]);
|
||||||
|
|
||||||
|
authenticationModule
|
||||||
|
.bind(DI_SYMBOLS.ISendPasswordRecoveryUseCase)
|
||||||
|
.toHigherOrderFunction(sendPasswordRecoveryUseCase, [
|
||||||
|
DI_SYMBOLS.IInstrumentationService,
|
||||||
|
DI_SYMBOLS.IAuthenticationService,
|
||||||
|
DI_SYMBOLS.IUsersRepository,
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
// Controllers
|
// Controllers
|
||||||
authenticationModule
|
authenticationModule
|
||||||
|
@ -90,6 +110,20 @@ export function createAuthenticationModule() {
|
||||||
DI_SYMBOLS.ISignOutUseCase,
|
DI_SYMBOLS.ISignOutUseCase,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
authenticationModule
|
||||||
|
.bind(DI_SYMBOLS.ISendMagicLinkController)
|
||||||
|
.toHigherOrderFunction(sendMagicLinkController, [
|
||||||
|
DI_SYMBOLS.IInstrumentationService,
|
||||||
|
DI_SYMBOLS.ISendMagicLinkUseCase,
|
||||||
|
]);
|
||||||
|
|
||||||
|
authenticationModule
|
||||||
|
.bind(DI_SYMBOLS.ISendPasswordRecoveryController)
|
||||||
|
.toHigherOrderFunction(sendPasswordRecoveryController, [
|
||||||
|
DI_SYMBOLS.IInstrumentationService,
|
||||||
|
DI_SYMBOLS.ISendPasswordRecoveryUseCase,
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
return authenticationModule;
|
return authenticationModule;
|
||||||
}
|
}
|
|
@ -1,9 +1,7 @@
|
||||||
import { createModule } from '@evyweb/ioctopus';
|
import { createModule } from '@evyweb/ioctopus';
|
||||||
|
|
||||||
|
|
||||||
import { DI_SYMBOLS } from '@/di/types';
|
import { DI_SYMBOLS } from '@/di/types';
|
||||||
import { UsersRepository } from '@/src/infrastructure/repositories/users.repository';
|
import { UsersRepository } from '@/src/infrastructure/repositories/users.repository';
|
||||||
import { getUsersUseCase } from '@/src/application/use-cases/users/get-users.use-case';
|
|
||||||
import { getUsersController } from '@/src/interface-adapters/controllers/users/get-users.controller';
|
import { getUsersController } from '@/src/interface-adapters/controllers/users/get-users.controller';
|
||||||
import { banUserController } from '@/src/interface-adapters/controllers/users/ban-user.controller';
|
import { banUserController } from '@/src/interface-adapters/controllers/users/ban-user.controller';
|
||||||
import { banUserUseCase } from '@/src/application/use-cases/users/ban-user.use-case';
|
import { banUserUseCase } from '@/src/application/use-cases/users/ban-user.use-case';
|
||||||
|
@ -25,6 +23,7 @@ import { deleteUserUseCase } from '@/src/application/use-cases/users/delete-user
|
||||||
import { getUserByUsernameUseCase } from '@/src/application/use-cases/users/get-user-by-username.use-case';
|
import { getUserByUsernameUseCase } from '@/src/application/use-cases/users/get-user-by-username.use-case';
|
||||||
import { getUserByEmailUseCase } from '@/src/application/use-cases/users/get-user-by-email.use-case';
|
import { getUserByEmailUseCase } from '@/src/application/use-cases/users/get-user-by-email.use-case';
|
||||||
import { updateUserUseCase } from '@/src/application/use-cases/users/update-user.use-case';
|
import { updateUserUseCase } from '@/src/application/use-cases/users/update-user.use-case';
|
||||||
|
import { getUsersUseCase } from '@/src/application/use-cases/users/get-users.use-case';
|
||||||
|
|
||||||
|
|
||||||
export function createUsersModule() {
|
export function createUsersModule() {
|
||||||
|
@ -169,7 +168,7 @@ export function createUsersModule() {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
usersModule
|
usersModule
|
||||||
.bind(DI_SYMBOLS.IGetUserByUserNameController)
|
.bind(DI_SYMBOLS.IGetUserByUsernameController)
|
||||||
.toHigherOrderFunction(getUserByUsernameController, [
|
.toHigherOrderFunction(getUserByUsernameController, [
|
||||||
DI_SYMBOLS.IInstrumentationService,
|
DI_SYMBOLS.IInstrumentationService,
|
||||||
DI_SYMBOLS.IGetUserByUserNameUseCase
|
DI_SYMBOLS.IGetUserByUserNameUseCase
|
||||||
|
@ -179,14 +178,16 @@ export function createUsersModule() {
|
||||||
.bind(DI_SYMBOLS.IInviteUserController)
|
.bind(DI_SYMBOLS.IInviteUserController)
|
||||||
.toHigherOrderFunction(inviteUserController, [
|
.toHigherOrderFunction(inviteUserController, [
|
||||||
DI_SYMBOLS.IInstrumentationService,
|
DI_SYMBOLS.IInstrumentationService,
|
||||||
DI_SYMBOLS.IInviteUserUseCase
|
DI_SYMBOLS.IInviteUserUseCase,
|
||||||
|
DI_SYMBOLS.IGetCurrentUserUseCase
|
||||||
]);
|
]);
|
||||||
|
|
||||||
usersModule
|
usersModule
|
||||||
.bind(DI_SYMBOLS.ICreateUserController)
|
.bind(DI_SYMBOLS.ICreateUserController)
|
||||||
.toHigherOrderFunction(createUserController, [
|
.toHigherOrderFunction(createUserController, [
|
||||||
DI_SYMBOLS.IInstrumentationService,
|
DI_SYMBOLS.IInstrumentationService,
|
||||||
DI_SYMBOLS.ICreateUserUseCase
|
DI_SYMBOLS.ICreateUserUseCase,
|
||||||
|
DI_SYMBOLS.IGetCurrentUserUseCase
|
||||||
]);
|
]);
|
||||||
|
|
||||||
usersModule
|
usersModule
|
||||||
|
|
|
@ -32,6 +32,11 @@ import { IInviteUserController } from '@/src/interface-adapters/controllers/user
|
||||||
import { IUpdateUserController } from '@/src/interface-adapters/controllers/users/update-user-controller';
|
import { IUpdateUserController } from '@/src/interface-adapters/controllers/users/update-user-controller';
|
||||||
import { ICreateUserController } from '@/src/interface-adapters/controllers/users/create-user.controller';
|
import { ICreateUserController } from '@/src/interface-adapters/controllers/users/create-user.controller';
|
||||||
import { IDeleteUserController } from '@/src/interface-adapters/controllers/users/delete-user.controller';
|
import { IDeleteUserController } from '@/src/interface-adapters/controllers/users/delete-user.controller';
|
||||||
|
import { IGetUsersController } from '@/src/interface-adapters/controllers/users/get-users.controller';
|
||||||
|
import { ISendMagicLinkUseCase } from '@/src/application/use-cases/auth/send-magic-link.use-case';
|
||||||
|
import { ISendPasswordRecoveryUseCase } from '@/src/application/use-cases/auth/send-password-recovery.use-case';
|
||||||
|
import { ISendMagicLinkController } from '@/src/interface-adapters/controllers/auth/send-magic-link.controller';
|
||||||
|
import { ISendPasswordRecoveryController } from '@/src/interface-adapters/controllers/auth/send-password-recovery.controller';
|
||||||
|
|
||||||
export const DI_SYMBOLS = {
|
export const DI_SYMBOLS = {
|
||||||
// Services
|
// Services
|
||||||
|
@ -48,6 +53,8 @@ export const DI_SYMBOLS = {
|
||||||
ISignUpUseCase: Symbol.for('ISignUpUseCase'),
|
ISignUpUseCase: Symbol.for('ISignUpUseCase'),
|
||||||
IVerifyOtpUseCase: Symbol.for('IVerifyOtpUseCase'),
|
IVerifyOtpUseCase: Symbol.for('IVerifyOtpUseCase'),
|
||||||
ISignOutUseCase: Symbol.for('ISignOutUseCase'),
|
ISignOutUseCase: Symbol.for('ISignOutUseCase'),
|
||||||
|
ISendMagicLinkUseCase: Symbol.for('ISendMagicLinkUseCase'),
|
||||||
|
ISendPasswordRecoveryUseCase: Symbol.for('ISendPasswordRecoveryUseCase'),
|
||||||
|
|
||||||
IBanUserUseCase: Symbol.for('IBanUserUseCase'),
|
IBanUserUseCase: Symbol.for('IBanUserUseCase'),
|
||||||
IUnbanUserUseCase: Symbol.for('IUnbanUserUseCase'),
|
IUnbanUserUseCase: Symbol.for('IUnbanUserUseCase'),
|
||||||
|
@ -65,6 +72,8 @@ export const DI_SYMBOLS = {
|
||||||
ISignInController: Symbol.for('ISignInController'),
|
ISignInController: Symbol.for('ISignInController'),
|
||||||
ISignOutController: Symbol.for('ISignOutController'),
|
ISignOutController: Symbol.for('ISignOutController'),
|
||||||
IVerifyOtpController: Symbol.for('IVerifyOtpController'),
|
IVerifyOtpController: Symbol.for('IVerifyOtpController'),
|
||||||
|
ISendMagicLinkController: Symbol.for('ISendMagicLinkController'),
|
||||||
|
ISendPasswordRecoveryController: Symbol.for('ISendPasswordRecoveryController'),
|
||||||
|
|
||||||
IBanUserController: Symbol.for('IBanUserController'),
|
IBanUserController: Symbol.for('IBanUserController'),
|
||||||
IUnbanUserController: Symbol.for('IUnbanUserController'),
|
IUnbanUserController: Symbol.for('IUnbanUserController'),
|
||||||
|
@ -72,7 +81,7 @@ export const DI_SYMBOLS = {
|
||||||
IGetUsersController: Symbol.for('IGetUsersController'),
|
IGetUsersController: Symbol.for('IGetUsersController'),
|
||||||
IGetUserByIdController: Symbol.for('IGetUserByIdController'),
|
IGetUserByIdController: Symbol.for('IGetUserByIdController'),
|
||||||
IGetUserByEmailController: Symbol.for('IGetUserByEmailController'),
|
IGetUserByEmailController: Symbol.for('IGetUserByEmailController'),
|
||||||
IGetUserByUserNameController: Symbol.for('IGetUserByUserNameController'),
|
IGetUserByUsernameController: Symbol.for('IGetUserByUsernameController'),
|
||||||
IInviteUserController: Symbol.for('IInviteUserController'),
|
IInviteUserController: Symbol.for('IInviteUserController'),
|
||||||
ICreateUserController: Symbol.for('ICreateUserController'),
|
ICreateUserController: Symbol.for('ICreateUserController'),
|
||||||
IUpdateUserController: Symbol.for('IUpdateUserController'),
|
IUpdateUserController: Symbol.for('IUpdateUserController'),
|
||||||
|
@ -94,6 +103,8 @@ export interface DI_RETURN_TYPES {
|
||||||
ISignUpUseCase: ISignUpUseCase;
|
ISignUpUseCase: ISignUpUseCase;
|
||||||
IVerifyOtpUseCase: IVerifyOtpUseCase;
|
IVerifyOtpUseCase: IVerifyOtpUseCase;
|
||||||
ISignOutUseCase: ISignOutUseCase;
|
ISignOutUseCase: ISignOutUseCase;
|
||||||
|
ISendMagicLinkUseCase: ISendMagicLinkUseCase;
|
||||||
|
ISendPasswordRecoveryUseCase: ISendPasswordRecoveryUseCase;
|
||||||
|
|
||||||
IBanUserUseCase: IBanUserUseCase;
|
IBanUserUseCase: IBanUserUseCase;
|
||||||
IUnbanUserUseCase: IUnbanUserUseCase;
|
IUnbanUserUseCase: IUnbanUserUseCase;
|
||||||
|
@ -111,14 +122,16 @@ export interface DI_RETURN_TYPES {
|
||||||
ISignInController: ISignInController;
|
ISignInController: ISignInController;
|
||||||
IVerifyOtpController: IVerifyOtpController;
|
IVerifyOtpController: IVerifyOtpController;
|
||||||
ISignOutController: ISignOutController;
|
ISignOutController: ISignOutController;
|
||||||
|
ISendMagicLinkController: ISendMagicLinkController;
|
||||||
|
ISendPasswordRecoveryController: ISendPasswordRecoveryController;
|
||||||
|
|
||||||
IBanUserController: IBanUserController;
|
IBanUserController: IBanUserController;
|
||||||
IUnbanUserController: IUnbanUserController;
|
IUnbanUserController: IUnbanUserController;
|
||||||
IGetCurrentUserController: IGetCurrentUserController;
|
IGetCurrentUserController: IGetCurrentUserController;
|
||||||
IGetUsersController: IGetUserByUsernameController;
|
IGetUsersController: IGetUsersController;
|
||||||
IGetUserByIdController: IGetUserByIdController;
|
IGetUserByIdController: IGetUserByIdController;
|
||||||
IGetUserByEmailController: IGetUserByEmailController;
|
IGetUserByEmailController: IGetUserByEmailController;
|
||||||
IGetUserByUserNameController: IGetUserByUsernameController;
|
IGetUserByUsernameController: IGetUserByUsernameController;
|
||||||
IInviteUserController: IInviteUserController;
|
IInviteUserController: IInviteUserController;
|
||||||
ICreateUserController: ICreateUserController;
|
ICreateUserController: ICreateUserController;
|
||||||
IUpdateUserController: IUpdateUserController;
|
IUpdateUserController: IUpdateUserController;
|
||||||
|
|
|
@ -2,24 +2,24 @@ import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
const prismaClientSingleton = () => {
|
const prismaClientSingleton = () => {
|
||||||
return new PrismaClient({
|
return new PrismaClient({
|
||||||
log: [
|
// log: [
|
||||||
{
|
// {
|
||||||
emit: 'event',
|
// emit: 'event',
|
||||||
level: 'query',
|
// level: 'query',
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
emit: 'stdout',
|
// emit: 'stdout',
|
||||||
level: 'error',
|
// level: 'error',
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
emit: 'stdout',
|
// emit: 'stdout',
|
||||||
level: 'info',
|
// level: 'info',
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
emit: 'stdout',
|
// emit: 'stdout',
|
||||||
level: 'warn',
|
// level: 'warn',
|
||||||
},
|
// },
|
||||||
],
|
// ],
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -33,8 +33,8 @@ export default db;
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = db;
|
if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = db;
|
||||||
|
|
||||||
db.$on('query', (e) => {
|
// db.$on('query', (e) => {
|
||||||
console.log('Query: ' + e.query)
|
// console.log('Query: ' + e.query)
|
||||||
console.log('Params: ' + e.params)
|
// console.log('Params: ' + e.params)
|
||||||
console.log('Duration: ' + e.duration + 'ms')
|
// console.log('Duration: ' + e.duration + 'ms')
|
||||||
})
|
// })
|
|
@ -1,173 +0,0 @@
|
||||||
"use server";
|
|
||||||
|
|
||||||
import { createClient as createServerClient } from "@/app/_utils/supabase/server";
|
|
||||||
import { SignInFormData } from "@/src/entities/models/auth/sign-in.model";
|
|
||||||
import { VerifyOtpFormData } from "@/src/entities/models/auth/verify-otp.model";
|
|
||||||
import { AuthenticationError } from "@/src/entities/errors/auth";
|
|
||||||
import { DatabaseOperationError } from "@/src/entities/errors/common";
|
|
||||||
import { createAdminClient } from "@/app/_utils/supabase/admin";
|
|
||||||
import { IInstrumentationServiceImpl } from "@/src/application/services/instrumentation.service.interface";
|
|
||||||
import { ICrashReporterServiceImpl } from "@/src/application/services/crash-reporter.service.interface";
|
|
||||||
|
|
||||||
let supabaseAdmin = createAdminClient();
|
|
||||||
let supabaseServer = createServerClient();
|
|
||||||
|
|
||||||
// Server actions for authentication
|
|
||||||
export async function signIn({ email }: SignInFormData) {
|
|
||||||
return await IInstrumentationServiceImpl.instrumentServerAction(
|
|
||||||
"auth.signIn",
|
|
||||||
{ email },
|
|
||||||
async () => {
|
|
||||||
try {
|
|
||||||
const supabase = await supabaseServer;
|
|
||||||
const { data, error } = await supabase.auth.signInWithOtp({
|
|
||||||
email,
|
|
||||||
options: {
|
|
||||||
shouldCreateUser: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error("Error signing in:", error);
|
|
||||||
throw new AuthenticationError(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: "Sign in email sent successfully",
|
|
||||||
data,
|
|
||||||
redirectTo: `/verify-otp?email=${encodeURIComponent(email)}`,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
ICrashReporterServiceImpl.report(err);
|
|
||||||
if (err instanceof AuthenticationError) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
throw new AuthenticationError("Failed to sign in. Please try again.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function verifyOtp({ email, token }: VerifyOtpFormData) {
|
|
||||||
return await IInstrumentationServiceImpl.instrumentServerAction(
|
|
||||||
"auth.verifyOtp",
|
|
||||||
{ email },
|
|
||||||
async () => {
|
|
||||||
try {
|
|
||||||
const supabase = await supabaseServer;
|
|
||||||
const { data, error } = await supabase.auth.verifyOtp({
|
|
||||||
email,
|
|
||||||
token,
|
|
||||||
type: "email",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error("Error verifying OTP:", error);
|
|
||||||
throw new AuthenticationError(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: "Successfully verified!",
|
|
||||||
data,
|
|
||||||
redirectTo: "/dashboard",
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
ICrashReporterServiceImpl.report(err);
|
|
||||||
if (err instanceof AuthenticationError) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
throw new AuthenticationError("Failed to verify OTP. Please try again.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function signOut() {
|
|
||||||
return await IInstrumentationServiceImpl.instrumentServerAction(
|
|
||||||
"auth.signOut",
|
|
||||||
{},
|
|
||||||
async () => {
|
|
||||||
try {
|
|
||||||
const supabase = await supabaseServer;
|
|
||||||
const { error } = await supabase.auth.signOut();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error("Error signing out:", error);
|
|
||||||
throw new AuthenticationError(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: "Sign out successful",
|
|
||||||
redirectTo: "/",
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
ICrashReporterServiceImpl.report(err);
|
|
||||||
throw new AuthenticationError("Failed to sign out. Please try again.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function sendPasswordRecovery(email: string) {
|
|
||||||
return await IInstrumentationServiceImpl.instrumentServerAction(
|
|
||||||
"auth.sendPasswordRecovery",
|
|
||||||
{ email },
|
|
||||||
async () => {
|
|
||||||
try {
|
|
||||||
const supabase = createAdminClient();
|
|
||||||
|
|
||||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
|
||||||
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/reset-password`,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error("Error sending password recovery:", error);
|
|
||||||
throw new DatabaseOperationError(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: "Password recovery email sent successfully",
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
ICrashReporterServiceImpl.report(err);
|
|
||||||
throw new DatabaseOperationError("Failed to send password recovery email. Please try again.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function sendMagicLink(email: string) {
|
|
||||||
return await IInstrumentationServiceImpl.instrumentServerAction(
|
|
||||||
"auth.sendMagicLink",
|
|
||||||
{ email },
|
|
||||||
async () => {
|
|
||||||
try {
|
|
||||||
const supabase = createAdminClient();
|
|
||||||
|
|
||||||
const { error } = await supabase.auth.signInWithOtp({
|
|
||||||
email,
|
|
||||||
options: {
|
|
||||||
emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error("Error sending magic link:", error);
|
|
||||||
throw new DatabaseOperationError(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: "Magic link email sent successfully",
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
ICrashReporterServiceImpl.report(err);
|
|
||||||
throw new DatabaseOperationError("Failed to send magic link email. Please try again.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,740 +1,25 @@
|
||||||
import { createAdminClient } from "@/app/_utils/supabase/admin";
|
import { createAdminClient } from "@/app/_utils/supabase/admin";
|
||||||
import { createClient } from "@/app/_utils/supabase/client";
|
import { createClient } from "@/app/_utils/supabase/client";
|
||||||
import { CreateUser, InviteUser, UpdateUser, User, UserResponse } from "@/src/entities/models/users/users.model";
|
import { IUserSchema, UserResponse } from "@/src/entities/models/users/users.model";
|
||||||
import { ITransaction } from "@/src/entities/models/transaction.interface";
|
import { ITransaction } from "@/src/entities/models/transaction.interface";
|
||||||
|
import { ICreateUserSchema } from "@/src/entities/models/users/create-user.model";
|
||||||
|
import { ICredentialUpdateUserSchema, IUpdateUserSchema } from "@/src/entities/models/users/update-user.model";
|
||||||
|
import { ICredentialsBanUserSchema, IBanUserSchema } from "@/src/entities/models/users/ban-user.model";
|
||||||
|
import { ICredentialsUnbanUserSchema } from "@/src/entities/models/users/unban-user.model";
|
||||||
|
import { ICredentialsDeleteUserSchema } from "@/src/entities/models/users/delete-user.model";
|
||||||
|
import { ICredentialsInviteUserSchema } from "@/src/entities/models/users/invite-user.model";
|
||||||
|
import { ICredentialGetUserByEmailSchema, ICredentialGetUserByIdSchema, ICredentialGetUserByUsernameSchema, IGetUserByUsernameSchema } from "@/src/entities/models/users/read-user.model";
|
||||||
|
|
||||||
export interface IUsersRepository {
|
export interface IUsersRepository {
|
||||||
getUsers(): Promise<User[]>;
|
getUsers(): Promise<IUserSchema[]>;
|
||||||
getCurrentUser(): Promise<User>;
|
getCurrentUser(): Promise<IUserSchema>;
|
||||||
getUserById(id: string): Promise<User | undefined>;
|
getUserById(credential: ICredentialGetUserByIdSchema): Promise<IUserSchema | undefined>;
|
||||||
getUserByUsername(username: string): Promise<User | undefined>;
|
getUserByUsername(credential: ICredentialGetUserByUsernameSchema): Promise<IUserSchema | undefined>;
|
||||||
getUserByEmail(email: string): Promise<User | undefined>;
|
getUserByEmail(credential: ICredentialGetUserByEmailSchema): Promise<IUserSchema | undefined>;
|
||||||
createUser(input: CreateUser, tx?: ITransaction): Promise<User>;
|
createUser(input: ICreateUserSchema, tx?: ITransaction): Promise<IUserSchema>;
|
||||||
inviteUser(email: string, tx?: ITransaction): Promise<User>;
|
inviteUser(credential: ICredentialsInviteUserSchema, tx?: ITransaction): Promise<IUserSchema>;
|
||||||
updateUser(id: string, input: Partial<UpdateUser>, tx?: ITransaction): Promise<User>;
|
updateUser(credential: ICredentialUpdateUserSchema, input: Partial<IUpdateUserSchema>, tx?: ITransaction): Promise<IUserSchema>;
|
||||||
deleteUser(id: string, tx?: ITransaction): Promise<User>;
|
deleteUser(credential: ICredentialsDeleteUserSchema, tx?: ITransaction): Promise<IUserSchema>;
|
||||||
banUser(id: string, ban_duration: string, tx?: ITransaction): Promise<User>;
|
banUser(credential: ICredentialsBanUserSchema, input: IBanUserSchema, tx?: ITransaction): Promise<IUserSchema>;
|
||||||
unbanUser(id: string, tx?: ITransaction): Promise<User>;
|
unbanUser(credential: ICredentialsUnbanUserSchema, tx?: ITransaction): Promise<IUserSchema>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// export class UsersRepository {
|
|
||||||
// constructor(
|
|
||||||
// private readonly instrumentationService: IInstrumentationService,
|
|
||||||
// private readonly crashReporterService: ICrashReporterService,
|
|
||||||
// private readonly supabaseAdmin = createAdminClient(),
|
|
||||||
// private readonly supabaseClient = createClient()
|
|
||||||
// ) { }
|
|
||||||
|
|
||||||
// async getUsers(): Promise<User[]> {
|
|
||||||
// return await this.instrumentationService.startSpan({
|
|
||||||
// name: "UsersRepository > getUsers",
|
|
||||||
// op: 'db.query',
|
|
||||||
// attributes: { 'db.system': 'postgres' },
|
|
||||||
// }, async () => {
|
|
||||||
// try {
|
|
||||||
|
|
||||||
// const users = await db.users.findMany({
|
|
||||||
// include: {
|
|
||||||
// profile: true,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (!users) {
|
|
||||||
// throw new NotFoundError("Users not found");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return users;
|
|
||||||
|
|
||||||
// } catch (err) {
|
|
||||||
// this.crashReporterService.report(err);
|
|
||||||
// throw err;
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async getCurrentUser(): Promise<UserResponse> {
|
|
||||||
// return await this.instrumentationService.startSpan({
|
|
||||||
// name: "UsersRepository > getCurrentUser",
|
|
||||||
// op: 'db.query',
|
|
||||||
// attributes: { 'db.system': 'postgres' },
|
|
||||||
// }, async () => {
|
|
||||||
// try {
|
|
||||||
// const supabase = await this.supabaseClient;
|
|
||||||
|
|
||||||
// const {
|
|
||||||
// data: { user },
|
|
||||||
// error,
|
|
||||||
// } = await supabase.auth.getUser();
|
|
||||||
|
|
||||||
// if (error) {
|
|
||||||
// console.error("Error fetching current user:", error);
|
|
||||||
// throw new AuthenticationError(error.message);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const userDetail = await db.users.findUnique({
|
|
||||||
// where: {
|
|
||||||
// id: user?.id,
|
|
||||||
// },
|
|
||||||
// include: {
|
|
||||||
// profile: true,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (!userDetail) {
|
|
||||||
// throw new NotFoundError("User not found");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// data: {
|
|
||||||
// user: userDetail,
|
|
||||||
// },
|
|
||||||
// error: null,
|
|
||||||
// };
|
|
||||||
// } catch (err) {
|
|
||||||
// this.crashReporterService.report(err);
|
|
||||||
// throw err;
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async createUser(params: CreateUser): Promise<UserResponse> {
|
|
||||||
// return await this.instrumentationService.startSpan({
|
|
||||||
// name: "UsersRepository > createUser",
|
|
||||||
// op: 'db.query',
|
|
||||||
// attributes: { 'db.system': 'postgres' },
|
|
||||||
// }, async () => {
|
|
||||||
// try {
|
|
||||||
// const supabase = this.supabaseAdmin;
|
|
||||||
|
|
||||||
// const { data, error } = await supabase.auth.admin.createUser({
|
|
||||||
// email: params.email,
|
|
||||||
// password: params.password,
|
|
||||||
// phone: params.phone,
|
|
||||||
// email_confirm: params.email_confirm,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (error) {
|
|
||||||
// console.error("Error creating user:", error);
|
|
||||||
// throw new DatabaseOperationError(error.message);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// data: {
|
|
||||||
// user: data.user,
|
|
||||||
// },
|
|
||||||
// error: null,
|
|
||||||
// };
|
|
||||||
// } catch (err) {
|
|
||||||
// this.crashReporterService.report(err);
|
|
||||||
// throw err;
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async inviteUser(params: InviteUser): Promise<void> {
|
|
||||||
// return await this.instrumentationService.startSpan({
|
|
||||||
// name: "UsersRepository > inviteUser",
|
|
||||||
// op: 'db.query',
|
|
||||||
// attributes: { 'db.system': 'postgres' },
|
|
||||||
// }, async () => {
|
|
||||||
// try {
|
|
||||||
// const supabase = this.supabaseAdmin;
|
|
||||||
|
|
||||||
// const { error } = await supabase.auth.admin.inviteUserByEmail(params.email, {
|
|
||||||
// redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/api/auth/callback`,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (error) {
|
|
||||||
// console.error("Error inviting user:", error);
|
|
||||||
// throw new DatabaseOperationError(error.message);
|
|
||||||
// }
|
|
||||||
// } catch (err) {
|
|
||||||
// this.crashReporterService.report(err);
|
|
||||||
// throw err;
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async uploadAvatar(userId: string, email: string, file: File) {
|
|
||||||
// return await this.instrumentationService.startSpan({
|
|
||||||
// name: "UsersRepository > uploadAvatar",
|
|
||||||
// op: 'db.query',
|
|
||||||
// attributes: { 'db.system': 'postgres' },
|
|
||||||
// }, async () => {
|
|
||||||
// try {
|
|
||||||
// const supabase = await this.supabaseClient;
|
|
||||||
|
|
||||||
// const fileExt = file.name.split(".").pop();
|
|
||||||
// const emailName = email.split("@")[0];
|
|
||||||
// const fileName = `AVR-${emailName}.${fileExt}`;
|
|
||||||
|
|
||||||
// const filePath = `${userId}/${fileName}`;
|
|
||||||
|
|
||||||
// const { error: uploadError } = await supabase.storage
|
|
||||||
// .from("avatars")
|
|
||||||
// .upload(filePath, file, {
|
|
||||||
// upsert: true,
|
|
||||||
// contentType: file.type,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (uploadError) {
|
|
||||||
// console.error("Error uploading avatar:", uploadError);
|
|
||||||
// throw new DatabaseOperationError(uploadError.message);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const {
|
|
||||||
// data: { publicUrl },
|
|
||||||
// } = supabase.storage.from("avatars").getPublicUrl(filePath);
|
|
||||||
|
|
||||||
// await db.users.update({
|
|
||||||
// where: {
|
|
||||||
// id: userId,
|
|
||||||
// },
|
|
||||||
// data: {
|
|
||||||
// profile: {
|
|
||||||
// update: {
|
|
||||||
// avatar: publicUrl,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return publicUrl;
|
|
||||||
// } catch (err) {
|
|
||||||
// this.crashReporterService.report(err);
|
|
||||||
// throw err;
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async updateUser(userId: string, params: UpdateUser): Promise<UserResponse> {
|
|
||||||
// return await this.instrumentationService.startSpan({
|
|
||||||
// name: "UsersRepository > updateUser",
|
|
||||||
// op: 'db.query',
|
|
||||||
// attributes: { 'db.system': 'postgres' },
|
|
||||||
// }, async () => {
|
|
||||||
// try {
|
|
||||||
// const supabase = this.supabaseAdmin;
|
|
||||||
|
|
||||||
// const { data, error } = await supabase.auth.admin.updateUserById(userId, {
|
|
||||||
// email: params.email,
|
|
||||||
// email_confirm: params.email_confirmed_at,
|
|
||||||
// password: params.encrypted_password ?? undefined,
|
|
||||||
// password_hash: params.encrypted_password ?? undefined,
|
|
||||||
// phone: params.phone,
|
|
||||||
// phone_confirm: params.phone_confirmed_at,
|
|
||||||
// role: params.role,
|
|
||||||
// user_metadata: params.user_metadata,
|
|
||||||
// app_metadata: params.app_metadata,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (error) {
|
|
||||||
// console.error("Error updating user:", error);
|
|
||||||
// throw new DatabaseOperationError(error.message);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const user = await db.users.findUnique({
|
|
||||||
// where: {
|
|
||||||
// id: userId,
|
|
||||||
// },
|
|
||||||
// include: {
|
|
||||||
// profile: true,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (!user) {
|
|
||||||
// throw new NotFoundError("User not found");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const updateUser = await db.users.update({
|
|
||||||
// where: {
|
|
||||||
// id: userId,
|
|
||||||
// },
|
|
||||||
// data: {
|
|
||||||
// role: params.role || user.role,
|
|
||||||
// invited_at: params.invited_at || user.invited_at,
|
|
||||||
// confirmed_at: params.confirmed_at || user.confirmed_at,
|
|
||||||
// last_sign_in_at: params.last_sign_in_at || user.last_sign_in_at,
|
|
||||||
// is_anonymous: params.is_anonymous || user.is_anonymous,
|
|
||||||
// created_at: params.created_at || user.created_at,
|
|
||||||
// updated_at: params.updated_at || user.updated_at,
|
|
||||||
// profile: {
|
|
||||||
// update: {
|
|
||||||
// avatar: params.profile?.avatar || user.profile?.avatar,
|
|
||||||
// username: params.profile?.username || user.profile?.username,
|
|
||||||
// first_name: params.profile?.first_name || user.profile?.first_name,
|
|
||||||
// last_name: params.profile?.last_name || user.profile?.last_name,
|
|
||||||
// bio: params.profile?.bio || user.profile?.bio,
|
|
||||||
// address: params.profile?.address || user.profile?.address,
|
|
||||||
// birth_date: params.profile?.birth_date || user.profile?.birth_date,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// include: {
|
|
||||||
// profile: true,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// data: {
|
|
||||||
// user: {
|
|
||||||
// ...data.user,
|
|
||||||
// role: params.role,
|
|
||||||
// profile: {
|
|
||||||
// user_id: userId,
|
|
||||||
// ...updateUser.profile,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// error: null,
|
|
||||||
// };
|
|
||||||
// } catch (err) {
|
|
||||||
// this.crashReporterService.report(err);
|
|
||||||
// throw err;
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async deleteUser(userId: string): Promise<void> {
|
|
||||||
// return await this.instrumentationService.startSpan({
|
|
||||||
// name: "UsersRepository > deleteUser",
|
|
||||||
// op: 'db.query',
|
|
||||||
// attributes: { 'db.system': 'postgres' },
|
|
||||||
// }, async () => {
|
|
||||||
// try {
|
|
||||||
// const supabase = this.supabaseAdmin;
|
|
||||||
|
|
||||||
// const { error } = await supabase.auth.admin.deleteUser(userId);
|
|
||||||
|
|
||||||
// if (error) {
|
|
||||||
// console.error("Error deleting user:", error);
|
|
||||||
// throw new DatabaseOperationError(error.message);
|
|
||||||
// }
|
|
||||||
// } catch (err) {
|
|
||||||
// this.crashReporterService.report(err);
|
|
||||||
// throw err;
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async banUser(userId: string): Promise<UserResponse> {
|
|
||||||
// return await this.instrumentationService.startSpan({
|
|
||||||
// name: "UsersRepository > banUser",
|
|
||||||
// op: 'db.query',
|
|
||||||
// attributes: { 'db.system': 'postgres' },
|
|
||||||
// }, async () => {
|
|
||||||
// try {
|
|
||||||
// const supabase = this.supabaseAdmin;
|
|
||||||
|
|
||||||
// const { data, error } = await supabase.auth.admin.updateUserById(userId, {
|
|
||||||
// ban_duration: "100h",
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (error) {
|
|
||||||
// console.error("Error banning user:", error);
|
|
||||||
// throw new DatabaseOperationError(error.message);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// data: {
|
|
||||||
// user: data.user,
|
|
||||||
// },
|
|
||||||
// error: null,
|
|
||||||
// };
|
|
||||||
// } catch (err) {
|
|
||||||
// this.crashReporterService.report(err);
|
|
||||||
// throw err;
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async unbanUser(userId: string): Promise<UserResponse> {
|
|
||||||
// return await this.instrumentationService.startSpan({
|
|
||||||
// name: "UsersRepository > unbanUser",
|
|
||||||
// op: 'db.query',
|
|
||||||
// attributes: { 'db.system': 'postgres' },
|
|
||||||
// }, async () => {
|
|
||||||
// try {
|
|
||||||
// const supabase = this.supabaseAdmin;
|
|
||||||
|
|
||||||
// const { data, error } = await supabase.auth.admin.updateUserById(userId, {
|
|
||||||
// ban_duration: "none",
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (error) {
|
|
||||||
// console.error("Error unbanning user:", error);
|
|
||||||
// throw new DatabaseOperationError(error.message);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const user = await db.users.findUnique({
|
|
||||||
// where: {
|
|
||||||
// id: userId,
|
|
||||||
// },
|
|
||||||
// select: {
|
|
||||||
// banned_until: true,
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (!user) {
|
|
||||||
// throw new NotFoundError("User not found");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// data: {
|
|
||||||
// user: data.user,
|
|
||||||
// },
|
|
||||||
// error: null,
|
|
||||||
// };
|
|
||||||
// } catch (err) {
|
|
||||||
// this.crashReporterService.report(err);
|
|
||||||
// throw err;
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export async function fetchUsers(): Promise<User[]> {
|
|
||||||
// // const { data, error } = await supabase.auth.admin.getUsers();
|
|
||||||
|
|
||||||
// // if (error) {
|
|
||||||
// // console.error("Error fetching users:", error);
|
|
||||||
// // throw new Error(error.message);
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// // return data.users.map((user) => ({
|
|
||||||
// // ...user,
|
|
||||||
// // })) as User[];
|
|
||||||
|
|
||||||
// const users = await db.users.findMany({
|
|
||||||
// include: {
|
|
||||||
// profile: true,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (!users) {
|
|
||||||
// throw new Error("Users not found");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// console.log("fetchedUsers");
|
|
||||||
|
|
||||||
// return users;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // get current user
|
|
||||||
// export async function getCurrentUser(): Promise<UserResponse> {
|
|
||||||
// const supabase = await createClient();
|
|
||||||
|
|
||||||
// const {
|
|
||||||
// data: { user },
|
|
||||||
// error,
|
|
||||||
// } = await supabase.auth.getUser();
|
|
||||||
|
|
||||||
// if (error) {
|
|
||||||
// console.error("Error fetching current user:", error);
|
|
||||||
// throw new Error(error.message);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const userDetail = await db.users.findUnique({
|
|
||||||
// where: {
|
|
||||||
// id: user?.id,
|
|
||||||
// },
|
|
||||||
// include: {
|
|
||||||
// profile: true,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (!userDetail) {
|
|
||||||
// throw new Error("User not found");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// data: {
|
|
||||||
// user: userDetail,
|
|
||||||
// },
|
|
||||||
// error: null,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Create a new user
|
|
||||||
// export async function createUser(
|
|
||||||
// params: CreateUser
|
|
||||||
// ): Promise<UserResponse> {
|
|
||||||
// const supabase = createAdminClient();
|
|
||||||
|
|
||||||
// const { data, error } = await supabase.auth.admin.createUser({
|
|
||||||
// email: params.email,
|
|
||||||
// password: params.password,
|
|
||||||
// phone: params.phone,
|
|
||||||
// email_confirm: params.email_confirm,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (error) {
|
|
||||||
// console.error("Error creating user:", error);
|
|
||||||
// throw new Error(error.message);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// data: {
|
|
||||||
// user: data.user,
|
|
||||||
// },
|
|
||||||
// error: null,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export async function uploadAvatar(userId: string, email: string, file: File) {
|
|
||||||
// try {
|
|
||||||
// const supabase = await createClient();
|
|
||||||
|
|
||||||
// const fileExt = file.name.split(".").pop();
|
|
||||||
// const emailName = email.split("@")[0];
|
|
||||||
// const fileName = `AVR-${emailName}.${fileExt}`;
|
|
||||||
|
|
||||||
// // Change this line - store directly in the user's folder
|
|
||||||
// const filePath = `${userId}/${fileName}`;
|
|
||||||
|
|
||||||
// // Upload the avatar to Supabase storage
|
|
||||||
// const { error: uploadError } = await supabase.storage
|
|
||||||
// .from("avatars")
|
|
||||||
// .upload(filePath, file, {
|
|
||||||
// upsert: true,
|
|
||||||
// contentType: file.type,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (uploadError) {
|
|
||||||
// console.error("Error uploading avatar:", uploadError);
|
|
||||||
// throw uploadError;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Get the public URL
|
|
||||||
// const {
|
|
||||||
// data: { publicUrl },
|
|
||||||
// } = supabase.storage.from("avatars").getPublicUrl(filePath);
|
|
||||||
|
|
||||||
// // Update user profile with the new avatar URL
|
|
||||||
// await db.users.update({
|
|
||||||
// where: {
|
|
||||||
// id: userId,
|
|
||||||
// },
|
|
||||||
// data: {
|
|
||||||
// profile: {
|
|
||||||
// update: {
|
|
||||||
// avatar: publicUrl,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return publicUrl;
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error("Error uploading avatar:", error);
|
|
||||||
// throw error;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// // Update an existing user
|
|
||||||
// export async function updateUser(
|
|
||||||
// userId: string,
|
|
||||||
// params: UpdateUser
|
|
||||||
// ): Promise<UserResponse> {
|
|
||||||
// const supabase = createAdminClient();
|
|
||||||
|
|
||||||
// const { data, error } = await supabase.auth.admin.updateUserById(userId, {
|
|
||||||
// email: params.email,
|
|
||||||
// email_confirm: params.email_confirmed_at,
|
|
||||||
// password: params.encrypted_password ?? undefined,
|
|
||||||
// password_hash: params.encrypted_password ?? undefined,
|
|
||||||
// phone: params.phone,
|
|
||||||
// phone_confirm: params.phone_confirmed_at,
|
|
||||||
// role: params.role,
|
|
||||||
// user_metadata: params.user_metadata,
|
|
||||||
// app_metadata: params.app_metadata,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (error) {
|
|
||||||
// console.error("Error updating user:", error);
|
|
||||||
// throw new Error(error.message);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const user = await db.users.findUnique({
|
|
||||||
// where: {
|
|
||||||
// id: userId,
|
|
||||||
// },
|
|
||||||
// include: {
|
|
||||||
// profile: true,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (!user) {
|
|
||||||
// throw new Error("User not found");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const updateUser = await db.users.update({
|
|
||||||
// where: {
|
|
||||||
// id: userId,
|
|
||||||
// },
|
|
||||||
// data: {
|
|
||||||
// role: params.role || user.role,
|
|
||||||
// invited_at: params.invited_at || user.invited_at,
|
|
||||||
// confirmed_at: params.confirmed_at || user.confirmed_at,
|
|
||||||
// // recovery_sent_at: params.recovery_sent_at || user.recovery_sent_at,
|
|
||||||
// last_sign_in_at: params.last_sign_in_at || user.last_sign_in_at,
|
|
||||||
// is_anonymous: params.is_anonymous || user.is_anonymous,
|
|
||||||
// created_at: params.created_at || user.created_at,
|
|
||||||
// updated_at: params.updated_at || user.updated_at,
|
|
||||||
// profile: {
|
|
||||||
// update: {
|
|
||||||
// avatar: params.profile?.avatar || user.profile?.avatar,
|
|
||||||
// username: params.profile?.username || user.profile?.username,
|
|
||||||
// first_name: params.profile?.first_name || user.profile?.first_name,
|
|
||||||
// last_name: params.profile?.last_name || user.profile?.last_name,
|
|
||||||
// bio: params.profile?.bio || user.profile?.bio,
|
|
||||||
// address: params.profile?.address || user.profile?.address,
|
|
||||||
// birth_date: params.profile?.birth_date || user.profile?.birth_date,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// include: {
|
|
||||||
// profile: true,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// data: {
|
|
||||||
// user: {
|
|
||||||
// ...data.user,
|
|
||||||
// role: params.role,
|
|
||||||
// profile: {
|
|
||||||
// user_id: userId,
|
|
||||||
// ...updateUser.profile,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// error: null,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Delete a user
|
|
||||||
// export async function deleteUser(userId: string): Promise<void> {
|
|
||||||
// const supabase = createAdminClient();
|
|
||||||
|
|
||||||
// const { error } = await supabase.auth.admin.deleteUser(userId);
|
|
||||||
|
|
||||||
// if (error) {
|
|
||||||
// console.error("Error deleting user:", error);
|
|
||||||
// throw new Error(error.message);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Send password recovery email
|
|
||||||
// export async function sendPasswordRecovery(email: string): Promise<void> {
|
|
||||||
// const supabase = createAdminClient();
|
|
||||||
|
|
||||||
// const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
|
||||||
// redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/reset-password`,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (error) {
|
|
||||||
// console.error("Error sending password recovery:", error);
|
|
||||||
// throw new Error(error.message);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Send magic link
|
|
||||||
// export async function sendMagicLink(email: string): Promise<void> {
|
|
||||||
// const supabase = createAdminClient();
|
|
||||||
|
|
||||||
// const { error } = await supabase.auth.signInWithOtp({
|
|
||||||
// email,
|
|
||||||
// options: {
|
|
||||||
// emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (error) {
|
|
||||||
// console.error("Error sending magic link:", error);
|
|
||||||
// throw new Error(error.message);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Ban a user
|
|
||||||
// export async function banUser(userId: string): Promise<UserResponse> {
|
|
||||||
// const supabase = createAdminClient();
|
|
||||||
|
|
||||||
// // Ban for 100 years (effectively permanent)
|
|
||||||
// const banUntil = new Date();
|
|
||||||
// banUntil.setFullYear(banUntil.getFullYear() + 100);
|
|
||||||
|
|
||||||
// const { data, error } = await supabase.auth.admin.updateUserById(userId, {
|
|
||||||
// ban_duration: "100h",
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (error) {
|
|
||||||
// console.error("Error banning user:", error);
|
|
||||||
// throw new Error(error.message);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// data: {
|
|
||||||
// user: data.user,
|
|
||||||
// },
|
|
||||||
// error: null,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Unban a user
|
|
||||||
// export async function unbanUser(userId: string): Promise<UserResponse> {
|
|
||||||
// const supabase = createAdminClient();
|
|
||||||
|
|
||||||
// const { data, error } = await supabase.auth.admin.updateUserById(userId, {
|
|
||||||
// ban_duration: "none",
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (error) {
|
|
||||||
// console.error("Error unbanning user:", error);
|
|
||||||
// throw new Error(error.message);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const user = await db.users.findUnique({
|
|
||||||
// where: {
|
|
||||||
// id: userId,
|
|
||||||
// },
|
|
||||||
// select: {
|
|
||||||
// banned_until: true,
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
// if (!user) {
|
|
||||||
// throw new Error("User not found");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // const updateUser = await db.users.update({
|
|
||||||
// // where: {
|
|
||||||
// // id: userId,
|
|
||||||
// // },
|
|
||||||
// // data: {
|
|
||||||
// // banned_until: null,
|
|
||||||
// // },
|
|
||||||
// // })
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// data: {
|
|
||||||
// user: data.user,
|
|
||||||
// },
|
|
||||||
// error: null,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Invite a user
|
|
||||||
// export async function inviteUser(params: InviteUser): Promise<void> {
|
|
||||||
// const supabase = createAdminClient();
|
|
||||||
|
|
||||||
// const { error } = await supabase.auth.admin.inviteUserByEmail(params.email, {
|
|
||||||
// redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (error) {
|
|
||||||
// console.error("Error inviting user:", error);
|
|
||||||
// throw new Error(error.message);
|
|
||||||
// }
|
|
||||||
// }
|
|
|
@ -1,18 +1,20 @@
|
||||||
import { AuthResult } from "@/src/entities/models/auth/auth-result.model"
|
import { AuthResult } from "@/src/entities/models/auth/auth-result.model"
|
||||||
|
import { ISendMagicLinkSchema } from "@/src/entities/models/auth/send-magic-link.model"
|
||||||
|
import { ISendPasswordRecoverySchema } from "@/src/entities/models/auth/send-password-recovery.model"
|
||||||
import { Session } from "@/src/entities/models/auth/session.model"
|
import { Session } from "@/src/entities/models/auth/session.model"
|
||||||
import { SignInFormData, SignInPasswordless, SignInWithPassword } from "@/src/entities/models/auth/sign-in.model"
|
import { TSignInSchema, ISignInWithPasswordSchema, ISignInPasswordlessSchema } from "@/src/entities/models/auth/sign-in.model"
|
||||||
import { SignUpWithEmail, SignUpWithPhone } from "@/src/entities/models/auth/sign-up.model"
|
import { SignUpWithEmailSchema, SignUpWithPhoneSchema, TSignUpSchema, ISignUpWithEmailSchema, ISignUpWithPhoneSchema } from "@/src/entities/models/auth/sign-up.model"
|
||||||
import { VerifyOtpFormData } from "@/src/entities/models/auth/verify-otp.model"
|
import { IVerifyOtpSchema } from "@/src/entities/models/auth/verify-otp.model"
|
||||||
import { User } from "@/src/entities/models/users/users.model"
|
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||||
|
|
||||||
export interface IAuthenticationService {
|
export interface IAuthenticationService {
|
||||||
signInPasswordless(credentials: SignInPasswordless): Promise<void>
|
signInPasswordless(credentials: ISignInPasswordlessSchema): Promise<void>
|
||||||
signInWithPassword(credentials: SignInWithPassword): Promise<void>
|
SignInWithPasswordSchema(credentials: ISignInWithPasswordSchema): Promise<void>
|
||||||
signUpWithEmail(credentials: SignUpWithEmail): Promise<User>
|
SignUpWithEmailSchema(credentials: ISignUpWithEmailSchema): Promise<IUserSchema>
|
||||||
signUpWithPhone(credentials: SignUpWithPhone): Promise<User>
|
SignUpWithPhoneSchema(credentials: ISignUpWithPhoneSchema): Promise<IUserSchema>
|
||||||
getSession(): Promise<Session | null>
|
getSession(): Promise<Session | null>
|
||||||
signOut(): Promise<void>
|
signOut(): Promise<void>
|
||||||
sendMagicLink(email: string): Promise<void>
|
sendMagicLink(credentials: ISendMagicLinkSchema): Promise<void>
|
||||||
sendPasswordRecovery(email: string): Promise<void>
|
sendPasswordRecovery(credentials: ISendPasswordRecoverySchema): Promise<void>
|
||||||
verifyOtp(credentials: VerifyOtpFormData): Promise<void>
|
verifyOtp(credentials: IVerifyOtpSchema): Promise<void>
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import type { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
|
import type { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
|
||||||
import { AuthenticationError, UnauthenticatedError } from "@/src/entities/errors/auth";
|
import { AuthenticationError, UnauthenticatedError } from "@/src/entities/errors/auth";
|
||||||
import { type SignInFormData, SignInPasswordless, SignInSchema } from "@/src/entities/models/auth/sign-in.model"
|
import { type TSignInSchema, ISignInPasswordlessSchema, SignInSchema } from "@/src/entities/models/auth/sign-in.model"
|
||||||
import { IAuthenticationService } from "@/src/application/services/authentication.service.interface";
|
import { IAuthenticationService } from "@/src/application/services/authentication.service.interface";
|
||||||
import { IUsersRepository } from "../../repositories/users.repository.interface";
|
import { IUsersRepository } from "../../repositories/users.repository.interface";
|
||||||
|
|
||||||
|
@ -12,11 +12,11 @@ export const signInUseCase =
|
||||||
authenticationService: IAuthenticationService,
|
authenticationService: IAuthenticationService,
|
||||||
usersRepository: IUsersRepository
|
usersRepository: IUsersRepository
|
||||||
) =>
|
) =>
|
||||||
async (input: SignInPasswordless): Promise<void> => {
|
async (input: ISignInPasswordlessSchema): Promise<void> => {
|
||||||
return instrumentationService.startSpan({ name: "signIn Use Case", op: "function" },
|
return instrumentationService.startSpan({ name: "signIn Use Case", op: "function" },
|
||||||
async () => {
|
async () => {
|
||||||
|
|
||||||
const existingUser = await usersRepository.getUserByEmail(input.email)
|
const existingUser = await usersRepository.getUserByEmail({ email: input.email })
|
||||||
|
|
||||||
if (!existingUser) {
|
if (!existingUser) {
|
||||||
throw new UnauthenticatedError("User not found. Please tell your admin to create an account for you.")
|
throw new UnauthenticatedError("User not found. Please tell your admin to create an account for you.")
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { CreateUser, User } from "@/src/entities/models/users/users.model"
|
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||||
import { IAuthenticationService } from "../../services/authentication.service.interface"
|
import { IAuthenticationService } from "../../services/authentication.service.interface"
|
||||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||||
import { AuthenticationError } from "@/src/entities/errors/auth"
|
import { AuthenticationError } from "@/src/entities/errors/auth"
|
||||||
import { SignUpFormData } from "@/src/entities/models/auth/sign-up.model"
|
import { ISignUpWithEmailSchema } from "@/src/entities/models/auth/sign-up.model"
|
||||||
import { AuthResult } from "@/src/entities/models/auth/auth-result.model"
|
|
||||||
|
|
||||||
|
|
||||||
export type ISignUpUseCase = ReturnType<typeof signUpUseCase>
|
export type ISignUpUseCase = ReturnType<typeof signUpUseCase>
|
||||||
|
@ -13,21 +13,21 @@ export const signUpUseCase = (
|
||||||
instrumentationService: IInstrumentationService,
|
instrumentationService: IInstrumentationService,
|
||||||
authenticationService: IAuthenticationService,
|
authenticationService: IAuthenticationService,
|
||||||
usersRepository: IUsersRepository
|
usersRepository: IUsersRepository
|
||||||
) => async (input: SignUpFormData): Promise<User> => {
|
) => async (input: ISignUpWithEmailSchema): Promise<IUserSchema> => {
|
||||||
return await instrumentationService.startSpan({ name: "signUp Use Case", op: "function" },
|
return await instrumentationService.startSpan({ name: "signUp Use Case", op: "function" },
|
||||||
async () => {
|
async () => {
|
||||||
const existingUser = await usersRepository.getUserByEmail(input.email)
|
const existingUser = await usersRepository.getUserByEmail({ email: input.email })
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
throw new AuthenticationError("User already exists")
|
throw new AuthenticationError("User already exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
const newUser = await authenticationService.signUpWithEmail({
|
const newUser = await authenticationService.SignUpWithEmailSchema({
|
||||||
email: input.email,
|
email: input.email,
|
||||||
password: input.password
|
password: input.password
|
||||||
})
|
})
|
||||||
|
|
||||||
await authenticationService.signInWithPassword({
|
await authenticationService.SignInWithPasswordSchema({
|
||||||
email: input.email,
|
email: input.email,
|
||||||
password: input.password
|
password: input.password
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { VerifyOtpFormData } from "@/src/entities/models/auth/verify-otp.model"
|
import { VerifyOtpSchema } from "@/src/entities/models/auth/verify-otp.model"
|
||||||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||||
import { IAuthenticationService } from "../../services/authentication.service.interface"
|
import { IAuthenticationService } from "../../services/authentication.service.interface"
|
||||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||||
|
@ -12,7 +12,7 @@ export const verifyOtpUseCase = (
|
||||||
instrumentationService: IInstrumentationService,
|
instrumentationService: IInstrumentationService,
|
||||||
authenticationService: IAuthenticationService,
|
authenticationService: IAuthenticationService,
|
||||||
usersRepository: IUsersRepository
|
usersRepository: IUsersRepository
|
||||||
) => async (input: VerifyOtpFormData): Promise<void> => {
|
) => async (input: VerifyOtpSchema): Promise<void> => {
|
||||||
return await instrumentationService.startSpan({ name: "verifyOtp Use Case", op: "function" },
|
return await instrumentationService.startSpan({ name: "verifyOtp Use Case", op: "function" },
|
||||||
async () => {
|
async () => {
|
||||||
const user = await usersRepository.getUserByEmail(input.email)
|
const user = await usersRepository.getUserByEmail(input.email)
|
||||||
|
|
|
@ -1,23 +1,26 @@
|
||||||
import { User } from "@/src/entities/models/users/users.model"
|
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||||
import { NotFoundError } from "@/src/entities/errors/common"
|
import { NotFoundError } from "@/src/entities/errors/common"
|
||||||
|
import { IBanUserSchema, ICredentialsBanUserSchema } from "@/src/entities/models/users/ban-user.model"
|
||||||
|
|
||||||
export type IBanUserUseCase = ReturnType<typeof banUserUseCase>
|
export type IBanUserUseCase = ReturnType<typeof banUserUseCase>
|
||||||
|
|
||||||
export const banUserUseCase = (
|
export const banUserUseCase = (
|
||||||
instrumentationService: IInstrumentationService,
|
instrumentationService: IInstrumentationService,
|
||||||
usersRepository: IUsersRepository
|
usersRepository: IUsersRepository
|
||||||
) => async (id: string, ban_duration: string): Promise<User> => {
|
) => async (credential: ICredentialsBanUserSchema, input: IBanUserSchema): Promise<IUserSchema> => {
|
||||||
return await instrumentationService.startSpan({ name: "banUser Use Case", op: "function" },
|
return await instrumentationService.startSpan({ name: "banUser Use Case", op: "function" },
|
||||||
async () => {
|
async () => {
|
||||||
const existingUser = await usersRepository.getUserById(id)
|
const existingUser = await usersRepository.getUserById(credential)
|
||||||
|
|
||||||
if (!existingUser) {
|
if (!existingUser) {
|
||||||
throw new NotFoundError("User not found")
|
throw new NotFoundError("User not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
const bannedUser = await usersRepository.banUser(id, ban_duration)
|
const bannedUser = await usersRepository.banUser(credential, input)
|
||||||
|
|
||||||
|
console.log("Use Case: Ban User")
|
||||||
|
|
||||||
return bannedUser
|
return bannedUser
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,9 @@ import { AuthenticationError } from "@/src/entities/errors/auth"
|
||||||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||||
import { IAuthenticationService } from "../../services/authentication.service.interface"
|
import { IAuthenticationService } from "../../services/authentication.service.interface"
|
||||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||||
import { CreateUser, User } from "@/src/entities/models/users/users.model"
|
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||||
import { InputParseError } from "@/src/entities/errors/common"
|
import { InputParseError } from "@/src/entities/errors/common"
|
||||||
|
import { ICreateUserSchema } from "@/src/entities/models/users/create-user.model"
|
||||||
|
|
||||||
|
|
||||||
export type ICreateUserUseCase = ReturnType<typeof createUserUseCase>
|
export type ICreateUserUseCase = ReturnType<typeof createUserUseCase>
|
||||||
|
@ -11,11 +12,11 @@ export type ICreateUserUseCase = ReturnType<typeof createUserUseCase>
|
||||||
export const createUserUseCase = (
|
export const createUserUseCase = (
|
||||||
instrumentationService: IInstrumentationService,
|
instrumentationService: IInstrumentationService,
|
||||||
usersRepository: IUsersRepository,
|
usersRepository: IUsersRepository,
|
||||||
) => async (input: CreateUser): Promise<User> => {
|
) => async (input: ICreateUserSchema): Promise<IUserSchema> => {
|
||||||
return await instrumentationService.startSpan({ name: "createUser Use Case", op: "function" },
|
return await instrumentationService.startSpan({ name: "createUser Use Case", op: "function" },
|
||||||
async () => {
|
async () => {
|
||||||
|
|
||||||
const existingUser = await usersRepository.getUserByEmail(input.email)
|
const existingUser = await usersRepository.getUserByEmail({ email: input.email })
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
throw new AuthenticationError("User already exists")
|
throw new AuthenticationError("User already exists")
|
||||||
|
|
|
@ -1,23 +1,24 @@
|
||||||
import { NotFoundError } from "@/src/entities/errors/common"
|
import { NotFoundError } from "@/src/entities/errors/common"
|
||||||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||||
import { User } from "@/src/entities/models/users/users.model"
|
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||||
|
import { ICredentialsDeleteUserSchema } from "@/src/entities/models/users/delete-user.model"
|
||||||
|
|
||||||
export type IDeleteUserUseCase = ReturnType<typeof deleteUserUseCase>
|
export type IDeleteUserUseCase = ReturnType<typeof deleteUserUseCase>
|
||||||
|
|
||||||
export const deleteUserUseCase = (
|
export const deleteUserUseCase = (
|
||||||
instrumentationService: IInstrumentationService,
|
instrumentationService: IInstrumentationService,
|
||||||
usersRepository: IUsersRepository
|
usersRepository: IUsersRepository
|
||||||
) => async (id: string): Promise<User> => {
|
) => async (credential: ICredentialsDeleteUserSchema): Promise<IUserSchema> => {
|
||||||
return await instrumentationService.startSpan({ name: "deleteUser Use Case", op: "function" },
|
return await instrumentationService.startSpan({ name: "deleteUser Use Case", op: "function" },
|
||||||
async () => {
|
async () => {
|
||||||
const user = await usersRepository.getUserById(id)
|
const user = await usersRepository.getUserById(credential)
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundError("User not found")
|
throw new NotFoundError("User not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
const deletedUser = await usersRepository.deleteUser(id)
|
const deletedUser = await usersRepository.deleteUser(credential)
|
||||||
|
|
||||||
return deletedUser
|
return deletedUser
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { NotFoundError } from "@/src/entities/errors/common"
|
import { NotFoundError } from "@/src/entities/errors/common"
|
||||||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||||
import { User, UserResponse } from "@/src/entities/models/users/users.model"
|
import { IUserSchema, UserResponse } from "@/src/entities/models/users/users.model"
|
||||||
import { AuthenticationError } from "@/src/entities/errors/auth"
|
import { AuthenticationError } from "@/src/entities/errors/auth"
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ export type IGetCurrentUserUseCase = ReturnType<typeof getCurrentUserUseCase>
|
||||||
export const getCurrentUserUseCase = (
|
export const getCurrentUserUseCase = (
|
||||||
instrumentationService: IInstrumentationService,
|
instrumentationService: IInstrumentationService,
|
||||||
usersRepository: IUsersRepository
|
usersRepository: IUsersRepository
|
||||||
) => async (): Promise<User> => {
|
) => async (): Promise<IUserSchema> => {
|
||||||
return await instrumentationService.startSpan({ name: "getCurrentUser Use Case", op: "function" },
|
return await instrumentationService.startSpan({ name: "getCurrentUser Use Case", op: "function" },
|
||||||
async () => {
|
async () => {
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
import { User } from "@/src/entities/models/users/users.model"
|
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||||
import { NotFoundError } from "@/src/entities/errors/common"
|
import { NotFoundError } from "@/src/entities/errors/common"
|
||||||
|
import { ICredentialGetUserByEmailSchema } from "@/src/entities/models/users/read-user.model"
|
||||||
|
|
||||||
export type IGetUserByEmailUseCase = ReturnType<typeof getUserByEmailUseCase>
|
export type IGetUserByEmailUseCase = ReturnType<typeof getUserByEmailUseCase>
|
||||||
|
|
||||||
export const getUserByEmailUseCase = (
|
export const getUserByEmailUseCase = (
|
||||||
instrumentationService: IInstrumentationService,
|
instrumentationService: IInstrumentationService,
|
||||||
usersRepository: IUsersRepository
|
usersRepository: IUsersRepository
|
||||||
) => async (email: string): Promise<User> => {
|
) => async (credential: ICredentialGetUserByEmailSchema): Promise<IUserSchema> => {
|
||||||
return await instrumentationService.startSpan({ name: "getUserByEmail Use Case", op: "function" },
|
return await instrumentationService.startSpan({ name: "getUserByEmail Use Case", op: "function" },
|
||||||
async () => {
|
async () => {
|
||||||
|
|
||||||
const user = await usersRepository.getUserByEmail(email)
|
const user = await usersRepository.getUserByEmail(credential)
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundError("User not found")
|
throw new NotFoundError("User not found")
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { User } from "@/src/entities/models/users/users.model"
|
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||||
import { AuthenticationError } from "@/src/entities/errors/auth"
|
import { AuthenticationError } from "@/src/entities/errors/auth"
|
||||||
import { NotFoundError } from "@/src/entities/errors/common"
|
import { NotFoundError } from "@/src/entities/errors/common"
|
||||||
|
import { ICredentialGetUserByIdSchema } from "@/src/entities/models/users/read-user.model"
|
||||||
|
|
||||||
|
|
||||||
export type IGetUserByIdUseCase = ReturnType<typeof getUserByIdUseCase>
|
export type IGetUserByIdUseCase = ReturnType<typeof getUserByIdUseCase>
|
||||||
|
@ -10,11 +11,11 @@ export type IGetUserByIdUseCase = ReturnType<typeof getUserByIdUseCase>
|
||||||
export const getUserByIdUseCase = (
|
export const getUserByIdUseCase = (
|
||||||
instrumentationService: IInstrumentationService,
|
instrumentationService: IInstrumentationService,
|
||||||
usersRepository: IUsersRepository
|
usersRepository: IUsersRepository
|
||||||
) => async (id: string): Promise<User> => {
|
) => async (credential: ICredentialGetUserByIdSchema): Promise<IUserSchema> => {
|
||||||
return await instrumentationService.startSpan({ name: "getUserById Use Case", op: "function" },
|
return await instrumentationService.startSpan({ name: "getUserById Use Case", op: "function" },
|
||||||
async () => {
|
async () => {
|
||||||
|
|
||||||
const user = await usersRepository.getUserById(id)
|
const user = await usersRepository.getUserById(credential)
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundError("User not found")
|
throw new NotFoundError("User not found")
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
import { NotFoundError } from "@/src/entities/errors/common"
|
import { NotFoundError } from "@/src/entities/errors/common"
|
||||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||||
import { User } from "@/src/entities/models/users/users.model"
|
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||||
|
import { ICredentialGetUserByUsernameSchema } from "@/src/entities/models/users/read-user.model"
|
||||||
|
|
||||||
export type IGetUserByUsernameUseCase = ReturnType<typeof getUserByUsernameUseCase>
|
export type IGetUserByUsernameUseCase = ReturnType<typeof getUserByUsernameUseCase>
|
||||||
|
|
||||||
export const getUserByUsernameUseCase = (
|
export const getUserByUsernameUseCase = (
|
||||||
instrumentationService: IInstrumentationService,
|
instrumentationService: IInstrumentationService,
|
||||||
usersRepository: IUsersRepository
|
usersRepository: IUsersRepository
|
||||||
) => async (username: string): Promise<User> => {
|
) => async (credential: ICredentialGetUserByUsernameSchema): Promise<IUserSchema> => {
|
||||||
return await instrumentationService.startSpan({ name: "getUserByUsername Use Case", op: "function" },
|
return await instrumentationService.startSpan({ name: "getUserByUsername Use Case", op: "function" },
|
||||||
async () => {
|
async () => {
|
||||||
|
|
||||||
const user = await usersRepository.getUserByUsername(username)
|
const user = await usersRepository.getUserByUsername(credential)
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundError("User not found")
|
throw new NotFoundError("User not found")
|
||||||
|
|
|
@ -8,7 +8,7 @@ export const getUsersUseCase = (
|
||||||
instrumentationService: IInstrumentationService,
|
instrumentationService: IInstrumentationService,
|
||||||
usersRepository: IUsersRepository
|
usersRepository: IUsersRepository
|
||||||
) => async (): Promise<User[]> => {
|
) => async (): Promise<User[]> => {
|
||||||
return await instrumentationService.startSpan({ name: "getgetUsers Use Case", op: "function" },
|
return instrumentationService.startSpan({ name: "getUsers Use Case", op: "function" },
|
||||||
async () => {
|
async () => {
|
||||||
const users = await usersRepository.getUsers()
|
const users = await usersRepository.getUsers()
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,8 @@ import { AuthenticationError } from "@/src/entities/errors/auth"
|
||||||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||||
import { IAuthenticationService } from "../../services/authentication.service.interface"
|
import { IAuthenticationService } from "../../services/authentication.service.interface"
|
||||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||||
import { User } from "@/src/entities/models/users/users.model"
|
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||||
|
import { ICredentialsInviteUserSchema } from "@/src/entities/models/users/invite-user.model"
|
||||||
|
|
||||||
|
|
||||||
export type IInviteUserUseCase = ReturnType<typeof inviteUserUseCase>
|
export type IInviteUserUseCase = ReturnType<typeof inviteUserUseCase>
|
||||||
|
@ -10,17 +11,16 @@ export type IInviteUserUseCase = ReturnType<typeof inviteUserUseCase>
|
||||||
export const inviteUserUseCase = (
|
export const inviteUserUseCase = (
|
||||||
instrumentationService: IInstrumentationService,
|
instrumentationService: IInstrumentationService,
|
||||||
usersRepository: IUsersRepository,
|
usersRepository: IUsersRepository,
|
||||||
authenticationService: IAuthenticationService,
|
) => async (credential: ICredentialsInviteUserSchema): Promise<IUserSchema> => {
|
||||||
) => async (email: string): Promise<User> => {
|
|
||||||
return await instrumentationService.startSpan({ name: "inviteUser Use Case", op: "function" },
|
return await instrumentationService.startSpan({ name: "inviteUser Use Case", op: "function" },
|
||||||
async () => {
|
async () => {
|
||||||
const existingUser = await usersRepository.getUserByEmail(email)
|
const existingUser = await usersRepository.getUserByEmail(credential)
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
throw new AuthenticationError("User already exists")
|
throw new AuthenticationError("User already exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
const invitedUser = await usersRepository.inviteUser(email)
|
const invitedUser = await usersRepository.inviteUser(credential)
|
||||||
|
|
||||||
if (!invitedUser) {
|
if (!invitedUser) {
|
||||||
throw new AuthenticationError("User not invited")
|
throw new AuthenticationError("User not invited")
|
||||||
|
|
|
@ -1,23 +1,24 @@
|
||||||
import { NotFoundError } from "@/src/entities/errors/common"
|
import { NotFoundError } from "@/src/entities/errors/common"
|
||||||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||||
import { User } from "@/src/entities/models/users/users.model"
|
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||||
|
import { ICredentialsUnbanUserSchema } from "@/src/entities/models/users/unban-user.model"
|
||||||
|
|
||||||
export type IUnbanUserUseCase = ReturnType<typeof unbanUserUseCase>
|
export type IUnbanUserUseCase = ReturnType<typeof unbanUserUseCase>
|
||||||
|
|
||||||
export const unbanUserUseCase = (
|
export const unbanUserUseCase = (
|
||||||
instrumentationService: IInstrumentationService,
|
instrumentationService: IInstrumentationService,
|
||||||
usersRepository: IUsersRepository
|
usersRepository: IUsersRepository
|
||||||
) => async (id: string): Promise<User> => {
|
) => async (credential: ICredentialsUnbanUserSchema): Promise<IUserSchema> => {
|
||||||
return await instrumentationService.startSpan({ name: "unbanUser Use Case", op: "function" },
|
return await instrumentationService.startSpan({ name: "unbanUser Use Case", op: "function" },
|
||||||
async () => {
|
async () => {
|
||||||
const existingUser = await usersRepository.getUserById(id)
|
const existingUser = await usersRepository.getUserById(credential)
|
||||||
|
|
||||||
if (!existingUser) {
|
if (!existingUser) {
|
||||||
throw new NotFoundError("User not found")
|
throw new NotFoundError("User not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
const unbanUser = await usersRepository.unbanUser(id)
|
const unbanUser = await usersRepository.unbanUser(credential)
|
||||||
|
|
||||||
return unbanUser
|
return unbanUser
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,25 @@
|
||||||
import { UpdateUser, User, UserResponse } from "@/src/entities/models/users/users.model"
|
import { IUserSchema } from "@/src/entities/models/users/users.model"
|
||||||
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
import { IUsersRepository } from "../../repositories/users.repository.interface"
|
||||||
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
|
||||||
import { NotFoundError } from "@/src/entities/errors/common"
|
import { NotFoundError } from "@/src/entities/errors/common"
|
||||||
|
import { ICredentialUpdateUserSchema, IUpdateUserSchema } from "@/src/entities/models/users/update-user.model"
|
||||||
|
|
||||||
export type IUpdateUserUseCase = ReturnType<typeof updateUserUseCase>
|
export type IUpdateUserUseCase = ReturnType<typeof updateUserUseCase>
|
||||||
|
|
||||||
export const updateUserUseCase = (
|
export const updateUserUseCase = (
|
||||||
instrumentationService: IInstrumentationService,
|
instrumentationService: IInstrumentationService,
|
||||||
usersRepository: IUsersRepository
|
usersRepository: IUsersRepository
|
||||||
) => async (id: string, input: UpdateUser): Promise<User> => {
|
) => async (credential: ICredentialUpdateUserSchema, input: IUpdateUserSchema): Promise<IUserSchema> => {
|
||||||
return await instrumentationService.startSpan({ name: "updateUser Use Case", op: "function" },
|
return await instrumentationService.startSpan({ name: "updateUser Use Case", op: "function" },
|
||||||
async () => {
|
async () => {
|
||||||
|
|
||||||
const existingUser = await usersRepository.getUserById(id)
|
const existingUser = await usersRepository.getUserById(credential)
|
||||||
|
|
||||||
if (!existingUser) {
|
if (!existingUser) {
|
||||||
throw new NotFoundError("User not found")
|
throw new NotFoundError("User not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedUser = await usersRepository.updateUser(id, input)
|
const updatedUser = await usersRepository.updateUser(credential, input)
|
||||||
|
|
||||||
return updatedUser
|
return updatedUser
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,27 @@
|
||||||
export class DatabaseOperationError extends Error {
|
export class DatabaseOperationError extends Error {
|
||||||
constructor(message: string, options?: ErrorOptions) {
|
constructor(message: string, options?: ErrorOptions) {
|
||||||
super(message, options);
|
super(message, options);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
export class NotFoundError extends Error {
|
|
||||||
constructor(message: string, options?: ErrorOptions) {
|
export class NotFoundError extends Error {
|
||||||
super(message, options);
|
constructor(message: string, options?: ErrorOptions) {
|
||||||
}
|
super(message, options);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
export class InputParseError extends Error {
|
|
||||||
constructor(message: string, options?: ErrorOptions) {
|
export class InputParseError extends Error {
|
||||||
super(message, options);
|
constructor(message: string, options?: ErrorOptions) {
|
||||||
}
|
super(message, options);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ServerActionError extends Error {
|
||||||
|
code: string;
|
||||||
|
|
||||||
|
constructor(message: string, code: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "ServerActionError";
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const SendMagicLinkSchema = z.object({
|
||||||
|
email: z.string().min(1, { message: "Email is required" }).email({ message: "Please enter a valid email address" }),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type ISendMagicLinkSchema = z.infer<typeof SendMagicLinkSchema>
|
||||||
|
|
||||||
|
export const defaulISendMagicLinkSchemaValues: ISendMagicLinkSchema = {
|
||||||
|
email: "",
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const SendPasswordRecoverySchema = z.object({
|
||||||
|
email: z.string().min(1, { message: "Email is required" }).email({ message: "Please enter a valid email address" }),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type ISendPasswordRecoverySchema = z.infer<typeof SendPasswordRecoverySchema>
|
||||||
|
|
||||||
|
export const defaulISendPasswordRecoverySchemaValues: ISendPasswordRecoverySchema = {
|
||||||
|
email: "",
|
||||||
|
}
|
|
@ -6,40 +6,40 @@ export const SignInSchema = z.object({
|
||||||
.string()
|
.string()
|
||||||
.min(1, { message: "Email is required" })
|
.min(1, { message: "Email is required" })
|
||||||
.email({ message: "Please enter a valid email address" }),
|
.email({ message: "Please enter a valid email address" }),
|
||||||
password: z.string().min(1, { message: "Password is required" }),
|
password: z.string().min(1, { message: "Password is required" }).min(8, { message: "Password must be at least 8 characters" }),
|
||||||
phone: z.string().optional(),
|
phone: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Export the type derived from the schema
|
// Export the type derived from the schema
|
||||||
export type SignInFormData = z.infer<typeof SignInSchema>;
|
export type TSignInSchema = z.infer<typeof SignInSchema>;
|
||||||
|
|
||||||
export const SignInWithPassword = SignInSchema.pick({
|
export const SignInWithPasswordSchema = SignInSchema.pick({
|
||||||
email: true,
|
email: true,
|
||||||
password: true,
|
password: true,
|
||||||
phone: true
|
phone: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export type ISignInWithPasswordSchema = z.infer<typeof SignInWithPasswordSchema>
|
||||||
|
|
||||||
// Default values for the form
|
// Default values for the form
|
||||||
export const defaultSignInWithPasswordValues: SignInWithPassword = {
|
export const defaulISignInWithPasswordSchemaValues: ISignInWithPasswordSchema = {
|
||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
phone: ""
|
phone: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SignInWithPassword = z.infer<typeof SignInWithPassword>
|
|
||||||
|
|
||||||
export const SignInPasswordlessSchema = SignInSchema.pick({
|
export const SignInPasswordlessSchema = SignInSchema.pick({
|
||||||
email: true,
|
email: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export type ISignInPasswordlessSchema = z.infer<typeof SignInPasswordlessSchema>
|
||||||
|
|
||||||
// Default values for the form
|
// Default values for the form
|
||||||
export const defaultSignInPasswordlessValues: SignInPasswordless = {
|
export const defaulISignInPasswordlessSchemaValues: ISignInPasswordlessSchema = {
|
||||||
email: "",
|
email: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SignInPasswordless = z.infer<typeof SignInPasswordlessSchema>
|
|
||||||
|
|
||||||
|
|
||||||
// Define the sign-in response schema using Zod
|
// Define the sign-in response schema using Zod
|
||||||
export const SignInResponseSchema = z.object({
|
export const SignInResponseSchema = z.object({
|
||||||
success: z.boolean(),
|
success: z.boolean(),
|
||||||
|
|
|
@ -5,32 +5,42 @@ export const SignUpSchema = z.object({
|
||||||
.string()
|
.string()
|
||||||
.min(1, { message: "Email is required" })
|
.min(1, { message: "Email is required" })
|
||||||
.email({ message: "Please enter a valid email address" }),
|
.email({ message: "Please enter a valid email address" }),
|
||||||
password: z.string().min(1, { message: "Password is required" }),
|
password: z.string().min(1, { message: "Password is required" }).min(8, { message: "Password must be at least 8 characters" }),
|
||||||
phone: z.string().optional(),
|
phone: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export type SignUpFormData = z.infer<typeof SignUpSchema>;
|
export type TSignUpSchema = z.infer<typeof SignUpSchema>;
|
||||||
|
|
||||||
export const SignUpWithEmail = SignUpSchema.pick({
|
export const SignUpWithEmailSchema = SignUpSchema.pick({
|
||||||
email: true,
|
email: true,
|
||||||
password: true,
|
password: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const defaultSignUpWithEmailValues: SignUpWithEmail = {
|
export type ISignUpWithEmailSchema = z.infer<typeof SignUpWithEmailSchema>
|
||||||
|
|
||||||
|
export const defaulISignUpWithEmailSchemaValues: ISignUpWithEmailSchema = {
|
||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SignUpWithEmail = z.infer<typeof SignUpWithEmail>
|
export const SignUpWithPhoneSchema = SignUpSchema.pick({
|
||||||
|
|
||||||
export const SignUpWithPhone = SignUpSchema.pick({
|
|
||||||
phone: true,
|
phone: true,
|
||||||
password: true,
|
password: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const defaultSignUpWithPhoneValues: SignUpWithPhone = {
|
export type ISignUpWithPhoneSchema = z.infer<typeof SignUpWithPhoneSchema>
|
||||||
|
|
||||||
|
export const defaulISignUpWithPhoneSchemaValues: ISignUpWithPhoneSchema = {
|
||||||
phone: "",
|
phone: "",
|
||||||
password: "",
|
password: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SignUpWithPhone = z.infer<typeof SignUpWithPhone>
|
export const SignUpWithOtpSchema = SignUpSchema.pick({
|
||||||
|
email: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
export type TSignUpWithOtpSchema = z.infer<typeof SignUpWithOtpSchema>
|
||||||
|
|
||||||
|
export const defaultSignUpWithOtpSchemaValues: TSignUpWithOtpSchema = {
|
||||||
|
email: "",
|
||||||
|
}
|
|
@ -5,9 +5,9 @@ export const verifyOtpSchema = z.object({
|
||||||
token: z.string().length(6, { message: "OTP must be 6 characters long" }),
|
token: z.string().length(6, { message: "OTP must be 6 characters long" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type VerifyOtpFormData = z.infer<typeof verifyOtpSchema>;
|
export type IVerifyOtpSchema = z.infer<typeof verifyOtpSchema>;
|
||||||
|
|
||||||
export const defaultVerifyOtpValues: VerifyOtpFormData = {
|
export const defaultVerifyOtpValues: IVerifyOtpSchema = {
|
||||||
email: "",
|
email: "",
|
||||||
token: "",
|
token: "",
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
// Schema Zod untuk validasi runtime
|
||||||
|
import { CRegex } from "@/app/_lib/const/regex";
|
||||||
|
import { ValidBanDuration } from "@/app/_lib/types/ban-duration";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const BanDurationSchema = z.custom<ValidBanDuration>(
|
||||||
|
(value) => typeof value === "string" && CRegex.BAN_DURATION_REGEX.test(value),
|
||||||
|
{ message: "Invalid ban duration format." }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Tipe untuk digunakan di kode
|
||||||
|
export type IBanDuration = z.infer<typeof BanDurationSchema>;
|
||||||
|
|
||||||
|
export const BanUserCredentialsSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type ICredentialsBanUserSchema = z.infer<typeof BanUserCredentialsSchema>
|
||||||
|
|
||||||
|
// Schema utama untuk user
|
||||||
|
export const BanUserSchema = z.object({
|
||||||
|
ban_duration: BanDurationSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IBanUserSchema = z.infer<typeof BanUserSchema>;
|
||||||
|
|
||||||
|
// Nilai default
|
||||||
|
export const defaulIBanUserSchemaValues: IBanUserSchema = {
|
||||||
|
ban_duration: "none",
|
||||||
|
};
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { CNumbers } from "@/app/_lib/const/number";
|
||||||
|
import { CTexts } from "@/app/_lib/const/string";
|
||||||
|
import { phonePrefixValidation, phoneRegexValidation } from "@/app/_utils/validation";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const CreateUserSchema = z.object({
|
||||||
|
email: z.string().min(1, "Email is required").email(),
|
||||||
|
password: z.string().min(1, { message: "Password is required" }).min(8, { message: "Password must be at least 8 characters" }),
|
||||||
|
phone: z.string()
|
||||||
|
.refine(phonePrefixValidation, {
|
||||||
|
message: `Phone number must start with one of the following: ${CTexts.PHONE_PREFIX.join(', ')}.`,
|
||||||
|
})
|
||||||
|
.refine(phoneRegexValidation, {
|
||||||
|
message: `Phone number must have a length between ${CNumbers.PHONE_MIN_LENGTH} and ${CNumbers.PHONE_MAX_LENGTH} digits without the country code.`,
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
email_confirm: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ICreateUserSchema = z.infer<typeof CreateUserSchema>;
|
||||||
|
|
||||||
|
export const defaulICreateUserSchemaValues: ICreateUserSchema = {
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
phone: "",
|
||||||
|
email_confirm: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CredentialCreateUserSchema = CreateUserSchema.pick({
|
||||||
|
email: true,
|
||||||
|
})
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
export const DeleteUserSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type IDeleteUserSchema = z.infer<typeof DeleteUserSchema>
|
||||||
|
|
||||||
|
export const defaulIDeleteUserSchemaValues: IDeleteUserSchema = {
|
||||||
|
id: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteUserCredentialsSchema = DeleteUserSchema.pick({ id: true })
|
||||||
|
|
||||||
|
export type ICredentialsDeleteUserSchema = z.infer<typeof DeleteUserCredentialsSchema>
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const InviteUserSchema = z.object({
|
||||||
|
email: z.string().min(1, "Email is required").email(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IInviteUserSchema = z.infer<typeof InviteUserSchema>;
|
||||||
|
|
||||||
|
export const defaulIInviteUserSchemaValues: IInviteUserSchema = {
|
||||||
|
email: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InviteUserCredentialsSchema = InviteUserSchema.pick({ email: true })
|
||||||
|
|
||||||
|
export type ICredentialsInviteUserSchema = z.infer<typeof InviteUserCredentialsSchema>
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema untuk mendapatkan user berdasarkan ID
|
||||||
|
* @typedef {Object} IGetUserByIdSchema
|
||||||
|
* @property {string} id - ID pengguna yang akan dicari
|
||||||
|
*/
|
||||||
|
export const GetUserByIdSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IGetUserByIdSchema = z.infer<typeof GetUserByIdSchema>;
|
||||||
|
|
||||||
|
export const defaulIGetUserByIdSchemaValues: IGetUserByIdSchema = {
|
||||||
|
id: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema credential untuk mendapatkan user berdasarkan ID
|
||||||
|
* Mengambil hanya properti 'id' dari GetUserByIdSchema
|
||||||
|
*/
|
||||||
|
export const ICredentialGetUserByIdSchema = GetUserByIdSchema.pick({ id: true });
|
||||||
|
|
||||||
|
export type ICredentialGetUserByIdSchema = z.infer<typeof ICredentialGetUserByIdSchema>;
|
||||||
|
|
||||||
|
export const GetUserByEmailSchema = z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tipe inferensi dari GetUserByEmailSchema
|
||||||
|
* @type {z.infer<typeof GetUserByEmailSchema>}
|
||||||
|
*/
|
||||||
|
export type IGetUserByEmailSchema = z.infer<typeof GetUserByEmailSchema>;
|
||||||
|
|
||||||
|
export const defaulIGetUserByEmailSchemaValues: IGetUserByEmailSchema = {
|
||||||
|
email: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ICredentialGetUserByEmailSchema = GetUserByEmailSchema.pick({ email: true });
|
||||||
|
|
||||||
|
export type ICredentialGetUserByEmailSchema = z.infer<typeof ICredentialGetUserByEmailSchema>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema untuk mendapatkan user berdasarkan username
|
||||||
|
* @typedef {Object} IGetUserByUsernameSchema
|
||||||
|
* @property {string} username - Nama pengguna yang akan dicari
|
||||||
|
*/
|
||||||
|
export const GetUserByUsernameSchema = z.object({
|
||||||
|
username: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IGetUserByUsernameSchema = z.infer<typeof GetUserByUsernameSchema>;
|
||||||
|
|
||||||
|
export const defaulIGetUserByUsernameSchemaValues: IGetUserByUsernameSchema = {
|
||||||
|
username: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ICredentialGetUserByUsernameSchema = GetUserByUsernameSchema.pick({ username: true });
|
||||||
|
|
||||||
|
export type ICredentialGetUserByUsernameSchema = z.infer<typeof ICredentialGetUserByUsernameSchema>;
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const UnbanUserSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type IUnbanUserSchema = z.infer<typeof UnbanUserSchema>;
|
||||||
|
|
||||||
|
export const defaulIUnbanUserSchemaValues: IUnbanUserSchema = {
|
||||||
|
id: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UnbanUserCredentialsSchema = UnbanUserSchema.pick({ id: true })
|
||||||
|
|
||||||
|
export type ICredentialsUnbanUserSchema = z.infer<typeof UnbanUserCredentialsSchema>
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const UpdateUserSchema = z.object({
|
||||||
|
email: z.string().email().optional(),
|
||||||
|
email_confirmed_at: z.boolean().optional(),
|
||||||
|
encrypted_password: z.string().optional(),
|
||||||
|
role: z.enum(["user", "staff", "admin"]).optional(),
|
||||||
|
phone: z.string().optional(),
|
||||||
|
phone_confirmed_at: z.boolean().optional(),
|
||||||
|
invited_at: z.union([z.string(), z.date()]).optional(),
|
||||||
|
confirmed_at: z.union([z.string(), z.date()]).optional(),
|
||||||
|
// recovery_sent_at: z.union([z.string(), z.date()]).optional(),
|
||||||
|
last_sign_in_at: z.union([z.string(), z.date()]).optional(),
|
||||||
|
created_at: z.union([z.string(), z.date()]).optional(),
|
||||||
|
updated_at: z.union([z.string(), z.date()]).optional(),
|
||||||
|
is_anonymous: z.boolean().optional(),
|
||||||
|
user_metadata: z.record(z.any()).optional(),
|
||||||
|
app_metadata: z.record(z.any()).optional(),
|
||||||
|
profile: z
|
||||||
|
.object({
|
||||||
|
avatar: z.string().optional(),
|
||||||
|
username: z.string().optional(),
|
||||||
|
first_name: z.string().optional(),
|
||||||
|
last_name: z.string().optional(),
|
||||||
|
bio: z.string().optional(),
|
||||||
|
address: z.any().optional(),
|
||||||
|
birth_date: z.date().optional(),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export type IUpdateUserSchema = z.infer<typeof UpdateUserSchema>;
|
||||||
|
|
||||||
|
export const defaulIUpdateUserSchemaValues: IUpdateUserSchema = {
|
||||||
|
email: "",
|
||||||
|
email_confirmed_at: false,
|
||||||
|
encrypted_password: "",
|
||||||
|
role: "user",
|
||||||
|
phone: "",
|
||||||
|
phone_confirmed_at: false,
|
||||||
|
invited_at: "",
|
||||||
|
confirmed_at: "",
|
||||||
|
last_sign_in_at: "",
|
||||||
|
created_at: "",
|
||||||
|
updated_at: "",
|
||||||
|
is_anonymous: false,
|
||||||
|
user_metadata: {},
|
||||||
|
app_metadata: {},
|
||||||
|
profile: {
|
||||||
|
avatar: "",
|
||||||
|
username: "",
|
||||||
|
first_name: "",
|
||||||
|
last_name: "",
|
||||||
|
bio: "",
|
||||||
|
address: "",
|
||||||
|
birth_date: new Date(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CredentialUpdateUserSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type ICredentialUpdateUserSchema = z.infer<typeof CredentialUpdateUserSchema>
|
|
@ -66,7 +66,7 @@ export const UserSchema = z.object({
|
||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type User = z.infer<typeof UserSchema>;
|
export type IUserSchema = z.infer<typeof UserSchema>;
|
||||||
|
|
||||||
export const ProfileSchema = z.object({
|
export const ProfileSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
|
@ -80,59 +80,30 @@ export const ProfileSchema = z.object({
|
||||||
birth_date: z.string().optional(),
|
birth_date: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Profile = z.infer<typeof ProfileSchema>;
|
export type IProfileSchema = z.infer<typeof ProfileSchema>;
|
||||||
|
|
||||||
export const CreateUserSchema = z.object({
|
// export type UserFilterOptions = {
|
||||||
email: z.string().email(),
|
// email: string
|
||||||
password: z.string().min(8),
|
// phone: string
|
||||||
phone: z.string().optional(),
|
// lastSignIn: string
|
||||||
email_confirm: z.boolean().optional(),
|
// createdAt: string
|
||||||
});
|
// status: string[]
|
||||||
|
// }
|
||||||
|
|
||||||
export type CreateUser = z.infer<typeof CreateUserSchema>;
|
export const UserFilterOptionsSchema = z.object({
|
||||||
|
email: z.string(),
|
||||||
|
phone: z.string(),
|
||||||
|
lastSignIn: z.string(),
|
||||||
|
createdAt: z.string(),
|
||||||
|
status: z.array(z.string()),
|
||||||
|
})
|
||||||
|
|
||||||
export const UpdateUserSchema = z.object({
|
export type IUserFilterOptionsSchema = z.infer<typeof UserFilterOptionsSchema>;
|
||||||
email: z.string().email().optional(),
|
|
||||||
email_confirmed_at: z.boolean().optional(),
|
|
||||||
encrypted_password: z.string().optional(),
|
|
||||||
role: z.enum(["user", "staff", "admin"]).optional(),
|
|
||||||
phone: z.string().optional(),
|
|
||||||
phone_confirmed_at: z.boolean().optional(),
|
|
||||||
invited_at: z.union([z.string(), z.date()]).optional(),
|
|
||||||
confirmed_at: z.union([z.string(), z.date()]).optional(),
|
|
||||||
// recovery_sent_at: z.union([z.string(), z.date()]).optional(),
|
|
||||||
last_sign_in_at: z.union([z.string(), z.date()]).optional(),
|
|
||||||
created_at: z.union([z.string(), z.date()]).optional(),
|
|
||||||
updated_at: z.union([z.string(), z.date()]).optional(),
|
|
||||||
is_anonymous: z.boolean().optional(),
|
|
||||||
user_metadata: z.record(z.any()).optional(),
|
|
||||||
app_metadata: z.record(z.any()).optional(),
|
|
||||||
profile: z
|
|
||||||
.object({
|
|
||||||
id: z.string().optional(),
|
|
||||||
user_id: z.string(),
|
|
||||||
avatar: z.string().optional(),
|
|
||||||
username: z.string().optional(),
|
|
||||||
first_name: z.string().optional(),
|
|
||||||
last_name: z.string().optional(),
|
|
||||||
bio: z.string().optional(),
|
|
||||||
address: z.any().optional(),
|
|
||||||
birth_date: z.date().optional(),
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
export type UpdateUser = z.infer<typeof UpdateUserSchema>;
|
|
||||||
|
|
||||||
export const InviteUserSchema = z.object({
|
|
||||||
email: z.string().email(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type InviteUser = z.infer<typeof InviteUserSchema>;
|
|
||||||
|
|
||||||
export type UserResponse =
|
export type UserResponse =
|
||||||
| {
|
| {
|
||||||
data: {
|
data: {
|
||||||
user: User;
|
user: IUserSchema;
|
||||||
};
|
};
|
||||||
error: null;
|
error: null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,18 @@ import { ICrashReporterService } from "@/src/application/services/crash-reporter
|
||||||
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
|
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
|
||||||
import { createAdminClient } from "@/app/_utils/supabase/admin";
|
import { createAdminClient } from "@/app/_utils/supabase/admin";
|
||||||
import { createClient as createServerClient } from "@/app/_utils/supabase/server";
|
import { createClient as createServerClient } from "@/app/_utils/supabase/server";
|
||||||
import { CreateUser, UpdateUser, User, UserResponse } from "@/src/entities/models/users/users.model";
|
import { IUserSchema } from "@/src/entities/models/users/users.model";
|
||||||
import { ITransaction } from "@/src/entities/models/transaction.interface";
|
import { ITransaction } from "@/src/entities/models/transaction.interface";
|
||||||
import db from "@/prisma/db";
|
import db from "@/prisma/db";
|
||||||
import { DatabaseOperationError, NotFoundError } from "@/src/entities/errors/common";
|
import { DatabaseOperationError, NotFoundError } from "@/src/entities/errors/common";
|
||||||
import { AuthenticationError } from "@/src/entities/errors/auth";
|
import { AuthenticationError } from "@/src/entities/errors/auth";
|
||||||
|
import { ICredentialGetUserByEmailSchema, ICredentialGetUserByUsernameSchema, IGetUserByIdSchema } from "@/src/entities/models/users/read-user.model";
|
||||||
|
import { ICredentialsInviteUserSchema } from "@/src/entities/models/users/invite-user.model";
|
||||||
|
import { ICredentialUpdateUserSchema, IUpdateUserSchema } from "@/src/entities/models/users/update-user.model";
|
||||||
|
import { ICredentialsDeleteUserSchema } from "@/src/entities/models/users/delete-user.model";
|
||||||
|
import { IBanUserSchema, ICredentialsBanUserSchema } from "@/src/entities/models/users/ban-user.model";
|
||||||
|
import { ICredentialsUnbanUserSchema } from "@/src/entities/models/users/unban-user.model";
|
||||||
|
import { ICreateUserSchema } from "@/src/entities/models/users/create-user.model";
|
||||||
|
|
||||||
export class UsersRepository implements IUsersRepository {
|
export class UsersRepository implements IUsersRepository {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -17,7 +24,7 @@ export class UsersRepository implements IUsersRepository {
|
||||||
private readonly supabaseServer = createServerClient()
|
private readonly supabaseServer = createServerClient()
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
async getUsers(): Promise<User[]> {
|
async getUsers(): Promise<IUserSchema[]> {
|
||||||
return await this.instrumentationService.startSpan({
|
return await this.instrumentationService.startSpan({
|
||||||
name: "UsersRepository > getUsers",
|
name: "UsersRepository > getUsers",
|
||||||
}, async () => {
|
}, async () => {
|
||||||
|
@ -40,7 +47,7 @@ export class UsersRepository implements IUsersRepository {
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!users) {
|
if (!users) {
|
||||||
throw new NotFoundError("Users not found");
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return users;
|
return users;
|
||||||
|
@ -51,14 +58,14 @@ export class UsersRepository implements IUsersRepository {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserById(id: string): Promise<User | undefined> {
|
async getUserById(credential: IGetUserByIdSchema): Promise<IUserSchema | undefined> {
|
||||||
return await this.instrumentationService.startSpan({
|
return await this.instrumentationService.startSpan({
|
||||||
name: "UsersRepository > getUserById",
|
name: "UsersRepository > getUserById",
|
||||||
}, async () => {
|
}, async () => {
|
||||||
try {
|
try {
|
||||||
const query = db.users.findUnique({
|
const query = db.users.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id: credential.id,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
profile: true,
|
profile: true,
|
||||||
|
@ -66,7 +73,7 @@ export class UsersRepository implements IUsersRepository {
|
||||||
})
|
})
|
||||||
|
|
||||||
const user = await this.instrumentationService.startSpan({
|
const user = await this.instrumentationService.startSpan({
|
||||||
name: `UsersRepository > getUserById > Prisma: db.users.findUnique(${id})`,
|
name: `UsersRepository > getUserById > Prisma: db.users.findUnique(${credential.id})`,
|
||||||
op: "db:query",
|
op: "db:query",
|
||||||
attributes: { "system": "prisma" },
|
attributes: { "system": "prisma" },
|
||||||
},
|
},
|
||||||
|
@ -81,7 +88,7 @@ export class UsersRepository implements IUsersRepository {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...user,
|
...user,
|
||||||
id,
|
id: credential.id,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.crashReporterService.report(err);
|
this.crashReporterService.report(err);
|
||||||
|
@ -90,7 +97,7 @@ export class UsersRepository implements IUsersRepository {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserByUsername(username: string): Promise<User | undefined> {
|
async getUserByUsername(credential: ICredentialGetUserByUsernameSchema): Promise<IUserSchema | undefined> {
|
||||||
return await this.instrumentationService.startSpan({
|
return await this.instrumentationService.startSpan({
|
||||||
name: "UsersRepository > getUserByUsername",
|
name: "UsersRepository > getUserByUsername",
|
||||||
}, async () => {
|
}, async () => {
|
||||||
|
@ -98,7 +105,7 @@ export class UsersRepository implements IUsersRepository {
|
||||||
const query = db.users.findFirst({
|
const query = db.users.findFirst({
|
||||||
where: {
|
where: {
|
||||||
profile: {
|
profile: {
|
||||||
username,
|
username: credential.username,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
|
@ -107,7 +114,7 @@ export class UsersRepository implements IUsersRepository {
|
||||||
})
|
})
|
||||||
|
|
||||||
const user = await this.instrumentationService.startSpan({
|
const user = await this.instrumentationService.startSpan({
|
||||||
name: `UsersRepository > getUserByUsername > Prisma: db.users.findFirst(${username})`,
|
name: `UsersRepository > getUserByUsername > Prisma: db.users.findFirst(${credential.username})`,
|
||||||
op: "db:query",
|
op: "db:query",
|
||||||
attributes: { "system": "prisma" },
|
attributes: { "system": "prisma" },
|
||||||
},
|
},
|
||||||
|
@ -130,7 +137,7 @@ export class UsersRepository implements IUsersRepository {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserByEmail(email: string): Promise<User | undefined> {
|
async getUserByEmail(credential: ICredentialGetUserByEmailSchema): Promise<IUserSchema | undefined> {
|
||||||
return await this.instrumentationService.startSpan({
|
return await this.instrumentationService.startSpan({
|
||||||
name: "UsersRepository > getUserByEmail",
|
name: "UsersRepository > getUserByEmail",
|
||||||
}, async () => {
|
}, async () => {
|
||||||
|
@ -138,7 +145,7 @@ export class UsersRepository implements IUsersRepository {
|
||||||
|
|
||||||
const query = db.users.findUnique({
|
const query = db.users.findUnique({
|
||||||
where: {
|
where: {
|
||||||
email,
|
email: credential.email,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
profile: true,
|
profile: true,
|
||||||
|
@ -146,7 +153,7 @@ export class UsersRepository implements IUsersRepository {
|
||||||
})
|
})
|
||||||
|
|
||||||
const user = await this.instrumentationService.startSpan({
|
const user = await this.instrumentationService.startSpan({
|
||||||
name: `UsersRepository > getUserByEmail > Prisma: db.users.findUnique(${email})`,
|
name: `UsersRepository > getUserByEmail > Prisma: db.users.findUnique(${credential.email})`,
|
||||||
op: "db:query",
|
op: "db:query",
|
||||||
attributes: { "system": "prisma" },
|
attributes: { "system": "prisma" },
|
||||||
},
|
},
|
||||||
|
@ -156,7 +163,7 @@ export class UsersRepository implements IUsersRepository {
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundError("User not found");
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
|
@ -167,7 +174,7 @@ export class UsersRepository implements IUsersRepository {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCurrentUser(): Promise<User> {
|
async getCurrentUser(): Promise<IUserSchema> {
|
||||||
return await this.instrumentationService.startSpan({
|
return await this.instrumentationService.startSpan({
|
||||||
name: "UsersRepository > getCurrentUser",
|
name: "UsersRepository > getCurrentUser",
|
||||||
}, async () => {
|
}, async () => {
|
||||||
|
@ -176,7 +183,7 @@ export class UsersRepository implements IUsersRepository {
|
||||||
|
|
||||||
const query = supabase.auth.getUser();
|
const query = supabase.auth.getUser();
|
||||||
|
|
||||||
const { data, error } = await this.instrumentationService.startSpan({
|
const { data: { user }, error } = await this.instrumentationService.startSpan({
|
||||||
name: "UsersRepository > getCurrentUser > supabase.auth.getUser",
|
name: "UsersRepository > getCurrentUser > supabase.auth.getUser",
|
||||||
op: "db:query",
|
op: "db:query",
|
||||||
attributes: { "system": "supabase.auth" },
|
attributes: { "system": "supabase.auth" },
|
||||||
|
@ -190,14 +197,11 @@ export class UsersRepository implements IUsersRepository {
|
||||||
throw new AuthenticationError("Failed to get current user");
|
throw new AuthenticationError("Failed to get current user");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data) {
|
if (!user) {
|
||||||
throw new NotFoundError("User not found");
|
throw new NotFoundError("User not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return user;
|
||||||
...data,
|
|
||||||
id: data.user.id,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.crashReporterService.report(err);
|
this.crashReporterService.report(err);
|
||||||
throw err;
|
throw err;
|
||||||
|
@ -205,14 +209,21 @@ export class UsersRepository implements IUsersRepository {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async createUser(input: CreateUser, tx?: ITransaction): Promise<User> {
|
async createUser(input: ICreateUserSchema, tx?: ITransaction): Promise<IUserSchema> {
|
||||||
return await this.instrumentationService.startSpan({
|
return await this.instrumentationService.startSpan({
|
||||||
name: "UsersRepository > createUser",
|
name: "UsersRepository > createUser",
|
||||||
}, async () => {
|
}, async () => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
console.log("Create User");
|
||||||
|
|
||||||
const supabase = this.supabaseAdmin;
|
const supabase = this.supabaseAdmin;
|
||||||
|
|
||||||
const query = supabase.auth.admin.createUser(input)
|
const query = supabase.auth.admin.createUser({
|
||||||
|
email: input.email,
|
||||||
|
password: input.password,
|
||||||
|
email_confirm: true,
|
||||||
|
})
|
||||||
|
|
||||||
const { data: { user } } = await this.instrumentationService.startSpan({
|
const { data: { user } } = await this.instrumentationService.startSpan({
|
||||||
name: "UsersRepository > createUser > supabase.auth.admin.createUser",
|
name: "UsersRepository > createUser > supabase.auth.admin.createUser",
|
||||||
|
@ -237,14 +248,14 @@ export class UsersRepository implements IUsersRepository {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async inviteUser(email: string, tx?: ITransaction): Promise<User> {
|
async inviteUser(credential: ICredentialsInviteUserSchema, tx?: ITransaction): Promise<IUserSchema> {
|
||||||
return await this.instrumentationService.startSpan({
|
return await this.instrumentationService.startSpan({
|
||||||
name: "UsersRepository > inviteUser",
|
name: "UsersRepository > inviteUser",
|
||||||
}, async () => {
|
}, async () => {
|
||||||
try {
|
try {
|
||||||
const supabase = this.supabaseAdmin;
|
const supabase = this.supabaseAdmin;
|
||||||
|
|
||||||
const query = supabase.auth.admin.inviteUserByEmail(email);
|
const query = supabase.auth.admin.inviteUserByEmail(credential.email);
|
||||||
|
|
||||||
const { data: { user } } = await this.instrumentationService.startSpan({
|
const { data: { user } } = await this.instrumentationService.startSpan({
|
||||||
name: "UsersRepository > inviteUser > supabase.auth.admin.inviteUserByEmail",
|
name: "UsersRepository > inviteUser > supabase.auth.admin.inviteUserByEmail",
|
||||||
|
@ -268,14 +279,14 @@ export class UsersRepository implements IUsersRepository {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUser(id: string, input: Partial<UpdateUser>, tx?: ITransaction): Promise<User> {
|
async updateUser(credential: ICredentialUpdateUserSchema, input: Partial<IUpdateUserSchema>, tx?: ITransaction): Promise<IUserSchema> {
|
||||||
return await this.instrumentationService.startSpan({
|
return await this.instrumentationService.startSpan({
|
||||||
name: "UsersRepository > updateUser",
|
name: "UsersRepository > updateUser",
|
||||||
}, async () => {
|
}, async () => {
|
||||||
try {
|
try {
|
||||||
const supabase = this.supabaseAdmin;
|
const supabase = this.supabaseAdmin;
|
||||||
|
|
||||||
const queryUpdateSupabaseUser = supabase.auth.admin.updateUserById(id, {
|
const queryUpdateSupabaseUser = supabase.auth.admin.updateUserById(credential.id, {
|
||||||
email: input.email,
|
email: input.email,
|
||||||
email_confirm: input.email_confirmed_at,
|
email_confirm: input.email_confirmed_at,
|
||||||
password: input.encrypted_password ?? undefined,
|
password: input.encrypted_password ?? undefined,
|
||||||
|
@ -303,7 +314,7 @@ export class UsersRepository implements IUsersRepository {
|
||||||
|
|
||||||
const queryGetUser = db.users.findUnique({
|
const queryGetUser = db.users.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id: credential.id,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
profile: true,
|
profile: true,
|
||||||
|
@ -326,7 +337,7 @@ export class UsersRepository implements IUsersRepository {
|
||||||
|
|
||||||
const queryUpdateUser = db.users.update({
|
const queryUpdateUser = db.users.update({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id: credential.id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
role: input.role || user.role,
|
role: input.role || user.role,
|
||||||
|
@ -369,7 +380,7 @@ export class UsersRepository implements IUsersRepository {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...updatedUser,
|
...updatedUser,
|
||||||
id,
|
id: credential.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -379,14 +390,14 @@ export class UsersRepository implements IUsersRepository {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteUser(id: string, tx?: ITransaction): Promise<User> {
|
async deleteUser(credential: ICredentialsDeleteUserSchema, tx?: ITransaction): Promise<IUserSchema> {
|
||||||
return await this.instrumentationService.startSpan({
|
return await this.instrumentationService.startSpan({
|
||||||
name: "UsersRepository > deleteUser",
|
name: "UsersRepository > deleteUser",
|
||||||
}, async () => {
|
}, async () => {
|
||||||
try {
|
try {
|
||||||
const supabase = this.supabaseAdmin;
|
const supabase = this.supabaseAdmin;
|
||||||
|
|
||||||
const query = supabase.auth.admin.deleteUser(id);
|
const query = supabase.auth.admin.deleteUser(credential.id);
|
||||||
|
|
||||||
const { data: user, error } = await this.instrumentationService.startSpan({
|
const { data: user, error } = await this.instrumentationService.startSpan({
|
||||||
name: "UsersRepository > deleteUser > supabase.auth.admin.deleteUser",
|
name: "UsersRepository > deleteUser > supabase.auth.admin.deleteUser",
|
||||||
|
@ -404,7 +415,7 @@ export class UsersRepository implements IUsersRepository {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...user,
|
...user,
|
||||||
id
|
id: credential.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -414,15 +425,15 @@ export class UsersRepository implements IUsersRepository {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async banUser(id: string, ban_duration: string, tx?: ITransaction): Promise<User> {
|
async banUser(credential: ICredentialsBanUserSchema, input: IBanUserSchema, tx?: ITransaction): Promise<IUserSchema> {
|
||||||
return await this.instrumentationService.startSpan({
|
return await this.instrumentationService.startSpan({
|
||||||
name: "UsersRepository > banUser",
|
name: "UsersRepository > banUser",
|
||||||
}, async () => {
|
}, async () => {
|
||||||
try {
|
try {
|
||||||
const supabase = this.supabaseAdmin;
|
const supabase = this.supabaseAdmin;
|
||||||
|
|
||||||
const query = supabase.auth.admin.updateUserById(id, {
|
const query = supabase.auth.admin.updateUserById(credential.id, {
|
||||||
ban_duration: ban_duration ?? "100h",
|
ban_duration: input.ban_duration ?? "24h",
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: user, error } = await this.instrumentationService.startSpan({
|
const { data: user, error } = await this.instrumentationService.startSpan({
|
||||||
|
@ -441,7 +452,7 @@ export class UsersRepository implements IUsersRepository {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...user,
|
...user,
|
||||||
id
|
id: credential.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -452,14 +463,14 @@ export class UsersRepository implements IUsersRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async unbanUser(id: string, tx?: ITransaction): Promise<User> {
|
async unbanUser(credential: ICredentialsUnbanUserSchema, tx?: ITransaction): Promise<IUserSchema> {
|
||||||
return await this.instrumentationService.startSpan({
|
return await this.instrumentationService.startSpan({
|
||||||
name: "UsersRepository > unbanUser",
|
name: "UsersRepository > unbanUser",
|
||||||
}, async () => {
|
}, async () => {
|
||||||
try {
|
try {
|
||||||
const supabase = this.supabaseAdmin;
|
const supabase = this.supabaseAdmin;
|
||||||
|
|
||||||
const query = supabase.auth.admin.updateUserById(id, {
|
const query = supabase.auth.admin.updateUserById(credential.id, {
|
||||||
ban_duration: "none",
|
ban_duration: "none",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -479,7 +490,7 @@ export class UsersRepository implements IUsersRepository {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...user,
|
...user,
|
||||||
id
|
id: credential.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -6,11 +6,13 @@ import { IAuthenticationService } from "@/src/application/services/authenticatio
|
||||||
import { ICrashReporterService } from "@/src/application/services/crash-reporter.service.interface";
|
import { ICrashReporterService } from "@/src/application/services/crash-reporter.service.interface";
|
||||||
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
|
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
|
||||||
import { AuthenticationError } from "@/src/entities/errors/auth";
|
import { AuthenticationError } from "@/src/entities/errors/auth";
|
||||||
|
import { ISendMagicLinkSchema } from "@/src/entities/models/auth/send-magic-link.model";
|
||||||
|
import { ISendPasswordRecoverySchema } from "@/src/entities/models/auth/send-password-recovery.model";
|
||||||
import { Session } from "@/src/entities/models/auth/session.model";
|
import { Session } from "@/src/entities/models/auth/session.model";
|
||||||
import { SignInPasswordless, SignInWithPassword } from "@/src/entities/models/auth/sign-in.model";
|
import { SignInWithPasswordSchema, ISignInPasswordlessSchema, ISignInWithPasswordSchema } from "@/src/entities/models/auth/sign-in.model";
|
||||||
import { SignUpWithEmail, SignUpWithPhone } from "@/src/entities/models/auth/sign-up.model";
|
import { SignUpWithEmailSchema, SignUpWithPhoneSchema, ISignUpWithEmailSchema, ISignUpWithPhoneSchema } from "@/src/entities/models/auth/sign-up.model";
|
||||||
import { VerifyOtpFormData } from "@/src/entities/models/auth/verify-otp.model";
|
import { IVerifyOtpSchema } from "@/src/entities/models/auth/verify-otp.model";
|
||||||
import { User } from "@/src/entities/models/users/users.model";
|
import { IUserSchema } from "@/src/entities/models/users/users.model";
|
||||||
|
|
||||||
export class AuthenticationService implements IAuthenticationService {
|
export class AuthenticationService implements IAuthenticationService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -21,7 +23,7 @@ export class AuthenticationService implements IAuthenticationService {
|
||||||
private readonly supabaseServer = createClient()
|
private readonly supabaseServer = createClient()
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
async signInPasswordless(credentials: SignInPasswordless): Promise<void> {
|
async signInPasswordless(credentials: ISignInPasswordlessSchema): Promise<void> {
|
||||||
return await this.instrumentationService.startSpan({
|
return await this.instrumentationService.startSpan({
|
||||||
name: "signInPasswordless Use Case",
|
name: "signInPasswordless Use Case",
|
||||||
}, async () => {
|
}, async () => {
|
||||||
|
@ -49,9 +51,9 @@ export class AuthenticationService implements IAuthenticationService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async signInWithPassword(credentials: SignInWithPassword): Promise<void> {
|
async SignInWithPasswordSchema(credentials: ISignInWithPasswordSchema): Promise<void> {
|
||||||
return await this.instrumentationService.startSpan({
|
return await this.instrumentationService.startSpan({
|
||||||
name: "signInWithPassword Use Case",
|
name: "SignInWithPasswordSchema Use Case",
|
||||||
}, async () => {
|
}, async () => {
|
||||||
try {
|
try {
|
||||||
const supabase = await this.supabaseServer
|
const supabase = await this.supabaseServer
|
||||||
|
@ -77,9 +79,9 @@ export class AuthenticationService implements IAuthenticationService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async signUpWithEmail(credentials: SignUpWithEmail): Promise<User> {
|
async SignUpWithEmailSchema(credentials: ISignUpWithEmailSchema): Promise<IUserSchema> {
|
||||||
return await this.instrumentationService.startSpan({
|
return await this.instrumentationService.startSpan({
|
||||||
name: "signUpWithEmail Use Case",
|
name: "SignUpWithEmailSchema Use Case",
|
||||||
}, async () => {
|
}, async () => {
|
||||||
try {
|
try {
|
||||||
const supabase = await this.supabaseServer
|
const supabase = await this.supabaseServer
|
||||||
|
@ -122,7 +124,7 @@ export class AuthenticationService implements IAuthenticationService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async signUpWithPhone(credentials: SignUpWithPhone): Promise<User> {
|
async SignUpWithPhoneSchema(credentials: ISignUpWithPhoneSchema): Promise<IUserSchema> {
|
||||||
throw new Error("Method not implemented.");
|
throw new Error("Method not implemented.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,14 +192,14 @@ export class AuthenticationService implements IAuthenticationService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendMagicLink(email: string): Promise<void> {
|
async sendMagicLink(credentials: ISendMagicLinkSchema): Promise<void> {
|
||||||
return await this.instrumentationService.startSpan({
|
return await this.instrumentationService.startSpan({
|
||||||
name: "sendMagicLink Use Case",
|
name: "sendMagicLink Use Case",
|
||||||
}, async () => {
|
}, async () => {
|
||||||
try {
|
try {
|
||||||
const supabase = await this.supabaseServer
|
const supabase = await this.supabaseServer
|
||||||
|
|
||||||
const magicLink = supabase.auth.signInWithOtp({ email })
|
const magicLink = supabase.auth.signInWithOtp({ email: credentials.email })
|
||||||
|
|
||||||
await this.instrumentationService.startSpan({
|
await this.instrumentationService.startSpan({
|
||||||
name: "supabase.auth.signIn",
|
name: "supabase.auth.signIn",
|
||||||
|
@ -216,14 +218,14 @@ export class AuthenticationService implements IAuthenticationService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendPasswordRecovery(email: string): Promise<void> {
|
async sendPasswordRecovery(credentials: ISendPasswordRecoverySchema): Promise<void> {
|
||||||
return await this.instrumentationService.startSpan({
|
return await this.instrumentationService.startSpan({
|
||||||
name: "sendPasswordRecovery Use Case",
|
name: "sendPasswordRecovery Use Case",
|
||||||
}, async () => {
|
}, async () => {
|
||||||
try {
|
try {
|
||||||
const supabase = await this.supabaseServer
|
const supabase = await this.supabaseServer
|
||||||
|
|
||||||
const passwordRecovery = supabase.auth.resetPasswordForEmail(email, {
|
const passwordRecovery = supabase.auth.resetPasswordForEmail(credentials.email, {
|
||||||
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/reset-password`,
|
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/reset-password`,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -244,7 +246,7 @@ export class AuthenticationService implements IAuthenticationService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyOtp(credentials: VerifyOtpFormData): Promise<void> {
|
async verifyOtp(credentials: IVerifyOtpSchema): Promise<void> {
|
||||||
return await this.instrumentationService.startSpan({
|
return await this.instrumentationService.startSpan({
|
||||||
name: "verifyOtp Use Case",
|
name: "verifyOtp Use Case",
|
||||||
}, async () => {
|
}, async () => {
|
||||||
|
|
|
@ -1,123 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useMutation } from '@tanstack/react-query';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { SignInFormData } from '@/src/entities/models/auth/sign-in.model';
|
|
||||||
import { VerifyOtpFormData } from '@/src/entities/models/auth/verify-otp.model';
|
|
||||||
import { useNavigations } from '@/app/_hooks/use-navigations';
|
|
||||||
import { AuthenticationError } from '@/src/entities/errors/auth';
|
|
||||||
import * as authRepository from '@/src/application/repositories/authentication.repository';
|
|
||||||
|
|
||||||
export function useAuthActions() {
|
|
||||||
const { router } = useNavigations();
|
|
||||||
|
|
||||||
// Sign In Mutation
|
|
||||||
const signInMutation = useMutation({
|
|
||||||
mutationFn: async (data: SignInFormData) => {
|
|
||||||
return await authRepository.signIn(data);
|
|
||||||
},
|
|
||||||
onSuccess: (result) => {
|
|
||||||
toast.success(result.message);
|
|
||||||
if (result.redirectTo && result.success) {
|
|
||||||
router.push(result.redirectTo);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
if (error instanceof AuthenticationError) {
|
|
||||||
toast.error(`Authentication failed: ${error.message}`);
|
|
||||||
} else {
|
|
||||||
toast.error('Failed to sign in. Please try again later.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify OTP Mutation
|
|
||||||
const verifyOtpMutation = useMutation({
|
|
||||||
mutationFn: async (data: VerifyOtpFormData) => {
|
|
||||||
return await authRepository.verifyOtp(data);
|
|
||||||
},
|
|
||||||
onSuccess: (result) => {
|
|
||||||
toast.success(result.message);
|
|
||||||
if (result.redirectTo) {
|
|
||||||
router.push(result.redirectTo);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
if (error instanceof AuthenticationError) {
|
|
||||||
toast.error(`Verification failed: ${error.message}`);
|
|
||||||
} else {
|
|
||||||
toast.error('Failed to verify OTP. Please try again.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sign Out Mutation
|
|
||||||
const signOutMutation = useMutation({
|
|
||||||
mutationFn: async () => {
|
|
||||||
return await authRepository.signOut();
|
|
||||||
},
|
|
||||||
onSuccess: (result) => {
|
|
||||||
toast.success(result.message);
|
|
||||||
if (result.redirectTo) {
|
|
||||||
router.push(result.redirectTo);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error('Failed to sign out. Please try again.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Password Recovery Mutation
|
|
||||||
const passwordRecoveryMutation = useMutation({
|
|
||||||
mutationFn: async (email: string) => {
|
|
||||||
return await authRepository.sendPasswordRecovery(email);
|
|
||||||
},
|
|
||||||
onSuccess: (result) => {
|
|
||||||
toast.success(result.message);
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error('Failed to send password recovery email. Please try again.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Magic Link Mutation
|
|
||||||
const magicLinkMutation = useMutation({
|
|
||||||
mutationFn: async (email: string) => {
|
|
||||||
return await authRepository.sendMagicLink(email);
|
|
||||||
},
|
|
||||||
onSuccess: (result) => {
|
|
||||||
toast.success(result.message);
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error('Failed to send magic link email. Please try again.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
signIn: {
|
|
||||||
mutate: signInMutation.mutateAsync,
|
|
||||||
isPending: signInMutation.isPending,
|
|
||||||
error: signInMutation.error,
|
|
||||||
},
|
|
||||||
verifyOtp: {
|
|
||||||
mutate: verifyOtpMutation.mutateAsync,
|
|
||||||
isPending: verifyOtpMutation.isPending,
|
|
||||||
error: verifyOtpMutation.error,
|
|
||||||
},
|
|
||||||
signOut: {
|
|
||||||
mutate: signOutMutation.mutateAsync,
|
|
||||||
isPending: signOutMutation.isPending,
|
|
||||||
error: signOutMutation.error,
|
|
||||||
},
|
|
||||||
passwordRecovery: {
|
|
||||||
mutate: passwordRecoveryMutation.mutateAsync,
|
|
||||||
isPending: passwordRecoveryMutation.isPending,
|
|
||||||
error: passwordRecoveryMutation.error,
|
|
||||||
},
|
|
||||||
magicLink: {
|
|
||||||
mutate: magicLinkMutation.mutateAsync,
|
|
||||||
isPending: magicLinkMutation.isPending,
|
|
||||||
error: magicLinkMutation.error,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
|
||||||
|
import { ISendMagicLinkUseCase } from "@/src/application/use-cases/auth/send-magic-link.use-case"
|
||||||
|
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { InputParseError } from "@/src/entities/errors/common"
|
||||||
|
|
||||||
|
const sendMagicLinkInputSchema = z.object({
|
||||||
|
email: z.string().email("Please enter a valid email address"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type ISendMagicLinkController = ReturnType<typeof sendMagicLinkController>
|
||||||
|
|
||||||
|
export const sendMagicLinkController =
|
||||||
|
(
|
||||||
|
instrumentationService: IInstrumentationService,
|
||||||
|
sendMagicLinkUseCase: ISendMagicLinkUseCase
|
||||||
|
) =>
|
||||||
|
async (input: Partial<z.infer<typeof sendMagicLinkInputSchema>>) => {
|
||||||
|
return await instrumentationService.startSpan({ name: "sendMagicLink Controller" },
|
||||||
|
async () => {
|
||||||
|
const { data, error: inputParseError } = sendMagicLinkInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
if (inputParseError) {
|
||||||
|
throw new InputParseError("Invalid data", { cause: inputParseError })
|
||||||
|
}
|
||||||
|
|
||||||
|
return await sendMagicLinkUseCase({
|
||||||
|
email: data.email
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
|
||||||
|
import { ISendPasswordRecoveryUseCase } from "@/src/application/use-cases/auth/send-password-recovery.use-case"
|
||||||
|
import { InputParseError } from "@/src/entities/errors/common"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
const sendPasswordRecoveryInputSchema = z.object({
|
||||||
|
email: z.string().email("Please enter a valid email address"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type ISendPasswordRecoveryController = ReturnType<typeof sendPasswordRecoveryController>
|
||||||
|
|
||||||
|
export const sendPasswordRecoveryController =
|
||||||
|
(
|
||||||
|
instrumentationService: IInstrumentationService,
|
||||||
|
sendPasswordRecoveryUseCase: ISendPasswordRecoveryUseCase
|
||||||
|
) =>
|
||||||
|
async (input: Partial<z.infer<typeof sendPasswordRecoveryInputSchema>>) => {
|
||||||
|
return await instrumentationService.startSpan({ name: "sendPasswordRecovery Controller" },
|
||||||
|
async () => {
|
||||||
|
const { data, error: inputParseError } = sendPasswordRecoveryInputSchema.safeParse(input)
|
||||||
|
|
||||||
|
if (inputParseError) {
|
||||||
|
throw new InputParseError("Invalid data", { cause: inputParseError })
|
||||||
|
}
|
||||||
|
|
||||||
|
return await sendPasswordRecoveryUseCase({
|
||||||
|
email: data.email
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ import { InputParseError } from "@/src/entities/errors/common";
|
||||||
|
|
||||||
// Sign In Controller
|
// Sign In Controller
|
||||||
const signInInputSchema = z.object({
|
const signInInputSchema = z.object({
|
||||||
email: z.string().email("Please enter a valid email address"),
|
email: z.string().min(1, "Email is Required").email("Please enter a valid email address"),
|
||||||
})
|
})
|
||||||
|
|
||||||
export type ISignInController = ReturnType<typeof signInController>
|
export type ISignInController = ReturnType<typeof signInController>
|
||||||
|
@ -20,7 +20,7 @@ export const signInController =
|
||||||
const { data, error: inputParseError } = signInInputSchema.safeParse(input)
|
const { data, error: inputParseError } = signInInputSchema.safeParse(input)
|
||||||
|
|
||||||
if (inputParseError) {
|
if (inputParseError) {
|
||||||
throw new InputParseError("Invalid data", { cause: inputParseError })
|
throw new InputParseError(inputParseError.errors[0].message)
|
||||||
}
|
}
|
||||||
|
|
||||||
return await signInUseCase({
|
return await signInUseCase({
|
||||||
|
|
|
@ -1,27 +1,38 @@
|
||||||
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
|
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
|
||||||
import { IBanUserUseCase } from "@/src/application/use-cases/users/ban-user.use-case";
|
import { IBanUserUseCase } from "@/src/application/use-cases/users/ban-user.use-case";
|
||||||
|
import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case";
|
||||||
import { InputParseError } from "@/src/entities/errors/common";
|
import { InputParseError } from "@/src/entities/errors/common";
|
||||||
|
import { BanDurationSchema, ICredentialsBanUserSchema } from "@/src/entities/models/users/ban-user.model";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const inputSchema = z.object({
|
const inputSchema = z.object({
|
||||||
id: z.string(),
|
ban_duration: BanDurationSchema
|
||||||
ban_duration: z.string()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export type IBanUserController = ReturnType<typeof banUserController>
|
export type IBanUserController = ReturnType<typeof banUserController>
|
||||||
|
|
||||||
export const banUserController = (
|
export const banUserController = (
|
||||||
instrumentationService: IInstrumentationService,
|
instrumentationService: IInstrumentationService,
|
||||||
banUserUseCase: IBanUserUseCase
|
banUserUseCase: IBanUserUseCase,
|
||||||
|
getCurrentUserUseCase: IGetCurrentUserUseCase
|
||||||
) =>
|
) =>
|
||||||
async (input: Partial<z.infer<typeof inputSchema>>) => {
|
async (credential: ICredentialsBanUserSchema, input: Partial<z.infer<typeof inputSchema>>) => {
|
||||||
return await instrumentationService.startSpan({ name: "banUser Controller" }, async () => {
|
return await instrumentationService.startSpan({ name: "banUser Controller" }, async () => {
|
||||||
|
|
||||||
|
const session = await getCurrentUserUseCase();
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
throw new InputParseError("Must be logged in to ban a user");
|
||||||
|
}
|
||||||
|
|
||||||
const { data, error: inputParseError } = inputSchema.safeParse(input);
|
const { data, error: inputParseError } = inputSchema.safeParse(input);
|
||||||
|
|
||||||
if (inputParseError) {
|
if (inputParseError) {
|
||||||
throw new InputParseError("Invalid data", { cause: inputParseError });
|
throw new InputParseError("Invalid data", { cause: inputParseError });
|
||||||
}
|
}
|
||||||
|
|
||||||
return await banUserUseCase(data.id, data.ban_duration);
|
console.log("Controller: Ban User");
|
||||||
|
|
||||||
|
return await banUserUseCase({ id: credential.id }, { ban_duration: data.ban_duration });
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -2,11 +2,24 @@ import { IUsersRepository } from "@/src/application/repositories/users.repositor
|
||||||
import { IAuthenticationService } from "@/src/application/services/authentication.service.interface"
|
import { IAuthenticationService } from "@/src/application/services/authentication.service.interface"
|
||||||
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
|
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
|
||||||
import { ICreateUserUseCase } from "@/src/application/use-cases/users/create-user.use-case"
|
import { ICreateUserUseCase } from "@/src/application/use-cases/users/create-user.use-case"
|
||||||
|
import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case"
|
||||||
import { UnauthenticatedError } from "@/src/entities/errors/auth"
|
import { UnauthenticatedError } from "@/src/entities/errors/auth"
|
||||||
import { InputParseError } from "@/src/entities/errors/common"
|
import { InputParseError } from "@/src/entities/errors/common"
|
||||||
import { CreateUserSchema } from "@/src/entities/models/users/users.model"
|
import { CreateUserSchema } from "@/src/entities/models/users/create-user.model"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
|
// const inputSchema = z.object({
|
||||||
|
// email: z.string().min(1, { message: "Email is required" }).email({ message: "Please enter a valid email address" }),
|
||||||
|
// password: z
|
||||||
|
// .string()
|
||||||
|
// .min(1, { message: "Password is required" })
|
||||||
|
// .min(8, { message: "Password must be at least 8 characters" })
|
||||||
|
// .regex(/[A-Z]/, { message: "Password must contain at least one uppercase letter" })
|
||||||
|
// .regex(/[a-z]/, { message: "Password must contain at least one lowercase letter" })
|
||||||
|
// .regex(/[0-9]/, { message: "Password must contain at least one number" }),
|
||||||
|
// email_confirm: z.boolean().optional(),
|
||||||
|
// })
|
||||||
|
|
||||||
const inputSchema = CreateUserSchema
|
const inputSchema = CreateUserSchema
|
||||||
|
|
||||||
export type ICreateUserController = ReturnType<typeof createUserController>
|
export type ICreateUserController = ReturnType<typeof createUserController>
|
||||||
|
@ -14,20 +27,23 @@ export type ICreateUserController = ReturnType<typeof createUserController>
|
||||||
export const createUserController = (
|
export const createUserController = (
|
||||||
instrumentationService: IInstrumentationService,
|
instrumentationService: IInstrumentationService,
|
||||||
createUserUseCase: ICreateUserUseCase,
|
createUserUseCase: ICreateUserUseCase,
|
||||||
authenticationService: IAuthenticationService
|
getCurrentUserUseCase: IGetCurrentUserUseCase
|
||||||
) => async (input: Partial<z.infer<typeof inputSchema>>) => {
|
) => async (input: Partial<z.infer<typeof inputSchema>>) => {
|
||||||
|
return await instrumentationService.startSpan({ name: "createUser Controller" }, async () => {
|
||||||
|
|
||||||
const session = await authenticationService.getSession()
|
const session = await getCurrentUserUseCase()
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new UnauthenticatedError("Must be logged in to create a todo")
|
throw new UnauthenticatedError("Must be logged in to create a user")
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, error: inputParseError } = inputSchema.safeParse(input)
|
const { data, error: inputParseError } = inputSchema.safeParse(input)
|
||||||
|
|
||||||
if (inputParseError) {
|
if (inputParseError) {
|
||||||
throw new InputParseError("Invalid data", { cause: inputParseError })
|
throw new InputParseError(inputParseError.errors[0].message)
|
||||||
}
|
}
|
||||||
|
|
||||||
return await createUserUseCase(data);
|
return await createUserUseCase(data);
|
||||||
|
|
||||||
|
})
|
||||||
}
|
}
|
|
@ -1,7 +1,10 @@
|
||||||
|
import { IUsersRepository } from "@/src/application/repositories/users.repository.interface"
|
||||||
import { IAuthenticationService } from "@/src/application/services/authentication.service.interface"
|
import { IAuthenticationService } from "@/src/application/services/authentication.service.interface"
|
||||||
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
|
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
|
||||||
import { IDeleteUserUseCase } from "@/src/application/use-cases/users/delete-user.use-case"
|
import { IDeleteUserUseCase } from "@/src/application/use-cases/users/delete-user.use-case"
|
||||||
|
import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case"
|
||||||
import { UnauthenticatedError } from "@/src/entities/errors/auth"
|
import { UnauthenticatedError } from "@/src/entities/errors/auth"
|
||||||
|
import { ICredentialsDeleteUserSchema } from "@/src/entities/models/users/delete-user.model"
|
||||||
|
|
||||||
export type IDeleteUserController = ReturnType<typeof deleteUserController>
|
export type IDeleteUserController = ReturnType<typeof deleteUserController>
|
||||||
|
|
||||||
|
@ -9,17 +12,17 @@ export const deleteUserController =
|
||||||
(
|
(
|
||||||
instrumentationService: IInstrumentationService,
|
instrumentationService: IInstrumentationService,
|
||||||
deleteUserUseCase: IDeleteUserUseCase,
|
deleteUserUseCase: IDeleteUserUseCase,
|
||||||
authenticationService: IAuthenticationService
|
getCurrentUserUseCase: IGetCurrentUserUseCase
|
||||||
) =>
|
) =>
|
||||||
async (id: string) => {
|
async (credential: ICredentialsDeleteUserSchema) => {
|
||||||
return await instrumentationService.startSpan({ name: "deleteUser Controller" }, async () => {
|
return await instrumentationService.startSpan({ name: "deleteUser Controller" }, async () => {
|
||||||
|
|
||||||
const session = await authenticationService.getSession()
|
const session = await getCurrentUserUseCase()
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new UnauthenticatedError("Must be logged in to create a todo")
|
throw new UnauthenticatedError("Must be logged in to create a user")
|
||||||
}
|
}
|
||||||
|
|
||||||
return await deleteUserUseCase(id);
|
return await deleteUserUseCase(credential);
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -22,6 +22,6 @@ export const getUserByEmailController =
|
||||||
throw new InputParseError("Invalid data", { cause: inputParseError });
|
throw new InputParseError("Invalid data", { cause: inputParseError });
|
||||||
}
|
}
|
||||||
|
|
||||||
return await getUserByEmailUseCase(data.email);
|
return await getUserByEmailUseCase({ email: data.email });
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -21,6 +21,6 @@ export const getUserByIdController = (
|
||||||
throw new InputParseError("Invalid data", { cause: inputParseError });
|
throw new InputParseError("Invalid data", { cause: inputParseError });
|
||||||
}
|
}
|
||||||
|
|
||||||
return await getUserByIdUseCase(data.id);
|
return await getUserByIdUseCase({ id: data.id });
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -22,6 +22,6 @@ export const getUserByUsernameController =
|
||||||
throw new InputParseError("Invalid data", { cause: inputParseError });
|
throw new InputParseError("Invalid data", { cause: inputParseError });
|
||||||
}
|
}
|
||||||
|
|
||||||
return await getUserByUsernameUseCase(data.username);
|
return await getUserByUsernameUseCase({ username: data.username });
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -1,15 +1,16 @@
|
||||||
import { IUsersRepository } from "@/src/application/repositories/users.repository.interface"
|
import { IUsersRepository } from "@/src/application/repositories/users.repository.interface"
|
||||||
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
|
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
|
||||||
|
import { IGetUsersUseCase } from "@/src/application/use-cases/users/get-users.use-case"
|
||||||
|
|
||||||
export type IGetUserController = ReturnType<typeof getUsersController>
|
export type IGetUsersController = ReturnType<typeof getUsersController>
|
||||||
|
|
||||||
export const getUsersController =
|
export const getUsersController =
|
||||||
(
|
(
|
||||||
instrumentationService: IInstrumentationService,
|
instrumentationService: IInstrumentationService,
|
||||||
usersRepository: IUsersRepository
|
getUsersUseCase: IGetUsersUseCase
|
||||||
) =>
|
) =>
|
||||||
async () => {
|
async () => {
|
||||||
return await instrumentationService.startSpan({ name: "getgetUsers Controller" }, async () => {
|
return await instrumentationService.startSpan({ name: "geIGetUsers Controller" }, async () => {
|
||||||
return await usersRepository.getUsers();
|
return await getUsersUseCase()
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
|
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
|
||||||
|
import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case";
|
||||||
import { IInviteUserUseCase } from "@/src/application/use-cases/users/invite-user.use-case";
|
import { IInviteUserUseCase } from "@/src/application/use-cases/users/invite-user.use-case";
|
||||||
import { InputParseError } from "@/src/entities/errors/common";
|
import { InputParseError } from "@/src/entities/errors/common";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
@ -12,17 +13,25 @@ export type IInviteUserController = ReturnType<typeof inviteUserController>
|
||||||
export const inviteUserController =
|
export const inviteUserController =
|
||||||
(
|
(
|
||||||
instrumentationService: IInstrumentationService,
|
instrumentationService: IInstrumentationService,
|
||||||
inviteUserUseCase: IInviteUserUseCase
|
inviteUserUseCase: IInviteUserUseCase,
|
||||||
|
getCurrentUserUseCase: IGetCurrentUserUseCase
|
||||||
) =>
|
) =>
|
||||||
async (input: Partial<z.infer<typeof inputSchema>>) => {
|
async (input: Partial<z.infer<typeof inputSchema>>) => {
|
||||||
return await instrumentationService.startSpan({ name: "inviteUser Controller" }, async () => {
|
return await instrumentationService.startSpan({ name: "inviteUser Controller" }, async () => {
|
||||||
|
|
||||||
|
const session = await getCurrentUserUseCase();
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
throw new InputParseError("Must be logged in to invite a user");
|
||||||
|
}
|
||||||
|
|
||||||
const { data, error: inputParseError } = inputSchema.safeParse(input);
|
const { data, error: inputParseError } = inputSchema.safeParse(input);
|
||||||
|
|
||||||
if (inputParseError) {
|
if (inputParseError) {
|
||||||
throw new InputParseError("Invalid data", { cause: inputParseError });
|
throw new InputParseError("Invalid data", { cause: inputParseError });
|
||||||
}
|
}
|
||||||
|
|
||||||
return await inviteUserUseCase(data.email);
|
return await inviteUserUseCase({ email: data.email });
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
|
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
|
||||||
|
import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case";
|
||||||
import { IUnbanUserUseCase } from "@/src/application/use-cases/users/unban-user.use-case";
|
import { IUnbanUserUseCase } from "@/src/application/use-cases/users/unban-user.use-case";
|
||||||
import { InputParseError } from "@/src/entities/errors/common";
|
import { InputParseError } from "@/src/entities/errors/common";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
@ -11,16 +12,24 @@ export type IUnbanUserController = ReturnType<typeof unbanUserController>
|
||||||
|
|
||||||
export const unbanUserController = (
|
export const unbanUserController = (
|
||||||
instrumentationService: IInstrumentationService,
|
instrumentationService: IInstrumentationService,
|
||||||
unbanUserUseCase: IUnbanUserUseCase
|
unbanUserUseCase: IUnbanUserUseCase,
|
||||||
|
getCurrentUserUseCase: IGetCurrentUserUseCase
|
||||||
) =>
|
) =>
|
||||||
async (input: Partial<z.infer<typeof inputSchema>>) => {
|
async (input: Partial<z.infer<typeof inputSchema>>) => {
|
||||||
return await instrumentationService.startSpan({ name: "unbanUser Controller" }, async () => {
|
return await instrumentationService.startSpan({ name: "unbanUser Controller" }, async () => {
|
||||||
|
|
||||||
|
const session = await getCurrentUserUseCase();
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
throw new InputParseError("Must be logged in to unban a user");
|
||||||
|
}
|
||||||
|
|
||||||
const { data, error: inputParseError } = inputSchema.safeParse(input);
|
const { data, error: inputParseError } = inputSchema.safeParse(input);
|
||||||
|
|
||||||
if (inputParseError) {
|
if (inputParseError) {
|
||||||
throw new InputParseError("Invalid data", { cause: inputParseError });
|
throw new InputParseError("Invalid data", { cause: inputParseError });
|
||||||
}
|
}
|
||||||
|
|
||||||
return await unbanUserUseCase(data.id);
|
return await unbanUserUseCase({ id: data.id });
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -1,9 +1,11 @@
|
||||||
import { IAuthenticationService } from "@/src/application/services/authentication.service.interface";
|
import { IAuthenticationService } from "@/src/application/services/authentication.service.interface";
|
||||||
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
|
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
|
||||||
|
import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case";
|
||||||
import { IUpdateUserUseCase } from "@/src/application/use-cases/users/update-user.use-case";
|
import { IUpdateUserUseCase } from "@/src/application/use-cases/users/update-user.use-case";
|
||||||
import { UnauthenticatedError } from "@/src/entities/errors/auth";
|
import { UnauthenticatedError } from "@/src/entities/errors/auth";
|
||||||
import { InputParseError } from "@/src/entities/errors/common";
|
import { InputParseError } from "@/src/entities/errors/common";
|
||||||
import { UpdateUser, UpdateUserSchema } from "@/src/entities/models/users/users.model";
|
import { UpdateUserSchema } from "@/src/entities/models/users/update-user.model";
|
||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const inputSchema = UpdateUserSchema
|
const inputSchema = UpdateUserSchema
|
||||||
|
@ -14,12 +16,12 @@ export const updateUserController =
|
||||||
(
|
(
|
||||||
instrumentationService: IInstrumentationService,
|
instrumentationService: IInstrumentationService,
|
||||||
updateUserUseCase: IUpdateUserUseCase,
|
updateUserUseCase: IUpdateUserUseCase,
|
||||||
authenticationService: IAuthenticationService
|
getCurrentUserUseCase: IGetCurrentUserUseCase
|
||||||
) =>
|
) =>
|
||||||
async (id: string, input: Partial<z.infer<typeof inputSchema>>,) => {
|
async (id: string, input: Partial<z.infer<typeof inputSchema>>,) => {
|
||||||
return await instrumentationService.startSpan({ name: "updateUser Controller" }, async () => {
|
return await instrumentationService.startSpan({ name: "updateUser Controller" }, async () => {
|
||||||
|
|
||||||
const session = await authenticationService.getSession()
|
const session = await getCurrentUserUseCase()
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new UnauthenticatedError("Must be logged in to create a todo")
|
throw new UnauthenticatedError("Must be logged in to create a todo")
|
||||||
|
@ -31,6 +33,6 @@ export const updateUserController =
|
||||||
throw new InputParseError("Invalid data", { cause: inputParseError })
|
throw new InputParseError("Invalid data", { cause: inputParseError })
|
||||||
}
|
}
|
||||||
|
|
||||||
return await updateUserUseCase(id, data);
|
return await updateUserUseCase({ id }, data);
|
||||||
})
|
})
|
||||||
}
|
}
|
Loading…
Reference in New Issue