From 7238403f399160e9028dd3107a4799be2438fb93 Mon Sep 17 00:00:00 2001 From: Gib Date: Fri, 30 May 2025 16:50:18 -0500 Subject: [PATCH] done for the day --- src/app/(auth-pages)/auth/callback/route.ts | 94 +++------- .../(auth-pages)/auth/callback/route.ts.bak | 77 ++++++-- src/app/(auth-pages)/layout.tsx | 8 - src/app/(auth-pages)/profile/page.tsx | 16 +- src/app/(auth-pages)/reset-password/page.tsx | 38 ---- src/app/(auth-pages)/sign-in/page.tsx | 170 ++++++++++++++---- .../default/profile/AvatarUpload.tsx | 5 +- .../default/profile/ProfileForm.tsx | 97 +++++----- .../default/profile/ResetPasswordForm.tsx | 74 +++++--- src/components/default/submit-button.tsx | 14 +- src/lib/actions/auth.ts | 38 +--- 11 files changed, 346 insertions(+), 285 deletions(-) delete mode 100644 src/app/(auth-pages)/layout.tsx delete mode 100644 src/app/(auth-pages)/reset-password/page.tsx diff --git a/src/app/(auth-pages)/auth/callback/route.ts b/src/app/(auth-pages)/auth/callback/route.ts index f0c360a..a2ec361 100644 --- a/src/app/(auth-pages)/auth/callback/route.ts +++ b/src/app/(auth-pages)/auth/callback/route.ts @@ -2,76 +2,32 @@ import 'server-only'; import { createServerClient } from '@/utils/supabase'; -import { NextResponse } from 'next/server'; +import { type EmailOtpType } from '@supabase/supabase-js'; +import { type NextRequest } from 'next/server'; +import { redirect } from 'next/navigation'; -export const GET = async (request: Request) => { - const requestUrl = new URL(request.url); - const code = requestUrl.searchParams.get('code'); - const token = requestUrl.searchParams.get('token'); - const type = requestUrl.searchParams.get('type'); - const origin = requestUrl.origin; - const redirectTo = requestUrl.searchParams.get('redirect_to')?.toString(); +export const GET = async (request: NextRequest) => { + const { searchParams } = new URL(request.url); + const token_hash = searchParams.get('token'); + const type = searchParams.get('type') as EmailOtpType | null; + const redirectTo = searchParams.get('redirect_to') ?? '/'; - const supabase = await createServerClient(); - - if (token && type) { - try { - if (type === 'signup') { - // Confirm email signup - const { error } = await supabase.auth.verifyOtp({ - token_hash: token, - type: 'signup', - }); - - if (error) { - console.error('Email confirmation error:', error); - return NextResponse.redirect(`${origin}/sign-in?error=Invalid or expired confirmation link`); - } - } else if (type === 'recovery') { - // Handle password recovery - const { error } = await supabase.auth.verifyOtp({ - token_hash: token, - type: 'recovery', - }); - - if (error) { - console.error('Password recovery error:', error); - return NextResponse.redirect(`${origin}/sign-in?error=Invalid or expired reset link`); - } else { - return NextResponse.redirect(`${origin}/reset-password`); - } - } else if (type === 'email_change') { - // Handle email change - const { error } = await supabase.auth.verifyOtp({ - token_hash: token, - type: 'email_change', - }); - - if (error) { - console.error('Email change error:', error); - return NextResponse.redirect(`${origin}/profile?error=Invalid or expired email change link`); - } - } - } catch (error) { - console.error('Verification error:', error); - return NextResponse.redirect(`${origin}/sign-in?error=Verification failed`); + if (token_hash && type) { + const supabase = await createServerClient(); + const { error } = await supabase.auth.verifyOtp({ + type, + token_hash, + }); + if (!error) { + if (type === 'signup' || type === 'magiclink' || type === 'email') + return redirect('/'); + if (type === 'recovery' || type === 'email_change') + return redirect('/profile'); + if (type === 'invite') + return redirect('/sign-up'); + else return redirect(`/?Could not identify type ${type as string}`) } + else return redirect(`/?${error.message}`); } - - // Handle code-based flow (OAuth, etc.) - if (code) { - await supabase.auth.exchangeCodeForSession(code); - } - - // Handle redirect - if (redirectTo) { - try { - new URL(redirectTo); - return NextResponse.redirect(redirectTo); - } catch { - return NextResponse.redirect(`${origin}${redirectTo}`); - } - } - - return NextResponse.redirect(origin); -} + return redirect('/'); +}; diff --git a/src/app/(auth-pages)/auth/callback/route.ts.bak b/src/app/(auth-pages)/auth/callback/route.ts.bak index a3bade1..812d617 100644 --- a/src/app/(auth-pages)/auth/callback/route.ts.bak +++ b/src/app/(auth-pages)/auth/callback/route.ts.bak @@ -1,24 +1,77 @@ +'use server'; + +import 'server-only'; import { createServerClient } from '@/utils/supabase'; -import { NextResponse } from 'next/server'; +import { type EmailOtpType } from '@supabase/supabase-js'; +import { type NextRequest, NextResponse } from 'next/server'; -export const GET = async (request: Request) => { - // The `/auth/callback` route is required for the server-side auth flow implemented - // by the SSR package. It exchanges an auth code for the user's session. - // https://supabase.com/docs/guides/auth/server-side/nextjs - const requestUrl = new URL(request.url); - const code = requestUrl.searchParams.get('code'); - const origin = requestUrl.origin; - const redirectTo = requestUrl.searchParams.get('redirect_to')?.toString(); +export const GET = async (request: NextRequest) => { + const { searchParams, origin } = new URL(request.url); + const code = searchParams.get('code'); + const token = searchParams.get('token'); + const type = searchParams.get('type') as EmailOtpType | null; + const redirectTo = searchParams.get('redirect_to')?.toString(); + const supabase = await createServerClient(); + + if (token && type) { + try { + if (type === 'signup') { + // Confirm email signup + const { error } = await supabase.auth.verifyOtp({ + token_hash: token, + type: 'signup', + }); + + if (error) { + console.error('Email confirmation error:', error); + return NextResponse.redirect(`${origin}/sign-in?error=Invalid or expired confirmation link`); + } + } else if (type === 'recovery') { + // Handle password recovery + const { error } = await supabase.auth.verifyOtp({ + token_hash: token, + type: 'recovery', + }); + + if (error) { + console.error('Password recovery error:', error); + return NextResponse.redirect(`${origin}/sign-in?error=Invalid or expired reset link`); + } else { + return NextResponse.redirect(`${origin}/reset-password`); + } + } else if (type === 'email_change') { + // Handle email change + const { error } = await supabase.auth.verifyOtp({ + token_hash: token, + type: 'email_change', + }); + + if (error) { + console.error('Email change error:', error); + return NextResponse.redirect(`${origin}/profile?error=Invalid or expired email change link`); + } + } + } catch (error) { + console.error('Verification error:', error); + return NextResponse.redirect(`${origin}/sign-in?error=Verification failed`); + } + } + + // Handle code-based flow (OAuth, etc.) if (code) { - const supabase = await createServerClient(); await supabase.auth.exchangeCodeForSession(code); } + // Handle redirect if (redirectTo) { - return NextResponse.redirect(`${origin}${redirectTo}`); + try { + new URL(redirectTo); + return NextResponse.redirect(redirectTo); + } catch { + return NextResponse.redirect(`${origin}${redirectTo}`); + } } - // URL to redirect to after sign up process completes return NextResponse.redirect(origin); } diff --git a/src/app/(auth-pages)/layout.tsx b/src/app/(auth-pages)/layout.tsx deleted file mode 100644 index a4dfbc4..0000000 --- a/src/app/(auth-pages)/layout.tsx +++ /dev/null @@ -1,8 +0,0 @@ -const Layout = async ({ children }: { children: React.ReactNode }) => { - return ( -
- {children} -
- ); -}; -export default Layout; diff --git a/src/app/(auth-pages)/profile/page.tsx b/src/app/(auth-pages)/profile/page.tsx index 094a11a..4b74787 100644 --- a/src/app/(auth-pages)/profile/page.tsx +++ b/src/app/(auth-pages)/profile/page.tsx @@ -5,7 +5,6 @@ import { useEffect } from 'react'; import { AvatarUpload, ProfileForm, ResetPasswordForm } from '@/components/default/profile'; import { Card, - CardContent, CardHeader, CardTitle, CardDescription, @@ -14,6 +13,7 @@ import { import { Loader2 } from 'lucide-react'; import { resetPassword } from '@/lib/actions'; import { toast } from 'sonner'; +import { type Result } from '@/lib/actions'; const ProfilePage = () => { const { profile, isLoading, isAuthenticated, updateProfile, refreshUserData } = useAuth(); @@ -44,19 +44,21 @@ const ProfilePage = () => { } }; - const handleResetPasswordSubmit = async (values: { - password: string; - confirmPassword: string; - }) => { + const handleResetPasswordSubmit = async ( + formData: FormData, + ): Promise> => { try { - const result = await resetPassword(values); + const result = await resetPassword(formData); if (!result.success) { toast.error(`Error resetting password: ${result.error}`) + return {success: false, error: result.error}; } + return {success: true, data: null}; } catch (error) { toast.error( `Error resetting password!: ${error as string ?? 'Unknown error'}` ); + return {success: false, error: 'Unknown error'}; } } @@ -87,7 +89,6 @@ const ProfilePage = () => { Manage your personal information and how it appears to others - {isLoading && !profile ? (
@@ -101,7 +102,6 @@ const ProfilePage = () => {
)} -
); diff --git a/src/app/(auth-pages)/reset-password/page.tsx b/src/app/(auth-pages)/reset-password/page.tsx deleted file mode 100644 index d332861..0000000 --- a/src/app/(auth-pages)/reset-password/page.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { resetPasswordFromEmail } from '@/lib/actions'; -import { FormMessage, type Message, SubmitButton } from '@/components/default'; -import { Input, Label } from '@/components/ui'; -import { getUser } from '@/lib/actions'; -import { redirect } from 'next/navigation'; - -const ResetPassword = async (props: { searchParams: Promise }) => { - const user = await getUser(); - if (!user.success) { - redirect('/sign-in'); - } - const searchParams = await props.searchParams; - return ( -
-

Reset password

-

- Please enter your new password below. -

- - - - - Reset password - - - ); -}; -export default ResetPassword; diff --git a/src/app/(auth-pages)/sign-in/page.tsx b/src/app/(auth-pages)/sign-in/page.tsx index b0c9df0..1cb2954 100644 --- a/src/app/(auth-pages)/sign-in/page.tsx +++ b/src/app/(auth-pages)/sign-in/page.tsx @@ -1,15 +1,53 @@ 'use client'; + +import { z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, + Input, + Label, +} from '@/components/ui'; import Link from 'next/link'; import { signIn } from '@/lib/actions'; import { SubmitButton } from '@/components/default'; -import { Input, Label } from '@/components/ui'; import { useRouter } from 'next/navigation'; import { useAuth } from '@/components/context/auth'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; + +const formSchema = z.object({ + email: z.string().email({ + message: 'Please enter a valid email address.', + }), + password: z.string().min(8, { + message: 'Password must be at least 8 characters.', + }), +}) const Login = () => { const router = useRouter(); - const { isAuthenticated, refreshUserData } = useAuth(); + const { isAuthenticated, isLoading, refreshUserData } = useAuth(); + const [statusMessage, setStatusMessage] = useState(''); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: '', + password: '', + }, + }); // Redirect if already authenticated useEffect(() => { @@ -18,46 +56,102 @@ const Login = () => { } }, [isAuthenticated, router]); - const handleSignIn = async (formData: FormData) => { - const result = await signIn(formData); - if (result?.success) { - await refreshUserData(); - router.push('/'); + const handleSignIn = async (values: z.infer) => { + try { + const formData = new FormData(); + formData.append('email', values.email); + formData.append('password', values.password); + const result = await signIn(formData); + if (result?.success) { + await refreshUserData(); + form.reset(); + router.push(''); + } else { + setStatusMessage(`Error: ${result.error}`) + } + } catch (error) { + setStatusMessage( + `Error: ${error instanceof Error ? error.message : 'Could not sign in!'}` + ); } }; return ( -
-

Sign in

-

- Don't have an account?{' '} - - Sign up - -

-
- - -
- - - Forgot Password? + + + + Sign In + + + Don't have an account?{' '} + + Sign up -
- - - Sign in - -
-
+ + + +
+ + ( + + Email + + + + + + )} + /> + + ( + +
+ Password + + Forgot Password? + +
+ + + + +
+ )} + /> + {statusMessage && ( +
+ {statusMessage} +
+ )} + + Sign in + + + + +
+ ); }; diff --git a/src/components/default/profile/AvatarUpload.tsx b/src/components/default/profile/AvatarUpload.tsx index e93bd78..98db8ea 100644 --- a/src/components/default/profile/AvatarUpload.tsx +++ b/src/components/default/profile/AvatarUpload.tsx @@ -1,6 +1,6 @@ import { useFileUpload } from '@/lib/hooks/useFileUpload'; import { useAuth } from '@/components/context/auth'; -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui'; +import { Avatar, AvatarFallback, AvatarImage, CardContent } from '@/components/ui'; import { Loader2, Pencil, Upload, User } from 'lucide-react'; type AvatarUploadProps = { @@ -43,6 +43,8 @@ export const AvatarUpload = ({ onAvatarUploaded }: AvatarUploadProps) => { }; return ( + +
{
)}
+
); }; diff --git a/src/components/default/profile/ProfileForm.tsx b/src/components/default/profile/ProfileForm.tsx index 79b4600..dab38ab 100644 --- a/src/components/default/profile/ProfileForm.tsx +++ b/src/components/default/profile/ProfileForm.tsx @@ -3,6 +3,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import { Button, + CardContent, Form, FormControl, FormDescription, @@ -53,53 +54,55 @@ export const ProfileForm = ({onSubmit}: ProfileFormProps) => { }; return ( -
- - ( - - Full Name - - - - Your public display name. - - - )} - /> - - ( - - Email - - - - - Your email address associated with your account. - - - - )} - /> - -
- -
- - + /> + + ( + + Email + + + + + Your email address associated with your account. + + + + )} + /> + +
+ +
+ + + ); } diff --git a/src/components/default/profile/ResetPasswordForm.tsx b/src/components/default/profile/ResetPasswordForm.tsx index ab0cea9..a138088 100644 --- a/src/components/default/profile/ResetPasswordForm.tsx +++ b/src/components/default/profile/ResetPasswordForm.tsx @@ -2,7 +2,7 @@ import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import { - Button, + CardContent, CardDescription, CardHeader, CardTitle, @@ -15,8 +15,10 @@ import { FormMessage, Input, } from '@/components/ui'; -import { Loader2 } from 'lucide-react'; +import { SubmitButton } from '@/components/default'; import { useState } from 'react'; +import { type Result } from '@/lib/actions'; +import { FormMessage as Pw } from '@/components/default'; const formSchema = z .object({ @@ -31,7 +33,7 @@ const formSchema = z }); type ResetPasswordFormProps = { - onSubmit: (values: z.infer) => Promise; + onSubmit: (formData: FormData) => Promise>; message?: string; }; @@ -50,14 +52,25 @@ export const ResetPasswordForm = ({ }, }); - const handleSubmit = async (values: z.infer) => { + const handleUpdatePassword = async (values: z.infer) => { setIsLoading(true); try { - await onSubmit(values); - setStatusMessage('Password updated successfully'); - form.reset(); + // Convert form values to FormData for your server action + const formData = new FormData(); + formData.append('password', values.password); + formData.append('confirmPassword', values.confirmPassword); + + const result = await onSubmit(formData); + if (result?.success) { + setStatusMessage('Password updated successfully!'); + form.reset(); // Clear the form on success + } else { + setStatusMessage('Error: Unable to update password!'); + } } catch (error) { - setStatusMessage(error instanceof Error ? error.message : 'An error occurred'); + setStatusMessage( + error instanceof Error ? error.message : 'Password was not updated!' + ); } finally { setIsLoading(false); } @@ -65,15 +78,19 @@ export const ResetPasswordForm = ({ return (
- - Change Password - - Update your password to keep your account secure - - - + + Change Password + + Update your password to keep your account secure + + + +
- + {statusMessage && ( -
+
{statusMessage}
)}
- + + Update Password +
+
); }; diff --git a/src/components/default/submit-button.tsx b/src/components/default/submit-button.tsx index 319f28c..22538c3 100644 --- a/src/components/default/submit-button.tsx +++ b/src/components/default/submit-button.tsx @@ -3,21 +3,31 @@ import { Button } from '@/components/ui'; import { type ComponentProps } from 'react'; import { useFormStatus } from 'react-dom'; +import { Loader2 } from 'lucide-react'; type Props = ComponentProps & { + disabled?: boolean; pendingText?: string; }; export const SubmitButton = ({ children, + disabled = false, pendingText = 'Submitting...', ...props }: Props) => { const { pending } = useFormStatus(); return ( - ); }; diff --git a/src/lib/actions/auth.ts b/src/lib/actions/auth.ts index dce5593..c9e460e 100644 --- a/src/lib/actions/auth.ts +++ b/src/lib/actions/auth.ts @@ -98,13 +98,9 @@ export const forgotPassword = async (formData: FormData) => { }; -export const resetPassword = async ({ - password, - confirmPassword, -}: { - password: string, - confirmPassword: string -}): Promise> => { +export const resetPassword = async (formData: FormData): Promise> => { + const password = formData.get('password') as string; + const confirmPassword = formData.get('confirmPassword') as string; if (!password || !confirmPassword) { return { success: false, error: 'Password and confirm password are required!' }; } @@ -121,34 +117,6 @@ export const resetPassword = async ({ return { success: true, data: null }; }; - -export const resetPasswordFromEmail = async (formData: FormData) => { - const password = formData.get('password') as string; - const confirmPassword = formData.get('confirmPassword') as string; - if (!password || !confirmPassword) { - encodedRedirect( - 'error', - '/reset-password', - 'Password and confirm password are required', - ); - } - const supabase = await createServerClient(); - - if (password !== confirmPassword) { - encodedRedirect('error', '/reset-password', 'Passwords do not match'); - } - - const { error } = await supabase.auth.updateUser({ - password: password, - }); - - if (error) { - encodedRedirect('error', '/reset-password', 'Password update failed'); - } - - encodedRedirect('success', '/reset-password', 'Password updated'); -}; - export const signOut = async (): Promise> => { const supabase = await createServerClient(); const { error } = await supabase.auth.signOut();