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.
157 lines
4.3 KiB
Markdown
157 lines
4.3 KiB
Markdown
# Server Package
|
|
|
|
This package implements the HTTP server for the member-console application, including HTMX partial handlers.
|
|
|
|
## Overview
|
|
|
|
The server uses Go's standard `net/http` package with:
|
|
- Bootstrap 5 for styling
|
|
- HTMX 2.0 for dynamic content updates
|
|
- gorilla/csrf for CSRF protection
|
|
- gorilla/sessions for session management
|
|
|
|
## Architecture
|
|
|
|
The HTTP server handles several types of requests:
|
|
|
|
- **Static Files** (`/static/*`) - CSS, JS, images served from embedded assets
|
|
- **Page Handlers** (`/`) - Full HTML page rendering
|
|
- **Partial Handlers** (`/partials/*`) - HTMX partial HTML responses
|
|
- **Auth Handlers** (`/login`, `/logout`, `/callback`) - OAuth2/OIDC authentication
|
|
|
|
Partial handlers interact with Temporal workflows for async operations like site provisioning.
|
|
|
|
## Files
|
|
|
|
- `server.go` - Main server setup, middleware chain, route registration
|
|
- `fedwiki_partials.go` - HTMX partial handlers for FedWiki site management
|
|
|
|
## HTMX Integration
|
|
|
|
### Partial Handler Pattern
|
|
|
|
Partial handlers return HTML fragments (not full pages) that HTMX swaps into the DOM:
|
|
|
|
```go
|
|
// Handler returns HTML fragment
|
|
func (h *Handler) GetSites(w http.ResponseWriter, r *http.Request) {
|
|
// ... fetch data ...
|
|
|
|
h.Templates.Render(w, "fedwiki_sites.html", data)
|
|
}
|
|
```
|
|
|
|
```html
|
|
<!-- Template returns partial HTML -->
|
|
<div class="table-responsive">
|
|
<table class="table">
|
|
{{ range .Sites }}
|
|
<tr>...</tr>
|
|
{{ end }}
|
|
</table>
|
|
</div>
|
|
```
|
|
|
|
### HTMX Triggers
|
|
|
|
Use `HX-Trigger` header to trigger events that refresh other parts of the page:
|
|
|
|
```go
|
|
// After successful operation, trigger refresh
|
|
w.Header().Set("HX-Trigger", `{"refreshSites": true}`)
|
|
```
|
|
|
|
```html
|
|
<!-- Element listens for the trigger -->
|
|
<div hx-get="/partials/fedwiki/sites"
|
|
hx-trigger="load, refreshSites from:body">
|
|
</div>
|
|
```
|
|
|
|
### CSRF with HTMX
|
|
|
|
CSRF tokens are included via `hx-headers` on forms:
|
|
|
|
```html
|
|
<form hx-post="/partials/fedwiki/sites"
|
|
hx-headers='{"X-CSRF-Token": "{{ .CSRFToken }}"}'>
|
|
```
|
|
|
|
Or globally in the page template using `hx-vals` or a meta tag approach.
|
|
|
|
## Synchronous Workflow Execution
|
|
|
|
Unlike fire-and-forget patterns, this server waits for Temporal workflows to complete:
|
|
|
|
```go
|
|
func (h *Handler) CreateSite(w http.ResponseWriter, r *http.Request) {
|
|
// 1. Start the workflow
|
|
we, err := h.TemporalClient.ExecuteWorkflow(ctx, options, workflow, input)
|
|
|
|
// 2. Wait for completion (BLOCKING)
|
|
var result WorkflowOutput
|
|
err = we.Get(ctx, &result)
|
|
|
|
// 3. Render result based on workflow outcome
|
|
if !result.Success {
|
|
h.renderError(w, result.ErrorMessage)
|
|
return
|
|
}
|
|
h.renderSuccess(w, result)
|
|
}
|
|
```
|
|
|
|
**Benefits:**
|
|
- User sees accurate success/failure immediately
|
|
- Spinner shows during actual operation
|
|
- Error messages come from workflow
|
|
|
|
**Trade-offs:**
|
|
- Request blocks until workflow completes
|
|
- Long operations may timeout (mitigated by workflow retry handling)
|
|
|
|
## Route Organization
|
|
|
|
Routes are organized by feature using handler structs:
|
|
|
|
```go
|
|
// FedWikiPartialsHandler groups related routes
|
|
type FedWikiPartialsHandler struct { ... }
|
|
|
|
func (h *FedWikiPartialsHandler) RegisterRoutes(mux *http.ServeMux) {
|
|
mux.HandleFunc("GET /partials/fedwiki/sites", h.GetSites)
|
|
mux.HandleFunc("POST /partials/fedwiki/sites", h.CreateSite)
|
|
mux.HandleFunc("DELETE /partials/fedwiki/sites/{domain}", h.DeleteSite)
|
|
// ...
|
|
}
|
|
```
|
|
|
|
## Error Rendering
|
|
|
|
Errors are rendered back into the same partial/modal that initiated the request:
|
|
|
|
```go
|
|
func (h *Handler) renderCreateFormError(w http.ResponseWriter, userID int64, errMsg string) {
|
|
// Re-fetch form data
|
|
data := h.getCreateFormData(userID)
|
|
data.Error = errMsg
|
|
|
|
// Re-render the form partial with error displayed
|
|
h.Templates.Render(w, "fedwiki_create_form.html", data)
|
|
}
|
|
```
|
|
|
|
This keeps the user in context - they see the error in the same modal/form they were using.
|
|
|
|
## Middleware Stack
|
|
|
|
```go
|
|
handler = middleware.Logging(logger)(handler) // Request logging
|
|
handler = middleware.Recover(logger)(handler) // Panic recovery
|
|
handler = middleware.Compress()(handler) // Gzip compression
|
|
handler = middleware.Decompress()(handler) // Gzip decompression
|
|
handler = middleware.SecurityHeaders()(handler) // Security headers
|
|
handler = middleware.RequestID()(handler) // Request ID generation
|
|
handler = csrf.Protect(key, opts...)(handler) // CSRF protection
|
|
```
|