Move web UI from /ui/ to /web/ path prefix
The web UI is now mounted at /web/ (previously /ui/) — cmd/server/main.go
wraps web.Server.Handler with http.StripPrefix("/web", ...), so
internal/web's own routes stay unprefixed (/, /login, /logout,
/shares/..., /static/...) and only the outer mux adds the prefix. All
templates, redirects, and cookie paths updated accordingly. The root '/'
route reverts to the original unauthenticated welcome page (linking to
/web/), and /cal/, /card/, /files/ are unaffected.
Also gitignore the bin/ build output directory.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -4,7 +4,7 @@ A self-hosted CalDAV, CardDAV, and WebDAV server written in Go, backed by a
|
|||||||
filesystem store, with calendar/address book sharing grants tracked in a
|
filesystem store, with calendar/address book sharing grants tracked in a
|
||||||
small SQLite database. HTTP Basic Auth (bcrypt) with per-user isolated
|
small SQLite database. HTTP Basic Auth (bcrypt) with per-user isolated
|
||||||
collections. A small server-rendered web UI (templ + Tailwind + htmx) at
|
collections. A small server-rendered web UI (templ + Tailwind + htmx) at
|
||||||
`/ui/` lets users log in and manage their shares.
|
`/web/` lets users log in and manage their shares.
|
||||||
|
|
||||||
## Build, test, lint
|
## Build, test, lint
|
||||||
|
|
||||||
@@ -120,10 +120,13 @@ Test files: `internal/store/store_test.go`, `internal/webdav/handler_test.go`,
|
|||||||
the caldav/carddav backends query the shares tables on every request
|
the caldav/carddav backends query the shares tables on every request
|
||||||
(no caching), changes take effect immediately without restarting the
|
(no caching), changes take effect immediately without restarting the
|
||||||
server.
|
server.
|
||||||
- `internal/web` — the web UI, mounted at `/ui/` in `cmd/server/main.go`
|
- `internal/web` — the web UI, mounted at `/web/` in `cmd/server/main.go`
|
||||||
(`web.NewServer(cfg, st, dbase, logger).Handler(webstatic.FS())`),
|
(`mux.Handle("/web/", http.StripPrefix("/web", web.NewServer(cfg, st,
|
||||||
entirely separate from `internal/auth`'s Basic Auth: logins go through
|
dbase, logger).Handler(webstatic.FS())))`, so `Server.Handler`'s own
|
||||||
`/ui/login` (username/password checked against `cfg.Users` the same way
|
routes are all unprefixed — `/login`, `/`, `/shares/...` — and only the
|
||||||
|
outer mux adds the `/web` prefix), entirely separate from `internal/auth`'s
|
||||||
|
Basic Auth: logins go through
|
||||||
|
`/web/login` (username/password checked against `cfg.Users` the same way
|
||||||
Basic Auth does, via bcrypt) and issue an opaque random session token
|
Basic Auth does, via bcrypt) and issue an opaque random session token
|
||||||
stored in the `web_sessions` SQLite table (`db.CreateSession`/
|
stored in the `web_sessions` SQLite table (`db.CreateSession`/
|
||||||
`SessionUser`/`DeleteSession`, see `internal/db/sessions.go`), set as an
|
`SessionUser`/`DeleteSession`, see `internal/db/sessions.go`), set as an
|
||||||
@@ -135,7 +138,7 @@ Test files: `internal/store/store_test.go`, `internal/webdav/handler_test.go`,
|
|||||||
(`SharesOfCalendar`/`SharesOfAddressBook`) and what's shared with them
|
(`SharesOfCalendar`/`SharesOfAddressBook`) and what's shared with them
|
||||||
(`CalendarsSharedWith`/`AddressBooksSharedWith`). `internal/web/shares.go`
|
(`CalendarsSharedWith`/`AddressBooksSharedWith`). `internal/web/shares.go`
|
||||||
handles POST (create/update share) and DELETE (revoke) at
|
handles POST (create/update share) and DELETE (revoke) at
|
||||||
`/ui/shares/{calendar,addressbook}`, re-rendering just the affected
|
`/web/shares/{calendar,addressbook}`, re-rendering just the affected
|
||||||
resource card for htmx's `hx-swap="outerHTML"`; it always checks
|
resource card for htmx's `hx-swap="outerHTML"`; it always checks
|
||||||
`ownsResource` first so a user can only share resources actually
|
`ownsResource` first so a user can only share resources actually
|
||||||
configured for their own account (never someone else's, even via a
|
configured for their own account (never someone else's, even via a
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
data/
|
data/
|
||||||
config.yaml
|
config.yaml
|
||||||
web/node_modules/
|
web/node_modules/
|
||||||
|
/bin/
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ A self-hosted **CalDAV**, **CardDAV**, and **WebDAV** server written in Go.
|
|||||||
- Per-user isolated collections
|
- Per-user isolated collections
|
||||||
- **Calendar/address book sharing** — grant other users read or write
|
- **Calendar/address book sharing** — grant other users read or write
|
||||||
access to your calendars/address books
|
access to your calendars/address books
|
||||||
- **Web UI** — a small dashboard (login, manage shares) at `/ui/`, built
|
- **Web UI** — a small dashboard (login, manage shares) at `/web/`, built
|
||||||
with templ + Tailwind + htmx
|
with templ + Tailwind + htmx
|
||||||
- Auto-discovery via `/.well-known/caldav` and `/.well-known/carddav`
|
- Auto-discovery via `/.well-known/caldav` and `/.well-known/carddav`
|
||||||
- Optional **TLS** (or use a reverse proxy)
|
- Optional **TLS** (or use a reverse proxy)
|
||||||
@@ -177,17 +177,17 @@ Read-only shares reject any write (PUT/DELETE) with `403 Forbidden`.
|
|||||||
|
|
||||||
## Web UI
|
## Web UI
|
||||||
|
|
||||||
A small server-rendered dashboard is served at `/ui/` (separate from the
|
A small server-rendered dashboard is served at `/web/` (separate from the
|
||||||
DAV endpoints, which stay on HTTP Basic Auth):
|
DAV endpoints, which stay on HTTP Basic Auth):
|
||||||
|
|
||||||
- **Login** (`/ui/login`) — cookie-based session, stored server-side in
|
- **Login** (`/web/login`) — cookie-based session, stored server-side in
|
||||||
`nidus.db` (`web_sessions` table), independent of DAV Basic Auth.
|
`nidus.db` (`web_sessions` table), independent of DAV Basic Auth.
|
||||||
- **Dashboard** (`/ui/`) — lists your own calendars/address books, who
|
- **Dashboard** (`/web/`) — lists your own calendars/address books, who
|
||||||
they're shared with, and any resources other users have shared with you.
|
they're shared with, and any resources other users have shared with you.
|
||||||
- **Share management** — add/remove shares directly from the dashboard
|
- **Share management** — add/remove shares directly from the dashboard
|
||||||
(same effect as `nidusctl`); updates happen in place via
|
(same effect as `nidusctl`); updates happen in place via
|
||||||
[htmx](https://htmx.org/) without a full page reload.
|
[htmx](https://htmx.org/) without a full page reload.
|
||||||
- **Logout** (`/ui/logout`).
|
- **Logout** (`/web/logout`).
|
||||||
|
|
||||||
Implementation: [templ](https://templ.guide/) for type-safe Go HTML
|
Implementation: [templ](https://templ.guide/) for type-safe Go HTML
|
||||||
templates, [Tailwind CSS v4](https://tailwindcss.com/) for styling, and
|
templates, [Tailwind CSS v4](https://tailwindcss.com/) for styling, and
|
||||||
|
|||||||
+4
-3
@@ -137,8 +137,9 @@ func buildMux(
|
|||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
// Web UI (own cookie-based auth, not Basic Auth) — dashboard, login,
|
// Web UI (own cookie-based auth, not Basic Auth) — dashboard, login,
|
||||||
// share management.
|
// share management. Mounted at "/web/"; /cal/, /card/, /files/ keep
|
||||||
mux.Handle("/ui/", webUI.Handler(webstatic.FS()))
|
// their own dedicated prefixes.
|
||||||
|
mux.Handle("/web/", http.StripPrefix("/web", webUI.Handler(webstatic.FS())))
|
||||||
|
|
||||||
// /.well-known/ redirects for auto-discovery
|
// /.well-known/ redirects for auto-discovery
|
||||||
mux.HandleFunc("/.well-known/caldav", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/.well-known/caldav", func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -256,6 +257,6 @@ const welcomePage = `<!DOCTYPE html>
|
|||||||
<li><code>%s/.well-known/carddav</code></li>
|
<li><code>%s/.well-known/carddav</code></li>
|
||||||
</ul>
|
</ul>
|
||||||
<p><em>Authentication: HTTP Basic Auth</em></p>
|
<p><em>Authentication: HTTP Basic Auth</em></p>
|
||||||
<p><a href="/ui/">Open the web dashboard →</a></p>
|
<p><a href="/web/">Open the web dashboard →</a></p>
|
||||||
</body>
|
</body>
|
||||||
</html>`
|
</html>`
|
||||||
|
|||||||
+11
-10
@@ -29,19 +29,20 @@ func NewServer(cfg *config.Config, st *store.Store, dbase *db.DB, logger *slog.L
|
|||||||
return &Server{cfg: cfg, store: st, dbase: dbase, logger: logger}
|
return &Server{cfg: cfg, store: st, dbase: dbase, logger: logger}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler returns the http.Handler serving the web UI, mounted at "/ui/"
|
// Handler returns the http.Handler serving the web UI, mounted at "/web/"
|
||||||
// by the caller (cmd/server). staticFS serves the compiled Tailwind CSS
|
// by the caller (cmd/server). staticFS serves the compiled Tailwind CSS
|
||||||
// and any other static assets.
|
// and any other static assets. Since it's mounted with a path prefix,
|
||||||
|
// the caller must wrap this handler in http.StripPrefix("/web", ...).
|
||||||
func (s *Server) Handler(staticFS http.FileSystem) http.Handler {
|
func (s *Server) Handler(staticFS http.FileSystem) http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
mux.Handle("/ui/static/", http.StripPrefix("/ui/static/", http.FileServer(staticFS)))
|
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(staticFS)))
|
||||||
|
|
||||||
mux.HandleFunc("/ui/login", s.handleLogin)
|
mux.HandleFunc("/login", s.handleLogin)
|
||||||
mux.HandleFunc("/ui/logout", s.handleLogout)
|
mux.HandleFunc("/logout", s.handleLogout)
|
||||||
mux.HandleFunc("/ui/", s.requireLogin(s.handleDashboard))
|
mux.HandleFunc("/", s.requireLogin(s.handleDashboard))
|
||||||
mux.HandleFunc("/ui/shares/calendar", s.requireLogin(s.handleCalendarShare))
|
mux.HandleFunc("/shares/calendar", s.requireLogin(s.handleCalendarShare))
|
||||||
mux.HandleFunc("/ui/shares/addressbook", s.requireLogin(s.handleAddressBookShare))
|
mux.HandleFunc("/shares/addressbook", s.requireLogin(s.handleAddressBookShare))
|
||||||
|
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
@@ -88,7 +89,7 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.setSessionCookie(w, token)
|
s.setSessionCookie(w, token)
|
||||||
http.Redirect(w, r, "/ui/", http.StatusSeeOther)
|
http.Redirect(w, r, "/web/", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -96,5 +97,5 @@ func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
|
|||||||
_ = s.dbase.DeleteSession(cookie.Value)
|
_ = s.dbase.DeleteSession(cookie.Value)
|
||||||
}
|
}
|
||||||
s.clearSessionCookie(w)
|
s.clearSessionCookie(w)
|
||||||
http.Redirect(w, r, "/ui/login", http.StatusSeeOther)
|
http.Redirect(w, r, "/web/login", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-10
@@ -51,7 +51,7 @@ func newTestServer(t *testing.T) *Server {
|
|||||||
func loginAs(t *testing.T, handler http.Handler, username, password string) *http.Cookie {
|
func loginAs(t *testing.T, handler http.Handler, username, password string) *http.Cookie {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
form := url.Values{"username": {username}, "password": {password}}
|
form := url.Values{"username": {username}, "password": {password}}
|
||||||
req := httptest.NewRequest(http.MethodPost, "/ui/login", strings.NewReader(form.Encode()))
|
req := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(rr, req)
|
handler.ServeHTTP(rr, req)
|
||||||
@@ -80,7 +80,7 @@ func TestLoginSuccessAndFailure(t *testing.T) {
|
|||||||
|
|
||||||
// Wrong password.
|
// Wrong password.
|
||||||
form := url.Values{"username": {"alice"}, "password": {"wrong"}}
|
form := url.Values{"username": {"alice"}, "password": {"wrong"}}
|
||||||
req := httptest.NewRequest(http.MethodPost, "/ui/login", strings.NewReader(form.Encode()))
|
req := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(rr, req)
|
handler.ServeHTTP(rr, req)
|
||||||
@@ -96,15 +96,15 @@ func TestDashboardRequiresLogin(t *testing.T) {
|
|||||||
s := newTestServer(t)
|
s := newTestServer(t)
|
||||||
handler := s.Handler(emptyStaticFS{})
|
handler := s.Handler(emptyStaticFS{})
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/ui/", nil)
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(rr, req)
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
if rr.Code != http.StatusSeeOther {
|
if rr.Code != http.StatusSeeOther {
|
||||||
t.Fatalf("expected redirect to login, got %d", rr.Code)
|
t.Fatalf("expected redirect to login, got %d", rr.Code)
|
||||||
}
|
}
|
||||||
if loc := rr.Result().Header.Get("Location"); loc != "/ui/login" {
|
if loc := rr.Result().Header.Get("Location"); loc != "/web/login" {
|
||||||
t.Fatalf("expected redirect to /ui/login, got %q", loc)
|
t.Fatalf("expected redirect to /web/login, got %q", loc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ func TestDashboardShowsOwnResources(t *testing.T) {
|
|||||||
handler := s.Handler(emptyStaticFS{})
|
handler := s.Handler(emptyStaticFS{})
|
||||||
cookie := loginAs(t, handler, "alice", "password")
|
cookie := loginAs(t, handler, "alice", "password")
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/ui/", nil)
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
req.AddCookie(cookie)
|
req.AddCookie(cookie)
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(rr, req)
|
handler.ServeHTTP(rr, req)
|
||||||
@@ -134,7 +134,7 @@ func TestShareUnshareCalendarFlow(t *testing.T) {
|
|||||||
|
|
||||||
// Share alice's "work" calendar with bob, write access.
|
// Share alice's "work" calendar with bob, write access.
|
||||||
form := url.Values{"resource": {"work"}, "shared_with": {"bob"}, "permission": {"write"}}
|
form := url.Values{"resource": {"work"}, "shared_with": {"bob"}, "permission": {"write"}}
|
||||||
req := httptest.NewRequest(http.MethodPost, "/ui/shares/calendar", strings.NewReader(form.Encode()))
|
req := httptest.NewRequest(http.MethodPost, "/shares/calendar", strings.NewReader(form.Encode()))
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
req.AddCookie(cookie)
|
req.AddCookie(cookie)
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
@@ -155,7 +155,7 @@ func TestShareUnshareCalendarFlow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Unshare — htmx v2 sends DELETE params as a URL query string.
|
// Unshare — htmx v2 sends DELETE params as a URL query string.
|
||||||
req = httptest.NewRequest(http.MethodDelete, "/ui/shares/calendar?resource=work&shared_with=bob", nil)
|
req = httptest.NewRequest(http.MethodDelete, "/shares/calendar?resource=work&shared_with=bob", nil)
|
||||||
req.AddCookie(cookie)
|
req.AddCookie(cookie)
|
||||||
rr = httptest.NewRecorder()
|
rr = httptest.NewRecorder()
|
||||||
handler.ServeHTTP(rr, req)
|
handler.ServeHTTP(rr, req)
|
||||||
@@ -182,7 +182,7 @@ func TestCannotShareResourceNotOwned(t *testing.T) {
|
|||||||
|
|
||||||
// alice doesn't own "personal" (that's bob's calendar).
|
// alice doesn't own "personal" (that's bob's calendar).
|
||||||
form := url.Values{"resource": {"personal"}, "shared_with": {"bob"}, "permission": {"write"}}
|
form := url.Values{"resource": {"personal"}, "shared_with": {"bob"}, "permission": {"write"}}
|
||||||
req := httptest.NewRequest(http.MethodPost, "/ui/shares/calendar", strings.NewReader(form.Encode()))
|
req := httptest.NewRequest(http.MethodPost, "/shares/calendar", strings.NewReader(form.Encode()))
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
req.AddCookie(cookie)
|
req.AddCookie(cookie)
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
@@ -198,7 +198,7 @@ func TestLogoutClearsSession(t *testing.T) {
|
|||||||
handler := s.Handler(emptyStaticFS{})
|
handler := s.Handler(emptyStaticFS{})
|
||||||
cookie := loginAs(t, handler, "alice", "password")
|
cookie := loginAs(t, handler, "alice", "password")
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/ui/logout", nil)
|
req := httptest.NewRequest(http.MethodGet, "/logout", nil)
|
||||||
req.AddCookie(cookie)
|
req.AddCookie(cookie)
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(rr, req)
|
handler.ServeHTTP(rr, req)
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ func (s *Server) requireLogin(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
cookie, err := r.Cookie(sessionCookieName)
|
cookie, err := r.Cookie(sessionCookieName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Redirect(w, r, "/ui/login", http.StatusSeeOther)
|
http.Redirect(w, r, "/web/login", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
username, err := s.dbase.SessionUser(cookie.Value)
|
username, err := s.dbase.SessionUser(cookie.Value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.clearSessionCookie(w)
|
s.clearSessionCookie(w)
|
||||||
http.Redirect(w, r, "/ui/login", http.StatusSeeOther)
|
http.Redirect(w, r, "/web/login", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx := context.WithValue(r.Context(), userCtxKey, username)
|
ctx := context.WithValue(r.Context(), userCtxKey, username)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
// handleCalendarShare handles POST (create/update share) and DELETE
|
// handleCalendarShare handles POST (create/update share) and DELETE
|
||||||
// (revoke share) for the current user's calendars, mounted at
|
// (revoke share) for the current user's calendars, mounted at
|
||||||
// /ui/shares/calendar. htmx sends the resource + shared_with (+ permission
|
// /shares/calendar. htmx sends the resource + shared_with (+ permission
|
||||||
// for POST) as form values and expects the updated resource card HTML
|
// for POST) as form values and expects the updated resource card HTML
|
||||||
// back for an out-of-band swap.
|
// back for an out-of-band swap.
|
||||||
func (s *Server) handleCalendarShare(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleCalendarShare(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -113,9 +113,9 @@ templ ResourceCardView(r ResourceCard) {
|
|||||||
|
|
||||||
func shareEndpoint(kind string) string {
|
func shareEndpoint(kind string) string {
|
||||||
if kind == "calendar" {
|
if kind == "calendar" {
|
||||||
return "/ui/shares/calendar"
|
return "/web/shares/calendar"
|
||||||
}
|
}
|
||||||
return "/ui/shares/addressbook"
|
return "/web/shares/addressbook"
|
||||||
}
|
}
|
||||||
|
|
||||||
func shareVals(resource, sharedWith string) string {
|
func shareVals(resource, sharedWith string) string {
|
||||||
|
|||||||
@@ -361,9 +361,9 @@ func ResourceCardView(r ResourceCard) templ.Component {
|
|||||||
|
|
||||||
func shareEndpoint(kind string) string {
|
func shareEndpoint(kind string) string {
|
||||||
if kind == "calendar" {
|
if kind == "calendar" {
|
||||||
return "/ui/shares/calendar"
|
return "/web/shares/calendar"
|
||||||
}
|
}
|
||||||
return "/ui/shares/addressbook"
|
return "/web/shares/addressbook"
|
||||||
}
|
}
|
||||||
|
|
||||||
func shareVals(resource, sharedWith string) string {
|
func shareVals(resource, sharedWith string) string {
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ templ Layout(title string, username string) {
|
|||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>{ title } · nidus</title>
|
<title>{ title } · nidus</title>
|
||||||
<link rel="stylesheet" href="/ui/static/app.css"/>
|
<link rel="stylesheet" href="/web/static/app.css"/>
|
||||||
<script src="/ui/static/htmx.min.js" defer></script>
|
<script src="/web/static/htmx.min.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="h-full text-gray-900">
|
<body class="h-full text-gray-900">
|
||||||
<nav class="bg-white border-b border-gray-200">
|
<nav class="bg-white border-b border-gray-200">
|
||||||
<div class="max-w-4xl mx-auto px-4 py-3 flex items-center justify-between">
|
<div class="max-w-4xl mx-auto px-4 py-3 flex items-center justify-between">
|
||||||
<a href="/ui/" class="font-semibold text-lg tracking-tight">nidus</a>
|
<a href="/web/" class="font-semibold text-lg tracking-tight">nidus</a>
|
||||||
if username != "" {
|
if username != "" {
|
||||||
<div class="flex items-center gap-4 text-sm text-gray-600">
|
<div class="flex items-center gap-4 text-sm text-gray-600">
|
||||||
<span>{ username }</span>
|
<span>{ username }</span>
|
||||||
<a href="/ui/logout" class="text-red-600 hover:underline">Logout</a>
|
<a href="/web/logout" class="text-red-600 hover:underline">Logout</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func Layout(title string, username 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, " · nidus</title><link rel=\"stylesheet\" href=\"/ui/static/app.css\"><script src=\"/ui/static/htmx.min.js\" defer></script></head><body class=\"h-full text-gray-900\"><nav class=\"bg-white border-b border-gray-200\"><div class=\"max-w-4xl mx-auto px-4 py-3 flex items-center justify-between\"><a href=\"/ui/\" class=\"font-semibold text-lg tracking-tight\">nidus</a> ")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " · nidus</title><link rel=\"stylesheet\" href=\"/web/static/app.css\"><script src=\"/web/static/htmx.min.js\" defer></script></head><body class=\"h-full text-gray-900\"><nav class=\"bg-white border-b border-gray-200\"><div class=\"max-w-4xl mx-auto px-4 py-3 flex items-center justify-between\"><a href=\"/web/\" class=\"font-semibold text-lg tracking-tight\">nidus</a> ")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ func Layout(title string, username 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, 4, "</span> <a href=\"/ui/logout\" class=\"text-red-600 hover:underline\">Logout</a></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</span> <a href=\"/web/logout\" class=\"text-red-600 hover:underline\">Logout</a></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ templ Login(errorMsg string) {
|
|||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>Login · nidus</title>
|
<title>Login · nidus</title>
|
||||||
<link rel="stylesheet" href="/ui/static/app.css"/>
|
<link rel="stylesheet" href="/web/static/app.css"/>
|
||||||
</head>
|
</head>
|
||||||
<body class="h-full flex items-center justify-center">
|
<body class="h-full flex items-center justify-center">
|
||||||
<div class="w-full max-w-sm bg-white p-8 rounded-lg shadow-sm border border-gray-200">
|
<div class="w-full max-w-sm bg-white p-8 rounded-lg shadow-sm border border-gray-200">
|
||||||
@@ -15,7 +15,7 @@ templ Login(errorMsg string) {
|
|||||||
if errorMsg != "" {
|
if errorMsg != "" {
|
||||||
<p class="mb-4 text-sm text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2">{ errorMsg }</p>
|
<p class="mb-4 text-sm text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2">{ errorMsg }</p>
|
||||||
}
|
}
|
||||||
<form method="POST" action="/ui/login" class="space-y-4">
|
<form method="POST" action="/web/login" class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="username" class="block text-sm font-medium text-gray-700">Username</label>
|
<label for="username" class="block text-sm font-medium text-gray-700">Username</label>
|
||||||
<input id="username" name="username" type="text" required autofocus
|
<input id="username" name="username" type="text" required autofocus
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func Login(errorMsg string) templ.Component {
|
|||||||
templ_7745c5c3_Var1 = templ.NopComponent
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" class=\"h-full bg-gray-50\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Login · nidus</title><link rel=\"stylesheet\" href=\"/ui/static/app.css\"></head><body class=\"h-full flex items-center justify-center\"><div class=\"w-full max-w-sm bg-white p-8 rounded-lg shadow-sm border border-gray-200\"><h1 class=\"text-xl font-semibold mb-6 text-center\">nidus</h1>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" class=\"h-full bg-gray-50\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Login · nidus</title><link rel=\"stylesheet\" href=\"/web/static/app.css\"></head><body class=\"h-full flex items-center justify-center\"><div class=\"w-full max-w-sm bg-white p-8 rounded-lg shadow-sm border border-gray-200\"><h1 class=\"text-xl font-semibold mb-6 text-center\">nidus</h1>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ func Login(errorMsg string) templ.Component {
|
|||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<form method=\"POST\" action=\"/ui/login\" class=\"space-y-4\"><div><label for=\"username\" class=\"block text-sm font-medium text-gray-700\">Username</label> <input id=\"username\" name=\"username\" type=\"text\" required autofocus class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500\"></div><div><label for=\"password\" class=\"block text-sm font-medium text-gray-700\">Password</label> <input id=\"password\" name=\"password\" type=\"password\" required class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500\"></div><button type=\"submit\" class=\"w-full bg-indigo-600 text-white rounded-md py-2 font-medium hover:bg-indigo-700\">Sign in</button></form></div></body></html>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<form method=\"POST\" action=\"/web/login\" class=\"space-y-4\"><div><label for=\"username\" class=\"block text-sm font-medium text-gray-700\">Username</label> <input id=\"username\" name=\"username\" type=\"text\" required autofocus class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500\"></div><div><label for=\"password\" class=\"block text-sm font-medium text-gray-700\">Password</label> <input id=\"password\" name=\"password\" type=\"password\" required class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500\"></div><button type=\"submit\" class=\"w-full bg-indigo-600 text-white rounded-md py-2 font-medium hover:bg-indigo-700\">Sign in</button></form></div></body></html>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user