b16e9f0bd1
Plan 0 (workspaces): add workspaces + workspace_memberships schema, extend JWT/Actor/claims with primary_workspace_id, seed default workspace per tenant, thread workspace_id through tenant monitoring quota. Plan A (desktop skeleton): new Electron app (apps/desktop-client) with main/ preload/renderer, shared Vue component package (packages/ui-shared), and server surface — desktop client registration + token rotation + heartbeat, SSE task event stream, desktop accounts/tasks/content handlers, publish job endpoint, and supporting repositories, services, sqlc queries, and migrations. Hard cutover per plan: remove browser-extension monitoring callback endpoints, stub legacy media API in admin-web, and delete monitoring_callback_handler.go.
170 lines
5.9 KiB
Go
170 lines
5.9 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/auth"
|
|
"github.com/geo-platform/tenant-api/internal/shared/messaging/rabbitmq"
|
|
"github.com/geo-platform/tenant-api/internal/shared/response"
|
|
"github.com/geo-platform/tenant-api/internal/tenant/repository"
|
|
)
|
|
|
|
type PublishJobService struct {
|
|
pool *pgxpool.Pool
|
|
messaging *rabbitmq.Client
|
|
logger *zap.Logger
|
|
}
|
|
|
|
func NewPublishJobService(pool *pgxpool.Pool, messaging *rabbitmq.Client, logger *zap.Logger) *PublishJobService {
|
|
return &PublishJobService{
|
|
pool: pool,
|
|
messaging: messaging,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
type CreatePublishJobAccountRequest struct {
|
|
AccountID string `json:"account_id" binding:"required"`
|
|
Mode string `json:"mode" binding:"required,oneof=auto manual"`
|
|
}
|
|
|
|
type CreatePublishJobRequest struct {
|
|
Title string `json:"title" binding:"required"`
|
|
ContentRef map[string]any `json:"content_ref" binding:"required"`
|
|
Accounts []CreatePublishJobAccountRequest `json:"accounts" binding:"required,min=1"`
|
|
ScheduledAt *time.Time `json:"scheduled_at"`
|
|
}
|
|
|
|
type CreatePublishJobResponse struct {
|
|
JobID string `json:"job_id"`
|
|
TaskIDs []string `json:"task_ids"`
|
|
}
|
|
|
|
func (s *PublishJobService) Create(ctx context.Context, actor auth.Actor, req CreatePublishJobRequest) (*CreatePublishJobResponse, error) {
|
|
if actor.TenantID == 0 || actor.PrimaryWorkspaceID == 0 || actor.UserID == 0 {
|
|
return nil, response.ErrUnauthorized(40132, "workspace_scope_missing", "workspace context is required")
|
|
}
|
|
|
|
if s.pool == nil {
|
|
return nil, response.ErrServiceUnavailable(50342, "desktop_publish_job_store_unavailable", "desktop publish job store is unavailable")
|
|
}
|
|
|
|
contentRefJSON, err := marshalOptionalJSON(req.ContentRef)
|
|
if err != nil {
|
|
return nil, response.ErrBadRequest(40091, "invalid_content_ref", "content_ref must be serializable")
|
|
}
|
|
|
|
tx, err := s.pool.Begin(ctx)
|
|
if err != nil {
|
|
return nil, response.ErrInternal(50096, "desktop_publish_job_begin_failed", "failed to start desktop publish job transaction")
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
|
|
accountRepo := repository.NewDesktopAccountRepository(tx)
|
|
taskRepo := repository.NewDesktopTaskRepository(tx)
|
|
|
|
jobID := uuid.New()
|
|
job, err := taskRepo.CreatePublishJob(ctx, repository.CreateDesktopPublishJobParams{
|
|
DesktopID: jobID,
|
|
TenantID: actor.TenantID,
|
|
WorkspaceID: actor.PrimaryWorkspaceID,
|
|
CreatedByUserID: actor.UserID,
|
|
Title: req.Title,
|
|
ContentRef: contentRefJSON,
|
|
ScheduledAt: req.ScheduledAt,
|
|
})
|
|
if err != nil {
|
|
return nil, response.ErrInternal(50097, "desktop_publish_job_create_failed", "failed to create desktop publish job")
|
|
}
|
|
|
|
taskIDs := make([]string, 0, len(req.Accounts))
|
|
createdTasks := make([]*repository.DesktopTask, 0, len(req.Accounts))
|
|
for _, accountReq := range req.Accounts {
|
|
accountID, parseErr := uuid.Parse(strings.TrimSpace(accountReq.AccountID))
|
|
if parseErr != nil {
|
|
return nil, response.ErrBadRequest(40092, "invalid_desktop_account_id", "account_id must be a uuid")
|
|
}
|
|
|
|
account, lookupErr := accountRepo.GetByDesktopID(ctx, accountID, actor.PrimaryWorkspaceID)
|
|
if lookupErr != nil {
|
|
if errors.Is(lookupErr, pgx.ErrNoRows) {
|
|
return nil, response.ErrBadRequest(40093, "desktop_account_not_found", "one or more selected desktop accounts do not exist")
|
|
}
|
|
return nil, response.ErrInternal(50098, "desktop_account_lookup_failed", "failed to load selected desktop account")
|
|
}
|
|
if account.ClientID == nil {
|
|
return nil, response.ErrConflict(40992, "desktop_account_client_missing", "one or more selected desktop accounts are not currently bound to a desktop client")
|
|
}
|
|
|
|
payloadJSON, payloadErr := marshalOptionalJSON(map[string]any{
|
|
"title": req.Title,
|
|
"content_ref": req.ContentRef,
|
|
"mode": accountReq.Mode,
|
|
"account_id": account.DesktopID.String(),
|
|
"platform": account.Platform,
|
|
})
|
|
if payloadErr != nil {
|
|
return nil, response.ErrBadRequest(40094, "invalid_desktop_task_payload", "publish job payload must be serializable")
|
|
}
|
|
|
|
task, createErr := taskRepo.CreateTask(ctx, repository.CreateDesktopTaskParams{
|
|
DesktopID: uuid.New(),
|
|
JobID: job.DesktopID,
|
|
TenantID: actor.TenantID,
|
|
WorkspaceID: actor.PrimaryWorkspaceID,
|
|
TargetAccountID: account.DesktopID,
|
|
TargetClientID: *account.ClientID,
|
|
Platform: account.Platform,
|
|
Kind: "publish",
|
|
Payload: payloadJSON,
|
|
Status: "queued",
|
|
})
|
|
if createErr != nil {
|
|
return nil, response.ErrInternal(50099, "desktop_task_create_failed", "failed to create desktop publish task")
|
|
}
|
|
|
|
createdTasks = append(createdTasks, task)
|
|
taskIDs = append(taskIDs, task.DesktopID.String())
|
|
}
|
|
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return nil, response.ErrInternal(50100, "desktop_publish_job_commit_failed", "failed to commit desktop publish job")
|
|
}
|
|
|
|
for _, task := range createdTasks {
|
|
if publishErr := publishDesktopTaskEvent(context.Background(), s.messaging, DesktopTaskEvent{
|
|
Type: "task_available",
|
|
TaskID: task.DesktopID.String(),
|
|
JobID: task.JobID.String(),
|
|
WorkspaceID: task.WorkspaceID,
|
|
TargetClientID: task.TargetClientID.String(),
|
|
Status: task.Status,
|
|
Kind: task.Kind,
|
|
UpdatedAt: task.UpdatedAt,
|
|
}); publishErr != nil {
|
|
s.logWarn("desktop task available event publish failed", publishErr, zap.String("task_id", task.DesktopID.String()))
|
|
}
|
|
}
|
|
|
|
return &CreatePublishJobResponse{
|
|
JobID: job.DesktopID.String(),
|
|
TaskIDs: taskIDs,
|
|
}, nil
|
|
}
|
|
|
|
func (s *PublishJobService) logWarn(message string, err error, fields ...zap.Field) {
|
|
if s.logger == nil || err == nil {
|
|
return
|
|
}
|
|
fields = append(fields, zap.Error(err))
|
|
s.logger.Warn(message, fields...)
|
|
}
|