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.
206 lines
6.7 KiB
Go
206 lines
6.7 KiB
Go
package fedwiki
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
fwmod "git.coopcloud.tech/wiki-cafe/member-console/internal/fedwiki"
|
|
"go.temporal.io/sdk/temporal"
|
|
"go.temporal.io/sdk/workflow"
|
|
)
|
|
|
|
// SyncFedWikiSitesWorkflowInput is the input for the SyncFedWikiSitesWorkflow.
|
|
type SyncFedWikiSitesWorkflowInput struct {
|
|
// DefaultWorkspaceID is the workspace ID to assign to sites that don't have a known owner.
|
|
DefaultWorkspaceID string
|
|
}
|
|
|
|
// SyncFedWikiSitesWorkflowOutput is the output for the SyncFedWikiSitesWorkflow.
|
|
type SyncFedWikiSitesWorkflowOutput struct {
|
|
Success bool
|
|
SitesFound int
|
|
SitesAdded int
|
|
SitesRemoved int
|
|
ErrorMessage string
|
|
SyncTimestamp time.Time
|
|
}
|
|
|
|
// SyncActivityOptions returns activity options for sync operations.
|
|
func SyncActivityOptions() workflow.ActivityOptions {
|
|
return workflow.ActivityOptions{
|
|
StartToCloseTimeout: 5 * time.Minute,
|
|
RetryPolicy: &temporal.RetryPolicy{
|
|
InitialInterval: time.Second,
|
|
BackoffCoefficient: 2.0,
|
|
MaximumInterval: time.Minute,
|
|
MaximumAttempts: 5,
|
|
},
|
|
}
|
|
}
|
|
|
|
// SyncFedWikiSitesWorkflow synchronizes sites from the FedWiki farm to the local database.
|
|
// It fetches all sites from the FarmManager API and ensures the local database reflects
|
|
// the current state of the farm.
|
|
func SyncFedWikiSitesWorkflow(ctx workflow.Context, input SyncFedWikiSitesWorkflowInput) (*SyncFedWikiSitesWorkflowOutput, error) {
|
|
logger := workflow.GetLogger(ctx)
|
|
logger.Info("SyncFedWikiSitesWorkflow started",
|
|
"defaultWorkspaceID", input.DefaultWorkspaceID)
|
|
|
|
var activities *Activities
|
|
activityCtx := workflow.WithActivityOptions(ctx, SyncActivityOptions())
|
|
|
|
// Step 1: Fetch all sites from the FedWiki farm
|
|
var listResult *ListFedWikiSitesOutput
|
|
err := workflow.ExecuteActivity(activityCtx, activities.ListFedWikiSitesActivity, ListFedWikiSitesInput{}).Get(ctx, &listResult)
|
|
if err != nil {
|
|
logger.Error("Failed to list sites from FedWiki farm", "error", err)
|
|
return &SyncFedWikiSitesWorkflowOutput{
|
|
Success: false,
|
|
ErrorMessage: "Failed to fetch sites from FedWiki farm. Please try again later.",
|
|
SyncTimestamp: workflow.Now(ctx),
|
|
}, nil
|
|
}
|
|
|
|
logger.Info("Fetched sites from FedWiki farm", "count", len(listResult.Sites))
|
|
|
|
// Step 2: Sync the sites to the local database
|
|
var syncResult *SyncSitesToDBOutput
|
|
err = workflow.ExecuteActivity(activityCtx, activities.SyncSitesToDBActivity, SyncSitesToDBInput{
|
|
FarmSites: listResult.Sites,
|
|
DefaultWorkspaceID: input.DefaultWorkspaceID,
|
|
}).Get(ctx, &syncResult)
|
|
if err != nil {
|
|
logger.Error("Failed to sync sites to database", "error", err)
|
|
return &SyncFedWikiSitesWorkflowOutput{
|
|
Success: false,
|
|
SitesFound: len(listResult.Sites),
|
|
ErrorMessage: "Failed to sync sites to local database.",
|
|
SyncTimestamp: workflow.Now(ctx),
|
|
}, nil
|
|
}
|
|
|
|
logger.Info("SyncFedWikiSitesWorkflow completed successfully",
|
|
"sitesFound", len(listResult.Sites),
|
|
"sitesAdded", syncResult.SitesAdded,
|
|
"sitesRemoved", syncResult.SitesRemoved)
|
|
|
|
return &SyncFedWikiSitesWorkflowOutput{
|
|
Success: true,
|
|
SitesFound: len(listResult.Sites),
|
|
SitesAdded: syncResult.SitesAdded,
|
|
SitesRemoved: syncResult.SitesRemoved,
|
|
SyncTimestamp: workflow.Now(ctx),
|
|
}, nil
|
|
}
|
|
|
|
// SyncSitesToDBInput is the input for the SyncSitesToDB activity.
|
|
type SyncSitesToDBInput struct {
|
|
FarmSites []SiteInfo
|
|
DefaultWorkspaceID string
|
|
}
|
|
|
|
// SyncSitesToDBOutput is the output for the SyncSitesToDB activity.
|
|
type SyncSitesToDBOutput struct {
|
|
SitesAdded int
|
|
SitesUpdated int
|
|
SitesRemoved int
|
|
}
|
|
|
|
// SyncSitesToDBActivity synchronizes sites from the FedWiki farm to the local database.
|
|
// It adds new farm sites to the default workspace and removes stale sites from the default workspace.
|
|
// Sites created by users (belonging to non-default workspaces) are never modified.
|
|
func (a *Activities) SyncSitesToDBActivity(ctx context.Context, input SyncSitesToDBInput) (*SyncSitesToDBOutput, error) {
|
|
a.Logger.Info("syncing sites to database",
|
|
slog.Int("farmSiteCount", len(input.FarmSites)),
|
|
slog.String("defaultWorkspaceID", input.DefaultWorkspaceID))
|
|
|
|
// If no default workspace is configured, we cannot add or scope deletes — skip gracefully
|
|
if input.DefaultWorkspaceID == "" {
|
|
a.Logger.Warn("no default workspace ID configured, skipping site sync")
|
|
return &SyncSitesToDBOutput{}, nil
|
|
}
|
|
|
|
// Get all current sites from the local database
|
|
localSites, err := a.SiteQ.ListAllSites(ctx)
|
|
if err != nil {
|
|
a.Logger.Error("failed to get local sites", slog.Any("error", err))
|
|
return nil, fmt.Errorf("failed to get local sites: %w", err)
|
|
}
|
|
|
|
// Create a map of local sites by domain for quick lookup
|
|
localSiteMap := make(map[string]fwmod.Site, len(localSites))
|
|
for _, site := range localSites {
|
|
localSiteMap[site.Domain] = site
|
|
}
|
|
|
|
// Create a map of farm domains for quick lookup
|
|
farmDomainMap := make(map[string]SiteInfo, len(input.FarmSites))
|
|
for _, site := range input.FarmSites {
|
|
farmDomainMap[site.Name] = site
|
|
}
|
|
|
|
sitesAdded := 0
|
|
sitesUpdated := 0
|
|
sitesRemoved := 0
|
|
|
|
// Add sites from the farm that don't exist locally
|
|
for _, farmSite := range input.FarmSites {
|
|
if farmSite.Status != "active" {
|
|
a.Logger.Debug("skipping inactive site", slog.String("domain", farmSite.Name), slog.String("status", farmSite.Status))
|
|
continue
|
|
}
|
|
|
|
_, exists := localSiteMap[farmSite.Name]
|
|
if !exists {
|
|
_, err := a.SiteQ.UpsertSiteByDomain(ctx, fwmod.UpsertSiteByDomainParams{
|
|
WorkspaceID: input.DefaultWorkspaceID,
|
|
Domain: farmSite.Name,
|
|
IsCustomDomain: false,
|
|
})
|
|
if err != nil {
|
|
a.Logger.Error("failed to add site",
|
|
slog.String("domain", farmSite.Name),
|
|
slog.Any("error", err))
|
|
continue
|
|
}
|
|
sitesAdded++
|
|
a.Logger.Info("added site from farm",
|
|
slog.String("domain", farmSite.Name))
|
|
}
|
|
}
|
|
|
|
// Remove sites that belong to the default workspace and no longer exist on the farm.
|
|
// Sites belonging to other workspaces (user-created) are never touched.
|
|
for _, localSite := range localSites {
|
|
if localSite.WorkspaceID != input.DefaultWorkspaceID {
|
|
continue // user-created site, never delete
|
|
}
|
|
|
|
farmSite, exists := farmDomainMap[localSite.Domain]
|
|
if !exists || farmSite.Status != "active" {
|
|
err := a.SiteQ.DeleteSiteByDomain(ctx, localSite.Domain)
|
|
if err != nil {
|
|
a.Logger.Error("failed to delete site",
|
|
slog.String("domain", localSite.Domain),
|
|
slog.Any("error", err))
|
|
continue
|
|
}
|
|
sitesRemoved++
|
|
a.Logger.Info("removed stale site from default workspace", slog.String("domain", localSite.Domain))
|
|
}
|
|
}
|
|
|
|
a.Logger.Info("site sync completed",
|
|
slog.Int("sitesAdded", sitesAdded),
|
|
slog.Int("sitesUpdated", sitesUpdated),
|
|
slog.Int("sitesRemoved", sitesRemoved))
|
|
|
|
return &SyncSitesToDBOutput{
|
|
SitesAdded: sitesAdded,
|
|
SitesUpdated: sitesUpdated,
|
|
SitesRemoved: sitesRemoved,
|
|
}, nil
|
|
}
|