Spaces:
Sleeping
Sleeping
Fix all auth edge cases: proxy blocks+redirects properly, auth pages redirect if logged in, loading state prevents form flash, callback honors ?next param
Browse files- web/app/auth/callback/route.ts +4 -3
- web/app/auth/forgot-password/page.tsx +29 -41
- web/app/auth/login/page.tsx +31 -13
- web/app/auth/signup/page.tsx +26 -23
- web/proxy.ts +23 -11
web/app/auth/callback/route.ts
CHANGED
|
@@ -4,17 +4,18 @@ import { NextResponse } from "next/server";
|
|
| 4 |
export async function GET(request: Request) {
|
| 5 |
const requestUrl = new URL(request.url);
|
| 6 |
const code = requestUrl.searchParams.get("code");
|
| 7 |
-
const next = requestUrl.searchParams.get("next")
|
| 8 |
const origin = requestUrl.origin;
|
| 9 |
|
| 10 |
if (code) {
|
| 11 |
const supabase = await createClient();
|
| 12 |
const { error } = await supabase.auth.exchangeCodeForSession(code);
|
| 13 |
-
|
| 14 |
if (!error) {
|
| 15 |
return NextResponse.redirect(`${origin}${next}`);
|
| 16 |
}
|
| 17 |
}
|
| 18 |
|
| 19 |
-
|
|
|
|
|
|
|
| 20 |
}
|
|
|
|
| 4 |
export async function GET(request: Request) {
|
| 5 |
const requestUrl = new URL(request.url);
|
| 6 |
const code = requestUrl.searchParams.get("code");
|
| 7 |
+
const next = requestUrl.searchParams.get("next") || "/dashboard-pages/dashboard";
|
| 8 |
const origin = requestUrl.origin;
|
| 9 |
|
| 10 |
if (code) {
|
| 11 |
const supabase = await createClient();
|
| 12 |
const { error } = await supabase.auth.exchangeCodeForSession(code);
|
|
|
|
| 13 |
if (!error) {
|
| 14 |
return NextResponse.redirect(`${origin}${next}`);
|
| 15 |
}
|
| 16 |
}
|
| 17 |
|
| 18 |
+
// If code exchange failed, try hash-based recovery (password reset flow)
|
| 19 |
+
// Supabase sends recovery tokens as hash fragments which the client handles
|
| 20 |
+
return NextResponse.redirect(`${origin}${next}`);
|
| 21 |
}
|
web/app/auth/forgot-password/page.tsx
CHANGED
|
@@ -1,59 +1,57 @@
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
-
import { useState } from "react";
|
| 4 |
import { createClient } from "@/lib/supabase/client";
|
| 5 |
import { getBaseUrl } from "@/lib/auth-url";
|
| 6 |
import Link from "next/link";
|
| 7 |
-
import { ArrowLeft, Mail, Check } from "lucide-react";
|
| 8 |
|
| 9 |
export default function ForgotPasswordPage() {
|
| 10 |
const [email, setEmail] = useState("");
|
| 11 |
const [loading, setLoading] = useState(false);
|
|
|
|
| 12 |
const [sent, setSent] = useState(false);
|
| 13 |
const [error, setError] = useState("");
|
| 14 |
const supabase = createClient();
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
| 20 |
|
|
|
|
|
|
|
| 21 |
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
| 22 |
redirectTo: `${getBaseUrl()}/auth/reset-password`,
|
| 23 |
});
|
| 24 |
-
|
| 25 |
-
if (error) { setError(error.message); }
|
| 26 |
-
else { setSent(true); }
|
| 27 |
setLoading(false);
|
| 28 |
}
|
| 29 |
|
| 30 |
async function handleMagicLink(e: React.FormEvent) {
|
| 31 |
-
e.preventDefault();
|
| 32 |
-
setLoading(true);
|
| 33 |
-
setError("");
|
| 34 |
-
|
| 35 |
const { error } = await supabase.auth.signInWithOtp({
|
| 36 |
-
email,
|
| 37 |
-
options: { emailRedirectTo: `${getBaseUrl()}/auth/callback` },
|
| 38 |
});
|
| 39 |
-
|
| 40 |
-
if (error) { setError(error.message); }
|
| 41 |
-
else { setSent(true); }
|
| 42 |
setLoading(false);
|
| 43 |
}
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
if (sent) {
|
| 46 |
return (
|
| 47 |
<div className="min-h-screen flex items-center justify-center bg-white px-4">
|
| 48 |
<div className="w-full max-w-sm text-center">
|
| 49 |
-
<div className="w-12 h-12 rounded-full bg-emerald-50 flex items-center justify-center mx-auto mb-4">
|
| 50 |
-
<Check className="w-6 h-6 text-emerald-600" />
|
| 51 |
-
</div>
|
| 52 |
<h1 className="text-xl font-semibold">Check your email</h1>
|
| 53 |
-
<p className="mt-2 text-sm text-zinc-500">We sent a link to <span className="font-medium text-zinc-700">{email}</span>.
|
| 54 |
-
<Link href="/auth/login" className="mt-6 inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-700">
|
| 55 |
-
<ArrowLeft className="w-3.5 h-3.5" /> Back to login
|
| 56 |
-
</Link>
|
| 57 |
</div>
|
| 58 |
</div>
|
| 59 |
);
|
|
@@ -63,16 +61,13 @@ export default function ForgotPasswordPage() {
|
|
| 63 |
<div className="min-h-screen flex items-center justify-center bg-white px-4">
|
| 64 |
<div className="w-full max-w-sm">
|
| 65 |
<div className="mb-8">
|
| 66 |
-
<Link href="/auth/login" className="inline-flex items-center gap-1.5 text-sm text-zinc-400 hover:text-zinc-600">
|
| 67 |
-
<ArrowLeft className="w-3.5 h-3.5" /> Back to login
|
| 68 |
-
</Link>
|
| 69 |
<h1 className="mt-4 text-xl font-semibold">Reset your password</h1>
|
| 70 |
-
<p className="mt-1 text-sm text-zinc-500">
|
| 71 |
</div>
|
| 72 |
|
| 73 |
<form onSubmit={handleReset} className="space-y-3">
|
| 74 |
-
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required
|
| 75 |
-
placeholder="Email address"
|
| 76 |
className="w-full px-3 py-2.5 border border-zinc-200 rounded-lg text-sm focus:outline-none focus:border-zinc-400" />
|
| 77 |
{error && <p className="text-xs text-red-600">{error}</p>}
|
| 78 |
<button type="submit" disabled={loading}
|
|
@@ -81,23 +76,16 @@ export default function ForgotPasswordPage() {
|
|
| 81 |
</button>
|
| 82 |
</form>
|
| 83 |
|
| 84 |
-
<div className="flex items-center gap-3 my-5">
|
| 85 |
-
<div className="flex-1 h-px bg-zinc-100" />
|
| 86 |
-
<span className="text-xs text-zinc-300">or</span>
|
| 87 |
-
<div className="flex-1 h-px bg-zinc-100" />
|
| 88 |
-
</div>
|
| 89 |
|
| 90 |
<form onSubmit={handleMagicLink}>
|
| 91 |
<button type="submit" disabled={loading || !email}
|
| 92 |
className="w-full flex items-center justify-center gap-2 border border-zinc-200 py-2.5 rounded-lg text-sm text-zinc-600 hover:bg-zinc-50 disabled:opacity-40 transition-colors">
|
| 93 |
-
<Mail className="w-4 h-4" />
|
| 94 |
-
Send magic link instead
|
| 95 |
</button>
|
| 96 |
</form>
|
| 97 |
|
| 98 |
-
<p className="mt-6 text-center text-xs text-zinc-400">
|
| 99 |
-
No account? <Link href="/auth/signup" className="text-zinc-600 hover:underline">Sign up</Link>
|
| 100 |
-
</p>
|
| 101 |
</div>
|
| 102 |
</div>
|
| 103 |
);
|
|
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
+
import { useState, useEffect } from "react";
|
| 4 |
import { createClient } from "@/lib/supabase/client";
|
| 5 |
import { getBaseUrl } from "@/lib/auth-url";
|
| 6 |
import Link from "next/link";
|
| 7 |
+
import { ArrowLeft, Mail, Check, Loader2 } from "lucide-react";
|
| 8 |
|
| 9 |
export default function ForgotPasswordPage() {
|
| 10 |
const [email, setEmail] = useState("");
|
| 11 |
const [loading, setLoading] = useState(false);
|
| 12 |
+
const [checking, setChecking] = useState(true);
|
| 13 |
const [sent, setSent] = useState(false);
|
| 14 |
const [error, setError] = useState("");
|
| 15 |
const supabase = createClient();
|
| 16 |
|
| 17 |
+
// Redirect if already logged in (no reason to reset password)
|
| 18 |
+
useEffect(() => {
|
| 19 |
+
supabase.auth.getUser().then(({ data: { user } }) => {
|
| 20 |
+
if (user) { window.location.href = "/dashboard-pages/settings"; }
|
| 21 |
+
else { setChecking(false); }
|
| 22 |
+
});
|
| 23 |
+
}, []);
|
| 24 |
|
| 25 |
+
async function handleReset(e: React.FormEvent) {
|
| 26 |
+
e.preventDefault(); setLoading(true); setError("");
|
| 27 |
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
| 28 |
redirectTo: `${getBaseUrl()}/auth/reset-password`,
|
| 29 |
});
|
| 30 |
+
if (error) { setError(error.message); } else { setSent(true); }
|
|
|
|
|
|
|
| 31 |
setLoading(false);
|
| 32 |
}
|
| 33 |
|
| 34 |
async function handleMagicLink(e: React.FormEvent) {
|
| 35 |
+
e.preventDefault(); setLoading(true); setError("");
|
|
|
|
|
|
|
|
|
|
| 36 |
const { error } = await supabase.auth.signInWithOtp({
|
| 37 |
+
email, options: { emailRedirectTo: `${getBaseUrl()}/auth/callback` },
|
|
|
|
| 38 |
});
|
| 39 |
+
if (error) { setError(error.message); } else { setSent(true); }
|
|
|
|
|
|
|
| 40 |
setLoading(false);
|
| 41 |
}
|
| 42 |
|
| 43 |
+
if (checking) {
|
| 44 |
+
return <div className="min-h-screen flex items-center justify-center bg-white"><Loader2 className="w-5 h-5 text-zinc-300 animate-spin" /></div>;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
if (sent) {
|
| 48 |
return (
|
| 49 |
<div className="min-h-screen flex items-center justify-center bg-white px-4">
|
| 50 |
<div className="w-full max-w-sm text-center">
|
| 51 |
+
<div className="w-12 h-12 rounded-full bg-emerald-50 flex items-center justify-center mx-auto mb-4"><Check className="w-6 h-6 text-emerald-600" /></div>
|
|
|
|
|
|
|
| 52 |
<h1 className="text-xl font-semibold">Check your email</h1>
|
| 53 |
+
<p className="mt-2 text-sm text-zinc-500">We sent a link to <span className="font-medium text-zinc-700">{email}</span>.</p>
|
| 54 |
+
<Link href="/auth/login" className="mt-6 inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-700"><ArrowLeft className="w-3.5 h-3.5" /> Back to login</Link>
|
|
|
|
|
|
|
| 55 |
</div>
|
| 56 |
</div>
|
| 57 |
);
|
|
|
|
| 61 |
<div className="min-h-screen flex items-center justify-center bg-white px-4">
|
| 62 |
<div className="w-full max-w-sm">
|
| 63 |
<div className="mb-8">
|
| 64 |
+
<Link href="/auth/login" className="inline-flex items-center gap-1.5 text-sm text-zinc-400 hover:text-zinc-600"><ArrowLeft className="w-3.5 h-3.5" /> Back to login</Link>
|
|
|
|
|
|
|
| 65 |
<h1 className="mt-4 text-xl font-semibold">Reset your password</h1>
|
| 66 |
+
<p className="mt-1 text-sm text-zinc-500">We will send you a reset link.</p>
|
| 67 |
</div>
|
| 68 |
|
| 69 |
<form onSubmit={handleReset} className="space-y-3">
|
| 70 |
+
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required placeholder="Email address"
|
|
|
|
| 71 |
className="w-full px-3 py-2.5 border border-zinc-200 rounded-lg text-sm focus:outline-none focus:border-zinc-400" />
|
| 72 |
{error && <p className="text-xs text-red-600">{error}</p>}
|
| 73 |
<button type="submit" disabled={loading}
|
|
|
|
| 76 |
</button>
|
| 77 |
</form>
|
| 78 |
|
| 79 |
+
<div className="flex items-center gap-3 my-5"><div className="flex-1 h-px bg-zinc-100" /><span className="text-xs text-zinc-300">or</span><div className="flex-1 h-px bg-zinc-100" /></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
<form onSubmit={handleMagicLink}>
|
| 82 |
<button type="submit" disabled={loading || !email}
|
| 83 |
className="w-full flex items-center justify-center gap-2 border border-zinc-200 py-2.5 rounded-lg text-sm text-zinc-600 hover:bg-zinc-50 disabled:opacity-40 transition-colors">
|
| 84 |
+
<Mail className="w-4 h-4" /> Send magic link instead
|
|
|
|
| 85 |
</button>
|
| 86 |
</form>
|
| 87 |
|
| 88 |
+
<p className="mt-6 text-center text-xs text-zinc-400">No account? <Link href="/auth/signup" className="text-zinc-600 hover:underline">Sign up</Link></p>
|
|
|
|
|
|
|
| 89 |
</div>
|
| 90 |
</div>
|
| 91 |
);
|
web/app/auth/login/page.tsx
CHANGED
|
@@ -1,45 +1,63 @@
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
-
import { useState } from "react";
|
| 4 |
import { createClient } from "@/lib/supabase/client";
|
| 5 |
import { getBaseUrl } from "@/lib/auth-url";
|
| 6 |
import Link from "next/link";
|
| 7 |
-
import {
|
|
|
|
| 8 |
|
| 9 |
export default function LoginPage() {
|
| 10 |
const [email, setEmail] = useState("");
|
| 11 |
const [password, setPassword] = useState("");
|
| 12 |
const [error, setError] = useState("");
|
| 13 |
const [loading, setLoading] = useState(false);
|
|
|
|
| 14 |
const [magicSent, setMagicSent] = useState(false);
|
| 15 |
const supabase = createClient();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
async function handleLogin(e: React.FormEvent) {
|
| 18 |
e.preventDefault(); setLoading(true); setError("");
|
| 19 |
const { error } = await supabase.auth.signInWithPassword({ email, password });
|
| 20 |
if (error) { setError(error.message); setLoading(false); }
|
| 21 |
-
else { window.location.href =
|
| 22 |
}
|
| 23 |
|
| 24 |
async function handleMagicLink() {
|
| 25 |
if (!email) { setError("Enter your email first."); return; }
|
| 26 |
setLoading(true); setError("");
|
| 27 |
const { error } = await supabase.auth.signInWithOtp({
|
| 28 |
-
email,
|
| 29 |
-
options: { emailRedirectTo: `${getBaseUrl()}/auth/callback` },
|
| 30 |
});
|
| 31 |
-
if (error) { setError(error.message); }
|
| 32 |
-
else { setMagicSent(true); }
|
| 33 |
setLoading(false);
|
| 34 |
}
|
| 35 |
|
| 36 |
async function handleOAuth(provider: "google" | "github") {
|
| 37 |
await supabase.auth.signInWithOAuth({
|
| 38 |
-
provider,
|
| 39 |
-
options: { redirectTo: `${getBaseUrl()}/auth/callback` },
|
| 40 |
});
|
| 41 |
}
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
if (magicSent) {
|
| 44 |
return (
|
| 45 |
<div className="min-h-screen flex items-center justify-center bg-white px-4">
|
|
@@ -75,10 +93,10 @@ export default function LoginPage() {
|
|
| 75 |
</div>
|
| 76 |
|
| 77 |
<form onSubmit={handleLogin} className="space-y-3">
|
| 78 |
-
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required
|
| 79 |
-
|
| 80 |
-
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required
|
| 81 |
-
|
| 82 |
{error && <p className="text-xs text-red-600">{error}</p>}
|
| 83 |
<button type="submit" disabled={loading}
|
| 84 |
className="w-full bg-zinc-900 text-white py-2.5 rounded-lg text-sm font-medium hover:bg-zinc-800 disabled:opacity-40 transition-colors">
|
|
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
+
import { useState, useEffect } from "react";
|
| 4 |
import { createClient } from "@/lib/supabase/client";
|
| 5 |
import { getBaseUrl } from "@/lib/auth-url";
|
| 6 |
import Link from "next/link";
|
| 7 |
+
import { useSearchParams } from "next/navigation";
|
| 8 |
+
import { ArrowLeft, Mail, Loader2 } from "lucide-react";
|
| 9 |
|
| 10 |
export default function LoginPage() {
|
| 11 |
const [email, setEmail] = useState("");
|
| 12 |
const [password, setPassword] = useState("");
|
| 13 |
const [error, setError] = useState("");
|
| 14 |
const [loading, setLoading] = useState(false);
|
| 15 |
+
const [checking, setChecking] = useState(true);
|
| 16 |
const [magicSent, setMagicSent] = useState(false);
|
| 17 |
const supabase = createClient();
|
| 18 |
+
const searchParams = useSearchParams();
|
| 19 |
+
const next = searchParams.get("next") || "/dashboard-pages/dashboard";
|
| 20 |
+
|
| 21 |
+
// Check if already logged in — redirect immediately
|
| 22 |
+
useEffect(() => {
|
| 23 |
+
supabase.auth.getUser().then(({ data: { user } }) => {
|
| 24 |
+
if (user) { window.location.href = next; }
|
| 25 |
+
else { setChecking(false); }
|
| 26 |
+
});
|
| 27 |
+
}, []);
|
| 28 |
|
| 29 |
async function handleLogin(e: React.FormEvent) {
|
| 30 |
e.preventDefault(); setLoading(true); setError("");
|
| 31 |
const { error } = await supabase.auth.signInWithPassword({ email, password });
|
| 32 |
if (error) { setError(error.message); setLoading(false); }
|
| 33 |
+
else { window.location.href = next; }
|
| 34 |
}
|
| 35 |
|
| 36 |
async function handleMagicLink() {
|
| 37 |
if (!email) { setError("Enter your email first."); return; }
|
| 38 |
setLoading(true); setError("");
|
| 39 |
const { error } = await supabase.auth.signInWithOtp({
|
| 40 |
+
email, options: { emailRedirectTo: `${getBaseUrl()}/auth/callback?next=${encodeURIComponent(next)}` },
|
|
|
|
| 41 |
});
|
| 42 |
+
if (error) { setError(error.message); } else { setMagicSent(true); }
|
|
|
|
| 43 |
setLoading(false);
|
| 44 |
}
|
| 45 |
|
| 46 |
async function handleOAuth(provider: "google" | "github") {
|
| 47 |
await supabase.auth.signInWithOAuth({
|
| 48 |
+
provider, options: { redirectTo: `${getBaseUrl()}/auth/callback?next=${encodeURIComponent(next)}` },
|
|
|
|
| 49 |
});
|
| 50 |
}
|
| 51 |
|
| 52 |
+
// Show nothing while checking auth (prevents form flash)
|
| 53 |
+
if (checking) {
|
| 54 |
+
return (
|
| 55 |
+
<div className="min-h-screen flex items-center justify-center bg-white">
|
| 56 |
+
<Loader2 className="w-5 h-5 text-zinc-300 animate-spin" />
|
| 57 |
+
</div>
|
| 58 |
+
);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
if (magicSent) {
|
| 62 |
return (
|
| 63 |
<div className="min-h-screen flex items-center justify-center bg-white px-4">
|
|
|
|
| 93 |
</div>
|
| 94 |
|
| 95 |
<form onSubmit={handleLogin} className="space-y-3">
|
| 96 |
+
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required placeholder="Email"
|
| 97 |
+
className="w-full px-3 py-2.5 border border-zinc-200 rounded-lg text-sm focus:outline-none focus:border-zinc-400" />
|
| 98 |
+
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required placeholder="Password"
|
| 99 |
+
className="w-full px-3 py-2.5 border border-zinc-200 rounded-lg text-sm focus:outline-none focus:border-zinc-400" />
|
| 100 |
{error && <p className="text-xs text-red-600">{error}</p>}
|
| 101 |
<button type="submit" disabled={loading}
|
| 102 |
className="w-full bg-zinc-900 text-white py-2.5 rounded-lg text-sm font-medium hover:bg-zinc-800 disabled:opacity-40 transition-colors">
|
web/app/auth/signup/page.tsx
CHANGED
|
@@ -1,27 +1,33 @@
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
-
import { useState } from "react";
|
| 4 |
import { createClient } from "@/lib/supabase/client";
|
| 5 |
import { getBaseUrl } from "@/lib/auth-url";
|
| 6 |
import Link from "next/link";
|
| 7 |
-
import { ArrowLeft } from "lucide-react";
|
| 8 |
|
| 9 |
export default function SignupPage() {
|
| 10 |
const [email, setEmail] = useState("");
|
| 11 |
const [password, setPassword] = useState("");
|
| 12 |
const [error, setError] = useState("");
|
| 13 |
const [loading, setLoading] = useState(false);
|
|
|
|
| 14 |
const [done, setDone] = useState(false);
|
| 15 |
const supabase = createClient();
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
async function handleSignup(e: React.FormEvent) {
|
| 18 |
e.preventDefault(); setLoading(true); setError("");
|
| 19 |
const { error } = await supabase.auth.signUp({
|
| 20 |
-
email,
|
| 21 |
-
|
| 22 |
-
options: {
|
| 23 |
-
emailRedirectTo: `${getBaseUrl()}/auth/callback`,
|
| 24 |
-
},
|
| 25 |
});
|
| 26 |
if (error) { setError(error.message); } else { setDone(true); }
|
| 27 |
setLoading(false);
|
|
@@ -29,11 +35,14 @@ export default function SignupPage() {
|
|
| 29 |
|
| 30 |
async function handleOAuth(provider: "google" | "github") {
|
| 31 |
await supabase.auth.signInWithOAuth({
|
| 32 |
-
provider,
|
| 33 |
-
options: { redirectTo: `${getBaseUrl()}/auth/callback` },
|
| 34 |
});
|
| 35 |
}
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
if (done) {
|
| 38 |
return (
|
| 39 |
<div className="min-h-screen flex items-center justify-center bg-white px-4">
|
|
@@ -50,28 +59,22 @@ export default function SignupPage() {
|
|
| 50 |
<div className="min-h-screen flex items-center justify-center bg-white px-4">
|
| 51 |
<div className="w-full max-w-sm">
|
| 52 |
<div className="mb-8">
|
| 53 |
-
<Link href="/" className="inline-flex items-center gap-1.5 text-sm text-zinc-400 hover:text-zinc-600">
|
| 54 |
-
<ArrowLeft className="w-3.5 h-3.5" /> Back
|
| 55 |
-
</Link>
|
| 56 |
<h1 className="mt-4 text-xl font-semibold">Create an account</h1>
|
| 57 |
</div>
|
| 58 |
|
| 59 |
<div className="space-y-2.5">
|
| 60 |
-
<button onClick={() => handleOAuth("google")}
|
| 61 |
-
|
| 62 |
-
<button onClick={() => handleOAuth("github")}
|
| 63 |
-
className="w-full px-4 py-2.5 border border-zinc-200 rounded-lg text-sm hover:bg-zinc-50 transition-colors">Continue with GitHub</button>
|
| 64 |
</div>
|
| 65 |
|
| 66 |
-
<div className="flex items-center gap-3 my-6">
|
| 67 |
-
<div className="flex-1 h-px bg-zinc-100" /><span className="text-xs text-zinc-300">or</span><div className="flex-1 h-px bg-zinc-100" />
|
| 68 |
-
</div>
|
| 69 |
|
| 70 |
<form onSubmit={handleSignup} className="space-y-3">
|
| 71 |
-
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required
|
| 72 |
-
|
| 73 |
-
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required minLength={8}
|
| 74 |
-
|
| 75 |
{error && <p className="text-xs text-red-600">{error}</p>}
|
| 76 |
<button type="submit" disabled={loading}
|
| 77 |
className="w-full bg-zinc-900 text-white py-2.5 rounded-lg text-sm font-medium hover:bg-zinc-800 disabled:opacity-40 transition-colors">
|
|
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
+
import { useState, useEffect } from "react";
|
| 4 |
import { createClient } from "@/lib/supabase/client";
|
| 5 |
import { getBaseUrl } from "@/lib/auth-url";
|
| 6 |
import Link from "next/link";
|
| 7 |
+
import { ArrowLeft, Loader2 } from "lucide-react";
|
| 8 |
|
| 9 |
export default function SignupPage() {
|
| 10 |
const [email, setEmail] = useState("");
|
| 11 |
const [password, setPassword] = useState("");
|
| 12 |
const [error, setError] = useState("");
|
| 13 |
const [loading, setLoading] = useState(false);
|
| 14 |
+
const [checking, setChecking] = useState(true);
|
| 15 |
const [done, setDone] = useState(false);
|
| 16 |
const supabase = createClient();
|
| 17 |
|
| 18 |
+
// Redirect if already logged in
|
| 19 |
+
useEffect(() => {
|
| 20 |
+
supabase.auth.getUser().then(({ data: { user } }) => {
|
| 21 |
+
if (user) { window.location.href = "/dashboard-pages/dashboard"; }
|
| 22 |
+
else { setChecking(false); }
|
| 23 |
+
});
|
| 24 |
+
}, []);
|
| 25 |
+
|
| 26 |
async function handleSignup(e: React.FormEvent) {
|
| 27 |
e.preventDefault(); setLoading(true); setError("");
|
| 28 |
const { error } = await supabase.auth.signUp({
|
| 29 |
+
email, password,
|
| 30 |
+
options: { emailRedirectTo: `${getBaseUrl()}/auth/callback` },
|
|
|
|
|
|
|
|
|
|
| 31 |
});
|
| 32 |
if (error) { setError(error.message); } else { setDone(true); }
|
| 33 |
setLoading(false);
|
|
|
|
| 35 |
|
| 36 |
async function handleOAuth(provider: "google" | "github") {
|
| 37 |
await supabase.auth.signInWithOAuth({
|
| 38 |
+
provider, options: { redirectTo: `${getBaseUrl()}/auth/callback` },
|
|
|
|
| 39 |
});
|
| 40 |
}
|
| 41 |
|
| 42 |
+
if (checking) {
|
| 43 |
+
return <div className="min-h-screen flex items-center justify-center bg-white"><Loader2 className="w-5 h-5 text-zinc-300 animate-spin" /></div>;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
if (done) {
|
| 47 |
return (
|
| 48 |
<div className="min-h-screen flex items-center justify-center bg-white px-4">
|
|
|
|
| 59 |
<div className="min-h-screen flex items-center justify-center bg-white px-4">
|
| 60 |
<div className="w-full max-w-sm">
|
| 61 |
<div className="mb-8">
|
| 62 |
+
<Link href="/" className="inline-flex items-center gap-1.5 text-sm text-zinc-400 hover:text-zinc-600"><ArrowLeft className="w-3.5 h-3.5" /> Back</Link>
|
|
|
|
|
|
|
| 63 |
<h1 className="mt-4 text-xl font-semibold">Create an account</h1>
|
| 64 |
</div>
|
| 65 |
|
| 66 |
<div className="space-y-2.5">
|
| 67 |
+
<button onClick={() => handleOAuth("google")} className="w-full px-4 py-2.5 border border-zinc-200 rounded-lg text-sm hover:bg-zinc-50 transition-colors">Continue with Google</button>
|
| 68 |
+
<button onClick={() => handleOAuth("github")} className="w-full px-4 py-2.5 border border-zinc-200 rounded-lg text-sm hover:bg-zinc-50 transition-colors">Continue with GitHub</button>
|
|
|
|
|
|
|
| 69 |
</div>
|
| 70 |
|
| 71 |
+
<div className="flex items-center gap-3 my-6"><div className="flex-1 h-px bg-zinc-100" /><span className="text-xs text-zinc-300">or</span><div className="flex-1 h-px bg-zinc-100" /></div>
|
|
|
|
|
|
|
| 72 |
|
| 73 |
<form onSubmit={handleSignup} className="space-y-3">
|
| 74 |
+
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required placeholder="Email"
|
| 75 |
+
className="w-full px-3 py-2.5 border border-zinc-200 rounded-lg text-sm focus:outline-none focus:border-zinc-400" />
|
| 76 |
+
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required minLength={8} placeholder="Password (8+ characters)"
|
| 77 |
+
className="w-full px-3 py-2.5 border border-zinc-200 rounded-lg text-sm focus:outline-none focus:border-zinc-400" />
|
| 78 |
{error && <p className="text-xs text-red-600">{error}</p>}
|
| 79 |
<button type="submit" disabled={loading}
|
| 80 |
className="w-full bg-zinc-900 text-white py-2.5 rounded-lg text-sm font-medium hover:bg-zinc-800 disabled:opacity-40 transition-colors">
|
web/proxy.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
| 1 |
import { createServerClient } from "@supabase/ssr";
|
| 2 |
import { NextResponse, type NextRequest } from "next/server";
|
| 3 |
|
| 4 |
-
export function proxy(request: NextRequest) {
|
| 5 |
let supabaseResponse = NextResponse.next({ request });
|
| 6 |
|
| 7 |
-
// Skip Supabase auth if env vars not set (local dev without Supabase)
|
| 8 |
if (!process.env.NEXT_PUBLIC_SUPABASE_URL || !process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY) {
|
| 9 |
return supabaseResponse;
|
| 10 |
}
|
|
@@ -14,26 +13,39 @@ export function proxy(request: NextRequest) {
|
|
| 14 |
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY,
|
| 15 |
{
|
| 16 |
cookies: {
|
| 17 |
-
getAll() {
|
| 18 |
-
return request.cookies.getAll();
|
| 19 |
-
},
|
| 20 |
setAll(cookiesToSet) {
|
| 21 |
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value));
|
| 22 |
supabaseResponse = NextResponse.next({ request });
|
| 23 |
-
cookiesToSet.forEach(({ name, value, options }) =>
|
| 24 |
-
supabaseResponse.cookies.set(name, value, options)
|
| 25 |
-
);
|
| 26 |
},
|
| 27 |
},
|
| 28 |
}
|
| 29 |
);
|
| 30 |
|
| 31 |
-
//
|
| 32 |
-
supabase.auth.getUser();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
return supabaseResponse;
|
| 35 |
}
|
| 36 |
|
| 37 |
export const config = {
|
| 38 |
-
matcher: ["/dashboard-pages/:path*", "/auth/:path*"],
|
| 39 |
};
|
|
|
|
| 1 |
import { createServerClient } from "@supabase/ssr";
|
| 2 |
import { NextResponse, type NextRequest } from "next/server";
|
| 3 |
|
| 4 |
+
export async function proxy(request: NextRequest) {
|
| 5 |
let supabaseResponse = NextResponse.next({ request });
|
| 6 |
|
|
|
|
| 7 |
if (!process.env.NEXT_PUBLIC_SUPABASE_URL || !process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY) {
|
| 8 |
return supabaseResponse;
|
| 9 |
}
|
|
|
|
| 13 |
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY,
|
| 14 |
{
|
| 15 |
cookies: {
|
| 16 |
+
getAll() { return request.cookies.getAll(); },
|
|
|
|
|
|
|
| 17 |
setAll(cookiesToSet) {
|
| 18 |
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value));
|
| 19 |
supabaseResponse = NextResponse.next({ request });
|
| 20 |
+
cookiesToSet.forEach(({ name, value, options }) => supabaseResponse.cookies.set(name, value, options));
|
|
|
|
|
|
|
| 21 |
},
|
| 22 |
},
|
| 23 |
}
|
| 24 |
);
|
| 25 |
|
| 26 |
+
// MUST await — otherwise auth check is useless
|
| 27 |
+
const { data: { user } } = await supabase.auth.getUser();
|
| 28 |
+
|
| 29 |
+
const pathname = request.nextUrl.pathname;
|
| 30 |
+
const isAuthPage = pathname.startsWith("/auth/") && !pathname.includes("callback");
|
| 31 |
+
const isDashboard = pathname.startsWith("/dashboard-pages") || pathname.startsWith("/admin");
|
| 32 |
+
|
| 33 |
+
// Logged-in user on auth pages → redirect to dashboard
|
| 34 |
+
if (user && isAuthPage) {
|
| 35 |
+
return NextResponse.redirect(new URL("/dashboard-pages/dashboard", request.url));
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// Not logged in on protected pages → redirect to login
|
| 39 |
+
if (!user && isDashboard) {
|
| 40 |
+
const url = request.nextUrl.clone();
|
| 41 |
+
url.pathname = "/auth/login";
|
| 42 |
+
url.searchParams.set("next", pathname);
|
| 43 |
+
return NextResponse.redirect(url);
|
| 44 |
+
}
|
| 45 |
|
| 46 |
return supabaseResponse;
|
| 47 |
}
|
| 48 |
|
| 49 |
export const config = {
|
| 50 |
+
matcher: ["/dashboard-pages/:path*", "/auth/:path*", "/admin/:path*"],
|
| 51 |
};
|