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 8a7590c..b9ce84b 100644 --- a/sigap-website/app/(pages)/(admin)/_components/navigations/nav-user.tsx +++ b/sigap-website/app/(pages)/(admin)/_components/navigations/nav-user.tsx @@ -25,7 +25,7 @@ import { } from "@/app/_components/ui/sidebar"; 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 { signOut } from "@/app/(pages)/(auth)/action"; import { SettingsDialog } from "../settings/setting-dialog"; export function NavUser({ user }: { user: User | null }) { @@ -131,7 +131,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 93ffb6a..7e92a6e 100644 --- a/sigap-website/app/(pages)/(auth)/_components/signin-form.tsx +++ b/sigap-website/app/(pages)/(auth)/_components/signin-form.tsx @@ -1,27 +1,20 @@ "use client"; import type React from "react"; - -import { Lock } from "lucide-react"; -import { Button } from "../../../_components/ui/button"; -import { Input } from "../../../_components/ui/input"; -import { SubmitButton } from "../../../_components/submit-button"; +import { Loader2, Lock } from "lucide-react"; +import { Button } from "@/app/_components/ui/button"; +import { Input } from "@/app/_components/ui/input"; +import { SubmitButton } from "@/app/_components/submit-button"; import Link from "next/link"; -import { FormField } from "../../../_components/form-field"; -import { useSignInForm } from "@/src/interface-adapters/controllers/auth/sign-in-controller"; +import { FormField } from "@/app/_components/form-field"; +import { useSignInController } from "@/src/interface-adapters/controllers/auth/sign-in-controller"; export function SignInForm({ className, ...props }: React.ComponentPropsWithoutRef<"form">) { - const { - formData, - errors, - isSubmitting, - message, - handleChange, - handleSubmit, - } = useSignInForm(); + + const { register, isPending, handleSubmit, errors } = useSignInController(); return (
@@ -48,7 +41,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={isSubmitting} + disabled={isPending} > Continue with SSO @@ -64,32 +57,36 @@ export function SignInForm({
-
+ } - error={errors.email} + error={errors.email ? errors.email.message : undefined} /> - - Sign In - + {isPending ? ( + <> + + Signing in... + + ) : ( + "Sign in" + )} +
diff --git a/sigap-website/app/(pages)/(auth)/_components/verify-otp-form.tsx b/sigap-website/app/(pages)/(auth)/_components/verify-otp-form.tsx index 3096cf9..6aa9b05 100644 --- a/sigap-website/app/(pages)/(auth)/_components/verify-otp-form.tsx +++ b/sigap-website/app/(pages)/(auth)/_components/verify-otp-form.tsx @@ -1,15 +1,5 @@ -// src/components/auth/VerifyOtpForm.tsx "use client"; - import { useSearchParams } from "next/navigation"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormMessage, -} from "@/app/_components/ui/form"; import { InputOTP, InputOTPGroup, @@ -24,76 +14,81 @@ import { CardTitle, } from "@/app/_components/ui/card"; import { cn } from "@/app/_lib/utils"; -import { useVerifyOtpForm } from "@/src/interface-adapters/controllers/auth/verify-otp.controller"; +import { useVerifyOtpController } from "@/src/interface-adapters/controllers/auth/verify-otp.controller"; +import { Loader2 } from "lucide-react"; +import { Controller } from "react-hook-form"; interface VerifyOtpFormProps extends React.HTMLAttributes {} export function VerifyOtpForm({ className, ...props }: VerifyOtpFormProps) { - const searchParams = useSearchParams(); - const email = searchParams.get("email") || ""; - - const { form, isSubmitting, message, onSubmit } = useVerifyOtpForm(email); + const searchParams = useSearchParams() + const email = searchParams.get("email") || "" + const { control, register, isPending, handleSubmit, handleOtpChange, errors } = useVerifyOtpController(email) return (
- - One-Time Password - + One-Time Password One time password is a security feature that helps protect your data -
- - - + +
+ ( - - - - - {[...Array(6)].map((_, index) => ( - - ))} - - - - - Please enter the one-time password sent to {email}. - - - + handleOtpChange(value, field.onChange)} + > + + {[...Array(6)].map((_, index) => ( + + ))} + + )} /> -
- - Submit - + {errors.token ?
{errors.token.message}
:
+ Successfully verified! +
} + +
+ Please enter the one-time password sent to {email}.
- {message && ( -
{message}
- )} - - +
+
+ + {isPending ? ( + <> + + Verifying... + + ) : ( + "Verify OTP" + )} + +
+
- By clicking continue, you agree to our Terms of Service{" "} - and Privacy Policy. + By clicking continue, you agree to our Terms of Service and Privacy Policy.
- ); + ) } + diff --git a/sigap-website/app/(pages)/(auth)/action.ts b/sigap-website/app/(pages)/(auth)/action.ts deleted file mode 100644 index 78e12b8..0000000 --- a/sigap-website/app/(pages)/(auth)/action.ts +++ /dev/null @@ -1,73 +0,0 @@ -// src/app/(auth)/actions.ts -"use server"; - -import db from "@/prisma/db"; -import { SignInFormData } from "@/src/entities/models/auth/sign-in.model"; -import { VerifyOtpFormData } from "@/src/entities/models/auth/verify-otp.model"; -import { User } from "@/src/entities/models/users/users.model"; -import { authRepository } from "@/src/repositories/authentication.repository"; -import { createClient } from "@/app/_utils/supabase/server"; -import { redirect } from "next/navigation"; - -export async function signIn( - data: SignInFormData -): Promise<{ success: boolean; message: string; redirectTo?: string }> { - try { - const result = await authRepository.signIn(data); - return { - success: true, - message: "Check your email for the login link!", - redirectTo: result.redirectTo, - }; - } catch (error) { - console.error("Authentication error:", error); - return { - success: false, - message: - error instanceof Error - ? error.message - : "Authentication failed. Please try again.", - }; - } -} - -export async function verifyOtp( - data: VerifyOtpFormData -): Promise<{ success: boolean; message: string; redirectTo?: string }> { - try { - const result = await authRepository.verifyOtp(data); - return { - success: true, - message: "Successfully authenticated!", - redirectTo: result.redirectTo, - }; - } catch (error) { - console.error("OTP verification error:", error); - return { - success: false, - message: - error instanceof Error - ? error.message - : "OTP verification failed. Please try again.", - }; - } -} - -export async function signOut() { - try { - const result = await authRepository.signOut(); - return { - success: true, - redirectTo: result.redirectTo, - }; - } catch (error) { - console.error("Sign out error:", error); - return { - success: false, - message: - error instanceof Error - ? error.message - : "Sign out failed. Please try again.", - }; - } -} \ 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 4bea82a..966f309 100644 --- a/sigap-website/app/(pages)/(auth)/sign-in/page.tsx +++ b/sigap-website/app/(pages)/(auth)/sign-in/page.tsx @@ -1,10 +1,12 @@ + + import { SignInForm } from "@/app/(pages)/(auth)/_components/signin-form"; import { Message } from "@/app/_components/form-message"; import { Button } from "@/app/_components/ui/button"; import { GalleryVerticalEnd, Globe } from "lucide-react"; export default async function Login(props: { searchParams: Promise }) { - const searchParams = await props.searchParams; + return (
diff --git a/sigap-website/prisma/db.ts b/sigap-website/prisma/db.ts index 0e2ce72..96acbc4 100644 --- a/sigap-website/prisma/db.ts +++ b/sigap-website/prisma/db.ts @@ -13,3 +13,6 @@ const db = globalThis.prismaGlobal ?? prismaClientSingleton(); export default db; if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = db; + + +export type Transaction = PrismaClient['$transaction']; \ No newline at end of file diff --git a/sigap-website/src/application/repositories/authentication.repository.ts b/sigap-website/src/application/repositories/authentication.repository.ts index 3068ed6..86fbcca 100644 --- a/sigap-website/src/application/repositories/authentication.repository.ts +++ b/sigap-website/src/application/repositories/authentication.repository.ts @@ -1,65 +1,343 @@ -// src/repositories/auth.repository.ts -import { createClient } from "@/app/_utils/supabase/server"; -import { SignInFormData } from "../entities/models/auth/sign-in.model"; -import { VerifyOtpFormData } from "../entities/models/auth/verify-otp.model"; +// // src/repositories/auth.repository.ts +// "use server"; -export class AuthRepository { - async signIn({ email }: SignInFormData) { - const supabase = await createClient(); - const { data, error } = await supabase.auth.signInWithOtp({ - email, - options: { - shouldCreateUser: false, - }, - }); - - if (error) { - throw new Error(error.message); +// import { createClient } from "@/app/_utils/supabase/server"; +// import { SignInFormData } from "@/src/entities/models/auth/sign-in.model"; +// import { VerifyOtpFormData } from "@/src/entities/models/auth/verify-otp.model"; +// import { AuthenticationError } from "@/src/entities/errors/auth"; +// import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; +// import { ICrashReporterService } from "@/src/application/services/crash-reporter.service.interface"; +// import { createAdminClient } from "@/app/_utils/supabase/admin"; +// import { DatabaseOperationError } from "@/src/entities/errors/common"; + +// export class AuthRepository { +// private static instance: AuthRepository; + +// private constructor( +// private readonly instrumentationService: IInstrumentationService, +// private readonly crashReporterService: ICrashReporterService, +// private readonly supabaseAdmin = createAdminClient(), +// private readonly supabaseServer = createClient() +// ) { } + +// // Method untuk mendapatkan singleton instance +// public static getInstance( +// instrumentationService: IInstrumentationService, +// crashReporterService: ICrashReporterService +// ): AuthRepository { +// if (!AuthRepository.instance) { +// AuthRepository.instance = new AuthRepository(instrumentationService, crashReporterService); +// } +// return AuthRepository.instance; +// } + +// async signIn({ email }: SignInFormData) { +// return await this.instrumentationService.startSpan({ +// name: "UsersRepository > signIn", +// op: 'db.query', +// attributes: { 'db.system': 'postgres' }, +// }, async () => { +// try { +// const supabase = await this.supabaseServer; +// const { data, error } = await supabase.auth.signInWithOtp({ +// email, +// options: { +// shouldCreateUser: false, +// }, +// }); + +// if (error) { +// console.error("Error signing in:", error); +// throw new AuthenticationError(error.message); +// } + +// return { +// data, +// redirectTo: `/verify-otp?email=${encodeURIComponent(email)}`, +// }; +// } catch (err) { +// this.crashReporterService.report(err); +// throw err; +// } +// }); +// } + +// async verifyOtp({ email, token }: VerifyOtpFormData) { +// return await this.instrumentationService.startSpan({ +// name: "UsersRepository > verifyOtp", +// op: 'db.query', +// attributes: { 'db.system': 'postgres' }, +// }, async () => { +// try { +// const supabase = await this.supabaseServer; +// const { data, error } = await supabase.auth.verifyOtp({ +// email, +// token, +// type: "email", +// }); + +// if (error) { +// console.error("Error verifying OTP:", error); +// throw new AuthenticationError(error.message); +// } + +// return { +// data, +// redirectTo: "/dashboard", +// }; +// } catch (err) { +// this.crashReporterService.report(err); +// throw err; +// } +// }); +// } + +// async signOut() { +// return await this.instrumentationService.startSpan({ +// name: "UsersRepository > signOut", +// op: 'db.query', +// attributes: { 'db.system': 'postgres' }, +// }, async () => { +// try { +// const supabase = await this.supabaseServer; +// const { error } = await supabase.auth.signOut(); + +// if (error) { +// console.error("Error signing out:", error); +// throw new AuthenticationError(error.message); +// } + +// return { +// success: true, +// redirectTo: "/", +// }; +// } catch (err) { +// this.crashReporterService.report(err); +// throw err; +// } +// }); +// } + +// async sendPasswordRecovery(email: string): Promise { +// return await this.instrumentationService.startSpan({ +// name: "UsersRepository > sendPasswordRecovery", +// op: 'db.query', +// attributes: { 'db.system': 'postgres' }, +// }, async () => { +// try { +// const supabase = this.supabaseAdmin; + +// const { error } = await supabase.auth.resetPasswordForEmail(email, { +// redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/reset-password`, +// }); + +// if (error) { +// console.error("Error sending password recovery:", error); +// throw new DatabaseOperationError(error.message); +// } +// } catch (err) { +// this.crashReporterService.report(err); +// throw err; +// } +// }); +// } + +// async sendMagicLink(email: string): Promise { +// return await this.instrumentationService.startSpan({ +// name: "UsersRepository > sendMagicLink", +// op: 'db.query', +// attributes: { 'db.system': 'postgres' }, +// }, async () => { +// try { +// const supabase = this.supabaseAdmin; + +// const { error } = await supabase.auth.signInWithOtp({ +// email, +// options: { +// emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`, +// }, +// }); + +// if (error) { +// console.error("Error sending magic link:", error); +// throw new DatabaseOperationError(error.message); +// } +// } catch (err) { +// this.crashReporterService.report(err); +// throw err; +// } +// }); +// } +// } + +// src/app/_actions/auth.actions.ts +"use server"; + +import { createClient } from "@/app/_utils/supabase/server"; +import { SignInFormData } from "@/src/entities/models/auth/sign-in.model"; +import { VerifyOtpFormData } from "@/src/entities/models/auth/verify-otp.model"; +import { AuthenticationError } from "@/src/entities/errors/auth"; +import { DatabaseOperationError } from "@/src/entities/errors/common"; +import { createAdminClient } from "@/app/_utils/supabase/admin"; +import { IInstrumentationServiceImpl } from "@/src/application/services/instrumentation.service.interface"; +import { ICrashReporterServiceImpl } from "@/src/application/services/crash-reporter.service.interface"; + +// Server actions for authentication +export async function signIn({ email }: SignInFormData) { + return await IInstrumentationServiceImpl.instrumentServerAction( + "auth.signIn", + { email }, + async () => { + try { + const supabase = await createClient(); + const { data, error } = await supabase.auth.signInWithOtp({ + email, + options: { + shouldCreateUser: false, + }, + }); + + if (error) { + console.error("Error signing in:", error); + throw new AuthenticationError(error.message); + } + + return { + success: true, + message: "Sign in email sent successfully", + data, + redirectTo: `/verify-otp?email=${encodeURIComponent(email)}`, + }; + } catch (err) { + ICrashReporterServiceImpl.report(err); + if (err instanceof AuthenticationError) { + throw err; + } + throw new AuthenticationError("Failed to sign in. Please try again."); + } } - - return { - data, - redirectTo: `/verify-otp?email=${encodeURIComponent(email)}` - }; - } - - async signOut() { - const supabase = await createClient(); - const { error } = await supabase.auth.signOut(); - - if (error) { - throw new Error(error.message); - } - - return { - success: true, - redirectTo: "/" - }; - } - - async getUser() { - const supabase = await createClient(); - const { data: { user } } = await supabase.auth.getUser(); - return user; - } - - async verifyOtp({ email, token }: VerifyOtpFormData) { - const supabase = await createClient(); - const { data, error } = await supabase.auth.verifyOtp({ - email, - token, - type: "email", - }); - - if (error) { - throw new Error(error.message); - } - - return { - data, - redirectTo: "/dashboard", - }; - } + ); } -export const authRepository = new AuthRepository(); \ No newline at end of file +export async function verifyOtp({ email, token }: VerifyOtpFormData) { + return await IInstrumentationServiceImpl.instrumentServerAction( + "auth.verifyOtp", + { email }, + async () => { + try { + const supabase = await createClient(); + const { data, error } = await supabase.auth.verifyOtp({ + email, + token, + type: "email", + }); + + if (error) { + console.error("Error verifying OTP:", error); + throw new AuthenticationError(error.message); + } + + return { + success: true, + message: "Successfully verified!", + data, + redirectTo: "/dashboard", + }; + } catch (err) { + ICrashReporterServiceImpl.report(err); + if (err instanceof AuthenticationError) { + throw err; + } + throw new AuthenticationError("Failed to verify OTP. Please try again."); + } + } + ); +} + +export async function signOut() { + return await IInstrumentationServiceImpl.instrumentServerAction( + "auth.signOut", + {}, + async () => { + try { + const supabase = await createClient(); + const { error } = await supabase.auth.signOut(); + + if (error) { + console.error("Error signing out:", error); + throw new AuthenticationError(error.message); + } + + return { + success: true, + message: "Sign out successful", + redirectTo: "/", + }; + } catch (err) { + ICrashReporterServiceImpl.report(err); + throw new AuthenticationError("Failed to sign out. Please try again."); + } + } + ); +} + +export async function sendPasswordRecovery(email: string) { + return await IInstrumentationServiceImpl.instrumentServerAction( + "auth.sendPasswordRecovery", + { email }, + async () => { + try { + const supabase = createAdminClient(); + + const { error } = await supabase.auth.resetPasswordForEmail(email, { + redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/reset-password`, + }); + + if (error) { + console.error("Error sending password recovery:", error); + throw new DatabaseOperationError(error.message); + } + + return { + success: true, + message: "Password recovery email sent successfully", + }; + } catch (err) { + ICrashReporterServiceImpl.report(err); + throw new DatabaseOperationError("Failed to send password recovery email. Please try again."); + } + } + ); +} + +export async function sendMagicLink(email: string) { + return await IInstrumentationServiceImpl.instrumentServerAction( + "auth.sendMagicLink", + { email }, + async () => { + try { + const supabase = createAdminClient(); + + const { error } = await supabase.auth.signInWithOtp({ + email, + options: { + emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`, + }, + }); + + if (error) { + console.error("Error sending magic link:", error); + throw new DatabaseOperationError(error.message); + } + + return { + success: true, + message: "Magic link email sent successfully", + }; + } catch (err) { + ICrashReporterServiceImpl.report(err); + throw new DatabaseOperationError("Failed to send magic link email. Please try again."); + } + } + ); +} \ No newline at end of file diff --git a/sigap-website/src/application/repositories/users.repository.ts b/sigap-website/src/application/repositories/users.repository.ts index 0cc5508..f2a9658 100644 --- a/sigap-website/src/application/repositories/users.repository.ts +++ b/sigap-website/src/application/repositories/users.repository.ts @@ -2,313 +2,386 @@ import { createAdminClient } from "@/app/_utils/supabase/admin"; import { createClient } from "@/app/_utils/supabase/client"; import { CreateUserParams, InviteUserParams, UpdateUserParams, User, UserResponse } from "@/src/entities/models/users/users.model"; import db from "@/prisma/db"; -import { DatabaseOperationError, NotFoundError } from "../entities/errors/common"; -import { AuthenticationError } from "../entities/errors/auth"; +import { DatabaseOperationError, NotFoundError } from "@/src/entities/errors/common"; +import { AuthenticationError } from "@/src/entities/errors/auth"; +import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"; +import { ICrashReporterService } from "@/src/application/services/crash-reporter.service.interface"; export class UsersRepository { - private supabaseAdmin = createAdminClient(); - private supabaseClient = createClient(); + constructor( + private readonly instrumentationService: IInstrumentationService, + private readonly crashReporterService: ICrashReporterService, + private readonly supabaseAdmin = createAdminClient(), + private readonly supabaseClient = createClient() + ) { } - async fetchUsers(): Promise { - const users = await db.users.findMany({ - include: { - profile: true, - }, - }); + async getUsers(): Promise { + return await this.instrumentationService.startSpan({ + name: "UsersRepository > getUsers", + op: 'db.query', + attributes: { 'db.system': 'postgres' }, + }, async () => { + try { - if (!users) { - throw new NotFoundError("Users not found"); - } + const users = await db.users.findMany({ + include: { + profile: true, + }, + }); - return users; + if (!users) { + throw new NotFoundError("Users not found"); + } + + return users; + + } catch (err) { + this.crashReporterService.report(err); + throw err; + } + }) } async getCurrentUser(): Promise { - const supabase = await this.supabaseClient; + return await this.instrumentationService.startSpan({ + name: "UsersRepository > getCurrentUser", + op: 'db.query', + attributes: { 'db.system': 'postgres' }, + }, async () => { + try { + const supabase = await this.supabaseClient; - const { - data: { user }, - error, - } = await supabase.auth.getUser(); + const { + data: { user }, + error, + } = await supabase.auth.getUser(); - if (error) { - console.error("Error fetching current user:", error); - throw new AuthenticationError(error.message); - } + if (error) { + console.error("Error fetching current user:", error); + throw new AuthenticationError(error.message); + } - const userDetail = await db.users.findUnique({ - where: { - id: user?.id, - }, - include: { - profile: true, - }, - }); + const userDetail = await db.users.findUnique({ + where: { + id: user?.id, + }, + include: { + profile: true, + }, + }); - if (!userDetail) { - throw new NotFoundError("User not found"); - } + if (!userDetail) { + throw new NotFoundError("User not found"); + } - return { - data: { - user: userDetail, - }, - error: null, - }; + return { + data: { + user: userDetail, + }, + error: null, + }; + } catch (err) { + this.crashReporterService.report(err); + throw err; + } + }) } async createUser(params: CreateUserParams): Promise { - const supabase = this.supabaseAdmin; + return await this.instrumentationService.startSpan({ + name: "UsersRepository > createUser", + op: 'db.query', + attributes: { 'db.system': 'postgres' }, + }, async () => { + try { + const supabase = this.supabaseAdmin; - const { data, error } = await supabase.auth.admin.createUser({ - email: params.email, - password: params.password, - phone: params.phone, - email_confirm: params.email_confirm, - }); - - if (error) { - console.error("Error creating user:", error); - throw new DatabaseOperationError(error.message); - } - - return { - data: { - user: data.user, - }, - error: null, - }; - } - - async uploadAvatar(userId: string, email: string, file: File) { - try { - const supabase = await this.supabaseClient; - - const fileExt = file.name.split(".").pop(); - const emailName = email.split("@")[0]; - const fileName = `AVR-${emailName}.${fileExt}`; - - const filePath = `${userId}/${fileName}`; - - const { error: uploadError } = await supabase.storage - .from("avatars") - .upload(filePath, file, { - upsert: true, - contentType: file.type, + const { data, error } = await supabase.auth.admin.createUser({ + email: params.email, + password: params.password, + phone: params.phone, + email_confirm: params.email_confirm, }); - if (uploadError) { - console.error("Error uploading avatar:", uploadError); - throw new DatabaseOperationError(uploadError.message); + if (error) { + console.error("Error creating user:", error); + throw new DatabaseOperationError(error.message); + } + + return { + data: { + user: data.user, + }, + error: null, + }; + } catch (err) { + this.crashReporterService.report(err); + throw err; } - - const { - data: { publicUrl }, - } = supabase.storage.from("avatars").getPublicUrl(filePath); - - await db.users.update({ - where: { - id: userId, - }, - data: { - profile: { - update: { - avatar: publicUrl, - }, - }, - }, - }); - - return publicUrl; - } catch (error) { - console.error("Error uploading avatar:", error); - throw new DatabaseOperationError(error.message); - } - } - - async updateUser(userId: string, params: UpdateUserParams): Promise { - const supabase = this.supabaseAdmin; - - const { data, error } = await supabase.auth.admin.updateUserById(userId, { - email: params.email, - email_confirm: params.email_confirmed_at, - password: params.encrypted_password ?? undefined, - password_hash: params.encrypted_password ?? undefined, - phone: params.phone, - phone_confirm: params.phone_confirmed_at, - role: params.role, - user_metadata: params.user_metadata, - app_metadata: params.app_metadata, - }); - - if (error) { - console.error("Error updating user:", error); - throw new DatabaseOperationError(error.message); - } - - const user = await db.users.findUnique({ - where: { - id: userId, - }, - include: { - profile: true, - }, - }); - - if (!user) { - throw new NotFoundError("User not found"); - } - - const updateUser = await db.users.update({ - where: { - id: userId, - }, - data: { - role: params.role || user.role, - invited_at: params.invited_at || user.invited_at, - confirmed_at: params.confirmed_at || user.confirmed_at, - last_sign_in_at: params.last_sign_in_at || user.last_sign_in_at, - is_anonymous: params.is_anonymous || user.is_anonymous, - created_at: params.created_at || user.created_at, - updated_at: params.updated_at || user.updated_at, - profile: { - update: { - avatar: params.profile?.avatar || user.profile?.avatar, - username: params.profile?.username || user.profile?.username, - first_name: params.profile?.first_name || user.profile?.first_name, - last_name: params.profile?.last_name || user.profile?.last_name, - bio: params.profile?.bio || user.profile?.bio, - address: params.profile?.address || user.profile?.address, - birth_date: params.profile?.birth_date || user.profile?.birth_date, - }, - }, - }, - include: { - profile: true, - }, - }); - - return { - data: { - user: { - ...data.user, - role: params.role, - profile: { - user_id: userId, - ...updateUser.profile, - }, - }, - }, - error: null, - }; - } - - async deleteUser(userId: string): Promise { - const supabase = this.supabaseAdmin; - - const { error } = await supabase.auth.admin.deleteUser(userId); - - if (error) { - console.error("Error deleting user:", error); - throw new DatabaseOperationError(error.message); - } - } - - async sendPasswordRecovery(email: string): Promise { - const supabase = this.supabaseAdmin; - - const { error } = await supabase.auth.resetPasswordForEmail(email, { - redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/reset-password`, - }); - - if (error) { - console.error("Error sending password recovery:", error); - throw new DatabaseOperationError(error.message); - } - } - - async sendMagicLink(email: string): Promise { - const supabase = this.supabaseAdmin; - - const { error } = await supabase.auth.signInWithOtp({ - email, - options: { - emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`, - }, - }); - - if (error) { - console.error("Error sending magic link:", error); - throw new DatabaseOperationError(error.message); - } - } - - async banUser(userId: string): Promise { - const supabase = this.supabaseAdmin; - - const banUntil = new Date(); - banUntil.setFullYear(banUntil.getFullYear() + 100); - - const { data, error } = await supabase.auth.admin.updateUserById(userId, { - ban_duration: "100h", - }); - - if (error) { - console.error("Error banning user:", error); - throw new DatabaseOperationError(error.message); - } - - return { - data: { - user: data.user, - }, - error: null, - }; - } - - async unbanUser(userId: string): Promise { - const supabase = this.supabaseAdmin; - - const { data, error } = await supabase.auth.admin.updateUserById(userId, { - ban_duration: "none", - }); - - if (error) { - console.error("Error unbanning user:", error); - throw new DatabaseOperationError(error.message); - } - - const user = await db.users.findUnique({ - where: { - id: userId, - }, - select: { - banned_until: true, - } - }); - - if (!user) { - throw new NotFoundError("User not found"); - } - - return { - data: { - user: data.user, - }, - error: null, - }; + }) } async inviteUser(params: InviteUserParams): Promise { - const supabase = this.supabaseAdmin; + return await this.instrumentationService.startSpan({ + name: "UsersRepository > inviteUser", + op: 'db.query', + attributes: { 'db.system': 'postgres' }, + }, async () => { + try { + const supabase = this.supabaseAdmin; - const { error } = await supabase.auth.admin.inviteUserByEmail(params.email, { - redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/api/auth/callback`, - }); + const { error } = await supabase.auth.admin.inviteUserByEmail(params.email, { + redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/api/auth/callback`, + }); - if (error) { - console.error("Error inviting user:", error); - throw new DatabaseOperationError(error.message); - } + if (error) { + console.error("Error inviting user:", error); + throw new DatabaseOperationError(error.message); + } + } catch (err) { + this.crashReporterService.report(err); + throw err; + } + }) + } + + async uploadAvatar(userId: string, email: string, file: File) { + return await this.instrumentationService.startSpan({ + name: "UsersRepository > uploadAvatar", + op: 'db.query', + attributes: { 'db.system': 'postgres' }, + }, async () => { + try { + const supabase = await this.supabaseClient; + + const fileExt = file.name.split(".").pop(); + const emailName = email.split("@")[0]; + const fileName = `AVR-${emailName}.${fileExt}`; + + const filePath = `${userId}/${fileName}`; + + const { error: uploadError } = await supabase.storage + .from("avatars") + .upload(filePath, file, { + upsert: true, + contentType: file.type, + }); + + if (uploadError) { + console.error("Error uploading avatar:", uploadError); + throw new DatabaseOperationError(uploadError.message); + } + + const { + data: { publicUrl }, + } = supabase.storage.from("avatars").getPublicUrl(filePath); + + await db.users.update({ + where: { + id: userId, + }, + data: { + profile: { + update: { + avatar: publicUrl, + }, + }, + }, + }); + + return publicUrl; + } catch (err) { + this.crashReporterService.report(err); + throw err; + } + }) + } + + async updateUser(userId: string, params: UpdateUserParams): Promise { + return await this.instrumentationService.startSpan({ + name: "UsersRepository > updateUser", + op: 'db.query', + attributes: { 'db.system': 'postgres' }, + }, async () => { + try { + const supabase = this.supabaseAdmin; + + const { data, error } = await supabase.auth.admin.updateUserById(userId, { + email: params.email, + email_confirm: params.email_confirmed_at, + password: params.encrypted_password ?? undefined, + password_hash: params.encrypted_password ?? undefined, + phone: params.phone, + phone_confirm: params.phone_confirmed_at, + role: params.role, + user_metadata: params.user_metadata, + app_metadata: params.app_metadata, + }); + + if (error) { + console.error("Error updating user:", error); + throw new DatabaseOperationError(error.message); + } + + const user = await db.users.findUnique({ + where: { + id: userId, + }, + include: { + profile: true, + }, + }); + + if (!user) { + throw new NotFoundError("User not found"); + } + + const updateUser = await db.users.update({ + where: { + id: userId, + }, + data: { + role: params.role || user.role, + invited_at: params.invited_at || user.invited_at, + confirmed_at: params.confirmed_at || user.confirmed_at, + last_sign_in_at: params.last_sign_in_at || user.last_sign_in_at, + is_anonymous: params.is_anonymous || user.is_anonymous, + created_at: params.created_at || user.created_at, + updated_at: params.updated_at || user.updated_at, + profile: { + update: { + avatar: params.profile?.avatar || user.profile?.avatar, + username: params.profile?.username || user.profile?.username, + first_name: params.profile?.first_name || user.profile?.first_name, + last_name: params.profile?.last_name || user.profile?.last_name, + bio: params.profile?.bio || user.profile?.bio, + address: params.profile?.address || user.profile?.address, + birth_date: params.profile?.birth_date || user.profile?.birth_date, + }, + }, + }, + include: { + profile: true, + }, + }); + + return { + data: { + user: { + ...data.user, + role: params.role, + profile: { + user_id: userId, + ...updateUser.profile, + }, + }, + }, + error: null, + }; + } catch (err) { + this.crashReporterService.report(err); + throw err; + } + }) + } + + async deleteUser(userId: string): Promise { + return await this.instrumentationService.startSpan({ + name: "UsersRepository > deleteUser", + op: 'db.query', + attributes: { 'db.system': 'postgres' }, + }, async () => { + try { + const supabase = this.supabaseAdmin; + + const { error } = await supabase.auth.admin.deleteUser(userId); + + if (error) { + console.error("Error deleting user:", error); + throw new DatabaseOperationError(error.message); + } + } catch (err) { + this.crashReporterService.report(err); + throw err; + } + }) + } + + async banUser(userId: string): Promise { + return await this.instrumentationService.startSpan({ + name: "UsersRepository > banUser", + op: 'db.query', + attributes: { 'db.system': 'postgres' }, + }, async () => { + try { + const supabase = this.supabaseAdmin; + + const banUntil = new Date(); + banUntil.setFullYear(banUntil.getFullYear() + 100); + + const { data, error } = await supabase.auth.admin.updateUserById(userId, { + ban_duration: "100h", + }); + + if (error) { + console.error("Error banning user:", error); + throw new DatabaseOperationError(error.message); + } + + return { + data: { + user: data.user, + }, + error: null, + }; + } catch (err) { + this.crashReporterService.report(err); + throw err; + } + }) + } + + async unbanUser(userId: string): Promise { + return await this.instrumentationService.startSpan({ + name: "UsersRepository > unbanUser", + op: 'db.query', + attributes: { 'db.system': 'postgres' }, + }, async () => { + try { + const supabase = this.supabaseAdmin; + + const { data, error } = await supabase.auth.admin.updateUserById(userId, { + ban_duration: "none", + }); + + if (error) { + console.error("Error unbanning user:", error); + throw new DatabaseOperationError(error.message); + } + + const user = await db.users.findUnique({ + where: { + id: userId, + }, + select: { + banned_until: true, + } + }); + + if (!user) { + throw new NotFoundError("User not found"); + } + + return { + data: { + user: data.user, + }, + error: null, + }; + } catch (err) { + this.crashReporterService.report(err); + throw err; + } + }) } } \ No newline at end of file diff --git a/sigap-website/src/application/services/crash-reporter.service.interface.ts b/sigap-website/src/application/services/crash-reporter.service.interface.ts index b94f6b7..03f0fc7 100644 --- a/sigap-website/src/application/services/crash-reporter.service.interface.ts +++ b/sigap-website/src/application/services/crash-reporter.service.interface.ts @@ -1,3 +1,12 @@ export interface ICrashReporterService { report(error: any): string; - } \ No newline at end of file + } + +class CrashReporterService implements ICrashReporterService { + report(error: any): string { + // Implementation of the report method + return "Error reported"; + } +} + +export const ICrashReporterServiceImpl = new CrashReporterService(); \ No newline at end of file diff --git a/sigap-website/src/application/services/instrumentation.service.interface.ts b/sigap-website/src/application/services/instrumentation.service.interface.ts index 9c8ff0f..fa994c1 100644 --- a/sigap-website/src/application/services/instrumentation.service.interface.ts +++ b/sigap-website/src/application/services/instrumentation.service.interface.ts @@ -8,4 +8,25 @@ export interface IInstrumentationService { options: Record, callback: () => T ): Promise; - } \ No newline at end of file + } + +class InstrumentationService implements IInstrumentationService { + startSpan( + options: { name: string; op?: string; attributes?: Record }, + callback: () => T + ): T { + // Implementation of the startSpan method + return callback(); + } + + async instrumentServerAction( + name: string, + options: Record, + callback: () => T + ): Promise { + // Implementation of the instrumentServerAction method + return callback(); + } +} + +export const IInstrumentationServiceImpl = new InstrumentationService(); \ No newline at end of file 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 286f315..b7d96d9 100644 --- a/sigap-website/src/entities/models/auth/sign-in.model.ts +++ b/sigap-website/src/entities/models/auth/sign-in.model.ts @@ -1,15 +1,15 @@ import { z } from "zod"; // Define the sign-in form schema using Zod -export const signInSchema = z.object({ +export const SignInSchema = z.object({ email: z .string() .min(1, { message: "Email is required" }) - .email({ message: "Invalid email address" }), + .email({ message: "Please enter a valid email address" }), }); // Export the type derived from the schema -export type SignInFormData = z.infer; +export type SignInFormData = z.infer; // Default values for the form export const defaultSignInValues: SignInFormData = { diff --git a/sigap-website/src/interface-adapters/controllers/auth/auth-controller.tsx b/sigap-website/src/interface-adapters/controllers/auth/auth-controller.tsx new file mode 100644 index 0000000..58418db --- /dev/null +++ b/sigap-website/src/interface-adapters/controllers/auth/auth-controller.tsx @@ -0,0 +1,123 @@ +"use client"; + +import { useMutation } from '@tanstack/react-query'; +import { toast } from 'sonner'; +import { SignInFormData } from '@/src/entities/models/auth/sign-in.model'; +import { VerifyOtpFormData } from '@/src/entities/models/auth/verify-otp.model'; +import { useNavigations } from '@/app/_hooks/use-navigations'; +import { AuthenticationError } from '@/src/entities/errors/auth'; +import * as authRepository from '@/src/application/repositories/authentication.repository'; + +export function useAuthMutation() { + const { router } = useNavigations(); + + // Sign In Mutation + const signInMutation = useMutation({ + mutationFn: async (data: SignInFormData) => { + return await authRepository.signIn(data); + }, + onSuccess: (result) => { + toast.success(result.message); + if (result.redirectTo && result.success) { + router.push(result.redirectTo); + } + }, + onError: (error) => { + if (error instanceof AuthenticationError) { + toast.error(`Authentication failed: ${error.message}`); + } else { + toast.error('Failed to sign in. Please try again later.'); + } + } + }); + + // Verify OTP Mutation + const verifyOtpMutation = useMutation({ + mutationFn: async (data: VerifyOtpFormData) => { + return await authRepository.verifyOtp(data); + }, + onSuccess: (result) => { + toast.success(result.message); + if (result.redirectTo) { + router.push(result.redirectTo); + } + }, + onError: (error) => { + if (error instanceof AuthenticationError) { + toast.error(`Verification failed: ${error.message}`); + } else { + toast.error('Failed to verify OTP. Please try again.'); + } + } + }); + + // Sign Out Mutation + const signOutMutation = useMutation({ + mutationFn: async () => { + return await authRepository.signOut(); + }, + onSuccess: (result) => { + toast.success(result.message); + if (result.redirectTo) { + router.push(result.redirectTo); + } + }, + onError: (error) => { + toast.error('Failed to sign out. Please try again.'); + } + }); + + // Password Recovery Mutation + const passwordRecoveryMutation = useMutation({ + mutationFn: async (email: string) => { + return await authRepository.sendPasswordRecovery(email); + }, + onSuccess: (result) => { + toast.success(result.message); + }, + onError: (error) => { + toast.error('Failed to send password recovery email. Please try again.'); + } + }); + + // Magic Link Mutation + const magicLinkMutation = useMutation({ + mutationFn: async (email: string) => { + return await authRepository.sendMagicLink(email); + }, + onSuccess: (result) => { + toast.success(result.message); + }, + onError: (error) => { + toast.error('Failed to send magic link email. Please try again.'); + } + }); + + return { + signIn: { + mutate: signInMutation.mutateAsync, + isPending: signInMutation.isPending, + error: signInMutation.error, + }, + verifyOtp: { + mutate: verifyOtpMutation.mutateAsync, + isPending: verifyOtpMutation.isPending, + error: verifyOtpMutation.error, + }, + signOut: { + mutate: signOutMutation.mutateAsync, + isPending: signOutMutation.isPending, + error: signOutMutation.error, + }, + passwordRecovery: { + mutate: passwordRecoveryMutation.mutateAsync, + isPending: passwordRecoveryMutation.isPending, + error: passwordRecoveryMutation.error, + }, + magicLink: { + mutate: magicLinkMutation.mutateAsync, + isPending: magicLinkMutation.isPending, + error: magicLinkMutation.error, + } + }; +} \ No newline at end of file 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 85fb85e..886ea12 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 @@ -1,97 +1,194 @@ -// src/controllers/sign-in.controller.tsx "use client"; - import { useRouter } from "next/navigation"; import { defaultSignInValues, SignInFormData, - signInSchema, + SignInSchema, } from "@/src/entities/models/auth/sign-in.model"; import { useState, type FormEvent, type ChangeEvent } from "react"; import { toast } from "sonner"; import { z } from "zod"; -import { signIn } from "@/app/(pages)/(auth)/action"; +// import { signIn } from ""; +import { useAuthMutation } from "./auth-controller"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { AuthenticationError } from "@/src/entities/errors/auth"; type SignInFormErrors = Partial>; -export function useSignInForm() { - const [formData, setFormData] = useState(defaultSignInValues); - const [errors, setErrors] = useState({}); - const [isSubmitting, setIsSubmitting] = useState(false); - const [message, setMessage] = useState(null); - const router = useRouter(); +// export function useSignInForm() { +// const [formData, setFormData] = useState(defaultSignInValues); +// const [errors, setErrors] = useState({}); +// const [isSubmitting, setIsSubmitting] = useState(false); +// const [message, setMessage] = useState(null); +// const router = useRouter(); - const validateForm = (): boolean => { - try { - signInSchema.parse(formData); - setErrors({}); - return true; - } catch (error) { - if (error instanceof z.ZodError) { - const formattedErrors: SignInFormErrors = {}; - error.errors.forEach((err) => { - const path = err.path[0] as keyof SignInFormData; - formattedErrors[path] = err.message; - }); - setErrors(formattedErrors); - } - return false; - } - }; +// const validateForm = (): boolean => { +// try { +// SignInSchema.parse(formData); +// setErrors({}); +// return true; +// } catch (error) { +// if (error instanceof z.ZodError) { +// const formattedErrors: SignInFormErrors = {}; +// error.errors.forEach((err) => { +// const path = err.path[0] as keyof SignInFormData; +// formattedErrors[path] = err.message; +// }); +// setErrors(formattedErrors); +// } +// return false; +// } +// }; - const handleChange = (e: ChangeEvent) => { - const { name, value } = e.target; - setFormData((prev) => ({ - ...prev, - [name]: value, - })); - }; +// const handleChange = (e: ChangeEvent) => { +// const { name, value } = e.target; +// setFormData((prev) => ({ +// ...prev, +// [name]: value, +// })); +// }; - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - if (!validateForm()) { - return; - } +// const handleSubmit = async (e: FormEvent) => { +// e.preventDefault(); +// if (!validateForm()) { +// return; +// } - setIsSubmitting(true); - setMessage(null); +// setIsSubmitting(true); +// setMessage(null); - try { - const result = await signIn(formData); +// try { +// const result = await signIn(formData); - if (result.success) { - setMessage(result.message); - toast.success(result.message); +// if (result.success) { +// setMessage(result.message); +// toast.success(result.message); - // Handle client-side navigation - if (result.redirectTo) { - router.push(result.redirectTo); - } - } else { - setErrors({ - email: result.message || "Sign in failed. Please try again.", - }); - toast.error(result.message || "Sign in failed. Please try again."); - } +// // Handle client-side navigation +// if (result.redirectTo) { +// router.push(result.redirectTo); +// } +// } else { +// setErrors({ +// email: result.message || "Sign in failed. Please try again.", +// }); +// toast.error(result.message || "Sign in failed. Please try again."); +// } +// } catch (error) { +// console.error("Sign in failed", error); +// setErrors({ +// email: "An unexpected error occurred. Please try again.", +// }); +// toast.error("An unexpected error occurred. Please try again."); +// } finally { +// setIsSubmitting(false); +// } +// }; + +// return { +// formData, +// errors, +// isSubmitting, +// message, +// setFormData, +// handleChange, +// handleSubmit, +// }; +// } + +// export function useSignInController() { +// const [formData, setFormData] = useState(defaultSignInValues); +// const [errors, setErrors] = useState>({}); + +// const { signIn } = useAuthMutation(); + +// const form = useForm({ +// resolver: zodResolver(SignInSchema), +// defaultValues: defaultSignInValues, +// }); + +// // Handle input changes +// const handleChange = (e: React.ChangeEvent) => { +// const { name, value } = e.target; +// setFormData(prev => ({ +// ...prev, +// [name]: value +// })); + +// // Clear error when user starts typing +// if (errors[name]) { +// setErrors(prev => ({ +// ...prev, +// [name]: '' +// })); +// } +// }; + +// // Direct handleSubmit handler for the form +// const handleSubmit = async (e: React.FormEvent) => { +// e.preventDefault(); +// setErrors({}); + +// try { +// // Basic email validation before sending to API +// if (!formData.email || !formData.email.includes('@')) { +// setErrors({ email: 'Please enter a valid email address' }); +// return; +// } + +// await signIn.mutate(formData); +// } catch (error) { +// // This catch block will likely not be used since errors are handled in the mutation +// console.error("Form submission error:", error); +// } +// }; + +// // Combine form validation errors with API errors +// const formErrors = { +// ...errors, +// // If there's an API error from the mutation, add it to the appropriate field +// ...(signIn.error instanceof AuthenticationError ? +// { email: signIn.error.message } : +// {}) +// }; + +// return { +// formData, +// handleChange, +// handleSubmit, +// isPending: signIn.isPending, +// error: formErrors +// }; +// } + +export function useSignInController() { + const { signIn } = useAuthMutation(); + + // Gunakan react-hook-form untuk mengelola form state & error handling + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(SignInSchema), + defaultValues: defaultSignInValues, + }); + + // Handler untuk submit form + const onSubmit = handleSubmit(async (data) => { + try { + signIn.mutate(data); } catch (error) { - console.error("Sign in failed", error); - setErrors({ - email: "An unexpected error occurred. Please try again.", - }); - toast.error("An unexpected error occurred. Please try again."); - } finally { - setIsSubmitting(false); + console.error("Sign-in submission error:", error); } - }; + }); return { - formData, + register, + handleSubmit: onSubmit, errors, - isSubmitting, - message, - setFormData, - handleChange, - handleSubmit, + isPending: signIn.isPending, }; } \ No newline at end of file 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 68fab3f..4461956 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,11 +1,11 @@ // src/hooks/useVerifyOtpForm.ts "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { verifyOtp } from "@/app/(pages)/(auth)/action"; +// import { verifyOtp } from ""; import { defaultVerifyOtpValues, VerifyOtpFormData, @@ -13,51 +13,104 @@ import { } from "@/src/entities/models/auth/verify-otp.model"; import { useNavigations } from "@/app/_hooks/use-navigations"; import { toast } from "sonner"; +import { useAuthMutation } from "./auth-controller"; -export function useVerifyOtpForm(initialEmail: string) { - const [isSubmitting, setIsSubmitting] = useState(false); - const [message, setMessage] = useState(null); - const { router } = useNavigations(); +// export function useVerifyOtpForm(email: string) { +// const [isSubmitting, setIsSubmitting] = useState(false); +// const [message, setMessage] = useState(null); +// const { router } = useNavigations(); - const form = useForm({ +// const form = useForm({ +// resolver: zodResolver(verifyOtpSchema), +// defaultValues: { ...defaultVerifyOtpValues, email: email }, +// }); + +// const onSubmit = async (data: VerifyOtpFormData) => { +// setIsSubmitting(true); +// setMessage(null); + +// try { +// const result = await verifyOtp(data); + +// if (result.success) { +// setMessage(result.message); +// // Redirect or update UI state as needed +// toast.success(result.message); +// if (result.redirectTo) { +// router.push(result.redirectTo); +// } +// } else { +// toast.error(result.message); +// form.setError("token", { type: "manual", message: result.message }); +// } +// } catch (error) { +// console.error("OTP verification failed", error); +// toast.error("An unexpected error occurred. Please try again."); +// form.setError("token", { +// type: "manual", +// message: "An unexpected error occurred. Please try again.", +// }); +// } finally { +// setIsSubmitting(false); +// } +// }; + +// return { +// form, +// isSubmitting, +// message, +// onSubmit, +// }; +// } + +export const useVerifyOtpController = (email: string) => { + const { verifyOtp } = useAuthMutation() + + const { + control, + register, + handleSubmit, + reset, + formState: { errors, isSubmitSuccessful }, + } = useForm({ resolver: zodResolver(verifyOtpSchema), - defaultValues: { ...defaultVerifyOtpValues, email: initialEmail }, - }); + defaultValues: { ...defaultVerifyOtpValues, email: email }, + }) - const onSubmit = async (data: VerifyOtpFormData) => { - setIsSubmitting(true); - setMessage(null); - - try { - const result = await verifyOtp(data); - - if (result.success) { - setMessage(result.message); - // Redirect or update UI state as needed - toast.success(result.message); - if (result.redirectTo) { - router.push(result.redirectTo); - } - } else { - toast.error(result.message); - form.setError("token", { type: "manual", message: result.message }); - } - } catch (error) { - console.error("OTP verification failed", error); - toast.error("An unexpected error occurred. Please try again."); - form.setError("token", { - type: "manual", - message: "An unexpected error occurred. Please try again.", - }); - } finally { - setIsSubmitting(false); + // 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) + } + }) + + // 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 + } + } return { - form, - isSubmitting, - message, - onSubmit, - }; + control, + register, + handleSubmit: onSubmit, + handleOtpChange, + errors, + isPending: verifyOtp.isPending, + } } +