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
-
-
-
- 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
+
+
+
+ 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)$).*)",
+ ],
+};