Refactor structure auth

This commit is contained in:
vergiLgood1 2025-03-14 16:37:06 +07:00
parent 18a7be5c19
commit 5830eedb18
14 changed files with 1201 additions and 623 deletions

View File

@ -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 }) {
/>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onSubmit={signOut} className="space-x-2">
<DropdownMenuItem onSubmit={() => { }} className="space-x-2">
<IconLogout className="size-4" />
<span>Log out</span>
</DropdownMenuItem>

View File

@ -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 (
<div>
@ -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}
>
<Lock className="mr-2 h-5 w-5" />
Continue with SSO
@ -64,32 +57,36 @@ export function SignInForm({
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-4" {...props}>
<form onSubmit={handleSubmit} className="space-y-4" {...props} noValidate>
<FormField
label="Email"
input={
<Input
id="email"
type="email"
name="email"
{...register("email")}
placeholder="you@example.com"
className={`bg-[#1C1C1C] border-gray-800 text-white placeholder:text-gray-500 focus:border-emerald-600 focus:ring-emerald-600 ${
errors.email ? "border-red-500" : ""
className={`bg-[#1C1C1C] border-gray-800 ${errors.email ? "ring-red-500 focus-visible:ring-red-500" : ""
}`}
value={formData.email}
onChange={handleChange}
disabled={isSubmitting}
disabled={isPending}
/>
}
error={errors.email}
error={errors.email ? errors.email.message : undefined}
/>
<SubmitButton
<Button
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white"
size="lg"
pendingText="Signing In..."
disabled={isPending}
>
Sign In
</SubmitButton>
{isPending ? (
<>
<Loader2 className="h-5 w-5 animate-spin" />
Signing in...
</>
) : (
"Sign in"
)}
</Button>
</form>
<div className="text-center text-lg">

View File

@ -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<HTMLDivElement> {}
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 (
<div className={cn("flex flex-col gap-6", className)} {...props}>
<Card className="bg-[#171717] border-gray-800 text-white border-none">
<CardHeader className="text-center">
<CardTitle className="text-2xl font-bold">
One-Time Password
</CardTitle>
<CardTitle className="text-2xl font-bold">One-Time Password</CardTitle>
<CardDescription className="text-gray-400">
One time password is a security feature that helps protect your data
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<input type="hidden" name="email" value={email} />
<FormField
control={form.control}
<form onSubmit={handleSubmit} className="space-y-4">
<input type="hidden" {...register("email")} />
<div className="space-y-6">
<Controller
name="token"
control={control}
render={({ field }) => (
<FormItem>
<FormControl>
<InputOTP maxLength={6} {...field}>
<InputOTPGroup className="flex w-full items-center justify-center space-x-2">
{[...Array(6)].map((_, index) => (
<InputOTPSlot
key={index}
index={index}
className="w-12 h-12 text-xl border-2 border-gray-700 bg-[#1C1C1C] text-white rounded-md focus:border-emerald-600 focus:ring-emerald-600"
/>
))}
</InputOTPGroup>
</InputOTP>
</FormControl>
<FormDescription className="flex w-full justify-center items-center text-gray-400">
Please enter the one-time password sent to {email}.
</FormDescription>
<FormMessage className="text-red-400" />
</FormItem>
<InputOTP
maxLength={6}
value={field.value || ""}
onChange={(value) => handleOtpChange(value, field.onChange)}
>
<InputOTPGroup className="flex w-full items-center justify-center space-x-2">
{[...Array(6)].map((_, index) => (
<InputOTPSlot
key={index}
index={index}
className={`w-12 h-12 text-xl border-2 border-gray-700 bg-[#1C1C1C] rounded-md ${errors.token ? "ring-red-400 ring-2 ring-offset-0" : ""}`}
/>
))}
</InputOTPGroup>
</InputOTP>
)}
/>
<div className="flex justify-center">
<SubmitButton
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white"
pendingText="Verifying..."
disabled={isSubmitting}
>
Submit
</SubmitButton>
{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">
Successfully verified!
</div>}
<div className="flex w-full justify-center items-center text-gray-400 text-sm">
Please enter the one-time password sent to {email}.
</div>
{message && (
<div className="text-center text-emerald-500">{message}</div>
)}
</form>
</Form>
</div>
<div className="flex justify-center">
<SubmitButton
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white"
disabled={isPending}
>
{isPending ? (
<>
<Loader2 className="h-5 w-5 animate-spin" />
Verifying...
</>
) : (
"Verify OTP"
)}
</SubmitButton>
</div>
</form>
</CardContent>
</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">
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 our <a href="#">Terms of Service</a> and <a href="#">Privacy Policy</a>.
</div>
</div>
);
)
}

View File

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

View File

@ -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<Message> }) {
const searchParams = await props.searchParams;
return (
<div className="grid min-h-svh lg:grid-cols-5">
<div className="flex flex-col gap-4 p-6 md:p-10 bg-[#171717] lg:col-span-2 relative border border-r-2 border-r-gray-400 border-opacity-20">

View File

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

View File

@ -1,65 +1,343 @@
// src/repositories/auth.repository.ts
// // src/repositories/auth.repository.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 { 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<void> {
// 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<void> {
// 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 "../entities/models/auth/sign-in.model";
import { VerifyOtpFormData } from "../entities/models/auth/verify-otp.model";
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";
export class AuthRepository {
async signIn({ email }: SignInFormData) {
const supabase = await createClient();
const { data, error } = await supabase.auth.signInWithOtp({
email,
options: {
shouldCreateUser: false,
},
});
// 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) {
throw new Error(error.message);
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();
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.");
}
}
);
}

View File

@ -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<User[]> {
const users = await db.users.findMany({
include: {
profile: true,
},
});
async getUsers(): Promise<User[]> {
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<UserResponse> {
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<UserResponse> {
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<UserResponse> {
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<void> {
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<void> {
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<void> {
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<UserResponse> {
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<UserResponse> {
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<void> {
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<UserResponse> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > updateUser",
op: 'db.query',
attributes: { 'db.system': 'postgres' },
}, async () => {
try {
const supabase = this.supabaseAdmin;
const { data, error } = await supabase.auth.admin.updateUserById(userId, {
email: params.email,
email_confirm: params.email_confirmed_at,
password: params.encrypted_password ?? undefined,
password_hash: params.encrypted_password ?? undefined,
phone: params.phone,
phone_confirm: params.phone_confirmed_at,
role: params.role,
user_metadata: params.user_metadata,
app_metadata: params.app_metadata,
});
if (error) {
console.error("Error updating user:", error);
throw new DatabaseOperationError(error.message);
}
const user = await db.users.findUnique({
where: {
id: userId,
},
include: {
profile: true,
},
});
if (!user) {
throw new NotFoundError("User not found");
}
const updateUser = await db.users.update({
where: {
id: userId,
},
data: {
role: params.role || user.role,
invited_at: params.invited_at || user.invited_at,
confirmed_at: params.confirmed_at || user.confirmed_at,
last_sign_in_at: params.last_sign_in_at || user.last_sign_in_at,
is_anonymous: params.is_anonymous || user.is_anonymous,
created_at: params.created_at || user.created_at,
updated_at: params.updated_at || user.updated_at,
profile: {
update: {
avatar: params.profile?.avatar || user.profile?.avatar,
username: params.profile?.username || user.profile?.username,
first_name: params.profile?.first_name || user.profile?.first_name,
last_name: params.profile?.last_name || user.profile?.last_name,
bio: params.profile?.bio || user.profile?.bio,
address: params.profile?.address || user.profile?.address,
birth_date: params.profile?.birth_date || user.profile?.birth_date,
},
},
},
include: {
profile: true,
},
});
return {
data: {
user: {
...data.user,
role: params.role,
profile: {
user_id: userId,
...updateUser.profile,
},
},
},
error: null,
};
} catch (err) {
this.crashReporterService.report(err);
throw err;
}
})
}
async deleteUser(userId: string): Promise<void> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > deleteUser",
op: 'db.query',
attributes: { 'db.system': 'postgres' },
}, async () => {
try {
const supabase = this.supabaseAdmin;
const { error } = await supabase.auth.admin.deleteUser(userId);
if (error) {
console.error("Error deleting user:", error);
throw new DatabaseOperationError(error.message);
}
} catch (err) {
this.crashReporterService.report(err);
throw err;
}
})
}
async banUser(userId: string): Promise<UserResponse> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > banUser",
op: 'db.query',
attributes: { 'db.system': 'postgres' },
}, async () => {
try {
const supabase = this.supabaseAdmin;
const 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<UserResponse> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > unbanUser",
op: 'db.query',
attributes: { 'db.system': 'postgres' },
}, async () => {
try {
const supabase = this.supabaseAdmin;
const { data, error } = await supabase.auth.admin.updateUserById(userId, {
ban_duration: "none",
});
if (error) {
console.error("Error unbanning user:", error);
throw new DatabaseOperationError(error.message);
}
const user = await db.users.findUnique({
where: {
id: userId,
},
select: {
banned_until: true,
}
});
if (!user) {
throw new NotFoundError("User not found");
}
return {
data: {
user: data.user,
},
error: null,
};
} catch (err) {
this.crashReporterService.report(err);
throw err;
}
})
}
}

View File

@ -1,3 +1,12 @@
export interface ICrashReporterService {
report(error: any): string;
}
class CrashReporterService implements ICrashReporterService {
report(error: any): string {
// Implementation of the report method
return "Error reported";
}
}
export const ICrashReporterServiceImpl = new CrashReporterService();

View File

@ -9,3 +9,24 @@ export interface IInstrumentationService {
callback: () => T
): Promise<T>;
}
class InstrumentationService implements IInstrumentationService {
startSpan<T>(
options: { name: string; op?: string; attributes?: Record<string, any> },
callback: () => T
): T {
// Implementation of the startSpan method
return callback();
}
async instrumentServerAction<T>(
name: string,
options: Record<string, any>,
callback: () => T
): Promise<T> {
// Implementation of the instrumentServerAction method
return callback();
}
}
export const IInstrumentationServiceImpl = new InstrumentationService();

View File

@ -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<typeof signInSchema>;
export type SignInFormData = z.infer<typeof SignInSchema>;
// Default values for the form
export const defaultSignInValues: SignInFormData = {

View File

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

View File

@ -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<Record<keyof SignInFormData, string>>;
export function useSignInForm() {
const [formData, setFormData] = useState<SignInFormData>(defaultSignInValues);
const [errors, setErrors] = useState<SignInFormErrors>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [message, setMessage] = useState<string | null>(null);
const router = useRouter();
// export function useSignInForm() {
// const [formData, setFormData] = useState<SignInFormData>(defaultSignInValues);
// const [errors, setErrors] = useState<SignInFormErrors>({});
// const [isSubmitting, setIsSubmitting] = useState(false);
// const [message, setMessage] = useState<string | null>(null);
// const router = useRouter();
const validateForm = (): boolean => {
// 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<HTMLInputElement>) => {
// const { name, value } = e.target;
// setFormData((prev) => ({
// ...prev,
// [name]: value,
// }));
// };
// const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
// e.preventDefault();
// if (!validateForm()) {
// return;
// }
// setIsSubmitting(true);
// setMessage(null);
// try {
// const result = await signIn(formData);
// 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.");
// }
// } 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<SignInFormData>(defaultSignInValues);
// const [errors, setErrors] = useState<Record<string, string>>({});
// const { signIn } = useAuthMutation();
// const form = useForm<SignInFormData>({
// resolver: zodResolver(SignInSchema),
// defaultValues: defaultSignInValues,
// });
// // Handle input changes
// const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// 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<HTMLFormElement>) => {
// 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<SignInFormData>({
resolver: zodResolver(SignInSchema),
defaultValues: defaultSignInValues,
});
// Handler untuk submit form
const onSubmit = handleSubmit(async (data) => {
try {
signInSchema.parse(formData);
setErrors({});
return true;
signIn.mutate(data);
} 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;
console.error("Sign-in submission error:", error);
}
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
};
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsSubmitting(true);
setMessage(null);
try {
const result = await signIn(formData);
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.");
}
} 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,
register,
handleSubmit: onSubmit,
errors,
isSubmitting,
message,
setFormData,
handleChange,
handleSubmit,
isPending: signIn.isPending,
};
}

View File

@ -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<string | null>(null);
const { router } = useNavigations();
// export function useVerifyOtpForm(email: string) {
// const [isSubmitting, setIsSubmitting] = useState(false);
// const [message, setMessage] = useState<string | null>(null);
// const { router } = useNavigations();
const form = useForm<VerifyOtpFormData>({
// const form = useForm<VerifyOtpFormData>({
// 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<VerifyOtpFormData>({
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,
}
}