diff --git a/sigap-website/app/(pages)/(admin)/_components/navigations/nav-main.tsx b/sigap-website/app/(pages)/(admin)/_components/navigations/nav-main.tsx index b02d90f..726fa10 100644 --- a/sigap-website/app/(pages)/(admin)/_components/navigations/nav-main.tsx +++ b/sigap-website/app/(pages)/(admin)/_components/navigations/nav-main.tsx @@ -19,7 +19,7 @@ import { import type * as TablerIcons from "@tabler/icons-react"; import { useNavigations } from "@/app/_hooks/use-navigations"; -import { formatUrl } from "@/app/_utils/utils"; +import { formatUrl } from "@/app/_utils/common"; interface SubSubItem { title: string; diff --git a/sigap-website/app/(pages)/(admin)/_components/navigations/nav-user.tsx b/sigap-website/app/(pages)/(admin)/_components/navigations/nav-user.tsx index b9ce84b..e8b01f8 100644 --- a/sigap-website/app/(pages)/(admin)/_components/navigations/nav-user.tsx +++ b/sigap-website/app/(pages)/(admin)/_components/navigations/nav-user.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { ChevronsUpDown } from "lucide-react"; +import { ChevronsUpDown, Loader2 } from "lucide-react"; import { Avatar, @@ -27,6 +27,8 @@ import { IconLogout, IconSettings, IconSparkles } from "@tabler/icons-react"; import type { User } from "@/src/entities/models/users/users.model"; // import { signOut } from "@/app/(pages)/(auth)/action"; import { SettingsDialog } from "../settings/setting-dialog"; +import { useSignOutHandler } from "@/app/(pages)/(auth)/handler"; +import { AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogCancel, AlertDialogAction } from "@/app/_components/ui/alert-dialog"; export function NavUser({ user }: { user: User | null }) { const { isMobile } = useSidebar(); @@ -62,6 +64,65 @@ export function NavUser({ user }: { user: User | null }) { // 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 */} + { + e.preventDefault(); + setOpen(true); // Buka dialog saat diklik + }} + disabled={isPending} + className="space-x-2" + > + + Log out + + + {/* Alert Dialog */} + + + + Log out + + + Are you sure you want to log out? + + + setOpen(false)}>Cancel + { + handleSignOut(); + + // Tutup dialog setelah tombol Log out diklik + if (!isPending) { + setOpen(false); + } + }} + className="btn btn-primary" + disabled={isPending} + > + {isPending ? ( + <> + + Logging You Out... + + ) : ( + Log out + )} + + + + + + ); + } + return ( @@ -131,10 +192,7 @@ export function NavUser({ user }: { user: User | null }) { /> - { }} className="space-x-2"> - - Log out - + diff --git a/sigap-website/app/(pages)/(auth)/_components/signin-form.tsx b/sigap-website/app/(pages)/(auth)/_components/signin-form.tsx index 184ee29..16b61eb 100644 --- a/sigap-website/app/(pages)/(auth)/_components/signin-form.tsx +++ b/sigap-website/app/(pages)/(auth)/_components/signin-form.tsx @@ -7,34 +7,33 @@ import { Input } from "@/app/_components/ui/input"; import { SubmitButton } from "@/app/_components/submit-button"; import Link from "next/link"; 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 { signIn } from "../action"; +import { useSignInHandler } from "../handler"; export function SignInForm({ className, ...props }: React.ComponentPropsWithoutRef<"form">) { + // const [error, setError] = useState(); + // const [loading, setLoading] = useState(false); - const [error, setError] = useState(); - const [loading, setLoading] = useState(false); + // const onSubmit = async (event: React.FormEvent) => { + // event.preventDefault(); + // if (loading) return; - const onSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - if (loading) return; + // const formData = new FormData(event.currentTarget); - 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 res = await signIn(formData); - if (res && res.error) { - setError(res.error); - } - setLoading(false); - }; - - - // const { register, isPending, handleSubmit, errors } = useSignInController(); + const { isPending, handleSubmit, error, errors, clearError } = useSignInHandler(); return (
@@ -61,7 +60,7 @@ export function SignInForm({ variant="outline" className="w-full bg-[#1C1C1C] text-white border-gray-800 hover:bg-[#2C2C2C] hover:border-gray-700" size="lg" - disabled={loading} + disabled={isPending} > Continue with SSO @@ -77,7 +76,7 @@ export function SignInForm({
-
+ } - error={error ? error : undefined} + error={error} />
- By clicking continue, you agree to our Terms of Service and Privacy Policy. + By clicking continue, you agree to Sigap's{" "} + Terms of Service and Privacy Policy.
) diff --git a/sigap-website/app/(pages)/(auth)/action.ts b/sigap-website/app/(pages)/(auth)/action.ts index 82a60ed..81d9d21 100644 --- a/sigap-website/app/(pages)/(auth)/action.ts +++ b/sigap-website/app/(pages)/(auth)/action.ts @@ -14,13 +14,17 @@ export async function signIn(formData: FormData) { recordResponse: true }, async () => { - try { - const email = formData.get("email")?.toString() + const email = formData.get("email")?.toString() + try { const signInController = getInjection("ISignInController") await signInController({ email }) - if (email) redirect(`/verify-otp?email=${encodeURIComponent(email)}`) + // if (email) { + // redirect(`/verify-otp?email=${encodeURIComponent(email)}`) + // } + + return { success: true } } catch (err) { if ( 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'); crashReporterService.report(err); @@ -75,15 +85,22 @@ export async function signOut() { const signOutController = getInjection("ISignOutController") await signOutController() - revalidatePath("/") - redirect("/sign-in") // Updated to match your route + // revalidatePath("/") + // redirect("/sign-in") // Updated to match your route + + return { success: true } } catch (err) { + // if (err instanceof AuthenticationError) { + // return { + // error: "An error occurred during sign out. Please try again later.", + // } + // } + const crashReporterService = getInjection("ICrashReporterService") crashReporterService.report(err) return { 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") await verifyOtpController({ email, token }) - redirect("/dashboard") // Updated to match your route + // redirect("/dashboard") + + return { success: true } } catch (err) { if (err instanceof InputParseError || err instanceof AuthenticationError) { - return { error: err.message, success: false } + return { error: err.message } } const crashReporterService = getInjection("ICrashReporterService") @@ -112,7 +131,6 @@ export async function verifyOtp(formData: FormData) { return { error: "An error occurred during OTP verification. Please try again later.", - success: false, } } }) diff --git a/sigap-website/app/(pages)/(auth)/handler.tsx b/sigap-website/app/(pages)/(auth)/handler.tsx new file mode 100644 index 0000000..80f395b --- /dev/null +++ b/sigap-website/app/(pages)/(auth)/handler.tsx @@ -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(); + *
...
+ */ +export function useSignInHandler() { + const { signIn } = useAuthActions(); + const { router } = useNavigations(); + + const [error, setError] = useState(); + + const handleSubmit = async (event: React.FormEvent) => { + 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() + + const { + register, + handleSubmit: hookFormSubmit, + control, + formState: { errors }, + setValue + } = useForm({ + 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() + + 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) + } +} diff --git a/sigap-website/app/(pages)/(auth)/mutation.ts b/sigap-website/app/(pages)/(auth)/mutation.ts new file mode 100644 index 0000000..7f04a4b --- /dev/null +++ b/sigap-website/app/(pages)/(auth)/mutation.ts @@ -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 + }; +} \ No newline at end of file diff --git a/sigap-website/app/(pages)/(auth)/sign-in/page.tsx b/sigap-website/app/(pages)/(auth)/sign-in/page.tsx index 966f309..4d5df87 100644 --- a/sigap-website/app/(pages)/(auth)/sign-in/page.tsx +++ b/sigap-website/app/(pages)/(auth)/sign-in/page.tsx @@ -1,5 +1,3 @@ - - import { SignInForm } from "@/app/(pages)/(auth)/_components/signin-form"; import { Message } from "@/app/_components/form-message"; import { Button } from "@/app/_components/ui/button"; diff --git a/sigap-website/app/_components/ui/input.tsx b/sigap-website/app/_components/ui/input.tsx index 30f87d4..02ece75 100644 --- a/sigap-website/app/_components/ui/input.tsx +++ b/sigap-website/app/_components/ui/input.tsx @@ -2,13 +2,19 @@ import * as React from "react" import { cn } from "@/app/_lib/utils" -const Input = React.forwardRef>( - ({ className, type, ...props }, ref) => { +export interface InputProps + extends React.InputHTMLAttributes { + error?: boolean +} + +const Input = React.forwardRef( + ({ className, type, error, ...props }, ref) => { return ( >( + setValue: UseFormSetValue +) { + const [errors, setErrors] = useState>({}); + + const handleChange = ( + e: React.ChangeEvent + ) => { + 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, + }; +} \ No newline at end of file diff --git a/sigap-website/app/_utils/utils.ts b/sigap-website/app/_utils/common.ts similarity index 78% rename from sigap-website/app/_utils/utils.ts rename to sigap-website/app/_utils/common.ts index d67a87d..32285ca 100644 --- a/sigap-website/app/_utils/utils.ts +++ b/sigap-website/app/_utils/common.ts @@ -34,4 +34,18 @@ export function formatUrl(url: string): string { } return "/" + url; -} \ No newline at end of file +} + +/** + * 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; +}; diff --git a/sigap-website/di/container.ts b/sigap-website/di/container.ts index e085861..d8ffab6 100644 --- a/sigap-website/di/container.ts +++ b/sigap-website/di/container.ts @@ -13,7 +13,6 @@ ApplicationContainer.load(Symbol('TransactionManagerModule'), createTransactionM ApplicationContainer.load(Symbol('AuthenticationModule'), createAuthenticationModule()); ApplicationContainer.load(Symbol('UsersModule'), createUsersModule()); - export function getInjection( symbol: K ): DI_RETURN_TYPES[K] { diff --git a/sigap-website/di/modules/authentication.module.ts b/sigap-website/di/modules/authentication.module.ts index 03cadc7..d9a056c 100644 --- a/sigap-website/di/modules/authentication.module.ts +++ b/sigap-website/di/modules/authentication.module.ts @@ -2,16 +2,15 @@ import { createModule } from '@evyweb/ioctopus'; import { AuthenticationService } from '@/src/infrastructure/services/authentication.service'; - 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 { signOutUseCase } from '@/src/application/use-cases/auth/sign-out.use-case'; - - import { DI_SYMBOLS } from '@/di/types'; import { signInController } from '@/src/interface-adapters/controllers/auth/sign-in.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() { const authenticationModule = createModule(); @@ -20,6 +19,12 @@ export function createAuthenticationModule() { // authenticationModule // .bind(DI_SYMBOLS.IAuthenticationService) // .toClass(MockAuthenticationService, [DI_SYMBOLS.IUsersRepository]); + authenticationModule + .bind(DI_SYMBOLS.IAuthenticationService) + .toClass(AuthenticationService, [ + DI_SYMBOLS.IUsersRepository, + DI_SYMBOLS.IInstrumentationService, + ]); } else { authenticationModule .bind(DI_SYMBOLS.IAuthenticationService) @@ -29,19 +34,13 @@ export function createAuthenticationModule() { ]); } + // Use Cases authenticationModule .bind(DI_SYMBOLS.ISignInUseCase) .toHigherOrderFunction(signInUseCase, [ DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IAuthenticationService, DI_SYMBOLS.IUsersRepository, - DI_SYMBOLS.IAuthenticationService, - ]); - - authenticationModule - .bind(DI_SYMBOLS.ISignOutUseCase) - .toHigherOrderFunction(signOutUseCase, [ - DI_SYMBOLS.IInstrumentationService, - DI_SYMBOLS.IAuthenticationService, ]); authenticationModule @@ -52,6 +51,23 @@ export function createAuthenticationModule() { 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 .bind(DI_SYMBOLS.ISignInController) .toHigherOrderFunction(signInController, [ @@ -59,11 +75,18 @@ export function createAuthenticationModule() { DI_SYMBOLS.ISignInUseCase, ]); + authenticationModule + .bind(DI_SYMBOLS.IVerifyOtpController) + .toHigherOrderFunction(verifyOtpController, [ + DI_SYMBOLS.IInstrumentationService, + DI_SYMBOLS.IVerifyOtpUseCase, + ]); + + authenticationModule .bind(DI_SYMBOLS.ISignOutController) .toHigherOrderFunction(signOutController, [ DI_SYMBOLS.IInstrumentationService, - DI_SYMBOLS.IAuthenticationService, DI_SYMBOLS.ISignOutUseCase, ]); diff --git a/sigap-website/di/types.ts b/sigap-website/di/types.ts index 832f5b5..be22123 100644 --- a/sigap-website/di/types.ts +++ b/sigap-website/di/types.ts @@ -25,9 +25,9 @@ export const DI_SYMBOLS = { // Use Cases ISignInUseCase: Symbol.for('ISignInUseCase'), - ISignOutUseCase: Symbol.for('ISignOutUseCase'), ISignUpUseCase: Symbol.for('ISignUpUseCase'), IVerifyOtpUseCase: Symbol.for('IVerifyOtpUseCase'), + ISignOutUseCase: Symbol.for('ISignOutUseCase'), // Controllers ISignInController: Symbol.for('ISignInController'), @@ -47,12 +47,12 @@ export interface DI_RETURN_TYPES { // Use Cases ISignInUseCase: ISignInUseCase; - ISignOutUseCase: ISignOutUseCase; ISignUpUseCase: ISignUpUseCase; IVerifyOtpUseCase: IVerifyOtpUseCase; + ISignOutUseCase: ISignOutUseCase; // Controllers ISignInController: ISignInController; - ISignOutController: ISignOutController; IVerifyOtpController: IVerifyOtpController; + ISignOutController: ISignOutController; } \ No newline at end of file diff --git a/sigap-website/package-lock.json b/sigap-website/package-lock.json index d8a41c6..7d9443b 100644 --- a/sigap-website/package-lock.json +++ b/sigap-website/package-lock.json @@ -8,6 +8,7 @@ "@evyweb/ioctopus": "^1.2.0", "@hookform/resolvers": "^4.1.2", "@prisma/client": "^6.4.1", + "@prisma/instrumentation": "^6.5.0", "@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-checkbox": "^1.1.1", @@ -2432,9 +2433,9 @@ } }, "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==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-6.5.0.tgz", + "integrity": "sha512-morJDtFRoAp5d/KENEm+K6Y3PQcn5bCvpJ5a9y3V3DNMrNy/ZSn2zulPGj+ld+Xj2UYVoaMJ8DpBX/o6iF6OiA==", "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" @@ -4184,6 +4185,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": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-9.5.0.tgz", diff --git a/sigap-website/package.json b/sigap-website/package.json index 736d1fd..ff7104c 100644 --- a/sigap-website/package.json +++ b/sigap-website/package.json @@ -13,6 +13,7 @@ "@evyweb/ioctopus": "^1.2.0", "@hookform/resolvers": "^4.1.2", "@prisma/client": "^6.4.1", + "@prisma/instrumentation": "^6.5.0", "@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-checkbox": "^1.1.1", diff --git a/sigap-website/prisma/db.ts b/sigap-website/prisma/db.ts index bb2e9e4..1d7d247 100644 --- a/sigap-website/prisma/db.ts +++ b/sigap-website/prisma/db.ts @@ -3,9 +3,24 @@ import { PrismaClient } from "@prisma/client"; const prismaClientSingleton = () => { return new PrismaClient({ log: [ - "query", - ] - }); + { + emit: 'event', + level: 'query', + }, + { + emit: 'stdout', + level: 'error', + }, + { + emit: 'stdout', + level: 'info', + }, + { + emit: 'stdout', + level: 'warn', + }, + ], + }) }; declare const globalThis: { @@ -17,3 +32,9 @@ const db = globalThis.prismaGlobal ?? prismaClientSingleton(); export default db; if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = db; + +db.$on('query', (e) => { + console.log('Query: ' + e.query) + console.log('Params: ' + e.params) + console.log('Duration: ' + e.duration + 'ms') +}) \ No newline at end of file diff --git a/sigap-website/sentry.client.config.ts b/sigap-website/sentry.client.config.ts index a2422b1..adfe041 100644 --- a/sigap-website/sentry.client.config.ts +++ b/sigap-website/sentry.client.config.ts @@ -10,8 +10,10 @@ Sentry.init({ // Add optional integrations for additional features integrations: [ Sentry.replayIntegration(), + // Sentry.prismaIntegration(), ], + // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. tracesSampleRate: 1, diff --git a/sigap-website/src/application/use-cases/auth/sign-in.use-case.ts b/sigap-website/src/application/use-cases/auth/sign-in.use-case.ts index 4511b76..a54b392 100644 --- a/sigap-website/src/application/use-cases/auth/sign-in.use-case.ts +++ b/sigap-website/src/application/use-cases/auth/sign-in.use-case.ts @@ -1,14 +1,8 @@ import type { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface" -import { AuthenticationError } from "@/src/entities/errors/auth"; -import { InputParseError, NotFoundError } from "@/src/entities/errors/common"; +import { AuthenticationError, UnauthenticatedError } from "@/src/entities/errors/auth"; import { type SignInFormData, SignInPasswordless, SignInSchema } from "@/src/entities/models/auth/sign-in.model" 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 { 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 @@ -16,26 +10,16 @@ export const signInUseCase = ( instrumentationService: IInstrumentationService, authenticationService: IAuthenticationService, - crashReporterService: ICrashReporterService, usersRepository: IUsersRepository ) => async (input: SignInPasswordless): Promise => { return instrumentationService.startSpan({ name: "signIn Use Case", op: "function" }, 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) 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 @@ -43,12 +27,6 @@ export const signInUseCase = email: input.email }) - const session = await authenticationService.getSession(); - - if (!session) { - throw new NotFoundError("Session not found") - } - return } ) diff --git a/sigap-website/src/entities/models/auth/sign-in.model.ts b/sigap-website/src/entities/models/auth/sign-in.model.ts index 627b3ae..369f380 100644 --- a/sigap-website/src/entities/models/auth/sign-in.model.ts +++ b/sigap-website/src/entities/models/auth/sign-in.model.ts @@ -28,7 +28,7 @@ export const defaultSignInWithPasswordValues: SignInWithPassword = { export type SignInWithPassword = z.infer -export const SignInPasswordless = SignInSchema.pick({ +export const SignInPasswordlessSchema = SignInSchema.pick({ email: true, }) @@ -37,7 +37,7 @@ export const defaultSignInPasswordlessValues: SignInPasswordless = { email: "", } -export type SignInPasswordless = z.infer +export type SignInPasswordless = z.infer // Define the sign-in response schema using Zod diff --git a/sigap-website/src/infrastructure/services/authentication.service.ts b/sigap-website/src/infrastructure/services/authentication.service.ts index 2bc654d..f127c78 100644 --- a/sigap-website/src/infrastructure/services/authentication.service.ts +++ b/sigap-website/src/infrastructure/services/authentication.service.ts @@ -26,12 +26,13 @@ export class AuthenticationService implements IAuthenticationService { name: "signInPasswordless Use Case", }, async () => { try { + const supabase = await this.supabaseServer const { email } = credentials const signIn = supabase.auth.signInWithOtp({ email }) - const { data: { session }, error } = await this.instrumentationService.startSpan({ + const { error } = await this.instrumentationService.startSpan({ name: "supabase.auth.signInWithOtp", op: "db:query", attributes: { "system": "supabase.auth" } @@ -40,7 +41,6 @@ export class AuthenticationService implements IAuthenticationService { }) return - } catch (err) { this.crashReporterService.report(err) throw err diff --git a/sigap-website/src/interface-adapters/controllers/auth/auth-controller.tsx b/sigap-website/src/interface-adapters/controllers/auth/auth-controller.tsx index 58418db..9d0c644 100644 --- a/sigap-website/src/interface-adapters/controllers/auth/auth-controller.tsx +++ b/sigap-website/src/interface-adapters/controllers/auth/auth-controller.tsx @@ -8,7 +8,7 @@ import { useNavigations } from '@/app/_hooks/use-navigations'; import { AuthenticationError } from '@/src/entities/errors/auth'; import * as authRepository from '@/src/application/repositories/authentication.repository'; -export function useAuthMutation() { +export function useAuthActions() { const { router } = useNavigations(); // Sign In Mutation diff --git a/sigap-website/src/interface-adapters/controllers/auth/sign-in.controller.tsx b/sigap-website/src/interface-adapters/controllers/auth/sign-in.controller.tsx index c238837..930ee09 100644 --- a/sigap-website/src/interface-adapters/controllers/auth/sign-in.controller.tsx +++ b/sigap-website/src/interface-adapters/controllers/auth/sign-in.controller.tsx @@ -10,7 +10,7 @@ import { useState, type FormEvent, type ChangeEvent } from "react"; import { toast } from "sonner"; import { z } from "zod"; // import { signIn } from ""; -import { useAuthMutation } from "./auth-controller"; +import { useAuthActions } from "./auth-controller"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { AuthenticationError } from "@/src/entities/errors/auth"; @@ -105,7 +105,7 @@ type SignInFormErrors = Partial>; // const [formData, setFormData] = useState(defaultSignInValues); // const [errors, setErrors] = useState>({}); -// const { signIn } = useAuthMutation(); +// const { signIn } = useAuthActions(); // const form = useForm({ // resolver: zodResolver(SignInSchema), @@ -167,7 +167,7 @@ type SignInFormErrors = Partial>; // } // export function useSignInController() { -// const { signIn } = useAuthMutation(); +// const { signIn } = useAuthActions(); // // Gunakan react-hook-form untuk mengelola form state & error handling // const { diff --git a/sigap-website/src/interface-adapters/controllers/auth/sign-out.controller.ts b/sigap-website/src/interface-adapters/controllers/auth/sign-out.controller.ts index 9366279..1b13e9b 100644 --- a/sigap-website/src/interface-adapters/controllers/auth/sign-out.controller.ts +++ b/sigap-website/src/interface-adapters/controllers/auth/sign-out.controller.ts @@ -1,5 +1,5 @@ -import { IAuthenticationService } from "@/src/application/services/authentication.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 export type ISignOutController = ReturnType @@ -7,12 +7,12 @@ export type ISignOutController = ReturnType export const signOutController = ( instrumentationService: IInstrumentationService, - authenticationService: IAuthenticationService + signOutUseCase: ISignOutUseCase ) => async () => { return await instrumentationService.startSpan({ name: "signOut Controller" }, async () => { - return await authenticationService.signOut() + return await signOutUseCase() }) } diff --git a/sigap-website/src/interface-adapters/controllers/auth/verify-otp.controller.tsx b/sigap-website/src/interface-adapters/controllers/auth/verify-otp.controller.tsx index b037665..462bdb4 100644 --- a/sigap-website/src/interface-adapters/controllers/auth/verify-otp.controller.tsx +++ b/sigap-website/src/interface-adapters/controllers/auth/verify-otp.controller.tsx @@ -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 { IVerifyOtpUseCase } from "@/src/application/use-cases/auth/verify-otp.use-case"; import { z } from "zod"; @@ -67,56 +51,56 @@ import { InputParseError } from "@/src/entities/errors/common"; // }; // } -export const useVerifyOtpController = (email: string) => { - const { verifyOtp } = useAuthMutation() +// export const useVerifyOtpController = (email: string) => { +// const { verifyOtp } = useAuthActions() - const { - control, - register, - handleSubmit, - reset, - formState: { errors, isSubmitSuccessful }, - } = useForm({ - resolver: zodResolver(verifyOtpSchema), - defaultValues: { ...defaultVerifyOtpValues, email: email }, - }) +// const { +// control, +// register, +// handleSubmit, +// reset, +// formState: { errors, isSubmitSuccessful }, +// } = useForm({ +// resolver: zodResolver(verifyOtpSchema), +// defaultValues: { ...defaultVerifyOtpValues, email: email }, +// }) - // Clear form after successful submission - useEffect(() => { - if (isSubmitSuccessful) { - reset({ ...defaultVerifyOtpValues, email }) - } - }, [isSubmitSuccessful, reset, email]) +// // Clear form after successful submission +// useEffect(() => { +// if (isSubmitSuccessful) { +// reset({ ...defaultVerifyOtpValues, email }) +// } +// }, [isSubmitSuccessful, reset, email]) - const onSubmit = handleSubmit(async (data) => { - try { - await verifyOtp.mutate(data) - } catch (error) { - console.error("OTP verification failed", error) - } - }) +// const onSubmit = handleSubmit(async (data) => { +// try { +// await verifyOtp.mutate(data) +// } catch (error) { +// console.error("OTP verification failed", error) +// } +// }) - // Function to handle auto-submission when all digits are entered - const handleOtpChange = (value: string, onChange: (value: string) => void) => { - onChange(value) +// // Function to handle auto-submission when all digits are entered +// const handleOtpChange = (value: string, onChange: (value: string) => void) => { +// onChange(value) - // Auto-submit when all 6 digits are entered - if (value.length === 6) { - setTimeout(() => { - onSubmit() - }, 300) // Small delay to allow the UI to update - } - } +// // Auto-submit when all 6 digits are entered +// if (value.length === 6) { +// setTimeout(() => { +// onSubmit() +// }, 300) // Small delay to allow the UI to update +// } +// } - return { - control, - register, - handleSubmit: onSubmit, - handleOtpChange, - errors, - isPending: verifyOtp.isPending, - } -} +// return { +// control, +// register, +// handleSubmit: onSubmit, +// handleOtpChange, +// errors, +// isPending: verifyOtp.isPending, +// } +// } // Verify OTP Controller const verifyOtpInputSchema = z.object({