From 99781de44c3e0b9974e4c58b031cfbfe3fdd4748 Mon Sep 17 00:00:00 2001 From: vergiLgood1 Date: Thu, 20 Feb 2025 02:26:18 +0700 Subject: [PATCH] Refactor(signin with otp): make code more clean --- sigap-website/actions/auth/sign-in.ts | 75 ++++--- sigap-website/components/auth/login-form.tsx | 204 +++++++++--------- .../entities/models/user.model.ts | 72 +++++++ .../repositories/signin.repository.ts | 5 + .../usecases/signin/signin.usecases.ts | 10 + .../src/infrastructure/hooks/use-signin.ts | 113 ++++++++++ .../repositories/signin.repository.impl.ts | 23 ++ .../validators/signin.validator.ts | 27 +++ 8 files changed, 399 insertions(+), 130 deletions(-) create mode 100644 sigap-website/src/applications/entities/models/user.model.ts create mode 100644 sigap-website/src/applications/repositories/signin.repository.ts create mode 100644 sigap-website/src/applications/usecases/signin/signin.usecases.ts create mode 100644 sigap-website/src/infrastructure/hooks/use-signin.ts create mode 100644 sigap-website/src/infrastructure/repositories/signin.repository.impl.ts create mode 100644 sigap-website/src/infrastructure/validators/signin.validator.ts diff --git a/sigap-website/actions/auth/sign-in.ts b/sigap-website/actions/auth/sign-in.ts index bc0811c..e0644d2 100644 --- a/sigap-website/actions/auth/sign-in.ts +++ b/sigap-website/actions/auth/sign-in.ts @@ -1,34 +1,53 @@ "use server"; -import { encodedRedirect } from "@/utils/utils"; + import { createClient } from "@/utils/supabase/server"; -import { redirect } from "next/navigation"; -export const signInAction = async (formData: FormData) => { - const email = formData.get("email") as string; +export const signInAction = async (formData: { email: string }) => { const supabase = await createClient(); + const encodeEmail = encodeURIComponent(formData.email); - if (!email || email === "") { - return encodedRedirect("error", "/sign-in", "Email is required"); + try { + // First, check for existing session + const { + data: { session }, + error: sessionError, + } = await supabase.auth.getSession(); + + // If there's an active session and the email matches + if (session && session.user.email === formData.email) { + return { + success: true, + message: "Already logged in", + redirectTo: "/protected/dashboard", // or wherever you want to redirect logged in users + }; + } + + // If no active session or different email, proceed with OTP + const { data, error } = await supabase.auth.signInWithOtp({ + email: formData.email, + options: { + shouldCreateUser: false, + }, + }); + + if (error) { + return { + success: false, + error: error.message, + redirectTo: `/verify-otp?email=${encodeEmail}`, + }; + } + + return { + success: true, + message: "OTP has been sent to your email", + redirectTo: `/verify-otp?email=${encodeEmail}`, + }; + } catch (error) { + return { + success: false, + error: "An unexpected error occurred", + redirectTo: "/sign-in", + }; } - - const { data, error } = await supabase.auth.signInWithOtp({ - email, - options: { - shouldCreateUser: true, - }, - }); - - if (error && error.message.includes("User not found")) { - // Encode email parameter untuk keamanan - const encodedEmail = encodeURIComponent(email); - return redirect(`/sign-in?email=${encodedEmail}`); - } - - if (error) { - return encodedRedirect("error", "/sign-in", error.message); - } - - // Redirect dengan email parameter - const encodedEmail = encodeURIComponent(email); - return redirect(`/verify-otp?email=${encodedEmail}`); -}; +}; \ No newline at end of file diff --git a/sigap-website/components/auth/login-form.tsx b/sigap-website/components/auth/login-form.tsx index 1b38c67..de88eeb 100644 --- a/sigap-website/components/auth/login-form.tsx +++ b/sigap-website/components/auth/login-form.tsx @@ -1,103 +1,103 @@ -import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { SubmitButton } from "../submit-button"; -import { signInAction } from "@/actions/auth/sign-in"; +// import { cn } from "@/lib/utils"; +// import { Button } from "@/components/ui/button"; +// import { +// Card, +// CardContent, +// CardDescription, +// CardHeader, +// CardTitle, +// } from "@/components/ui/card"; +// import { Input } from "@/components/ui/input"; +// import { Label } from "@/components/ui/label"; +// import { SubmitButton } from "../submit-button"; +// import { signInAction } from "@/actions/auth/sign-in"; -export function LoginForm({ - className, - ...props -}: React.ComponentPropsWithoutRef<"div">) { - return ( -
- - - Welcome back - - Login with your Apple or Google account - - - -
-
- {/*
- - -
*/} -
- {/* - Or continue with - */} -
-
-
- - -
- {/*
- - -
*/} - - Login - -
-
- Don't have an account?{" "} - - Contact Us - -
-
-
-
-
-
- By clicking continue, you agree to our Terms of Service{" "} - and Privacy Policy. -
-
- ); -} +// export function LoginForm({ +// className, +// ...props +// }: React.ComponentPropsWithoutRef<"div">) { +// return ( +//
+// +// +// Welcome back +// +// Login with your Apple or Google account +// +// +// +//
+//
+// {/*
+// +// +//
*/} +//
+// {/* +// Or continue with +// */} +//
+//
+//
+// +// +//
+// {/*
+//
+// +// +// Forgot your password? +// +//
+// +//
*/} +// +// Login +// +//
+//
+// Don't have an account?{" "} +// +// Contact Us +// +//
+//
+//
+//
+//
+//
+// By clicking continue, you agree to our Terms of Service{" "} +// and Privacy Policy. +//
+//
+// ); +// } diff --git a/sigap-website/src/applications/entities/models/user.model.ts b/sigap-website/src/applications/entities/models/user.model.ts new file mode 100644 index 0000000..2d4a1b3 --- /dev/null +++ b/sigap-website/src/applications/entities/models/user.model.ts @@ -0,0 +1,72 @@ +import { z } from "zod"; + +export const Roles = { + ADMIN: "admin", + USER: "user", + STAFF: "staff", +}; + +export type Roles = typeof Roles; + +export const userSchema = z.object({ + id: z.string(), + email: z.string(), + emailVerified: z.boolean().default(false), + password: z.string().nullable(), + firstName: z.string().nullable(), + lastName: z.string().nullable(), + avatar: z.string().nullable(), + role: z.nativeEnum(Roles).default(Roles.USER), + createdAt: z.date(), + updatedAt: z.date(), + lastSignedIn: z.date().nullable(), + metadata: z.any().nullable(), + profile: z + .object({ + id: z.string(), + userId: z.string(), + bio: z.string().nullable(), + phone: z.string().nullable(), + address: z.string().nullable(), + city: z.string().nullable(), + country: z.string().nullable(), + birthDate: z.date().nullable(), + }) + .nullable(), +}); + +export type User = z.infer; + +export const userInsertSchema = userSchema.pick({ + email: true, + password: true, + firstName: true, + lastName: true, + role: true, + profile: true, +}); + +export type UserInsert = z.infer; + +export const signInSchema = z.object({ + email: z.string(), + password: z.string(), + phone: z.string(), + username: z.string(), +}); + +export type SignIn = z.infer; + +export const signInWithPasswordlessSchema = signInSchema.pick({ + email: true, +}); + +export type SignInWithOtp = z.infer; + +export interface SignInResponse { + success: boolean; + message?: string; + error?: string; + errors?: Record; + redirectTo?: string; +} diff --git a/sigap-website/src/applications/repositories/signin.repository.ts b/sigap-website/src/applications/repositories/signin.repository.ts new file mode 100644 index 0000000..3ae62ca --- /dev/null +++ b/sigap-website/src/applications/repositories/signin.repository.ts @@ -0,0 +1,5 @@ +import { SignInResponse, SignInWithOtp } from "../entities/models/user.model"; + +export interface SignInRepository { + signInWithPasswordless(email: SignInWithOtp): Promise; +} diff --git a/sigap-website/src/applications/usecases/signin/signin.usecases.ts b/sigap-website/src/applications/usecases/signin/signin.usecases.ts new file mode 100644 index 0000000..f18b45e --- /dev/null +++ b/sigap-website/src/applications/usecases/signin/signin.usecases.ts @@ -0,0 +1,10 @@ +import { SignInResponse, SignInWithOtp } from "../../entities/models/user.model"; +import { SignInRepository } from "../../repositories/signin.repository"; + +export class SignInUseCase { + constructor(private signInRepository: SignInRepository) {} + + async executeSignInWithPasswordless(email: SignInWithOtp): Promise { + return this.signInRepository.signInWithPasswordless(email); + } +} diff --git a/sigap-website/src/infrastructure/hooks/use-signin.ts b/sigap-website/src/infrastructure/hooks/use-signin.ts new file mode 100644 index 0000000..62af726 --- /dev/null +++ b/sigap-website/src/infrastructure/hooks/use-signin.ts @@ -0,0 +1,113 @@ +import { toast } from "@/hooks/use-toast"; +import { useState } from "react"; +import { ContactRepositoryImpl } from "../repositories/contact-us.repository.impl"; +import { validateContactForm } from "../validators/contact-us.validator"; +import { SignInWithOtp } from "@/src/applications/entities/models/user.model"; +import { SignInRepositoryImpl } from "../repositories/signin.repository.impl"; +import { validateSignInWithOtp } from "../validators/signin.validator"; +import Router from "next/router"; +import { useNavigations } from "@/hooks/use-navigations"; + +export const useSignInForm = () => { + const [isSubmitting, setIsSubmitting] = useState(false); + const [formData, setFormData] = useState({ + email: "", + }); + const [errors, setErrors] = useState< + Partial> + >({}); + const { router } = useNavigations(); + + const contactRepository = new SignInRepositoryImpl(); + + // Handle input change + const handleChange = ( + e: React.ChangeEvent + ) => { + const { id, value } = e.target; + setFormData((prev) => ({ ...prev, [id]: value })); + + if (errors[id as keyof SignInWithOtp]) { + setErrors((prev) => { + const newErrors = { ...prev }; + delete newErrors[id as keyof SignInWithOtp]; + return newErrors; + }); + } + }; + + // Handle select change + const handleSelectChange = (value: string) => { + setFormData((prev) => ({ ...prev, typeMessage: value })); + + // Clear error when selecting + if (errors.email) { + setErrors((prev) => { + const newErrors = { ...prev }; + delete newErrors.email; + return newErrors; + }); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const validation = validateSignInWithOtp(formData); + if (!validation.success) { + setErrors(validation.errors); + return; + } + + setIsSubmitting(true); + try { + const response = await contactRepository.signInWithPasswordless(formData); + + if (!response.success) { + if (response.errors) { + setErrors(response.errors); + } else { + toast({ + title: "Error", + description: response.error || "An unexpected error occurred.", + variant: "destructive", + }); + } + return; + } + // Add redirect handling + if (response.redirectTo) { + router.push(response.redirectTo); + } + + toast({ + title: "Success", + description: + response.message || `OTP has been sent to ${formData.email}`, + }); + + setFormData({ + email: "", + }); + setErrors({}); + } catch (error) { + + toast({ + title: "Error", + description: "An unexpected error occurred. Please try again later.", + variant: "destructive", + }); + } finally { + setIsSubmitting(false); + } + }; + + return { + formData, + errors, + isSubmitting, + setFormData, + handleChange, + handleSelectChange, + handleSubmit, + }; +}; diff --git a/sigap-website/src/infrastructure/repositories/signin.repository.impl.ts b/sigap-website/src/infrastructure/repositories/signin.repository.impl.ts new file mode 100644 index 0000000..9805dc4 --- /dev/null +++ b/sigap-website/src/infrastructure/repositories/signin.repository.impl.ts @@ -0,0 +1,23 @@ +import { signInAction } from "@/actions/auth/sign-in"; +import { + SignInResponse, + SignInWithOtp, +} from "@/src/applications/entities/models/user.model"; +import { SignInRepository } from "@/src/applications/repositories/signin.repository"; +import Router from "next/router"; + +export class SignInRepositoryImpl implements SignInRepository { + async signInWithPasswordless(email: SignInWithOtp): Promise { + try { + const result = await signInAction(email); + + return result; + } catch (error) { + console.error("Error in SignInRepositoryImpl:", error); + return { + success: false, + error: "An unexpected error occurred. Please try again later.", + }; + } + } +} diff --git a/sigap-website/src/infrastructure/validators/signin.validator.ts b/sigap-website/src/infrastructure/validators/signin.validator.ts new file mode 100644 index 0000000..10a98f6 --- /dev/null +++ b/sigap-website/src/infrastructure/validators/signin.validator.ts @@ -0,0 +1,27 @@ +import { ContactUsInsert } from "@/src/applications/entities/models/contact-us.model"; +import { SignInWithOtp } from "@/src/applications/entities/models/user.model"; +import { TValidator } from "@/utils/validator"; + +/** + * Validate the contact form + * @param {Object} formData - The form data to validate + * @returns {Object} - Validation result with success flag and errors object + */ +export const validateSignInWithOtp = ( + formData: SignInWithOtp +): { success: boolean; errors: Record } => { + const errors: Record = {}; + let isValid = true; + + // Validate email + const emailResult = TValidator.validateEmail(formData.email); + if (!emailResult.success) { + errors.email = emailResult.error!; + isValid = false; + } + + return { + success: isValid, + errors: isValid ? {} : errors, + }; +};