diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818..9edff1c 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/src/app/(public)/login/page.tsx b/src/app/(public)/login/page.tsx index 0b7670f..bdfb771 100644 --- a/src/app/(public)/login/page.tsx +++ b/src/app/(public)/login/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, Suspense } from "react"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; import { signIn } from "next-auth/react"; @@ -13,6 +13,14 @@ import { AlertCircle } from "lucide-react"; import { toast } from "sonner"; export default function LoginPage() { + return ( + + + + ); +} + +function LoginPageContent() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -36,15 +44,11 @@ export default function LoginPage() { setError(null); setIsLoading(true); try { - const result = await signIn("credentials", { + await signIn("credentials", { email, password, redirectTo: "/app", }); - // If signIn returns with ok: false, it means auth failed - if (result && !result.ok) { - setError("Invalid email or password"); - } } catch { setError("An error occurred. Please try again."); } finally { diff --git a/src/app/(public)/navbar.tsx b/src/app/(public)/navbar.tsx index f2f654c..b2b5676 100644 --- a/src/app/(public)/navbar.tsx +++ b/src/app/(public)/navbar.tsx @@ -2,8 +2,9 @@ import Link from "next/link"; import { useSession } from "next-auth/react"; -import { Button } from "@/components/ui/button"; +import { buttonVariants } from "@/components/ui/button"; import { ChartColumn } from "lucide-react"; +import { cn } from "@/lib/utils"; export function Navbar() { const { data: session } = useSession(); @@ -19,17 +20,26 @@ export function Navbar() {
{session ? ( - + + Go to App + ) : ( <> - - + + Log in + + + Get Started + )}
diff --git a/src/app/(public)/page.tsx b/src/app/(public)/page.tsx index c347641..a8d8d9d 100644 --- a/src/app/(public)/page.tsx +++ b/src/app/(public)/page.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; -import { Button } from "@/components/ui/button"; +import { buttonVariants } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; import { Navbar } from "./navbar"; import { ChartColumn, @@ -36,19 +37,18 @@ export default function LandingPage() { keyboard-driven workspace.

- - + Start Annotating + + + Try Demo +
@@ -162,11 +162,12 @@ export default function LandingPage() {

No credit card required. Start annotating charts in seconds.

- + + Create Free Account + {/* Footer */} diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts index 2e06971..7c62e2d 100644 --- a/src/app/api/auth/[...nextauth]/route.ts +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -1 +1,2 @@ -export { GET, POST } from "@/auth"; +import { handlers } from "@/auth"; +export const { GET, POST } = handlers; diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..f62edea --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 1edbe95..51cfa0f 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,6 +1,10 @@ import { auth } from "@/auth"; -export async function getAuthUser() { +export type AuthUser = { id: string; name?: string | null; email?: string | null; image?: string | null }; + +export async function getAuthUser(): Promise { const session = await auth(); - return session?.user ?? null; + const user = session?.user; + if (!user?.id) return null; + return user as AuthUser; } diff --git a/src/middleware.ts b/src/middleware.ts deleted file mode 100644 index 1509671..0000000 --- a/src/middleware.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { auth } from "@/auth"; -import { NextResponse } from "next/server"; - -export const middleware = auth((req) => { - const { pathname } = req.nextUrl; - const isAuthenticated = !!req.auth; - - // Protect /api/* except /api/auth/* and /api/health - if (pathname.startsWith("/api/")) { - const isAuthRoute = pathname.startsWith("/api/auth/"); - const isHealthRoute = pathname === "/api/health"; - - if (!isAuthRoute && !isHealthRoute && !isAuthenticated) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - return NextResponse.next(); - } - - // Redirect authenticated users away from /login and /register - if (isAuthenticated && (pathname === "/login" || pathname === "/register")) { - return NextResponse.redirect(new URL("/app", req.nextUrl.origin)); - } - - // Protect /app/* routes — redirect unauthenticated users to /login - if (pathname.startsWith("/app") && !isAuthenticated) { - return NextResponse.redirect(new URL("/login", req.nextUrl.origin)); - } - - return NextResponse.next(); -}); - -export const config = { - matcher: [ - /* - * Match all request paths except: - * - _next/static (static files) - * - _next/image (image optimisation) - * - favicon.ico / favicon.png / favicon.svg - * - public assets (top-level files that are not pages) - */ - "/((?!_next/static|_next/image|favicon\\.ico|favicon\\.png|favicon\\.svg).*)", - ], -}; diff --git a/src/proxy.ts b/src/proxy.ts index f97c340..e30ab0f 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -1,42 +1,53 @@ +import { auth } from "@/auth"; import { NextResponse } from "next/server"; -import type { NextRequest } from "next/server"; -export function proxy(request: NextRequest) { - const { pathname } = request.nextUrl; +export const proxy = auth((req) => { + const { pathname } = req.nextUrl; + const isAuthenticated = !!req.auth; + + // Protect /api/* except /api/auth/* and /api/health + if (pathname.startsWith("/api/")) { + const isAuthRoute = pathname.startsWith("/api/auth/"); + const isHealthRoute = pathname === "/api/health"; + + if (!isAuthRoute && !isHealthRoute && !isAuthenticated) { + // Allow external clients with a valid API key + const apiKey = process.env.API_KEY; + if (apiKey) { + const requestApiKey = req.headers.get("X-API-Key"); + if (requestApiKey === apiKey) { + return NextResponse.next(); + } + } + + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } - // Skip auth check for the health endpoint - if (pathname === "/api/health") { return NextResponse.next(); } - const apiKey = process.env.API_KEY; - - // If API_KEY is not configured, skip auth check (fail-open for development) - if (!apiKey) { - console.warn( - "Warning: API_KEY environment variable is not set. API authentication is disabled." - ); - return NextResponse.next(); + // Redirect authenticated users away from /login and /register + if (isAuthenticated && (pathname === "/login" || pathname === "/register")) { + return NextResponse.redirect(new URL("/app", req.nextUrl.origin)); } - const requestApiKey = request.headers.get("X-API-Key"); - - // Allow same-origin browser requests (UI -> /api/*) without exposing API_KEY to client JS. - // Keep API key auth for non-browser/external clients. - const fetchSite = request.headers.get("sec-fetch-site"); - const isSameOriginBrowserRequest = fetchSite === "same-origin"; - - if (isSameOriginBrowserRequest) { - return NextResponse.next(); - } - - if (!requestApiKey || requestApiKey !== apiKey) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + // Protect /app/* routes — redirect unauthenticated users to /login + if (pathname.startsWith("/app") && !isAuthenticated) { + return NextResponse.redirect(new URL("/login", req.nextUrl.origin)); } return NextResponse.next(); -} +}); export const config = { - matcher: ["/api/:path*"], + matcher: [ + /* + * Match all request paths except: + * - _next/static (static files) + * - _next/image (image optimisation) + * - favicon.ico / favicon.png / favicon.svg + * - public assets (top-level files that are not pages) + */ + "/((?!_next/static|_next/image|favicon\\.ico|favicon\\.png|favicon\\.svg).*)", + ], };