implmentation clean architecture for auth services
This commit is contained in:
parent
aed9eba5d3
commit
1a39925c97
|
@ -19,7 +19,7 @@ import {
|
||||||
import type * as TablerIcons from "@tabler/icons-react";
|
import type * as TablerIcons from "@tabler/icons-react";
|
||||||
|
|
||||||
import { useNavigations } from "@/app/_hooks/use-navigations";
|
import { useNavigations } from "@/app/_hooks/use-navigations";
|
||||||
import { formatUrl } from "@/app/_utils/utils";
|
import { formatUrl } from "@/app/_utils/common";
|
||||||
|
|
||||||
interface SubSubItem {
|
interface SubSubItem {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ChevronsUpDown } from "lucide-react";
|
import { ChevronsUpDown, Loader2 } from "lucide-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
|
@ -27,6 +27,8 @@ import { IconLogout, IconSettings, IconSparkles } from "@tabler/icons-react";
|
||||||
import type { User } from "@/src/entities/models/users/users.model";
|
import type { User } 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 { 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: User | null }) {
|
||||||
const { isMobile } = useSidebar();
|
const { isMobile } = useSidebar();
|
||||||
|
@ -62,6 +64,65 @@ export function NavUser({ user }: { user: User | null }) {
|
||||||
// You might want to refresh the user data here
|
// You might want to refresh the user data here
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { handleSignOut, isPending, errors, error } = useSignOutHandler();
|
||||||
|
|
||||||
|
function LogoutButton({ handleSignOut, isPending }: { handleSignOut: () => void; isPending: boolean }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Dropdown Item */}
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setOpen(true); // Buka dialog saat diklik
|
||||||
|
}}
|
||||||
|
disabled={isPending}
|
||||||
|
className="space-x-2"
|
||||||
|
>
|
||||||
|
<IconLogout className="size-4" />
|
||||||
|
<span>Log out</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
{/* Alert Dialog */}
|
||||||
|
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Log out</AlertDialogTitle>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Are you sure you want to log out?
|
||||||
|
</AlertDialogDescription>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel onClick={() => setOpen(false)}>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={() => {
|
||||||
|
handleSignOut();
|
||||||
|
|
||||||
|
// Tutup dialog setelah tombol Log out diklik
|
||||||
|
if (!isPending) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="btn btn-primary"
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
{isPending ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="size-4" />
|
||||||
|
<span>Logging You Out...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span>Log out</span>
|
||||||
|
)}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
|
@ -131,10 +192,7 @@ export function NavUser({ user }: { user: User | null }) {
|
||||||
/>
|
/>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onSubmit={() => { }} className="space-x-2">
|
<LogoutButton handleSignOut={handleSignOut} isPending={isPending} />
|
||||||
<IconLogout className="size-4" />
|
|
||||||
<span>Log out</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
|
|
|
@ -7,34 +7,33 @@ import { Input } from "@/app/_components/ui/input";
|
||||||
import { SubmitButton } from "@/app/_components/submit-button";
|
import { SubmitButton } from "@/app/_components/submit-button";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { FormField } from "@/app/_components/form-field";
|
import { FormField } from "@/app/_components/form-field";
|
||||||
import { useSignInController } from "@/src/interface-adapters/controllers/auth/sign-in.controller";
|
// import { useSignInController } from "@/src/interface-adapters/controllers/auth/sign-in.controller";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { signIn } from "../action";
|
import { signIn } from "../action";
|
||||||
|
import { useSignInHandler } from "../handler";
|
||||||
|
|
||||||
export function SignInForm({
|
export function SignInForm({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentPropsWithoutRef<"form">) {
|
}: React.ComponentPropsWithoutRef<"form">) {
|
||||||
|
// const [error, setError] = useState<string>();
|
||||||
|
// const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const [error, setError] = useState<string>();
|
// const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
const [loading, setLoading] = useState(false);
|
// event.preventDefault();
|
||||||
|
// if (loading) return;
|
||||||
|
|
||||||
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
// const formData = new FormData(event.currentTarget);
|
||||||
event.preventDefault();
|
|
||||||
if (loading) return;
|
|
||||||
|
|
||||||
const formData = new FormData(event.currentTarget);
|
// setLoading(true);
|
||||||
|
// const res = await signIn(formData);
|
||||||
|
// if (res && res.error) {
|
||||||
|
// setError(res.error);
|
||||||
|
// }
|
||||||
|
// setLoading(false);
|
||||||
|
// };
|
||||||
|
|
||||||
setLoading(true);
|
const { isPending, handleSubmit, error, errors, clearError } = useSignInHandler();
|
||||||
const res = await signIn(formData);
|
|
||||||
if (res && res.error) {
|
|
||||||
setError(res.error);
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// const { register, isPending, handleSubmit, errors } = useSignInController();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -61,7 +60,7 @@ export function SignInForm({
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full bg-[#1C1C1C] text-white border-gray-800 hover:bg-[#2C2C2C] hover:border-gray-700"
|
className="w-full bg-[#1C1C1C] text-white border-gray-800 hover:bg-[#2C2C2C] hover:border-gray-700"
|
||||||
size="lg"
|
size="lg"
|
||||||
disabled={loading}
|
disabled={isPending}
|
||||||
>
|
>
|
||||||
<Lock className="mr-2 h-5 w-5" />
|
<Lock className="mr-2 h-5 w-5" />
|
||||||
Continue with SSO
|
Continue with SSO
|
||||||
|
@ -77,7 +76,7 @@ export function SignInForm({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={onSubmit} className="space-y-4" {...props} noValidate>
|
<form onSubmit={handleSubmit} className="space-y-4" {...props} noValidate>
|
||||||
<FormField
|
<FormField
|
||||||
label="Email"
|
label="Email"
|
||||||
input={
|
input={
|
||||||
|
@ -86,19 +85,19 @@ export function SignInForm({
|
||||||
type="email"
|
type="email"
|
||||||
name="email"
|
name="email"
|
||||||
placeholder="you@example.com"
|
placeholder="you@example.com"
|
||||||
className={`bg-[#1C1C1C] border-gray-800 ${error ? "ring-red-500 focus-visible:ring-red-500" : ""
|
className={`bg-[#1C1C1C] border-gray-800`}
|
||||||
}`}
|
error={!!errors}
|
||||||
disabled={loading}
|
disabled={isPending}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
error={error ? error : undefined}
|
error={error}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white"
|
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white"
|
||||||
size="lg"
|
size="lg"
|
||||||
disabled={loading}
|
disabled={isPending}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{isPending ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-5 w-5 animate-spin" />
|
<Loader2 className="h-5 w-5 animate-spin" />
|
||||||
Signing in...
|
Signing in...
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
InputOTP,
|
InputOTP,
|
||||||
InputOTPGroup,
|
InputOTPGroup,
|
||||||
InputOTPSlot,
|
InputOTPSlot,
|
||||||
} from "@/app/_components/ui/input-otp";
|
} from "@/app/_components/ui/input-otp";
|
||||||
import { SubmitButton } from "@/app/_components/submit-button";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
|
@ -14,16 +14,25 @@ import {
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/app/_components/ui/card";
|
} from "@/app/_components/ui/card";
|
||||||
import { cn } from "@/app/_lib/utils";
|
import { cn } from "@/app/_lib/utils";
|
||||||
import { useVerifyOtpController } from "@/src/interface-adapters/controllers/auth/verify-otp.controller";
|
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller } from "react-hook-form";
|
||||||
|
import { useVerifyOtpHandler } from "../handler";
|
||||||
|
import { Button } from "@/app/_components/ui/button";
|
||||||
|
|
||||||
interface VerifyOtpFormProps extends React.HTMLAttributes<HTMLDivElement> {}
|
interface VerifyOtpFormProps extends React.HTMLAttributes<HTMLDivElement> {}
|
||||||
|
|
||||||
export function VerifyOtpForm({ className, ...props }: VerifyOtpFormProps) {
|
export function VerifyOtpForm({ className, ...props }: VerifyOtpFormProps) {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const email = searchParams.get("email") || ""
|
const email = searchParams.get("email") || ""
|
||||||
const { control, register, isPending, handleSubmit, handleOtpChange, errors } = useVerifyOtpController(email)
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
handleOtpChange,
|
||||||
|
errors,
|
||||||
|
isPending
|
||||||
|
} = useVerifyOtpHandler(email)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||||
|
@ -59,34 +68,42 @@ export function VerifyOtpForm({ className, ...props }: VerifyOtpFormProps) {
|
||||||
</InputOTP>
|
</InputOTP>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{errors.token ? <div className="flex w-full justify-center text-red-400 text-center text-sm">{errors.token.message}</div> : <div className="flex w-full justify-center text-background text-center text-sm">
|
{errors.token ? (
|
||||||
Successfully verified!
|
<div className="flex w-full justify-center text-red-400 text-center text-sm">
|
||||||
</div>}
|
{errors.token.message}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex w-full justify-center text-background text-center text-sm">
|
||||||
|
Successfully verified!
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex w-full justify-center items-center text-gray-400 text-sm">
|
<div className="flex w-full justify-center items-center text-gray-400 text-sm">
|
||||||
Please enter the one-time password sent to {email}.
|
Please enter the one-time password sent to {email}.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<SubmitButton
|
<Button
|
||||||
|
type="submit"
|
||||||
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white"
|
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
>
|
>
|
||||||
{isPending ? (
|
{isPending ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-5 w-5 animate-spin" />
|
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||||
Verifying...
|
Verifying...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
"Verify OTP"
|
"Verify OTP"
|
||||||
)}
|
)}
|
||||||
</SubmitButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<div className="text-balance text-center text-xs text-gray-400 [&_a]:text-emerald-500 [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-emerald-400">
|
<div className="text-balance text-center text-xs text-gray-400 [&_a]:text-emerald-500 [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-emerald-400">
|
||||||
By clicking continue, you agree to our <a href="#">Terms of Service</a> and <a href="#">Privacy Policy</a>.
|
By clicking continue, you agree to Sigap's{" "}
|
||||||
|
<a href="#">Terms of Service</a> and <a href="#">Privacy Policy</a>.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,13 +14,17 @@ export async function signIn(formData: FormData) {
|
||||||
recordResponse: true
|
recordResponse: true
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
const email = formData.get("email")?.toString()
|
||||||
const email = formData.get("email")?.toString()
|
|
||||||
|
|
||||||
|
try {
|
||||||
const signInController = getInjection("ISignInController")
|
const signInController = getInjection("ISignInController")
|
||||||
await signInController({ email })
|
await signInController({ email })
|
||||||
|
|
||||||
if (email) redirect(`/verify-otp?email=${encodeURIComponent(email)}`)
|
// if (email) {
|
||||||
|
// redirect(`/verify-otp?email=${encodeURIComponent(email)}`)
|
||||||
|
// }
|
||||||
|
|
||||||
|
return { success: true }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (
|
if (
|
||||||
err instanceof InputParseError ||
|
err instanceof InputParseError ||
|
||||||
|
@ -31,6 +35,12 @@ export async function signIn(formData: FormData) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (err instanceof UnauthenticatedError) {
|
||||||
|
return {
|
||||||
|
error: 'User not found. Please tell your admin to create an account for you.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const crashReporterService = getInjection('ICrashReporterService');
|
const crashReporterService = getInjection('ICrashReporterService');
|
||||||
crashReporterService.report(err);
|
crashReporterService.report(err);
|
||||||
|
|
||||||
|
@ -75,15 +85,22 @@ export async function signOut() {
|
||||||
const signOutController = getInjection("ISignOutController")
|
const signOutController = getInjection("ISignOutController")
|
||||||
await signOutController()
|
await signOutController()
|
||||||
|
|
||||||
revalidatePath("/")
|
// revalidatePath("/")
|
||||||
redirect("/sign-in") // Updated to match your route
|
// redirect("/sign-in") // Updated to match your route
|
||||||
|
|
||||||
|
return { success: true }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// if (err instanceof AuthenticationError) {
|
||||||
|
// return {
|
||||||
|
// error: "An error occurred during sign out. Please try again later.",
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
const crashReporterService = getInjection("ICrashReporterService")
|
const crashReporterService = getInjection("ICrashReporterService")
|
||||||
crashReporterService.report(err)
|
crashReporterService.report(err)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
error: "An error occurred during sign out. Please try again later.",
|
error: "An error occurred during sign out. Please try again later.",
|
||||||
success: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -101,10 +118,12 @@ export async function verifyOtp(formData: FormData) {
|
||||||
const verifyOtpController = getInjection("IVerifyOtpController")
|
const verifyOtpController = getInjection("IVerifyOtpController")
|
||||||
await verifyOtpController({ email, token })
|
await verifyOtpController({ email, token })
|
||||||
|
|
||||||
redirect("/dashboard") // Updated to match your route
|
// redirect("/dashboard")
|
||||||
|
|
||||||
|
return { success: true }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof InputParseError || err instanceof AuthenticationError) {
|
if (err instanceof InputParseError || err instanceof AuthenticationError) {
|
||||||
return { error: err.message, success: false }
|
return { error: err.message }
|
||||||
}
|
}
|
||||||
|
|
||||||
const crashReporterService = getInjection("ICrashReporterService")
|
const crashReporterService = getInjection("ICrashReporterService")
|
||||||
|
@ -112,7 +131,6 @@ export async function verifyOtp(formData: FormData) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
error: "An error occurred during OTP verification. Please try again later.",
|
error: "An error occurred during OTP verification. Please try again later.",
|
||||||
success: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,176 @@
|
||||||
|
import { AuthenticationError } from "@/src/entities/errors/auth";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useAuthActions } from "./mutation";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { defaultSignInPasswordlessValues, SignInFormData, SignInPasswordless, SignInPasswordlessSchema, SignInSchema } from "@/src/entities/models/auth/sign-in.model";
|
||||||
|
import { createFormData } from "@/app/_utils/common";
|
||||||
|
import { useFormHandler } from "@/app/_hooks/use-form-handler";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { signIn } from "./action";
|
||||||
|
import { useNavigations } from "@/app/_hooks/use-navigations";
|
||||||
|
import { VerifyOtpFormData, verifyOtpSchema } from "@/src/entities/models/auth/verify-otp.model";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook untuk menangani proses sign in
|
||||||
|
*
|
||||||
|
* @returns {Object} Object berisi handler dan state untuk form sign in
|
||||||
|
* @example
|
||||||
|
* const { handleSubmit, isPending, error } = useSignInHandler();
|
||||||
|
* <form onSubmit={handleSubmit}>...</form>
|
||||||
|
*/
|
||||||
|
export function useSignInHandler() {
|
||||||
|
const { signIn } = useAuthActions();
|
||||||
|
const { router } = useNavigations();
|
||||||
|
|
||||||
|
const [error, setError] = useState<string>();
|
||||||
|
|
||||||
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (signIn.isPending) return;
|
||||||
|
|
||||||
|
setError(undefined);
|
||||||
|
|
||||||
|
const formData = new FormData(event.currentTarget);
|
||||||
|
const email = formData.get("email")?.toString()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await signIn.mutateAsync(formData, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast("An email has been sent to you. Please check your inbox.");
|
||||||
|
if (email) router.push(`/verify-otp?email=${encodeURIComponent(email)}`);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setError(error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
setError(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
// formData,
|
||||||
|
// handleChange,
|
||||||
|
handleSubmit,
|
||||||
|
error,
|
||||||
|
isPending: signIn.isPending,
|
||||||
|
errors: !!error || signIn.error,
|
||||||
|
clearError: () => setError(undefined)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function useVerifyOtpHandler(email: string) {
|
||||||
|
const { router } = useNavigations()
|
||||||
|
const { verifyOtp } = useAuthActions()
|
||||||
|
const [error, setError] = useState<string>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit: hookFormSubmit,
|
||||||
|
control,
|
||||||
|
formState: { errors },
|
||||||
|
setValue
|
||||||
|
} = useForm<VerifyOtpFormData>({
|
||||||
|
resolver: zodResolver(verifyOtpSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email,
|
||||||
|
token: ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleOtpChange = (value: string, onChange: (value: string) => void) => {
|
||||||
|
onChange(value)
|
||||||
|
|
||||||
|
// Clear error when user starts typing
|
||||||
|
if (error) {
|
||||||
|
setError(undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = hookFormSubmit(async (data) => {
|
||||||
|
if (verifyOtp.isPending) return
|
||||||
|
|
||||||
|
setError(undefined)
|
||||||
|
|
||||||
|
// Create FormData object
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append("email", data.email)
|
||||||
|
formData.append("token", data.token)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await verifyOtp.mutateAsync(formData, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("OTP verified successfully")
|
||||||
|
// Navigate to dashboard on success
|
||||||
|
router.push("/dashboard")
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setError(error.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
setError(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
register,
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
handleOtpChange,
|
||||||
|
errors: {
|
||||||
|
...errors,
|
||||||
|
token: error ? { message: error } : errors.token
|
||||||
|
},
|
||||||
|
isPending: verifyOtp.isPending,
|
||||||
|
clearError: () => setError(undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSignOutHandler() {
|
||||||
|
const { signOut } = useAuthActions()
|
||||||
|
const { router } = useNavigations()
|
||||||
|
const [error, setError] = useState<string>()
|
||||||
|
|
||||||
|
const handleSignOut = async () => {
|
||||||
|
if (signOut.isPending) return
|
||||||
|
|
||||||
|
setError(undefined)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await signOut.mutateAsync(undefined, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("You have been signed out successfully")
|
||||||
|
router.push("/sign-in")
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
if (error instanceof AuthenticationError) {
|
||||||
|
setError(error.message)
|
||||||
|
toast.error(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
setError(error.message)
|
||||||
|
toast.error(error.message)
|
||||||
|
// toast.error("An error occurred during sign out. Please try again later.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleSignOut,
|
||||||
|
error,
|
||||||
|
isPending: signOut.isPending,
|
||||||
|
errors: !!error || signOut.error,
|
||||||
|
clearError: () => setError(undefined)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import { signIn, signOut, verifyOtp } from './action';
|
||||||
|
|
||||||
|
export function useAuthActions() {
|
||||||
|
// Sign In Mutation
|
||||||
|
const signInMutation = useMutation({
|
||||||
|
mutationFn: async (formData: FormData) => {
|
||||||
|
const email = formData.get("email")?.toString()
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { email };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const verifyOtpMutation = useMutation({
|
||||||
|
mutationFn: async (formData: FormData) => {
|
||||||
|
const email = formData.get("email")?.toString()
|
||||||
|
const token = formData.get("token")?.toString()
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { email, token };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const signOutMutation = useMutation({
|
||||||
|
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 response;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
signIn: signInMutation,
|
||||||
|
verifyOtp: verifyOtpMutation,
|
||||||
|
signOut: signOutMutation
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,5 +1,3 @@
|
||||||
|
|
||||||
|
|
||||||
import { SignInForm } from "@/app/(pages)/(auth)/_components/signin-form";
|
import { SignInForm } from "@/app/(pages)/(auth)/_components/signin-form";
|
||||||
import { Message } from "@/app/_components/form-message";
|
import { Message } from "@/app/_components/form-message";
|
||||||
import { Button } from "@/app/_components/ui/button";
|
import { Button } from "@/app/_components/ui/button";
|
||||||
|
|
|
@ -2,13 +2,19 @@ import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/app/_lib/utils"
|
import { cn } from "@/app/_lib/utils"
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
export interface InputProps
|
||||||
({ className, type, ...props }, ref) => {
|
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
error?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
({ className, type, error, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
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-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground 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 md:text-sm",
|
"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",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { UseFormSetValue } from "react-hook-form";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a reusable change handler for form inputs
|
||||||
|
* @param setValue - The setValue function from react-hook-form
|
||||||
|
* @returns Object with handleChange function and error management
|
||||||
|
*/
|
||||||
|
export function useFormHandler<T extends Record<string, any>>(
|
||||||
|
setValue: UseFormSetValue<T>
|
||||||
|
) {
|
||||||
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
const handleChange = (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
|
||||||
|
) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
|
||||||
|
// Update the form value
|
||||||
|
setValue(name as any, value as any);
|
||||||
|
|
||||||
|
// Clear error when user starts typing
|
||||||
|
if (errors[name]) {
|
||||||
|
setErrors((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[name]: "",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setError = (name: string, message: string) => {
|
||||||
|
setErrors((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[name]: message,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearErrors = () => {
|
||||||
|
setErrors({});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleChange,
|
||||||
|
errors,
|
||||||
|
setError,
|
||||||
|
clearErrors,
|
||||||
|
};
|
||||||
|
}
|
|
@ -35,3 +35,17 @@ export function formatUrl(url: string): string {
|
||||||
|
|
||||||
return "/" + url;
|
return "/" + url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a FormData object from the FormData object.
|
||||||
|
* @returns {FormData} The FormData object.
|
||||||
|
*/
|
||||||
|
export function createFormData(): FormData {
|
||||||
|
const data = new FormData();
|
||||||
|
Object.entries(FormData).forEach(([key, value]) => {
|
||||||
|
if (value) {
|
||||||
|
data.append(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
};
|
|
@ -13,7 +13,6 @@ ApplicationContainer.load(Symbol('TransactionManagerModule'), createTransactionM
|
||||||
ApplicationContainer.load(Symbol('AuthenticationModule'), createAuthenticationModule());
|
ApplicationContainer.load(Symbol('AuthenticationModule'), createAuthenticationModule());
|
||||||
ApplicationContainer.load(Symbol('UsersModule'), createUsersModule());
|
ApplicationContainer.load(Symbol('UsersModule'), createUsersModule());
|
||||||
|
|
||||||
|
|
||||||
export function getInjection<K extends keyof typeof DI_SYMBOLS>(
|
export function getInjection<K extends keyof typeof DI_SYMBOLS>(
|
||||||
symbol: K
|
symbol: K
|
||||||
): DI_RETURN_TYPES[K] {
|
): DI_RETURN_TYPES[K] {
|
||||||
|
|
|
@ -2,16 +2,15 @@ import { createModule } from '@evyweb/ioctopus';
|
||||||
|
|
||||||
import { AuthenticationService } from '@/src/infrastructure/services/authentication.service';
|
import { AuthenticationService } from '@/src/infrastructure/services/authentication.service';
|
||||||
|
|
||||||
|
|
||||||
import { signInUseCase } from '@/src/application/use-cases/auth/sign-in.use-case';
|
import { signInUseCase } from '@/src/application/use-cases/auth/sign-in.use-case';
|
||||||
import { signUpUseCase } from '@/src/application/use-cases/auth/sign-up.use-case';
|
import { signUpUseCase } from '@/src/application/use-cases/auth/sign-up.use-case';
|
||||||
import { signOutUseCase } from '@/src/application/use-cases/auth/sign-out.use-case';
|
import { signOutUseCase } from '@/src/application/use-cases/auth/sign-out.use-case';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import { DI_SYMBOLS } from '@/di/types';
|
import { DI_SYMBOLS } from '@/di/types';
|
||||||
import { signInController } from '@/src/interface-adapters/controllers/auth/sign-in.controller';
|
import { signInController } from '@/src/interface-adapters/controllers/auth/sign-in.controller';
|
||||||
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 { verifyOtpController } from '@/src/interface-adapters/controllers/auth/verify-otp.controller';
|
||||||
|
|
||||||
export function createAuthenticationModule() {
|
export function createAuthenticationModule() {
|
||||||
const authenticationModule = createModule();
|
const authenticationModule = createModule();
|
||||||
|
@ -20,6 +19,12 @@ export function createAuthenticationModule() {
|
||||||
// authenticationModule
|
// authenticationModule
|
||||||
// .bind(DI_SYMBOLS.IAuthenticationService)
|
// .bind(DI_SYMBOLS.IAuthenticationService)
|
||||||
// .toClass(MockAuthenticationService, [DI_SYMBOLS.IUsersRepository]);
|
// .toClass(MockAuthenticationService, [DI_SYMBOLS.IUsersRepository]);
|
||||||
|
authenticationModule
|
||||||
|
.bind(DI_SYMBOLS.IAuthenticationService)
|
||||||
|
.toClass(AuthenticationService, [
|
||||||
|
DI_SYMBOLS.IUsersRepository,
|
||||||
|
DI_SYMBOLS.IInstrumentationService,
|
||||||
|
]);
|
||||||
} else {
|
} else {
|
||||||
authenticationModule
|
authenticationModule
|
||||||
.bind(DI_SYMBOLS.IAuthenticationService)
|
.bind(DI_SYMBOLS.IAuthenticationService)
|
||||||
|
@ -29,19 +34,13 @@ export function createAuthenticationModule() {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use Cases
|
||||||
authenticationModule
|
authenticationModule
|
||||||
.bind(DI_SYMBOLS.ISignInUseCase)
|
.bind(DI_SYMBOLS.ISignInUseCase)
|
||||||
.toHigherOrderFunction(signInUseCase, [
|
.toHigherOrderFunction(signInUseCase, [
|
||||||
DI_SYMBOLS.IInstrumentationService,
|
DI_SYMBOLS.IInstrumentationService,
|
||||||
|
DI_SYMBOLS.IAuthenticationService,
|
||||||
DI_SYMBOLS.IUsersRepository,
|
DI_SYMBOLS.IUsersRepository,
|
||||||
DI_SYMBOLS.IAuthenticationService,
|
|
||||||
]);
|
|
||||||
|
|
||||||
authenticationModule
|
|
||||||
.bind(DI_SYMBOLS.ISignOutUseCase)
|
|
||||||
.toHigherOrderFunction(signOutUseCase, [
|
|
||||||
DI_SYMBOLS.IInstrumentationService,
|
|
||||||
DI_SYMBOLS.IAuthenticationService,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
authenticationModule
|
authenticationModule
|
||||||
|
@ -52,6 +51,23 @@ export function createAuthenticationModule() {
|
||||||
DI_SYMBOLS.IUsersRepository,
|
DI_SYMBOLS.IUsersRepository,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
authenticationModule
|
||||||
|
.bind(DI_SYMBOLS.IVerifyOtpUseCase)
|
||||||
|
.toHigherOrderFunction(verifyOtpUseCase, [
|
||||||
|
DI_SYMBOLS.IInstrumentationService,
|
||||||
|
DI_SYMBOLS.IAuthenticationService,
|
||||||
|
DI_SYMBOLS.IUsersRepository
|
||||||
|
]);
|
||||||
|
|
||||||
|
authenticationModule
|
||||||
|
.bind(DI_SYMBOLS.ISignOutUseCase)
|
||||||
|
.toHigherOrderFunction(signOutUseCase, [
|
||||||
|
DI_SYMBOLS.IInstrumentationService,
|
||||||
|
DI_SYMBOLS.IAuthenticationService,
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
// Controllers
|
||||||
authenticationModule
|
authenticationModule
|
||||||
.bind(DI_SYMBOLS.ISignInController)
|
.bind(DI_SYMBOLS.ISignInController)
|
||||||
.toHigherOrderFunction(signInController, [
|
.toHigherOrderFunction(signInController, [
|
||||||
|
@ -59,11 +75,18 @@ export function createAuthenticationModule() {
|
||||||
DI_SYMBOLS.ISignInUseCase,
|
DI_SYMBOLS.ISignInUseCase,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
authenticationModule
|
||||||
|
.bind(DI_SYMBOLS.IVerifyOtpController)
|
||||||
|
.toHigherOrderFunction(verifyOtpController, [
|
||||||
|
DI_SYMBOLS.IInstrumentationService,
|
||||||
|
DI_SYMBOLS.IVerifyOtpUseCase,
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
authenticationModule
|
authenticationModule
|
||||||
.bind(DI_SYMBOLS.ISignOutController)
|
.bind(DI_SYMBOLS.ISignOutController)
|
||||||
.toHigherOrderFunction(signOutController, [
|
.toHigherOrderFunction(signOutController, [
|
||||||
DI_SYMBOLS.IInstrumentationService,
|
DI_SYMBOLS.IInstrumentationService,
|
||||||
DI_SYMBOLS.IAuthenticationService,
|
|
||||||
DI_SYMBOLS.ISignOutUseCase,
|
DI_SYMBOLS.ISignOutUseCase,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -25,9 +25,9 @@ export const DI_SYMBOLS = {
|
||||||
|
|
||||||
// Use Cases
|
// Use Cases
|
||||||
ISignInUseCase: Symbol.for('ISignInUseCase'),
|
ISignInUseCase: Symbol.for('ISignInUseCase'),
|
||||||
ISignOutUseCase: Symbol.for('ISignOutUseCase'),
|
|
||||||
ISignUpUseCase: Symbol.for('ISignUpUseCase'),
|
ISignUpUseCase: Symbol.for('ISignUpUseCase'),
|
||||||
IVerifyOtpUseCase: Symbol.for('IVerifyOtpUseCase'),
|
IVerifyOtpUseCase: Symbol.for('IVerifyOtpUseCase'),
|
||||||
|
ISignOutUseCase: Symbol.for('ISignOutUseCase'),
|
||||||
|
|
||||||
// Controllers
|
// Controllers
|
||||||
ISignInController: Symbol.for('ISignInController'),
|
ISignInController: Symbol.for('ISignInController'),
|
||||||
|
@ -47,12 +47,12 @@ export interface DI_RETURN_TYPES {
|
||||||
|
|
||||||
// Use Cases
|
// Use Cases
|
||||||
ISignInUseCase: ISignInUseCase;
|
ISignInUseCase: ISignInUseCase;
|
||||||
ISignOutUseCase: ISignOutUseCase;
|
|
||||||
ISignUpUseCase: ISignUpUseCase;
|
ISignUpUseCase: ISignUpUseCase;
|
||||||
IVerifyOtpUseCase: IVerifyOtpUseCase;
|
IVerifyOtpUseCase: IVerifyOtpUseCase;
|
||||||
|
ISignOutUseCase: ISignOutUseCase;
|
||||||
|
|
||||||
// Controllers
|
// Controllers
|
||||||
ISignInController: ISignInController;
|
ISignInController: ISignInController;
|
||||||
ISignOutController: ISignOutController;
|
|
||||||
IVerifyOtpController: IVerifyOtpController;
|
IVerifyOtpController: IVerifyOtpController;
|
||||||
|
ISignOutController: ISignOutController;
|
||||||
}
|
}
|
|
@ -8,6 +8,7 @@
|
||||||
"@evyweb/ioctopus": "^1.2.0",
|
"@evyweb/ioctopus": "^1.2.0",
|
||||||
"@hookform/resolvers": "^4.1.2",
|
"@hookform/resolvers": "^4.1.2",
|
||||||
"@prisma/client": "^6.4.1",
|
"@prisma/client": "^6.4.1",
|
||||||
|
"@prisma/instrumentation": "^6.5.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-avatar": "^1.1.3",
|
"@radix-ui/react-avatar": "^1.1.3",
|
||||||
"@radix-ui/react-checkbox": "^1.1.1",
|
"@radix-ui/react-checkbox": "^1.1.1",
|
||||||
|
@ -2432,9 +2433,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/instrumentation": {
|
"node_modules/@prisma/instrumentation": {
|
||||||
"version": "6.4.1",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-6.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-6.5.0.tgz",
|
||||||
"integrity": "sha512-1SeN0IvMp5zm3RLJnEr+Zn67WDqUIPP1lF/PkLbi/X64vsnFyItcXNRBrYr0/sI2qLcH9iNzJUhyd3emdGizaQ==",
|
"integrity": "sha512-morJDtFRoAp5d/KENEm+K6Y3PQcn5bCvpJ5a9y3V3DNMrNy/ZSn2zulPGj+ld+Xj2UYVoaMJ8DpBX/o6iF6OiA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0"
|
"@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0"
|
||||||
|
@ -4184,6 +4185,18 @@
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@sentry/node/node_modules/@prisma/instrumentation": {
|
||||||
|
"version": "6.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-6.4.1.tgz",
|
||||||
|
"integrity": "sha512-1SeN0IvMp5zm3RLJnEr+Zn67WDqUIPP1lF/PkLbi/X64vsnFyItcXNRBrYr0/sI2qLcH9iNzJUhyd3emdGizaQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": "^1.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@sentry/opentelemetry": {
|
"node_modules/@sentry/opentelemetry": {
|
||||||
"version": "9.5.0",
|
"version": "9.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-9.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-9.5.0.tgz",
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
"@evyweb/ioctopus": "^1.2.0",
|
"@evyweb/ioctopus": "^1.2.0",
|
||||||
"@hookform/resolvers": "^4.1.2",
|
"@hookform/resolvers": "^4.1.2",
|
||||||
"@prisma/client": "^6.4.1",
|
"@prisma/client": "^6.4.1",
|
||||||
|
"@prisma/instrumentation": "^6.5.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-avatar": "^1.1.3",
|
"@radix-ui/react-avatar": "^1.1.3",
|
||||||
"@radix-ui/react-checkbox": "^1.1.1",
|
"@radix-ui/react-checkbox": "^1.1.1",
|
||||||
|
|
|
@ -3,9 +3,24 @@ import { PrismaClient } from "@prisma/client";
|
||||||
const prismaClientSingleton = () => {
|
const prismaClientSingleton = () => {
|
||||||
return new PrismaClient({
|
return new PrismaClient({
|
||||||
log: [
|
log: [
|
||||||
"query",
|
{
|
||||||
]
|
emit: 'event',
|
||||||
});
|
level: 'query',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emit: 'stdout',
|
||||||
|
level: 'error',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emit: 'stdout',
|
||||||
|
level: 'info',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emit: 'stdout',
|
||||||
|
level: 'warn',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
declare const globalThis: {
|
declare const globalThis: {
|
||||||
|
@ -17,3 +32,9 @@ const db = globalThis.prismaGlobal ?? prismaClientSingleton();
|
||||||
export default db;
|
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) => {
|
||||||
|
console.log('Query: ' + e.query)
|
||||||
|
console.log('Params: ' + e.params)
|
||||||
|
console.log('Duration: ' + e.duration + 'ms')
|
||||||
|
})
|
|
@ -10,8 +10,10 @@ Sentry.init({
|
||||||
// Add optional integrations for additional features
|
// Add optional integrations for additional features
|
||||||
integrations: [
|
integrations: [
|
||||||
Sentry.replayIntegration(),
|
Sentry.replayIntegration(),
|
||||||
|
// Sentry.prismaIntegration(),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
||||||
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
|
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
|
||||||
tracesSampleRate: 1,
|
tracesSampleRate: 1,
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,8 @@
|
||||||
import type { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
|
import type { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
|
||||||
import { AuthenticationError } from "@/src/entities/errors/auth";
|
import { AuthenticationError, UnauthenticatedError } from "@/src/entities/errors/auth";
|
||||||
import { InputParseError, NotFoundError } from "@/src/entities/errors/common";
|
|
||||||
import { type SignInFormData, SignInPasswordless, SignInSchema } from "@/src/entities/models/auth/sign-in.model"
|
import { type SignInFormData, SignInPasswordless, 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 { User } from "@/src/entities/models/users/users.model";
|
|
||||||
import { Session } from "@/src/entities/models/auth/session.model";
|
|
||||||
import { IUsersRepository } from "../../repositories/users.repository.interface";
|
import { IUsersRepository } from "../../repositories/users.repository.interface";
|
||||||
import { AuthResult } from "@/src/entities/models/auth/auth-result.model";
|
|
||||||
import { UsersRepository } from "@/src/infrastructure/repositories/users.repository.impl";
|
|
||||||
import { ICrashReporterService } from "../../services/crash-reporter.service.interface";
|
|
||||||
|
|
||||||
export type ISignInUseCase = ReturnType<typeof signInUseCase>
|
export type ISignInUseCase = ReturnType<typeof signInUseCase>
|
||||||
|
|
||||||
|
@ -16,26 +10,16 @@ export const signInUseCase =
|
||||||
(
|
(
|
||||||
instrumentationService: IInstrumentationService,
|
instrumentationService: IInstrumentationService,
|
||||||
authenticationService: IAuthenticationService,
|
authenticationService: IAuthenticationService,
|
||||||
crashReporterService: ICrashReporterService,
|
|
||||||
usersRepository: IUsersRepository
|
usersRepository: IUsersRepository
|
||||||
) =>
|
) =>
|
||||||
async (input: SignInPasswordless): Promise<void> => {
|
async (input: SignInPasswordless): Promise<void> => {
|
||||||
return instrumentationService.startSpan({ name: "signIn Use Case", op: "function" },
|
return instrumentationService.startSpan({ name: "signIn Use Case", op: "function" },
|
||||||
async () => {
|
async () => {
|
||||||
|
|
||||||
console.log("Injected usersRepository:", usersRepository);
|
|
||||||
|
|
||||||
// Create a direct instance as a test
|
|
||||||
const directRepo = new UsersRepository(
|
|
||||||
instrumentationService,
|
|
||||||
crashReporterService
|
|
||||||
);
|
|
||||||
console.log("Direct repo methods:", Object.keys(directRepo));
|
|
||||||
|
|
||||||
const existingUser = await usersRepository.getUserByEmail(input.email)
|
const existingUser = await usersRepository.getUserByEmail(input.email)
|
||||||
|
|
||||||
if (!existingUser) {
|
if (!existingUser) {
|
||||||
throw new NotFoundError("User does not exist")
|
throw new UnauthenticatedError("User not found. Please tell your admin to create an account for you.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to sign in
|
// Attempt to sign in
|
||||||
|
@ -43,12 +27,6 @@ export const signInUseCase =
|
||||||
email: input.email
|
email: input.email
|
||||||
})
|
})
|
||||||
|
|
||||||
const session = await authenticationService.getSession();
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
throw new NotFoundError("Session not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -28,7 +28,7 @@ export const defaultSignInWithPasswordValues: SignInWithPassword = {
|
||||||
|
|
||||||
export type SignInWithPassword = z.infer<typeof SignInWithPassword>
|
export type SignInWithPassword = z.infer<typeof SignInWithPassword>
|
||||||
|
|
||||||
export const SignInPasswordless = SignInSchema.pick({
|
export const SignInPasswordlessSchema = SignInSchema.pick({
|
||||||
email: true,
|
email: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ export const defaultSignInPasswordlessValues: SignInPasswordless = {
|
||||||
email: "",
|
email: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SignInPasswordless = z.infer<typeof SignInPasswordless>
|
export type SignInPasswordless = z.infer<typeof SignInPasswordlessSchema>
|
||||||
|
|
||||||
|
|
||||||
// Define the sign-in response schema using Zod
|
// Define the sign-in response schema using Zod
|
||||||
|
|
|
@ -26,12 +26,13 @@ export class AuthenticationService implements IAuthenticationService {
|
||||||
name: "signInPasswordless Use Case",
|
name: "signInPasswordless Use Case",
|
||||||
}, async () => {
|
}, async () => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const supabase = await this.supabaseServer
|
const supabase = await this.supabaseServer
|
||||||
|
|
||||||
const { email } = credentials
|
const { email } = credentials
|
||||||
const signIn = supabase.auth.signInWithOtp({ email })
|
const signIn = supabase.auth.signInWithOtp({ email })
|
||||||
|
|
||||||
const { data: { session }, error } = await this.instrumentationService.startSpan({
|
const { error } = await this.instrumentationService.startSpan({
|
||||||
name: "supabase.auth.signInWithOtp",
|
name: "supabase.auth.signInWithOtp",
|
||||||
op: "db:query",
|
op: "db:query",
|
||||||
attributes: { "system": "supabase.auth" }
|
attributes: { "system": "supabase.auth" }
|
||||||
|
@ -40,7 +41,6 @@ export class AuthenticationService implements IAuthenticationService {
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.crashReporterService.report(err)
|
this.crashReporterService.report(err)
|
||||||
throw err
|
throw err
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { useNavigations } from '@/app/_hooks/use-navigations';
|
||||||
import { AuthenticationError } from '@/src/entities/errors/auth';
|
import { AuthenticationError } from '@/src/entities/errors/auth';
|
||||||
import * as authRepository from '@/src/application/repositories/authentication.repository';
|
import * as authRepository from '@/src/application/repositories/authentication.repository';
|
||||||
|
|
||||||
export function useAuthMutation() {
|
export function useAuthActions() {
|
||||||
const { router } = useNavigations();
|
const { router } = useNavigations();
|
||||||
|
|
||||||
// Sign In Mutation
|
// Sign In Mutation
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { useState, type FormEvent, type ChangeEvent } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
// import { signIn } from "";
|
// import { signIn } from "";
|
||||||
import { useAuthMutation } from "./auth-controller";
|
import { useAuthActions } from "./auth-controller";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { AuthenticationError } from "@/src/entities/errors/auth";
|
import { AuthenticationError } from "@/src/entities/errors/auth";
|
||||||
|
@ -105,7 +105,7 @@ type SignInFormErrors = Partial<Record<keyof SignInFormData, string>>;
|
||||||
// const [formData, setFormData] = useState<SignInFormData>(defaultSignInValues);
|
// const [formData, setFormData] = useState<SignInFormData>(defaultSignInValues);
|
||||||
// const [errors, setErrors] = useState<Record<string, string>>({});
|
// const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
// const { signIn } = useAuthMutation();
|
// const { signIn } = useAuthActions();
|
||||||
|
|
||||||
// const form = useForm<SignInFormData>({
|
// const form = useForm<SignInFormData>({
|
||||||
// resolver: zodResolver(SignInSchema),
|
// resolver: zodResolver(SignInSchema),
|
||||||
|
@ -167,7 +167,7 @@ type SignInFormErrors = Partial<Record<keyof SignInFormData, string>>;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// export function useSignInController() {
|
// export function useSignInController() {
|
||||||
// const { signIn } = useAuthMutation();
|
// const { signIn } = useAuthActions();
|
||||||
|
|
||||||
// // Gunakan react-hook-form untuk mengelola form state & error handling
|
// // Gunakan react-hook-form untuk mengelola form state & error handling
|
||||||
// const {
|
// const {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
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 { ISignOutUseCase } from "@/src/application/use-cases/auth/sign-out.use-case"
|
||||||
|
|
||||||
// Sign Out Controller
|
// Sign Out Controller
|
||||||
export type ISignOutController = ReturnType<typeof signOutController>
|
export type ISignOutController = ReturnType<typeof signOutController>
|
||||||
|
@ -7,12 +7,12 @@ export type ISignOutController = ReturnType<typeof signOutController>
|
||||||
export const signOutController =
|
export const signOutController =
|
||||||
(
|
(
|
||||||
instrumentationService: IInstrumentationService,
|
instrumentationService: IInstrumentationService,
|
||||||
authenticationService: IAuthenticationService
|
signOutUseCase: ISignOutUseCase
|
||||||
) =>
|
) =>
|
||||||
async () => {
|
async () => {
|
||||||
return await instrumentationService.startSpan({
|
return await instrumentationService.startSpan({
|
||||||
name: "signOut Controller"
|
name: "signOut Controller"
|
||||||
}, async () => {
|
}, async () => {
|
||||||
return await authenticationService.signOut()
|
return await signOutUseCase()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,3 @@
|
||||||
// src/hooks/useVerifyOtpForm.ts
|
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
|
|
||||||
// import { verifyOtp } from "";
|
|
||||||
import {
|
|
||||||
defaultVerifyOtpValues,
|
|
||||||
VerifyOtpFormData,
|
|
||||||
verifyOtpSchema,
|
|
||||||
} from "@/src/entities/models/auth/verify-otp.model";
|
|
||||||
import { useNavigations } from "@/app/_hooks/use-navigations";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useAuthMutation } from "./auth-controller";
|
|
||||||
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
|
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
|
||||||
import { IVerifyOtpUseCase } from "@/src/application/use-cases/auth/verify-otp.use-case";
|
import { IVerifyOtpUseCase } from "@/src/application/use-cases/auth/verify-otp.use-case";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
@ -67,56 +51,56 @@ import { InputParseError } from "@/src/entities/errors/common";
|
||||||
// };
|
// };
|
||||||
// }
|
// }
|
||||||
|
|
||||||
export const useVerifyOtpController = (email: string) => {
|
// export const useVerifyOtpController = (email: string) => {
|
||||||
const { verifyOtp } = useAuthMutation()
|
// const { verifyOtp } = useAuthActions()
|
||||||
|
|
||||||
const {
|
// const {
|
||||||
control,
|
// control,
|
||||||
register,
|
// register,
|
||||||
handleSubmit,
|
// handleSubmit,
|
||||||
reset,
|
// reset,
|
||||||
formState: { errors, isSubmitSuccessful },
|
// formState: { errors, isSubmitSuccessful },
|
||||||
} = useForm<VerifyOtpFormData>({
|
// } = useForm<VerifyOtpFormData>({
|
||||||
resolver: zodResolver(verifyOtpSchema),
|
// resolver: zodResolver(verifyOtpSchema),
|
||||||
defaultValues: { ...defaultVerifyOtpValues, email: email },
|
// defaultValues: { ...defaultVerifyOtpValues, email: email },
|
||||||
})
|
// })
|
||||||
|
|
||||||
// Clear form after successful submission
|
// // Clear form after successful submission
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (isSubmitSuccessful) {
|
// if (isSubmitSuccessful) {
|
||||||
reset({ ...defaultVerifyOtpValues, email })
|
// reset({ ...defaultVerifyOtpValues, email })
|
||||||
}
|
// }
|
||||||
}, [isSubmitSuccessful, reset, email])
|
// }, [isSubmitSuccessful, reset, email])
|
||||||
|
|
||||||
const onSubmit = handleSubmit(async (data) => {
|
// const onSubmit = handleSubmit(async (data) => {
|
||||||
try {
|
// try {
|
||||||
await verifyOtp.mutate(data)
|
// await verifyOtp.mutate(data)
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.error("OTP verification failed", error)
|
// console.error("OTP verification failed", error)
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
|
|
||||||
// Function to handle auto-submission when all digits are entered
|
// // Function to handle auto-submission when all digits are entered
|
||||||
const handleOtpChange = (value: string, onChange: (value: string) => void) => {
|
// const handleOtpChange = (value: string, onChange: (value: string) => void) => {
|
||||||
onChange(value)
|
// onChange(value)
|
||||||
|
|
||||||
// Auto-submit when all 6 digits are entered
|
// // Auto-submit when all 6 digits are entered
|
||||||
if (value.length === 6) {
|
// if (value.length === 6) {
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
onSubmit()
|
// onSubmit()
|
||||||
}, 300) // Small delay to allow the UI to update
|
// }, 300) // Small delay to allow the UI to update
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return {
|
// return {
|
||||||
control,
|
// control,
|
||||||
register,
|
// register,
|
||||||
handleSubmit: onSubmit,
|
// handleSubmit: onSubmit,
|
||||||
handleOtpChange,
|
// handleOtpChange,
|
||||||
errors,
|
// errors,
|
||||||
isPending: verifyOtp.isPending,
|
// isPending: verifyOtp.isPending,
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Verify OTP Controller
|
// Verify OTP Controller
|
||||||
const verifyOtpInputSchema = z.object({
|
const verifyOtpInputSchema = z.object({
|
||||||
|
|
Loading…
Reference in New Issue