diff --git a/app/auth/callback/route.ts b/app/auth/callback/route.ts index f806d08..ce3d49c 100644 --- a/app/auth/callback/route.ts +++ b/app/auth/callback/route.ts @@ -6,38 +6,31 @@ import type { NextRequest } from "next/server" export async function GET(request: NextRequest) { const requestUrl = new URL(request.url) const code = requestUrl.searchParams.get("code") + const type = requestUrl.searchParams.get("type") + + // For password recovery, pass the code to the client so it can exchange it + // directly — this triggers the PASSWORD_RECOVERY auth event client-side, + // which is required to gate the reset form properly. + if (type === "recovery" && code) { + return NextResponse.redirect(`${requestUrl.origin}/reset-password?code=${code}`) + } if (code) { const cookieStore = cookies() const supabase = createRouteHandlerClient({ cookies: () => cookieStore }) - + try { - // Exchange the code for a session await supabase.auth.exchangeCodeForSession(code) - - // Check if this is an email verification - const type = requestUrl.searchParams.get("type") - + if (type === "email_confirmation") { - // Email verification - redirect to login with success message - return NextResponse.redirect(`https://intaroomv2.vercel.app/login?verified=true`) - } - - if (type === "recovery") { - // Password recovery - redirect to reset password page - return NextResponse.redirect(`https://intaroomv2.vercel.app/reset-password`) + return NextResponse.redirect(`${requestUrl.origin}/login?verified=true`) } - - // Regular auth callback - redirect to main app - return NextResponse.redirect(`https://intaroomv2.vercel.app/`) - - } catch (error) { - console.error("Auth callback error:", error) - // If there's an error, redirect to login with error parameter - return NextResponse.redirect(`https://intaroomv2.vercel.app/login?error=auth_callback_failed`) + + return NextResponse.redirect(`${requestUrl.origin}/`) + } catch { + return NextResponse.redirect(`${requestUrl.origin}/login?error=auth_callback_failed`) } } - // No code provided - redirect to login - return NextResponse.redirect(`https://intaroomv2.vercel.app/login?error=missing_auth_code`) + return NextResponse.redirect(`${requestUrl.origin}/login?error=missing_auth_code`) } diff --git a/app/forgot-password/page.tsx b/app/forgot-password/page.tsx index 9f3b8e8..b23b63a 100644 --- a/app/forgot-password/page.tsx +++ b/app/forgot-password/page.tsx @@ -7,18 +7,14 @@ import { createClientComponentClient } from "@supabase/auth-helpers-nextjs" import Link from "next/link" import { supabaseUrl, supabaseAnonKey } from "@/app/env" +const supabase = createClientComponentClient({ supabaseUrl, supabaseKey: supabaseAnonKey }) + export default function ForgotPasswordPage() { const [email, setEmail] = useState("") const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [success, setSuccess] = useState(false) - // Initialize Supabase client with explicit URL and key - const supabase = createClientComponentClient({ - supabaseUrl, - supabaseKey: supabaseAnonKey, - }) - const handleResetPassword = async (e: React.FormEvent) => { e.preventDefault() setError(null) @@ -26,7 +22,7 @@ export default function ForgotPasswordPage() { try { const { error } = await supabase.auth.resetPasswordForEmail(email, { - redirectTo: `${window.location.origin}/reset-password`, + redirectTo: `${window.location.origin}/auth/callback?type=recovery`, }) if (error) throw error @@ -53,7 +49,6 @@ export default function ForgotPasswordPage() {

Password reset email sent. Please check your inbox.

-
-
- setEmail(e.target.value)} - className="w-full px-4 py-3 rounded-full bg-transparent border border-white/30 text-white placeholder:text-white/70 focus:outline-none focus:border-white/50" - required - /> -
+ setEmail(e.target.value)} + className="w-full px-4 py-3 rounded-full bg-transparent border border-white/30 text-white placeholder:text-white/70 focus:outline-none focus:border-white/50" + required + /> + +
+ + Back to Login + +
)} - -
- - Back to Login - -
) diff --git a/app/login/page.tsx b/app/login/page.tsx index 7595e24..1591b77 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -10,9 +10,10 @@ import { Label } from "@/components/ui/label" import Link from "next/link" import { supabaseUrl, supabaseAnonKey } from "@/app/env" import { CheckCircle, Loader2, AlertCircle } from "lucide-react" -import LineLoginButton from "@/components/line-login-button" import { useLiff } from "@/components/liff-provider" +const supabase = createClientComponentClient({ supabaseUrl, supabaseKey: supabaseAnonKey }) + export default function LoginPage() { const router = useRouter() const searchParams = useSearchParams() @@ -25,13 +26,6 @@ export default function LoginPage() { const [verificationSuccess, setVerificationSuccess] = useState(false) const { isLoggedIn, profile } = useLiff() - // Initialize Supabase client with explicit URL and key - const supabase = createClientComponentClient({ - supabaseUrl, - supabaseKey: supabaseAnonKey, - }) - - // Check for verification success parameter useEffect(() => { const verified = searchParams.get("verified") if (verified === "true") { @@ -41,27 +35,21 @@ export default function LoginPage() { // Check auth state only once on mount useEffect(() => { - let isMounted = true; - + let isMounted = true + const checkAuthState = async () => { try { - console.log("🔍 Checking authentication state...") - - // Check admin login first (highest priority) via the HttpOnly cookie. try { const adminRes = await fetch("/api/admin/verify") if (adminRes.ok) { - console.log("👑 Admin session found, redirecting to admin panel") if (isMounted) router.push("/admin") return } } catch { - // No admin session — continue with regular auth checks + // No admin session — continue } - // Check LINE login if (isLoggedIn && profile) { - console.log("📱 LINE user logged in, checking profile completion") try { const { data: userProfile } = await supabase .from("profiles") @@ -69,50 +57,37 @@ export default function LoginPage() { .eq("line_user_id", profile.userId) .single() - if (userProfile && userProfile.full_name && userProfile.telephone) { - console.log("✅ LINE user profile complete, redirecting to main page") + if (userProfile?.full_name && userProfile?.telephone) { if (isMounted) router.push("/") } else { - console.log("⚠️ LINE user profile incomplete, redirecting to profile setup") if (isMounted) router.push("/profile?setup=true") } - } catch (error) { - console.error("❌ Error checking LINE user profile:", error) + } catch { if (isMounted) router.push("/profile?setup=true") } return } - // Check Supabase session (lowest priority) const { data } = await supabase.auth.getSession() if (data.session) { - // If user logged in without "Remember Me", sessionStorage flag is gone after - // the browser closes (sessionStorage doesn't survive restarts). Sign them out. - const remembered = sessionStorage.getItem("rememberMe") + const remembered = localStorage.getItem("rememberMe") const sessionActive = sessionStorage.getItem("sessionActive") if (!remembered && !sessionActive) { - console.log("🚪 Session found but not remembered — signing out") await supabase.auth.signOut() return } sessionStorage.setItem("sessionActive", "true") - console.log("🔐 Supabase session found, redirecting to main page") - console.log("👤 Session user:", data.session.user.email) if (isMounted) router.push("/") - return } - - console.log("🚫 No active sessions found") - } catch (error) { - console.error("❌ Auth state check error:", error) + } catch { + // Silently ignore auth check errors on mount } } - // Debounce the auth check to prevent multiple rapid calls const timeoutId = setTimeout(checkAuthState, 100) - + return () => { - isMounted = false; + isMounted = false clearTimeout(timeoutId) } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -123,138 +98,91 @@ export default function LoginPage() { setError(null) setLoading(true) - console.log("🔐 Login attempt started:", { username, userType, timestamp: new Date().toISOString() }) - - // Add timeout to prevent infinite loading states const loginTimeout = setTimeout(() => { setLoading(false) setError("Login is taking too long. Please try again.") - console.error("⏰ Login timeout reached") - }, 30000) // 30 seconds timeout + }, 30000) try { if (userType === "admin") { - // Admin login - validated server-side via API route const res = await fetch("/api/admin/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password }), }) - if (!res.ok) { - throw new Error("Invalid admin credentials") - } + if (!res.ok) throw new Error("Invalid admin credentials") - console.log("✅ Admin login successful, redirecting...") + clearTimeout(loginTimeout) router.push("/admin") return - } else { - // Regular user login - let email = username - let userProfile = null - - // If username doesn't contain @ symbol, look up the email by username - if (!username.includes("@")) { - console.log("🔍 Looking up email for username:", username) - - try { - const { data: profileData, error: profileError } = await supabase - .from("profiles") - .select("email, id, full_name, username") - .eq("username", username) - .single() + } - if (profileError) { - console.error("❌ Profile lookup error:", profileError) - if (profileError.code === "PGRST116") { - throw new Error("Username not found. Please check your username or register a new account.") - } else { - throw new Error(`Profile lookup failed: ${profileError.message}`) - } - } + let email = username + let userProfile = null - if (!profileData) { + if (!username.includes("@")) { + try { + const { data: profileData, error: profileError } = await supabase + .from("profiles") + .select("email, id, full_name, username") + .eq("username", username) + .single() + + if (profileError) { + if (profileError.code === "PGRST116") { throw new Error("Username not found. Please check your username or register a new account.") } - - email = profileData.email - userProfile = profileData - console.log("✅ Found email for username:", email) - } catch (lookupError: any) { - console.error("❌ Username lookup failed:", lookupError) - throw lookupError + throw new Error(`Profile lookup failed: ${profileError.message}`) } - } - - // Now login with the email - console.log("🔑 Attempting Supabase authentication with email:", email) - - const { data, error } = await supabase.auth.signInWithPassword({ - email: email, - password: password, - }) - if (error) { - console.error("❌ Supabase authentication error:", error) - - if (error.message.includes("Invalid login credentials")) { - throw new Error("Invalid username or password. Please check your credentials and try again.") - } else if (error.message.includes("Email not confirmed")) { - throw new Error("Please verify your email before logging in. Check your inbox for the verification link.") - } else { - throw new Error(`Authentication failed: ${error.message}`) + if (!profileData) { + throw new Error("Username not found. Please check your username or register a new account.") } - } - if (!data.user) { - throw new Error("Login failed. No user data received from authentication service.") + email = profileData.email + userProfile = profileData + } catch (lookupError: unknown) { + throw lookupError } + } - console.log("✅ Authentication successful for user:", data.user.id) + const { data, error } = await supabase.auth.signInWithPassword({ email, password }) - // Check if email is verified - if (!data.user.email_confirmed_at) { - console.warn("⚠️ User email not confirmed, signing out") - await supabase.auth.signOut() + if (error) { + if (error.message.includes("Invalid login credentials")) { + throw new Error("Invalid username or password. Please check your credentials and try again.") + } else if (error.message.includes("Email not confirmed")) { throw new Error("Please verify your email before logging in. Check your inbox for the verification link.") } + throw new Error(`Authentication failed: ${error.message}`) + } - // Store user info in localStorage for quick access - if (userProfile) { - const userData = { - id: data.user.id, - email: data.user.email, - username: userProfile.username, - full_name: userProfile.full_name, - } - localStorage.setItem("currentUser", JSON.stringify(userData)) - console.log("💾 User data stored in localStorage:", userData) - } - - console.log("🎉 Login successful, redirecting to main page") + if (!data.user) { + throw new Error("Login failed. No user data received from authentication service.") + } - // Persist "remember me" choice so the auth check knows what to do - if (rememberMe) { - sessionStorage.setItem("rememberMe", "true") - } else { - sessionStorage.removeItem("rememberMe") - } - sessionStorage.setItem("sessionActive", "true") + if (!data.user.email_confirmed_at) { + await supabase.auth.signOut() + throw new Error("Please verify your email before logging in. Check your inbox for the verification link.") + } - // Clear the timeout since login was successful - clearTimeout(loginTimeout) + void userProfile // profile data is available from the session if needed - // Use router for navigation instead of window.location - router.push("/") - return + if (rememberMe) { + localStorage.setItem("rememberMe", "true") + } else { + localStorage.removeItem("rememberMe") } + sessionStorage.setItem("sessionActive", "true") + + clearTimeout(loginTimeout) + router.push("/") } catch (error) { - console.error("❌ Overall login error:", error) - clearTimeout(loginTimeout) // Clear timeout on error too + clearTimeout(loginTimeout) setError(error instanceof Error ? error.message : "Failed to login. Please try again.") } finally { setLoading(false) - console.log("🏁 Login attempt completed") } } @@ -306,31 +234,27 @@ export default function LoginPage() {
-
- setUsername(e.target.value)} - className="w-full px-4 py-3 rounded-full bg-transparent border border-white/30 text-white placeholder:text-white/70 focus:outline-none focus:border-white/50" - required - disabled={loading} - suppressHydrationWarning - /> -
- -
- setPassword(e.target.value)} - className="w-full px-4 py-3 rounded-full bg-transparent border border-white/30 text-white placeholder:text-white/70 focus:outline-none focus:border-white/50" - required - disabled={loading} - suppressHydrationWarning - /> -
+ setUsername(e.target.value)} + className="w-full px-4 py-3 rounded-full bg-transparent border border-white/30 text-white placeholder:text-white/70 focus:outline-none focus:border-white/50" + required + disabled={loading} + suppressHydrationWarning + /> + + setPassword(e.target.value)} + className="w-full px-4 py-3 rounded-full bg-transparent border border-white/30 text-white placeholder:text-white/70 focus:outline-none focus:border-white/50" + required + disabled={loading} + suppressHydrationWarning + />
@@ -380,7 +304,6 @@ export default function LoginPage() {
)} -
) diff --git a/app/register-success/page.tsx b/app/register-success/page.tsx index 6e11862..a8b31bc 100644 --- a/app/register-success/page.tsx +++ b/app/register-success/page.tsx @@ -1,69 +1,28 @@ "use client" import Link from "next/link" -import { useRouter } from "next/navigation" -import { useEffect } from "react" +import { MailCheck } from "lucide-react" export default function RegisterSuccessPage() { - const router = useRouter() - - useEffect(() => { - const timer = setTimeout(() => { - router.push("/") - }, 2000) - - return () => clearTimeout(timer) - }, [router]) - return (
- {/* Redirect Icon */}
- - - +

- Redirecting to Main Page + Check Your Email

- You will be redirected to the main page in just a few seconds + We sent a verification link to your email address. Click the link to activate your account before logging in.

- {/*
-

Account created successfully!

-
*/} - - {/*
-

- 💡 Tip: Check your spam folder if you don't see the email -

-
*/} - - {/*
-

Open your email to verify your account:

- - - - - - - - Open Gmail - -

- 💡 Check your spam folder if you don't see the email -

-
*/} +
+ Check your spam folder if you don't see it within a few minutes. +
) -} \ No newline at end of file +} diff --git a/app/register/page.tsx b/app/register/page.tsx index 72ad669..5173b82 100644 --- a/app/register/page.tsx +++ b/app/register/page.tsx @@ -8,6 +8,8 @@ import { createClientComponentClient } from "@supabase/auth-helpers-nextjs" import Link from "next/link" import { supabaseUrl, supabaseAnonKey } from "@/app/env" +const supabase = createClientComponentClient({ supabaseUrl, supabaseKey: supabaseAnonKey }) + export default function RegisterPage() { const router = useRouter() const [email, setEmail] = useState("") @@ -18,17 +20,10 @@ export default function RegisterPage() { const [loading, setLoading] = useState(false) const [error, setError] = useState(null) - // Initialize Supabase client with explicit URL and key - const supabase = createClientComponentClient({ - supabaseUrl, - supabaseKey: supabaseAnonKey, - }) - const handleRegister = async (e: React.FormEvent) => { e.preventDefault() setError(null) - // Validate passwords match if (password !== confirmPassword) { setError("Passwords do not match") return @@ -37,16 +32,14 @@ export default function RegisterPage() { setLoading(true) try { - // Sign up with email and password const { data, error } = await supabase.auth.signUp({ email, password, options: { - emailRedirectTo: `https://intaroomv2.vercel.app/auth/callback?type=email_confirmation`, + emailRedirectTo: `${window.location.origin}/auth/callback?type=email_confirmation`, data: { full_name: fullName, username: username, - // Include any additional fields you want to capture }, }, }) @@ -54,11 +47,6 @@ export default function RegisterPage() { if (error) throw error if (data.user) { - // Wait a bit for the database trigger to create the profile - await new Promise(resolve => setTimeout(resolve, 1000)) - - console.log("Registration successful, redirecting to success page") - // Redirect to success page immediately router.push("/register-success") } else { throw new Error("Registration failed. Please try again.") @@ -81,16 +69,14 @@ export default function RegisterPage() {
-
- setFullName(e.target.value)} - className="w-full px-4 py-3 rounded-full bg-transparent border border-white/30 text-white placeholder:text-white/70 focus:outline-none focus:border-white/50" - required - /> -
+ setFullName(e.target.value)} + className="w-full px-4 py-3 rounded-full bg-transparent border border-white/30 text-white placeholder:text-white/70 focus:outline-none focus:border-white/50" + required + />
Choose a unique username for login

-
- setEmail(e.target.value)} - className="w-full px-4 py-3 rounded-full bg-transparent border border-white/30 text-white placeholder:text-white/70 focus:outline-none focus:border-white/50" - required - /> -
- -
- setPassword(e.target.value)} - className="w-full px-4 py-3 rounded-full bg-transparent border border-white/30 text-white placeholder:text-white/70 focus:outline-none focus:border-white/50" - required - /> -
- -
- setConfirmPassword(e.target.value)} - className="w-full px-4 py-3 rounded-full bg-transparent border border-white/30 text-white placeholder:text-white/70 focus:outline-none focus:border-white/50" - required - /> -
+ setEmail(e.target.value)} + className="w-full px-4 py-3 rounded-full bg-transparent border border-white/30 text-white placeholder:text-white/70 focus:outline-none focus:border-white/50" + required + /> + + setPassword(e.target.value)} + className="w-full px-4 py-3 rounded-full bg-transparent border border-white/30 text-white placeholder:text-white/70 focus:outline-none focus:border-white/50" + required + /> + + setConfirmPassword(e.target.value)} + className="w-full px-4 py-3 rounded-full bg-transparent border border-white/30 text-white placeholder:text-white/70 focus:outline-none focus:border-white/50" + required + /> @@ -157,4 +137,3 @@ export default function RegisterPage() {
) } - diff --git a/app/reset-password/page.tsx b/app/reset-password/page.tsx index c845964..1e017b3 100644 --- a/app/reset-password/page.tsx +++ b/app/reset-password/page.tsx @@ -2,42 +2,71 @@ import type React from "react" -import { useState, useEffect } from "react" -import { useRouter } from "next/navigation" +import { useState, useEffect, useRef } from "react" +import { useRouter, useSearchParams } from "next/navigation" import { createClientComponentClient } from "@supabase/auth-helpers-nextjs" import { supabaseUrl, supabaseAnonKey } from "@/app/env" +const supabase = createClientComponentClient({ supabaseUrl, supabaseKey: supabaseAnonKey }) + export default function ResetPasswordPage() { const router = useRouter() + const searchParams = useSearchParams() const [password, setPassword] = useState("") const [confirmPassword, setConfirmPassword] = useState("") const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [success, setSuccess] = useState(false) + const [sessionReady, setSessionReady] = useState(false) + const redirectTimer = useRef | null>(null) - // Initialize Supabase client with explicit URL and key - const supabase = createClientComponentClient({ - supabaseUrl, - supabaseKey: supabaseAnonKey, - }) - - // Check if user has a valid recovery session useEffect(() => { - const checkSession = async () => { - const { data, error } = await supabase.auth.getSession() - if (error || !data.session) { + const code = searchParams.get("code") + + const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => { + if (event === "PASSWORD_RECOVERY") { + setSessionReady(true) + // Remove the code from the URL so it can't be replayed + router.replace("/reset-password") + } else if (!session) { router.push("/login") } + }) + + if (code) { + // Exchange client-side — this triggers the PASSWORD_RECOVERY event above + supabase.auth.exchangeCodeForSession(code).catch(() => router.push("/login")) + } else { + // No code in URL: if there's no existing session either, redirect to login. + // A logged-in user who navigates here directly will not get a PASSWORD_RECOVERY + // event, so sessionReady stays false and they are redirected by the timeout below. + supabase.auth.getSession().then(({ data }) => { + if (!data.session) router.push("/login") + }) } - checkSession() - }, [router, supabase.auth]) + // Fallback: if PASSWORD_RECOVERY never fires within 8 seconds, redirect to login + const fallback = setTimeout(() => { + if (!sessionReady) router.push("/login") + }, 8000) + + return () => { + subscription.unsubscribe() + clearTimeout(fallback) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + useEffect(() => { + return () => { + if (redirectTimer.current) clearTimeout(redirectTimer.current) + } + }, []) const handleResetPassword = async (e: React.FormEvent) => { e.preventDefault() setError(null) - // Validate passwords match if (password !== confirmPassword) { setError("Passwords do not match") return @@ -46,18 +75,10 @@ export default function ResetPasswordPage() { setLoading(true) try { - const { error } = await supabase.auth.updateUser({ - password, - }) - + const { error } = await supabase.auth.updateUser({ password }) if (error) throw error - setSuccess(true) - - // Redirect to login after 3 seconds - setTimeout(() => { - router.push("/login") - }, 3000) + redirectTimer.current = setTimeout(() => router.push("/login"), 3000) } catch (error) { setError(error instanceof Error ? error.message : "Failed to reset password") } finally { @@ -65,6 +86,14 @@ export default function ResetPasswordPage() { } } + if (!sessionReady && !success) { + return ( +
+
Verifying reset link...
+
+ ) + } + return (
@@ -75,40 +104,32 @@ export default function ResetPasswordPage() { )} {success ? ( -
-
-

Password has been reset successfully. Redirecting to login...

-
+
+

Password has been reset successfully. Redirecting to login...

) : (
-
- setPassword(e.target.value)} - className="w-full px-4 py-3 rounded-full bg-transparent border border-white/30 text-white placeholder:text-white/70 focus:outline-none focus:border-white/50" - required - /> -
- -
- setConfirmPassword(e.target.value)} - className="w-full px-4 py-3 rounded-full bg-transparent border border-white/30 text-white placeholder:text-white/70 focus:outline-none focus:border-white/50" - required - /> -
- + setPassword(e.target.value)} + className="w-full px-4 py-3 rounded-full bg-transparent border border-white/30 text-white placeholder:text-white/70 focus:outline-none focus:border-white/50" + required + /> + setConfirmPassword(e.target.value)} + className="w-full px-4 py-3 rounded-full bg-transparent border border-white/30 text-white placeholder:text-white/70 focus:outline-none focus:border-white/50" + required + />