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
This commit is contained in:
2026-05-05 04:02:36 +00:00
parent 611ef5fc4a
commit 14fcce50af
3 changed files with 20 additions and 12 deletions

View File

@@ -222,12 +222,12 @@ paths:
application/json: application/json:
schema: schema:
type: object type: object
required: [pubkey, username, subscription_type] required: [pubkey]
properties: properties:
pubkey: { type: string } pubkey: { type: string }
username: { type: string } username: { type: string, description: "Optional NIP-05 username to assign" }
subscription_type: { type: string, enum: [yearly, lifetime] } subscription_type: { type: string, enum: [yearly, lifetime], description: "Defaults to lifetime when omitted" }
years: { type: integer } years: { type: integer, description: "Required when subscription_type is yearly" }
responses: responses:
'201': '201':
description: Created description: Created

View File

@@ -42,22 +42,28 @@ func (h *AdminUsers) Add(w http.ResponseWriter, r *http.Request) {
return return
} }
sub := user.SubscriptionType(body.SubscriptionType) 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") WriteError(w, http.StatusBadRequest, "ValidationError", "invalid subscription_type")
return return
} }
years := body.Years years := body.Years
if years <= 0 { if sub == user.SubYearly && years <= 0 {
years = 1 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 { if existing, err := h.Users.Repo().GetByPubkey(r.Context(), hexpk); err == nil && existing != nil {
WriteError(w, http.StatusConflict, "Conflict", "user already exists") WriteError(w, http.StatusConflict, "Conflict", "user already exists")
return return
} }
if existing, err := h.Users.Repo().GetByUsername(r.Context(), user.NormalizeUsername(body.Username)); err == nil && existing != nil { if body.Username != "" {
WriteError(w, http.StatusConflict, "Conflict", "username taken") if existing, err := h.Users.Repo().GetByUsername(r.Context(), user.NormalizeUsername(body.Username)); err == nil && existing != nil {
return WriteError(w, http.StatusConflict, "Conflict", "username taken")
return
}
} }
u, err := h.Users.CreateOrActivate(r.Context(), hexpk, body.Username, sub, years, true) u, err := h.Users.CreateOrActivate(r.Context(), hexpk, body.Username, sub, years, true)

View File

@@ -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). // 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) { func (s *Service) CreateOrActivate(ctx context.Context, pubkey, username string, sub SubscriptionType, years int, manual bool) (*User, error) {
username = NormalizeUsername(username) username = NormalizeUsername(username)
if err := ValidateUsername(username, s.reserved); err != nil { if username != "" {
return nil, err if err := ValidateUsername(username, s.reserved); err != nil {
return nil, err
}
} }
now := time.Now().UTC() now := time.Now().UTC()
expiresAt := computeExpiry(sub, years, time.Time{}, now) expiresAt := computeExpiry(sub, years, time.Time{}, now)