implmentation clean architecture for auth services

This commit is contained in:
vergiLgood1 2025-03-16 22:25:33 +07:00
parent aed9eba5d3
commit 1a39925c97
25 changed files with 581 additions and 173 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -34,4 +34,18 @@ 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;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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