mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-19 00:36:38 +00:00
[management, reverse proxy] Add reverse proxy feature (#5291)
* implement reverse proxy --------- Co-authored-by: Alisdair MacLeod <git@alisdairmacleod.co.uk> Co-authored-by: mlsmaycon <mlsmaycon@gmail.com> Co-authored-by: Eduard Gert <kontakt@eduardgert.de> Co-authored-by: Viktor Liu <viktor@netbird.io> Co-authored-by: Diego Noguês <diego.sure@gmail.com> Co-authored-by: Diego Noguês <49420+diegocn@users.noreply.github.com> Co-authored-by: Bethuel Mmbaga <bethuelmbaga12@gmail.com> Co-authored-by: Zoltan Papp <zoltan.pmail@gmail.com> Co-authored-by: Ashley Mensah <ashleyamo982@gmail.com>
This commit is contained in:
227
proxy/web/src/App.tsx
Normal file
227
proxy/web/src/App.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import {Loader2, Lock, Binary, LogIn} from "lucide-react";
|
||||
import { getData, type Data } from "@/data";
|
||||
import Button from "@/components/Button";
|
||||
import { Input } from "@/components/Input";
|
||||
import PinCodeInput, { type PinCodeInputRef } from "@/components/PinCodeInput";
|
||||
import { SegmentedTabs } from "@/components/SegmentedTabs";
|
||||
import { PoweredByNetBird } from "@/components/PoweredByNetBird";
|
||||
import { Card } from "@/components/Card";
|
||||
import { Title } from "@/components/Title";
|
||||
import { Description } from "@/components/Description";
|
||||
import { Separator } from "@/components/Separator";
|
||||
import { ErrorMessage } from "@/components/ErrorMessage";
|
||||
import { Label } from "@/components/Label";
|
||||
|
||||
const data = getData();
|
||||
|
||||
// For testing, show all methods if none are configured
|
||||
const methods: NonNullable<Data["methods"]> =
|
||||
data.methods && Object.keys(data.methods).length > 0
|
||||
? data.methods
|
||||
: { password:"password", pin: "pin", oidc: "/auth/oidc" };
|
||||
|
||||
function App() {
|
||||
useEffect(() => {
|
||||
document.title = "Authentication Required - NetBird Service";
|
||||
}, []);
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [submitting, setSubmitting] = useState<string | null>(null);
|
||||
const [pin, setPin] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const passwordRef = useRef<HTMLInputElement>(null);
|
||||
const pinRef = useRef<PinCodeInputRef>(null);
|
||||
const [activeTab, setActiveTab] = useState<"password" | "pin">(
|
||||
methods.password ? "password" : "pin"
|
||||
);
|
||||
|
||||
const handleAuthError = (method: "password" | "pin", message: string) => {
|
||||
setError(message);
|
||||
setSubmitting(null);
|
||||
if (method === "password") {
|
||||
setPassword("");
|
||||
setTimeout(() => passwordRef.current?.focus(), 200);
|
||||
} else {
|
||||
setPin("");
|
||||
setTimeout(() => pinRef.current?.focus(), 200);
|
||||
}
|
||||
};
|
||||
|
||||
const submitCredentials = (method: "password" | "pin", value: string) => {
|
||||
setError(null);
|
||||
setSubmitting(method);
|
||||
|
||||
const formData = new FormData();
|
||||
if (method === "password") {
|
||||
formData.append(methods.password!, value);
|
||||
} else {
|
||||
formData.append(methods.pin!, value);
|
||||
}
|
||||
|
||||
fetch(globalThis.location.href, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
redirect: "manual",
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.type === "opaqueredirect" || res.status === 0) {
|
||||
setSubmitting("redirect");
|
||||
globalThis.location.reload();
|
||||
} else {
|
||||
handleAuthError(method, "Authentication failed. Please try again.");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
handleAuthError(method, "An error occurred. Please try again.");
|
||||
});
|
||||
};
|
||||
|
||||
const handlePinChange = (value: string) => {
|
||||
setPin(value);
|
||||
if (value.length === 6) {
|
||||
submitCredentials("pin", value);
|
||||
}
|
||||
};
|
||||
|
||||
const isPinComplete = pin.length === 6;
|
||||
const isPasswordEntered = password.length > 0;
|
||||
const isButtonDisabled = submitting !== null ||
|
||||
(activeTab === "password" && !isPasswordEntered) ||
|
||||
(activeTab === "pin" && !isPinComplete);
|
||||
|
||||
const hasCredentialAuth = methods.password || methods.pin;
|
||||
const hasBothCredentials = methods.password && methods.pin;
|
||||
const buttonLabel = activeTab === "password" ? "Sign in" : "Submit";
|
||||
|
||||
if (submitting === "redirect") {
|
||||
return (
|
||||
<main className="mt-20">
|
||||
<Card className="max-w-105 mx-auto">
|
||||
<Title>Authenticated</Title>
|
||||
<Description>Loading service...</Description>
|
||||
<div className="flex justify-center mt-7">
|
||||
<Loader2 className="animate-spin" size={24} />
|
||||
</div>
|
||||
</Card>
|
||||
<PoweredByNetBird />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="mt-20">
|
||||
<Card className="max-w-105 mx-auto">
|
||||
<Title>Authentication Required</Title>
|
||||
<Description>
|
||||
The service you are trying to access is protected. Please authenticate to continue.
|
||||
</Description>
|
||||
|
||||
<div className="flex flex-col gap-4 mt-7 z-10 relative">
|
||||
{error && <ErrorMessage error={error} />}
|
||||
|
||||
{/* SSO Button */}
|
||||
{methods.oidc && (
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
onClick={() => { globalThis.location.href = methods.oidc!; }}
|
||||
>
|
||||
<LogIn size={16} />
|
||||
Sign in with SSO
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Separator */}
|
||||
{methods.oidc && hasCredentialAuth && <Separator />}
|
||||
|
||||
{/* Credential Authentication */}
|
||||
{hasCredentialAuth && (
|
||||
<form onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
submitCredentials(activeTab, activeTab === "password" ? password : pin);
|
||||
}}>
|
||||
{hasBothCredentials && (
|
||||
<SegmentedTabs
|
||||
value={activeTab}
|
||||
onChange={(v) => {
|
||||
setActiveTab(v as "password" | "pin");
|
||||
setTimeout(() => {
|
||||
if (v === "password") {
|
||||
passwordRef.current?.focus();
|
||||
} else {
|
||||
pinRef.current?.focus();
|
||||
}
|
||||
}, 0);
|
||||
}}
|
||||
>
|
||||
<SegmentedTabs.List className="rounded-lg border mb-4">
|
||||
<SegmentedTabs.Trigger value="password">
|
||||
<Lock size={14} />
|
||||
Password
|
||||
</SegmentedTabs.Trigger>
|
||||
<SegmentedTabs.Trigger value="pin">
|
||||
<Binary size={14} />
|
||||
PIN
|
||||
</SegmentedTabs.Trigger>
|
||||
</SegmentedTabs.List>
|
||||
</SegmentedTabs>
|
||||
)}
|
||||
|
||||
<div className="mb-4">
|
||||
{methods.password && (activeTab === "password" || !methods.pin) && (
|
||||
<>
|
||||
{!hasBothCredentials && <Label htmlFor="password">Password</Label>}
|
||||
<Input
|
||||
ref={passwordRef}
|
||||
type="password"
|
||||
id="password"
|
||||
placeholder="Enter password"
|
||||
disabled={submitting !== null}
|
||||
showPasswordToggle
|
||||
autoFocus
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{methods.pin && (activeTab === "pin" || !methods.password) && (
|
||||
<>
|
||||
{!hasBothCredentials && <Label htmlFor="pin-0">Enter PIN Code</Label>}
|
||||
<PinCodeInput
|
||||
ref={pinRef}
|
||||
value={pin}
|
||||
onChange={handlePinChange}
|
||||
disabled={submitting !== null}
|
||||
autoFocus={!methods.password}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isButtonDisabled}
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
>
|
||||
{submitting === null ? (
|
||||
buttonLabel
|
||||
) : (
|
||||
<>
|
||||
<Loader2 className="animate-spin" size={16} />
|
||||
Verifying...
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<PoweredByNetBird />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
Reference in New Issue
Block a user