"use client"; import Link from "next/link"; import { useEffect, useRef, useState } from "react"; import { BookOpen, Code2, Copy, KeyRound, RefreshCw, Trash2, X, } from "lucide-react"; import { useI18n } from "@/components/I18nProvider"; import { developerScopes } from "@/shared/developer-docs"; import styles from "@/app/dev/api/page.module.css"; import type { DeveloperKey, DeveloperScope } from "@/shared/developer-api"; type Scope = DeveloperScope; type Key = DeveloperKey; async function request( method = "GET", body?: unknown, signal?: AbortSignal, ): Promise { const response = await fetch("/api/dev/keys", { method, cache: "no-store", credentials: "same-origin", signal: signal ?? AbortSignal.timeout(30000), headers: body === undefined ? undefined : { "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), }); const result = await response.json(); if (!response.ok) throw new Error( typeof result.error === "string" ? result.error : (result.error?.message ?? "Request failed. Please try again."), ); return result as T; } export function DeveloperPanel({ signedIn }: { signedIn: boolean }) { const { locale } = useI18n(); const ru = locale === "ru"; const l = (en: string, russian: string) => (ru ? russian : en); const [keys, setKeys] = useState([]); const [loaded, setLoaded] = useState(false); const [loading, setLoading] = useState(signedIn); const [busy, setBusy] = useState(false); const busyRef = useRef(false); const [name, setName] = useState(""); const [expiresDays, setExpiresDays] = useState(90); const [scopes, setScopes] = useState(["maps:read"]); const [token, setToken] = useState(null); const [confirmRevoke, setConfirmRevoke] = useState(null); const [error, setError] = useState(""); const [message, setMessage] = useState(""); useEffect(() => { if (!signedIn) return; const controller = new AbortController(); void request<{ keys: Key[] }>("GET", undefined, controller.signal) .then((data) => { if (!controller.signal.aborted) { setKeys(data.keys); setLoaded(true); } }) .catch((e: unknown) => { if (!controller.signal.aborted) setError( e instanceof Error ? e.message : ru ? "Не удалось загрузить ключи API." : "Could not load API keys.", ); }) .finally(() => { if (!controller.signal.aborted) setLoading(false); }); return () => controller.abort(); }, [signedIn, ru]); async function reload() { setLoading(true); setError(""); try { const data = await request<{ keys: Key[] }>(); setKeys(data.keys); setLoaded(true); } catch (e) { setError( e instanceof Error ? e.message : l("Could not load API keys.", "Не удалось загрузить ключи API."), ); } finally { setLoading(false); } } async function create(event: React.FormEvent) { event.preventDefault(); if (busyRef.current) return; busyRef.current = true; setBusy(true); setError(""); setMessage(""); try { const result = await request<{ key: Key; token: string }>("POST", { name: name.trim(), scopes, expiresDays, }); setToken(result.token); setKeys((list) => [result.key, ...list]); setName(""); setMessage( l( "Key created. Copy it before leaving or hiding it.", "Ключ создан. Скопируйте его перед уходом со страницы или скрытием.", ), ); } catch (e) { setError( e instanceof Error ? e.message : l("Could not create the key.", "Не удалось создать ключ."), ); } finally { busyRef.current = false; setBusy(false); } } async function revoke(id: string) { if (busyRef.current) return; busyRef.current = true; setBusy(true); setError(""); setMessage(""); try { await request("DELETE", { id }); setKeys((list) => list.map((key) => key.id === id ? { ...key, revokedAt: new Date().toISOString() } : key, ), ); setConfirmRevoke(null); setToken(null); setMessage( l( "Key revoked. Requests using it will no longer work.", "Ключ отозван. Запросы с ним больше не работают.", ), ); } catch (e) { setError( e instanceof Error ? e.message : l("Could not revoke the key.", "Не удалось отозвать ключ."), ); } finally { busyRef.current = false; setBusy(false); } } async function copy(value: string) { try { await navigator.clipboard.writeText(value); setMessage(l("Copied to clipboard.", "Скопировано в буфер обмена.")); setError(""); } catch { setError( l( "Could not copy. Select the key below and copy it manually.", "Не удалось скопировать. Выделите ключ ниже и скопируйте вручную.", ), ); } } const activeCount = keys.filter( (key) => !key.revokedAt && new Date(key.expiresAt).getTime() > Date.now(), ).length; const date = (value: string) => new Date(value).toLocaleString(ru ? "ru-RU" : "en-US"); return (
{l("BUILD WITH MINEGEO", "СОЗДАВАЙТЕ С MINEGEO")}

{l("Developer API.", "API для разработчиков.")}

{l( "Use MineGeo maps and your own account data in scripts and integrations. Choose exactly what each key can read.", "Используйте карты MineGeo и данные своего аккаунта в скриптах и интеграциях. Выберите, что именно сможет читать каждый ключ.", )}

{l( "Keys belong to your account. Keep them private and grant only the access your integration needs. This API is read-only: keys cannot upload, publish, change maps or use moderation tools.", "Ключи принадлежат вашему аккаунту. Храните их в секрете и выдавайте только необходимые права. Этот API доступен только для чтения: ключи не позволяют загружать, публиковать, изменять карты или использовать инструменты модерации.", )}

{!signedIn ? (

{l( "Everyone can read the documentation. Log in to create and manage your own keys; no Admin permission is required.", "Документация доступна всем. Войдите, чтобы создавать свои ключи и управлять ими; права администратора не нужны.", )}

{l("Log in to manage keys", "Войти для управления ключами")}
) : ( <> {error && (

{error}

)} {message && (

{message}

)} {token && (

{l("Copy your new key now", "Скопируйте новый ключ сейчас")}

{l( "This is the only time the full key is shown. Hiding it or leaving this page removes it from view. MineGeo stores only its hash.", "Полный ключ показывается только один раз. После скрытия или ухода со страницы его нельзя посмотреть снова. MineGeo хранит только хеш ключа.", )}

)} {loading && (

{l("Loading keys…", "Загрузка ключей…")}

)} {!loading && !loaded && ( )} {loaded && ( <>
void create(event)}>

{l("Create a key", "Создать ключ")}

{l( `${activeCount} of 5 active keys. Each key expires after the number of days you choose.`, `Активных ключей: ${activeCount} из 5. Срок действия каждого ключа задаётся в днях.`, )}

{l("Permissions", "Права доступа")}
{developerScopes.map((scope) => ( ))}
{token && (

{l( "Copy and hide the current key before creating another.", "Скопируйте и скройте текущий ключ перед созданием следующего.", )}

)}
{keys.length === 0 && (

{l( "You have no API keys yet.", "У вас пока нет ключей API.", )}

)}
    {keys.map((key) => { const expired = new Date(key.expiresAt).getTime() <= Date.now(); return (
  • {key.name}

    {key.revokedAt ? l("Revoked", "Отозван") : expired ? l("Expired", "Истёк") : l("Active", "Активен")}
    {key.prefix}…
    {l("Permissions", "Права")}
    {key.scopes.join(", ")}
    {l("Created", "Создан")}
    {date(key.createdAt)}
    {l("Expires", "Истекает")}
    {date(key.expiresAt)}
    {l("Last used", "Последнее использование")}
    {key.lastUsedAt ? date(key.lastUsedAt) : l("Not used yet", "Ещё не использован")}
    {!key.revokedAt && (confirmRevoke === key.id ? (

    {l( "Revoke this key? Every integration using it will lose access immediately.", "Отозвать этот ключ? Все интеграции с ним сразу потеряют доступ.", )}

    ) : ( ))}
  • ); })}
)} )}
); }