WGX shares its name with several other WireGuard tools, so the project becomes ihasvpn, alongside ihasmail. - Module github.com/Coffey-Labs/ihasvpn, command cmd/ihasvpn, image ghcr.io/coffey-labs/ihasvpn. - Environment variables move from WGX_* to IHASVPN_*. The default database is ihasvpn.db, the nftables table is `ihasvpn`, metrics are ihasvpn_*, and the session cookie and theme key are renamed, so existing sessions end. - The mark is the ihasmail cat peeking over the edge of a shield, drawn as a vector. docs/brand/generate.py builds the mark, mono mark, wordmarks, social card, favicons and app icons from that one drawing. - The console takes ihasmail's palette: the ihasmail.org teal-navy for dark, its contrast-checked light tiers with the site's light accent, received traffic in the cat's orange and sent in teal. The wordmark weight and font stack follow ihasmail.org. - Detail values wrap at spaces before breaking inside an address, so an IPv6 tunnel address no longer splits mid-number. - The README history note about the earlier WGX installer is gone with the name it explained. Screenshots retaken.
71 lines
2.8 KiB
TypeScript
71 lines
2.8 KiB
TypeScript
import { useState, type FormEvent } from "react";
|
|
import { api } from "../api";
|
|
import { errorMessage, useAuth } from "../state";
|
|
import { Field } from "../components/ui";
|
|
import { Mark } from "../components/Mark";
|
|
import { Legal } from "../components/Legal";
|
|
|
|
export function Setup() {
|
|
const { refresh } = useAuth();
|
|
const [username, setUsername] = useState("admin");
|
|
const [password, setPassword] = useState("");
|
|
const [confirm, setConfirm] = useState("");
|
|
const [endpointHost, setEndpointHost] = useState(window.location.hostname === "localhost" ? "" : window.location.hostname);
|
|
const [error, setError] = useState("");
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
async function submit(e: FormEvent) {
|
|
e.preventDefault();
|
|
setError("");
|
|
if (password !== confirm) {
|
|
setError("The passwords do not match.");
|
|
return;
|
|
}
|
|
setBusy(true);
|
|
try {
|
|
await api.setup({ username, password, endpointHost });
|
|
await refresh();
|
|
} catch (err) {
|
|
setError(errorMessage(err));
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="auth">
|
|
<form className="card" onSubmit={submit}>
|
|
<div className="card-body">
|
|
<div className="brand">
|
|
<div className="brand-mark">
|
|
<Mark size={44} />
|
|
</div>
|
|
<div className="brand-name">ihasvpn</div>
|
|
</div>
|
|
<h1>Welcome</h1>
|
|
<p className="muted" style={{ textAlign: "center", marginBottom: 16 }}>
|
|
Create the first administrator. This form only works once.
|
|
</p>
|
|
{error && <div className="error">{error}</div>}
|
|
<Field label="Username">
|
|
<input className="input" autoComplete="username" value={username} onChange={(e) => setUsername(e.target.value)} required />
|
|
</Field>
|
|
<Field label="Password" hint="At least 12 characters. Length beats complexity.">
|
|
<input className="input" type="password" autoComplete="new-password" value={password} onChange={(e) => setPassword(e.target.value)} required minLength={12} />
|
|
</Field>
|
|
<Field label="Confirm password">
|
|
<input className="input" type="password" autoComplete="new-password" value={confirm} onChange={(e) => setConfirm(e.target.value)} required />
|
|
</Field>
|
|
<Field label="Public endpoint" hint="The hostname or IP address clients will connect to. You can change it later in Settings.">
|
|
<input className="input" value={endpointHost} onChange={(e) => setEndpointHost(e.target.value)} placeholder="vpn.example.com" required />
|
|
</Field>
|
|
<button className="btn primary" type="submit" disabled={busy} style={{ width: "100%", justifyContent: "center" }}>
|
|
{busy ? "…" : "Create administrator"}
|
|
</button>
|
|
<Legal center />
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|