mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-09 08:03:38 +00:00
261 lines
7.0 KiB
Go
261 lines
7.0 KiB
Go
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/warmbly/warmbly/internal/api/middleware"
|
|
"github.com/warmbly/warmbly/internal/app/orgtransfer"
|
|
"github.com/warmbly/warmbly/internal/errx"
|
|
"github.com/warmbly/warmbly/internal/models"
|
|
)
|
|
|
|
// Operator-side workspace archives: the same export and import flow as
|
|
// org_transfer.go, keyed by the :id path parameter instead of the caller's own
|
|
// organization, and audited to the admin log rather than the org's.
|
|
|
|
// adminTransferOrg resolves :id to an existing organization and confirms the
|
|
// transfer service is wired, answering the request itself otherwise.
|
|
func (h *Handler) adminTransferOrg(c *gin.Context) (uuid.UUID, bool) {
|
|
orgID, ok := parseUUIDParam(c, "id")
|
|
if !ok {
|
|
return uuid.Nil, false
|
|
}
|
|
org, err := h.OrgRepo.GetByID(c.Request.Context(), orgID)
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.Internal, err.Error()))
|
|
return uuid.Nil, false
|
|
}
|
|
if org == nil {
|
|
errx.JSON(c, errx.New(errx.NotFound, "organization not found"))
|
|
return uuid.Nil, false
|
|
}
|
|
if h.OrgTransferService == nil {
|
|
errx.JSON(c, errx.New(errx.NotImplemented, "Workspace transfer is not available on this instance."))
|
|
return uuid.Nil, false
|
|
}
|
|
return orgID, true
|
|
}
|
|
|
|
// ---------- export ----------
|
|
|
|
// AdminListOrgExports returns a workspace's recent archive builds.
|
|
//
|
|
// GET /admin/organizations/:id/exports
|
|
func (h *Handler) AdminListOrgExports(c *gin.Context) {
|
|
orgID, ok := h.adminTransferOrg(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
jobs, xerr := h.OrgTransferService.ListExports(c.Request.Context(), orgID)
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"data": jobs})
|
|
}
|
|
|
|
// AdminCreateOrgExport starts an archive build on the operator's behalf.
|
|
//
|
|
// POST /admin/organizations/:id/exports
|
|
func (h *Handler) AdminCreateOrgExport(c *gin.Context) {
|
|
orgID, ok := h.adminTransferOrg(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req models.CreateOrgExportRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid request body"))
|
|
return
|
|
}
|
|
|
|
job, xerr := h.OrgTransferService.RequestExport(c.Request.Context(), orgID, middleware.GetAdminUserID(c), &req)
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
|
|
h.audit(c, models.AuditActionExport, models.AuditEntityOrgArchive, &job.ID, map[string]string{
|
|
"organization_id": orgID.String(),
|
|
"include_secrets": fmt.Sprintf("%t", job.IncludeSecrets),
|
|
"groups": fmt.Sprintf("%d", len(job.Groups)),
|
|
})
|
|
c.JSON(http.StatusAccepted, job)
|
|
}
|
|
|
|
// AdminGetOrgExport returns one archive build, for progress polling.
|
|
//
|
|
// GET /admin/organizations/:id/exports/:exportId
|
|
func (h *Handler) AdminGetOrgExport(c *gin.Context) {
|
|
orgID, ok := h.adminTransferOrg(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
id, ok := parseUUIDParam(c, "exportId")
|
|
if !ok {
|
|
return
|
|
}
|
|
job, xerr := h.OrgTransferService.GetExport(c.Request.Context(), orgID, id)
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, job)
|
|
}
|
|
|
|
// AdminDownloadOrgExport streams a finished archive.
|
|
//
|
|
// GET /admin/organizations/:id/exports/:exportId/download
|
|
func (h *Handler) AdminDownloadOrgExport(c *gin.Context) {
|
|
orgID, ok := h.adminTransferOrg(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
id, ok := parseUUIDParam(c, "exportId")
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
body, job, xerr := h.OrgTransferService.OpenExport(c.Request.Context(), orgID, id)
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
defer body.Close()
|
|
|
|
name := "workspace-" + job.ID.String()[:8] + ".warmbly.zip"
|
|
if org, err := h.OrgRepo.GetByID(c.Request.Context(), orgID); err == nil && org != nil {
|
|
name = orgtransfer.ArchiveFilename(org.Name, job.ID)
|
|
}
|
|
|
|
h.audit(c, models.AuditActionExport, models.AuditEntityOrgArchive, &job.ID, map[string]string{
|
|
"organization_id": orgID.String(),
|
|
"downloaded": "true",
|
|
})
|
|
|
|
c.Header("Content-Type", "application/zip")
|
|
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name))
|
|
c.Header("X-Content-Type-Options", "nosniff")
|
|
if job.ArchiveBytes != nil {
|
|
c.Header("Content-Length", fmt.Sprintf("%d", *job.ArchiveBytes))
|
|
}
|
|
if job.ArchiveSHA256 != nil {
|
|
c.Header("X-Archive-SHA256", *job.ArchiveSHA256)
|
|
}
|
|
|
|
if _, err := io.Copy(c.Writer, body); err != nil {
|
|
// The client hung up mid-download; the status line is already sent.
|
|
return
|
|
}
|
|
}
|
|
|
|
// AdminDeleteOrgExport removes an archive and its stored object.
|
|
//
|
|
// DELETE /admin/organizations/:id/exports/:exportId
|
|
func (h *Handler) AdminDeleteOrgExport(c *gin.Context) {
|
|
orgID, ok := h.adminTransferOrg(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
id, ok := parseUUIDParam(c, "exportId")
|
|
if !ok {
|
|
return
|
|
}
|
|
if xerr := h.OrgTransferService.DeleteExport(c.Request.Context(), orgID, id); xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
h.audit(c, models.AuditActionDelete, models.AuditEntityOrgArchive, &id, map[string]string{
|
|
"organization_id": orgID.String(),
|
|
})
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// ---------- import ----------
|
|
|
|
// AdminListOrgImports returns a workspace's recent imports.
|
|
//
|
|
// GET /admin/organizations/:id/imports
|
|
func (h *Handler) AdminListOrgImports(c *gin.Context) {
|
|
orgID, ok := h.adminTransferOrg(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
jobs, xerr := h.OrgTransferService.ListImports(c.Request.Context(), orgID)
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"data": jobs})
|
|
}
|
|
|
|
// AdminPreflightOrgImport reports what an uploaded archive would do to the
|
|
// workspace, writing nothing.
|
|
//
|
|
// POST /admin/organizations/:id/imports/preflight
|
|
func (h *Handler) AdminPreflightOrgImport(c *gin.Context) {
|
|
orgID, ok := h.adminTransferOrg(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
spooled, xerr := spoolArchiveUpload(c)
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
defer spooled.Close()
|
|
|
|
report, xerr := h.OrgTransferService.Preflight(
|
|
c.Request.Context(), orgID, spooled, c.Request.FormValue("passphrase"))
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, report)
|
|
}
|
|
|
|
// AdminCreateOrgImport applies an uploaded archive to the workspace.
|
|
//
|
|
// POST /admin/organizations/:id/imports
|
|
func (h *Handler) AdminCreateOrgImport(c *gin.Context) {
|
|
orgID, ok := h.adminTransferOrg(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// Ownership of the spooled file passes to the service on success; every
|
|
// failure path before that closes it here.
|
|
spooled, xerr := spoolArchiveUpload(c)
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
|
|
var req models.CreateOrgImportRequest
|
|
if opts := c.Request.FormValue("options"); opts != "" {
|
|
if err := jsonUnmarshalString(opts, &req); err != nil {
|
|
_ = spooled.Close()
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid 'options' JSON: "+err.Error()))
|
|
return
|
|
}
|
|
}
|
|
req.Passphrase = c.Request.FormValue("passphrase")
|
|
|
|
job, xerr := h.OrgTransferService.RequestImport(c.Request.Context(), orgID, middleware.GetAdminUserID(c), spooled, &req)
|
|
if xerr != nil {
|
|
_ = spooled.Close()
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
|
|
h.audit(c, models.AuditActionImport, models.AuditEntityOrgArchive, &job.ID, map[string]string{
|
|
"organization_id": orgID.String(),
|
|
"conflict_strategy": string(job.ConflictStrategy),
|
|
})
|
|
c.JSON(http.StatusAccepted, job)
|
|
}
|