Files
member-console/internal/workflows/fedwiki/activities.go

270 lines
8.6 KiB
Go

package fedwiki
import (
"context"
"errors"
"fmt"
"log/slog"
"git.coopcloud.tech/wiki-cafe/member-console/internal/db"
"go.temporal.io/sdk/temporal"
)
// Activities holds dependencies for FedWiki domain activities.
type Activities struct {
DB db.Querier
Logger *slog.Logger
Client *FarmManagerClient
FedWikiAllowedDomains []string // Domains where users can create sites
SupportURL string
}
// ActivitiesConfig holds configuration for creating FedWiki activities.
type ActivitiesConfig struct {
DB db.Querier
Logger *slog.Logger
FedWikiFarmAPIURL string // URL for FarmManager API calls
FedWikiAllowedDomains []string // Domains where users can create sites
FedWikiAdminToken string
SupportURL string
}
// NewActivities creates a new Activities instance with the FarmManager client.
func NewActivities(cfg ActivitiesConfig) *Activities {
client := NewFarmManagerClient(cfg.FedWikiFarmAPIURL, cfg.FedWikiAdminToken)
if cfg.Logger != nil {
cfg.Logger.Info("FedWiki activities initialized",
slog.String("farmAPIURL", cfg.FedWikiFarmAPIURL),
slog.Any("allowedDomains", cfg.FedWikiAllowedDomains),
slog.Bool("hasToken", cfg.FedWikiAdminToken != ""))
}
return &Activities{
DB: cfg.DB,
Logger: cfg.Logger,
Client: client,
FedWikiAllowedDomains: cfg.FedWikiAllowedDomains,
SupportURL: cfg.SupportURL,
}
}
// CreateFedWikiSiteInput is the input for the CreateFedWikiSite activity.
type CreateFedWikiSiteInput struct {
UserID int64
Domain string // The subdomain part (e.g., "mysite")
SiteDomain string // The parent domain (e.g., "localtest.me") - ignored for custom domains
OwnerName string // Display name for the owner
OwnerID string // OIDC subject (OAuth2 ID) for owner verification
IsCustomDomain bool
}
// CreateFedWikiSiteOutput is the output for the CreateFedWikiSite activity.
type CreateFedWikiSiteOutput struct {
SiteID int64
Domain string
}
// CreateFedWikiSiteActivity creates a new site on the FedWiki farm and records it in the database.
func (a *Activities) CreateFedWikiSiteActivity(ctx context.Context, input CreateFedWikiSiteInput) (*CreateFedWikiSiteOutput, error) {
// Get the site domain - use provided or fall back to first allowed domain
siteDomain := input.SiteDomain
if siteDomain == "" && !input.IsCustomDomain {
if len(a.FedWikiAllowedDomains) == 0 {
return nil, temporal.NewNonRetryableApplicationError(
"No allowed domains configured for site creation",
"ConfigurationError",
nil,
)
}
siteDomain = a.FedWikiAllowedDomains[0]
}
// Build the full domain for FarmManager API and storage
fullDomain := BuildFullDomain(input.Domain, siteDomain, input.IsCustomDomain)
a.Logger.Info("creating FedWiki site",
slog.Int64("userID", input.UserID),
slog.String("domain", fullDomain),
slog.String("ownerName", input.OwnerName),
slog.String("ownerID", input.OwnerID))
// Call the FarmManager API to create the site
_, err := a.Client.CreateSite(ctx, fullDomain, input.OwnerName, input.OwnerID)
if err != nil {
a.Logger.Error("failed to create site via FarmManager API",
slog.String("domain", fullDomain),
slog.Any("error", err))
// Check if this is a non-retryable error (e.g., 409 Conflict)
var apiErr *FarmManagerAPIError
if errors.As(err, &apiErr) && apiErr.IsNonRetryable() {
return nil, temporal.NewNonRetryableApplicationError(
apiErr.UserMessage(),
"FarmManagerAPIError",
err,
)
}
return nil, fmt.Errorf("failed to create site on FedWiki farm: %w", err)
}
// Record the site in our database with the full domain
site, err := a.DB.CreateSite(ctx, db.CreateSiteParams{
UserID: input.UserID,
Domain: fullDomain,
IsCustomDomain: boolToInt64(input.IsCustomDomain),
})
if err != nil {
a.Logger.Error("failed to record site in database",
slog.String("domain", fullDomain),
slog.Any("error", err))
// Note: Site was created on FedWiki but not recorded locally
// This is a partial failure state that may need manual resolution
return nil, fmt.Errorf("site created on FedWiki but failed to record locally: %w", err)
}
a.Logger.Info("FedWiki site created successfully",
slog.Int64("siteID", site.ID),
slog.String("domain", fullDomain))
return &CreateFedWikiSiteOutput{
SiteID: site.ID,
Domain: site.Domain,
}, nil
}
// DeleteFedWikiSiteInput is the input for the DeleteFedWikiSite activity.
type DeleteFedWikiSiteInput struct {
UserID int64
Domain string // Full domain as stored in database
}
// DeleteFedWikiSiteOutput is the output for the DeleteFedWikiSite activity.
type DeleteFedWikiSiteOutput struct {
Success bool
}
// DeleteFedWikiSiteActivity deletes a site from the FedWiki farm and removes it from the database.
func (a *Activities) DeleteFedWikiSiteActivity(ctx context.Context, input DeleteFedWikiSiteInput) (*DeleteFedWikiSiteOutput, error) {
a.Logger.Info("deleting FedWiki site",
slog.Int64("userID", input.UserID),
slog.String("domain", input.Domain))
// Call the FarmManager API to delete the site (hard delete)
_, err := a.Client.HardDeleteSite(ctx, input.Domain)
if err != nil {
a.Logger.Error("failed to delete site via FarmManager API",
slog.String("domain", input.Domain),
slog.Any("error", err))
// Check if this is a non-retryable error (e.g., 403 Forbidden, 401 Unauthorized)
var apiErr *FarmManagerAPIError
if errors.As(err, &apiErr) && apiErr.IsNonRetryable() {
return nil, temporal.NewNonRetryableApplicationError(
apiErr.UserMessage(),
"FarmManagerAPIError",
err,
)
}
return nil, fmt.Errorf("failed to delete site on FedWiki farm: %w", err)
}
// Remove the site from our database
err = a.DB.DeleteSiteByUserIDAndDomain(ctx, db.DeleteSiteByUserIDAndDomainParams{
UserID: input.UserID,
Domain: input.Domain,
})
if err != nil {
a.Logger.Error("failed to remove site from database",
slog.String("domain", input.Domain),
slog.Any("error", err))
// Note: Site was deleted on FedWiki but not removed locally
return nil, fmt.Errorf("site deleted on FedWiki but failed to remove locally: %w", err)
}
a.Logger.Info("FedWiki site deleted successfully",
slog.String("domain", input.Domain))
return &DeleteFedWikiSiteOutput{Success: true}, nil
}
// ListFedWikiSitesInput is the input for the ListFedWikiSites activity.
type ListFedWikiSitesInput struct{}
// ListFedWikiSitesOutput is the output for the ListFedWikiSites activity.
type ListFedWikiSitesOutput struct {
Sites []SiteInfo
}
// ListFedWikiSitesActivity lists all sites on the FedWiki farm.
func (a *Activities) ListFedWikiSitesActivity(ctx context.Context, input ListFedWikiSitesInput) (*ListFedWikiSitesOutput, error) {
a.Logger.Info("listing FedWiki sites")
sites, err := a.Client.ListSites(ctx)
if err != nil {
a.Logger.Error("failed to list sites via FarmManager API",
slog.Any("error", err))
return nil, fmt.Errorf("failed to list sites on FedWiki farm: %w", err)
}
a.Logger.Info("FedWiki sites listed successfully",
slog.Int("count", len(sites)))
return &ListFedWikiSitesOutput{Sites: sites}, nil
}
// CheckQuotaInput is the input for the CheckQuota activity.
type CheckQuotaInput struct {
UserID int64
}
// CheckQuotaOutput is the output for the CheckQuota activity.
type CheckQuotaOutput struct {
CurrentCount int64
Quota int64
CanCreate bool
}
// CheckQuotaActivity checks if a user has quota available to create a new site.
func (a *Activities) CheckQuotaActivity(ctx context.Context, input CheckQuotaInput) (*CheckQuotaOutput, error) {
a.Logger.Info("checking user quota", slog.Int64("userID", input.UserID))
// Get current site count
count, err := a.DB.GetSiteCountByUserID(ctx, input.UserID)
if err != nil {
a.Logger.Error("failed to get site count",
slog.Int64("userID", input.UserID),
slog.Any("error", err))
return nil, fmt.Errorf("failed to get site count: %w", err)
}
// Get user's quota
quota, err := a.DB.GetUserSitesQuota(ctx, input.UserID)
if err != nil {
a.Logger.Error("failed to get user quota",
slog.Int64("userID", input.UserID),
slog.Any("error", err))
return nil, fmt.Errorf("failed to get user quota: %w", err)
}
canCreate := count < quota
a.Logger.Info("quota check completed",
slog.Int64("userID", input.UserID),
slog.Int64("currentCount", count),
slog.Int64("quota", quota),
slog.Bool("canCreate", canCreate))
return &CheckQuotaOutput{
CurrentCount: count,
Quota: quota,
CanCreate: canCreate,
}, nil
}
// boolToInt64 converts a bool to int64 for SQLite compatibility.
func boolToInt64(b bool) int64 {
if b {
return 1
}
return 0
}