Refactor structure auth
This commit is contained in:
parent
18a7be5c19
commit
5830eedb18
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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.",
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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">
|
||||
|
|
|
@ -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'];
|
|
@ -1,65 +1,343 @@
|
|||
// src/repositories/auth.repository.ts
|
||||
import { createClient } from "@/app/_utils/supabase/server";
|
||||
import { SignInFormData } from "../entities/models/auth/sign-in.model";
|
||||
import { VerifyOtpFormData } from "../entities/models/auth/verify-otp.model";
|
||||
// // src/repositories/auth.repository.ts
|
||||
// "use server";
|
||||
|
||||
export class AuthRepository {
|
||||
async signIn({ email }: SignInFormData) {
|
||||
const supabase = await createClient();
|
||||
const { data, error } = await supabase.auth.signInWithOtp({
|
||||
email,
|
||||
options: {
|
||||
shouldCreateUser: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message);
|
||||
// import { createClient } from "@/app/_utils/supabase/server";
|
||||
// import { SignInFormData } from "@/src/entities/models/auth/sign-in.model";
|
||||
// import { VerifyOtpFormData } from "@/src/entities/models/auth/verify-otp.model";
|
||||
// import { AuthenticationError } from "@/src/entities/errors/auth";
|
||||
// import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
|
||||
// import { ICrashReporterService } from "@/src/application/services/crash-reporter.service.interface";
|
||||
// import { createAdminClient } from "@/app/_utils/supabase/admin";
|
||||
// import { DatabaseOperationError } from "@/src/entities/errors/common";
|
||||
|
||||
// export class AuthRepository {
|
||||
// private static instance: AuthRepository;
|
||||
|
||||
// private constructor(
|
||||
// private readonly instrumentationService: IInstrumentationService,
|
||||
// private readonly crashReporterService: ICrashReporterService,
|
||||
// private readonly supabaseAdmin = createAdminClient(),
|
||||
// private readonly supabaseServer = createClient()
|
||||
// ) { }
|
||||
|
||||
// // Method untuk mendapatkan singleton instance
|
||||
// public static getInstance(
|
||||
// instrumentationService: IInstrumentationService,
|
||||
// crashReporterService: ICrashReporterService
|
||||
// ): AuthRepository {
|
||||
// if (!AuthRepository.instance) {
|
||||
// AuthRepository.instance = new AuthRepository(instrumentationService, crashReporterService);
|
||||
// }
|
||||
// return AuthRepository.instance;
|
||||
// }
|
||||
|
||||
// async signIn({ email }: SignInFormData) {
|
||||
// return await this.instrumentationService.startSpan({
|
||||
// name: "UsersRepository > signIn",
|
||||
// op: 'db.query',
|
||||
// attributes: { 'db.system': 'postgres' },
|
||||
// }, async () => {
|
||||
// try {
|
||||
// const supabase = await this.supabaseServer;
|
||||
// const { data, error } = await supabase.auth.signInWithOtp({
|
||||
// email,
|
||||
// options: {
|
||||
// shouldCreateUser: false,
|
||||
// },
|
||||
// });
|
||||
|
||||
// if (error) {
|
||||
// console.error("Error signing in:", error);
|
||||
// throw new AuthenticationError(error.message);
|
||||
// }
|
||||
|
||||
// return {
|
||||
// data,
|
||||
// redirectTo: `/verify-otp?email=${encodeURIComponent(email)}`,
|
||||
// };
|
||||
// } catch (err) {
|
||||
// this.crashReporterService.report(err);
|
||||
// throw err;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// async verifyOtp({ email, token }: VerifyOtpFormData) {
|
||||
// return await this.instrumentationService.startSpan({
|
||||
// name: "UsersRepository > verifyOtp",
|
||||
// op: 'db.query',
|
||||
// attributes: { 'db.system': 'postgres' },
|
||||
// }, async () => {
|
||||
// try {
|
||||
// const supabase = await this.supabaseServer;
|
||||
// const { data, error } = await supabase.auth.verifyOtp({
|
||||
// email,
|
||||
// token,
|
||||
// type: "email",
|
||||
// });
|
||||
|
||||
// if (error) {
|
||||
// console.error("Error verifying OTP:", error);
|
||||
// throw new AuthenticationError(error.message);
|
||||
// }
|
||||
|
||||
// return {
|
||||
// data,
|
||||
// redirectTo: "/dashboard",
|
||||
// };
|
||||
// } catch (err) {
|
||||
// this.crashReporterService.report(err);
|
||||
// throw err;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// async signOut() {
|
||||
// return await this.instrumentationService.startSpan({
|
||||
// name: "UsersRepository > signOut",
|
||||
// op: 'db.query',
|
||||
// attributes: { 'db.system': 'postgres' },
|
||||
// }, async () => {
|
||||
// try {
|
||||
// const supabase = await this.supabaseServer;
|
||||
// const { error } = await supabase.auth.signOut();
|
||||
|
||||
// if (error) {
|
||||
// console.error("Error signing out:", error);
|
||||
// throw new AuthenticationError(error.message);
|
||||
// }
|
||||
|
||||
// return {
|
||||
// success: true,
|
||||
// redirectTo: "/",
|
||||
// };
|
||||
// } catch (err) {
|
||||
// this.crashReporterService.report(err);
|
||||
// throw err;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// async sendPasswordRecovery(email: string): Promise<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.");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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();
|
|
@ -8,4 +8,25 @@ export interface IInstrumentationService {
|
|||
options: Record<string, any>,
|
||||
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();
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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 => {
|
||||
try {
|
||||
signInSchema.parse(formData);
|
||||
setErrors({});
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
const formattedErrors: SignInFormErrors = {};
|
||||
error.errors.forEach((err) => {
|
||||
const path = err.path[0] as keyof SignInFormData;
|
||||
formattedErrors[path] = err.message;
|
||||
});
|
||||
setErrors(formattedErrors);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
// const validateForm = (): boolean => {
|
||||
// try {
|
||||
// SignInSchema.parse(formData);
|
||||
// setErrors({});
|
||||
// return true;
|
||||
// } catch (error) {
|
||||
// if (error instanceof z.ZodError) {
|
||||
// const formattedErrors: SignInFormErrors = {};
|
||||
// error.errors.forEach((err) => {
|
||||
// const path = err.path[0] as keyof SignInFormData;
|
||||
// formattedErrors[path] = err.message;
|
||||
// });
|
||||
// setErrors(formattedErrors);
|
||||
// }
|
||||
// return false;
|
||||
// }
|
||||
// };
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
// 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;
|
||||
}
|
||||
// const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
// e.preventDefault();
|
||||
// if (!validateForm()) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
setIsSubmitting(true);
|
||||
setMessage(null);
|
||||
// setIsSubmitting(true);
|
||||
// setMessage(null);
|
||||
|
||||
try {
|
||||
const result = await signIn(formData);
|
||||
// try {
|
||||
// const result = await signIn(formData);
|
||||
|
||||
if (result.success) {
|
||||
setMessage(result.message);
|
||||
toast.success(result.message);
|
||||
// if (result.success) {
|
||||
// setMessage(result.message);
|
||||
// toast.success(result.message);
|
||||
|
||||
// Handle client-side navigation
|
||||
if (result.redirectTo) {
|
||||
router.push(result.redirectTo);
|
||||
}
|
||||
} else {
|
||||
setErrors({
|
||||
email: result.message || "Sign in failed. Please try again.",
|
||||
});
|
||||
toast.error(result.message || "Sign in failed. Please try again.");
|
||||
}
|
||||
// // Handle client-side navigation
|
||||
// if (result.redirectTo) {
|
||||
// router.push(result.redirectTo);
|
||||
// }
|
||||
// } else {
|
||||
// setErrors({
|
||||
// email: result.message || "Sign in failed. Please try again.",
|
||||
// });
|
||||
// toast.error(result.message || "Sign in failed. Please try again.");
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error("Sign in failed", error);
|
||||
// setErrors({
|
||||
// email: "An unexpected error occurred. Please try again.",
|
||||
// });
|
||||
// toast.error("An unexpected error occurred. Please try again.");
|
||||
// } finally {
|
||||
// setIsSubmitting(false);
|
||||
// }
|
||||
// };
|
||||
|
||||
// return {
|
||||
// formData,
|
||||
// errors,
|
||||
// isSubmitting,
|
||||
// message,
|
||||
// setFormData,
|
||||
// handleChange,
|
||||
// handleSubmit,
|
||||
// };
|
||||
// }
|
||||
|
||||
// export function useSignInController() {
|
||||
// const [formData, setFormData] = useState<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) {
|
||||
console.error("Sign in failed", error);
|
||||
setErrors({
|
||||
email: "An unexpected error occurred. Please try again.",
|
||||
});
|
||||
toast.error("An unexpected error occurred. Please try again.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
console.error("Sign-in submission error:", error);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
formData,
|
||||
register,
|
||||
handleSubmit: onSubmit,
|
||||
errors,
|
||||
isSubmitting,
|
||||
message,
|
||||
setFormData,
|
||||
handleChange,
|
||||
handleSubmit,
|
||||
isPending: signIn.isPending,
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue