Files
hanko/backend/handler/metadata_admin.go
2025-09-25 19:15:20 +02:00

189 lines
5.4 KiB
Go

package handler
import (
"encoding/json"
"errors"
"fmt"
"net/http"
jsonpatch "github.com/evanphx/json-patch"
"github.com/gobuffalo/nulls"
"github.com/gofrs/uuid"
"github.com/labstack/echo/v4"
"github.com/teamhanko/hanko/backend/v2/dto/admin"
"github.com/teamhanko/hanko/backend/v2/persistence"
"github.com/teamhanko/hanko/backend/v2/persistence/models"
"github.com/tidwall/gjson"
)
type MetadataAdminHandler struct {
persister persistence.Persister
}
func NewMetadataAdminHandler(persister persistence.Persister) *MetadataAdminHandler {
return &MetadataAdminHandler{
persister: persister,
}
}
func (h *MetadataAdminHandler) GetMetadata(c echo.Context) error {
userID, err := uuid.FromString(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid user id")
}
userExists, err := h.persister.GetConnection().Where("id = ?", userID).Exists(&models.User{ID: userID})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "could not fetch user").SetInternal(err)
}
if !userExists {
return echo.NewHTTPError(http.StatusNotFound, "user not found").SetInternal(err)
}
metadataModel, err := h.persister.GetUserMetadataPersister().Get(userID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "could not fetch metadata").SetInternal(err)
}
response := admin.NewMetadata(metadataModel)
if response == nil {
return c.NoContent(http.StatusNoContent)
}
return c.JSON(http.StatusOK, response)
}
func (h *MetadataAdminHandler) PatchMetadata(c echo.Context) error {
userID, err := uuid.FromString(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid user id")
}
patchMetadataRequest, err := loadDto[admin.PatchMetadataRequest](c)
if err != nil {
return err
}
userExists, err := h.persister.GetConnection().Where("id = ?", userID).Exists(&models.User{ID: userID})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "could not fetch user").SetInternal(err)
}
if !userExists {
return echo.NewHTTPError(http.StatusNotFound, "user not found").SetInternal(err)
}
currentMetadataModel, err := h.persister.GetUserMetadataPersister().Get(userID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "could not fetch metadata").SetInternal(err)
}
_, err = h.applyMetadataPatch(currentMetadataModel, patchMetadataRequest)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "could not patch metadata").SetInternal(err)
}
err = h.persister.GetUserMetadataPersister().Update(currentMetadataModel)
if err != nil {
if persistence.IsMetadataLimitExceededError(err) {
return echo.NewHTTPError(http.StatusBadRequest, err.Error()).
SetInternal(errors.Unwrap(err))
}
return echo.NewHTTPError(http.StatusInternalServerError, "could not save metadata").
SetInternal(err)
}
response := admin.NewMetadata(currentMetadataModel)
if response == nil {
return c.NoContent(http.StatusNoContent)
}
return c.JSON(http.StatusOK, response)
}
func (h *MetadataAdminHandler) applyMetadataPatch(currentMetadataModel *models.UserMetadata, patchMetadataRequest *admin.PatchMetadataRequest) ([]byte, error) {
if patchMetadataRequest.Metadata.Raw == "null" {
currentMetadataModel.Public = nulls.String{}
currentMetadataModel.Private = nulls.String{}
currentMetadataModel.Unsafe = nulls.String{}
return []byte("{}"), nil
}
currentMetadataJSON, err := h.buildCurrentMetadataJSON(currentMetadataModel)
if err != nil {
return nil, err
}
patchedMetadataBytes, err := jsonpatch.MergePatch(
[]byte(currentMetadataJSON),
[]byte(patchMetadataRequest.Metadata.String()),
)
if err != nil {
return nil, fmt.Errorf("could not apply merge patch: %w", err)
}
if !json.Valid(patchedMetadataBytes) {
return nil, fmt.Errorf("invalid metadata JSON after applying merge patch")
}
if err = h.updateMetadataModel(currentMetadataModel, patchedMetadataBytes); err != nil {
return nil, fmt.Errorf("could not update user metadata model: %w", err)
}
return patchedMetadataBytes, nil
}
func (h *MetadataAdminHandler) buildCurrentMetadataJSON(metadata *models.UserMetadata) (string, error) {
result := make(map[string]json.RawMessage)
if metadata.Public.Valid && metadata.Public.String != "{}" {
result["public_metadata"] = json.RawMessage(metadata.Public.String)
}
if metadata.Private.Valid && metadata.Private.String != "{}" {
result["private_metadata"] = json.RawMessage(metadata.Private.String)
}
if metadata.Unsafe.Valid && metadata.Unsafe.String != "{}" {
result["unsafe_metadata"] = json.RawMessage(metadata.Unsafe.String)
}
if len(result) == 0 {
return "{}", nil
}
bytes, err := json.Marshal(result)
if err != nil {
return "", fmt.Errorf("could not build JSON for current metadata: %w", err)
}
return string(bytes), nil
}
func (h *MetadataAdminHandler) updateMetadataModel(metadata *models.UserMetadata, patchedBytes []byte) error {
metadataTypes := map[string]*nulls.String{
"public_metadata": &metadata.Public,
"private_metadata": &metadata.Private,
"unsafe_metadata": &metadata.Unsafe,
}
for key, target := range metadataTypes {
if gjson.GetBytes(patchedBytes, key).Exists() {
value := gjson.GetBytes(patchedBytes, key).String()
if !json.Valid([]byte(value)) {
return fmt.Errorf("invalid JSON for %s", key)
}
*target = nulls.String{
Valid: true,
String: value,
}
} else {
*target = nulls.String{}
}
}
return nil
}