From 018be5049e9ede7876492462934d1d160bfe4652 Mon Sep 17 00:00:00 2001 From: pavel444-byte Date: Mon, 12 Oct 2026 19:59:37 +0500 Subject: [PATCH] Add developer API, Prometheus metrics, and community links --- .env.example | 2 + app/api/dev/keys/route.ts | 8 + app/api/dev/openapi.json/route.ts | 10 + app/api/dev/route.ts | 3 + app/api/dev/v1/[...path]/route.ts | 17 + app/api/metrics/route.ts | 16 + app/appearance.css | 4 +- app/dev/api/page.module.css | 225 ++++++++ app/dev/api/page.tsx | 23 + app/docs/page.tsx | 33 +- app/globals.css | 7 + app/moderation/page.tsx | 2 +- app/polish.css | 61 ++- components/Dashboard.tsx | 18 +- components/DeveloperDocumentation.tsx | 219 ++++++++ components/DeveloperPanel.tsx | 496 ++++++++++++++++++ components/Moderation.tsx | 2 + components/PrometheusIntegration.tsx | 271 ++++++++++ components/Shell.tsx | 16 +- components/SiteNav.tsx | 7 +- components/StartGame.tsx | 7 +- components/Support.tsx | 35 +- docs/bedrock-feasibility.md | 66 +++ docs/developer-api.md | 69 +++ docs/forum.md | 58 ++ docs/prometheus.md | 77 +++ docs/vercel-deployment.md | 15 + lib/api.ts | 5 + lib/developer-api.ts | 148 ++++++ lib/developer-key-management.ts | 137 +++++ lib/developer-response.ts | 192 +++++++ lib/forum.ts | 16 + lib/prometheus-api.ts | 38 ++ lib/prometheus-response.ts | 48 ++ proxy.ts | 6 + scripts/test-developer-keys-sql.mjs | 127 +++++ scripts/test-prometheus-sql.mjs | 196 +++++++ shared/database.ts | 29 + shared/developer-api.ts | 71 +++ shared/developer-docs.ts | 86 +++ shared/developer-openapi.ts | 208 ++++++++ shared/forum.json | 3 + shared/grafana-dashboard.ts | 236 +++++++++ shared/locales/ru.json | 37 +- shared/prometheus.ts | 152 ++++++ supabase/.temp/cli-latest | 1 - .../20261012085217_prometheus_metrics.sql | 102 ++++ .../20261012124943_developer_api_keys.sql | 128 +++++ tests/developer-api.test.ts | 238 +++++++++ tests/prometheus.test.ts | 147 ++++++ 50 files changed, 4087 insertions(+), 31 deletions(-) create mode 100644 app/api/dev/keys/route.ts create mode 100644 app/api/dev/openapi.json/route.ts create mode 100644 app/api/dev/route.ts create mode 100644 app/api/dev/v1/[...path]/route.ts create mode 100644 app/api/metrics/route.ts create mode 100644 app/dev/api/page.module.css create mode 100644 app/dev/api/page.tsx create mode 100644 components/DeveloperDocumentation.tsx create mode 100644 components/DeveloperPanel.tsx create mode 100644 components/PrometheusIntegration.tsx create mode 100644 docs/bedrock-feasibility.md create mode 100644 docs/developer-api.md create mode 100644 docs/forum.md create mode 100644 docs/prometheus.md create mode 100644 lib/developer-api.ts create mode 100644 lib/developer-key-management.ts create mode 100644 lib/developer-response.ts create mode 100644 lib/forum.ts create mode 100644 lib/prometheus-api.ts create mode 100644 lib/prometheus-response.ts create mode 100644 scripts/test-developer-keys-sql.mjs create mode 100644 scripts/test-prometheus-sql.mjs create mode 100644 shared/developer-api.ts create mode 100644 shared/developer-docs.ts create mode 100644 shared/developer-openapi.ts create mode 100644 shared/forum.json create mode 100644 shared/grafana-dashboard.ts create mode 100644 shared/prometheus.ts create mode 100644 supabase/migrations/20261012085217_prometheus_metrics.sql create mode 100644 supabase/migrations/20261012124943_developer_api_keys.sql create mode 100644 tests/developer-api.test.ts create mode 100644 tests/prometheus.test.ts diff --git a/.env.example b/.env.example index 89e81fa..2b4738d 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,8 @@ MINEGEO_ALLOWED_ORIGINS= # Telegram: https://t.me/ (private group invite links also work). MINEGEO_DISCORD_URL= MINEGEO_TELEGRAM_URL= +# Optional HTTPS override for the hosted community forum linked in the header/footer. +MINEGEO_FORUM_URL= # Optional server-only Discord bot integration. All published parties use one channel. # Create a bot at https://discord.com/developers/applications and invite it to your server. # Channel IDs are copied with Discord Developer Mode enabled. Never use NEXT_PUBLIC_ here. diff --git a/app/api/dev/keys/route.ts b/app/api/dev/keys/route.ts new file mode 100644 index 0000000..8130f3a --- /dev/null +++ b/app/api/dev/keys/route.ts @@ -0,0 +1,8 @@ +import { developerKeys } from "@/lib/developer-key-management"; +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; +export const GET = developerKeys; +export const POST = developerKeys; +export const DELETE = developerKeys; +export const PUT = developerKeys; +export const PATCH = developerKeys; diff --git a/app/api/dev/openapi.json/route.ts b/app/api/dev/openapi.json/route.ts new file mode 100644 index 0000000..cb96659 --- /dev/null +++ b/app/api/dev/openapi.json/route.ts @@ -0,0 +1,10 @@ +import { developerOpenApi } from "@/shared/developer-openapi"; +export const dynamic = "force-static"; +export function GET() { + return Response.json(developerOpenApi, { + headers: { + "Cache-Control": "public, max-age=3600", + "X-Content-Type-Options": "nosniff", + }, + }); +} diff --git a/app/api/dev/route.ts b/app/api/dev/route.ts new file mode 100644 index 0000000..020c242 --- /dev/null +++ b/app/api/dev/route.ts @@ -0,0 +1,3 @@ +export function GET(request: Request) { + return Response.redirect(new URL("/dev/api", request.url), 307); +} diff --git a/app/api/dev/v1/[...path]/route.ts b/app/api/dev/v1/[...path]/route.ts new file mode 100644 index 0000000..16e7282 --- /dev/null +++ b/app/api/dev/v1/[...path]/route.ts @@ -0,0 +1,17 @@ +import { developerApi } from "@/lib/developer-api"; +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; +export const maxDuration = 30; +async function handle( + request: Request, + { params }: { params: Promise<{ path: string[] }> }, +) { + return developerApi(request, (await params).path); +} +export { + handle as GET, + handle as POST, + handle as PATCH, + handle as PUT, + handle as DELETE, +}; diff --git a/app/api/metrics/route.ts b/app/api/metrics/route.ts new file mode 100644 index 0000000..cf6e327 --- /dev/null +++ b/app/api/metrics/route.ts @@ -0,0 +1,16 @@ +import { admin, required } from "@/lib/supabase"; +import { metricsResponse } from "@/lib/prometheus-response"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; +export const maxDuration = 30; +export function GET(request: Request) { + return metricsResponse(request, async (tokenHash) => { + const result = await admin().rpc("read_prometheus_metrics", { + p_token_hash: tokenHash, + }); + // NULL is an invalid or disabled credential, not a missing database record. + if (result.error) required(result); + return result.data; + }); +} diff --git a/app/appearance.css b/app/appearance.css index ae83a9d..8bd0aec 100644 --- a/app/appearance.css +++ b/app/appearance.css @@ -384,7 +384,7 @@ html[data-theme="light"] .site-cursor-dot span { background: #273b21; box-shadow: 0 0 1px #fff; } -@media (max-width: 1000px) { +@media (max-width: 1200px) { .site-header { height: auto; } @@ -436,7 +436,7 @@ html[data-theme="light"] .site-cursor-dot span { font-size: 17px; } } -@media (max-width: 380px) { +@media (max-width: 600px) { .header-actions { grid-row: 2; grid-column: 1 / -1; diff --git a/app/dev/api/page.module.css b/app/dev/api/page.module.css new file mode 100644 index 0000000..4cefd0f --- /dev/null +++ b/app/dev/api/page.module.css @@ -0,0 +1,225 @@ +.intro { + max-width: 780px; +} +.tabs { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin: 24px 0 36px; +} +.stack { + display: grid; + gap: 32px; +} +.panel { + padding: clamp(20px, 4vw, 32px); + border: 1px solid var(--line); + border-radius: 18px; + background: var(--panel); + min-width: 0; +} +.panel h2 { + font-size: clamp(24px, 3vw, 32px); +} +.heading { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 18px; +} +.heading h2 { + margin: 0; +} +.fields { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(140px, 200px); + gap: 20px; +} +.fieldset { + border: 0; + padding: 0; + margin: 24px 0; + min-width: 0; +} +.fieldset legend { + font-weight: 700; + margin-bottom: 12px; +} +.scopes { + display: grid; + gap: 12px; +} +.scope { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 16px; + border: 1px solid var(--line); + border-radius: 10px; +} +.scope input { + margin-top: 5px; + width: 18px; + height: 18px; + flex: 0 0 18px; + accent-color: var(--green); +} +.scope strong, +.scope small { + display: block; +} +.scope small { + color: var(--muted); + line-height: 1.6; +} +.actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px; + margin: 20px 0 0; +} +.token { + margin-bottom: 24px; + padding: 20px; + border: 1px solid var(--green); + border-radius: 12px; + background: var(--panel2); +} +.token input { + width: 100%; + font-family: monospace; + font-size: 13px; +} +.keyList { + display: grid; + gap: 16px; + padding: 0; + list-style: none; + margin: 24px 0 0; +} +.key { + border: 1px solid var(--line); + border-radius: 12px; + padding: 20px; + min-width: 0; +} +.key h3 { + overflow-wrap: anywhere; + margin: 0; + font-size: 20px; +} +.key dl { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 6px 18px; + margin: 16px 0; +} +.key dt { + color: var(--muted); +} +.key dd { + margin: 0; + overflow-wrap: anywhere; +} +.key code { + overflow-wrap: anywhere; +} +.notice { + padding: 14px 18px; + border-left: 3px solid var(--green); + background: var(--panel2); + margin: 20px 0; +} +.error { + color: var(--text); + border-left-color: #ed7878; +} +.docs { + scroll-margin-top: 120px; +} +.docs h3 { + margin-top: 32px; +} +.docs p, +.docs li { + line-height: 1.8; +} +.docs ul, +.docs ol { + padding-left: 24px; +} +.docs ul { + list-style: disc; +} +.docs ol { + list-style: decimal; +} +.docs a { + color: var(--green); + text-decoration: underline; + text-underline-offset: 3px; +} +.docs code { + overflow-wrap: anywhere; + font-family: monospace; +} +.code { + overflow-x: auto; + padding: 20px; + border-radius: 10px; + border: 1px solid var(--line); + background: var(--bg); + font-family: monospace; + font-size: 13px; + line-height: 1.7; + max-width: 100%; +} +.code code { + overflow-wrap: normal; +} +.tableWrap { + overflow-x: auto; + max-width: 100%; +} +.table { + width: 100%; + border-collapse: collapse; + text-align: left; +} +.table th, +.table td { + padding: 14px 16px; + border-bottom: 1px solid var(--line); + vertical-align: top; +} +.table th { + color: var(--muted); +} +.table td:first-child { + min-width: 150px; +} +.table td:last-child { + min-width: 200px; +} +@media (max-width: 600px) { + .fields { + grid-template-columns: minmax(0, 1fr); + } + .key dl { + grid-template-columns: minmax(0, 1fr); + gap: 2px; + } + .key dd { + margin-bottom: 10px; + } + .code { + padding: 14px; + font-size: 12px; + } + .table th, + .table td { + padding: 12px; + } +} diff --git a/app/dev/api/page.tsx b/app/dev/api/page.tsx new file mode 100644 index 0000000..7346b58 --- /dev/null +++ b/app/dev/api/page.tsx @@ -0,0 +1,23 @@ +import type { Metadata } from "next"; +import { Header, Footer } from "@/components/Shell"; +import { DeveloperPanel } from "@/components/DeveloperPanel"; +import { user } from "@/lib/supabase"; + +export const metadata: Metadata = { + title: "Developer API", + description: "Create and manage scoped MineGeo API keys.", +}; +export const dynamic = "force-dynamic"; + +export default async function DeveloperApiPage() { + const account = await user(); + return ( + <> +
+
+ +
+