Add explicit login functionality

This commit is contained in:
2026-09-13 00:50:15 +02:00
parent 74bfdbfd8a
commit 1b77cdd165
14 changed files with 629 additions and 258 deletions
+34 -23
View File
@@ -24,11 +24,13 @@ type Server struct {
tokens *auth.File
log *slog.Logger
pages map[string]*template.Template
handler http.Handler
limiter *limiter
deleteLimiter *limiter // consumed only by failed deletions
slots chan struct{} // bounds uploads in flight
pages map[string]*template.Template
handler http.Handler
limiter *limiter
// authLimiter is consumed only by failed credential attempts - a wrong
// delete token or a wrong login - so correct ones are never delayed.
authLimiter *limiter
slots chan struct{} // bounds uploads in flight
now func() time.Time // swappable in tests
}
@@ -39,15 +41,15 @@ func New(cfg *config.Config, st *store.Store, tokens *auth.File, log *slog.Logge
return nil, err
}
s := &Server{
cfg: cfg,
store: st,
tokens: tokens,
log: log,
pages: pages,
limiter: newLimiter(cfg.UploadRate, cfg.UploadBurst),
deleteLimiter: newLimiter(120, 20),
slots: make(chan struct{}, cfg.MaxConcurrent),
now: time.Now,
cfg: cfg,
store: st,
tokens: tokens,
log: log,
pages: pages,
limiter: newLimiter(cfg.UploadRate, cfg.UploadBurst),
authLimiter: newLimiter(120, 20),
slots: make(chan struct{}, cfg.MaxConcurrent),
now: time.Now,
}
s.handler = s.routes()
return s, nil
@@ -65,11 +67,14 @@ func (s *Server) routes() http.Handler {
mux.HandleFunc("GET /d/{id}", s.handleDownload)
mux.HandleFunc("GET /i/{id}", s.handleInfo)
mux.HandleFunc("POST /api/d/{id}/delete", s.handleDelete)
mux.HandleFunc("POST /api/forget", s.handleForget)
mux.HandleFunc("GET /login", s.handleLoginPage)
mux.HandleFunc("POST /login", s.handleLogin)
mux.HandleFunc("POST /logout", s.handleLogout)
mux.Handle("GET /static/", http.StripPrefix("/static/", s.staticHandler()))
mux.HandleFunc("/", s.handleNotFound)
var h http.Handler = mux
h = s.requireSameOrigin(h)
h = s.securityHeaders(h)
if s.cfg.BasePath == "/" {
@@ -153,7 +158,8 @@ func bearer(r *http.Request) string {
// --- rendering -----------------------------------------------------------
var pageNames = []string{"index.html", "result.html", "info.html", "error.html", "admin.html"}
var pageNames = []string{"index.html", "result.html", "info.html", "error.html",
"admin.html", "login.html"}
// parsePages pairs each page with the shared layout. They cannot all be parsed
// into one template set because every page defines "content".
@@ -175,17 +181,22 @@ type page struct {
Base string
Title string
Script bool
Admin bool // show the administration link in the header
// User is the logged-in token's name, empty when nobody is logged in. The
// header renders the whole session state from these two fields, so every
// page agrees about who you are without any script involved.
User string
Admin bool
}
// page builds the common fields, resolving whether the caller is an admin so
// the header can offer the link only to someone who can use it.
// page builds the common fields, resolving the session so the header can show
// who is logged in and offer only the links they can use.
func (s *Server) page(r *http.Request, title string, script bool) page {
admin := false
if lim, err := s.limitsFor(credential(r)); err == nil {
admin = lim.Admin
p := page{Base: s.cfg.BasePath, Title: title, Script: script}
if lim, err := s.limitsFor(cookieCredential(r)); err == nil {
p.User, p.Admin = lim.Name, lim.Admin
}
return page{Base: s.cfg.BasePath, Title: title, Script: script, Admin: admin}
return p
}
func (s *Server) render(w http.ResponseWriter, status int, name string, data any) {