login screen serving correctly
This commit is contained in:
+5
-5
@@ -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:
|
||||
|
||||
+4
-1
@@ -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;
|
||||
|
||||
+2
-2
@@ -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"
|
||||
},
|
||||
|
||||
+5
-3
@@ -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);
|
||||
}
|
||||
|
||||
+1
-15
@@ -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 (
|
||||
<html
|
||||
lang="en"
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
>
|
||||
<html lang="en" className="h-full antialiased">
|
||||
<body className="min-h-full flex flex-col">{children}</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
+10
-64
@@ -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 (
|
||||
<div className="flex min-h-screen items-center justify-center bg-gray-50 p-4">
|
||||
<div className="w-full max-w-md space-y-8 rounded-xl bg-white p-8 shadow-sm ring-1 ring-gray-200">
|
||||
<div className="text-center">
|
||||
<h2 className="text-3xl font-bold tracking-tight text-gray-900">Sign in to Postcard</h2>
|
||||
</div>
|
||||
<form action={formAction} className="mt-8 space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="username" className="sr-only">Username</label>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
required
|
||||
className="relative block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
placeholder="Username"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password" className="sr-only">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
className="relative block w-full rounded-md border-0 py-1.5 px-3 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
placeholder="Password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{state?.error && (
|
||||
<div className="text-sm text-red-500 text-center bg-red-50 p-2 rounded-md ring-1 ring-red-200">
|
||||
{state.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="group relative flex w-full justify-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 disabled:opacity-70 disabled:cursor-not-allowed transition-all"
|
||||
>
|
||||
{isPending ? <Loader2 className="animate-spin h-5 w-5" /> : "Sign in"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="text-center text-sm">
|
||||
<span className="text-gray-500">Don't have an account? </span>
|
||||
<Link href="/register" className="font-semibold text-blue-600 hover:text-blue-500">
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <LoginForm />;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex min-h-screen items-center justify-center bg-gray-50 p-4">
|
||||
<div className="w-full max-w-md space-y-8 rounded-xl bg-white p-8 shadow-sm ring-1 ring-gray-200">
|
||||
<div className="text-center">
|
||||
<h2 className="text-3xl font-bold tracking-tight text-gray-900">Sign in to Postcard</h2>
|
||||
</div>
|
||||
<form action={formAction} className="mt-8 space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="username" className="sr-only">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
required
|
||||
className="relative block w-full rounded-md border-0 px-3 py-1.5 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
placeholder="Username"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password" className="sr-only">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
className="relative block w-full rounded-md border-0 px-3 py-1.5 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
placeholder="Password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{state?.error && (
|
||||
<div className="rounded-md bg-red-50 p-2 text-center text-sm text-red-500 ring-1 ring-red-200">
|
||||
{state.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="group relative flex w-full justify-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white transition-all hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 disabled:cursor-not-allowed disabled:opacity-70"
|
||||
>
|
||||
{isPending ? <Loader2 className="h-5 w-5 animate-spin" /> : "Sign in"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="text-center text-sm">
|
||||
<span className="text-gray-500">Don't have an account? </span>
|
||||
<Link href="/register" className="font-semibold text-blue-600 hover:text-blue-500">
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+19
-11
@@ -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("/");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)$).*)',
|
||||
],
|
||||
}
|
||||
@@ -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)$).*)",
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user