Can't log in from a fresh LXC deploy, and the service is LAN-only by
design. Rip out the whole bcrypt-password / signed-cookie session
layer: internal/auth, login templates, gen-admin-password binary +
Makefile targets, auth config block, login/logout routes and the
RequireSession middleware wrap. Agent bearer-token auth on
/api/v1/runs/{id}/* is untouched.
Operators who want a password can front the service with a reverse
proxy — noted in README and docs/operations.md.
This commit is contained in:
@@ -28,14 +28,6 @@ agent: ## Build agent for host OS (handy for unit testing only — real agent ru
|
|||||||
agent-linux: ## Cross-build agent for linux-amd64 (consumed by live-image build)
|
agent-linux: ## Cross-build agent for linux-amd64 (consumed by live-image build)
|
||||||
$(GOOS_LINUX) go build -ldflags="$(LDFLAGS)" -o bin/vetting-agent.linux-amd64 ./cmd/vetting-agent
|
$(GOOS_LINUX) go build -ldflags="$(LDFLAGS)" -o bin/vetting-agent.linux-amd64 ./cmd/vetting-agent
|
||||||
|
|
||||||
.PHONY: gen-admin-password
|
|
||||||
gen-admin-password: ## Build the bcrypt password generator
|
|
||||||
go build -o bin/gen-admin-password$(if $(filter Windows%,$(UNAME_S)),.exe,) ./tools/gen-admin-password
|
|
||||||
|
|
||||||
.PHONY: gen-admin-password-linux
|
|
||||||
gen-admin-password-linux: ## Cross-build the bcrypt password generator for linux-amd64
|
|
||||||
$(GOOS_LINUX) go build -ldflags="$(LDFLAGS)" -o bin/gen-admin-password-linux-amd64 ./tools/gen-admin-password
|
|
||||||
|
|
||||||
.PHONY: tidy
|
.PHONY: tidy
|
||||||
tidy: ## go mod tidy
|
tidy: ## go mod tidy
|
||||||
go mod tidy
|
go mod tidy
|
||||||
@@ -68,7 +60,7 @@ endif
|
|||||||
$(MAKE) -C live-image all
|
$(MAKE) -C live-image all
|
||||||
|
|
||||||
.PHONY: all
|
.PHONY: all
|
||||||
all: orchestrator agent gen-admin-password ## Build everything buildable on host OS
|
all: orchestrator agent ## Build everything buildable on host OS
|
||||||
|
|
||||||
.PHONY: run
|
.PHONY: run
|
||||||
run: orchestrator ## Build and run orchestrator with example config
|
run: orchestrator ## Build and run orchestrator with example config
|
||||||
|
|||||||
@@ -22,20 +22,16 @@ notifications.
|
|||||||
## Quick start (local, against QEMU)
|
## Quick start (local, against QEMU)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Build
|
|
||||||
make all
|
make all
|
||||||
|
|
||||||
# 2. Generate an admin password hash and paste it into the config.
|
|
||||||
./bin/gen-admin-password 'your-password'
|
|
||||||
# Edit deploy/vetting.example.yaml:
|
|
||||||
# auth.admin_password_bcrypt = <that hash>
|
|
||||||
# auth.session_secret_hex = $(openssl rand -hex 32)
|
|
||||||
|
|
||||||
# 3. Run
|
|
||||||
./bin/vetting --config deploy/vetting.example.yaml
|
./bin/vetting --config deploy/vetting.example.yaml
|
||||||
# → http://localhost:8080
|
# → http://localhost:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The UI has no built-in auth — bind to loopback or LAN only, or front
|
||||||
|
the service with a reverse proxy (Caddy/nginx basic-auth) if you
|
||||||
|
want a password. The agent↔orchestrator channel keeps its own
|
||||||
|
bearer-token auth and is unaffected.
|
||||||
|
|
||||||
For a full end-to-end QEMU walk-through (bridge setup, host registration,
|
For a full end-to-end QEMU walk-through (bridge setup, host registration,
|
||||||
PXE boot), see [docs/operations.md § First vetting run](docs/operations.md#first-vetting-run).
|
PXE boot), see [docs/operations.md § First vetting run](docs/operations.md#first-vetting-run).
|
||||||
|
|
||||||
@@ -53,7 +49,7 @@ which lays down the binary, systemd unit, example config, and
|
|||||||
`vetting` service user. Then:
|
`vetting` service user. Then:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Edit /etc/vetting/vetting.yaml (bcrypt password, session secret, public URL)
|
# Edit /etc/vetting/vetting.yaml (server.bind + server.public_url)
|
||||||
sudo systemctl enable --now vetting
|
sudo systemctl enable --now vetting
|
||||||
journalctl -fu vetting
|
journalctl -fu vetting
|
||||||
```
|
```
|
||||||
@@ -80,7 +76,7 @@ live-image/ mkosi config for the PXE-bootable Debian live image
|
|||||||
deploy/ systemd unit + install.sh + example config
|
deploy/ systemd unit + install.sh + example config
|
||||||
docs/ operator + developer docs
|
docs/ operator + developer docs
|
||||||
test/e2e/ build-tag-gated QEMU + PXE full-stack test
|
test/e2e/ build-tag-gated QEMU + PXE full-stack test
|
||||||
tools/ small CLI helpers (e.g. gen-admin-password)
|
tools/ small CLI helpers
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"vetting/internal/api"
|
"vetting/internal/api"
|
||||||
"vetting/internal/auth"
|
|
||||||
"vetting/internal/config"
|
"vetting/internal/config"
|
||||||
"vetting/internal/db"
|
"vetting/internal/db"
|
||||||
"vetting/internal/events"
|
"vetting/internal/events"
|
||||||
@@ -54,19 +53,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer func() { _ = conn.Close() }()
|
defer func() { _ = conn.Close() }()
|
||||||
|
|
||||||
secret, err := cfg.Auth.SessionSecret()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("auth: %v", err)
|
|
||||||
}
|
|
||||||
authMgr := &auth.Manager{
|
|
||||||
PasswordHash: cfg.Auth.AdminPasswordBcrypt,
|
|
||||||
Secret: secret,
|
|
||||||
TTL: time.Duration(cfg.Auth.SessionTTLHours) * time.Hour,
|
|
||||||
}
|
|
||||||
if err := validateAuth(cfg, authMgr); err != nil {
|
|
||||||
log.Fatalf("auth: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hostStore := &store.Hosts{DB: conn}
|
hostStore := &store.Hosts{DB: conn}
|
||||||
runStore := &store.Runs{DB: conn}
|
runStore := &store.Runs{DB: conn}
|
||||||
stageStore := &store.Stages{DB: conn}
|
stageStore := &store.Stages{DB: conn}
|
||||||
@@ -113,7 +99,6 @@ func main() {
|
|||||||
Hosts: hostStore,
|
Hosts: hostStore,
|
||||||
Runs: runStore,
|
Runs: runStore,
|
||||||
Artifacts: artifactStore,
|
Artifacts: artifactStore,
|
||||||
Auth: authMgr,
|
|
||||||
EventHub: hub,
|
EventHub: hub,
|
||||||
Runner: runner,
|
Runner: runner,
|
||||||
Tiles: tiles,
|
Tiles: tiles,
|
||||||
@@ -163,7 +148,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
router := httpserver.NewRouter(httpserver.Deps{
|
router := httpserver.NewRouter(httpserver.Deps{
|
||||||
Auth: authMgr,
|
|
||||||
UI: ui,
|
UI: ui,
|
||||||
Agent: agentAPI,
|
Agent: agentAPI,
|
||||||
LiveDir: cfg.PXE.LiveDir,
|
LiveDir: cfg.PXE.LiveDir,
|
||||||
@@ -231,19 +215,3 @@ func main() {
|
|||||||
}
|
}
|
||||||
_ = hub.Shutdown(ctx)
|
_ = hub.Shutdown(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateAuth(cfg *config.Config, _ *auth.Manager) error {
|
|
||||||
if cfg.Auth.AdminPasswordBcrypt == "" || cfg.Auth.AdminPasswordBcrypt == "$2a$10$REPLACE_ME_WITH_A_REAL_BCRYPT_HASH_0123456789abcdefABCDEFxx" {
|
|
||||||
return errPlaceholderPassword
|
|
||||||
}
|
|
||||||
if len(cfg.Auth.AdminPasswordBcrypt) < 4 || cfg.Auth.AdminPasswordBcrypt[0] != '$' {
|
|
||||||
return errPlaceholderPassword
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var errPlaceholderPassword = plainErr("auth.admin_password_bcrypt is the placeholder; run bin/gen-admin-password and paste the hash into your config")
|
|
||||||
|
|
||||||
type plainErr string
|
|
||||||
|
|
||||||
func (e plainErr) Error() string { return string(e) }
|
|
||||||
|
|||||||
+10
-19
@@ -7,9 +7,9 @@
|
|||||||
# 2. Creates the `vetting` system user with /var/lib/vetting homedir.
|
# 2. Creates the `vetting` system user with /var/lib/vetting homedir.
|
||||||
# 3. Copies the pre-built `vetting` binary into /usr/local/bin.
|
# 3. Copies the pre-built `vetting` binary into /usr/local/bin.
|
||||||
# 4. Drops the systemd unit and example config into /etc/vetting.
|
# 4. Drops the systemd unit and example config into /etc/vetting.
|
||||||
# 5. Reminds the operator to edit the config and set a bcrypt
|
# 5. Reminds the operator to edit the config before enabling
|
||||||
# password before enabling the service — we don't auto-start
|
# the service — we don't auto-start because the default bind
|
||||||
# because a placeholder password would just refuse to boot.
|
# is loopback-only and needs at least a tweak to be useful.
|
||||||
#
|
#
|
||||||
# What it deliberately does NOT do:
|
# What it deliberately does NOT do:
|
||||||
# - Build the orchestrator (this script assumes you ran
|
# - Build the orchestrator (this script assumes you ran
|
||||||
@@ -95,20 +95,6 @@ install -d -m 0755 "${CONFIG_DIR}"
|
|||||||
echo "==> installing binary"
|
echo "==> installing binary"
|
||||||
install -m 0755 "${BINARY}" /usr/local/bin/vetting
|
install -m 0755 "${BINARY}" /usr/local/bin/vetting
|
||||||
|
|
||||||
# Install the bcrypt password generator too if we can find it — the
|
|
||||||
# operator needs it to fill in auth.admin_password_bcrypt.
|
|
||||||
GEN_PW=""
|
|
||||||
for cand in \
|
|
||||||
"${REPO_ROOT}/bin/gen-admin-password-linux-amd64" \
|
|
||||||
"${REPO_ROOT}/bin/gen-admin-password" \
|
|
||||||
"${SCRIPT_DIR}/gen-admin-password"; do
|
|
||||||
if [[ -x "${cand}" ]]; then GEN_PW="${cand}"; break; fi
|
|
||||||
done
|
|
||||||
if [[ -n "${GEN_PW}" ]]; then
|
|
||||||
echo "==> installing gen-admin-password"
|
|
||||||
install -m 0755 "${GEN_PW}" /usr/local/bin/gen-admin-password
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> installing config and systemd unit"
|
echo "==> installing config and systemd unit"
|
||||||
# vetting.production.yaml uses absolute /var/lib/vetting + /var/log/vetting
|
# vetting.production.yaml uses absolute /var/lib/vetting + /var/log/vetting
|
||||||
# paths that match the systemd unit's ReadWritePaths. vetting.example.yaml
|
# paths that match the systemd unit's ReadWritePaths. vetting.example.yaml
|
||||||
@@ -140,8 +126,9 @@ vetting is installed but not yet enabled.
|
|||||||
|
|
||||||
Next steps:
|
Next steps:
|
||||||
1. Edit ${CONFIG_DIR}/vetting.yaml and set:
|
1. Edit ${CONFIG_DIR}/vetting.yaml and set:
|
||||||
- auth.admin_password_bcrypt (run: gen-admin-password 'YOURPW')
|
- server.bind (127.0.0.1:8080 by default; switch to
|
||||||
- auth.session_secret_hex (run: openssl rand -hex 32)
|
0.0.0.0:8080 once you're ready to expose
|
||||||
|
it on the LAN)
|
||||||
- server.public_url (the URL you'll browse to)
|
- server.public_url (the URL you'll browse to)
|
||||||
- pxe.* if you want PXE boot support
|
- pxe.* if you want PXE boot support
|
||||||
- notifiers + routes (optional)
|
- notifiers + routes (optional)
|
||||||
@@ -150,4 +137,8 @@ Next steps:
|
|||||||
3. Watch the logs:
|
3. Watch the logs:
|
||||||
journalctl -fu vetting
|
journalctl -fu vetting
|
||||||
|
|
||||||
|
The UI has no built-in auth — it trusts the LAN. If you need a
|
||||||
|
password, front the service with a reverse proxy (Caddy/nginx
|
||||||
|
basic-auth) instead.
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -65,9 +65,9 @@ fi
|
|||||||
echo "==> installing templ ${TEMPL_VERSION}"
|
echo "==> installing templ ${TEMPL_VERSION}"
|
||||||
GOBIN=/usr/local/bin go install "github.com/a-h/templ/cmd/templ@${TEMPL_VERSION}"
|
GOBIN=/usr/local/bin go install "github.com/a-h/templ/cmd/templ@${TEMPL_VERSION}"
|
||||||
|
|
||||||
echo "==> building orchestrator + gen-admin-password"
|
echo "==> building orchestrator (make orchestrator-linux)"
|
||||||
cd "${SRC_DIR}"
|
cd "${SRC_DIR}"
|
||||||
make orchestrator-linux gen-admin-password-linux
|
make orchestrator-linux
|
||||||
|
|
||||||
echo "==> running deploy/install.sh"
|
echo "==> running deploy/install.sh"
|
||||||
bash deploy/install.sh --binary "bin/vetting-linux-amd64"
|
bash deploy/install.sh --binary "bin/vetting-linux-amd64"
|
||||||
|
|||||||
@@ -28,15 +28,6 @@ janitor:
|
|||||||
# Interval between cleanup sweeps. 0 defaults to 60.
|
# Interval between cleanup sweeps. 0 defaults to 60.
|
||||||
interval_minutes: 60
|
interval_minutes: 60
|
||||||
|
|
||||||
auth:
|
|
||||||
# bcrypt hash of your admin password.
|
|
||||||
# Generate via: ./bin/gen-admin-password "your-password"
|
|
||||||
admin_password_bcrypt: "$2a$10$REPLACE_ME_WITH_A_REAL_BCRYPT_HASH_0123456789abcdefABCDEFxx"
|
|
||||||
# Random 32-byte hex string used to sign session cookies.
|
|
||||||
# Generate via: openssl rand -hex 32 (or use PowerShell equivalent)
|
|
||||||
session_secret_hex: "0000000000000000000000000000000000000000000000000000000000000000"
|
|
||||||
session_ttl_hours: 24
|
|
||||||
|
|
||||||
dispatcher:
|
dispatcher:
|
||||||
max_concurrent_runs: 3
|
max_concurrent_runs: 3
|
||||||
|
|
||||||
|
|||||||
@@ -28,15 +28,6 @@ janitor:
|
|||||||
# Interval between cleanup sweeps. 0 defaults to 60.
|
# Interval between cleanup sweeps. 0 defaults to 60.
|
||||||
interval_minutes: 60
|
interval_minutes: 60
|
||||||
|
|
||||||
auth:
|
|
||||||
# bcrypt hash of your admin password.
|
|
||||||
# Generate via: gen-admin-password 'your-password'
|
|
||||||
admin_password_bcrypt: "$2a$10$REPLACE_ME_WITH_A_REAL_BCRYPT_HASH_0123456789abcdefABCDEFxx"
|
|
||||||
# Random 32-byte hex string used to sign session cookies.
|
|
||||||
# Generate via: openssl rand -hex 32
|
|
||||||
session_secret_hex: "0000000000000000000000000000000000000000000000000000000000000000"
|
|
||||||
session_ttl_hours: 24
|
|
||||||
|
|
||||||
dispatcher:
|
dispatcher:
|
||||||
max_concurrent_runs: 3
|
max_concurrent_runs: 3
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ Operator browser (HTMX + SSE, admin login)
|
|||||||
| `internal/api` | HTTP handlers: `agent_handlers.go` (the agent-facing API) and `ui_handlers.go` (HTMX fragments + SSE). |
|
| `internal/api` | HTTP handlers: `agent_handlers.go` (the agent-facing API) and `ui_handlers.go` (HTMX fragments + SSE). |
|
||||||
| `internal/httpserver` | chi router assembly — lives here to avoid `api ↔ orchestrator` cyclic imports. |
|
| `internal/httpserver` | chi router assembly — lives here to avoid `api ↔ orchestrator` cyclic imports. |
|
||||||
| `internal/web` | Embedded static assets + compiled Templ templates. |
|
| `internal/web` | Embedded static assets + compiled Templ templates. |
|
||||||
| `internal/auth` | Single-admin bcrypt + signed-cookie sessions. |
|
|
||||||
| `internal/pxe` | dnsmasq subprocess supervisor + per-MAC iPXE script generator. |
|
| `internal/pxe` | dnsmasq subprocess supervisor + per-MAC iPXE script generator. |
|
||||||
| `internal/events` | In-process SSE hub (fan-out to live browser clients). |
|
| `internal/events` | In-process SSE hub (fan-out to live browser clients). |
|
||||||
| `internal/logs` | Per-run flat-file writer + SSE fan-out of live log tail. |
|
| `internal/logs` | Per-run flat-file writer + SSE fan-out of live log tail. |
|
||||||
|
|||||||
+19
-18
@@ -37,25 +37,18 @@ repaired nodes so DHCP and WoL work.
|
|||||||
- disables the distro-default dnsmasq (the orchestrator supervises
|
- disables the distro-default dnsmasq (the orchestrator supervises
|
||||||
its own)
|
its own)
|
||||||
|
|
||||||
The installer does **not** enable the service, because the default
|
The installer does **not** enable the service. You'll want to edit
|
||||||
config has a placeholder bcrypt password that the binary refuses to
|
the config first.
|
||||||
start with.
|
|
||||||
|
|
||||||
3. Generate an admin password hash and a session secret, then edit
|
3. Edit `/etc/vetting/vetting.yaml`:
|
||||||
`/etc/vetting/vetting.yaml`:
|
|
||||||
|
|
||||||
```
|
- `server.bind` — defaults to `127.0.0.1:8080`. Switch to
|
||||||
./bin/gen-admin-password 'your-password-here' # prints a bcrypt hash
|
`0.0.0.0:8080` (or bind to a specific LAN IP) once you're ready
|
||||||
openssl rand -hex 32 # prints a 64-char hex string
|
to expose it. There is no built-in auth — see *Exposing outside
|
||||||
```
|
the LAN* below.
|
||||||
|
|
||||||
Required fields:
|
|
||||||
- `auth.admin_password_bcrypt` — the bcrypt hash
|
|
||||||
- `auth.session_secret_hex` — the 32-byte hex string
|
|
||||||
- `server.public_url` — the URL your browser hits the LXC on
|
- `server.public_url` — the URL your browser hits the LXC on
|
||||||
(e.g. `https://vetting.lan:8443`). This is used as the
|
(e.g. `http://vetting.lan:8080`). Used as the click-through link
|
||||||
click-through link in notifications, so it must be the *external*
|
in notifications.
|
||||||
URL, not the bind address.
|
|
||||||
|
|
||||||
4. (Optional) Configure notifiers in the same file — see the
|
4. (Optional) Configure notifiers in the same file — see the
|
||||||
commented-out example block for ntfy / Discord / SMTP.
|
commented-out example block for ntfy / Discord / SMTP.
|
||||||
@@ -79,7 +72,7 @@ Against a QEMU VM first, before you point it at real hardware:
|
|||||||
sudo ip link set br-vetting up
|
sudo ip link set br-vetting up
|
||||||
```
|
```
|
||||||
|
|
||||||
2. In the UI at `https://<lxc>:8443`, log in and register a host:
|
2. In the UI at `http://<lxc>:8080`, register a host:
|
||||||
- Name: `qemu-test`
|
- Name: `qemu-test`
|
||||||
- MAC: `52:54:00:12:34:56`
|
- MAC: `52:54:00:12:34:56`
|
||||||
- WoL broadcast IP: `10.77.0.255`
|
- WoL broadcast IP: `10.77.0.255`
|
||||||
@@ -145,11 +138,19 @@ Retention is governed by the `artifacts.retention_days` and
|
|||||||
`logs.retention_days` settings. DB rows (run history) are preserved
|
`logs.retention_days` settings. DB rows (run history) are preserved
|
||||||
indefinitely; only on-disk files get pruned.
|
indefinitely; only on-disk files get pruned.
|
||||||
|
|
||||||
|
## Exposing outside the LAN
|
||||||
|
|
||||||
|
The orchestrator UI has no built-in auth. It's designed to live on a
|
||||||
|
trusted home LAN and trust whatever reaches it. If you want to reach
|
||||||
|
it from outside that LAN, don't expose the bind port directly — put
|
||||||
|
it behind a reverse proxy (Caddy, nginx, Traefik) that terminates TLS
|
||||||
|
and adds basic-auth or OIDC. The agent↔orchestrator bearer token
|
||||||
|
auth is independent and keeps working either way.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
| Symptom | First check |
|
| Symptom | First check |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Service refuses to start with `auth.admin_password_bcrypt is the placeholder` | You didn't replace the bcrypt hash in the config. Run `gen-admin-password`. |
|
|
||||||
| PXE client gets no DHCP offer | `journalctl -u vetting` for dnsmasq errors; confirm the LXC has `CAP_NET_ADMIN` (the shipped systemd unit does); confirm the host MAC is actually registered (`sqlite3 /var/lib/vetting/vetting.db 'SELECT name, mac FROM hosts;'`). |
|
| PXE client gets no DHCP offer | `journalctl -u vetting` for dnsmasq errors; confirm the LXC has `CAP_NET_ADMIN` (the shipped systemd unit does); confirm the host MAC is actually registered (`sqlite3 /var/lib/vetting/vetting.db 'SELECT name, mac FROM hosts;'`). |
|
||||||
| Agent `/hello` never fires | Check the live image is actually loading the agent binary — SSH into the live env (use the hold key path), `systemctl status vetting-agent`. |
|
| Agent `/hello` never fires | Check the live image is actually loading the agent binary — SSH into the live env (use the hold key path), `systemctl status vetting-agent`. |
|
||||||
| Tile stuck on `Booting` | Most likely the live image booted but the agent can't reach the orchestrator. Verify `vetting.orchestrator=` in the kernel cmdline resolves from the host's network. |
|
| Tile stuck on `Booting` | Most likely the live image booted but the agent can't reach the orchestrator. Verify `vetting.orchestrator=` in the kernel cmdline resolves from the host's network. |
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"vetting/internal/auth"
|
|
||||||
"vetting/internal/events"
|
"vetting/internal/events"
|
||||||
"vetting/internal/model"
|
"vetting/internal/model"
|
||||||
"vetting/internal/orchestrator"
|
"vetting/internal/orchestrator"
|
||||||
@@ -23,7 +22,6 @@ type UI struct {
|
|||||||
Hosts *store.Hosts
|
Hosts *store.Hosts
|
||||||
Runs *store.Runs
|
Runs *store.Runs
|
||||||
Artifacts *store.Artifacts
|
Artifacts *store.Artifacts
|
||||||
Auth *auth.Manager
|
|
||||||
EventHub *events.Hub
|
EventHub *events.Hub
|
||||||
Runner *orchestrator.Runner
|
Runner *orchestrator.Runner
|
||||||
Tiles *TileEnricher
|
Tiles *TileEnricher
|
||||||
@@ -93,38 +91,6 @@ func (u *UI) StartRun(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UI) LoginForm(w http.ResponseWriter, r *http.Request) {
|
|
||||||
next := r.URL.Query().Get("next")
|
|
||||||
if next == "" {
|
|
||||||
next = "/"
|
|
||||||
}
|
|
||||||
_ = templates.Login("", next).Render(r.Context(), w)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UI) LoginSubmit(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if err := r.ParseForm(); err != nil {
|
|
||||||
http.Error(w, "bad form", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
password := r.PostForm.Get("password")
|
|
||||||
next := r.PostForm.Get("next")
|
|
||||||
if next == "" || !strings.HasPrefix(next, "/") {
|
|
||||||
next = "/"
|
|
||||||
}
|
|
||||||
if !u.Auth.VerifyPassword(password) {
|
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
_ = templates.Login("Invalid password.", next).Render(r.Context(), w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
u.Auth.Issue(w, r)
|
|
||||||
http.Redirect(w, r, next, http.StatusSeeOther)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UI) Logout(w http.ResponseWriter, r *http.Request) {
|
|
||||||
u.Auth.Clear(w)
|
|
||||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UI) NewHostForm(w http.ResponseWriter, r *http.Request) {
|
func (u *UI) NewHostForm(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = templates.Registration(templates.RegistrationForm{}).Render(r.Context(), w)
|
_ = templates.Registration(templates.RegistrationForm{}).Render(r.Context(), w)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RequireSession redirects unauthenticated requests to /login.
|
|
||||||
func (m *Manager) RequireSession(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if err := m.Validate(r); err != nil {
|
|
||||||
if acceptsHTML(r) {
|
|
||||||
http.Redirect(w, r, "/login?next="+r.URL.RequestURI(), http.StatusSeeOther)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func acceptsHTML(r *http.Request) bool {
|
|
||||||
accept := r.Header.Get("Accept")
|
|
||||||
if accept == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
for _, part := range splitComma(accept) {
|
|
||||||
if part == "text/html" || part == "*/*" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func splitComma(s string) []string {
|
|
||||||
var out []string
|
|
||||||
start := 0
|
|
||||||
for i := 0; i < len(s); i++ {
|
|
||||||
if s[i] == ',' {
|
|
||||||
out = append(out, trimSpace(s[start:i]))
|
|
||||||
start = i + 1
|
|
||||||
} else if s[i] == ';' {
|
|
||||||
out = append(out, trimSpace(s[start:i]))
|
|
||||||
for i < len(s) && s[i] != ',' {
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
start = i + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if start < len(s) {
|
|
||||||
out = append(out, trimSpace(s[start:]))
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func trimSpace(s string) string {
|
|
||||||
for len(s) > 0 && (s[0] == ' ' || s[0] == '\t') {
|
|
||||||
s = s[1:]
|
|
||||||
}
|
|
||||||
for len(s) > 0 && (s[len(s)-1] == ' ' || s[len(s)-1] == '\t') {
|
|
||||||
s = s[:len(s)-1]
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
)
|
|
||||||
|
|
||||||
const cookieName = "vetting_session"
|
|
||||||
|
|
||||||
type Manager struct {
|
|
||||||
PasswordHash string
|
|
||||||
Secret []byte
|
|
||||||
TTL time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) VerifyPassword(password string) bool {
|
|
||||||
if m.PasswordHash == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return bcrypt.CompareHashAndPassword([]byte(m.PasswordHash), []byte(password)) == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Issue writes a signed session cookie valid for m.TTL.
|
|
||||||
func (m *Manager) Issue(w http.ResponseWriter, r *http.Request) {
|
|
||||||
expiry := time.Now().Add(m.TTL).Unix()
|
|
||||||
payload := strconv.FormatInt(expiry, 10)
|
|
||||||
sig := m.sign(payload)
|
|
||||||
value := payload + "." + sig
|
|
||||||
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: cookieName,
|
|
||||||
Value: value,
|
|
||||||
Path: "/",
|
|
||||||
HttpOnly: true,
|
|
||||||
Secure: r.TLS != nil,
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
Expires: time.Unix(expiry, 0),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Clear(w http.ResponseWriter) {
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: cookieName,
|
|
||||||
Value: "",
|
|
||||||
Path: "/",
|
|
||||||
HttpOnly: true,
|
|
||||||
MaxAge: -1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var errInvalidSession = errors.New("invalid session")
|
|
||||||
|
|
||||||
// Validate returns nil if the request's cookie is present, signed, and not expired.
|
|
||||||
func (m *Manager) Validate(r *http.Request) error {
|
|
||||||
c, err := r.Cookie(cookieName)
|
|
||||||
if err != nil {
|
|
||||||
return errInvalidSession
|
|
||||||
}
|
|
||||||
parts := strings.SplitN(c.Value, ".", 2)
|
|
||||||
if len(parts) != 2 {
|
|
||||||
return errInvalidSession
|
|
||||||
}
|
|
||||||
payload, sig := parts[0], parts[1]
|
|
||||||
expected := m.sign(payload)
|
|
||||||
if !hmac.Equal([]byte(sig), []byte(expected)) {
|
|
||||||
return errInvalidSession
|
|
||||||
}
|
|
||||||
expiry, err := strconv.ParseInt(payload, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return errInvalidSession
|
|
||||||
}
|
|
||||||
if time.Now().Unix() >= expiry {
|
|
||||||
return errInvalidSession
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) sign(payload string) string {
|
|
||||||
mac := hmac.New(sha256.New, m.Secret)
|
|
||||||
_, _ = mac.Write([]byte(payload))
|
|
||||||
return base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
// BcryptHash is a helper used by the gen-admin-password tool.
|
|
||||||
func BcryptHash(password string) (string, error) {
|
|
||||||
b, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("bcrypt: %w", err)
|
|
||||||
}
|
|
||||||
return string(b), nil
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
@@ -13,7 +12,6 @@ type Config struct {
|
|||||||
Database Database `yaml:"database"`
|
Database Database `yaml:"database"`
|
||||||
Artifacts Artifacts `yaml:"artifacts"`
|
Artifacts Artifacts `yaml:"artifacts"`
|
||||||
Logs Logs `yaml:"logs"`
|
Logs Logs `yaml:"logs"`
|
||||||
Auth Auth `yaml:"auth"`
|
|
||||||
Dispatcher Dispatcher `yaml:"dispatcher"`
|
Dispatcher Dispatcher `yaml:"dispatcher"`
|
||||||
Janitor Janitor `yaml:"janitor"`
|
Janitor Janitor `yaml:"janitor"`
|
||||||
PXE PXE `yaml:"pxe"`
|
PXE PXE `yaml:"pxe"`
|
||||||
@@ -52,23 +50,6 @@ type Janitor struct {
|
|||||||
IntervalMinutes int `yaml:"interval_minutes"` // 0 = 60
|
IntervalMinutes int `yaml:"interval_minutes"` // 0 = 60
|
||||||
}
|
}
|
||||||
|
|
||||||
type Auth struct {
|
|
||||||
AdminPasswordBcrypt string `yaml:"admin_password_bcrypt"`
|
|
||||||
SessionSecretHex string `yaml:"session_secret_hex"`
|
|
||||||
SessionTTLHours int `yaml:"session_ttl_hours"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a Auth) SessionSecret() ([]byte, error) {
|
|
||||||
b, err := hex.DecodeString(a.SessionSecretHex)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("session_secret_hex: %w", err)
|
|
||||||
}
|
|
||||||
if len(b) < 32 {
|
|
||||||
return nil, fmt.Errorf("session_secret_hex must decode to at least 32 bytes, got %d", len(b))
|
|
||||||
}
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Dispatcher struct {
|
type Dispatcher struct {
|
||||||
MaxConcurrentRuns int `yaml:"max_concurrent_runs"`
|
MaxConcurrentRuns int `yaml:"max_concurrent_runs"`
|
||||||
}
|
}
|
||||||
@@ -132,9 +113,6 @@ func Load(path string) (*Config, error) {
|
|||||||
if c.Logs.Dir == "" {
|
if c.Logs.Dir == "" {
|
||||||
c.Logs.Dir = "./var/logs"
|
c.Logs.Dir = "./var/logs"
|
||||||
}
|
}
|
||||||
if c.Auth.SessionTTLHours == 0 {
|
|
||||||
c.Auth.SessionTTLHours = 24
|
|
||||||
}
|
|
||||||
if c.Dispatcher.MaxConcurrentRuns == 0 {
|
if c.Dispatcher.MaxConcurrentRuns == 0 {
|
||||||
c.Dispatcher.MaxConcurrentRuns = 3
|
c.Dispatcher.MaxConcurrentRuns = 3
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,10 @@ import (
|
|||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
|
|
||||||
"vetting/internal/api"
|
"vetting/internal/api"
|
||||||
"vetting/internal/auth"
|
|
||||||
"vetting/internal/web"
|
"vetting/internal/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Deps struct {
|
type Deps struct {
|
||||||
Auth *auth.Manager
|
|
||||||
UI *api.UI
|
UI *api.UI
|
||||||
Agent *api.Agent
|
Agent *api.Agent
|
||||||
LiveDir string // directory containing vmlinuz + initrd.img; "" disables /live
|
LiveDir string // directory containing vmlinuz + initrd.img; "" disables /live
|
||||||
@@ -38,13 +36,8 @@ func NewRouter(d Deps) http.Handler {
|
|||||||
r.Handle("/live/*", http.StripPrefix("/live/", http.FileServer(http.Dir(d.LiveDir))))
|
r.Handle("/live/*", http.StripPrefix("/live/", http.FileServer(http.Dir(d.LiveDir))))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public (no session required) endpoints.
|
|
||||||
r.Get("/login", d.UI.LoginForm)
|
|
||||||
r.Post("/login", d.UI.LoginSubmit)
|
|
||||||
r.Post("/logout", d.UI.Logout)
|
|
||||||
|
|
||||||
// Agent / PXE endpoints — authenticated per-request by bearer token
|
// Agent / PXE endpoints — authenticated per-request by bearer token
|
||||||
// or by the unforgeable MAC path parameter, never by the UI session.
|
// or by the unforgeable MAC path parameter.
|
||||||
r.Get("/ipxe/{mac}", d.Agent.IPXEScript)
|
r.Get("/ipxe/{mac}", d.Agent.IPXEScript)
|
||||||
r.Route("/api/v1/runs/{id}", func(r chi.Router) {
|
r.Route("/api/v1/runs/{id}", func(r chi.Router) {
|
||||||
r.Post("/hello", d.Agent.Hello)
|
r.Post("/hello", d.Agent.Hello)
|
||||||
@@ -56,20 +49,16 @@ func NewRouter(d Deps) http.Handler {
|
|||||||
r.Post("/sensor", d.Agent.Sensor)
|
r.Post("/sensor", d.Agent.Sensor)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Session-gated browser UI.
|
// Browser UI — no auth; bind to loopback or LAN only, or front
|
||||||
r.Group(func(r chi.Router) {
|
// with a reverse proxy if you need a password.
|
||||||
r.Use(d.Auth.RequireSession)
|
r.Get("/", d.UI.Dashboard)
|
||||||
|
r.Get("/hosts/new", d.UI.NewHostForm)
|
||||||
r.Get("/", d.UI.Dashboard)
|
r.Post("/hosts", d.UI.CreateHost)
|
||||||
r.Get("/hosts/new", d.UI.NewHostForm)
|
r.Post("/hosts/{id}/delete", d.UI.DeleteHost)
|
||||||
r.Post("/hosts", d.UI.CreateHost)
|
r.Post("/hosts/{id}/start", d.UI.StartRun)
|
||||||
r.Post("/hosts/{id}/delete", d.UI.DeleteHost)
|
r.Post("/hosts/{id}/override-wipe", d.UI.OverrideWipeStorage)
|
||||||
r.Post("/hosts/{id}/start", d.UI.StartRun)
|
r.Get("/reports/{runID}", d.UI.Report)
|
||||||
r.Post("/hosts/{id}/override-wipe", d.UI.OverrideWipeStorage)
|
r.Get("/events", d.UI.SSE)
|
||||||
r.Get("/reports/{runID}", d.UI.Report)
|
|
||||||
|
|
||||||
r.Get("/events", d.UI.SSE)
|
|
||||||
})
|
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,6 @@ templ Layout(title string) {
|
|||||||
</nav>
|
</nav>
|
||||||
<div class="session">
|
<div class="session">
|
||||||
<span class="heartbeat" hx-ext="sse" sse-connect="/events" sse-swap="heartbeat">·</span>
|
<span class="heartbeat" hx-ext="sse" sse-connect="/events" sse-swap="heartbeat">·</span>
|
||||||
<form method="post" action="/logout" class="logout-form">
|
|
||||||
<button type="submit">Log out</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func Layout(title string) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " — Vetting</title><link rel=\"stylesheet\" href=\"/static/app.css\"><script src=\"https://unpkg.com/htmx.org@2.0.2\" integrity=\"sha384-Y7hw+L/jvKeWIRRkqWYfPcvVxHzVzn5REgzbawhxAuQGwX1XWe70vji+VSeHOThJ\" crossorigin=\"anonymous\"></script><script src=\"https://unpkg.com/htmx-ext-sse@2.2.2\" integrity=\"sha384-Y4gc0CK6Kg4hmulDc1rNM+vbMvjbW/5rRCA6pC5gj5dLV1/4+OZGkQpJtHQvQTCr\" crossorigin=\"anonymous\"></script></head><body hx-boost=\"true\"><header class=\"topbar\"><div class=\"brand\">Vetting</div><nav><a href=\"/\">Dashboard</a> <a href=\"/hosts/new\">Register host</a></nav><div class=\"session\"><span class=\"heartbeat\" hx-ext=\"sse\" sse-connect=\"/events\" sse-swap=\"heartbeat\">·</span><form method=\"post\" action=\"/logout\" class=\"logout-form\"><button type=\"submit\">Log out</button></form></div></header><main>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " — Vetting</title><link rel=\"stylesheet\" href=\"/static/app.css\"><script src=\"https://unpkg.com/htmx.org@2.0.2\" integrity=\"sha384-Y7hw+L/jvKeWIRRkqWYfPcvVxHzVzn5REgzbawhxAuQGwX1XWe70vji+VSeHOThJ\" crossorigin=\"anonymous\"></script><script src=\"https://unpkg.com/htmx-ext-sse@2.2.2\" integrity=\"sha384-Y4gc0CK6Kg4hmulDc1rNM+vbMvjbW/5rRCA6pC5gj5dLV1/4+OZGkQpJtHQvQTCr\" crossorigin=\"anonymous\"></script></head><body hx-boost=\"true\"><header class=\"topbar\"><div class=\"brand\">Vetting</div><nav><a href=\"/\">Dashboard</a> <a href=\"/hosts/new\">Register host</a></nav><div class=\"session\"><span class=\"heartbeat\" hx-ext=\"sse\" sse-connect=\"/events\" sse-swap=\"heartbeat\">·</span></div></header><main>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ func BareLayout(title string) templ.Component {
|
|||||||
var templ_7745c5c3_Var4 string
|
var templ_7745c5c3_Var4 string
|
||||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/layout.templ`, Line: 41, Col: 17}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/layout.templ`, Line: 38, Col: 17}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
package templates
|
|
||||||
|
|
||||||
templ Login(errMsg, next string) {
|
|
||||||
@BareLayout("Sign in") {
|
|
||||||
<div class="login-card">
|
|
||||||
<h1>Vetting</h1>
|
|
||||||
if errMsg != "" {
|
|
||||||
<div class="error">{ errMsg }</div>
|
|
||||||
}
|
|
||||||
<form method="post" action="/login">
|
|
||||||
<input type="hidden" name="next" value={ next }/>
|
|
||||||
<label>
|
|
||||||
Password
|
|
||||||
<input type="password" name="password" autofocus required/>
|
|
||||||
</label>
|
|
||||||
<button type="submit">Sign in</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.1001
|
|
||||||
package templates
|
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
|
||||||
|
|
||||||
import "github.com/a-h/templ"
|
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
|
||||||
|
|
||||||
func Login(errMsg, next string) templ.Component {
|
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
|
||||||
return templ_7745c5c3_CtxErr
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
|
||||||
if !templ_7745c5c3_IsBuffer {
|
|
||||||
defer func() {
|
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err == nil {
|
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
ctx = templ.InitializeContext(ctx)
|
|
||||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
|
||||||
if templ_7745c5c3_Var1 == nil {
|
|
||||||
templ_7745c5c3_Var1 = templ.NopComponent
|
|
||||||
}
|
|
||||||
ctx = templ.ClearChildren(ctx)
|
|
||||||
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
|
||||||
if !templ_7745c5c3_IsBuffer {
|
|
||||||
defer func() {
|
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err == nil {
|
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
ctx = templ.InitializeContext(ctx)
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"login-card\"><h1>Vetting</h1>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if errMsg != "" {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"error\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var3 string
|
|
||||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(errMsg)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/login.templ`, Line: 8, Col: 31}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<form method=\"post\" action=\"/login\"><input type=\"hidden\" name=\"next\" value=\"")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var4 string
|
|
||||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(next)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/login.templ`, Line: 11, Col: 49}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"> <label>Password <input type=\"password\" name=\"password\" autofocus required></label> <button type=\"submit\">Sign in</button></form></div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
templ_7745c5c3_Err = BareLayout("Sign in").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ = templruntime.GeneratedTemplate
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"vetting/internal/auth"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if len(os.Args) != 2 {
|
|
||||||
fmt.Fprintln(os.Stderr, "usage: gen-admin-password <plaintext>")
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
hash, err := auth.BcryptHash(os.Args[1])
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
fmt.Println(hash)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user