Refactor structure auth
This commit is contained in:
parent
18a7be5c19
commit
5830eedb18
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 { 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">
|
||||||
|
|
|
@ -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'];
|
|
@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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();
|
|
@ -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();
|
|
@ -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 = {
|
||||||
|
|
|
@ -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";
|
"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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue