gaurv007 commited on
Commit
f36152d
·
verified ·
1 Parent(s): 74baa7b

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 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") ?? "/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
-
14
  if (!error) {
15
  return NextResponse.redirect(`${origin}${next}`);
16
  }
17
  }
18
 
19
- return NextResponse.redirect(`${origin}/auth/login?error=callback_failed`);
 
 
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
- async function handleReset(e: React.FormEvent) {
17
- e.preventDefault();
18
- setLoading(true);
19
- setError("");
 
 
 
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>. Click it to continue.</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">
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">Enter your email and we will send you a reset link.</p>
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 { ArrowLeft, Mail } from "lucide-react";
 
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 = "/dashboard-pages/dashboard"; }
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
- placeholder="Email" className="w-full px-3 py-2.5 border border-zinc-200 rounded-lg text-sm focus:outline-none focus:border-zinc-400" />
80
- <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required
81
- placeholder="Password" className="w-full px-3 py-2.5 border border-zinc-200 rounded-lg text-sm focus:outline-none focus:border-zinc-400" />
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
- password,
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
- 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>
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
- placeholder="Email" className="w-full px-3 py-2.5 border border-zinc-200 rounded-lg text-sm focus:outline-none focus:border-zinc-400" />
73
- <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required minLength={8}
74
- placeholder="Password (8+ characters)" className="w-full px-3 py-2.5 border border-zinc-200 rounded-lg text-sm focus:outline-none focus:border-zinc-400" />
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
- // Refresh session tokens
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
  };