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"; } from "@/app/_components/ui/sidebar";
import { IconLogout, IconSettings, IconSparkles } from "@tabler/icons-react"; import { IconLogout, IconSettings, IconSparkles } from "@tabler/icons-react";
import type { User } from "@/src/entities/models/users/users.model"; import type { User } from "@/src/entities/models/users/users.model";
import { signOut } from "@/app/(pages)/(auth)/action"; // import { signOut } from "@/app/(pages)/(auth)/action";
import { SettingsDialog } from "../settings/setting-dialog"; import { SettingsDialog } from "../settings/setting-dialog";
export function NavUser({ user }: { user: User | null }) { export function NavUser({ user }: { user: User | null }) {
@ -131,7 +131,7 @@ export function NavUser({ user }: { user: User | null }) {
/> />
</DropdownMenuGroup> </DropdownMenuGroup>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onSubmit={signOut} className="space-x-2"> <DropdownMenuItem onSubmit={() => { }} className="space-x-2">
<IconLogout className="size-4" /> <IconLogout className="size-4" />
<span>Log out</span> <span>Log out</span>
</DropdownMenuItem> </DropdownMenuItem>

View File

@ -1,27 +1,20 @@
"use client"; "use client";
import type React from "react"; import type React from "react";
import { Loader2, Lock } from "lucide-react";
import { Lock } from "lucide-react"; import { Button } from "@/app/_components/ui/button";
import { Button } from "../../../_components/ui/button"; import { Input } from "@/app/_components/ui/input";
import { Input } from "../../../_components/ui/input"; import { SubmitButton } from "@/app/_components/submit-button";
import { SubmitButton } from "../../../_components/submit-button";
import Link from "next/link"; import Link from "next/link";
import { FormField } from "../../../_components/form-field"; import { FormField } from "@/app/_components/form-field";
import { useSignInForm } from "@/src/interface-adapters/controllers/auth/sign-in-controller"; import { useSignInController } from "@/src/interface-adapters/controllers/auth/sign-in-controller";
export function SignInForm({ export function SignInForm({
className, className,
...props ...props
}: React.ComponentPropsWithoutRef<"form">) { }: React.ComponentPropsWithoutRef<"form">) {
const {
formData, const { register, isPending, handleSubmit, errors } = useSignInController();
errors,
isSubmitting,
message,
handleChange,
handleSubmit,
} = useSignInForm();
return ( return (
<div> <div>
@ -48,7 +41,7 @@ export function SignInForm({
variant="outline" variant="outline"
className="w-full bg-[#1C1C1C] text-white border-gray-800 hover:bg-[#2C2C2C] hover:border-gray-700" className="w-full bg-[#1C1C1C] text-white border-gray-800 hover:bg-[#2C2C2C] hover:border-gray-700"
size="lg" size="lg"
disabled={isSubmitting} disabled={isPending}
> >
<Lock className="mr-2 h-5 w-5" /> <Lock className="mr-2 h-5 w-5" />
Continue with SSO Continue with SSO
@ -64,32 +57,36 @@ export function SignInForm({
</div> </div>
</div> </div>
<form onSubmit={handleSubmit} className="space-y-4" {...props}> <form onSubmit={handleSubmit} className="space-y-4" {...props} noValidate>
<FormField <FormField
label="Email" label="Email"
input={ input={
<Input <Input
id="email" id="email"
type="email" type="email"
name="email" {...register("email")}
placeholder="you@example.com" placeholder="you@example.com"
className={`bg-[#1C1C1C] border-gray-800 text-white placeholder:text-gray-500 focus:border-emerald-600 focus:ring-emerald-600 ${ className={`bg-[#1C1C1C] border-gray-800 ${errors.email ? "ring-red-500 focus-visible:ring-red-500" : ""
errors.email ? "border-red-500" : ""
}`} }`}
value={formData.email} disabled={isPending}
onChange={handleChange}
disabled={isSubmitting}
/> />
} }
error={errors.email} error={errors.email ? errors.email.message : undefined}
/> />
<SubmitButton <Button
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white" className="w-full bg-emerald-600 hover:bg-emerald-700 text-white"
size="lg" size="lg"
pendingText="Signing In..." disabled={isPending}
> >
Sign In {isPending ? (
</SubmitButton> <>
<Loader2 className="h-5 w-5 animate-spin" />
Signing in...
</>
) : (
"Sign in"
)}
</Button>
</form> </form>
<div className="text-center text-lg"> <div className="text-center text-lg">

View File

@ -1,15 +1,5 @@
// src/components/auth/VerifyOtpForm.tsx
"use client"; "use client";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormMessage,
} from "@/app/_components/ui/form";
import { import {
InputOTP, InputOTP,
InputOTPGroup, InputOTPGroup,
@ -24,76 +14,81 @@ import {
CardTitle, CardTitle,
} from "@/app/_components/ui/card"; } from "@/app/_components/ui/card";
import { cn } from "@/app/_lib/utils"; import { cn } from "@/app/_lib/utils";
import { 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> {} interface VerifyOtpFormProps extends React.HTMLAttributes<HTMLDivElement> {}
export function VerifyOtpForm({ className, ...props }: VerifyOtpFormProps) { export function VerifyOtpForm({ className, ...props }: VerifyOtpFormProps) {
const searchParams = useSearchParams(); const searchParams = useSearchParams()
const email = searchParams.get("email") || ""; const email = searchParams.get("email") || ""
const { control, register, isPending, handleSubmit, handleOtpChange, errors } = useVerifyOtpController(email)
const { form, isSubmitting, message, onSubmit } = useVerifyOtpForm(email);
return ( return (
<div className={cn("flex flex-col gap-6", className)} {...props}> <div className={cn("flex flex-col gap-6", className)} {...props}>
<Card className="bg-[#171717] border-gray-800 text-white border-none"> <Card className="bg-[#171717] border-gray-800 text-white border-none">
<CardHeader className="text-center"> <CardHeader className="text-center">
<CardTitle className="text-2xl font-bold"> <CardTitle className="text-2xl font-bold">One-Time Password</CardTitle>
One-Time Password
</CardTitle>
<CardDescription className="text-gray-400"> <CardDescription className="text-gray-400">
One time password is a security feature that helps protect your data One time password is a security feature that helps protect your data
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Form {...form}> <form onSubmit={handleSubmit} className="space-y-4">
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> <input type="hidden" {...register("email")} />
<input type="hidden" name="email" value={email} /> <div className="space-y-6">
<FormField <Controller
control={form.control}
name="token" name="token"
control={control}
render={({ field }) => ( render={({ field }) => (
<FormItem> <InputOTP
<FormControl> maxLength={6}
<InputOTP maxLength={6} {...field}> value={field.value || ""}
<InputOTPGroup className="flex w-full items-center justify-center space-x-2"> onChange={(value) => handleOtpChange(value, field.onChange)}
{[...Array(6)].map((_, index) => ( >
<InputOTPSlot <InputOTPGroup className="flex w-full items-center justify-center space-x-2">
key={index} {[...Array(6)].map((_, index) => (
index={index} <InputOTPSlot
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" 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> ))}
</FormControl> </InputOTPGroup>
<FormDescription className="flex w-full justify-center items-center text-gray-400"> </InputOTP>
Please enter the one-time password sent to {email}.
</FormDescription>
<FormMessage className="text-red-400" />
</FormItem>
)} )}
/> />
<div className="flex justify-center"> {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">
<SubmitButton Successfully verified!
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white" </div>}
pendingText="Verifying..."
disabled={isSubmitting} <div className="flex w-full justify-center items-center text-gray-400 text-sm">
> Please enter the one-time password sent to {email}.
Submit
</SubmitButton>
</div> </div>
{message && ( </div>
<div className="text-center text-emerald-500">{message}</div> <div className="flex justify-center">
)} <SubmitButton
</form> className="w-full bg-emerald-600 hover:bg-emerald-700 text-white"
</Form> disabled={isPending}
>
{isPending ? (
<>
<Loader2 className="h-5 w-5 animate-spin" />
Verifying...
</>
) : (
"Verify OTP"
)}
</SubmitButton>
</div>
</form>
</CardContent> </CardContent>
</Card> </Card>
<div className="text-balance text-center text-xs text-gray-400 [&_a]:text-emerald-500 [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-emerald-400"> <div className="text-balance text-center text-xs text-gray-400 [&_a]:text-emerald-500 [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-emerald-400">
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "} By clicking continue, you agree to our <a href="#">Terms of Service</a> and <a href="#">Privacy Policy</a>.
and <a href="#">Privacy Policy</a>.
</div> </div>
</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 { SignInForm } from "@/app/(pages)/(auth)/_components/signin-form";
import { Message } from "@/app/_components/form-message"; import { Message } from "@/app/_components/form-message";
import { Button } from "@/app/_components/ui/button"; import { Button } from "@/app/_components/ui/button";
import { GalleryVerticalEnd, Globe } from "lucide-react"; import { GalleryVerticalEnd, Globe } from "lucide-react";
export default async function Login(props: { searchParams: Promise<Message> }) { export default async function Login(props: { searchParams: Promise<Message> }) {
const searchParams = await props.searchParams;
return ( return (
<div className="grid min-h-svh lg:grid-cols-5"> <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"> <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; export default db;
if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = 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
import { createClient } from "@/app/_utils/supabase/server"; // "use server";
import { SignInFormData } from "../entities/models/auth/sign-in.model";
import { VerifyOtpFormData } from "../entities/models/auth/verify-otp.model";
export class AuthRepository { // import { createClient } from "@/app/_utils/supabase/server";
async signIn({ email }: SignInFormData) { // import { SignInFormData } from "@/src/entities/models/auth/sign-in.model";
const supabase = await createClient(); // import { VerifyOtpFormData } from "@/src/entities/models/auth/verify-otp.model";
const { data, error } = await supabase.auth.signInWithOtp({ // import { AuthenticationError } from "@/src/entities/errors/auth";
email, // import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
options: { // import { ICrashReporterService } from "@/src/application/services/crash-reporter.service.interface";
shouldCreateUser: false, // import { createAdminClient } from "@/app/_utils/supabase/admin";
}, // import { DatabaseOperationError } from "@/src/entities/errors/common";
});
// export class AuthRepository {
if (error) { // private static instance: AuthRepository;
throw new Error(error.message);
// 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 "@/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(); 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 { createClient } from "@/app/_utils/supabase/client";
import { CreateUserParams, InviteUserParams, UpdateUserParams, User, UserResponse } from "@/src/entities/models/users/users.model"; import { CreateUserParams, InviteUserParams, UpdateUserParams, User, UserResponse } from "@/src/entities/models/users/users.model";
import db from "@/prisma/db"; import db from "@/prisma/db";
import { DatabaseOperationError, NotFoundError } from "../entities/errors/common"; import { DatabaseOperationError, NotFoundError } from "@/src/entities/errors/common";
import { AuthenticationError } from "../entities/errors/auth"; 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 { export class UsersRepository {
private supabaseAdmin = createAdminClient(); constructor(
private supabaseClient = createClient(); private readonly instrumentationService: IInstrumentationService,
private readonly crashReporterService: ICrashReporterService,
private readonly supabaseAdmin = createAdminClient(),
private readonly supabaseClient = createClient()
) { }
async fetchUsers(): Promise<User[]> { async getUsers(): Promise<User[]> {
const users = await db.users.findMany({ return await this.instrumentationService.startSpan({
include: { name: "UsersRepository > getUsers",
profile: true, op: 'db.query',
}, attributes: { 'db.system': 'postgres' },
}); }, async () => {
try {
if (!users) { const users = await db.users.findMany({
throw new NotFoundError("Users not found"); 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> { 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 { const {
data: { user }, data: { user },
error, error,
} = await supabase.auth.getUser(); } = await supabase.auth.getUser();
if (error) { if (error) {
console.error("Error fetching current user:", error); console.error("Error fetching current user:", error);
throw new AuthenticationError(error.message); throw new AuthenticationError(error.message);
} }
const userDetail = await db.users.findUnique({ const userDetail = await db.users.findUnique({
where: { where: {
id: user?.id, id: user?.id,
}, },
include: { include: {
profile: true, profile: true,
}, },
}); });
if (!userDetail) { if (!userDetail) {
throw new NotFoundError("User not found"); throw new NotFoundError("User not found");
} }
return { return {
data: { data: {
user: userDetail, user: userDetail,
}, },
error: null, error: null,
}; };
} catch (err) {
this.crashReporterService.report(err);
throw err;
}
})
} }
async createUser(params: CreateUserParams): Promise<UserResponse> { 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({ const { data, error } = await supabase.auth.admin.createUser({
email: params.email, email: params.email,
password: params.password, password: params.password,
phone: params.phone, phone: params.phone,
email_confirm: params.email_confirm, 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,
}); });
if (uploadError) { if (error) {
console.error("Error uploading avatar:", uploadError); console.error("Error creating user:", error);
throw new DatabaseOperationError(uploadError.message); 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> { 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, { const { error } = await supabase.auth.admin.inviteUserByEmail(params.email, {
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/api/auth/callback`, redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/api/auth/callback`,
}); });
if (error) { if (error) {
console.error("Error inviting user:", error); console.error("Error inviting user:", error);
throw new DatabaseOperationError(error.message); 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 { export interface ICrashReporterService {
report(error: any): string; 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

@ -8,4 +8,25 @@ export interface IInstrumentationService {
options: Record<string, any>, options: Record<string, any>,
callback: () => T callback: () => T
): Promise<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"; import { z } from "zod";
// Define the sign-in form schema using Zod // Define the sign-in form schema using Zod
export const signInSchema = z.object({ export const SignInSchema = z.object({
email: z email: z
.string() .string()
.min(1, { message: "Email is required" }) .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 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 // Default values for the form
export const defaultSignInValues: SignInFormData = { 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"; "use client";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { import {
defaultSignInValues, defaultSignInValues,
SignInFormData, SignInFormData,
signInSchema, SignInSchema,
} from "@/src/entities/models/auth/sign-in.model"; } from "@/src/entities/models/auth/sign-in.model";
import { useState, type FormEvent, type ChangeEvent } from "react"; import { useState, type FormEvent, type ChangeEvent } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { signIn } from "@/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>>; type SignInFormErrors = Partial<Record<keyof SignInFormData, string>>;
export function useSignInForm() { // export function useSignInForm() {
const [formData, setFormData] = useState<SignInFormData>(defaultSignInValues); // const [formData, setFormData] = useState<SignInFormData>(defaultSignInValues);
const [errors, setErrors] = useState<SignInFormErrors>({}); // const [errors, setErrors] = useState<SignInFormErrors>({});
const [isSubmitting, setIsSubmitting] = useState(false); // const [isSubmitting, setIsSubmitting] = useState(false);
const [message, setMessage] = useState<string | null>(null); // const [message, setMessage] = useState<string | null>(null);
const router = useRouter(); // const router = useRouter();
const validateForm = (): boolean => { // const validateForm = (): boolean => {
try { // try {
signInSchema.parse(formData); // SignInSchema.parse(formData);
setErrors({}); // setErrors({});
return true; // return true;
} catch (error) { // } catch (error) {
if (error instanceof z.ZodError) { // if (error instanceof z.ZodError) {
const formattedErrors: SignInFormErrors = {}; // const formattedErrors: SignInFormErrors = {};
error.errors.forEach((err) => { // error.errors.forEach((err) => {
const path = err.path[0] as keyof SignInFormData; // const path = err.path[0] as keyof SignInFormData;
formattedErrors[path] = err.message; // formattedErrors[path] = err.message;
}); // });
setErrors(formattedErrors); // setErrors(formattedErrors);
} // }
return false; // return false;
} // }
}; // };
const handleChange = (e: ChangeEvent<HTMLInputElement>) => { // const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target; // const { name, value } = e.target;
setFormData((prev) => ({ // setFormData((prev) => ({
...prev, // ...prev,
[name]: value, // [name]: value,
})); // }));
}; // };
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { // const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); // e.preventDefault();
if (!validateForm()) { // if (!validateForm()) {
return; // return;
} // }
setIsSubmitting(true); // setIsSubmitting(true);
setMessage(null); // setMessage(null);
try { // try {
const result = await signIn(formData); // const result = await signIn(formData);
if (result.success) { // if (result.success) {
setMessage(result.message); // setMessage(result.message);
toast.success(result.message); // toast.success(result.message);
// Handle client-side navigation // // Handle client-side navigation
if (result.redirectTo) { // if (result.redirectTo) {
router.push(result.redirectTo); // router.push(result.redirectTo);
} // }
} else { // } else {
setErrors({ // setErrors({
email: result.message || "Sign in failed. Please try again.", // email: result.message || "Sign in failed. Please try again.",
}); // });
toast.error(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 {
signIn.mutate(data);
} catch (error) { } catch (error) {
console.error("Sign in failed", error); console.error("Sign-in submission error:", error);
setErrors({
email: "An unexpected error occurred. Please try again.",
});
toast.error("An unexpected error occurred. Please try again.");
} finally {
setIsSubmitting(false);
} }
}; });
return { return {
formData, register,
handleSubmit: onSubmit,
errors, errors,
isSubmitting, isPending: signIn.isPending,
message,
setFormData,
handleChange,
handleSubmit,
}; };
} }

View File

@ -1,11 +1,11 @@
// src/hooks/useVerifyOtpForm.ts // src/hooks/useVerifyOtpForm.ts
"use client"; "use client";
import { useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { verifyOtp } from "@/app/(pages)/(auth)/action"; // import { verifyOtp } from "";
import { import {
defaultVerifyOtpValues, defaultVerifyOtpValues,
VerifyOtpFormData, VerifyOtpFormData,
@ -13,51 +13,104 @@ import {
} from "@/src/entities/models/auth/verify-otp.model"; } from "@/src/entities/models/auth/verify-otp.model";
import { useNavigations } from "@/app/_hooks/use-navigations"; import { useNavigations } from "@/app/_hooks/use-navigations";
import { toast } from "sonner"; import { toast } from "sonner";
import { useAuthMutation } from "./auth-controller";
export function useVerifyOtpForm(initialEmail: string) { // export function useVerifyOtpForm(email: string) {
const [isSubmitting, setIsSubmitting] = useState(false); // const [isSubmitting, setIsSubmitting] = useState(false);
const [message, setMessage] = useState<string | null>(null); // const [message, setMessage] = useState<string | null>(null);
const { router } = useNavigations(); // 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), resolver: zodResolver(verifyOtpSchema),
defaultValues: { ...defaultVerifyOtpValues, email: initialEmail }, defaultValues: { ...defaultVerifyOtpValues, email: email },
}); })
const onSubmit = async (data: VerifyOtpFormData) => { // Clear form after successful submission
setIsSubmitting(true); useEffect(() => {
setMessage(null); if (isSubmitSuccessful) {
reset({ ...defaultVerifyOtpValues, email })
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);
} }
}; }, [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 { return {
form, control,
isSubmitting, register,
message, handleSubmit: onSubmit,
onSubmit, handleOtpChange,
}; errors,
isPending: verifyOtp.isPending,
}
} }