From c5368fa8cc60b1d9f858103926f727692d328e57 Mon Sep 17 00:00:00 2001 From: pavel444-byte Date: Mon, 5 Oct 2026 13:41:36 +0500 Subject: [PATCH] Improve MineGeo exploration and mapping features --- README.md | 6 + app/account.css | 286 +++++++++++++++ app/auth/callback/route.ts | 48 ++- app/auth/confirm/route.ts | 1 + app/auth/reset-password/page.tsx | 56 +++ app/dashboard/[id]/page.tsx | 7 +- app/dashboard/page.tsx | 3 + app/downloads/page.tsx | 26 +- app/globals.css | 167 ++++++++- app/layout.tsx | 1 + app/log-in/page.tsx | 32 ++ app/pair/page.tsx | 8 +- app/servers/[id]/page.tsx | 12 + app/settings/page.tsx | 40 ++ artifacts-plugin-regression.json | 10 +- components/AccountSettings.tsx | 152 ++++++++ components/Brand.tsx | 13 + components/ContributionInstructions.tsx | 54 +++ components/Dashboard.tsx | 21 +- components/DraftEditor.tsx | 93 ++++- components/Game.tsx | 2 +- components/MapCard.tsx | 2 + components/Moderation.tsx | 13 +- components/OwnerContributions.tsx | 236 ++++++++++++ components/PanoramaPreview.tsx | 132 +++++-- components/PasswordForm.tsx | 148 ++++++++ components/Shell.tsx | 24 +- components/SignIn.tsx | 294 ++++++++++++--- docs/account-settings.md | 23 ++ docs/moderation.md | 2 + docs/panorama-contributions.md | 42 +++ docs/server-plugin.md | 3 + docs/verification.md | 48 +++ lib/account-api.ts | 212 +++++++++++ lib/api.ts | 346 ++++++++++++------ lib/catalog.ts | 21 ++ lib/password-recovery.ts | 6 + lib/profile.ts | 27 ++ lib/supabase.ts | 10 +- .../dev/minegeo/fabric/CaptureScreen.java | 17 +- .../dev/minegeo/fabric/MineGeoClient.java | 9 +- .../dev/minegeo/fabric/SurfaceDataset.java | 2 +- .../dev/minegeo/plugin/MineGeoPlugin.java | 26 +- .../dev/minegeo/plugin/ServerAddress.java | 33 ++ .../java/dev/minegeo/plugin/UploadClient.java | 29 +- .../plugin/src/main/resources/config.yml | 5 +- proxy.ts | 5 +- public/tools/mod/minegeo-fabric-1.21.11.jar | Bin 35024 -> 39133 bytes .../tools/plugin/minegeo-plugin-1.21.11.jar | Bin 336868 -> 340716 bytes scripts/java-build.mjs | 1 + scripts/repair-panoramas.mjs | 9 + scripts/reprocess.mjs | 11 +- scripts/test-plugin.ts | 74 +++- shared/account.ts | 69 ++++ shared/database.ts | 115 +++++- shared/password-recovery.ts | 41 +++ shared/protocol.ts | 64 +++- shared/server-address.ts | 32 ++ .../20261005063646_panorama_contributions.sql | 172 +++++++++ .../20261005065029_withdraw_contribution.sql | 16 + ...261005073253_contribution_server_check.sql | 187 ++++++++++ tests/account.test.ts | 116 ++++++ tests/contributions.test.ts | 103 ++++++ tests/java/ServerAddressSmoke.java | 17 + tests/java/UploadSmoke.java | 2 +- tests/reprocess.test.ts | 21 +- tests/server-address.test.ts | 43 +++ tests/sql/contribution-server.sql | 61 +++ worker/main.ts | 7 +- 69 files changed, 3581 insertions(+), 333 deletions(-) create mode 100644 app/account.css create mode 100644 app/auth/confirm/route.ts create mode 100644 app/auth/reset-password/page.tsx create mode 100644 app/log-in/page.tsx create mode 100644 app/settings/page.tsx create mode 100644 components/AccountSettings.tsx create mode 100644 components/Brand.tsx create mode 100644 components/ContributionInstructions.tsx create mode 100644 components/OwnerContributions.tsx create mode 100644 components/PasswordForm.tsx create mode 100644 docs/account-settings.md create mode 100644 docs/panorama-contributions.md create mode 100644 lib/account-api.ts create mode 100644 lib/password-recovery.ts create mode 100644 lib/profile.ts create mode 100644 minecraft/plugin/src/main/java/dev/minegeo/plugin/ServerAddress.java create mode 100644 shared/account.ts create mode 100644 shared/password-recovery.ts create mode 100644 shared/server-address.ts create mode 100644 supabase/migrations/20261005063646_panorama_contributions.sql create mode 100644 supabase/migrations/20261005065029_withdraw_contribution.sql create mode 100644 supabase/migrations/20261005073253_contribution_server_check.sql create mode 100644 tests/account.test.ts create mode 100644 tests/contributions.test.ts create mode 100644 tests/java/ServerAddressSmoke.java create mode 100644 tests/server-address.test.ts create mode 100644 tests/sql/contribution-server.sql diff --git a/README.md b/README.md index 9050f61..a493d22 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,10 @@ Missing vanilla assets are also prepared automatically when a plugin export is r ## Install the capture tools +Maps support up to 24 locations and games choose five at random. Owners can opt into player contributions, with a cumulative 12-panorama limit per player and owner approval followed by moderator review. See [panorama contributions](docs/panorama-contributions.md) for both tool workflows and deployment instructions. + +Contributions check the server address recorded during capture. Owners can correct or clear the map's expected address. Fabric records the connected server automatically; plugin admins must set `server-address` or run `/minegeo server ` before exporting. + Ready-to-install JARs are committed under `public/tools/plugin` and `public/tools/mod` and served by `/downloads`. After pulling an update, replace the Minecraft server's installed JAR and restart that server; pulling the website alone does not update an already installed plugin. - **Server:** Java 21 and Bukkit/Paper 1.21.11. Follow [the plugin guide](docs/server-plugin.md). @@ -43,6 +47,8 @@ The build copies both JARs to their Git-visible download folders. Include those ## Checks +Accounts support display names, email/password login and password recovery. See [account settings and Supabase configuration](docs/account-settings.md). + ```sh npm test npm run test:renderer diff --git a/app/account.css b/app/account.css new file mode 100644 index 0000000..c9b4672 --- /dev/null +++ b/app/account.css @@ -0,0 +1,286 @@ +/* Shared account and authentication surfaces, layered after the Minecraft theme. */ +.auth-page { + padding-top: 48px; + padding-bottom: 72px; +} +.auth-layout { + display: grid; + grid-template-columns: 1.1fr 1fr; + gap: clamp(32px, 7vw, 100px); + align-items: center; + max-width: 1120px; + margin: 0 auto; +} +.auth-story h1 { + font-size: clamp(42px, 4.4vw, 64px); + line-height: 1.05; + margin: 18px 0; +} +.auth-story > p { + color: var(--muted); + max-width: 420px; + line-height: 1.8; + font-size: 15px; +} +.auth-story-map { + position: relative; + height: 210px; + border: 1px solid #4b5c43; + margin: 30px 0 22px; + padding: 26px; + display: flex; + flex-direction: column; + justify-content: space-between; + color: #c0e7a2; + background: + linear-gradient(90deg, #101a1cd9, #11192388), + url("/images/example-map.webp") center/cover; + box-shadow: inset 0 0 70px #09121977; +} +.auth-story-map span { + font: + 24px MineGeoPixel, + monospace; +} +.auth-benefits { + display: flex; + gap: 18px; + flex-wrap: wrap; + color: var(--muted); + font-size: 12px; +} +.auth-benefits span { + display: flex; + align-items: center; + gap: 8px; +} +.auth-benefits svg { + color: var(--green); +} +.auth-layout .auth-card { + width: 100%; + max-width: none; + margin: 0; + padding: 32px; + background: #121c28f5; + border: 1px solid #3a4958; + box-shadow: 0 20px 60px #0003; +} +.auth-card h2 { + font-size: 38px; + line-height: 1.1; + margin: 14px 0 10px; +} +.auth-card .field { + font-size: 13px; + margin: 18px 0; +} +.field small { + display: block; + margin-top: 8px; + font-size: 11px; + color: var(--muted); + line-height: 1.6; +} +.auth-tabs { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4px; + background: #0a121c; + padding: 4px; + border: 1px solid var(--line); + margin-bottom: 28px; +} +.auth-tabs button { + padding: 10px 8px; + border: 0; + color: var(--muted); + font-size: 13px; + cursor: pointer; +} +.auth-tabs button[aria-pressed="true"] { + color: var(--text); + background: #223342; + box-shadow: 0 2px 7px #0004; +} +.auth-recovery { + display: block; + margin: -4px 0 22px auto; + font-size: 12px; +} +.auth-card .text-link { + cursor: pointer; + color: var(--green); +} +.auth-alternative { + border-top: 1px solid var(--line); + padding-top: 20px; + margin-top: 24px; + text-align: center; + display: grid; + gap: 8px; + font-size: 12px; + color: var(--muted); +} +.auth-footnote { + color: var(--muted); + font-size: 11px; + line-height: 1.6; + text-align: center; + margin: 24px 0 0; +} +.auth-back { + display: block; + margin: 22px 0 0; + font-size: 12px; +} +.auth-card input, +.settings-card input { + min-height: 46px; + border-radius: 3px; +} +.reset-card { + max-width: 520px; + padding: 32px; + margin: 24px auto; +} +.reset-card h1 { + font-size: 46px; +} +.account-page-heading { + margin: 28px 0 36px; +} +.account-page-heading h1 { + margin: 10px 0; +} +.settings-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; + align-items: start; +} +.settings-card { + border: 1px solid #3a4958; + background: #121c28e8; + padding: 28px; + box-shadow: 0 12px 30px #0002; + min-width: 0; +} +.settings-heading { + display: flex; + align-items: flex-start; + gap: 14px; + border-bottom: 1px solid var(--line); + padding-bottom: 20px; +} +.settings-heading > svg { + margin-top: 6px; + color: var(--green); + flex: none; +} +.settings-heading h2 { + font-size: 32px; + line-height: 1.2; + margin-bottom: 6px; +} +.settings-heading p { + font-size: 12px; + margin: 0; +} +.account-email { + display: flex; + gap: 12px; + margin: 24px 0; + padding: 16px; + border: 1px solid var(--line); + background: #0d1520; + min-width: 0; +} +.account-email svg { + flex: none; + color: var(--muted); + margin-top: 4px; +} +.account-email div { + min-width: 0; + display: grid; + gap: 5px; +} +.account-email span, +.account-email small { + font-size: 11px; + color: var(--muted); +} +.account-email strong { + font-weight: 400; + overflow-wrap: anywhere; +} +.settings-recovery { + margin-top: 28px; + padding-top: 24px; + border-top: 1px solid var(--line); +} +.settings-recovery h3 { + font-size: 24px; + margin-bottom: 8px; +} +.settings-recovery p { + font-size: 12px; +} +.map-credit { + color: var(--muted); + font-size: 12px; + margin: 14px 0 0; +} +.dashboard-toolbar .inline-actions { + flex-wrap: wrap; + justify-content: flex-end; +} +@media (max-width: 850px) { + .auth-layout { + grid-template-columns: 1fr; + max-width: 520px; + gap: 24px; + } + .auth-story-map, + .auth-benefits { + display: none; + } + .auth-story h1 { + font-size: 42px; + margin: 12px 0; + } + .auth-story > p { + margin-bottom: 0; + font-size: 13px; + } + .settings-grid { + grid-template-columns: 1fr; + } +} +@media (max-width: 500px) { + .auth-page { + padding-top: 28px; + } + .auth-layout .auth-card, + .settings-card, + .reset-card { + padding: 22px; + } + .auth-story h1 { + font-size: 36px; + } + .auth-card h2 { + font-size: 32px; + } + .dashboard-toolbar { + flex-direction: column; + } + .dashboard-toolbar .inline-actions { + justify-content: flex-start; + } + .preview-toolbar { + padding: 10px 16px; + gap: 8px; + } +} diff --git a/app/auth/callback/route.ts b/app/auth/callback/route.ts index 290218e..e0cced0 100644 --- a/app/auth/callback/route.ts +++ b/app/auth/callback/route.ts @@ -1,13 +1,53 @@ import { NextRequest, NextResponse } from "next/server"; import { authClient } from "@/lib/supabase"; import { requestOrigin } from "@/lib/request-origin"; +import { authDestination } from "@/shared/account"; +import { createRecoveryGrant, recoveryCookie } from "@/lib/password-recovery"; +import { accountProfile } from "@/lib/profile"; export async function GET(request: NextRequest) { const origin = requestOrigin(request, process.env.MINEGEO_URL); const code = request.nextUrl.searchParams.get("code"); - if (code) { + const tokenHash = request.nextUrl.searchParams.get("token_hash"); + const type = request.nextUrl.searchParams.get("type"); + const next = authDestination(request.nextUrl.searchParams.get("next")); + if ( + code || + (tokenHash && + (type === "email" || type === "recovery" || type === "magiclink")) + ) { const client = await authClient(); - const { error } = await client.auth.exchangeCodeForSession(code); - if (!error) return NextResponse.redirect(new URL("/dashboard", origin)); + const { data, error } = code + ? await client.auth.exchangeCodeForSession(code) + : await client.auth.verifyOtp({ + token_hash: tokenHash!, + type: type as "email" | "recovery" | "magiclink", + }); + if (!error && data.user) { + await accountProfile(data.user); + const destination = type === "recovery" ? "/auth/reset-password" : next; + const response = NextResponse.redirect(new URL(destination, origin)); + if (destination === "/auth/reset-password") { + const { data: claims } = await client.auth.getClaims(); + const sessionId = claims?.claims.session_id; + if (typeof sessionId === "string") + response.cookies.set( + recoveryCookie, + createRecoveryGrant( + data.user.id, + sessionId, + process.env.SUPABASE_SECRET_KEY!, + ), + { + httpOnly: true, + sameSite: "lax", + secure: new URL(origin).protocol === "https:", + path: "/", + maxAge: 600, + }, + ); + } + return response; + } } - return NextResponse.redirect(new URL("/dashboard?auth=failed", origin)); + return NextResponse.redirect(new URL("/log-in?auth=failed", origin)); } diff --git a/app/auth/confirm/route.ts b/app/auth/confirm/route.ts new file mode 100644 index 0000000..57ff4bf --- /dev/null +++ b/app/auth/confirm/route.ts @@ -0,0 +1 @@ +export { GET } from "../callback/route"; diff --git a/app/auth/reset-password/page.tsx b/app/auth/reset-password/page.tsx new file mode 100644 index 0000000..7b389d3 --- /dev/null +++ b/app/auth/reset-password/page.tsx @@ -0,0 +1,56 @@ +import { cookies } from "next/headers"; +import Link from "next/link"; +import { Header, Footer } from "@/components/Shell"; +import { PasswordForm } from "@/components/PasswordForm"; +import { authClient, user } from "@/lib/supabase"; +import { recoveryCookie, verifyRecoveryGrant } from "@/lib/password-recovery"; +export const metadata = { title: "Reset password" }; +export default async function ResetPassword() { + const [account, jar, client] = await Promise.all([ + user(), + cookies(), + authClient(), + ]); + const { data } = account ? await client.auth.getClaims() : { data: null }; + const sessionId = data?.claims.session_id; + const valid = + account && + typeof sessionId === "string" && + verifyRecoveryGrant( + jar.get(recoveryCookie)?.value, + account.id, + sessionId, + process.env.SUPABASE_SECRET_KEY!, + ); + return ( + <> +
+
+
+ ACCOUNT RECOVERY +

A fresh start.

+ {valid ? ( + <> +

Choose a new password for your account.

+ + + ) : ( + <> +

+ This reset link has expired or has already been used. Request a + fresh link to continue. +

+ + Request a new reset link → + + + )} +
+
+