Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@
"noValidPlayers": "No valid players.",
"noValidClubs": "No valid teams.",
"noValidYears": "No valid years.",
"updateFailed": "Update failed."
"updateFailed": "Update failed.",
"payloadTooLarge": "Request body too large."
}
}
3 changes: 2 additions & 1 deletion messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@
"noValidPlayers": "Aucun joueur valide.",
"noValidClubs": "Aucun club valide.",
"noValidYears": "Aucune année valide.",
"updateFailed": "Échec de la mise à jour."
"updateFailed": "Échec de la mise à jour.",
"payloadTooLarge": "Corps de requête trop volumineux."
}
}
19 changes: 19 additions & 0 deletions src/app/(app)/collection/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export default function CollectionLoading() {
return (
<div className="space-y-6 animate-pulse">
<div className="space-y-2">
<div className="h-8 w-40 rounded bg-muted" />
<div className="h-4 w-56 rounded bg-muted" />
</div>
<div className="h-10 rounded-md bg-muted" />
<div className="space-y-2">
{Array.from({ length: 8 }).map((_, i) => (
<div
key={i}
className="h-14 rounded-lg border border-border bg-card"
/>
))}
</div>
</div>
);
}
19 changes: 19 additions & 0 deletions src/app/(app)/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export default function AppLoading() {
return (
<div className="min-w-0 space-y-6 sm:space-y-8 animate-pulse">
<div className="min-w-0 space-y-2">
<div className="h-8 w-48 rounded bg-muted" />
<div className="h-4 w-64 rounded bg-muted" />
</div>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6">
{Array.from({ length: 6 }).map((_, i) => (
<div
key={i}
className="h-24 rounded-lg border border-border bg-card"
/>
))}
</div>
<div className="h-64 rounded-lg border border-border bg-card" />
</div>
);
}
18 changes: 18 additions & 0 deletions src/app/(app)/player/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export default function PlayersLoading() {
return (
<div className="space-y-6 animate-pulse">
<div className="space-y-2">
<div className="h-8 w-32 rounded bg-muted" />
<div className="h-4 w-48 rounded bg-muted" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Array.from({ length: 6 }).map((_, i) => (
<div
key={i}
className="h-28 rounded-lg border border-border bg-card"
/>
))}
</div>
</div>
);
}
4 changes: 4 additions & 0 deletions src/app/api/auth/bootstrap/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ import { hashPassword } from "@/lib/password";
import { bootstrapFirstUser, type UserRecord } from "@/lib/users-store";
import { authMisconfiguredResponse } from "@/lib/auth-config";
import { checkRateLimit, getClientIp } from "@/lib/rate-limit";
import { rejectCrossSiteMutation } from "@/lib/request-guard";

export async function POST(request: NextRequest) {
const crossSite = rejectCrossSiteMutation(request);
if (crossSite) return crossSite;

const misconfigured = authMisconfiguredResponse(request);
if (misconfigured) return misconfigured;

Expand Down
11 changes: 9 additions & 2 deletions src/app/api/auth/login/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
SESSION_MAX_AGE_SEC,
} from "@/lib/auth-session";
import { loginSchema } from "@/lib/auth-validation";
import { verifyPassword } from "@/lib/password";
import { dummyVerify, verifyPassword } from "@/lib/password";
import {
checkRateLimit,
getClientIp,
Expand All @@ -16,8 +16,12 @@ import {
} from "@/lib/rate-limit";
import { findUserByUsername } from "@/lib/users-store";
import { authMisconfiguredResponse } from "@/lib/auth-config";
import { rejectCrossSiteMutation } from "@/lib/request-guard";

export async function POST(request: NextRequest) {
const crossSite = rejectCrossSiteMutation(request);
if (crossSite) return crossSite;

const misconfigured = authMisconfiguredResponse(request);
if (misconfigured) return misconfigured;

Expand Down Expand Up @@ -66,7 +70,10 @@ export async function POST(request: NextRequest) {
}

const user = findUserByUsername(username);
if (!user || !verifyPassword(password, user.passwordHash)) {
const passwordValid = user
? verifyPassword(password, user.passwordHash)
: (dummyVerify(password), false);
if (!user || !passwordValid) {
const failed = checkRateLimit(failureKey, {
limit: 5,
windowMs: 15 * 60 * 1000,
Expand Down
20 changes: 17 additions & 3 deletions src/app/api/cards/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import {
formatZodError,
} from "@/lib/card-schema";
import { getRequestTranslator } from "@/i18n/request";
import { rejectCrossSiteMutation } from "@/lib/request-guard";
import {
rejectCrossSiteMutation,
rejectOversizedBody,
} from "@/lib/request-guard";

export const dynamic = "force-dynamic";

Expand All @@ -26,6 +29,8 @@ export async function POST(request: NextRequest) {
if (gate instanceof NextResponse) return gate;
const crossSite = rejectCrossSiteMutation(request);
if (crossSite) return crossSite;
const oversized = rejectOversizedBody(request);
if (oversized) return oversized;
const t = getRequestTranslator(request);

let body: unknown;
Expand Down Expand Up @@ -70,6 +75,8 @@ export async function PUT(request: NextRequest) {
if (gate instanceof NextResponse) return gate;
const crossSite = rejectCrossSiteMutation(request);
if (crossSite) return crossSite;
const oversized = rejectOversizedBody(request);
if (oversized) return oversized;
const t = getRequestTranslator(request);

let body: unknown;
Expand Down Expand Up @@ -105,8 +112,15 @@ export async function DELETE(request: NextRequest) {
const crossSite = rejectCrossSiteMutation(request);
if (crossSite) return crossSite;
const t = getRequestTranslator(request);
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");

let id: string | null = null;
try {
const body = (await request.json()) as { id?: unknown };
id = typeof body.id === "string" ? body.id.trim() : null;
} catch {
const { searchParams } = new URL(request.url);
id = searchParams.get("id");
}

if (!id) {
return NextResponse.json(
Expand Down
5 changes: 4 additions & 1 deletion src/app/api/health/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ import { getDataHealth } from "@/lib/data";

export async function GET() {
const health = getDataHealth();
return NextResponse.json({ ok: health.ok }, { status: health.ok ? 200 : 503 });
return NextResponse.json(
{ status: health.ok ? "healthy" : "degraded" },
{ status: health.ok ? 200 : 503 }
);
}
7 changes: 6 additions & 1 deletion src/app/api/references/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import {
} from "@/lib/reference-schema";
import { getRequestTranslator } from "@/i18n/request";
import type { Translator } from "@/i18n/translator";
import { rejectCrossSiteMutation } from "@/lib/request-guard";
import {
rejectCrossSiteMutation,
rejectOversizedBody,
} from "@/lib/request-guard";

export async function GET(request: NextRequest) {
const gate = requireAuth(request);
Expand Down Expand Up @@ -143,6 +146,8 @@ export async function PATCH(request: NextRequest) {
if (gate instanceof NextResponse) return gate;
const crossSite = rejectCrossSiteMutation(request);
if (crossSite) return crossSite;
const oversized = rejectOversizedBody(request);
if (oversized) return oversized;
const t = getRequestTranslator(request);

let body: unknown;
Expand Down
8 changes: 8 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,17 @@
}
body {
@apply bg-background text-foreground;
-webkit-tap-highlight-color: transparent;
-webkit-text-size-adjust: 100%;
}
html {
@apply font-sans scroll-smooth;
touch-action: manipulation;
}
input,
select,
textarea {
font-size: 16px;
}
}

Expand Down
10 changes: 9 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import type { Metadata } from "next";
import type { Metadata, Viewport } from "next";
import { connection } from "next/server";
import { LocaleProvider } from "@/i18n/client";
import { getTranslations } from "@/i18n/server";
import "./globals.css";

export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
maximumScale: 1,
viewportFit: "cover",
themeColor: "#0a0a0a",
};

export async function generateMetadata(): Promise<Metadata> {
const { t } = await getTranslations();
return {
Expand Down
Loading