From 3cc3136762c832c6db13a1f3b7b4889d7b8df743 Mon Sep 17 00:00:00 2001 From: pavel444-byte Date: Sun, 11 Oct 2026 18:53:46 +0500 Subject: [PATCH] Add docs navigation and tighten API request security --- app/docs/page.module.css | 66 ++++++++++++++++ app/docs/page.tsx | 119 +++++++++++++++++++++++++++++ components/Shell.tsx | 1 + components/SiteNav.tsx | 1 + docs/gitbook/README.md | 29 +++++++ docs/gitbook/SUMMARY.md | 15 ++++ docs/gitbook/accounts.md | 34 +++++++++ docs/gitbook/content-reports.md | 27 +++++++ docs/gitbook/contributions.md | 35 +++++++++ docs/gitbook/creating-maps.md | 34 +++++++++ docs/gitbook/desktop.md | 26 +++++++ docs/gitbook/map-sharing.md | 38 +++++++++ docs/gitbook/multiplayer.md | 41 ++++++++++ docs/gitbook/playing.md | 34 +++++++++ docs/gitbook/server-plugin.md | 36 +++++++++ docs/gitbook/staff.md | 30 ++++++++ docs/gitbook/support.md | 41 ++++++++++ docs/gitbook/troubleshooting.md | 49 ++++++++++++ docs/security-review-2026-10-11.md | 22 ++++++ lib/api.ts | 30 ++------ lib/json-body.ts | 21 +++++ lib/multiplayer-api.ts | 3 +- lib/request-security.ts | 23 ++++++ package-lock.json | 12 +-- package.json | 6 ++ shared/locales/ru.json | 1 + shared/player-docs.json | 67 ++++++++++++++++ tests/request-security.test.ts | 103 +++++++++++++++++++++++++ 28 files changed, 915 insertions(+), 29 deletions(-) create mode 100644 app/docs/page.module.css create mode 100644 app/docs/page.tsx create mode 100644 docs/gitbook/README.md create mode 100644 docs/gitbook/SUMMARY.md create mode 100644 docs/gitbook/accounts.md create mode 100644 docs/gitbook/content-reports.md create mode 100644 docs/gitbook/contributions.md create mode 100644 docs/gitbook/creating-maps.md create mode 100644 docs/gitbook/desktop.md create mode 100644 docs/gitbook/map-sharing.md create mode 100644 docs/gitbook/multiplayer.md create mode 100644 docs/gitbook/playing.md create mode 100644 docs/gitbook/server-plugin.md create mode 100644 docs/gitbook/staff.md create mode 100644 docs/gitbook/support.md create mode 100644 docs/gitbook/troubleshooting.md create mode 100644 docs/security-review-2026-10-11.md create mode 100644 lib/json-body.ts create mode 100644 lib/request-security.ts create mode 100644 shared/player-docs.json create mode 100644 tests/request-security.test.ts diff --git a/app/docs/page.module.css b/app/docs/page.module.css new file mode 100644 index 0000000..5bbd6f9 --- /dev/null +++ b/app/docs/page.module.css @@ -0,0 +1,66 @@ +.layout { + display: grid; + grid-template-columns: 240px minmax(0, 1fr); + gap: 48px; +} +.menu { + display: flex; + flex-direction: column; + gap: 12px; +} +.menu a { + line-height: 1.5; +} +.menu a[aria-current="page"] { + color: var(--green); + font-weight: 700; +} +.article { + min-width: 0; + max-width: 780px; +} +.article h1 { + font-size: clamp(30px, 4vw, 48px); +} +.article a { + color: var(--green); + text-decoration: underline; + text-underline-offset: 3px; +} +.article li { + margin: 8px 0; + line-height: 1.7; +} +.article ol { list-style: decimal; padding-left: 24px; } +.article ul { list-style: disc; padding-left: 24px; } +.article blockquote { + margin: 24px 0; + padding: 12px 20px; + border-left: 3px solid var(--green); +} +.article figure { + margin: 24px 0; +} +.article img { + max-width: 100%; + height: auto; + border-radius: 12px; +} +.paging { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 24px; + margin-top: 48px; +} +@media (max-width: 800px) { + .layout { + grid-template-columns: minmax(0, 1fr); + gap: 32px; + } + .menu { + flex-direction: row; + flex-wrap: wrap; + gap: 10px 20px; + } +} diff --git a/app/docs/page.tsx b/app/docs/page.tsx new file mode 100644 index 0000000..fb10f80 --- /dev/null +++ b/app/docs/page.tsx @@ -0,0 +1,119 @@ +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { Header, Footer } from "@/components/Shell"; +import guides from "@/shared/player-docs.json"; +import styles from "./page.module.css"; + +export const metadata = { title: "Player docs" }; + +function inline(text: string) { + return text + .split(/(\*\*[^*]+\*\*|\[[^\]]+\]\(https:\/\/[^)]+\))/g) + .map((part, index) => { + if (part.startsWith("**")) + return {part.slice(2, -2)}; + const link = part.match(/^\[([^\]]+)\]\((https:\/\/[^)]+)\)$/); + return link ? ( + + {link[1]} + + ) : ( + part + ); + }); +} + +function content(markdown: string) { + return markdown.split(/\n\s*\n/).map((block, index) => { + const heading = block.match(/^(#{1,3}) (.+)$/); + if (heading) { + const text = inline(heading[2]); + return heading[1].length === 1 ? ( +

{text}

+ ) : heading[1].length === 2 ? ( +

{text}

+ ) : ( +

{text}

+ ); + } + const image = block.match(/^!\[([^\]]*)\]\((https:\/\/[^)]+)\)$/); + if (image) + return ( +
+ {image[1]} +
+ ); + if (block.startsWith("> ")) + return ( +
+ {inline(block.replace(/^> /gm, ""))} +
+ ); + const lines = block.split("\n"); + if (lines.every((line) => /^[-*] /.test(line))) + return ( + + ); + if (lines.every((line) => /^\d+\. /.test(line))) + return ( +
    + {lines.map((line, i) => ( +
  1. {inline(line.replace(/^\d+\. /, ""))}
  2. + ))} +
+ ); + return

{inline(block)}

; + }); +} + +export default async function Docs({ + searchParams, +}: { + searchParams: Promise<{ page?: string }>; +}) { + const { page = "welcome" } = await searchParams; + const guide = guides.find((item) => item.slug === page); + if (!guide) notFound(); + const index = guides.indexOf(guide); + return ( + <> +
+
+ +
+ {content(guide.markdown)} +
+ {index > 0 && ( + + ← {guides[index - 1].title} + + )} + {index < guides.length - 1 && ( + + {guides[index + 1].title} → + + )} +
+
+
+