import type { Metadata } from "next" import { getTranslations, getMessages, setRequestLocale } from "next-intl/server" import { Link } from "@/i18n/navigation" import { ExternalLink } from "lucide-react" import { DocHeader } from "@/components/ui/doc-header" import { Callout } from "@/components/ui/callout" import CopyableCode from "@/components/CopyableCode" export async function generateMetadata({ params, }: { params: Promise<{ locale: string }> }): Promise { const { locale } = await params const t = await getTranslations({ locale, namespace: "docs.monitor.accessAuth.meta" }) return { title: t("title"), description: t("description"), keywords: [ "proxmox 2fa", "proxmox totp", "proxmox dashboard authentication", "proxmox user profile", "proxmox dashboard avatar", "proxmox api tokens", "proxmox reverse proxy", "proxmox nginx", "proxmox caddy", "proxmox traefik", "proxmox fail2ban dashboard", ], alternates: { canonical: "https://proxmenux.com/docs/monitor/access-auth" }, openGraph: { title: t("ogTitle"), description: t("ogDescription"), type: "article", url: "https://proxmenux.com/docs/monitor/access-auth", }, twitter: { card: "summary", title: t("twitterTitle"), description: t("twitterDescription"), }, } } type Row2 = { button: string; what: string; api: string } type FieldRow = { field: string; required: string; notes: string } type EndpointRow = { endpoint: string; what: string } type CryptoRow = { asset: string; algorithm: string; where: string } type AppRow = { name: string; href: string; platforms: string; notes: string } type WhereNextItem = { label: string; href: string; tail?: string; tailRich?: string } export default async function MonitorAccessAuthPage({ params, }: { params: Promise<{ locale: string }> }) { const { locale } = await params setRequestLocale(locale) const t = await getTranslations({ locale, namespace: "docs.monitor.accessAuth" }) const messages = (await getMessages({ locale })) as unknown as { docs: { monitor: { accessAuth: { firstLaunch: { rows: Row2[] fieldRows: FieldRow[] endpointRows: EndpointRow[] } password: { items: string[] publicItems: string[] cryptoRows: CryptoRow[] } twofa: { apps: AppRow[] setupSteps: string[] setupStep4Sub: string[] lostItems: string[] rejectedItems: string[] } apiTokens: { generateSteps: string[]; cheatItems: string[] } https: { items: string[] } fail2ban: { items: string[] } whereNext: { items: WhereNextItem[] } } } } } const aa = messages.docs.monitor.accessAuth const firstLaunchRows = aa.firstLaunch.rows const fieldRows = aa.firstLaunch.fieldRows const endpointRows = aa.firstLaunch.endpointRows const passwordItems = aa.password.items const publicItems = aa.password.publicItems const cryptoRows = aa.password.cryptoRows const apps = aa.twofa.apps const setupSteps = aa.twofa.setupSteps const setupStep4Sub = aa.twofa.setupStep4Sub const lostItems = aa.twofa.lostItems const rejectedItems = aa.twofa.rejectedItems const generateSteps = aa.apiTokens.generateSteps const cheatItems = aa.apiTokens.cheatItems const httpsItems = aa.https.items const fail2banItems = aa.fail2ban.items const whereNextItems = aa.whereNext.items const code = (chunks: React.ReactNode) => {chunks} const strong = (chunks: React.ReactNode) => {chunks} const em = (chunks: React.ReactNode) => {chunks} const apiLink = (chunks: React.ReactNode) => ( {chunks} ) const intLink = (chunks: React.ReactNode) => ( {chunks} ) const gatewayLink = (chunks: React.ReactNode) => ( {chunks} ) const fail2banLink = (chunks: React.ReactNode) => ( {chunks} ) const tailscaleAnchor = (chunks: React.ReactNode) => ( {chunks} ) const tsKeysAnchor = (chunks: React.ReactNode) => ( {chunks} ) return (
{t.rich("intro.body", { em, strong })}

{t("reaching.heading")}

{t.rich("reaching.intro", { code })}

:8008 # 2) Behind a reverse proxy with a dedicated host name (recommended off-LAN) https://monitor.example.com # 3) Through Secure Gateway (Tailscale) — same LAN URL, from anywhere http://:8008 # works from any device on your tailnet`} className="my-4" />

{t.rich("reaching.outro", { code })}

{t("firstLaunch.heading")}

{t.rich("firstLaunch.intro", { code, em })}

{t("firstLaunch.imageAlt")}
{t("firstLaunch.imageCaption")}
{firstLaunchRows.map((row, idx) => ( ))}
{t("firstLaunch.headerButton")} {t("firstLaunch.headerWhat")} {t("firstLaunch.headerApi")}
{row.button} {t.rich(`firstLaunch.rows.${idx}.what`, { em, code })} {row.api}
{t.rich("firstLaunch.twofaCalloutBody", { strong })}

{t("firstLaunch.createTitle")}

{t.rich("firstLaunch.createIntro", { em })}

{fieldRows.map((row, idx) => ( ))}
{t("firstLaunch.headerField")} {t("firstLaunch.headerRequired")} {t("firstLaunch.headerNotes")}
{row.field} {row.required} {t.rich(`firstLaunch.fieldRows.${idx}.notes`, { code, strong })}
{t("firstLaunch.createImageAlt")}
{t("firstLaunch.createImageCaption")}
{t.rich("firstLaunch.saveCalloutBody", { code })}

{t("firstLaunch.avatarTitle")}

{t.rich("firstLaunch.avatarBody1", { strong })}

{t("firstLaunch.avatarBody2")}

{t("firstLaunch.profileImageAlt")}
{t("firstLaunch.profileImageCaption")}
{endpointRows.map((row, idx) => ( ))}
{t("firstLaunch.headerEndpoint")} {t("firstLaunch.headerEpWhat")}
{row.endpoint} {t.rich(`firstLaunch.endpointRows.${idx}.what`, { code })}
{t.rich("firstLaunch.reversibleBody", { em, strong, code })}

{t("password.heading")}

{t.rich("password.intro", { code })}

    {passwordItems.map((_, idx) => (
  • {t.rich(`password.items.${idx}`, { strong, code })}
  • ))}
{t("password.loginImageAlt")}
{t("password.loginImageCaption")}

{t("password.loginFlowTitle")}

:8008/api/auth/login \\ -H "Content-Type: application/json" \\ -d '{"username":"","password":""}' # Response { "success": true, "token": "eyJhbGciOiJIUzI1NiIs..." }`} className="my-4" />

{t.rich("password.twofaIntro", { code })}

:8008/api/auth/login \\ -H "Content-Type: application/json" \\ -d '{"username":"","password":"","totp_token":"123456"}'`} className="my-4" />

{t("password.publicTitle")}

{t("password.publicIntro")}

    {publicItems.map((_, idx) => (
  • {t.rich(`password.publicItems.${idx}`, { code })}
  • ))}

{t("password.cryptoTitle")}

{t.rich("password.cryptoIntro", { code })}

{cryptoRows.map((row, idx) => ( ))}
{t("password.headerAsset")} {t("password.headerAlgo")} {t("password.headerWhere")}
{row.asset} {t.rich(`password.cryptoRows.${idx}.algorithm`, { code })} {t.rich(`password.cryptoRows.${idx}.where`, { code })}
{t.rich("password.authJsonBody", { code, em })} {t.rich("password.rotateBody", { code, strong })}

{t("password.recoverTitle")}

{t.rich("password.recoverIntro", { code })}

# - Stop the proxmenux-monitor service # - Clear username / password_hash / TOTP secret / backup codes # - Keep jwt_secret and api_tokens intact # - Restart the service # 3. Open the dashboard at http://:8008 # The setup wizard appears — create a new admin account.`} className="my-4" /> {t.rich("password.survivesBody", { code })} {t.rich("password.physicalBody", { strong, code })}

{t("twofa.heading")}

{t.rich("twofa.intro", { strong })}

{t("twofa.pickTitle")}

{t("twofa.pickIntro")}

{apps.map((row, idx) => ( ))}
{t("twofa.headerApp")} {t("twofa.headerPlatforms")} {t("twofa.headerAppNotes")}
{row.name} {row.platforms} {row.notes}
{t("twofa.backupBody")}

{t("twofa.setupTitle")}

{t("twofa.setupImageAlt")}
{t("twofa.setupImageCaption")}
    {setupSteps.map((_, idx) => (
  1. {t.rich(`twofa.setupSteps.${idx}`, { strong, em, code })} {idx === 3 && (
      {setupStep4Sub.map((_, sIdx) => (
    • {t.rich(`twofa.setupStep4Sub.${sIdx}`, { em, code })}
    • ))}
    )}
  2. ))}
{t.rich("twofa.testBody", { em, code })}

{t("twofa.lostTitle")}

{t("twofa.lostIntro")}

    {lostItems.map((_, idx) => (
  • {t.rich(`twofa.lostItems.${idx}`, { strong, code })} {idx === 2 && (
    {`systemctl restart proxmenux-monitor.service`}
    )}
  • ))}

{t("twofa.lostShellOutro")}

{t("twofa.disableTitle")}

{t.rich("twofa.disableBody", { strong, code })}

{t("twofa.rejectedIntro")}
    {rejectedItems.map((_, idx) => (
  • {t.rich(`twofa.rejectedItems.${idx}`, { strong, code })}
  • ))}
{t("twofa.rejectedOutro")}

{t("apiTokens.heading")}

{t.rich("apiTokens.intro", { strong, code })}

{t("apiTokens.imageAlt")}
{t("apiTokens.imageCaption")}

{t("apiTokens.generateTitle")}

{t("apiTokens.generateIntro")}

    {generateSteps.map((_, idx) => (
  1. {t.rich(`apiTokens.generateSteps.${idx}`, { strong, em })}
  2. ))}

{t("apiTokens.generateCli")}

:8008/api/auth/generate-api-token \\ -H "Authorization: Bearer " \\ -H "Content-Type: application/json" \\ -d '{ "password": "", "totp_token": "123456", "token_name": "Home Assistant" }' # Response — the "token" field is the only place the token appears. { "success": true, "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token_name": "Home Assistant", "expires_in": "365 days" }`} className="my-4" />

{t("apiTokens.useTitle")}

" \\ http://:8008/api/system`} className="my-4" />

{t("apiTokens.revokeTitle")}

{t.rich("apiTokens.revokeBody", { strong, code })}

:8008/api/auth/api-tokens/ \\ -H "Authorization: Bearer "`} className="my-4" />
    {cheatItems.map((_, idx) => (
  • {t.rich(`apiTokens.cheatItems.${idx}`, { code })}
  • ))}

{t.rich("apiTokens.outro", { apiLink, intLink })}

{t("https.heading")}

{t("https.intro")}

    {httpsItems.map((_, idx) => (
  1. {t.rich(`https.items.${idx}`, { strong, code })}
  2. ))}
{t("https.calloutBody")}

{t("gateway.heading")}

{t.rich("gateway.intro", { strong, a: tailscaleAnchor })}

{t.rich("gateway.calloutBody", { code })}

{t.rich("gateway.deployBody", { a: tsKeysAnchor })}

{t.rich("gateway.outro", { link: gatewayLink })}

{t("proxy.heading")}

{t.rich("proxy.intro", { strong, code })}

{t("proxy.nginxTitle")}

{t("proxy.caddyTitle")}

{t("proxy.traefikTitle")}

{t.rich("proxy.subPathBody", { code, strong })}

{t("audit.heading")}

{t.rich("audit.intro", { code })}

{t.rich("audit.outro", { code })}

{t("fail2ban.heading")}

{t.rich("fail2ban.calloutBody", { strong, link: fail2banLink })}

{t.rich("fail2ban.intro", { code })}

    {fail2banItems.map((_, idx) => (
  • {t.rich(`fail2ban.items.${idx}`, { code })}
  • ))}

{t.rich("fail2ban.outro", { link: fail2banLink })}

{t("troubleshoot.heading")}

{t.rich("troubleshoot.noScreenBody", { code })}
{`rm /root/.config/proxmenux-monitor/auth.json
systemctl restart proxmenux-monitor.service`}
{t.rich("troubleshoot.noScreenOutro", { code })}
{t.rich("troubleshoot.tokenBody", { code })}
{`curl -H "Authorization: Bearer " \\
  http://:8008/api/system | jq .`}
{t.rich("troubleshoot.tokenOutro", { code })}
{t.rich("troubleshoot.no2faBody", { code })} {t.rich("troubleshoot.wsBody", { code })}

{t("whereNext.heading")}

    {whereNextItems.map((item, idx) => (
  • {item.label} {item.tailRich ? t.rich(`whereNext.items.${idx}.tailRich`, { code }) : item.tail}
  • ))}
) }