From 14fcce50af44e77fed1351a933fec42d3634ff9d Mon Sep 17 00:00:00 2001 From: Michilis Date: Tue, 5 May 2026 04:02:36 +0000 Subject: [PATCH] Make subscription_type, username, and years optional on POST /v1/admin/users - subscription_type defaults to lifetime when omitted; validated when provided - years is only required (and enforced) when subscription_type is yearly - username uniqueness check and validation are skipped when username is empty - Update OpenAPI spec to reflect pubkey as the only required field --- internal/http/docs/openapi.yaml | 8 ++++---- internal/http/handlers/admin_users.go | 18 ++++++++++++------ internal/user/service.go | 6 ++++-- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/internal/http/docs/openapi.yaml b/internal/http/docs/openapi.yaml index 527588d..5098554 100644 --- a/internal/http/docs/openapi.yaml +++ b/internal/http/docs/openapi.yaml @@ -222,12 +222,12 @@ paths: application/json: schema: type: object - required: [pubkey, username, subscription_type] + required: [pubkey] properties: pubkey: { type: string } - username: { type: string } - subscription_type: { type: string, enum: [yearly, lifetime] } - years: { type: integer } + username: { type: string, description: "Optional NIP-05 username to assign" } + subscription_type: { type: string, enum: [yearly, lifetime], description: "Defaults to lifetime when omitted" } + years: { type: integer, description: "Required when subscription_type is yearly" } responses: '201': description: Created diff --git a/internal/http/handlers/admin_users.go b/internal/http/handlers/admin_users.go index 1f5b1d9..983dcc5 100644 --- a/internal/http/handlers/admin_users.go +++ b/internal/http/handlers/admin_users.go @@ -42,22 +42,28 @@ func (h *AdminUsers) Add(w http.ResponseWriter, r *http.Request) { return } sub := user.SubscriptionType(body.SubscriptionType) - if !sub.Valid() { + if body.SubscriptionType == "" { + sub = user.SubLifetime + } else if !sub.Valid() { WriteError(w, http.StatusBadRequest, "ValidationError", "invalid subscription_type") return } + years := body.Years - if years <= 0 { - years = 1 + if sub == user.SubYearly && years <= 0 { + WriteError(w, http.StatusBadRequest, "ValidationError", "years is required for yearly subscription") + return } if existing, err := h.Users.Repo().GetByPubkey(r.Context(), hexpk); err == nil && existing != nil { WriteError(w, http.StatusConflict, "Conflict", "user already exists") return } - if existing, err := h.Users.Repo().GetByUsername(r.Context(), user.NormalizeUsername(body.Username)); err == nil && existing != nil { - WriteError(w, http.StatusConflict, "Conflict", "username taken") - return + if body.Username != "" { + if existing, err := h.Users.Repo().GetByUsername(r.Context(), user.NormalizeUsername(body.Username)); err == nil && existing != nil { + WriteError(w, http.StatusConflict, "Conflict", "username taken") + return + } } u, err := h.Users.CreateOrActivate(r.Context(), hexpk, body.Username, sub, years, true) diff --git a/internal/user/service.go b/internal/user/service.go index bbdd300..662e617 100644 --- a/internal/user/service.go +++ b/internal/user/service.go @@ -39,8 +39,10 @@ func (s *Service) IsAvailable(ctx context.Context, username string) (bool, error // concerns (e.g. payments worker uses this within a tx). func (s *Service) CreateOrActivate(ctx context.Context, pubkey, username string, sub SubscriptionType, years int, manual bool) (*User, error) { username = NormalizeUsername(username) - if err := ValidateUsername(username, s.reserved); err != nil { - return nil, err + if username != "" { + if err := ValidateUsername(username, s.reserved); err != nil { + return nil, err + } } now := time.Now().UTC() expiresAt := computeExpiry(sub, years, time.Time{}, now)