Add error page

This commit is contained in:
Eduard Gert
2026-02-04 15:11:22 +01:00
parent ca33849f31
commit 5da2b0fdcc
12 changed files with 106 additions and 22 deletions

View File

@@ -6,6 +6,7 @@ import (
"sync" "sync"
"github.com/netbirdio/netbird/proxy/internal/roundtrip" "github.com/netbirdio/netbird/proxy/internal/roundtrip"
"github.com/netbirdio/netbird/proxy/web"
) )
type ReverseProxy struct { type ReverseProxy struct {
@@ -30,9 +31,14 @@ func NewReverseProxy(transport http.RoundTripper) *ReverseProxy {
func (p *ReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (p *ReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
target, serviceId, accountID, exists := p.findTargetForRequest(r) target, serviceId, accountID, exists := p.findTargetForRequest(r)
if !exists { if !exists {
// No mapping found so return an error here. web.ServeHTTP(w, r, map[string]any{
// TODO: prettier error page. "page": "error",
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) "error": map[string]any{
"code": 404,
"title": "Service Not Found",
"message": "The requested service could not be found. Please check the URL, try refreshing, or check if the peer is running. If that doesn't work, see our documentation for help.",
},
}, http.StatusNotFound)
return return
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,10 +4,10 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/assets/favicon-Cv-2QvSV.ico" /> <link rel="icon" type="image/x-icon" href="/assets/favicon-Cv-2QvSV.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Authentication Required</title> <title>NetBird Service</title>
<meta name="robots" content="noindex, nofollow" /> <meta name="robots" content="noindex, nofollow" />
<script type="module" crossorigin src="/assets/index-BQ7jeUNq.js"></script> <script type="module" crossorigin src="/assets/index-ClfM9m3s.js"></script>
<link rel="stylesheet" crossorigin href="/assets/style-B08XFatU.css"> <link rel="stylesheet" crossorigin href="/assets/style-B1NSEbha.css">
</head> </head>
<body> <body>
<!-- Go template variables injected here --> <!-- Go template variables injected here -->

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/src/assets/favicon.ico" /> <link rel="icon" type="image/x-icon" href="/src/assets/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Authentication Required</title> <title>NetBird Service</title>
<meta name="robots" content="noindex, nofollow" /> <meta name="robots" content="noindex, nofollow" />
</head> </head>
<body> <body>

View File

@@ -63,6 +63,7 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0", "@babel/generator": "^7.29.0",
@@ -1709,6 +1710,7 @@
"integrity": "sha512-+0/4J266CBGPUq/ELg7QUHhN25WYjE0wYTPSQJn1xeu8DOlIOPxXxrNGiLmfAWl7HMMgWFWXpt9IDjMWrF5Iow==", "integrity": "sha512-+0/4J266CBGPUq/ELg7QUHhN25WYjE0wYTPSQJn1xeu8DOlIOPxXxrNGiLmfAWl7HMMgWFWXpt9IDjMWrF5Iow==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
@@ -1719,6 +1721,7 @@
"integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@@ -1778,6 +1781,7 @@
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/scope-manager": "8.54.0",
"@typescript-eslint/types": "8.54.0", "@typescript-eslint/types": "8.54.0",
@@ -2029,6 +2033,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -2134,6 +2139,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -2388,6 +2394,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -3384,6 +3391,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -3445,6 +3453,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -3678,6 +3687,7 @@
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "~0.27.0", "esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5" "get-tsconfig": "^4.7.5"
@@ -3711,6 +3721,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -3797,6 +3808,7 @@
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.27.0", "esbuild": "^0.27.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -3918,6 +3930,7 @@
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }

View File

@@ -1,4 +1,4 @@
import { useState, useRef } from "react"; import { useState, useRef, useEffect } from "react";
import {Loader2, Lock, Binary, LogIn} from "lucide-react"; import {Loader2, Lock, Binary, LogIn} from "lucide-react";
import { getData, type Data } from "@/data"; import { getData, type Data } from "@/data";
import Button from "@/components/Button"; import Button from "@/components/Button";
@@ -22,6 +22,10 @@ const methods: NonNullable<Data["methods"]> =
: { password:"password", pin: "pin", oidc: "/auth/oidc" }; : { password:"password", pin: "pin", oidc: "/auth/oidc" };
function App() { function App() {
useEffect(() => {
document.title = "Authentication Required - NetBird Service";
}, []);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [submitting, setSubmitting] = useState<string | null>(null); const [submitting, setSubmitting] = useState<string | null>(null);
const [pin, setPin] = useState(""); const [pin, setPin] = useState("");

View File

@@ -2,11 +2,16 @@ import { NetBirdLogo } from "./NetBirdLogo";
export function PoweredByNetBird() { export function PoweredByNetBird() {
return ( return (
<div className="flex items-center justify-center mt-8 gap-2 group cursor-pointer"> <a
href="https://netbird.io?utm_source=netbird-proxy&utm_medium=web&utm_campaign=powered_by"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center mt-8 gap-2 group cursor-pointer"
>
<span className="text-sm text-nb-gray-400 font-light text-center group-hover:opacity-80 transition-all"> <span className="text-sm text-nb-gray-400 font-light text-center group-hover:opacity-80 transition-all">
Powered by Powered by
</span> </span>
<NetBirdLogo size="small" mobile={false} /> <NetBirdLogo size="small" mobile={false} />
</div> </a>
); );
} }

View File

@@ -1,9 +1,21 @@
// Auth method types matching Go // Auth method types matching Go
export type AuthMethod = 'pin' | 'password' | 'oidc' | "link" export type AuthMethod = 'pin' | 'password' | 'oidc' | "link"
// Page types
export type PageType = 'auth' | 'error'
// Error data structure
export interface ErrorData {
code: number
title: string
message: string
}
// Data injected by Go templates // Data injected by Go templates
export interface Data { export interface Data {
page?: PageType
methods?: Partial<Record<AuthMethod, string>> methods?: Partial<Record<AuthMethod, string>>
error?: ErrorData
} }
declare global { declare global {

View File

@@ -2,9 +2,17 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import './index.css' import './index.css'
import App from './App.tsx' import App from './App.tsx'
import { ErrorPage } from './pages/ErrorPage.tsx'
import { getData } from '@/data'
const data = getData()
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<App /> {data.page === 'error' && data.error ? (
<ErrorPage {...data.error} />
) : (
<App />
)}
</StrictMode>, </StrictMode>,
) )

View File

@@ -0,0 +1,42 @@
import { useEffect } from "react";
import { BookText, RotateCw } from "lucide-react";
import { Title } from "@/components/Title";
import { Description } from "@/components/Description";
import { PoweredByNetBird } from "@/components/PoweredByNetBird";
import { Card } from "@/components/Card";
import Button from "@/components/Button";
import type { ErrorData } from "@/data";
export function ErrorPage({ code, title, message }: ErrorData) {
useEffect(() => {
document.title = `${title} - NetBird Service`;
}, [title]);
return (
<main className="flex flex-col items-center mt-40 px-4 max-w-xl mx-auto">
<Card className="text-center">
<div className="text-5xl font-bold text-nb-gray-200 mb-4">{code}</div>
<Title>{title}</Title>
<Description className="mt-2">{message}</Description>
<div className="mt-6 flex gap-3 justify-center">
<Button
variant="primary"
onClick={() => window.location.reload()}
>
<RotateCw size={16} />
Refresh Page
</Button>
<Button
variant="secondary"
onClick={() => window.open("https://docs.netbird.io", "_blank")}
>
<BookText size={16} />
Documentation
</Button>
</div>
</Card>
<PoweredByNetBird />
</main>
);
}

View File

@@ -37,7 +37,8 @@ func init() {
// ServeHTTP serves the web UI. For static assets it serves them directly, // ServeHTTP serves the web UI. For static assets it serves them directly,
// for other paths it renders the page with the provided data. // for other paths it renders the page with the provided data.
func ServeHTTP(w http.ResponseWriter, r *http.Request, data any) { // Optional statusCode can be passed to set a custom HTTP status code (default 200).
func ServeHTTP(w http.ResponseWriter, r *http.Request, data any, statusCode ...int) {
if initErr != nil { if initErr != nil {
http.Error(w, initErr.Error(), http.StatusInternalServerError) http.Error(w, initErr.Error(), http.StatusInternalServerError)
return return
@@ -101,5 +102,8 @@ func ServeHTTP(w http.ResponseWriter, r *http.Request, data any) {
} }
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
if len(statusCode) > 0 {
w.WriteHeader(statusCode[0])
}
w.Write(buf.Bytes()) w.Write(buf.Bytes())
} }