vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Build out the forge web UI
f09ef39efe44de693748053a7b596ef328fcf06b · Keenan Nemetz · 15d ago · parent 70b70264
Build out the forge web UI Turn the web surface into a compact, server-rendered forge with HTMX as the progressive dynamic layer. - Split templates and assets into embedded files; add a tokenised dark theme, Lucide icons, and vendored HTMX. - Add the repository Overview page and a separate chronological Activity feed with type filters and HTMX fragment updates. - Finish the issue and change workflows: create pages, comments, close/reopen, labels and assignees, approval, discussion, line-anchored review comments, and HTMX list filters. - Add syntax-highlighted blobs, contributor/statistics derivation, branch-aware code browsing, and a repository Runs list. - Add a loopback-only development login for local write testing, with explicit configuration and listener checks. - Extend tests for the new pages, fragments, labels, assignees, anchors, and development login.
server/web/auth_handlers.go +36
| @@ -346,7 +346,19 @@ func (w *Web) Logout(rw http.ResponseWriter, r *http.Request) { | |||
| 346 | 346 | if c, err := r.Cookie(sessionCookieName); err == nil && c.Value != "" && w.Sessions != nil { | |
| 347 | 347 | _ = w.Sessions.Revoke(c.Value) | |
| 348 | 348 | } | |
| 349 | if c, err := r.Cookie(devSessionCookieName); err == nil && c.Value != "" && w.Sessions != nil { | ||
| 350 | _ = w.Sessions.Revoke(c.Value) | ||
| 351 | } | ||
| 349 | 352 | w.clearCookie(rw, sessionCookieName) | |
| 353 | if w.devAccount != nil { | ||
| 354 | // Log out must stick even though loopback would otherwise sign us back | ||
| 355 | // in. /dev/login clears this opt-out. | ||
| 356 | w.clearCookieFor(rw, devSessionCookieName, false) | ||
| 357 | http.SetCookie(rw, &http.Cookie{ | ||
| 358 | Name: devOffCookieName, Value: "1", Path: "/", HttpOnly: true, | ||
| 359 | SameSite: http.SameSiteLaxMode, MaxAge: 3600, | ||
| 360 | }) | ||
| 361 | } | ||
| 350 | 362 | http.Redirect(rw, r, "/", http.StatusSeeOther) | |
| 351 | 363 | } | |
| 352 | 364 | ||
| @@ -462,3 +474,27 @@ func (w *Web) AddPasskeyFinish(rw http.ResponseWriter, r *http.Request) { | |||
| 462 | 474 | } | |
| 463 | 475 | writeJSON(rw, http.StatusOK, map[string]any{"ok": true}) | |
| 464 | 476 | } | |
| 477 | |||
| 478 | // DevLogin signs a loopback browser in as the account named by dev_login. It is | ||
| 479 | // a development shortcut, not a second authentication path: the listener must be | ||
| 480 | // loopback, the account is resolved at startup, and a normal instance has no | ||
| 481 | // devAccount at all. | ||
| 482 | func (w *Web) DevLogin(rw http.ResponseWriter, r *http.Request) { | ||
| 483 | if w.devAccount == nil || !isLoopbackRequest(r) { | ||
| 484 | http.NotFound(rw, r) | ||
| 485 | return | ||
| 486 | } | ||
| 487 | if w.Sessions == nil { | ||
| 488 | http.Error(rw, "sessions are unavailable", http.StatusInternalServerError) | ||
| 489 | return | ||
| 490 | } | ||
| 491 | // Visiting /dev/login is the explicit way back after a logout opted out. | ||
| 492 | w.clearCookieFor(rw, devOffCookieName, false) | ||
| 493 | sess := w.ensureDevSession() | ||
| 494 | if sess == nil { | ||
| 495 | http.Error(rw, "cannot create a development session", http.StatusInternalServerError) | ||
| 496 | return | ||
| 497 | } | ||
| 498 | w.setDevSessionCookie(rw, sess) | ||
| 499 | http.Redirect(rw, r, "/", http.StatusSeeOther) | ||
| 500 | } | ||