diff --git a/middleware.ts b/middleware.ts deleted file mode 100644 index bad4641..0000000 --- a/middleware.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { createServerClient } from "@supabase/ssr"; -import { NextResponse, type NextRequest } from "next/server"; - -export async function middleware(request: NextRequest) { - let supabaseResponse = NextResponse.next({ request }); - - const supabase = createServerClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, - { - cookies: { - getAll() { - return request.cookies.getAll(); - }, - setAll(cookiesToSet) { - cookiesToSet.forEach(({ name, value }) => - request.cookies.set(name, value), - ); - supabaseResponse = NextResponse.next({ request }); - cookiesToSet.forEach(({ name, value, options }) => - supabaseResponse.cookies.set(name, value, options), - ); - }, - }, - }, - ); - - // Refresh session — must not run any code between createServerClient and getUser - const { - data: { user }, - } = await supabase.auth.getUser(); - - const { pathname } = request.nextUrl; - - // Public paths that never require auth - const isPublicPath = - pathname.startsWith("/login") || pathname.startsWith("/auth"); - - if (isPublicPath) { - // Already logged in — send them to the map instead of showing login again - if (user && pathname === "/login") { - return NextResponse.redirect(new URL("/map", request.url)); - } - return supabaseResponse; - } - - // Everything else requires a session - if (!user) { - const loginUrl = new URL("/login", request.url); - return NextResponse.redirect(loginUrl); - } - - return supabaseResponse; -} - -export const config = { - matcher: [ - /* - * Match all paths except: - * - _next/static (static files) - * - _next/image (image optimisation) - * - favicon, icons, brand, logo assets, manifest - */ - "/((?!_next/static|_next/image|favicon|icons|brand|logo|manifest).*)", - ], -}; diff --git a/src/app/auth/callback/route.ts b/src/app/auth/callback/route.ts index 7c4c5f1..303bedf 100644 --- a/src/app/auth/callback/route.ts +++ b/src/app/auth/callback/route.ts @@ -5,7 +5,7 @@ import { NextResponse } from "next/server"; export async function GET(request: Request) { const { searchParams, origin } = new URL(request.url); const code = searchParams.get("code"); - const next = searchParams.get("next") ?? "/map"; + const next = searchParams.get("next") ?? "/"; if (code) { const cookieStore = await cookies(); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index eec8985..136f94d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -12,6 +12,7 @@ import { Analytics } from "@/components/analytics"; import { cn } from "@/lib/utils"; export const metadata: Metadata = { + metadataBase: new URL(site.url), title: { default: `${site.name}: student places and benefits`, template: `%s | ${site.name}`, diff --git a/src/app/login/login-form.tsx b/src/app/login/login-form.tsx new file mode 100644 index 0000000..0bce45c --- /dev/null +++ b/src/app/login/login-form.tsx @@ -0,0 +1,195 @@ +"use client"; + +import * as React from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { toast } from "sonner"; +import { Loader2 } from "lucide-react"; + +import { createClient } from "@/lib/supabase/client"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +export function LoginForm() { + const router = useRouter(); + const searchParams = useSearchParams(); + const next = searchParams.get("next") ?? "/"; + + const supabase = createClient(); + + const [mode, setMode] = React.useState<"signin" | "signup">("signin"); + const [email, setEmail] = React.useState(""); + const [password, setPassword] = React.useState(""); + const [loading, setLoading] = React.useState(false); + + async function handleEmailAuth(e: React.FormEvent) { + e.preventDefault(); + setLoading(true); + + if (mode === "signup") { + const { error } = await supabase.auth.signUp({ email, password }); + if (error) { + toast.error(error.message); + } else { + toast.success("Check your email to confirm your account."); + } + } else { + const { error } = await supabase.auth.signInWithPassword({ + email, + password, + }); + if (error) { + toast.error(error.message); + } else { + router.push(next); + router.refresh(); + } + } + + setLoading(false); + } + + async function handleGoogle() { + const { error } = await supabase.auth.signInWithOAuth({ + provider: "google", + options: { + redirectTo: `${window.location.origin}/auth/callback?next=${encodeURIComponent(next)}`, + }, + }); + if (error) toast.error(error.message); + } + + return ( +
+
+ {/* Logo */} +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + StudyMap + {/* eslint-disable-next-line @next/next/no-img-element */} + StudyMap +

+ {mode === "signin" ? "Sign in to continue" : "Create your account"} +

+
+ + {/* Card */} +
+ {/* Google */} + + +
+
+ +
+
+ + or + +
+
+ + {/* Email form */} +
+
+ + setEmail(e.target.value)} + disabled={loading} + /> +
+
+ + setPassword(e.target.value)} + disabled={loading} + /> +
+ + +
+
+ + {/* Toggle mode */} +

+ {mode === "signin" ? ( + <> + No account?{" "} + + + ) : ( + <> + Already have an account?{" "} + + + )} +

+
+
+ ); +} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 7533c81..e399b29 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,196 +1,10 @@ -"use client"; - -import * as React from "react"; -import { useRouter } from "next/navigation"; -import { toast } from "sonner"; -import { Loader2 } from "lucide-react"; - -import { createClient } from "@/lib/supabase/client"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; +import { Suspense } from "react"; +import { LoginForm } from "./login-form"; export default function LoginPage() { - const router = useRouter(); - const supabase = createClient(); - - const [mode, setMode] = React.useState<"signin" | "signup">("signin"); - const [email, setEmail] = React.useState(""); - const [password, setPassword] = React.useState(""); - const [loading, setLoading] = React.useState(false); - - async function handleEmailAuth(e: React.FormEvent) { - e.preventDefault(); - setLoading(true); - - if (mode === "signup") { - const { error } = await supabase.auth.signUp({ email, password }); - if (error) { - toast.error(error.message); - } else { - toast.success("Check your email to confirm your account."); - } - } else { - const { error } = await supabase.auth.signInWithPassword({ - email, - password, - }); - if (error) { - toast.error(error.message); - } else { - router.push("/map"); - router.refresh(); - } - } - - setLoading(false); - } - - async function handleGoogle() { - const { error } = await supabase.auth.signInWithOAuth({ - provider: "google", - options: { - redirectTo: `${window.location.origin}/auth/callback`, - }, - }); - if (error) toast.error(error.message); - } - return ( -
-
- {/* Logo */} -
- {/* eslint-disable-next-line @next/next/no-img-element */} - StudyMap - {/* eslint-disable-next-line @next/next/no-img-element */} - StudyMap -

- {mode === "signin" - ? "Sign in to continue" - : "Create your account"} -

-
- - {/* Card */} -
- {/* Google */} - - -
-
- -
-
- - or - -
-
- - {/* Email form */} -
-
- - setEmail(e.target.value)} - disabled={loading} - /> -
-
- - setPassword(e.target.value)} - disabled={loading} - /> -
- - -
-
- - {/* Toggle mode */} -

- {mode === "signin" ? ( - <> - No account?{" "} - - - ) : ( - <> - Already have an account?{" "} - - - )} -

-
-
+ + + ); } diff --git a/src/components/layout/navbar.tsx b/src/components/layout/navbar.tsx index d9a726e..a74d410 100644 --- a/src/components/layout/navbar.tsx +++ b/src/components/layout/navbar.tsx @@ -3,7 +3,7 @@ import * as React from "react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; -import { Menu, LogOut } from "lucide-react"; +import { Menu, LogOut, LogIn } from "lucide-react"; import { cn } from "@/lib/utils"; import { navLinks, site } from "@/lib/site"; @@ -30,7 +30,6 @@ export function Navbar() { async function handleSignOut() { const supabase = createClient(); await supabase.auth.signOut(); - router.push("/login"); router.refresh(); } @@ -64,7 +63,7 @@ export function Navbar() {
- {loggedIn && ( + {loggedIn ? ( + ) : ( + )} @@ -98,7 +109,7 @@ export function Navbar() { {link.label} ))} - {loggedIn && ( + {loggedIn ? ( + ) : ( + setOpen(false)} + className="flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" + > + + Sign in + )} diff --git a/src/lib/site.ts b/src/lib/site.ts index 9e81aa1..399cd01 100644 --- a/src/lib/site.ts +++ b/src/lib/site.ts @@ -3,6 +3,7 @@ export const site = { tagline: "Find student places and perks across Mumbai, Thane, and Navi Mumbai.", description: "A crowdsourced map of student-important places (exam centres, libraries, book shops, and more) for the Mumbai Metropolitan Region.", + url: "https://studymapp.vercel.app", repo: "https://github.com/anaydhawan/studymap", } as const; diff --git a/src/proxy.ts b/src/proxy.ts new file mode 100644 index 0000000..363eecd --- /dev/null +++ b/src/proxy.ts @@ -0,0 +1,39 @@ +import { createServerClient } from "@supabase/ssr"; +import { NextResponse, type NextRequest } from "next/server"; + +export async function proxy(request: NextRequest) { + let proxyResponse = NextResponse.next({ request }); + + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return request.cookies.getAll(); + }, + setAll(cookiesToSet) { + cookiesToSet.forEach(({ name, value }) => + request.cookies.set(name, value), + ); + proxyResponse = NextResponse.next({ request }); + cookiesToSet.forEach(({ name, value, options }) => + proxyResponse.cookies.set(name, value, options), + ); + }, + }, + }, + ); + + // Refresh the session so it doesn't expire mid-visit. + // No auth enforcement — the site is fully public. + await supabase.auth.getUser(); + + return proxyResponse; +} + +export const config = { + matcher: [ + "/((?!_next/static|_next/image|favicon\\.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico)$).*)", + ], +};