473 lines
15 KiB
Go
473 lines
15 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"html/template"
|
|
"io/fs"
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/auth"
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/db"
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/embeds"
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/workflows/fedwiki"
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/workflows/queues"
|
|
"github.com/google/uuid"
|
|
"go.temporal.io/sdk/client"
|
|
)
|
|
|
|
// FedWikiPartialsHandler handles HTMX partial requests for FedWiki management.
|
|
type FedWikiPartialsHandler struct {
|
|
DB db.Querier
|
|
Logger *slog.Logger
|
|
TemporalClient client.Client
|
|
AuthConfig *auth.Config
|
|
FedWikiAllowedDomains []string // Domains where users can create sites
|
|
FedWikiSiteScheme string // http or https
|
|
SupportURL string
|
|
Templates *template.Template
|
|
}
|
|
|
|
// FedWikiPartialsConfig holds configuration for the FedWiki partials handler.
|
|
type FedWikiPartialsConfig struct {
|
|
DB db.Querier
|
|
Logger *slog.Logger
|
|
TemporalClient client.Client
|
|
AuthConfig *auth.Config
|
|
FedWikiAllowedDomains []string
|
|
FedWikiSiteScheme string
|
|
SupportURL string
|
|
}
|
|
|
|
// NewFedWikiPartialsHandler creates a new FedWikiPartialsHandler.
|
|
func NewFedWikiPartialsHandler(cfg FedWikiPartialsConfig) (*FedWikiPartialsHandler, error) {
|
|
// Parse partial templates
|
|
templateSubFS, err := fs.Sub(embeds.Templates, "templates/partials")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tmpl, err := template.ParseFS(templateSubFS, "*.html")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &FedWikiPartialsHandler{
|
|
DB: cfg.DB,
|
|
Logger: cfg.Logger,
|
|
TemporalClient: cfg.TemporalClient,
|
|
AuthConfig: cfg.AuthConfig,
|
|
FedWikiAllowedDomains: cfg.FedWikiAllowedDomains,
|
|
FedWikiSiteScheme: cfg.FedWikiSiteScheme,
|
|
SupportURL: cfg.SupportURL,
|
|
Templates: tmpl,
|
|
}, nil
|
|
}
|
|
|
|
// RegisterRoutes registers all HTMX partial routes.
|
|
func (h *FedWikiPartialsHandler) RegisterRoutes(mux *http.ServeMux) {
|
|
mux.HandleFunc("GET /partials/fedwiki/sites", h.GetSites)
|
|
mux.HandleFunc("GET /partials/fedwiki/create-form", h.GetCreateForm)
|
|
mux.HandleFunc("POST /partials/fedwiki/sites", h.CreateSite)
|
|
mux.HandleFunc("GET /partials/fedwiki/delete-confirm/{domain}", h.GetDeleteConfirm)
|
|
mux.HandleFunc("DELETE /partials/fedwiki/sites/{domain}", h.DeleteSite)
|
|
}
|
|
|
|
// getUserIDFromSession extracts the user's database ID from the session.
|
|
func (h *FedWikiPartialsHandler) getUserIDFromSession(r *http.Request) (int64, error) {
|
|
session, err := h.AuthConfig.Store.Get(r, h.AuthConfig.SessionName)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
userID, ok := session.Values["user_db_id"].(int64)
|
|
if !ok {
|
|
return 0, err
|
|
}
|
|
|
|
return userID, nil
|
|
}
|
|
|
|
// SiteViewModel represents a site for template rendering.
|
|
type SiteViewModel struct {
|
|
ID int64
|
|
Domain string // Full domain as stored in database
|
|
IsCustomDomain bool
|
|
CreatedAt string
|
|
URL string
|
|
}
|
|
|
|
// SitesData holds data for the sites list partial.
|
|
type SitesData struct {
|
|
Sites []SiteViewModel
|
|
CurrentCount int64
|
|
Quota int64
|
|
IsMember bool
|
|
CanCreate bool
|
|
Error string
|
|
}
|
|
|
|
// GetSites handles GET /partials/fedwiki/sites
|
|
func (h *FedWikiPartialsHandler) GetSites(w http.ResponseWriter, r *http.Request) {
|
|
userID, err := h.getUserIDFromSession(r)
|
|
if err != nil {
|
|
h.Logger.Error("failed to get user ID from session", slog.Any("error", err))
|
|
h.renderError(w, "fedwiki_sites.html", "Unauthorized")
|
|
return
|
|
}
|
|
|
|
// Get sites
|
|
sites, err := h.DB.GetSitesByUserID(r.Context(), userID)
|
|
if err != nil {
|
|
h.Logger.Error("failed to get user sites", slog.Any("error", err))
|
|
h.renderError(w, "fedwiki_sites.html", "Failed to retrieve sites")
|
|
return
|
|
}
|
|
|
|
// Get quota info
|
|
count, err := h.DB.GetSiteCountByUserID(r.Context(), userID)
|
|
if err != nil {
|
|
h.Logger.Error("failed to get site count", slog.Any("error", err))
|
|
h.renderError(w, "fedwiki_sites.html", "Failed to retrieve quota")
|
|
return
|
|
}
|
|
|
|
membership, err := h.DB.GetUserMembership(r.Context(), userID)
|
|
if err != nil {
|
|
h.Logger.Error("failed to get membership", slog.Any("error", err))
|
|
h.renderError(w, "fedwiki_sites.html", "Failed to retrieve membership")
|
|
return
|
|
}
|
|
|
|
// Build view models
|
|
siteVMs := make([]SiteViewModel, len(sites))
|
|
for i, site := range sites {
|
|
isCustom := site.IsCustomDomain != 0
|
|
siteVMs[i] = SiteViewModel{
|
|
ID: site.ID,
|
|
Domain: site.Domain,
|
|
IsCustomDomain: isCustom,
|
|
CreatedAt: site.CreatedAt.Format("Jan 2, 2006"),
|
|
URL: h.buildSiteURL(site.Domain, isCustom),
|
|
}
|
|
}
|
|
|
|
data := SitesData{
|
|
Sites: siteVMs,
|
|
CurrentCount: count,
|
|
Quota: membership.SitesQuota,
|
|
IsMember: membership.IsMember != 0,
|
|
CanCreate: count < membership.SitesQuota,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html")
|
|
if err := h.Templates.ExecuteTemplate(w, "fedwiki_sites.html", data); err != nil {
|
|
h.Logger.Error("template error", slog.Any("error", err))
|
|
}
|
|
}
|
|
|
|
// CreateFormData holds data for the create form partial.
|
|
type CreateFormData struct {
|
|
AllowedDomains []string // All domains where users can create sites
|
|
CanCreate bool
|
|
CurrentCount int64
|
|
Quota int64
|
|
IsMember bool
|
|
Error string
|
|
}
|
|
|
|
// GetCreateForm handles GET /partials/fedwiki/create-form
|
|
func (h *FedWikiPartialsHandler) GetCreateForm(w http.ResponseWriter, r *http.Request) {
|
|
userID, err := h.getUserIDFromSession(r)
|
|
if err != nil {
|
|
h.Logger.Error("failed to get user ID from session", slog.Any("error", err))
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Get quota info
|
|
count, err := h.DB.GetSiteCountByUserID(r.Context(), userID)
|
|
if err != nil {
|
|
h.Logger.Error("failed to get site count", slog.Any("error", err))
|
|
}
|
|
|
|
membership, err := h.DB.GetUserMembership(r.Context(), userID)
|
|
if err != nil {
|
|
h.Logger.Error("failed to get membership", slog.Any("error", err))
|
|
}
|
|
|
|
data := CreateFormData{
|
|
AllowedDomains: h.FedWikiAllowedDomains,
|
|
CanCreate: count < membership.SitesQuota,
|
|
CurrentCount: count,
|
|
Quota: membership.SitesQuota,
|
|
IsMember: membership.IsMember != 0,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html")
|
|
if err := h.Templates.ExecuteTemplate(w, "fedwiki_create_form.html", data); err != nil {
|
|
h.Logger.Error("template error", slog.Any("error", err))
|
|
}
|
|
}
|
|
|
|
// CreateSite handles POST /partials/fedwiki/sites
|
|
func (h *FedWikiPartialsHandler) CreateSite(w http.ResponseWriter, r *http.Request) {
|
|
userID, err := h.getUserIDFromSession(r)
|
|
if err != nil {
|
|
h.Logger.Error("failed to get user ID from session", slog.Any("error", err))
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Parse form
|
|
if err := r.ParseForm(); err != nil {
|
|
h.renderCreateFormError(w, userID, "Invalid form data")
|
|
return
|
|
}
|
|
|
|
domain := strings.TrimSpace(strings.ToLower(r.FormValue("domain")))
|
|
selectedDomain := strings.TrimSpace(r.FormValue("selectedDomain"))
|
|
isCustomDomain := r.FormValue("isCustomDomain") == "true"
|
|
|
|
if domain == "" {
|
|
h.renderCreateFormError(w, userID, "Domain is required")
|
|
return
|
|
}
|
|
|
|
// Validate selected domain is in allowed list (unless custom domain)
|
|
if !isCustomDomain {
|
|
if selectedDomain == "" && len(h.FedWikiAllowedDomains) > 0 {
|
|
selectedDomain = h.FedWikiAllowedDomains[0]
|
|
}
|
|
validDomain := false
|
|
for _, allowed := range h.FedWikiAllowedDomains {
|
|
if selectedDomain == allowed {
|
|
validDomain = true
|
|
break
|
|
}
|
|
}
|
|
if !validDomain {
|
|
h.renderCreateFormError(w, userID, "Invalid domain selected")
|
|
return
|
|
}
|
|
}
|
|
|
|
// Get user info
|
|
user, err := h.DB.GetUserByID(r.Context(), userID)
|
|
if err != nil {
|
|
h.Logger.Error("failed to get user", slog.Any("error", err))
|
|
h.renderCreateFormError(w, userID, "Failed to get user information")
|
|
return
|
|
}
|
|
|
|
// Check Temporal client
|
|
if h.TemporalClient == nil {
|
|
h.Logger.Error("Temporal client not configured")
|
|
h.renderCreateFormError(w, userID, "Site provisioning service is not available")
|
|
return
|
|
}
|
|
|
|
// Start the workflow
|
|
workflowID := "create-fedwiki-site-" + uuid.New().String()
|
|
workflowOptions := client.StartWorkflowOptions{
|
|
ID: workflowID,
|
|
TaskQueue: queues.Main,
|
|
}
|
|
|
|
input := fedwiki.CreateFedWikiSiteWorkflowInput{
|
|
UserID: userID,
|
|
Domain: domain,
|
|
SiteDomain: selectedDomain, // The domain suffix (e.g., "localtest.me")
|
|
OwnerName: user.Username,
|
|
OwnerID: user.OidcSubject,
|
|
IsCustomDomain: isCustomDomain,
|
|
SupportURL: h.SupportURL,
|
|
}
|
|
|
|
we, err := h.TemporalClient.ExecuteWorkflow(r.Context(), workflowOptions, fedwiki.CreateFedWikiSiteWorkflow, input)
|
|
if err != nil {
|
|
h.Logger.Error("failed to start workflow", slog.Any("error", err))
|
|
h.renderCreateFormError(w, userID, "Failed to initiate site creation")
|
|
return
|
|
}
|
|
|
|
h.Logger.Info("site creation workflow started",
|
|
slog.String("workflowID", we.GetID()),
|
|
slog.String("domain", domain),
|
|
slog.String("siteDomain", selectedDomain))
|
|
|
|
// Wait for the workflow to complete
|
|
var result fedwiki.CreateFedWikiSiteWorkflowOutput
|
|
if err := we.Get(r.Context(), &result); err != nil {
|
|
h.Logger.Error("workflow execution failed", slog.Any("error", err))
|
|
h.renderCreateFormError(w, userID, "Site creation failed. Please try again.")
|
|
return
|
|
}
|
|
|
|
// Check if the workflow reported failure
|
|
if !result.Success {
|
|
h.Logger.Warn("site creation workflow returned failure",
|
|
slog.String("domain", domain),
|
|
slog.String("error", result.ErrorMessage))
|
|
h.renderCreateFormError(w, userID, result.ErrorMessage)
|
|
return
|
|
}
|
|
|
|
// Render success partial with HX-Trigger to refresh sites list
|
|
w.Header().Set("Content-Type", "text/html")
|
|
w.Header().Set("HX-Trigger", `{"refreshSites": true}`)
|
|
data := struct{ Domain string }{Domain: result.Domain}
|
|
if err := h.Templates.ExecuteTemplate(w, "fedwiki_create_success.html", data); err != nil {
|
|
h.Logger.Error("template error", slog.Any("error", err))
|
|
}
|
|
}
|
|
|
|
// DeleteConfirmData holds data for the delete confirmation partial.
|
|
type DeleteConfirmData struct {
|
|
Domain string
|
|
Error string
|
|
}
|
|
|
|
// GetDeleteConfirm handles GET /partials/fedwiki/delete-confirm/{domain}
|
|
func (h *FedWikiPartialsHandler) GetDeleteConfirm(w http.ResponseWriter, r *http.Request) {
|
|
domain := r.PathValue("domain")
|
|
if domain == "" {
|
|
http.Error(w, "Domain required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
data := DeleteConfirmData{Domain: domain}
|
|
|
|
w.Header().Set("Content-Type", "text/html")
|
|
if err := h.Templates.ExecuteTemplate(w, "fedwiki_delete_confirm.html", data); err != nil {
|
|
h.Logger.Error("template error", slog.Any("error", err))
|
|
}
|
|
}
|
|
|
|
// DeleteSite handles DELETE /partials/fedwiki/sites/{domain}
|
|
func (h *FedWikiPartialsHandler) DeleteSite(w http.ResponseWriter, r *http.Request) {
|
|
userID, err := h.getUserIDFromSession(r)
|
|
if err != nil {
|
|
h.Logger.Error("failed to get user ID from session", slog.Any("error", err))
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
domain := r.PathValue("domain")
|
|
if domain == "" {
|
|
h.renderDeleteConfirmError(w, domain, "Domain is required")
|
|
return
|
|
}
|
|
|
|
// Verify site belongs to user
|
|
site, err := h.DB.GetSiteByDomain(r.Context(), domain)
|
|
if err != nil {
|
|
h.Logger.Error("failed to get site", slog.Any("error", err))
|
|
h.renderDeleteConfirmError(w, domain, "Site not found")
|
|
return
|
|
}
|
|
|
|
if site.UserID != userID {
|
|
h.renderDeleteConfirmError(w, domain, "You do not own this site")
|
|
return
|
|
}
|
|
|
|
// Check Temporal client
|
|
if h.TemporalClient == nil {
|
|
h.Logger.Error("Temporal client not configured")
|
|
h.renderDeleteConfirmError(w, domain, "Site provisioning service is not available")
|
|
return
|
|
}
|
|
|
|
// Start the workflow
|
|
workflowID := "delete-fedwiki-site-" + uuid.New().String()
|
|
workflowOptions := client.StartWorkflowOptions{
|
|
ID: workflowID,
|
|
TaskQueue: queues.Main,
|
|
}
|
|
|
|
input := fedwiki.DeleteFedWikiSiteWorkflowInput{
|
|
UserID: userID,
|
|
Domain: domain,
|
|
SupportURL: h.SupportURL,
|
|
}
|
|
|
|
we, err := h.TemporalClient.ExecuteWorkflow(r.Context(), workflowOptions, fedwiki.DeleteFedWikiSiteWorkflow, input)
|
|
if err != nil {
|
|
h.Logger.Error("failed to start workflow", slog.Any("error", err))
|
|
h.renderDeleteConfirmError(w, domain, "Failed to initiate site deletion")
|
|
return
|
|
}
|
|
|
|
h.Logger.Info("site deletion workflow started",
|
|
slog.String("workflowID", we.GetID()),
|
|
slog.String("domain", domain))
|
|
|
|
// Wait for the workflow to complete
|
|
var result fedwiki.DeleteFedWikiSiteWorkflowOutput
|
|
if err := we.Get(r.Context(), &result); err != nil {
|
|
h.Logger.Error("workflow execution failed", slog.Any("error", err))
|
|
h.renderDeleteConfirmError(w, domain, "Site deletion failed. Please try again.")
|
|
return
|
|
}
|
|
|
|
// Check if the workflow reported failure
|
|
if !result.Success {
|
|
h.Logger.Warn("site deletion workflow returned failure",
|
|
slog.String("domain", domain),
|
|
slog.String("error", result.ErrorMessage))
|
|
h.renderDeleteConfirmError(w, domain, result.ErrorMessage)
|
|
return
|
|
}
|
|
|
|
// Render success partial with HX-Trigger to refresh sites list
|
|
w.Header().Set("Content-Type", "text/html")
|
|
w.Header().Set("HX-Trigger", `{"refreshSites": true}`)
|
|
data := struct{ Domain string }{Domain: domain}
|
|
if err := h.Templates.ExecuteTemplate(w, "fedwiki_delete_success.html", data); err != nil {
|
|
h.Logger.Error("template error", slog.Any("error", err))
|
|
}
|
|
}
|
|
|
|
// Helper methods
|
|
|
|
func (h *FedWikiPartialsHandler) buildSiteURL(domain string, isCustomDomain bool) string {
|
|
// Domain is stored as full domain (e.g., test.localtest.me or custom.example.com)
|
|
// Just need to add the appropriate scheme
|
|
scheme := h.FedWikiSiteScheme
|
|
if scheme == "" {
|
|
scheme = "https"
|
|
}
|
|
return scheme + "://" + domain
|
|
}
|
|
|
|
func (h *FedWikiPartialsHandler) renderError(w http.ResponseWriter, template string, message string) {
|
|
w.Header().Set("Content-Type", "text/html")
|
|
data := SitesData{Error: message}
|
|
h.Templates.ExecuteTemplate(w, template, data)
|
|
}
|
|
|
|
func (h *FedWikiPartialsHandler) renderCreateFormError(w http.ResponseWriter, userID int64, message string) {
|
|
count, _ := h.DB.GetSiteCountByUserID(context.TODO(), userID)
|
|
membership, _ := h.DB.GetUserMembership(context.TODO(), userID)
|
|
|
|
data := CreateFormData{
|
|
AllowedDomains: h.FedWikiAllowedDomains,
|
|
CanCreate: count < membership.SitesQuota,
|
|
CurrentCount: count,
|
|
Quota: membership.SitesQuota,
|
|
IsMember: membership.IsMember != 0,
|
|
Error: message,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html")
|
|
h.Templates.ExecuteTemplate(w, "fedwiki_create_form.html", data)
|
|
}
|
|
|
|
func (h *FedWikiPartialsHandler) renderDeleteConfirmError(w http.ResponseWriter, domain string, message string) {
|
|
data := DeleteConfirmData{Domain: domain, Error: message}
|
|
w.Header().Set("Content-Type", "text/html")
|
|
h.Templates.ExecuteTemplate(w, "fedwiki_delete_confirm.html", data)
|
|
}
|