mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-06-03 20:13:01 +00:00
1180 lines
31 KiB
Go
1180 lines
31 KiB
Go
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/go-playground/validator/v10"
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"github.com/tdvorak/primora/apps/backend/internal/middleware"
|
|
"github.com/tdvorak/primora/apps/backend/internal/observability"
|
|
apperrors "github.com/tdvorak/primora/apps/backend/internal/response"
|
|
"github.com/tdvorak/primora/apps/backend/internal/services"
|
|
)
|
|
|
|
type HTTPHandler struct {
|
|
Platform *services.PlatformService
|
|
Validate *validator.Validate
|
|
Metrics *observability.Metrics
|
|
Readiness func(*gin.Context) map[string]any
|
|
}
|
|
|
|
func (h *HTTPHandler) Register(router *gin.Engine) {
|
|
router.GET("/api/v1/health/liveness", h.liveness)
|
|
router.GET("/api/v1/health/readiness", h.readiness)
|
|
router.GET("/api/v1/openapi.yaml", h.openapi)
|
|
|
|
api := router.Group("/api/v1")
|
|
api.GET("/me", h.me)
|
|
api.POST("/bootstrap", h.bootstrap)
|
|
api.GET("/organizations", h.organizations)
|
|
api.POST("/organizations", h.createOrganization)
|
|
api.PATCH("/organizations/:organizationID", h.updateOrganization)
|
|
api.DELETE("/organizations/:organizationID", h.deleteOrganization)
|
|
api.GET("/organizations/:organizationID/members", h.organizationMembers)
|
|
api.PATCH("/organizations/:organizationID/members/:userID", h.updateOrganizationMemberRole)
|
|
api.DELETE("/organizations/:organizationID/members/:userID", h.removeOrganizationMember)
|
|
api.GET("/organizations/:organizationID/projects", h.projects)
|
|
api.POST("/organizations/:organizationID/projects", h.createProject)
|
|
api.GET("/organizations/:organizationID/invitations", h.invitations)
|
|
api.POST("/organizations/:organizationID/invitations", h.createInvitation)
|
|
api.DELETE("/organizations/:organizationID/invitations/:invitationID", h.revokeInvitation)
|
|
api.POST("/invitations/accept", h.acceptInvitation)
|
|
api.GET("/projects/:projectID/overview", h.projectOverview)
|
|
api.GET("/projects/:projectID/api-keys", h.apiKeys)
|
|
api.POST("/projects/:projectID/api-keys", h.createAPIKey)
|
|
api.DELETE("/projects/:projectID/api-keys/:apiKeyID", h.revokeAPIKey)
|
|
api.PATCH("/projects/:projectID", h.updateProject)
|
|
api.DELETE("/projects/:projectID", h.deleteProject)
|
|
api.GET("/projects/:projectID/members", h.projectMembers)
|
|
api.PATCH("/projects/:projectID/members/:userID", h.updateProjectMemberRole)
|
|
api.DELETE("/projects/:projectID/members/:userID", h.removeProjectMember)
|
|
api.GET("/projects/:projectID/buckets", h.buckets)
|
|
api.POST("/projects/:projectID/buckets", h.createBucket)
|
|
api.PATCH("/buckets/:bucketID", h.updateBucket)
|
|
api.DELETE("/buckets/:bucketID", h.deleteBucket)
|
|
api.GET("/projects/:projectID/audit-logs", h.auditLogs)
|
|
api.GET("/projects/:projectID/collections", h.listCollections)
|
|
api.POST("/projects/:projectID/collections", h.createCollection)
|
|
api.GET("/projects/:projectID/collections/:collectionID", h.getCollection)
|
|
api.PATCH("/projects/:projectID/collections/:collectionID", h.updateCollection)
|
|
api.DELETE("/projects/:projectID/collections/:collectionID", h.deleteCollection)
|
|
api.GET("/collections/:collectionID/documents", h.listDocuments)
|
|
api.POST("/collections/:collectionID/documents", h.createDocument)
|
|
api.GET("/collections/:collectionID/documents/:documentID", h.getDocument)
|
|
api.PATCH("/collections/:collectionID/documents/:documentID", h.updateDocument)
|
|
api.DELETE("/collections/:collectionID/documents/:documentID", h.deleteDocument)
|
|
api.GET("/buckets/:bucketID/objects", h.listObjects)
|
|
api.POST("/buckets/:bucketID/objects", h.uploadObject)
|
|
api.POST("/buckets/:bucketID/object-copies", h.copyObject)
|
|
api.GET("/buckets/:bucketID/objects/*objectKey", h.downloadObject)
|
|
api.PATCH("/buckets/:bucketID/objects/*objectKey", h.updateObject)
|
|
api.DELETE("/buckets/:bucketID/objects/*objectKey", h.deleteObject)
|
|
}
|
|
|
|
func (h *HTTPHandler) liveness(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
|
}
|
|
|
|
func (h *HTTPHandler) readiness(c *gin.Context) {
|
|
c.JSON(http.StatusOK, h.Readiness(c))
|
|
}
|
|
|
|
func (h *HTTPHandler) openapi(c *gin.Context) {
|
|
c.File("openapi/openapi.yaml")
|
|
}
|
|
|
|
func (h *HTTPHandler) me(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
result, err := h.Platform.Me(c.Request.Context(), actor)
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
func (h *HTTPHandler) bootstrap(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var body services.BootstrapInput
|
|
if !h.bindAndValidate(c, &body) {
|
|
return
|
|
}
|
|
result, err := h.Platform.Bootstrap(c.Request.Context(), actor, body, middleware.RequestIDFromContext(c))
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, result)
|
|
}
|
|
|
|
func (h *HTTPHandler) organizations(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
result, err := h.Platform.ListOrganizations(c.Request.Context(), actor)
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
items := make([]organizationMembershipResponse, 0, len(result))
|
|
for _, row := range result {
|
|
items = append(items, toOrganizationMembershipResponse(row))
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"items": items})
|
|
}
|
|
|
|
func (h *HTTPHandler) createOrganization(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var body services.CreateOrganizationInput
|
|
if !h.bindAndValidate(c, &body) {
|
|
return
|
|
}
|
|
result, err := h.Platform.CreateOrganization(c.Request.Context(), actor, body, middleware.RequestIDFromContext(c))
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, toOrganizationMembershipResponseFromCore(result, "owner"))
|
|
}
|
|
|
|
func (h *HTTPHandler) deleteOrganization(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
organizationID, ok := parseUUIDParam(c, "organizationID")
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := h.Platform.DeleteOrganization(c.Request.Context(), actor, organizationID, middleware.RequestIDFromContext(c)); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
apperrors.Abort(c, http.StatusNotFound, "not_found", "organization not found")
|
|
return
|
|
}
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *HTTPHandler) updateOrganization(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
organizationID, ok := parseUUIDParam(c, "organizationID")
|
|
if !ok {
|
|
return
|
|
}
|
|
var body services.UpdateOrganizationInput
|
|
if !h.bindAndValidate(c, &body) {
|
|
return
|
|
}
|
|
if _, err := h.Platform.UpdateOrganization(c.Request.Context(), actor, organizationID, body, middleware.RequestIDFromContext(c)); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
apperrors.Abort(c, http.StatusNotFound, "not_found", "organization not found")
|
|
return
|
|
}
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *HTTPHandler) projects(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
organizationID, ok := parseUUIDParam(c, "organizationID")
|
|
if !ok {
|
|
return
|
|
}
|
|
query := strings.TrimSpace(c.Query("q"))
|
|
result, err := h.Platform.ListProjects(c.Request.Context(), actor, organizationID, query)
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"items": toProjectListResponse(result)})
|
|
}
|
|
|
|
func (h *HTTPHandler) organizationMembers(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
organizationID, ok := parseUUIDParam(c, "organizationID")
|
|
if !ok {
|
|
return
|
|
}
|
|
result, err := h.Platform.ListOrganizationMembers(c.Request.Context(), actor, organizationID)
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"items": result})
|
|
}
|
|
|
|
func (h *HTTPHandler) updateOrganizationMemberRole(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
organizationID, ok := parseUUIDParam(c, "organizationID")
|
|
if !ok {
|
|
return
|
|
}
|
|
userID, ok := parseUUIDParam(c, "userID")
|
|
if !ok {
|
|
return
|
|
}
|
|
var body services.UpdateOrganizationMemberRoleInput
|
|
if !h.bindAndValidate(c, &body) {
|
|
return
|
|
}
|
|
result, err := h.Platform.UpdateOrganizationMemberRole(c.Request.Context(), actor, organizationID, userID, body, middleware.RequestIDFromContext(c))
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
apperrors.Abort(c, http.StatusNotFound, "not_found", "organization member not found")
|
|
return
|
|
}
|
|
if strings.Contains(err.Error(), "retain at least one") {
|
|
apperrors.Abort(c, http.StatusConflict, "role_conflict", err.Error())
|
|
return
|
|
}
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
func (h *HTTPHandler) removeOrganizationMember(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
organizationID, ok := parseUUIDParam(c, "organizationID")
|
|
if !ok {
|
|
return
|
|
}
|
|
userID, ok := parseUUIDParam(c, "userID")
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := h.Platform.RemoveOrganizationMember(c.Request.Context(), actor, organizationID, userID, middleware.RequestIDFromContext(c)); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
apperrors.Abort(c, http.StatusNotFound, "not_found", "organization member not found")
|
|
return
|
|
}
|
|
if strings.Contains(err.Error(), "retain at least one") {
|
|
apperrors.Abort(c, http.StatusConflict, "role_conflict", err.Error())
|
|
return
|
|
}
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *HTTPHandler) createProject(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
organizationID, ok := parseUUIDParam(c, "organizationID")
|
|
if !ok {
|
|
return
|
|
}
|
|
var body services.CreateProjectInput
|
|
if !h.bindAndValidate(c, &body) {
|
|
return
|
|
}
|
|
result, err := h.Platform.CreateProject(c.Request.Context(), actor, organizationID, body, middleware.RequestIDFromContext(c))
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, toProjectFromCore(result))
|
|
}
|
|
|
|
func (h *HTTPHandler) deleteProject(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
projectID, ok := parseUUIDParam(c, "projectID")
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := h.Platform.DeleteProject(c.Request.Context(), actor, projectID, middleware.RequestIDFromContext(c)); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
apperrors.Abort(c, http.StatusNotFound, "not_found", "project not found")
|
|
return
|
|
}
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *HTTPHandler) updateProject(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
projectID, ok := parseUUIDParam(c, "projectID")
|
|
if !ok {
|
|
return
|
|
}
|
|
var body services.UpdateProjectInput
|
|
if !h.bindAndValidate(c, &body) {
|
|
return
|
|
}
|
|
if _, err := h.Platform.UpdateProject(c.Request.Context(), actor, projectID, body, middleware.RequestIDFromContext(c)); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
apperrors.Abort(c, http.StatusNotFound, "not_found", "project not found")
|
|
return
|
|
}
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *HTTPHandler) createInvitation(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
organizationID, ok := parseUUIDParam(c, "organizationID")
|
|
if !ok {
|
|
return
|
|
}
|
|
var body services.CreateInvitationInput
|
|
if !h.bindAndValidate(c, &body) {
|
|
return
|
|
}
|
|
result, err := h.Platform.CreateInvitation(c.Request.Context(), actor, organizationID, body, middleware.RequestIDFromContext(c))
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
apperrors.Abort(c, http.StatusNotFound, "not_found", "project not found")
|
|
return
|
|
}
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, result)
|
|
}
|
|
|
|
func (h *HTTPHandler) invitations(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
organizationID, ok := parseUUIDParam(c, "organizationID")
|
|
if !ok {
|
|
return
|
|
}
|
|
result, err := h.Platform.ListInvitations(c.Request.Context(), actor, organizationID)
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"items": result})
|
|
}
|
|
|
|
func (h *HTTPHandler) revokeInvitation(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
organizationID, ok := parseUUIDParam(c, "organizationID")
|
|
if !ok {
|
|
return
|
|
}
|
|
invitationID, ok := parseUUIDParam(c, "invitationID")
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := h.Platform.RevokeInvitation(c.Request.Context(), actor, organizationID, invitationID, middleware.RequestIDFromContext(c)); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
apperrors.Abort(c, http.StatusNotFound, "not_found", "invitation not found")
|
|
return
|
|
}
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *HTTPHandler) acceptInvitation(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var body services.AcceptInvitationInput
|
|
if !h.bindAndValidate(c, &body) {
|
|
return
|
|
}
|
|
if err := h.Platform.AcceptInvitation(c.Request.Context(), actor, body, middleware.RequestIDFromContext(c)); err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *HTTPHandler) projectOverview(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
projectID, ok := parseUUIDParam(c, "projectID")
|
|
if !ok {
|
|
return
|
|
}
|
|
result, err := h.Platform.GetProjectOverview(c.Request.Context(), actor, projectID)
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
func (h *HTTPHandler) apiKeys(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
projectID, ok := parseUUIDParam(c, "projectID")
|
|
if !ok {
|
|
return
|
|
}
|
|
result, err := h.Platform.ListAPIKeys(c.Request.Context(), actor, projectID)
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"items": toAPIKeyListResponse(result)})
|
|
}
|
|
|
|
func (h *HTTPHandler) createAPIKey(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
projectID, ok := parseUUIDParam(c, "projectID")
|
|
if !ok {
|
|
return
|
|
}
|
|
var body services.CreateAPIKeyInput
|
|
if !h.bindAndValidate(c, &body) {
|
|
return
|
|
}
|
|
result, err := h.Platform.CreateAPIKey(c.Request.Context(), actor, projectID, body, middleware.RequestIDFromContext(c))
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, result)
|
|
}
|
|
|
|
func (h *HTTPHandler) revokeAPIKey(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
projectID, ok := parseUUIDParam(c, "projectID")
|
|
if !ok {
|
|
return
|
|
}
|
|
apiKeyID, ok := parseUUIDParam(c, "apiKeyID")
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := h.Platform.RevokeAPIKey(c.Request.Context(), actor, projectID, apiKeyID, middleware.RequestIDFromContext(c)); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
apperrors.Abort(c, http.StatusNotFound, "not_found", "api key not found")
|
|
return
|
|
}
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *HTTPHandler) projectMembers(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
projectID, ok := parseUUIDParam(c, "projectID")
|
|
if !ok {
|
|
return
|
|
}
|
|
result, err := h.Platform.ListProjectMembers(c.Request.Context(), actor, projectID)
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"items": result})
|
|
}
|
|
|
|
func (h *HTTPHandler) updateProjectMemberRole(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
projectID, ok := parseUUIDParam(c, "projectID")
|
|
if !ok {
|
|
return
|
|
}
|
|
userID, ok := parseUUIDParam(c, "userID")
|
|
if !ok {
|
|
return
|
|
}
|
|
var body services.UpdateProjectMemberRoleInput
|
|
if !h.bindAndValidate(c, &body) {
|
|
return
|
|
}
|
|
result, err := h.Platform.UpdateProjectMemberRole(c.Request.Context(), actor, projectID, userID, body, middleware.RequestIDFromContext(c))
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
apperrors.Abort(c, http.StatusNotFound, "not_found", "project member not found")
|
|
return
|
|
}
|
|
if strings.Contains(err.Error(), "retain at least one") {
|
|
apperrors.Abort(c, http.StatusConflict, "role_conflict", err.Error())
|
|
return
|
|
}
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
func (h *HTTPHandler) removeProjectMember(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
projectID, ok := parseUUIDParam(c, "projectID")
|
|
if !ok {
|
|
return
|
|
}
|
|
userID, ok := parseUUIDParam(c, "userID")
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := h.Platform.RemoveProjectMember(c.Request.Context(), actor, projectID, userID, middleware.RequestIDFromContext(c)); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
apperrors.Abort(c, http.StatusNotFound, "not_found", "project member not found")
|
|
return
|
|
}
|
|
if strings.Contains(err.Error(), "retain at least one") {
|
|
apperrors.Abort(c, http.StatusConflict, "role_conflict", err.Error())
|
|
return
|
|
}
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *HTTPHandler) buckets(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
projectID, ok := parseUUIDParam(c, "projectID")
|
|
if !ok {
|
|
return
|
|
}
|
|
query := strings.TrimSpace(c.Query("q"))
|
|
result, err := h.Platform.ListBuckets(c.Request.Context(), actor, projectID, query)
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"items": toBucketListResponse(result)})
|
|
}
|
|
|
|
func (h *HTTPHandler) createBucket(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
projectID, ok := parseUUIDParam(c, "projectID")
|
|
if !ok {
|
|
return
|
|
}
|
|
var body services.CreateBucketInput
|
|
if !h.bindAndValidate(c, &body) {
|
|
return
|
|
}
|
|
result, err := h.Platform.CreateBucket(c.Request.Context(), actor, projectID, body, middleware.RequestIDFromContext(c))
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, toBucketResponse(result))
|
|
}
|
|
|
|
func (h *HTTPHandler) updateBucket(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
bucketID, ok := parseUUIDParam(c, "bucketID")
|
|
if !ok {
|
|
return
|
|
}
|
|
var body services.UpdateBucketInput
|
|
if !h.bindAndValidate(c, &body) {
|
|
return
|
|
}
|
|
if _, err := h.Platform.UpdateBucket(c.Request.Context(), actor, bucketID, body, middleware.RequestIDFromContext(c)); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
apperrors.Abort(c, http.StatusNotFound, "not_found", "bucket not found")
|
|
return
|
|
}
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *HTTPHandler) deleteBucket(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
bucketID, ok := parseUUIDParam(c, "bucketID")
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := h.Platform.DeleteBucket(c.Request.Context(), actor, bucketID, middleware.RequestIDFromContext(c)); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
apperrors.Abort(c, http.StatusNotFound, "not_found", "bucket not found")
|
|
return
|
|
}
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *HTTPHandler) listObjects(c *gin.Context) {
|
|
bucketID, ok := parseUUIDParam(c, "bucketID")
|
|
if !ok {
|
|
return
|
|
}
|
|
limit, ok := parsePaginationQuery(c, "limit", 50, 1, 200)
|
|
if !ok {
|
|
return
|
|
}
|
|
offset, ok := parsePaginationQuery(c, "offset", 0, 0, 100000)
|
|
if !ok {
|
|
return
|
|
}
|
|
query := strings.TrimSpace(c.Query("q"))
|
|
actor, _ := middleware.ActorFromContext(c)
|
|
result, err := h.Platform.ListObjects(c.Request.Context(), actor, bucketID, query, limit, offset)
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
total, err := h.Platform.CountObjects(c.Request.Context(), actor, bucketID, query)
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"items": toObjectListResponse(result),
|
|
"total": total,
|
|
"limit": limit,
|
|
"offset": offset,
|
|
"has_more": int64(offset)+int64(len(result)) < total,
|
|
})
|
|
}
|
|
|
|
func (h *HTTPHandler) uploadObject(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
bucketID, ok := parseUUIDParam(c, "bucketID")
|
|
if !ok {
|
|
return
|
|
}
|
|
fileHeader, err := c.FormFile("file")
|
|
if err != nil {
|
|
apperrors.Abort(c, http.StatusBadRequest, "invalid_file", "multipart file field `file` is required")
|
|
return
|
|
}
|
|
objectKey := c.PostForm("objectKey")
|
|
if objectKey == "" {
|
|
objectKey = fileHeader.Filename
|
|
}
|
|
file, err := fileHeader.Open()
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
defer file.Close()
|
|
result, err := h.Platform.UploadObject(c.Request.Context(), actor, bucketID, strings.TrimPrefix(objectKey, "/"), fileHeader.Header.Get("Content-Type"), file, middleware.RequestIDFromContext(c))
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, toObjectResponse(result))
|
|
}
|
|
|
|
func (h *HTTPHandler) copyObject(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
bucketID, ok := parseUUIDParam(c, "bucketID")
|
|
if !ok {
|
|
return
|
|
}
|
|
var body services.CopyObjectInput
|
|
if !h.bindAndValidate(c, &body) {
|
|
return
|
|
}
|
|
result, err := h.Platform.CopyObject(c.Request.Context(), actor, bucketID, body, middleware.RequestIDFromContext(c))
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
apperrors.Abort(c, http.StatusNotFound, "not_found", "bucket or object not found")
|
|
return
|
|
}
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, toObjectResponse(result))
|
|
}
|
|
|
|
func (h *HTTPHandler) downloadObject(c *gin.Context) {
|
|
bucketID, ok := parseUUIDParam(c, "bucketID")
|
|
if !ok {
|
|
return
|
|
}
|
|
objectKey := strings.TrimPrefix(c.Param("objectKey"), "/")
|
|
actor, _ := middleware.ActorFromContext(c)
|
|
object, reader, err := h.Platform.GetObject(c.Request.Context(), actor, bucketID, objectKey)
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
defer reader.Close()
|
|
c.Header("Content-Type", object.ContentType)
|
|
c.Header("Content-Length", strconv.FormatInt(object.SizeBytes, 10))
|
|
c.Header("ETag", object.ChecksumSha256)
|
|
_, _ = io.Copy(c.Writer, reader)
|
|
}
|
|
|
|
func (h *HTTPHandler) updateObject(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
bucketID, ok := parseUUIDParam(c, "bucketID")
|
|
if !ok {
|
|
return
|
|
}
|
|
objectKey := strings.TrimPrefix(c.Param("objectKey"), "/")
|
|
var body services.UpdateObjectInput
|
|
if !h.bindAndValidate(c, &body) {
|
|
return
|
|
}
|
|
if _, err := h.Platform.UpdateObject(c.Request.Context(), actor, bucketID, objectKey, body, middleware.RequestIDFromContext(c)); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
apperrors.Abort(c, http.StatusNotFound, "not_found", "bucket or object not found")
|
|
return
|
|
}
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *HTTPHandler) deleteObject(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
bucketID, ok := parseUUIDParam(c, "bucketID")
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := h.Platform.DeleteObject(c.Request.Context(), actor, bucketID, strings.TrimPrefix(c.Param("objectKey"), "/"), middleware.RequestIDFromContext(c)); err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *HTTPHandler) auditLogs(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
projectID, ok := parseUUIDParam(c, "projectID")
|
|
if !ok {
|
|
return
|
|
}
|
|
limit, ok := parsePaginationQuery(c, "limit", 50, 1, 200)
|
|
if !ok {
|
|
return
|
|
}
|
|
offset, ok := parsePaginationQuery(c, "offset", 0, 0, 100000)
|
|
if !ok {
|
|
return
|
|
}
|
|
query := strings.TrimSpace(c.Query("q"))
|
|
action := strings.TrimSpace(c.Query("action"))
|
|
result, err := h.Platform.ListAuditLogs(c.Request.Context(), actor, projectID, query, action, limit, offset)
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
total, err := h.Platform.CountAuditLogs(c.Request.Context(), actor, projectID, query, action)
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"items": toAuditLogListResponse(result),
|
|
"total": total,
|
|
"limit": limit,
|
|
"offset": offset,
|
|
"has_more": int64(offset)+int64(len(result)) < total,
|
|
})
|
|
}
|
|
|
|
func (h *HTTPHandler) listCollections(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
projectID, ok := parseUUIDParam(c, "projectID")
|
|
if !ok {
|
|
return
|
|
}
|
|
result, err := h.Platform.ListCollections(c.Request.Context(), actor, projectID)
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"items": toCollectionListResponse(result)})
|
|
}
|
|
|
|
func (h *HTTPHandler) createCollection(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
projectID, ok := parseUUIDParam(c, "projectID")
|
|
if !ok {
|
|
return
|
|
}
|
|
var body services.CreateCollectionInput
|
|
if !h.bindAndValidate(c, &body) {
|
|
return
|
|
}
|
|
result, err := h.Platform.CreateCollection(c.Request.Context(), actor, projectID, body, middleware.RequestIDFromContext(c))
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, toCollectionResponse(result))
|
|
}
|
|
|
|
func (h *HTTPHandler) getCollection(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
projectID, ok := parseUUIDParam(c, "projectID")
|
|
if !ok {
|
|
return
|
|
}
|
|
collectionID, ok := parseUUIDParam(c, "collectionID")
|
|
if !ok {
|
|
return
|
|
}
|
|
result, err := h.Platform.GetCollection(c.Request.Context(), actor, projectID, collectionID)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
apperrors.Abort(c, http.StatusNotFound, "not_found", "collection not found")
|
|
return
|
|
}
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, toCollectionResponse(result))
|
|
}
|
|
|
|
func (h *HTTPHandler) updateCollection(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
projectID, ok := parseUUIDParam(c, "projectID")
|
|
if !ok {
|
|
return
|
|
}
|
|
collectionID, ok := parseUUIDParam(c, "collectionID")
|
|
if !ok {
|
|
return
|
|
}
|
|
var body services.UpdateCollectionInput
|
|
if !h.bindAndValidate(c, &body) {
|
|
return
|
|
}
|
|
result, err := h.Platform.UpdateCollection(c.Request.Context(), actor, projectID, collectionID, body, middleware.RequestIDFromContext(c))
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
apperrors.Abort(c, http.StatusNotFound, "not_found", "collection not found")
|
|
return
|
|
}
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, toCollectionResponse(result))
|
|
}
|
|
|
|
func (h *HTTPHandler) deleteCollection(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
projectID, ok := parseUUIDParam(c, "projectID")
|
|
if !ok {
|
|
return
|
|
}
|
|
collectionID, ok := parseUUIDParam(c, "collectionID")
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := h.Platform.DeleteCollection(c.Request.Context(), actor, projectID, collectionID, middleware.RequestIDFromContext(c)); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
apperrors.Abort(c, http.StatusNotFound, "not_found", "collection not found")
|
|
return
|
|
}
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *HTTPHandler) listDocuments(c *gin.Context) {
|
|
collectionID, ok := parseUUIDParam(c, "collectionID")
|
|
if !ok {
|
|
return
|
|
}
|
|
limit, ok := parsePaginationQuery(c, "limit", 50, 1, 200)
|
|
if !ok {
|
|
return
|
|
}
|
|
offset, ok := parsePaginationQuery(c, "offset", 0, 0, 100000)
|
|
if !ok {
|
|
return
|
|
}
|
|
actor, _ := middleware.ActorFromContext(c)
|
|
result, err := h.Platform.ListDocuments(c.Request.Context(), actor, collectionID, limit, offset)
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
total, err := h.Platform.CountDocuments(c.Request.Context(), actor, collectionID)
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"items": toDocumentListResponse(result),
|
|
"total": total,
|
|
"limit": limit,
|
|
"offset": offset,
|
|
"has_more": int64(offset)+int64(len(result)) < total,
|
|
})
|
|
}
|
|
|
|
func (h *HTTPHandler) createDocument(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
collectionID, ok := parseUUIDParam(c, "collectionID")
|
|
if !ok {
|
|
return
|
|
}
|
|
var body services.CreateDocumentInput
|
|
if !h.bindAndValidate(c, &body) {
|
|
return
|
|
}
|
|
result, err := h.Platform.CreateDocument(c.Request.Context(), actor, collectionID, body, middleware.RequestIDFromContext(c))
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, toDocumentResponse(result))
|
|
}
|
|
|
|
func (h *HTTPHandler) getDocument(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
collectionID, ok := parseUUIDParam(c, "collectionID")
|
|
if !ok {
|
|
return
|
|
}
|
|
documentID, ok := parseUUIDParam(c, "documentID")
|
|
if !ok {
|
|
return
|
|
}
|
|
result, err := h.Platform.GetDocument(c.Request.Context(), actor, collectionID, documentID)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
apperrors.Abort(c, http.StatusNotFound, "not_found", "document not found")
|
|
return
|
|
}
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, toDocumentResponse(result))
|
|
}
|
|
|
|
func (h *HTTPHandler) updateDocument(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
collectionID, ok := parseUUIDParam(c, "collectionID")
|
|
if !ok {
|
|
return
|
|
}
|
|
documentID, ok := parseUUIDParam(c, "documentID")
|
|
if !ok {
|
|
return
|
|
}
|
|
var body services.UpdateDocumentInput
|
|
if !h.bindAndValidate(c, &body) {
|
|
return
|
|
}
|
|
result, err := h.Platform.UpdateDocument(c.Request.Context(), actor, collectionID, documentID, body, middleware.RequestIDFromContext(c))
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
apperrors.Abort(c, http.StatusNotFound, "not_found", "document not found")
|
|
return
|
|
}
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, toDocumentResponse(result))
|
|
}
|
|
|
|
func (h *HTTPHandler) deleteDocument(c *gin.Context) {
|
|
actor, ok := middleware.RequireActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
collectionID, ok := parseUUIDParam(c, "collectionID")
|
|
if !ok {
|
|
return
|
|
}
|
|
documentID, ok := parseUUIDParam(c, "documentID")
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := h.Platform.DeleteDocument(c.Request.Context(), actor, collectionID, documentID, middleware.RequestIDFromContext(c)); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
apperrors.Abort(c, http.StatusNotFound, "not_found", "document not found")
|
|
return
|
|
}
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func parsePaginationQuery(c *gin.Context, name string, defaultValue, minValue, maxValue int32) (int32, bool) {
|
|
raw := strings.TrimSpace(c.Query(name))
|
|
if raw == "" {
|
|
return defaultValue, true
|
|
}
|
|
value, err := strconv.Atoi(raw)
|
|
if err != nil {
|
|
apperrors.Abort(c, http.StatusBadRequest, "invalid_query", "invalid "+name)
|
|
return 0, false
|
|
}
|
|
intValue := int32(value)
|
|
if intValue < minValue || intValue > maxValue {
|
|
apperrors.Abort(c, http.StatusBadRequest, "invalid_query", name+" out of range")
|
|
return 0, false
|
|
}
|
|
return intValue, true
|
|
}
|
|
|
|
func (h *HTTPHandler) bindAndValidate(c *gin.Context, target any) bool {
|
|
if err := c.ShouldBindJSON(target); err != nil {
|
|
apperrors.Abort(c, http.StatusBadRequest, "invalid_json", err.Error())
|
|
return false
|
|
}
|
|
if err := h.Validate.Struct(target); err != nil {
|
|
apperrors.Abort(c, http.StatusBadRequest, "validation_failed", err.Error())
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (h *HTTPHandler) handleError(c *gin.Context, err error) {
|
|
switch {
|
|
case errors.Is(err, io.EOF):
|
|
apperrors.Abort(c, http.StatusBadRequest, "invalid_body", "request body is empty")
|
|
default:
|
|
status := http.StatusInternalServerError
|
|
message := strings.ToLower(err.Error())
|
|
if strings.Contains(message, "insufficient") || strings.Contains(message, "access denied") {
|
|
status = http.StatusForbidden
|
|
}
|
|
if strings.Contains(message, "already") || strings.Contains(message, "exists") || strings.Contains(message, "duplicate key") || strings.Contains(message, "unique") {
|
|
status = http.StatusConflict
|
|
}
|
|
if strings.Contains(message, "expired") || strings.Contains(message, "invalid") {
|
|
status = http.StatusBadRequest
|
|
}
|
|
apperrors.Abort(c, status, "request_failed", err.Error())
|
|
}
|
|
}
|
|
|
|
func parseUUIDParam(c *gin.Context, name string) (uuid.UUID, bool) {
|
|
value := c.Param(name)
|
|
id, err := uuid.Parse(value)
|
|
if err != nil {
|
|
apperrors.Abort(c, http.StatusBadRequest, "invalid_id", "invalid "+name)
|
|
return uuid.Nil, false
|
|
}
|
|
return id, true
|
|
}
|