This is an automated email from the ASF dual-hosted git repository.

lahirujayathilake pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git


The following commit(s) were added to refs/heads/master by this push:
     new 66f4e38b5 Temp account creation  (#495)
66f4e38b5 is described below

commit 66f4e38b5045b0849b63801cdb2532cb0df5fa0b
Author: Dimuthu Wannipurage <[email protected]>
AuthorDate: Fri Jun 19 00:35:46 2026 -0400

    Temp account creation  (#495)
    
    * Initial change for global config
    
    * Fixing loaders
    
    * Removing un used config loading
    
    * Intial temp account connector impl
    
    * Adding user type to user
    
    * Assigning an allocation for a temp account
    
    * Adding support apis and integrate with core
    
    * Rename TempAccount loader package to tempaccount to drop import alias
    
    * Include license headers
    
    * lign AMIE handlers with pkg/common JSON helpers
    
    ---------
    
    Co-authored-by: lahiruj <[email protected]>
---
 config/custos.yaml                                 |   4 +
 .../ACCESS/AMIE-Processor/server/handlers.go       |  56 +--
 connectors/TempAccount/internal/handlers.go        | 187 +++++++++
 connectors/TempAccount/pkg/tempaccount/loader.go   |  36 ++
 internal/connectors/loader.go                      |   5 +-
 internal/db/migrations/000015_user_type.down.sql   |  18 +
 internal/db/migrations/000015_user_type.up.sql     |  18 +
 internal/server/audit_handlers.go                  |  29 +-
 internal/server/auth.go                            |   7 +-
 internal/server/privilege.go                       |  41 +-
 internal/server/role.go                            |  89 ++--
 internal/server/server.go                          | 446 ++++++++++-----------
 internal/store/user_store.go                       |  12 +-
 pkg/common/support.go                              |  62 +++
 pkg/models/project.go                              |  26 ++
 pkg/service/integration_common_test.go             |   4 +-
 pkg/service/user.go                                |   6 +
 17 files changed, 681 insertions(+), 365 deletions(-)

diff --git a/config/custos.yaml b/config/custos.yaml
index ca8e749f1..6296d21a7 100644
--- a/config/custos.yaml
+++ b/config/custos.yaml
@@ -58,3 +58,7 @@ connectors:
     timeouts:
       connect_timeout: "5s"
       read_timeout: "20s"
+
+  temp-account:
+    type: "temp-account"
+    enabled: true
diff --git a/connectors/ACCESS/AMIE-Processor/server/handlers.go 
b/connectors/ACCESS/AMIE-Processor/server/handlers.go
index bd4dfb36b..feb4f60f3 100644
--- a/connectors/ACCESS/AMIE-Processor/server/handlers.go
+++ b/connectors/ACCESS/AMIE-Processor/server/handlers.go
@@ -19,7 +19,6 @@
 package server
 
 import (
-       "encoding/json"
        "errors"
        "net/http"
        "strconv"
@@ -27,6 +26,7 @@ import (
        "time"
 
        
"github.com/apache/airavata-custos/connectors/ACCESS/AMIE-Processor/store"
+       "github.com/apache/airavata-custos/pkg/common"
 )
 
 type Handlers struct {
@@ -65,20 +65,20 @@ func (h *Handlers) RegisterRoutes(mux *http.ServeMux) {
 // @Router     /connectors/amie/packets/{packet_id}/audits [get]
 func (h *Handlers) listPacketAudits(w http.ResponseWriter, r *http.Request) {
        if h.audits == nil {
-               writeError(w, http.StatusServiceUnavailable, errors.New("amie 
packet audit store not configured"))
+               common.WriteError(w, http.StatusServiceUnavailable, 
errors.New("amie packet audit store not configured"))
                return
        }
        packetID := strings.TrimSpace(r.PathValue("packet_id"))
        if packetID == "" {
-               writeError(w, http.StatusBadRequest, errors.New("packet_id is 
required"))
+               common.WriteError(w, http.StatusBadRequest, 
errors.New("packet_id is required"))
                return
        }
        events, err := h.audits.ListAuditsForPacket(r.Context(), packetID)
        if err != nil {
-               writeError(w, http.StatusInternalServerError, err)
+               common.WriteError(w, http.StatusInternalServerError, err)
                return
        }
-       writeJSON(w, http.StatusOK, map[string]any{
+       common.WriteJSON(w, http.StatusOK, map[string]any{
                "packet_id": packetID,
                "events":    events,
        })
@@ -101,24 +101,24 @@ func (h *Handlers) listPacketAudits(w 
http.ResponseWriter, r *http.Request) {
 // @Router     /connectors/amie/packets [get]
 func (h *Handlers) listPackets(w http.ResponseWriter, r *http.Request) {
        if h.packets == nil {
-               writeJSON(w, http.StatusOK, emptyPacketPage(parseLimit(r), 
parseOffset(r)))
+               common.WriteJSON(w, http.StatusOK, 
emptyPacketPage(parseLimit(r), parseOffset(r)))
                return
        }
        f, err := parsePacketFilter(r)
        if err != nil {
-               writeError(w, http.StatusBadRequest, err)
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        rows, total, err := h.packets.ListPackets(r.Context(), f)
        if err != nil {
-               writeError(w, http.StatusInternalServerError, err)
+               common.WriteError(w, http.StatusInternalServerError, err)
                return
        }
        items := make([]PacketResponse, 0, len(rows))
        for _, p := range rows {
                items = append(items, packetResponseFrom(p))
        }
-       writeJSON(w, http.StatusOK, PacketListResponse{
+       common.WriteJSON(w, http.StatusOK, PacketListResponse{
                Packets: items,
                Total:   total,
                Limit:   effectiveLimit(f.Limit),
@@ -137,19 +137,19 @@ func (h *Handlers) listPackets(w http.ResponseWriter, r 
*http.Request) {
 // @Router     /connectors/amie/packets/{id} [get]
 func (h *Handlers) getPacket(w http.ResponseWriter, r *http.Request) {
        if h.packets == nil {
-               writeError(w, http.StatusNotFound, errors.New("not found"))
+               common.WriteError(w, http.StatusNotFound, errors.New("not 
found"))
                return
        }
        p, err := h.packets.FindByID(r.Context(), r.PathValue("id"))
        if err != nil {
-               writeError(w, http.StatusInternalServerError, err)
+               common.WriteError(w, http.StatusInternalServerError, err)
                return
        }
        if p == nil {
-               writeError(w, http.StatusNotFound, errors.New("not found"))
+               common.WriteError(w, http.StatusNotFound, errors.New("not 
found"))
                return
        }
-       writeJSON(w, http.StatusOK, packetResponseFrom(*p))
+       common.WriteJSON(w, http.StatusOK, packetResponseFrom(*p))
 }
 
 // @Summary    List processing events for an AMIE packet
@@ -162,19 +162,19 @@ func (h *Handlers) getPacket(w http.ResponseWriter, r 
*http.Request) {
 // @Router     /connectors/amie/packets/{id}/events [get]
 func (h *Handlers) listPacketEvents(w http.ResponseWriter, r *http.Request) {
        if h.packets == nil {
-               writeJSON(w, http.StatusOK, []PacketEventResponse{})
+               common.WriteJSON(w, http.StatusOK, []PacketEventResponse{})
                return
        }
        rows, err := h.packets.ListPacketEvents(r.Context(), r.PathValue("id"))
        if err != nil {
-               writeError(w, http.StatusInternalServerError, err)
+               common.WriteError(w, http.StatusInternalServerError, err)
                return
        }
        out := make([]PacketEventResponse, 0, len(rows))
        for _, e := range rows {
                out = append(out, packetEventResponseFrom(e))
        }
-       writeJSON(w, http.StatusOK, out)
+       common.WriteJSON(w, http.StatusOK, out)
 }
 
 // @Summary    Per-day packet stats grouped by status and type
@@ -187,16 +187,16 @@ func (h *Handlers) listPacketEvents(w 
http.ResponseWriter, r *http.Request) {
 // @Router     /connectors/amie/stats [get]
 func (h *Handlers) getStats(w http.ResponseWriter, r *http.Request) {
        if h.packets == nil {
-               writeJSON(w, http.StatusOK, PacketStatsResponse{ByDay: 
[]PacketStatBucketResponse{}})
+               common.WriteJSON(w, http.StatusOK, PacketStatsResponse{ByDay: 
[]PacketStatBucketResponse{}})
                return
        }
        window := parseWindow(r.URL.Query().Get("window"))
        buckets, err := h.packets.GetStats(r.Context(), window)
        if err != nil {
-               writeError(w, http.StatusInternalServerError, err)
+               common.WriteError(w, http.StatusInternalServerError, err)
                return
        }
-       writeJSON(w, http.StatusOK, packetStatsResponseFrom(buckets))
+       common.WriteJSON(w, http.StatusOK, packetStatsResponseFrom(buckets))
 }
 
 // ReplyListResponse is the paginated list envelope for connector replies.
@@ -216,7 +216,7 @@ type ReplyListResponse struct {
 // @Success    200     {object}        ReplyListResponse
 // @Router     /connectors/amie/replies [get]
 func (h *Handlers) listReplies(w http.ResponseWriter, r *http.Request) {
-       writeJSON(w, http.StatusOK, ReplyListResponse{
+       common.WriteJSON(w, http.StatusOK, ReplyListResponse{
                Replies: []any{},
                Total:   0,
                Limit:   effectiveLimit(parseLimit(r)),
@@ -233,7 +233,7 @@ func (h *Handlers) listReplies(w http.ResponseWriter, r 
*http.Request) {
 // @Success    200     {object}        PacketListResponse
 // @Router     /connectors/amie/unmapped [get]
 func (h *Handlers) listUnmapped(w http.ResponseWriter, r *http.Request) {
-       writeJSON(w, http.StatusOK, emptyPacketPage(parseLimit(r), 
parseOffset(r)))
+       common.WriteJSON(w, http.StatusOK, emptyPacketPage(parseLimit(r), 
parseOffset(r)))
 }
 
 // @Summary    Retry an AMIE packet (not yet implemented)
@@ -281,7 +281,7 @@ func (h *Handlers) linkUnmapped(w http.ResponseWriter, _ 
*http.Request) {
 }
 
 func writeNotImplemented(w http.ResponseWriter, op string) {
-       writeJSON(w, http.StatusNotImplemented, map[string]string{
+       common.WriteJSON(w, http.StatusNotImplemented, map[string]string{
                "error":   "not_implemented",
                "message": op + " not supported on this branch",
        })
@@ -362,15 +362,3 @@ func parseWindow(v string) time.Duration {
        }
        return 30 * 24 * time.Hour
 }
-
-func writeJSON(w http.ResponseWriter, status int, v any) {
-       w.Header().Set("Content-Type", "application/json")
-       w.WriteHeader(status)
-       _ = json.NewEncoder(w).Encode(v)
-}
-
-func writeError(w http.ResponseWriter, status int, err error) {
-       w.Header().Set("Content-Type", "application/json")
-       w.WriteHeader(status)
-       _ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
-}
diff --git a/connectors/TempAccount/internal/handlers.go 
b/connectors/TempAccount/internal/handlers.go
new file mode 100644
index 000000000..32ee121a1
--- /dev/null
+++ b/connectors/TempAccount/internal/handlers.go
@@ -0,0 +1,187 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package internal
+
+import (
+       "errors"
+       "net/http"
+
+       "github.com/apache/airavata-custos/pkg/common"
+       "github.com/apache/airavata-custos/pkg/models"
+       "github.com/apache/airavata-custos/pkg/service"
+)
+
+type Handlers struct {
+       coreService *service.Service
+}
+
+func NewHandlers(coreService *service.Service) *Handlers {
+       return &Handlers{coreService: coreService}
+}
+
+// RegisterRoutes attaches the TempAccount connector's HTTP endpoints to mux.
+func (h *Handlers) RegisterRoutes(mux *http.ServeMux) {
+       mux.HandleFunc("/connectors/temp-account/create", h.createTempAccount)
+       mux.HandleFunc("/connectors/temp-account/assign-allocation", 
h.assignAllocationToTempAccount)
+       mux.HandleFunc("/connectors/temp-account/update-allocation", 
h.updateAllocationToTempAccount)
+       mux.HandleFunc("/connectors/temp-account/remove/{user_id}", 
h.removeTempAccount)
+       mux.HandleFunc("/connectors/temp-account/membership/{user_id}", 
h.getAllocationMembershipForTempUser)
+}
+
+func (h *Handlers) createTempAccount(w http.ResponseWriter, r *http.Request) {
+       var u models.User
+       if err := common.DecodeJSON(r, &u); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
+               return
+       }
+
+       if u.Type != models.UserTypeVirtual {
+               common.WriteError(w, http.StatusBadRequest, errors.New("User 
type must be VIRTUAL"))
+               return
+       }
+
+       created, err := h.coreService.CreateUser(r.Context(), &u)
+
+       if err != nil {
+               common.WriteServiceError(w, err)
+               return
+       }
+       common.WriteJSON(w, http.StatusCreated, created)
+}
+
+func (h *Handlers) removeTempAccount(w http.ResponseWriter, r *http.Request) {
+       userID := r.PathValue("user_id")
+       if userID == "" {
+               common.WriteError(w, http.StatusBadRequest, errors.New("user_id 
query parameter is required"))
+               return
+       }
+
+       user, err := h.coreService.GetUser(r.Context(), userID)
+       if err != nil {
+               common.WriteServiceError(w, err)
+               return
+       }
+       if user == nil {
+               common.WriteError(w, http.StatusNotFound, errors.New("user not 
found"))
+               return
+       }
+
+       if user.Type != models.UserTypeVirtual {
+               common.WriteError(w, http.StatusBadRequest, errors.New("User 
type must be VIRTUAL"))
+               return
+       }
+
+       err = h.coreService.DeleteUser(r.Context(), userID)
+       if err != nil {
+               common.WriteServiceError(w, err)
+               return
+       }
+       w.WriteHeader(http.StatusNoContent)
+}
+
+func (h *Handlers) assignAllocationToTempAccount(w http.ResponseWriter, r 
*http.Request) {
+       var m models.ComputeAllocationMembership
+       if err := common.DecodeJSON(r, &m); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
+               return
+       }
+
+       if m.UserID == "" || m.ComputeAllocationID == "" {
+               common.WriteError(w, http.StatusBadRequest, errors.New("UserID 
and ComputeAllocationID must be provided"))
+               return
+       }
+
+       // In future, we may want to check that the user already has a 
membership to an allocation
+       // If that's the case, prevent user from getting membershup to multiple 
allocations, which TempAccount connector doesn't support.
+
+       user, err := h.coreService.GetUser(r.Context(), m.UserID)
+
+       if err != nil {
+               common.WriteServiceError(w, err)
+               return
+       }
+
+       if user.Type != models.UserTypeVirtual {
+               common.WriteError(w, http.StatusBadRequest, errors.New("User 
type must be VIRTUAL"))
+               return
+       }
+
+       assigned, err := 
h.coreService.CreateComputeAllocationMembership(r.Context(), &m)
+
+       if err != nil {
+               common.WriteServiceError(w, err)
+               return
+       }
+       common.WriteJSON(w, http.StatusOK, assigned)
+}
+
+func (h *Handlers) updateAllocationToTempAccount(w http.ResponseWriter, r 
*http.Request) {
+       var m models.ComputeAllocationMembership
+       if err := common.DecodeJSON(r, &m); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
+               return
+       }
+
+       if m.UserID == "" || m.ComputeAllocationID == "" {
+               common.WriteError(w, http.StatusBadRequest, errors.New("UserID 
and ComputeAllocationID must be provided"))
+               return
+       }
+
+       updated, err := 
h.coreService.UpdateComputeAllocationMembership(r.Context(), &m)
+
+       if err != nil {
+               common.WriteServiceError(w, err)
+               return
+       }
+       common.WriteJSON(w, http.StatusOK, updated)
+}
+
+func (h *Handlers) getAllocationMembershipForTempUser(w http.ResponseWriter, r 
*http.Request) {
+       userID := r.PathValue("user_id")
+       if userID == "" {
+               common.WriteError(w, http.StatusBadRequest, errors.New("user_id 
query parameter is required"))
+               return
+       }
+
+       user, err := h.coreService.GetUser(r.Context(), userID)
+       if err != nil {
+               common.WriteServiceError(w, err)
+               return
+       }
+       if user == nil {
+               common.WriteError(w, http.StatusNotFound, errors.New("user not 
found"))
+               return
+       }
+
+       if user.Type != models.UserTypeVirtual {
+               common.WriteError(w, http.StatusBadRequest, errors.New("User 
type must be VIRTUAL"))
+               return
+       }
+
+       memberships, err := h.coreService.ListAllocationsForUser(r.Context(), 
userID)
+       if err != nil {
+               common.WriteServiceError(w, err)
+               return
+       }
+       if memberships == nil {
+               common.WriteError(w, http.StatusNotFound, 
errors.New("membership not found for user"))
+               return
+       }
+
+       common.WriteJSON(w, http.StatusOK, memberships)
+}
diff --git a/connectors/TempAccount/pkg/tempaccount/loader.go 
b/connectors/TempAccount/pkg/tempaccount/loader.go
new file mode 100644
index 000000000..6ad36f307
--- /dev/null
+++ b/connectors/TempAccount/pkg/tempaccount/loader.go
@@ -0,0 +1,36 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package tempaccount
+
+import (
+       "context"
+       "net/http"
+       "sync"
+
+       "github.com/apache/airavata-custos/connectors/TempAccount/internal"
+       "github.com/apache/airavata-custos/internal/config"
+       "github.com/apache/airavata-custos/pkg/events"
+       "github.com/apache/airavata-custos/pkg/service"
+       "github.com/jmoiron/sqlx"
+)
+
+func LoadConnector(ctx context.Context, _ *sqlx.DB, eventBus *events.Bus, 
coreService *service.Service, wg *sync.WaitGroup, mux *http.ServeMux, 
connectorConfig *config.ConnectorConfig) error {
+       handlers := internal.NewHandlers(coreService)
+       handlers.RegisterRoutes(mux)
+       return nil
+}
diff --git a/internal/connectors/loader.go b/internal/connectors/loader.go
index a5e487a44..f9190d9c4 100644
--- a/internal/connectors/loader.go
+++ b/internal/connectors/loader.go
@@ -23,15 +23,15 @@ import (
        "net/http"
        "sync"
 
-       "github.com/jmoiron/sqlx"
-
        
"github.com/apache/airavata-custos/connectors/ACCESS/AMIE-Processor/pkg/amie"
        
"github.com/apache/airavata-custos/connectors/COmanage/Identity-Provisioner/pkg/comanage"
        
"github.com/apache/airavata-custos/connectors/SLURM/Association-Mapper/pkg/smapper"
        
"github.com/apache/airavata-custos/connectors/SLURM/Usage-Monitor/pkg/monitor"
+       
"github.com/apache/airavata-custos/connectors/TempAccount/pkg/tempaccount"
        "github.com/apache/airavata-custos/internal/config"
        "github.com/apache/airavata-custos/pkg/events"
        "github.com/apache/airavata-custos/pkg/service"
+       "github.com/jmoiron/sqlx"
 )
 
 func LoadConnectorsFromConfig(ctx context.Context, cfg *config.Config, 
database *sqlx.DB, eventBus *events.Bus, coreService *service.Service, wg 
*sync.WaitGroup, mux *http.ServeMux) error {
@@ -42,6 +42,7 @@ func LoadConnectorsFromConfig(ctx context.Context, cfg 
*config.Config, database
                "amie-processor":                amie.LoadConnector,
                "comanage-identity-provisioner": comanage.LoadConnector,
                "slurm-usage-monitor":           monitor.LoadConnector,
+               "temp-account":                  tempaccount.LoadConnector,
        }
 
        for connectorName, connectorCfg := range cfg.Connectors {
diff --git a/internal/db/migrations/000015_user_type.down.sql 
b/internal/db/migrations/000015_user_type.down.sql
new file mode 100644
index 000000000..f0849b478
--- /dev/null
+++ b/internal/db/migrations/000015_user_type.down.sql
@@ -0,0 +1,18 @@
+-- Licensed to the Apache Software Foundation (ASF) under one
+-- or more contributor license agreements.  See the NOTICE file
+-- distributed with this work for additional information
+-- regarding copyright ownership.  The ASF licenses this file
+-- to you under the Apache License, Version 2.0 (the
+-- "License"); you may not use this file except in compliance
+-- with the License.  You may obtain a copy of the License at
+--
+--   http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing,
+-- software distributed under the License is distributed on an
+-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+-- KIND, either express or implied.  See the License for the
+-- specific language governing permissions and limitations
+-- under the License.
+
+ALTER TABLE users DROP COLUMN type;
diff --git a/internal/db/migrations/000015_user_type.up.sql 
b/internal/db/migrations/000015_user_type.up.sql
new file mode 100644
index 000000000..43501e547
--- /dev/null
+++ b/internal/db/migrations/000015_user_type.up.sql
@@ -0,0 +1,18 @@
+-- Licensed to the Apache Software Foundation (ASF) under one
+-- or more contributor license agreements.  See the NOTICE file
+-- distributed with this work for additional information
+-- regarding copyright ownership.  The ASF licenses this file
+-- to you under the Apache License, Version 2.0 (the
+-- "License"); you may not use this file except in compliance
+-- with the License.  You may obtain a copy of the License at
+--
+--   http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing,
+-- software distributed under the License is distributed on an
+-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+-- KIND, either express or implied.  See the License for the
+-- specific language governing permissions and limitations
+-- under the License.
+
+ALTER TABLE users ADD COLUMN type VARCHAR(32) NOT NULL DEFAULT 'CLUSTER_LOCAL';
diff --git a/internal/server/audit_handlers.go 
b/internal/server/audit_handlers.go
index e3ba7a450..ea785daf6 100644
--- a/internal/server/audit_handlers.go
+++ b/internal/server/audit_handlers.go
@@ -25,6 +25,7 @@ import (
        "time"
 
        "github.com/apache/airavata-custos/internal/store"
+       "github.com/apache/airavata-custos/pkg/common"
        "github.com/apache/airavata-custos/pkg/models"
 )
 
@@ -39,7 +40,7 @@ const (
 
 func (s *Server) requireAuditStore(w http.ResponseWriter) 
(store.AuditTraceStore, bool) {
        if s.admin == nil || s.admin.AuditTraces == nil {
-               writeError(w, http.StatusServiceUnavailable, errors.New("audit 
trace store not configured"))
+               common.WriteError(w, http.StatusServiceUnavailable, 
errors.New("audit trace store not configured"))
                return nil, false
        }
        return s.admin.AuditTraces, true
@@ -68,15 +69,15 @@ func (s *Server) handleListTraces(w http.ResponseWriter, r 
*http.Request) {
        }
        f, err := parseTraceFilter(r)
        if err != nil {
-               writeError(w, http.StatusBadRequest, err)
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        rows, total, err := ts.ListTraces(r.Context(), f)
        if err != nil {
-               writeError(w, http.StatusInternalServerError, err)
+               common.WriteError(w, http.StatusInternalServerError, err)
                return
        }
-       writeJSON(w, http.StatusOK, map[string]any{
+       common.WriteJSON(w, http.StatusOK, map[string]any{
                "traces": rows,
                "total":  total,
                "limit":  f.Limit,
@@ -101,16 +102,16 @@ func (s *Server) handleGetTrace(w http.ResponseWriter, r 
*http.Request) {
        }
        traceID, err := validateHexID(r.PathValue("trace_id"), traceIDHexLen)
        if err != nil {
-               writeError(w, http.StatusBadRequest, err)
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        tree, truncated, err := ts.GetTraceTree(r.Context(), traceID)
        if err != nil {
-               writeError(w, http.StatusInternalServerError, err)
+               common.WriteError(w, http.StatusInternalServerError, err)
                return
        }
        if tree == nil {
-               writeError(w, http.StatusNotFound, errors.New("trace not 
found"))
+               common.WriteError(w, http.StatusNotFound, errors.New("trace not 
found"))
                return
        }
 
@@ -119,7 +120,7 @@ func (s *Server) handleGetTrace(w http.ResponseWriter, r 
*http.Request) {
                "tree":      tree.Children,
                "truncated": truncated,
        }
-       writeJSON(w, http.StatusOK, resp)
+       common.WriteJSON(w, http.StatusOK, resp)
 }
 
 // @Summary    List audit events for a trace (optionally one span)
@@ -140,26 +141,26 @@ func (s *Server) handleListEvents(w http.ResponseWriter, 
r *http.Request) {
        q := r.URL.Query()
        traceID, err := validateHexID(q.Get("trace_id"), traceIDHexLen)
        if err != nil {
-               writeError(w, http.StatusBadRequest, err)
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        var spanID string
        if raw := q.Get("span_id"); raw != "" {
                spanID, err = validateHexID(raw, spanIDHexLen)
                if err != nil {
-                       writeError(w, http.StatusBadRequest, err)
+                       common.WriteError(w, http.StatusBadRequest, err)
                        return
                }
        }
        events, err := ts.ListEvents(r.Context(), traceID, spanID)
        if err != nil {
-               writeError(w, http.StatusInternalServerError, err)
+               common.WriteError(w, http.StatusInternalServerError, err)
                return
        }
        if events == nil {
                events = []models.TraceEvent{}
        }
-       writeJSON(w, http.StatusOK, map[string]any{"events": events})
+       common.WriteJSON(w, http.StatusOK, map[string]any{"events": events})
 }
 
 // @Summary    List distinct audit-event sources
@@ -176,10 +177,10 @@ func (s *Server) handleListSources(w http.ResponseWriter, 
r *http.Request) {
        }
        sources, err := ts.ListSources(r.Context())
        if err != nil {
-               writeError(w, http.StatusInternalServerError, err)
+               common.WriteError(w, http.StatusInternalServerError, err)
                return
        }
-       writeJSON(w, http.StatusOK, map[string]any{"sources": sources})
+       common.WriteJSON(w, http.StatusOK, map[string]any{"sources": sources})
 }
 
 func parseTraceFilter(r *http.Request) (store.TraceFilter, error) {
diff --git a/internal/server/auth.go b/internal/server/auth.go
index b6aaf2a0d..14f5aaefc 100644
--- a/internal/server/auth.go
+++ b/internal/server/auth.go
@@ -24,6 +24,7 @@ import (
        "sync"
        "time"
 
+       "github.com/apache/airavata-custos/pkg/common"
        "github.com/apache/airavata-custos/pkg/models"
 )
 
@@ -122,16 +123,16 @@ func (s *Server) requirePrivilege(p models.PrivilegeKey, 
next http.HandlerFunc)
        return func(w http.ResponseWriter, r *http.Request) {
                callerID := r.Header.Get(callerHeader)
                if callerID == "" {
-                       writeError(w, http.StatusUnauthorized, 
errors.New("missing "+callerHeader+" header"))
+                       common.WriteError(w, http.StatusUnauthorized, 
errors.New("missing "+callerHeader+" header"))
                        return
                }
                profile, err := s.lookupAuthProfile(r.Context(), callerID)
                if err != nil {
-                       writeError(w, http.StatusServiceUnavailable, 
errors.New("auth lookup failed"))
+                       common.WriteError(w, http.StatusServiceUnavailable, 
errors.New("auth lookup failed"))
                        return
                }
                if !profile.has(p) {
-                       writeError(w, http.StatusForbidden, 
errors.New("insufficient privilege"))
+                       common.WriteError(w, http.StatusForbidden, 
errors.New("insufficient privilege"))
                        return
                }
                next(w, r)
diff --git a/internal/server/privilege.go b/internal/server/privilege.go
index e33b1acb1..401cb7d73 100644
--- a/internal/server/privilege.go
+++ b/internal/server/privilege.go
@@ -21,6 +21,7 @@ import (
        "errors"
        "net/http"
 
+       "github.com/apache/airavata-custos/pkg/common"
        "github.com/apache/airavata-custos/pkg/models"
 )
 
@@ -36,19 +37,19 @@ import (
 func (s *Server) getCallerPrivileges(w http.ResponseWriter, r *http.Request) {
        callerID := r.Header.Get(callerHeader)
        if callerID == "" {
-               writeError(w, http.StatusUnauthorized, errors.New("missing 
"+callerHeader+" header"))
+               common.WriteError(w, http.StatusUnauthorized, 
errors.New("missing "+callerHeader+" header"))
                return
        }
        profile, err := s.lookupAuthProfile(r.Context(), callerID)
        if err != nil {
-               writeError(w, http.StatusServiceUnavailable, errors.New("auth 
lookup failed"))
+               common.WriteError(w, http.StatusServiceUnavailable, 
errors.New("auth lookup failed"))
                return
        }
        keys := make([]models.PrivilegeKey, 0, len(profile.privileges))
        for k := range profile.privileges {
                keys = append(keys, k)
        }
-       writeJSON(w, http.StatusOK, map[string]any{"privileges": keys})
+       common.WriteJSON(w, http.StatusOK, map[string]any{"privileges": keys})
 }
 
 // @Summary    List the declared privilege catalog
@@ -60,7 +61,7 @@ func (s *Server) getCallerPrivileges(w http.ResponseWriter, r 
*http.Request) {
 // @Failure    403     {object}        object{error=string}    "Caller lacks 
privileges:grant"
 // @Router     /privileges/catalog [get]
 func (s *Server) getPrivilegeCatalog(w http.ResponseWriter, _ *http.Request) {
-       writeJSON(w, http.StatusOK, s.svc.PrivilegeCatalog())
+       common.WriteJSON(w, http.StatusOK, s.svc.PrivilegeCatalog())
 }
 
 // @Summary    List a user's direct privilege grants
@@ -76,15 +77,15 @@ func (s *Server) getPrivilegeCatalog(w http.ResponseWriter, 
_ *http.Request) {
 func (s *Server) listUserPrivileges(w http.ResponseWriter, r *http.Request) {
        userID := r.PathValue("id")
        if userID == "" {
-               writeError(w, http.StatusBadRequest, errors.New("user id is 
required"))
+               common.WriteError(w, http.StatusBadRequest, errors.New("user id 
is required"))
                return
        }
        rows, err := s.svc.ListUserPrivileges(r.Context(), userID)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, rows)
+       common.WriteJSON(w, http.StatusOK, rows)
 }
 
 // @Summary    List direct holders of a privilege
@@ -99,15 +100,15 @@ func (s *Server) listUserPrivileges(w http.ResponseWriter, 
r *http.Request) {
 func (s *Server) listPrivilegeHolders(w http.ResponseWriter, r *http.Request) {
        key := models.PrivilegeKey(r.PathValue("key"))
        if !models.IsKnownPrivilege(key) {
-               writeError(w, http.StatusBadRequest, errors.New("unknown 
privilege key"))
+               common.WriteError(w, http.StatusBadRequest, errors.New("unknown 
privilege key"))
                return
        }
        rows, err := s.svc.ListPrivilegeHolders(r.Context(), key)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, rows)
+       common.WriteJSON(w, http.StatusOK, rows)
 }
 
 type grantPrivilegeRequest struct {
@@ -131,26 +132,26 @@ type grantPrivilegeRequest struct {
 func (s *Server) grantPrivilege(w http.ResponseWriter, r *http.Request) {
        userID := r.PathValue("id")
        if userID == "" {
-               writeError(w, http.StatusBadRequest, errors.New("user id is 
required"))
+               common.WriteError(w, http.StatusBadRequest, errors.New("user id 
is required"))
                return
        }
        granterID := r.Header.Get(callerHeader)
        if granterID == "" {
-               writeError(w, http.StatusUnauthorized, errors.New("missing 
"+callerHeader+" header"))
+               common.WriteError(w, http.StatusUnauthorized, 
errors.New("missing "+callerHeader+" header"))
                return
        }
        var req grantPrivilegeRequest
-       if err := decodeJSON(r, &req); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &req); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        grant, err := s.svc.GrantPrivilege(r.Context(), userID, req.Privilege, 
granterID, req.Reason)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
        s.authCache.invalidate(userID)
-       writeJSON(w, http.StatusCreated, grant)
+       common.WriteJSON(w, http.StatusCreated, grant)
 }
 
 type revokePrivilegeRequest struct {
@@ -175,18 +176,18 @@ func (s *Server) revokePrivilege(w http.ResponseWriter, r 
*http.Request) {
        userID := r.PathValue("id")
        key := models.PrivilegeKey(r.PathValue("key"))
        if userID == "" || key == "" {
-               writeError(w, http.StatusBadRequest, errors.New("user id and 
privilege key are required"))
+               common.WriteError(w, http.StatusBadRequest, errors.New("user id 
and privilege key are required"))
                return
        }
        revokerID := r.Header.Get(callerHeader)
        if revokerID == "" {
-               writeError(w, http.StatusUnauthorized, errors.New("missing 
"+callerHeader+" header"))
+               common.WriteError(w, http.StatusUnauthorized, 
errors.New("missing "+callerHeader+" header"))
                return
        }
        var req revokePrivilegeRequest
-       _ = decodeJSON(r, &req)
+       _ = common.DecodeJSON(r, &req)
        if err := s.svc.RevokePrivilege(r.Context(), userID, key, revokerID, 
req.Reason); err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
        s.authCache.invalidate(userID)
diff --git a/internal/server/role.go b/internal/server/role.go
index ee91ea7b7..e7a8fdbbe 100644
--- a/internal/server/role.go
+++ b/internal/server/role.go
@@ -21,6 +21,7 @@ import (
        "errors"
        "net/http"
 
+       "github.com/apache/airavata-custos/pkg/common"
        "github.com/apache/airavata-custos/pkg/models"
 )
 
@@ -35,10 +36,10 @@ import (
 func (s *Server) listRoles(w http.ResponseWriter, r *http.Request) {
        rows, err := s.svc.ListRoles(r.Context())
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, rows)
+       common.WriteJSON(w, http.StatusOK, rows)
 }
 
 // @Summary    Get a role with its privilege bundle
@@ -53,20 +54,20 @@ func (s *Server) listRoles(w http.ResponseWriter, r 
*http.Request) {
 func (s *Server) getRole(w http.ResponseWriter, r *http.Request) {
        roleID := r.PathValue("id")
        if roleID == "" {
-               writeError(w, http.StatusBadRequest, errors.New("role id is 
required"))
+               common.WriteError(w, http.StatusBadRequest, errors.New("role id 
is required"))
                return
        }
        role, err := s.svc.GetRole(r.Context(), roleID)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
        keys, err := s.svc.ListRolePrivileges(r.Context(), roleID)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, map[string]any{
+       common.WriteJSON(w, http.StatusOK, map[string]any{
                "role":       role,
                "privileges": keys,
        })
@@ -91,20 +92,20 @@ type createRoleRequest struct {
 func (s *Server) createRole(w http.ResponseWriter, r *http.Request) {
        actorID := r.Header.Get(callerHeader)
        if actorID == "" {
-               writeError(w, http.StatusUnauthorized, errors.New("missing 
"+callerHeader+" header"))
+               common.WriteError(w, http.StatusUnauthorized, 
errors.New("missing "+callerHeader+" header"))
                return
        }
        var req createRoleRequest
-       if err := decodeJSON(r, &req); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &req); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        role, err := s.svc.CreateRole(r.Context(), req.Name, req.Description, 
actorID)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusCreated, role)
+       common.WriteJSON(w, http.StatusCreated, role)
 }
 
 type updateRoleRequest struct {
@@ -127,26 +128,26 @@ type updateRoleRequest struct {
 func (s *Server) updateRole(w http.ResponseWriter, r *http.Request) {
        roleID := r.PathValue("id")
        if roleID == "" {
-               writeError(w, http.StatusBadRequest, errors.New("role id is 
required"))
+               common.WriteError(w, http.StatusBadRequest, errors.New("role id 
is required"))
                return
        }
        actorID := r.Header.Get(callerHeader)
        if actorID == "" {
-               writeError(w, http.StatusUnauthorized, errors.New("missing 
"+callerHeader+" header"))
+               common.WriteError(w, http.StatusUnauthorized, 
errors.New("missing "+callerHeader+" header"))
                return
        }
        var req updateRoleRequest
-       if err := decodeJSON(r, &req); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &req); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        role, err := s.svc.UpdateRole(r.Context(), roleID, req.Name, 
req.Description, actorID)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
        s.authCache.invalidateAll()
-       writeJSON(w, http.StatusOK, role)
+       common.WriteJSON(w, http.StatusOK, role)
 }
 
 // @Summary    Delete a role
@@ -161,16 +162,16 @@ func (s *Server) updateRole(w http.ResponseWriter, r 
*http.Request) {
 func (s *Server) deleteRole(w http.ResponseWriter, r *http.Request) {
        roleID := r.PathValue("id")
        if roleID == "" {
-               writeError(w, http.StatusBadRequest, errors.New("role id is 
required"))
+               common.WriteError(w, http.StatusBadRequest, errors.New("role id 
is required"))
                return
        }
        actorID := r.Header.Get(callerHeader)
        if actorID == "" {
-               writeError(w, http.StatusUnauthorized, errors.New("missing 
"+callerHeader+" header"))
+               common.WriteError(w, http.StatusUnauthorized, 
errors.New("missing "+callerHeader+" header"))
                return
        }
        if err := s.svc.DeleteRole(r.Context(), roleID, actorID); err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
        s.authCache.invalidateAll()
@@ -196,21 +197,21 @@ type rolePrivilegeRequest struct {
 func (s *Server) addRolePrivilege(w http.ResponseWriter, r *http.Request) {
        roleID := r.PathValue("id")
        if roleID == "" {
-               writeError(w, http.StatusBadRequest, errors.New("role id is 
required"))
+               common.WriteError(w, http.StatusBadRequest, errors.New("role id 
is required"))
                return
        }
        actorID := r.Header.Get(callerHeader)
        if actorID == "" {
-               writeError(w, http.StatusUnauthorized, errors.New("missing 
"+callerHeader+" header"))
+               common.WriteError(w, http.StatusUnauthorized, 
errors.New("missing "+callerHeader+" header"))
                return
        }
        var req rolePrivilegeRequest
-       if err := decodeJSON(r, &req); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &req); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        if err := s.svc.AddPrivilegeToRole(r.Context(), roleID, req.Privilege, 
actorID); err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
        s.authCache.invalidateAll()
@@ -232,16 +233,16 @@ func (s *Server) removeRolePrivilege(w 
http.ResponseWriter, r *http.Request) {
        roleID := r.PathValue("id")
        key := models.PrivilegeKey(r.PathValue("key"))
        if roleID == "" || key == "" {
-               writeError(w, http.StatusBadRequest, errors.New("role id and 
privilege key are required"))
+               common.WriteError(w, http.StatusBadRequest, errors.New("role id 
and privilege key are required"))
                return
        }
        actorID := r.Header.Get(callerHeader)
        if actorID == "" {
-               writeError(w, http.StatusUnauthorized, errors.New("missing 
"+callerHeader+" header"))
+               common.WriteError(w, http.StatusUnauthorized, 
errors.New("missing "+callerHeader+" header"))
                return
        }
        if err := s.svc.RemovePrivilegeFromRole(r.Context(), roleID, key, 
actorID); err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
        s.authCache.invalidateAll()
@@ -259,15 +260,15 @@ func (s *Server) removeRolePrivilege(w 
http.ResponseWriter, r *http.Request) {
 func (s *Server) listUserRoles(w http.ResponseWriter, r *http.Request) {
        userID := r.PathValue("id")
        if userID == "" {
-               writeError(w, http.StatusBadRequest, errors.New("user id is 
required"))
+               common.WriteError(w, http.StatusBadRequest, errors.New("user id 
is required"))
                return
        }
        rows, err := s.svc.ListUserRoles(r.Context(), userID)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, rows)
+       common.WriteJSON(w, http.StatusOK, rows)
 }
 
 // @Summary    List users holding the role
@@ -281,15 +282,15 @@ func (s *Server) listUserRoles(w http.ResponseWriter, r 
*http.Request) {
 func (s *Server) listRoleHolders(w http.ResponseWriter, r *http.Request) {
        roleID := r.PathValue("id")
        if roleID == "" {
-               writeError(w, http.StatusBadRequest, errors.New("role id is 
required"))
+               common.WriteError(w, http.StatusBadRequest, errors.New("role id 
is required"))
                return
        }
        rows, err := s.svc.ListRoleHolders(r.Context(), roleID)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, rows)
+       common.WriteJSON(w, http.StatusOK, rows)
 }
 
 type grantRoleRequest struct {
@@ -312,26 +313,26 @@ type grantRoleRequest struct {
 func (s *Server) grantRoleToUser(w http.ResponseWriter, r *http.Request) {
        userID := r.PathValue("id")
        if userID == "" {
-               writeError(w, http.StatusBadRequest, errors.New("user id is 
required"))
+               common.WriteError(w, http.StatusBadRequest, errors.New("user id 
is required"))
                return
        }
        granterID := r.Header.Get(callerHeader)
        if granterID == "" {
-               writeError(w, http.StatusUnauthorized, errors.New("missing 
"+callerHeader+" header"))
+               common.WriteError(w, http.StatusUnauthorized, 
errors.New("missing "+callerHeader+" header"))
                return
        }
        var req grantRoleRequest
-       if err := decodeJSON(r, &req); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &req); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        assignment, err := s.svc.GrantRoleToUser(r.Context(), userID, 
req.RoleID, granterID, req.Reason)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
        s.authCache.invalidate(userID)
-       writeJSON(w, http.StatusCreated, assignment)
+       common.WriteJSON(w, http.StatusCreated, assignment)
 }
 
 type revokeRoleRequest struct {
@@ -355,18 +356,18 @@ func (s *Server) revokeRoleFromUser(w 
http.ResponseWriter, r *http.Request) {
        userID := r.PathValue("id")
        roleID := r.PathValue("roleId")
        if userID == "" || roleID == "" {
-               writeError(w, http.StatusBadRequest, errors.New("user id and 
role id are required"))
+               common.WriteError(w, http.StatusBadRequest, errors.New("user id 
and role id are required"))
                return
        }
        revokerID := r.Header.Get(callerHeader)
        if revokerID == "" {
-               writeError(w, http.StatusUnauthorized, errors.New("missing 
"+callerHeader+" header"))
+               common.WriteError(w, http.StatusUnauthorized, 
errors.New("missing "+callerHeader+" header"))
                return
        }
        var req revokeRoleRequest
-       _ = decodeJSON(r, &req)
+       _ = common.DecodeJSON(r, &req)
        if err := s.svc.RevokeRoleFromUser(r.Context(), userID, roleID, 
revokerID, req.Reason); err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
        s.authCache.invalidate(userID)
diff --git a/internal/server/server.go b/internal/server/server.go
index 622c2c703..72f24554c 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -19,7 +19,6 @@
 package server
 
 import (
-       "encoding/json"
        "errors"
        "log/slog"
        "net/http"
@@ -29,6 +28,7 @@ import (
 
        "github.com/apache/airavata-custos/internal/httputil"
        "github.com/apache/airavata-custos/internal/store"
+       "github.com/apache/airavata-custos/pkg/common"
        "github.com/apache/airavata-custos/pkg/models"
        "github.com/apache/airavata-custos/pkg/service"
 )
@@ -193,7 +193,7 @@ func (s *Server) routes() {
 }
 
 func (s *Server) healthz(w http.ResponseWriter, _ *http.Request) {
-       writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
+       common.WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
 }
 
 // @Summary    Create an organization
@@ -207,16 +207,16 @@ func (s *Server) healthz(w http.ResponseWriter, _ 
*http.Request) {
 // @Router     /organizations [post]
 func (s *Server) createOrganization(w http.ResponseWriter, r *http.Request) {
        var org models.Organization
-       if err := decodeJSON(r, &org); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &org); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        created, err := s.svc.CreateOrganization(r.Context(), &org)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusCreated, created)
+       common.WriteJSON(w, http.StatusCreated, created)
 }
 
 // @Summary    Get an organization by ID
@@ -230,10 +230,10 @@ func (s *Server) createOrganization(w 
http.ResponseWriter, r *http.Request) {
 func (s *Server) getOrganization(w http.ResponseWriter, r *http.Request) {
        org, err := s.svc.GetOrganization(r.Context(), r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, org)
+       common.WriteJSON(w, http.StatusOK, org)
 }
 
 // @Summary    Create a user
@@ -247,16 +247,16 @@ func (s *Server) getOrganization(w http.ResponseWriter, r 
*http.Request) {
 // @Router     /users [post]
 func (s *Server) createUser(w http.ResponseWriter, r *http.Request) {
        var u models.User
-       if err := decodeJSON(r, &u); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &u); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        created, err := s.svc.CreateUser(r.Context(), &u)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusCreated, created)
+       common.WriteJSON(w, http.StatusCreated, created)
 }
 
 // @Summary    Get a user by ID
@@ -270,10 +270,10 @@ func (s *Server) createUser(w http.ResponseWriter, r 
*http.Request) {
 func (s *Server) getUser(w http.ResponseWriter, r *http.Request) {
        u, err := s.svc.GetUser(r.Context(), r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, u)
+       common.WriteJSON(w, http.StatusOK, u)
 }
 
 // @Summary    Create a project
@@ -287,16 +287,16 @@ func (s *Server) getUser(w http.ResponseWriter, r 
*http.Request) {
 // @Router     /projects [post]
 func (s *Server) createProject(w http.ResponseWriter, r *http.Request) {
        var p models.Project
-       if err := decodeJSON(r, &p); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &p); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        created, err := s.svc.CreateProject(r.Context(), &p)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusCreated, created)
+       common.WriteJSON(w, http.StatusCreated, created)
 }
 
 // @Summary    Get a project by ID
@@ -310,10 +310,10 @@ func (s *Server) createProject(w http.ResponseWriter, r 
*http.Request) {
 func (s *Server) getProject(w http.ResponseWriter, r *http.Request) {
        p, err := s.svc.GetProjectWithPI(r.Context(), r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, projectResponseFrom(p))
+       common.WriteJSON(w, http.StatusOK, projectResponseFrom(p))
 }
 
 // projectResponseFrom builds the API response shape from a JOIN-fetched row.
@@ -338,16 +338,16 @@ func projectResponseFrom(p *store.ProjectWithPI) 
ProjectResponse {
 // @Router     /compute-clusters [post]
 func (s *Server) createComputeCluster(w http.ResponseWriter, r *http.Request) {
        var c models.ComputeCluster
-       if err := decodeJSON(r, &c); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &c); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        created, err := s.svc.CreateComputeCluster(r.Context(), &c)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusCreated, created)
+       common.WriteJSON(w, http.StatusCreated, created)
 }
 
 // @Summary    Get a compute cluster by ID
@@ -361,10 +361,10 @@ func (s *Server) createComputeCluster(w 
http.ResponseWriter, r *http.Request) {
 func (s *Server) getComputeCluster(w http.ResponseWriter, r *http.Request) {
        c, err := s.svc.GetComputeCluster(r.Context(), r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, c)
+       common.WriteJSON(w, http.StatusOK, c)
 }
 
 // @Summary    List compute clusters
@@ -377,10 +377,10 @@ func (s *Server) getComputeCluster(w http.ResponseWriter, 
r *http.Request) {
 func (s *Server) listComputeClusters(w http.ResponseWriter, r *http.Request) {
        clusters, err := s.svc.ListComputeClusters(r.Context())
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, clusters)
+       common.WriteJSON(w, http.StatusOK, clusters)
 }
 
 // @Summary    Create a compute cluster user
@@ -394,16 +394,16 @@ func (s *Server) listComputeClusters(w 
http.ResponseWriter, r *http.Request) {
 // @Router     /compute-cluster-users [post]
 func (s *Server) createComputeClusterUser(w http.ResponseWriter, r 
*http.Request) {
        var cu models.ComputeClusterUser
-       if err := decodeJSON(r, &cu); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &cu); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        created, err := s.svc.CreateComputeClusterUser(r.Context(), &cu)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusCreated, created)
+       common.WriteJSON(w, http.StatusCreated, created)
 }
 
 // @Summary    Get a compute cluster user by ID
@@ -417,10 +417,10 @@ func (s *Server) createComputeClusterUser(w 
http.ResponseWriter, r *http.Request
 func (s *Server) getComputeClusterUser(w http.ResponseWriter, r *http.Request) 
{
        cu, err := s.svc.GetComputeClusterUser(r.Context(), r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, cu)
+       common.WriteJSON(w, http.StatusOK, cu)
 }
 
 // @Summary    Update a compute cluster user
@@ -436,16 +436,16 @@ func (s *Server) getComputeClusterUser(w 
http.ResponseWriter, r *http.Request) {
 // @Router     /compute-cluster-users/{id} [put]
 func (s *Server) updateComputeClusterUser(w http.ResponseWriter, r 
*http.Request) {
        var cu models.ComputeClusterUser
-       if err := decodeJSON(r, &cu); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &cu); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        cu.ID = r.PathValue("id")
        if err := s.svc.UpdateComputeClusterUser(r.Context(), &cu); err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, &cu)
+       common.WriteJSON(w, http.StatusOK, &cu)
 }
 
 // @Summary    Delete a compute cluster user
@@ -457,7 +457,7 @@ func (s *Server) updateComputeClusterUser(w 
http.ResponseWriter, r *http.Request
 // @Router     /compute-cluster-users/{id} [delete]
 func (s *Server) deleteComputeClusterUser(w http.ResponseWriter, r 
*http.Request) {
        if err := s.svc.DeleteComputeClusterUser(r.Context(), 
r.PathValue("id")); err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
        w.WriteHeader(http.StatusNoContent)
@@ -474,10 +474,10 @@ func (s *Server) deleteComputeClusterUser(w 
http.ResponseWriter, r *http.Request
 func (s *Server) listComputeClusterUsersByCluster(w http.ResponseWriter, r 
*http.Request) {
        users, err := s.svc.ListComputeClusterUsersByCluster(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, users)
+       common.WriteJSON(w, http.StatusOK, users)
 }
 
 // @Summary    Get a compute cluster user by (cluster, user) pair
@@ -492,10 +492,10 @@ func (s *Server) listComputeClusterUsersByCluster(w 
http.ResponseWriter, r *http
 func (s *Server) getComputeClusterUserByPair(w http.ResponseWriter, r 
*http.Request) {
        cu, err := s.svc.GetComputeClusterUserByPair(r.Context(), 
r.PathValue("id"), r.PathValue("userId"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, cu)
+       common.WriteJSON(w, http.StatusOK, cu)
 }
 
 // @Summary    List compute cluster users for a user
@@ -509,10 +509,10 @@ func (s *Server) getComputeClusterUserByPair(w 
http.ResponseWriter, r *http.Requ
 func (s *Server) listComputeClusterUsersByUser(w http.ResponseWriter, r 
*http.Request) {
        users, err := s.svc.ListComputeClusterUsersByUser(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, users)
+       common.WriteJSON(w, http.StatusOK, users)
 }
 
 // @Summary    Create a compute allocation
@@ -526,16 +526,16 @@ func (s *Server) listComputeClusterUsersByUser(w 
http.ResponseWriter, r *http.Re
 // @Router     /compute-allocations [post]
 func (s *Server) createComputeAllocation(w http.ResponseWriter, r 
*http.Request) {
        var a models.ComputeAllocation
-       if err := decodeJSON(r, &a); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &a); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        created, err := s.svc.CreateComputeAllocation(r.Context(), &a)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusCreated, created)
+       common.WriteJSON(w, http.StatusCreated, created)
 }
 
 // @Summary    Get a compute allocation by ID
@@ -549,10 +549,10 @@ func (s *Server) createComputeAllocation(w 
http.ResponseWriter, r *http.Request)
 func (s *Server) getComputeAllocation(w http.ResponseWriter, r *http.Request) {
        a, err := s.svc.GetComputeAllocation(r.Context(), r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, a)
+       common.WriteJSON(w, http.StatusOK, a)
 }
 
 // @Summary    Create a compute allocation resource
@@ -568,16 +568,16 @@ func (s *Server) getComputeAllocation(w 
http.ResponseWriter, r *http.Request) {
 // @Router     /compute-allocation-resources [post]
 func (s *Server) createComputeAllocationResource(w http.ResponseWriter, r 
*http.Request) {
        var res models.ComputeAllocationResource
-       if err := decodeJSON(r, &res); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &res); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        created, err := s.svc.CreateComputeAllocationResource(r.Context(), &res)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusCreated, created)
+       common.WriteJSON(w, http.StatusCreated, created)
 }
 
 // @Summary    Get a compute allocation resource
@@ -591,10 +591,10 @@ func (s *Server) createComputeAllocationResource(w 
http.ResponseWriter, r *http.
 func (s *Server) getComputeAllocationResource(w http.ResponseWriter, r 
*http.Request) {
        res, err := s.svc.GetComputeAllocationResource(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, res)
+       common.WriteJSON(w, http.StatusOK, res)
 }
 
 // @Summary    List all compute allocation resources
@@ -607,10 +607,10 @@ func (s *Server) getComputeAllocationResource(w 
http.ResponseWriter, r *http.Req
 func (s *Server) listComputeAllocationResources(w http.ResponseWriter, r 
*http.Request) {
        resources, err := s.svc.ListComputeAllocationResources(r.Context())
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, resources)
+       common.WriteJSON(w, http.StatusOK, resources)
 }
 
 type attachResourceRequest struct {
@@ -633,16 +633,16 @@ type attachResourceRequest struct {
 // @Router     /compute-allocations/{id}/resources [post]
 func (s *Server) attachResourceToAllocation(w http.ResponseWriter, r 
*http.Request) {
        var body attachResourceRequest
-       if err := decodeJSON(r, &body); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &body); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        mapping, err := s.svc.AttachResourceToAllocation(r.Context(), 
r.PathValue("id"), body.ComputeAllocationResourceID, body.ResourceAmount, 
body.ResourceTime)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusCreated, mapping)
+       common.WriteJSON(w, http.StatusCreated, mapping)
 }
 
 type updateAllocationResourceMappingRequest struct {
@@ -664,16 +664,16 @@ type updateAllocationResourceMappingRequest struct {
 // @Router     /compute-allocations/{id}/resources/{resourceId} [put]
 func (s *Server) updateAllocationResourceMapping(w http.ResponseWriter, r 
*http.Request) {
        var body updateAllocationResourceMappingRequest
-       if err := decodeJSON(r, &body); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &body); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        mapping, err := s.svc.UpdateAllocationResourceMapping(r.Context(), 
r.PathValue("id"), r.PathValue("resourceId"), body.ResourceAmount, 
body.ResourceTime)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, mapping)
+       common.WriteJSON(w, http.StatusOK, mapping)
 }
 
 // @Summary    Detach a resource from a compute allocation
@@ -686,7 +686,7 @@ func (s *Server) updateAllocationResourceMapping(w 
http.ResponseWriter, r *http.
 // @Router     /compute-allocations/{id}/resources/{resourceId} [delete]
 func (s *Server) detachResourceFromAllocation(w http.ResponseWriter, r 
*http.Request) {
        if err := s.svc.DetachResourceFromAllocation(r.Context(), 
r.PathValue("id"), r.PathValue("resourceId")); err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
        w.WriteHeader(http.StatusNoContent)
@@ -703,10 +703,10 @@ func (s *Server) detachResourceFromAllocation(w 
http.ResponseWriter, r *http.Req
 func (s *Server) listResourcesForAllocation(w http.ResponseWriter, r 
*http.Request) {
        resources, err := s.svc.ListResourcesForAllocation(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, resources)
+       common.WriteJSON(w, http.StatusOK, resources)
 }
 
 // @Summary    List compute allocations attached to a resource
@@ -720,10 +720,10 @@ func (s *Server) listResourcesForAllocation(w 
http.ResponseWriter, r *http.Reque
 func (s *Server) listAllocationsForResource(w http.ResponseWriter, r 
*http.Request) {
        allocs, err := s.svc.ListAllocationsForResource(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, allocs)
+       common.WriteJSON(w, http.StatusOK, allocs)
 }
 
 // @Summary    Create a compute allocation resource rate
@@ -739,16 +739,16 @@ func (s *Server) listAllocationsForResource(w 
http.ResponseWriter, r *http.Reque
 // @Router     /compute-allocation-resource-rates [post]
 func (s *Server) createComputeAllocationResourceRate(w http.ResponseWriter, r 
*http.Request) {
        var rate models.ComputeAllocationResourceRate
-       if err := decodeJSON(r, &rate); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &rate); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        created, err := s.svc.CreateComputeAllocationResourceRate(r.Context(), 
&rate)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusCreated, created)
+       common.WriteJSON(w, http.StatusCreated, created)
 }
 
 // @Summary    Get a compute allocation resource rate
@@ -762,10 +762,10 @@ func (s *Server) createComputeAllocationResourceRate(w 
http.ResponseWriter, r *h
 func (s *Server) getComputeAllocationResourceRate(w http.ResponseWriter, r 
*http.Request) {
        rate, err := s.svc.GetComputeAllocationResourceRate(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, rate)
+       common.WriteJSON(w, http.StatusOK, rate)
 }
 
 // @Summary    List rate history for a resource
@@ -779,10 +779,10 @@ func (s *Server) getComputeAllocationResourceRate(w 
http.ResponseWriter, r *http
 func (s *Server) listRatesForResource(w http.ResponseWriter, r *http.Request) {
        rates, err := s.svc.ListRatesForResource(r.Context(), r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, rates)
+       common.WriteJSON(w, http.StatusOK, rates)
 }
 
 // @Summary    Get the effective rate for a resource at a given time
@@ -801,17 +801,17 @@ func (s *Server) getEffectiveRateForResource(w 
http.ResponseWriter, r *http.Requ
        if raw := r.URL.Query().Get("at"); raw != "" {
                parsed, err := time.Parse(time.RFC3339Nano, raw)
                if err != nil {
-                       writeError(w, http.StatusBadRequest, 
errors.New("invalid 'at' query parameter; expected RFC 3339"))
+                       common.WriteError(w, http.StatusBadRequest, 
errors.New("invalid 'at' query parameter; expected RFC 3339"))
                        return
                }
                at = parsed
        }
        rate, err := s.svc.GetEffectiveRateForResource(r.Context(), 
r.PathValue("id"), at)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, rate)
+       common.WriteJSON(w, http.StatusOK, rate)
 }
 
 // @Summary    Create a compute allocation diff
@@ -827,16 +827,16 @@ func (s *Server) getEffectiveRateForResource(w 
http.ResponseWriter, r *http.Requ
 // @Router     /compute-allocation-diffs [post]
 func (s *Server) createComputeAllocationDiff(w http.ResponseWriter, r 
*http.Request) {
        var diff models.ComputeAllocationDiff
-       if err := decodeJSON(r, &diff); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &diff); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        created, err := s.svc.CreateComputeAllocationDiff(r.Context(), &diff)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusCreated, created)
+       common.WriteJSON(w, http.StatusCreated, created)
 }
 
 // @Summary    Get a compute allocation diff
@@ -850,10 +850,10 @@ func (s *Server) createComputeAllocationDiff(w 
http.ResponseWriter, r *http.Requ
 func (s *Server) getComputeAllocationDiff(w http.ResponseWriter, r 
*http.Request) {
        diff, err := s.svc.GetComputeAllocationDiff(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, diff)
+       common.WriteJSON(w, http.StatusOK, diff)
 }
 
 // @Summary    Delete a compute allocation diff
@@ -865,7 +865,7 @@ func (s *Server) getComputeAllocationDiff(w 
http.ResponseWriter, r *http.Request
 // @Router     /compute-allocation-diffs/{id} [delete]
 func (s *Server) deleteComputeAllocationDiff(w http.ResponseWriter, r 
*http.Request) {
        if err := s.svc.DeleteComputeAllocationDiff(r.Context(), 
r.PathValue("id")); err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
        w.WriteHeader(http.StatusNoContent)
@@ -882,10 +882,10 @@ func (s *Server) deleteComputeAllocationDiff(w 
http.ResponseWriter, r *http.Requ
 func (s *Server) listDiffsForAllocation(w http.ResponseWriter, r 
*http.Request) {
        diffs, err := s.svc.ListDiffsForAllocation(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, diffs)
+       common.WriteJSON(w, http.StatusOK, diffs)
 }
 
 // @Summary    Get the most recent diff for a compute allocation
@@ -899,10 +899,10 @@ func (s *Server) listDiffsForAllocation(w 
http.ResponseWriter, r *http.Request)
 func (s *Server) getLatestDiffForAllocation(w http.ResponseWriter, r 
*http.Request) {
        diff, err := s.svc.GetLatestDiffForAllocation(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, diff)
+       common.WriteJSON(w, http.StatusOK, diff)
 }
 
 // @Summary    Create a compute allocation change request
@@ -916,16 +916,16 @@ func (s *Server) getLatestDiffForAllocation(w 
http.ResponseWriter, r *http.Reque
 // @Router     /compute-allocation-change-requests [post]
 func (s *Server) createComputeAllocationChangeRequest(w http.ResponseWriter, r 
*http.Request) {
        var req models.ComputeAllocationChangeRequest
-       if err := decodeJSON(r, &req); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &req); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        created, err := s.svc.CreateComputeAllocationChangeRequest(r.Context(), 
&req)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusCreated, created)
+       common.WriteJSON(w, http.StatusCreated, created)
 }
 
 // @Summary    Get a compute allocation change request
@@ -939,10 +939,10 @@ func (s *Server) createComputeAllocationChangeRequest(w 
http.ResponseWriter, r *
 func (s *Server) getComputeAllocationChangeRequest(w http.ResponseWriter, r 
*http.Request) {
        req, err := s.svc.GetComputeAllocationChangeRequest(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, req)
+       common.WriteJSON(w, http.StatusOK, req)
 }
 
 // @Summary    Update a compute allocation change request
@@ -958,17 +958,17 @@ func (s *Server) getComputeAllocationChangeRequest(w 
http.ResponseWriter, r *htt
 // @Router     /compute-allocation-change-requests/{id} [put]
 func (s *Server) updateComputeAllocationChangeRequest(w http.ResponseWriter, r 
*http.Request) {
        var req models.ComputeAllocationChangeRequest
-       if err := decodeJSON(r, &req); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &req); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        req.ID = r.PathValue("id")
        updated, err := s.svc.UpdateComputeAllocationChangeRequest(r.Context(), 
&req)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, updated)
+       common.WriteJSON(w, http.StatusOK, updated)
 }
 
 // @Summary    Delete a compute allocation change request
@@ -980,7 +980,7 @@ func (s *Server) updateComputeAllocationChangeRequest(w 
http.ResponseWriter, r *
 // @Router     /compute-allocation-change-requests/{id} [delete]
 func (s *Server) deleteComputeAllocationChangeRequest(w http.ResponseWriter, r 
*http.Request) {
        if err := s.svc.DeleteComputeAllocationChangeRequest(r.Context(), 
r.PathValue("id")); err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
        w.WriteHeader(http.StatusNoContent)
@@ -997,10 +997,10 @@ func (s *Server) deleteComputeAllocationChangeRequest(w 
http.ResponseWriter, r *
 func (s *Server) listChangeRequestsForAllocation(w http.ResponseWriter, r 
*http.Request) {
        rows, err := s.svc.ListChangeRequestsForAllocation(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, rows)
+       common.WriteJSON(w, http.StatusOK, rows)
 }
 
 // @Summary    List change requests submitted by a user
@@ -1014,10 +1014,10 @@ func (s *Server) listChangeRequestsForAllocation(w 
http.ResponseWriter, r *http.
 func (s *Server) listChangeRequestsByRequester(w http.ResponseWriter, r 
*http.Request) {
        rows, err := s.svc.ListChangeRequestsByRequester(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, rows)
+       common.WriteJSON(w, http.StatusOK, rows)
 }
 
 // @Summary    Create a change request event
@@ -1031,16 +1031,16 @@ func (s *Server) listChangeRequestsByRequester(w 
http.ResponseWriter, r *http.Re
 // @Router     /compute-allocation-change-request-events [post]
 func (s *Server) createComputeAllocationChangeRequestEvent(w 
http.ResponseWriter, r *http.Request) {
        var evt models.ComputeAllocationChangeRequestEvent
-       if err := decodeJSON(r, &evt); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &evt); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        created, err := 
s.svc.CreateComputeAllocationChangeRequestEvent(r.Context(), &evt)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusCreated, created)
+       common.WriteJSON(w, http.StatusCreated, created)
 }
 
 // @Summary    Get a change request event
@@ -1054,10 +1054,10 @@ func (s *Server) 
createComputeAllocationChangeRequestEvent(w http.ResponseWriter
 func (s *Server) getComputeAllocationChangeRequestEvent(w http.ResponseWriter, 
r *http.Request) {
        evt, err := s.svc.GetComputeAllocationChangeRequestEvent(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, evt)
+       common.WriteJSON(w, http.StatusOK, evt)
 }
 
 // @Summary    Delete a change request event
@@ -1069,7 +1069,7 @@ func (s *Server) getComputeAllocationChangeRequestEvent(w 
http.ResponseWriter, r
 // @Router     /compute-allocation-change-request-events/{id} [delete]
 func (s *Server) deleteComputeAllocationChangeRequestEvent(w 
http.ResponseWriter, r *http.Request) {
        if err := s.svc.DeleteComputeAllocationChangeRequestEvent(r.Context(), 
r.PathValue("id")); err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
        w.WriteHeader(http.StatusNoContent)
@@ -1086,10 +1086,10 @@ func (s *Server) 
deleteComputeAllocationChangeRequestEvent(w http.ResponseWriter
 func (s *Server) listEventsForChangeRequest(w http.ResponseWriter, r 
*http.Request) {
        rows, err := s.svc.ListEventsForChangeRequest(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, rows)
+       common.WriteJSON(w, http.StatusOK, rows)
 }
 
 // @Summary    Get the most recent event for a change request
@@ -1103,10 +1103,10 @@ func (s *Server) listEventsForChangeRequest(w 
http.ResponseWriter, r *http.Reque
 func (s *Server) getLatestEventForChangeRequest(w http.ResponseWriter, r 
*http.Request) {
        evt, err := s.svc.GetLatestEventForChangeRequest(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, evt)
+       common.WriteJSON(w, http.StatusOK, evt)
 }
 
 // @Summary    Create a compute allocation membership
@@ -1120,16 +1120,16 @@ func (s *Server) getLatestEventForChangeRequest(w 
http.ResponseWriter, r *http.R
 // @Router     /compute-allocation-memberships [post]
 func (s *Server) createComputeAllocationMembership(w http.ResponseWriter, r 
*http.Request) {
        var m models.ComputeAllocationMembership
-       if err := decodeJSON(r, &m); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &m); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        created, err := s.svc.CreateComputeAllocationMembership(r.Context(), &m)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusCreated, created)
+       common.WriteJSON(w, http.StatusCreated, created)
 }
 
 // @Summary    Get a compute allocation membership
@@ -1143,10 +1143,10 @@ func (s *Server) createComputeAllocationMembership(w 
http.ResponseWriter, r *htt
 func (s *Server) getComputeAllocationMembership(w http.ResponseWriter, r 
*http.Request) {
        m, err := s.svc.GetComputeAllocationMembership(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, m)
+       common.WriteJSON(w, http.StatusOK, m)
 }
 
 // @Summary    Update a compute allocation membership
@@ -1162,17 +1162,17 @@ func (s *Server) getComputeAllocationMembership(w 
http.ResponseWriter, r *http.R
 // @Router     /compute-allocation-memberships/{id} [put]
 func (s *Server) updateComputeAllocationMembership(w http.ResponseWriter, r 
*http.Request) {
        var m models.ComputeAllocationMembership
-       if err := decodeJSON(r, &m); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &m); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        m.ID = r.PathValue("id")
        updated, err := s.svc.UpdateComputeAllocationMembership(r.Context(), &m)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, updated)
+       common.WriteJSON(w, http.StatusOK, updated)
 }
 
 // @Summary    Update a membership's status
@@ -1190,16 +1190,16 @@ func (s *Server) updateMembershipStatus(w 
http.ResponseWriter, r *http.Request)
        var body struct {
                MembershipStatus models.AllocationStatus 
`json:"membership_status"`
        }
-       if err := decodeJSON(r, &body); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &body); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        updated, err := s.svc.UpdateMembershipStatus(r.Context(), 
r.PathValue("id"), body.MembershipStatus)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, updated)
+       common.WriteJSON(w, http.StatusOK, updated)
 }
 
 // @Summary    Delete a compute allocation membership
@@ -1211,7 +1211,7 @@ func (s *Server) updateMembershipStatus(w 
http.ResponseWriter, r *http.Request)
 // @Router     /compute-allocation-memberships/{id} [delete]
 func (s *Server) deleteComputeAllocationMembership(w http.ResponseWriter, r 
*http.Request) {
        if err := s.svc.DeleteComputeAllocationMembership(r.Context(), 
r.PathValue("id")); err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
        w.WriteHeader(http.StatusNoContent)
@@ -1228,7 +1228,7 @@ func (s *Server) deleteComputeAllocationMembership(w 
http.ResponseWriter, r *htt
 func (s *Server) listMembersForAllocation(w http.ResponseWriter, r 
*http.Request) {
        rows, err := s.svc.ListMembersForAllocation(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
        out := make([]AllocationMembershipResponse, 0, len(rows))
@@ -1240,7 +1240,7 @@ func (s *Server) listMembersForAllocation(w 
http.ResponseWriter, r *http.Request
                        Email:                       m.Email,
                })
        }
-       writeJSON(w, http.StatusOK, out)
+       common.WriteJSON(w, http.StatusOK, out)
 }
 
 // @Summary    List a user's compute allocation memberships
@@ -1254,10 +1254,10 @@ func (s *Server) listMembersForAllocation(w 
http.ResponseWriter, r *http.Request
 func (s *Server) listAllocationsForUser(w http.ResponseWriter, r 
*http.Request) {
        rows, err := s.svc.ListAllocationsForUser(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, rows)
+       common.WriteJSON(w, http.StatusOK, rows)
 }
 
 // @Summary    Create a compute allocation usage record
@@ -1271,16 +1271,16 @@ func (s *Server) listAllocationsForUser(w 
http.ResponseWriter, r *http.Request)
 // @Router     /compute-allocation-usages [post]
 func (s *Server) createComputeAllocationUsage(w http.ResponseWriter, r 
*http.Request) {
        var u models.ComputeAllocationUsage
-       if err := decodeJSON(r, &u); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &u); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        created, err := s.svc.CreateComputeAllocationUsage(r.Context(), &u)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusCreated, created)
+       common.WriteJSON(w, http.StatusCreated, created)
 }
 
 // @Summary    Get a compute allocation usage record
@@ -1294,10 +1294,10 @@ func (s *Server) createComputeAllocationUsage(w 
http.ResponseWriter, r *http.Req
 func (s *Server) getComputeAllocationUsage(w http.ResponseWriter, r 
*http.Request) {
        u, err := s.svc.GetComputeAllocationUsage(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, u)
+       common.WriteJSON(w, http.StatusOK, u)
 }
 
 // @Summary    Delete a compute allocation usage record
@@ -1309,7 +1309,7 @@ func (s *Server) getComputeAllocationUsage(w 
http.ResponseWriter, r *http.Reques
 // @Router     /compute-allocation-usages/{id} [delete]
 func (s *Server) deleteComputeAllocationUsage(w http.ResponseWriter, r 
*http.Request) {
        if err := s.svc.DeleteComputeAllocationUsage(r.Context(), 
r.PathValue("id")); err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
        w.WriteHeader(http.StatusNoContent)
@@ -1326,10 +1326,10 @@ func (s *Server) deleteComputeAllocationUsage(w 
http.ResponseWriter, r *http.Req
 func (s *Server) listUsagesForAllocation(w http.ResponseWriter, r 
*http.Request) {
        rows, err := s.svc.ListUsagesForAllocation(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, rows)
+       common.WriteJSON(w, http.StatusOK, rows)
 }
 
 // @Summary    List usages submitted by a user
@@ -1343,10 +1343,10 @@ func (s *Server) listUsagesForAllocation(w 
http.ResponseWriter, r *http.Request)
 func (s *Server) listUsagesByUser(w http.ResponseWriter, r *http.Request) {
        rows, err := s.svc.ListUsagesByUser(r.Context(), r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, rows)
+       common.WriteJSON(w, http.StatusOK, rows)
 }
 
 // @Summary    Create a membership resource override
@@ -1360,16 +1360,16 @@ func (s *Server) listUsagesByUser(w 
http.ResponseWriter, r *http.Request) {
 // @Router     /compute-allocation-membership-resource-overrides [post]
 func (s *Server) createComputeAllocationMembershipResourceOverride(w 
http.ResponseWriter, r *http.Request) {
        var o models.ComputeAllocationMembershipResourceOverride
-       if err := decodeJSON(r, &o); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &o); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        created, err := 
s.svc.CreateComputeAllocationMembershipResourceOverride(r.Context(), &o)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusCreated, created)
+       common.WriteJSON(w, http.StatusCreated, created)
 }
 
 // @Summary    Get a membership resource override
@@ -1383,10 +1383,10 @@ func (s *Server) 
createComputeAllocationMembershipResourceOverride(w http.Respon
 func (s *Server) getComputeAllocationMembershipResourceOverride(w 
http.ResponseWriter, r *http.Request) {
        o, err := 
s.svc.GetComputeAllocationMembershipResourceOverride(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, o)
+       common.WriteJSON(w, http.StatusOK, o)
 }
 
 // @Summary    Update a membership resource override
@@ -1402,17 +1402,17 @@ func (s *Server) 
getComputeAllocationMembershipResourceOverride(w http.ResponseW
 // @Router     /compute-allocation-membership-resource-overrides/{id} [put]
 func (s *Server) updateComputeAllocationMembershipResourceOverride(w 
http.ResponseWriter, r *http.Request) {
        var o models.ComputeAllocationMembershipResourceOverride
-       if err := decodeJSON(r, &o); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &o); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        o.ID = r.PathValue("id")
        updated, err := 
s.svc.UpdateComputeAllocationMembershipResourceOverride(r.Context(), &o)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, updated)
+       common.WriteJSON(w, http.StatusOK, updated)
 }
 
 // @Summary    Delete a membership resource override
@@ -1424,7 +1424,7 @@ func (s *Server) 
updateComputeAllocationMembershipResourceOverride(w http.Respon
 // @Router     /compute-allocation-membership-resource-overrides/{id} [delete]
 func (s *Server) deleteComputeAllocationMembershipResourceOverride(w 
http.ResponseWriter, r *http.Request) {
        if err := 
s.svc.DeleteComputeAllocationMembershipResourceOverride(r.Context(), 
r.PathValue("id")); err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
        w.WriteHeader(http.StatusNoContent)
@@ -1441,10 +1441,10 @@ func (s *Server) 
deleteComputeAllocationMembershipResourceOverride(w http.Respon
 func (s *Server) listOverridesForMembership(w http.ResponseWriter, r 
*http.Request) {
        rows, err := s.svc.ListOverridesForMembership(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, rows)
+       common.WriteJSON(w, http.StatusOK, rows)
 }
 
 // @Summary    List membership overrides referencing a resource
@@ -1458,10 +1458,10 @@ func (s *Server) listOverridesForMembership(w 
http.ResponseWriter, r *http.Reque
 func (s *Server) listOverridesForResource(w http.ResponseWriter, r 
*http.Request) {
        rows, err := s.svc.ListOverridesForResource(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, rows)
+       common.WriteJSON(w, http.StatusOK, rows)
 }
 
 // @Summary    Get total SU usage for a compute allocation
@@ -1475,10 +1475,10 @@ func (s *Server) listOverridesForResource(w 
http.ResponseWriter, r *http.Request
 func (s *Server) getTotalSUUsageForAllocation(w http.ResponseWriter, r 
*http.Request) {
        total, err := s.svc.GetTotalSUUsageForAllocation(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, AllocationSUTotalResponse{
+       common.WriteJSON(w, http.StatusOK, AllocationSUTotalResponse{
                ComputeAllocationID: r.PathValue("id"),
                TotalSUAmount:       total,
        })
@@ -1498,10 +1498,10 @@ func (s *Server) getTotalSUUsageForUserInAllocation(w 
http.ResponseWriter, r *ht
        userID := r.PathValue("userId")
        total, err := s.svc.GetTotalSUUsageForUserInAllocation(r.Context(), 
allocationID, userID)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, UserAllocationSUTotalResponse{
+       common.WriteJSON(w, http.StatusOK, UserAllocationSUTotalResponse{
                ComputeAllocationID: allocationID,
                UserID:              userID,
                TotalSUAmount:       total,
@@ -1525,16 +1525,16 @@ type statusUpdateRequest struct {
 // @Router     /users/{id}/status [put]
 func (s *Server) updateUserStatus(w http.ResponseWriter, r *http.Request) {
        var req statusUpdateRequest
-       if err := decodeJSON(r, &req); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &req); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        u, err := s.svc.UpdateUserStatus(r.Context(), r.PathValue("id"), 
models.UserStatus(req.Status))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, u)
+       common.WriteJSON(w, http.StatusOK, u)
 }
 
 // @Summary    Update a project's status
@@ -1550,16 +1550,16 @@ func (s *Server) updateUserStatus(w 
http.ResponseWriter, r *http.Request) {
 // @Router     /projects/{id}/status [put]
 func (s *Server) updateProjectStatus(w http.ResponseWriter, r *http.Request) {
        var req statusUpdateRequest
-       if err := decodeJSON(r, &req); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &req); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        p, err := s.svc.UpdateProjectStatus(r.Context(), r.PathValue("id"), 
models.ProjectStatus(req.Status))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, p)
+       common.WriteJSON(w, http.StatusOK, p)
 }
 
 // @Summary    Create a user identity
@@ -1573,16 +1573,16 @@ func (s *Server) updateProjectStatus(w 
http.ResponseWriter, r *http.Request) {
 // @Router     /user-identities [post]
 func (s *Server) createUserIdentity(w http.ResponseWriter, r *http.Request) {
        var e models.UserIdentity
-       if err := decodeJSON(r, &e); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &e); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        created, err := s.svc.CreateUserIdentity(r.Context(), &e)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusCreated, created)
+       common.WriteJSON(w, http.StatusCreated, created)
 }
 
 // @Summary    Get a user identity
@@ -1596,10 +1596,10 @@ func (s *Server) createUserIdentity(w 
http.ResponseWriter, r *http.Request) {
 func (s *Server) getUserIdentity(w http.ResponseWriter, r *http.Request) {
        e, err := s.svc.GetUserIdentity(r.Context(), r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, e)
+       common.WriteJSON(w, http.StatusOK, e)
 }
 
 // @Summary    Get a user identity by source and external ID
@@ -1614,10 +1614,10 @@ func (s *Server) getUserIdentity(w http.ResponseWriter, 
r *http.Request) {
 func (s *Server) getUserIdentityBySourceAndExternalID(w http.ResponseWriter, r 
*http.Request) {
        e, err := s.svc.GetUserIdentityBySourceAndExternalID(r.Context(), 
r.PathValue("source"), r.PathValue("externalId"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, e)
+       common.WriteJSON(w, http.StatusOK, e)
 }
 
 // @Summary    Get a user identity by its OIDC subject claim
@@ -1631,10 +1631,10 @@ func (s *Server) getUserIdentityBySourceAndExternalID(w 
http.ResponseWriter, r *
 func (s *Server) getUserIdentityByOIDCSub(w http.ResponseWriter, r 
*http.Request) {
        e, err := s.svc.GetUserIdentityByOIDCSub(r.Context(), 
r.PathValue("oidcSub"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, e)
+       common.WriteJSON(w, http.StatusOK, e)
 }
 
 // @Summary    List a user's identities
@@ -1648,10 +1648,10 @@ func (s *Server) getUserIdentityByOIDCSub(w 
http.ResponseWriter, r *http.Request
 func (s *Server) listUserIdentitiesForUser(w http.ResponseWriter, r 
*http.Request) {
        out, err := s.svc.ListUserIdentitiesForUser(r.Context(), 
r.PathValue("id"))
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, out)
+       common.WriteJSON(w, http.StatusOK, out)
 }
 
 // @Summary    Update a user identity
@@ -1667,16 +1667,16 @@ func (s *Server) listUserIdentitiesForUser(w 
http.ResponseWriter, r *http.Reques
 // @Router     /user-identities/{id} [put]
 func (s *Server) updateUserIdentity(w http.ResponseWriter, r *http.Request) {
        var e models.UserIdentity
-       if err := decodeJSON(r, &e); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &e); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        e.ID = r.PathValue("id")
        if err := s.svc.UpdateUserIdentity(r.Context(), &e); err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, &e)
+       common.WriteJSON(w, http.StatusOK, &e)
 }
 
 // @Summary    Delete a user identity
@@ -1688,7 +1688,7 @@ func (s *Server) updateUserIdentity(w 
http.ResponseWriter, r *http.Request) {
 // @Router     /user-identities/{id} [delete]
 func (s *Server) deleteUserIdentity(w http.ResponseWriter, r *http.Request) {
        if err := s.svc.DeleteUserIdentity(r.Context(), r.PathValue("id")); err 
!= nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
        w.WriteHeader(http.StatusNoContent)
@@ -1712,16 +1712,16 @@ type mergeUsersRequest struct {
 // @Router     /users/merge [post]
 func (s *Server) mergeUsers(w http.ResponseWriter, r *http.Request) {
        var req mergeUsersRequest
-       if err := decodeJSON(r, &req); err != nil {
-               writeError(w, http.StatusBadRequest, err)
+       if err := common.DecodeJSON(r, &req); err != nil {
+               common.WriteError(w, http.StatusBadRequest, err)
                return
        }
        survivor, err := s.svc.MergeUsers(r.Context(), req.SurvivingUserID, 
req.RetiringUserID)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
-       writeJSON(w, http.StatusOK, survivor)
+       common.WriteJSON(w, http.StatusOK, survivor)
 }
 
 // LoggingMiddleware logs every request once it completes. It wraps
@@ -1746,40 +1746,6 @@ func LoggingMiddleware(next http.Handler) http.Handler {
        })
 }
 
-func decodeJSON(r *http.Request, dst any) error {
-       dec := json.NewDecoder(r.Body)
-       dec.DisallowUnknownFields()
-       return dec.Decode(dst)
-}
-
-func writeJSON(w http.ResponseWriter, status int, body any) {
-       w.Header().Set("Content-Type", "application/json")
-       w.WriteHeader(status)
-       if body == nil {
-               return
-       }
-       _ = json.NewEncoder(w).Encode(body)
-}
-
-func writeError(w http.ResponseWriter, status int, err error) {
-       writeJSON(w, status, map[string]string{"error": err.Error()})
-}
-
-func writeServiceError(w http.ResponseWriter, err error) {
-       switch {
-       case errors.Is(err, service.ErrNotFound):
-               writeError(w, http.StatusNotFound, err)
-       case errors.Is(err, service.ErrAlreadyExists):
-               writeError(w, http.StatusConflict, err)
-       case errors.Is(err, service.ErrInvalidInput):
-               writeError(w, http.StatusBadRequest, err)
-       default:
-               // Avoid leaking driver messages to clients; log the full error.
-               slog.Error("internal server error", "error", err.Error())
-               writeError(w, http.StatusInternalServerError, 
errors.New(strings.TrimSpace("internal server error")))
-       }
-}
-
 // @Summary    List projects (filtered + paginated, PI joined)
 // @Tags       Projects
 // @Security   CustosUserHeader
@@ -1802,14 +1768,14 @@ func (s *Server) listProjects(w http.ResponseWriter, r 
*http.Request) {
        }
        rows, total, err := s.svc.ListProjectsWithPI(r.Context(), f)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
        items := make([]ProjectResponse, 0, len(rows))
        for i := range rows {
                items = append(items, projectResponseFrom(&rows[i]))
        }
-       writeJSON(w, http.StatusOK, ProjectListResponse{Items: items, Total: 
total})
+       common.WriteJSON(w, http.StatusOK, ProjectListResponse{Items: items, 
Total: total})
 }
 
 // @Summary    List project members (one row per distinct user, with 
allocations)
@@ -1824,7 +1790,7 @@ func (s *Server) listProjectMembers(w 
http.ResponseWriter, r *http.Request) {
        projectID := r.PathValue("id")
        rows, err := s.svc.ListMembersForProject(r.Context(), projectID)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
 
@@ -1865,7 +1831,7 @@ func (s *Server) listProjectMembers(w 
http.ResponseWriter, r *http.Request) {
                        Allocations: agg.allocations,
                })
        }
-       writeJSON(w, http.StatusOK, out)
+       common.WriteJSON(w, http.StatusOK, out)
 }
 
 // @Summary    List compute allocations (filtered + paginated)
@@ -1890,13 +1856,13 @@ func (s *Server) listComputeAllocations(w 
http.ResponseWriter, r *http.Request)
        }
        rows, total, err := s.svc.ListComputeAllocations(r.Context(), f)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
        if rows == nil {
                rows = []models.ComputeAllocation{}
        }
-       writeJSON(w, http.StatusOK, ComputeAllocationListResponse{Items: rows, 
Total: total})
+       common.WriteJSON(w, http.StatusOK, ComputeAllocationListResponse{Items: 
rows, Total: total})
 }
 
 func (s *Server) listChangeRequests(w http.ResponseWriter, r *http.Request) {
@@ -1907,13 +1873,13 @@ func (s *Server) listChangeRequests(w 
http.ResponseWriter, r *http.Request) {
        }
        rows, err := s.svc.ListChangeRequests(r.Context(), f)
        if err != nil {
-               writeServiceError(w, err)
+               common.WriteServiceError(w, err)
                return
        }
        if rows == nil {
                rows = []models.ComputeAllocationChangeRequest{}
        }
-       writeJSON(w, http.StatusOK, rows)
+       common.WriteJSON(w, http.StatusOK, rows)
 }
 
 func atoiOr(s string, def int) int {
diff --git a/internal/store/user_store.go b/internal/store/user_store.go
index 6c88f3e3f..32d89da1f 100644
--- a/internal/store/user_store.go
+++ b/internal/store/user_store.go
@@ -36,7 +36,7 @@ func NewUserStore(db *sqlx.DB) UserStore {
        return &mysqlUserStore{db: db}
 }
 
-const userColumns = `id, organization_id, first_name, last_name, middle_name, 
email, status`
+const userColumns = `id, organization_id, first_name, last_name, middle_name, 
email, status, type`
 
 func (s *mysqlUserStore) FindByID(ctx context.Context, id string) 
(*models.User, error) {
        var u models.User
@@ -76,17 +76,17 @@ func (s *mysqlUserStore) FindByOrganization(ctx 
context.Context, organizationID
 
 func (s *mysqlUserStore) Create(ctx context.Context, tx *sql.Tx, u 
*models.User) error {
        _, err := tx.ExecContext(ctx,
-               `INSERT INTO users (id, organization_id, first_name, last_name, 
middle_name, email, status)
-                VALUES (?, ?, ?, ?, ?, ?, ?)`,
-               u.ID, u.OrganizationID, u.FirstName, u.LastName, u.MiddleName, 
u.Email, u.Status)
+               `INSERT INTO users (id, organization_id, first_name, last_name, 
middle_name, email, status, type)
+                VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
+               u.ID, u.OrganizationID, u.FirstName, u.LastName, u.MiddleName, 
u.Email, u.Status, u.Type)
        return err
 }
 
 func (s *mysqlUserStore) Update(ctx context.Context, tx *sql.Tx, u 
*models.User) error {
        _, err := tx.ExecContext(ctx,
-               `UPDATE users SET organization_id = ?, first_name = ?, 
last_name = ?, middle_name = ?, email = ?, status = ?
+               `UPDATE users SET organization_id = ?, first_name = ?, 
last_name = ?, middle_name = ?, email = ?, status = ?, type = ?
                 WHERE id = ?`,
-               u.OrganizationID, u.FirstName, u.LastName, u.MiddleName, 
u.Email, u.Status, u.ID)
+               u.OrganizationID, u.FirstName, u.LastName, u.MiddleName, 
u.Email, u.Status, u.Type, u.ID)
        return err
 }
 
diff --git a/pkg/common/support.go b/pkg/common/support.go
new file mode 100644
index 000000000..ddd8715d8
--- /dev/null
+++ b/pkg/common/support.go
@@ -0,0 +1,62 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package common
+
+import (
+       "encoding/json"
+       "errors"
+       "log/slog"
+       "net/http"
+       "strings"
+
+       "github.com/apache/airavata-custos/pkg/service"
+)
+
+func DecodeJSON(r *http.Request, dst any) error {
+       dec := json.NewDecoder(r.Body)
+       dec.DisallowUnknownFields()
+       return dec.Decode(dst)
+}
+
+func WriteJSON(w http.ResponseWriter, status int, body any) {
+       w.Header().Set("Content-Type", "application/json")
+       w.WriteHeader(status)
+       if body == nil {
+               return
+       }
+       _ = json.NewEncoder(w).Encode(body)
+}
+
+func WriteError(w http.ResponseWriter, status int, err error) {
+       WriteJSON(w, status, map[string]string{"error": err.Error()})
+}
+
+func WriteServiceError(w http.ResponseWriter, err error) {
+       switch {
+       case errors.Is(err, service.ErrNotFound):
+               WriteError(w, http.StatusNotFound, err)
+       case errors.Is(err, service.ErrAlreadyExists):
+               WriteError(w, http.StatusConflict, err)
+       case errors.Is(err, service.ErrInvalidInput):
+               WriteError(w, http.StatusBadRequest, err)
+       default:
+               // Avoid leaking driver messages to clients; log the full error.
+               slog.Error("internal server error", "error", err.Error())
+               WriteError(w, http.StatusInternalServerError, 
errors.New(strings.TrimSpace("internal server error")))
+       }
+}
diff --git a/pkg/models/project.go b/pkg/models/project.go
index f73ec63de..50a70756f 100644
--- a/pkg/models/project.go
+++ b/pkg/models/project.go
@@ -1,3 +1,20 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
 package models
 
 import "time"
@@ -37,6 +54,14 @@ type Organization struct {
        Name         string `json:"name"          db:"name"`
 }
 
+// UserType enumerates the types of users.
+type UserType string
+
+const (
+       UserTypeClusterLocal UserType = "CLUSTER_LOCAL"
+       UserTypeVirtual      UserType = "VIRTUAL"
+)
+
 type User struct {
        ID             string     `json:"id"              db:"id"`
        OrganizationID string     `json:"organization_id" db:"organization_id"`
@@ -45,4 +70,5 @@ type User struct {
        MiddleName     string     `json:"middle_name,omitempty" 
db:"middle_name"`
        Email          string     `json:"email"           db:"email"`
        Status         UserStatus `json:"status"          db:"status"`
+       Type           UserType   `json:"type"            db:"type"`
 }
diff --git a/pkg/service/integration_common_test.go 
b/pkg/service/integration_common_test.go
index 39508711a..10e19f508 100644
--- a/pkg/service/integration_common_test.go
+++ b/pkg/service/integration_common_test.go
@@ -121,8 +121,8 @@ func seedUser(t *testing.T, database *sqlx.DB, email 
string) string {
        orgID := seedOrg(t, database)
        userID := uuid.NewString()
        if _, err := database.Exec(
-               "INSERT INTO users (id, organization_id, first_name, last_name, 
middle_name, email, status) VALUES (?, ?, ?, ?, ?, ?, ?)",
-               userID, orgID, "Test", "User", "", email, 
string(models.UserActive),
+               "INSERT INTO users (id, organization_id, first_name, last_name, 
middle_name, email, status, type) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
+               userID, orgID, "Test", "User", "", email, 
string(models.UserActive), string(models.UserTypeClusterLocal),
        ); err != nil {
                t.Fatalf("seed user %s: %v", email, err)
        }
diff --git a/pkg/service/user.go b/pkg/service/user.go
index 1da52c807..cc3be72fe 100644
--- a/pkg/service/user.go
+++ b/pkg/service/user.go
@@ -57,6 +57,9 @@ func (s *Service) CreateUser(ctx context.Context, user 
*models.User) (*models.Us
        if user.Status == "" {
                user.Status = models.UserActive
        }
+       if user.Type == "" {
+               user.Type = models.UserTypeClusterLocal
+       }
 
        if err := s.inTx(ctx, func(tx *sql.Tx) error {
                return s.users.Create(ctx, tx, user)
@@ -160,6 +163,9 @@ func (s *Service) UpdateUser(ctx context.Context, user 
*models.User) error {
        if user.Status == "" {
                user.Status = existing.Status
        }
+       if user.Type == "" {
+               user.Type = existing.Type
+       }
 
        if err := s.inTx(ctx, func(tx *sql.Tx) error {
                return s.users.Update(ctx, tx, user)

Reply via email to