Initial commit
This commit is contained in:
146
internal/handlers/handlers.go
Normal file
146
internal/handlers/handlers.go
Normal file
@@ -0,0 +1,146 @@
|
||||
// Package handlers wires HTTP routes for Veola. Each file in the package owns
|
||||
// a related cluster of routes; this file holds the shared App container and
|
||||
// helper functions used across all handlers.
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"veola/internal/apify"
|
||||
"veola/internal/auth"
|
||||
"veola/internal/config"
|
||||
"veola/internal/db"
|
||||
"veola/internal/ntfy"
|
||||
"veola/internal/scheduler"
|
||||
"veola/templates"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
Cfg *config.Config
|
||||
Store *db.Store
|
||||
Auth *auth.Manager
|
||||
Apify *apify.Client
|
||||
Ntfy *ntfy.Client
|
||||
Scheduler *scheduler.Scheduler
|
||||
Preview *PreviewCache
|
||||
}
|
||||
|
||||
func New(cfg *config.Config, store *db.Store, am *auth.Manager, ap *apify.Client, nt *ntfy.Client, sc *scheduler.Scheduler) *App {
|
||||
return &App{
|
||||
Cfg: cfg, Store: store, Auth: am,
|
||||
Apify: ap, Ntfy: nt, Scheduler: sc,
|
||||
Preview: NewPreviewCache(10 * time.Minute),
|
||||
}
|
||||
}
|
||||
|
||||
// Routes returns the chi router with everything wired up.
|
||||
func (a *App) Routes() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
fs := http.FileServer(http.Dir("./static"))
|
||||
r.Handle("/static/*", http.StripPrefix("/static/", fs))
|
||||
|
||||
// All other routes pass through session loading + setup gate.
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(a.Auth.Sessions.LoadAndSave)
|
||||
r.Use(a.Auth.LoadUser)
|
||||
r.Use(a.setupGate)
|
||||
|
||||
// Public auth pages.
|
||||
r.Get("/login", a.GetLogin)
|
||||
r.With(a.Auth.CSRFProtect).Post("/login", a.PostLogin)
|
||||
r.Get("/setup", a.GetSetup)
|
||||
r.With(a.Auth.CSRFProtect).Post("/setup", a.PostSetup)
|
||||
|
||||
// Authenticated section.
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(a.Auth.RequireAuth)
|
||||
r.With(a.Auth.CSRFProtect).Post("/logout", a.PostLogout)
|
||||
|
||||
r.Get("/", a.GetDashboard)
|
||||
r.Get("/dashboard/refresh", a.GetDashboardRefresh)
|
||||
|
||||
r.Get("/items", a.GetItems)
|
||||
r.Get("/items/new", a.GetNewItem)
|
||||
r.With(a.Auth.CSRFProtect).Post("/items/preview", a.PostPreview)
|
||||
r.With(a.Auth.CSRFProtect).Post("/items", a.PostCreateItem)
|
||||
r.Get("/items/{id}/edit", a.GetEditItem)
|
||||
r.With(a.Auth.CSRFProtect).Post("/items/{id}", a.PostUpdateItem)
|
||||
r.With(a.Auth.CSRFProtect).Post("/items/{id}/toggle", a.PostToggleItem)
|
||||
r.With(a.Auth.CSRFProtect).Post("/items/{id}/delete", a.PostDeleteItem)
|
||||
r.With(a.Auth.CSRFProtect).Post("/items/{id}/run", a.PostRunItem)
|
||||
r.Get("/items/{id}/error", a.GetItemError)
|
||||
r.Get("/items/{id}/results", a.GetItemResults)
|
||||
|
||||
r.Get("/results", a.GetGlobalResults)
|
||||
|
||||
r.Get("/settings", a.GetSettings)
|
||||
r.With(a.Auth.CSRFProtect).Post("/settings", a.PostSettings)
|
||||
r.With(a.Auth.CSRFProtect).Post("/settings/password", a.PostPasswordChange)
|
||||
r.With(a.Auth.CSRFProtect).Post("/settings/test-ntfy", a.PostTestNtfy)
|
||||
r.With(a.Auth.CSRFProtect).Post("/settings/test-apify", a.PostTestApify)
|
||||
r.With(a.Auth.CSRFProtect, a.Auth.RequireAdmin).Post("/users", a.PostCreateUser)
|
||||
r.With(a.Auth.CSRFProtect, a.Auth.RequireAdmin).Post("/users/{id}/delete", a.PostDeleteUser)
|
||||
r.With(a.Auth.CSRFProtect, a.Auth.RequireAdmin).Post("/users/{id}/reset-password", a.PostResetPassword)
|
||||
})
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
// setupGate redirects every request to /setup if no users exist.
|
||||
func (a *App) setupGate(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/setup" || isStaticPath(r.URL.Path) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
n, err := a.Store.UserCount(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "db error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
http.Redirect(w, r, "/setup", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
// Once at least one user exists, /setup is a 404.
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func isStaticPath(p string) bool {
|
||||
return len(p) >= 8 && p[:8] == "/static/"
|
||||
}
|
||||
|
||||
func (a *App) page(r *http.Request, title, active string) templates.Page {
|
||||
return templates.Page{
|
||||
Title: title,
|
||||
Active: active,
|
||||
CSRFToken: a.Auth.CSRFToken(r.Context()),
|
||||
CurrentUser: auth.CurrentUserFromRequest(r),
|
||||
}
|
||||
}
|
||||
|
||||
func render(w http.ResponseWriter, r *http.Request, c templ.Component) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := c.Render(r.Context(), w); err != nil {
|
||||
slog.Error("render failed", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func parseInt64(s string) int64 {
|
||||
n, _ := strconv.ParseInt(s, 10, 64)
|
||||
return n
|
||||
}
|
||||
|
||||
func intParam(r *http.Request, key string) int64 {
|
||||
return parseInt64(chi.URLParam(r, key))
|
||||
}
|
||||
|
||||
func ctxBg() context.Context { return context.Background() }
|
||||
Reference in New Issue
Block a user