All checks were successful
check / check (push) Successful in 1m53s
/ now checks for an authenticated session: - Authenticated users → 303 redirect to /sources - Unauthenticated users → 303 redirect to /pages/login Removes the old index page template rendering and formatUptime helper (now dead code). Adds tests for both redirect cases.
163 lines
4.9 KiB
Go
163 lines
4.9 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"html/template"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"go.uber.org/fx"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
"sneak.berlin/go/webhooker/internal/delivery"
|
|
"sneak.berlin/go/webhooker/internal/globals"
|
|
"sneak.berlin/go/webhooker/internal/healthcheck"
|
|
"sneak.berlin/go/webhooker/internal/logger"
|
|
"sneak.berlin/go/webhooker/internal/middleware"
|
|
"sneak.berlin/go/webhooker/internal/session"
|
|
"sneak.berlin/go/webhooker/templates"
|
|
)
|
|
|
|
// nolint:revive // HandlersParams is a standard fx naming convention
|
|
type HandlersParams struct {
|
|
fx.In
|
|
Logger *logger.Logger
|
|
Globals *globals.Globals
|
|
Database *database.Database
|
|
WebhookDBMgr *database.WebhookDBManager
|
|
Healthcheck *healthcheck.Healthcheck
|
|
Session *session.Session
|
|
Notifier delivery.Notifier
|
|
}
|
|
|
|
type Handlers struct {
|
|
params *HandlersParams
|
|
log *slog.Logger
|
|
hc *healthcheck.Healthcheck
|
|
db *database.Database
|
|
dbMgr *database.WebhookDBManager
|
|
session *session.Session
|
|
notifier delivery.Notifier
|
|
templates map[string]*template.Template
|
|
}
|
|
|
|
// parsePageTemplate parses a page-specific template set from the embedded FS.
|
|
// Each page template is combined with the shared base, htmlheader, and navbar templates.
|
|
// The page file must be listed first so that its root action ({{template "base" .}})
|
|
// becomes the template set's entry point. If a shared partial (e.g. htmlheader.html)
|
|
// is listed first, its {{define}} block becomes the root — which is empty — and
|
|
// Execute() produces no output.
|
|
func parsePageTemplate(pageFile string) *template.Template {
|
|
return template.Must(
|
|
template.ParseFS(templates.Templates, pageFile, "base.html", "htmlheader.html", "navbar.html"),
|
|
)
|
|
}
|
|
|
|
func New(lc fx.Lifecycle, params HandlersParams) (*Handlers, error) {
|
|
s := new(Handlers)
|
|
s.params = ¶ms
|
|
s.log = params.Logger.Get()
|
|
s.hc = params.Healthcheck
|
|
s.db = params.Database
|
|
s.dbMgr = params.WebhookDBMgr
|
|
s.session = params.Session
|
|
s.notifier = params.Notifier
|
|
|
|
// Parse all page templates once at startup
|
|
s.templates = map[string]*template.Template{
|
|
"login.html": parsePageTemplate("login.html"),
|
|
"profile.html": parsePageTemplate("profile.html"),
|
|
"sources_list.html": parsePageTemplate("sources_list.html"),
|
|
"sources_new.html": parsePageTemplate("sources_new.html"),
|
|
"source_detail.html": parsePageTemplate("source_detail.html"),
|
|
"source_edit.html": parsePageTemplate("source_edit.html"),
|
|
"source_logs.html": parsePageTemplate("source_logs.html"),
|
|
}
|
|
|
|
lc.Append(fx.Hook{
|
|
OnStart: func(ctx context.Context) error {
|
|
return nil
|
|
},
|
|
})
|
|
return s, nil
|
|
}
|
|
|
|
//nolint:unparam // r parameter will be used in the future for request context
|
|
func (s *Handlers) respondJSON(w http.ResponseWriter, r *http.Request, data interface{}, status int) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
if data != nil {
|
|
err := json.NewEncoder(w).Encode(data)
|
|
if err != nil {
|
|
s.log.Error("json encode error", "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
//nolint:unparam,unused // will be used for handling JSON requests
|
|
func (s *Handlers) decodeJSON(w http.ResponseWriter, r *http.Request, v interface{}) error {
|
|
return json.NewDecoder(r.Body).Decode(v)
|
|
}
|
|
|
|
// UserInfo represents user information for templates
|
|
type UserInfo struct {
|
|
ID string
|
|
Username string
|
|
}
|
|
|
|
// renderTemplate renders a pre-parsed template with common data
|
|
func (s *Handlers) renderTemplate(w http.ResponseWriter, r *http.Request, pageTemplate string, data interface{}) {
|
|
tmpl, ok := s.templates[pageTemplate]
|
|
if !ok {
|
|
s.log.Error("template not found", "template", pageTemplate)
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Get user from session if available
|
|
var userInfo *UserInfo
|
|
sess, err := s.session.Get(r)
|
|
if err == nil && s.session.IsAuthenticated(sess) {
|
|
if username, ok := s.session.GetUsername(sess); ok {
|
|
if userID, ok := s.session.GetUserID(sess); ok {
|
|
userInfo = &UserInfo{
|
|
ID: userID,
|
|
Username: username,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get CSRF token from request context (set by CSRF middleware)
|
|
csrfToken := middleware.CSRFToken(r)
|
|
|
|
// If data is a map, merge user info and CSRF token into it
|
|
if m, ok := data.(map[string]interface{}); ok {
|
|
m["User"] = userInfo
|
|
m["CSRFToken"] = csrfToken
|
|
if err := tmpl.Execute(w, m); err != nil {
|
|
s.log.Error("failed to execute template", "error", err)
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Wrap data with base template data
|
|
type templateDataWrapper struct {
|
|
User *UserInfo
|
|
CSRFToken string
|
|
Data interface{}
|
|
}
|
|
|
|
wrapper := templateDataWrapper{
|
|
User: userInfo,
|
|
CSRFToken: csrfToken,
|
|
Data: data,
|
|
}
|
|
|
|
if err := tmpl.Execute(w, wrapper); err != nil {
|
|
s.log.Error("failed to execute template", "error", err)
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
}
|
|
}
|