package handler import ( "baron-sso-backend/internal/service" "bytes" "context" "encoding/csv" "errors" "log/slog" "strings" "github.com/gofiber/fiber/v2" ) type WorksmobileHandler struct { Service service.WorksmobileAdminService } func NewWorksmobileHandler(svc service.WorksmobileAdminService) *WorksmobileHandler { return &WorksmobileHandler{Service: svc} } func (h *WorksmobileHandler) GetOverview(c *fiber.Ctx) error { overview, err := h.Service.GetTenantOverview(c.Context(), strings.TrimSpace(c.Params("tenantId"))) if err != nil { return worksmobileGuardError(c, err, "get_overview") } if !worksmobileOverviewAllowed(overview) { return errorJSON(c, fiber.StatusNotFound, "worksmobile is only available for hanmac-family root tenant") } return c.JSON(overview) } func (h *WorksmobileHandler) GetComparison(c *fiber.Ctx) error { includeMatched := strings.EqualFold(strings.TrimSpace(c.Query("includeMatched")), "true") comparison, err := h.Service.GetComparison(c.Context(), strings.TrimSpace(c.Params("tenantId")), includeMatched) if err != nil { return worksmobileGuardError(c, err, "get_comparison") } return c.JSON(comparison) } func (h *WorksmobileHandler) OAuthCallback(c *fiber.Ctx) error { return c.Type("html").SendString("Worksmobile OAuth callback reachable") } func (h *WorksmobileHandler) BackfillDryRun(c *fiber.Ctx) error { result, err := h.Service.EnqueueBackfillDryRun(c.Context(), strings.TrimSpace(c.Params("tenantId"))) if err != nil { return worksmobileGuardError(c, err, "backfill_dry_run") } return c.JSON(result) } func (h *WorksmobileHandler) SyncOrgUnit(c *fiber.Ctx) error { orgUnitID := strings.TrimSpace(c.Params("orgUnitId")) job, err := h.Service.EnqueueOrgUnitSync(c.Context(), strings.TrimSpace(c.Params("tenantId")), orgUnitID) if err != nil { return worksmobileGuardError(c, err, "sync_orgunit", "org_unit_id", orgUnitID) } return c.Status(fiber.StatusAccepted).JSON(job) } func (h *WorksmobileHandler) DeleteOrgUnit(c *fiber.Ctx) error { orgUnitID := strings.TrimSpace(c.Params("orgUnitId")) job, err := h.Service.EnqueueOrgUnitDelete(c.Context(), strings.TrimSpace(c.Params("tenantId")), orgUnitID) if err != nil { return worksmobileGuardError(c, err, "delete_orgunit", "org_unit_id", orgUnitID) } return c.Status(fiber.StatusAccepted).JSON(job) } func (h *WorksmobileHandler) SyncUser(c *fiber.Ctx) error { userID := strings.TrimSpace(c.Params("userId")) credentialRequest, err := parseWorksmobileCredentialRequest(c) if err != nil { return errorJSON(c, fiber.StatusBadRequest, err.Error()) } job, err := h.Service.EnqueueUserSync( c.Context(), strings.TrimSpace(c.Params("tenantId")), userID, credentialRequest.CredentialBatchID, credentialRequest.InitialPassword, ) if err != nil { return worksmobileGuardError(c, err, "sync_user", "user_id", userID) } return c.Status(fiber.StatusAccepted).JSON(job) } func (h *WorksmobileHandler) ImportUsersFromWorks(c *fiber.Ctx) error { var req struct { WorksmobileUserIDs []string `json:"worksmobileUserIds"` } if len(c.Body()) > 0 { if err := c.BodyParser(&req); err != nil { return errorJSON(c, fiber.StatusBadRequest, err.Error()) } } result, err := h.Service.ImportUsersFromWorks( c.Context(), strings.TrimSpace(c.Params("tenantId")), req.WorksmobileUserIDs, ) if err != nil { return worksmobileGuardError(c, err, "import_users_from_works") } return c.JSON(result) } func (h *WorksmobileHandler) ResetUserPassword(c *fiber.Ctx) error { userID := strings.TrimSpace(c.Params("userId")) credentialBatchID, err := parseWorksmobileCredentialBatchID(c) if err != nil { return errorJSON(c, fiber.StatusBadRequest, err.Error()) } job, err := h.Service.EnqueueUserPasswordReset(c.Context(), strings.TrimSpace(c.Params("tenantId")), userID, credentialBatchID) if err != nil { return worksmobileGuardError(c, err, "reset_user_password", "user_id", userID) } return c.Status(fiber.StatusAccepted).JSON(job) } func (h *WorksmobileHandler) RetryJob(c *fiber.Ctx) error { jobID := strings.TrimSpace(c.Params("jobId")) job, err := h.Service.RetryJob(c.Context(), strings.TrimSpace(c.Params("tenantId")), jobID) if err != nil { return worksmobileGuardError(c, err, "retry_job", "job_id", jobID) } return c.JSON(job) } func (h *WorksmobileHandler) DeletePendingJobs(c *fiber.Ctx) error { result, err := h.Service.DeletePendingJobs(c.Context(), strings.TrimSpace(c.Params("tenantId"))) if err != nil { return worksmobileGuardError(c, err, "delete_pending_jobs") } return c.JSON(result) } func (h *WorksmobileHandler) DownloadInitialPasswordsCSV(c *fiber.Ctx) error { credentials, err := h.Service.ListInitialPasswordCredentials(c.Context(), strings.TrimSpace(c.Params("tenantId")), strings.TrimSpace(c.Query("batchId"))) if err != nil { return worksmobileGuardError(c, err, "download_initial_passwords") } var buf bytes.Buffer writer := csv.NewWriter(&buf) if err := writer.Write([]string{"email", "name", "primaryLeafOrgName", "initialPassword", "status", "lastError"}); err != nil { return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } for _, credential := range credentials { if err := writer.Write([]string{credential.Email, credential.Name, credential.PrimaryLeafOrgName, credential.InitialPassword, credential.Status, credential.LastError}); err != nil { return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } } writer.Flush() if err := writer.Error(); err != nil { return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } c.Set(fiber.HeaderContentType, "text/csv; charset=utf-8") c.Set(fiber.HeaderContentDisposition, `attachment; filename="worksmobile_initial_passwords.csv"`) return c.Send(buf.Bytes()) } func (h *WorksmobileHandler) ListCredentialBatches(c *fiber.Ctx) error { batches, err := h.Service.ListCredentialBatches(c.Context(), strings.TrimSpace(c.Params("tenantId"))) if err != nil { return worksmobileGuardError(c, err, "list_credential_batches") } return c.JSON(batches) } func (h *WorksmobileHandler) DeleteCredentialBatchPasswords(c *fiber.Ctx) error { batchID := strings.TrimSpace(c.Params("batchId")) batch, err := h.Service.DeleteCredentialBatchPasswords(c.Context(), strings.TrimSpace(c.Params("tenantId")), batchID) if err != nil { return worksmobileGuardError(c, err, "delete_credential_batch_passwords", "batch_id", batchID) } return c.JSON(batch) } type worksmobileCredentialBatchRequest struct { CredentialBatchID string `json:"credentialBatchId"` InitialPassword string `json:"initialPassword"` } func parseWorksmobileCredentialBatchID(c *fiber.Ctx) (string, error) { req, err := parseWorksmobileCredentialRequest(c) return req.CredentialBatchID, err } func parseWorksmobileCredentialRequest(c *fiber.Ctx) (worksmobileCredentialBatchRequest, error) { batchID := strings.TrimSpace(c.Query("credentialBatchId")) req := worksmobileCredentialBatchRequest{CredentialBatchID: batchID} if len(bytes.TrimSpace(c.Body())) == 0 { return req, nil } if err := c.BodyParser(&req); err != nil { return worksmobileCredentialBatchRequest{}, err } req.InitialPassword = strings.TrimSpace(req.InitialPassword) if bodyBatchID := strings.TrimSpace(req.CredentialBatchID); bodyBatchID != "" { req.CredentialBatchID = bodyBatchID return req, nil } req.CredentialBatchID = batchID return req, nil } func worksmobileOverviewAllowed(overview service.WorksmobileTenantOverview) bool { return overview.Tenant.Slug == service.HanmacFamilyTenantSlug && overview.Tenant.ParentID == nil } func worksmobileGuardError(c *fiber.Ctx, err error, operation string, attrs ...any) error { if err == nil { return nil } logAttrs := []any{ "operation", operation, "tenant_id", strings.TrimSpace(c.Params("tenantId")), "path", c.Path(), "error", err, } logAttrs = append(logAttrs, attrs...) if errors.Is(err, context.Canceled) { slog.Warn("worksmobile admin operation failed", logAttrs...) return errorJSON(c, fiber.StatusRequestTimeout, err.Error()) } slog.Error("worksmobile admin operation failed", logAttrs...) if strings.Contains(err.Error(), "hanmac-family root") { return errorJSON(c, fiber.StatusNotFound, err.Error()) } if worksmobileBadRequestError(err) { return errorJSON(c, fiber.StatusBadRequest, err.Error()) } return errorJSON(c, fiber.StatusInternalServerError, err.Error()) } func worksmobileBadRequestError(err error) bool { message := err.Error() return strings.Contains(message, "target user tenant is excluded from Worksmobile sync") || strings.Contains(message, "target user is outside hanmac-family subtree") || strings.Contains(message, "target user has no tenant") || strings.Contains(message, "target user status is excluded from Worksmobile sync") || strings.Contains(message, "target tenant is excluded from Worksmobile sync") || strings.Contains(message, "target tenant is not a worksmobile orgunit tenant") || strings.Contains(message, "target orgunit is outside hanmac-family subtree") }