Introduce SafeTemplates.Render to execute templates into a buffer and prevent partial HTML on errors. Replace direct ExecuteTemplate calls in partial handlers and add a make lint-templates target to catch bypasses. Update operator sites template/view model to use OwnerOrgName. Guard the FedWiki sync by skipping inserts when DefaultWorkspaceID is empty and scope deletes to the configured default workspace only.
97 lines
2.6 KiB
Go
97 lines
2.6 KiB
Go
package server
|
|
|
|
import (
|
|
"html/template"
|
|
"io/fs"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/auth"
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/embeds"
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/middleware"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
// OperatorHandler handles the main operator page
|
|
type OperatorHandler struct {
|
|
AuthConfig *auth.Config
|
|
Logger *slog.Logger
|
|
Templates *SafeTemplates
|
|
}
|
|
|
|
// OperatorHandlerConfig holds configuration for the operator handler
|
|
type OperatorHandlerConfig struct {
|
|
AuthConfig *auth.Config
|
|
Logger *slog.Logger
|
|
}
|
|
|
|
// NewOperatorHandler creates a new OperatorHandler
|
|
func NewOperatorHandler(cfg OperatorHandlerConfig) (*OperatorHandler, error) {
|
|
// Parse operator template
|
|
templateSubFS, err := fs.Sub(embeds.Templates, "templates")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tmpl, err := template.ParseFS(templateSubFS, "operator.html")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &OperatorHandler{
|
|
AuthConfig: cfg.AuthConfig,
|
|
Logger: cfg.Logger,
|
|
Templates: NewSafeTemplates(tmpl, cfg.Logger),
|
|
}, nil
|
|
}
|
|
|
|
// RegisterRoutes registers operator routes
|
|
func (h *OperatorHandler) RegisterRoutes(mux *http.ServeMux) {
|
|
mux.HandleFunc("GET /operator", h.requireOperatorRole(h.GetOperatorPage))
|
|
}
|
|
|
|
// requireOperatorRole is middleware that checks for the operator role
|
|
func (h *OperatorHandler) requireOperatorRole(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if !h.AuthConfig.HasRole(r, OperatorRole) {
|
|
h.Logger.Warn("operator access denied",
|
|
slog.String("path", r.URL.Path),
|
|
slog.String("reason", "missing operator-member role"))
|
|
http.Error(w, "Forbidden: operator-member role required", http.StatusForbidden)
|
|
return
|
|
}
|
|
next(w, r)
|
|
}
|
|
}
|
|
|
|
// OperatorPageData holds data for the operator page template
|
|
type OperatorPageData struct {
|
|
Name string
|
|
Username string
|
|
Email string
|
|
KeycloakAccountURL string
|
|
CSRFToken string
|
|
}
|
|
|
|
// GetOperatorPage handles GET /operator
|
|
func (h *OperatorHandler) GetOperatorPage(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
name := h.AuthConfig.GetUserName(ctx)
|
|
username := h.AuthConfig.GetUsername(ctx)
|
|
email := h.AuthConfig.GetUserEmail(ctx)
|
|
|
|
keycloakAccountURL := viper.GetString("oidc-idp-issuer-url") + "/account"
|
|
csrfToken := middleware.CSRFToken(r)
|
|
|
|
data := OperatorPageData{
|
|
Name: name,
|
|
Username: username,
|
|
Email: email,
|
|
KeycloakAccountURL: keycloakAccountURL,
|
|
CSRFToken: csrfToken,
|
|
}
|
|
|
|
h.Templates.Render(w, "operator.html", data)
|
|
}
|