From a98960c0be8d9345d480822ac71e1cb706db7db0 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Mon, 23 Mar 2026 17:12:39 -0500 Subject: [PATCH] login screen serving correctly --- docker-compose.yml | 10 ++--- next.config.ts | 5 ++- package.json | 4 +- src/app/globals.css | 8 ++-- src/app/layout.tsx | 16 +------ src/app/page.tsx | 74 +++++-------------------------- src/components/auth/LoginForm.tsx | 72 ++++++++++++++++++++++++++++++ src/lib/auth.ts | 30 ++++++++----- src/middleware.ts | 28 ------------ src/proxy.ts | 25 +++++++++++ 10 files changed, 143 insertions(+), 129 deletions(-) create mode 100644 src/components/auth/LoginForm.tsx delete mode 100644 src/middleware.ts create mode 100644 src/proxy.ts diff --git a/docker-compose.yml b/docker-compose.yml index 25763ff..9f0062f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,13 +3,13 @@ services: build: context: . dockerfile: Dockerfile - container_name: postcard-web + container_name: ${POSTCARD_WEB_CONTAINER_NAME:-postcard-web} ports: - - "3000:3000" + - "${POSTCARD_WEB_HOST_PORT:-3000}:3000" environment: - - NODE_ENV=production - - PORT=3000 - - HOSTNAME=0.0.0.0 + NODE_ENV: production + PORT: 3000 + HOSTNAME: 0.0.0.0 restart: unless-stopped # Optional: Podman specific security options # security_opt: diff --git a/next.config.ts b/next.config.ts index ad52782..8400921 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,8 +1,11 @@ +import path from "node:path"; import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "standalone", - /* config options here */ + turbopack: { + root: path.join(__dirname), + }, }; export default nextConfig; diff --git a/package.json b/package.json index 68590ad..bf36ec4 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,8 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", - "build": "next build", + "dev": "next dev --turbopack", + "build": "next build --turbopack", "start": "next start", "lint": "eslint" }, diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..bbcfe52 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -3,13 +3,15 @@ :root { --background: #ffffff; --foreground: #171717; + --font-sans: "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif; + --font-mono: "SFMono-Regular", "Consolas", "Liberation Mono", monospace; } @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); + --font-sans: var(--font-sans); + --font-mono: var(--font-mono); } @media (prefers-color-scheme: dark) { @@ -22,5 +24,5 @@ body { background: var(--background); color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + font-family: var(--font-sans); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 976eb90..32700d5 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,17 +1,6 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); - export const metadata: Metadata = { title: "Create Next App", description: "Generated by create next app", @@ -23,10 +12,7 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + {children} ); diff --git a/src/app/page.tsx b/src/app/page.tsx index 17cdd9d..0ff9b91 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,68 +1,14 @@ -"use client"; +import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import { LoginForm } from "@/components/auth/LoginForm"; -import { useActionState } from "react"; -import { login } from "@/lib/auth"; -import Link from "next/link"; -import { Loader2 } from "lucide-react"; +export default async function HomePage() { + const cookieStore = await cookies(); + const authToken = cookieStore.get("auth_token"); -export default function LoginPage() { - const [state, formAction, isPending] = useActionState(login, null); + if (authToken) { + redirect("/c"); + } - return ( -
-
-
-

Sign in to Postcard

-
-
-
-
- - -
-
- - -
-
- - {state?.error && ( -
- {state.error} -
- )} - -
- -
-
-
- Don't have an account? - - Sign up - -
-
-
- ); + return ; } diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx new file mode 100644 index 0000000..b3afe00 --- /dev/null +++ b/src/components/auth/LoginForm.tsx @@ -0,0 +1,72 @@ +"use client"; + +import Link from "next/link"; +import { useActionState } from "react"; +import { Loader2 } from "lucide-react"; +import { login } from "@/lib/auth"; + +export function LoginForm() { + const [state, formAction, isPending] = useActionState(login, null); + + return ( +
+
+
+

Sign in to Postcard

+
+
+
+
+ + +
+
+ + +
+
+ + {state?.error && ( +
+ {state.error} +
+ )} + +
+ +
+
+
+ Don't have an account? + + Sign up + +
+
+
+ ); +} diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 65a20a8..08d54fc 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -5,7 +5,14 @@ import { redirect } from "next/navigation"; const API_URL = process.env.API_URL || "http://localhost:8080"; -export async function login(prevState: any, formData: FormData) { +type AuthActionState = { + error?: string; +} | null; + +const isSecureCookie = process.env.NODE_ENV !== "development"; + +export async function login(prevState: AuthActionState, formData: FormData) { + void prevState; const username = formData.get("username") as string; const password = formData.get("password") as string; @@ -28,7 +35,7 @@ export async function login(prevState: any, formData: FormData) { const cookieStore = await cookies(); cookieStore.set("auth_token", mockJwt, { httpOnly: true, - secure: process.env.NODE_ENV === "production", + secure: isSecureCookie, sameSite: "lax", path: "/", maxAge: 60 * 60 * 24 * 7, // 1 week @@ -47,14 +54,14 @@ export async function login(prevState: any, formData: FormData) { const cookieStore = await cookies(); cookieStore.set("auth_token", token, { httpOnly: true, - secure: process.env.NODE_ENV === "production", + secure: isSecureCookie, sameSite: "lax", path: "/", maxAge: 60 * 60 * 24 * 7, // 1 week }); } - } catch (error) { + } catch { // Mock fallback if connection fails completely (e.g. backend down) if (process.env.NODE_ENV === "development") { console.warn("Mocking login because backend connection failed."); @@ -62,7 +69,7 @@ export async function login(prevState: any, formData: FormData) { const cookieStore = await cookies(); cookieStore.set("auth_token", mockJwt, { httpOnly: true, - secure: process.env.NODE_ENV === "production", + secure: isSecureCookie, sameSite: "lax", path: "/", maxAge: 60 * 60 * 24 * 7, // 1 week @@ -75,7 +82,8 @@ export async function login(prevState: any, formData: FormData) { redirect("/c"); } -export async function register(prevState: any, formData: FormData) { +export async function register(prevState: AuthActionState, formData: FormData) { + void prevState; const username = formData.get("username") as string; const password = formData.get("password") as string; const displayName = formData.get("displayName") as string; @@ -99,7 +107,7 @@ export async function register(prevState: any, formData: FormData) { const cookieStore = await cookies(); cookieStore.set("auth_token", mockJwt, { httpOnly: true, - secure: process.env.NODE_ENV === "production", + secure: isSecureCookie, sameSite: "lax", path: "/", maxAge: 60 * 60 * 24 * 7, // 1 week @@ -118,14 +126,14 @@ export async function register(prevState: any, formData: FormData) { const cookieStore = await cookies(); cookieStore.set("auth_token", token, { httpOnly: true, - secure: process.env.NODE_ENV === "production", + secure: isSecureCookie, sameSite: "lax", path: "/", maxAge: 60 * 60 * 24 * 7, // 1 week }); } - } catch (error) { + } catch { // Mock fallback if (process.env.NODE_ENV === "development") { console.warn("Mocking register because backend connection failed."); @@ -133,7 +141,7 @@ export async function register(prevState: any, formData: FormData) { const cookieStore = await cookies(); cookieStore.set("auth_token", mockJwt, { httpOnly: true, - secure: process.env.NODE_ENV === "production", + secure: isSecureCookie, sameSite: "lax", path: "/", maxAge: 60 * 60 * 24 * 7, // 1 week @@ -150,4 +158,4 @@ export async function logout() { const cookieStore = await cookies(); cookieStore.delete("auth_token"); redirect("/"); -} \ No newline at end of file +} diff --git a/src/middleware.ts b/src/middleware.ts deleted file mode 100644 index b6bf63e..0000000 --- a/src/middleware.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NextResponse } from 'next/server' -import type { NextRequest } from 'next/server' - -export function middleware(request: NextRequest) { - const authToken = request.cookies.get('auth_token') - const { pathname } = request.nextUrl - - const isAuthRoute = pathname === '/' || pathname === '/register' - - // Redirect unauthenticated users to / - if (!authToken && !isAuthRoute) { - return NextResponse.redirect(new URL('/', request.url)) - } - - // Redirect authenticated users away from auth routes - if (authToken && isAuthRoute) { - return NextResponse.redirect(new URL('/c', request.url)) - } - - return NextResponse.next() -} - -// Match all routes except api, static files, images, etc. -export const config = { - matcher: [ - '/((?!api|_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', - ], -} \ No newline at end of file diff --git a/src/proxy.ts b/src/proxy.ts new file mode 100644 index 0000000..93f2a03 --- /dev/null +++ b/src/proxy.ts @@ -0,0 +1,25 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +export function proxy(request: NextRequest) { + const authToken = request.cookies.get("auth_token"); + const { pathname } = request.nextUrl; + + const isAuthRoute = pathname === "/" || pathname === "/register"; + + if (!authToken && !isAuthRoute) { + return NextResponse.redirect(new URL("/", request.url)); + } + + if (authToken && isAuthRoute) { + return NextResponse.redirect(new URL("/c", request.url)); + } + + return NextResponse.next(); +} + +export const config = { + matcher: [ + "/((?!api|_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)", + ], +};