first commit
This commit is contained in:
80
internal/http/handlers/admin_extend.go
Normal file
80
internal/http/handlers/admin_extend.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/noderunners/nip05api/internal/audit"
|
||||
"github.com/noderunners/nip05api/internal/dm"
|
||||
"github.com/noderunners/nip05api/internal/nostr"
|
||||
"github.com/noderunners/nip05api/internal/user"
|
||||
"github.com/noderunners/nip05api/internal/webhook"
|
||||
)
|
||||
|
||||
type AdminExtend struct {
|
||||
Users *user.Service
|
||||
DMs *dm.Service
|
||||
Hooks *webhook.Service
|
||||
Audit *audit.Logger
|
||||
Domain string
|
||||
Frontend string
|
||||
}
|
||||
|
||||
type extendReq struct {
|
||||
Years int `json:"years"`
|
||||
SubscriptionType string `json:"subscription_type"`
|
||||
}
|
||||
|
||||
func (h *AdminExtend) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
hexpk, err := nostr.NormalizePubkey(chi.URLParam(r, "pubkey"))
|
||||
if err != nil {
|
||||
WriteError(w, http.StatusBadRequest, "ValidationError", "Invalid pubkey format")
|
||||
return
|
||||
}
|
||||
var body extendReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
WriteError(w, http.StatusBadRequest, "ValidationError", "invalid JSON")
|
||||
return
|
||||
}
|
||||
|
||||
u, err := h.Users.Repo().GetByPubkey(r.Context(), hexpk)
|
||||
if errors.Is(err, user.ErrUserNotFound) {
|
||||
WriteError(w, http.StatusNotFound, "NotFound", "user not found")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
WriteError(w, http.StatusInternalServerError, "InternalError", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
sub := u.SubscriptionType
|
||||
if body.SubscriptionType != "" {
|
||||
s := user.SubscriptionType(body.SubscriptionType)
|
||||
if !s.Valid() {
|
||||
WriteError(w, http.StatusBadRequest, "ValidationError", "invalid subscription_type")
|
||||
return
|
||||
}
|
||||
sub = s
|
||||
}
|
||||
years := body.Years
|
||||
if sub == user.SubYearly && years <= 0 {
|
||||
years = 1
|
||||
}
|
||||
|
||||
if err := h.Users.Renew(r.Context(), u, sub, years); err != nil {
|
||||
WriteError(w, http.StatusInternalServerError, "InternalError", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
vars := dmVars(u, h.Domain, h.Frontend)
|
||||
_ = h.DMs.Send(r.Context(), dm.EventExtended, u.Pubkey, vars)
|
||||
_ = h.Hooks.Enqueue(r.Context(), webhook.EventUserExtended, hookData(u, h.Domain))
|
||||
h.Audit.Log(r.Context(), audit.ActionUserExtended, audit.ActorAdmin, u.Pubkey, map[string]any{
|
||||
"subscription_type": string(sub),
|
||||
"years": years,
|
||||
})
|
||||
|
||||
WriteJSON(w, http.StatusOK, userResponse(u))
|
||||
}
|
||||
57
internal/http/handlers/admin_helpers.go
Normal file
57
internal/http/handlers/admin_helpers.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/noderunners/nip05api/internal/nostr"
|
||||
"github.com/noderunners/nip05api/internal/user"
|
||||
)
|
||||
|
||||
func userResponse(u *user.User) map[string]any {
|
||||
resp := map[string]any{
|
||||
"pubkey": u.Pubkey,
|
||||
"npub": nostr.HexToNpub(u.Pubkey),
|
||||
"username": u.Username,
|
||||
"subscription_type": string(u.SubscriptionType),
|
||||
"is_active": u.IsActive,
|
||||
"manual_username": u.ManualUsername,
|
||||
"created_at": u.CreatedAt.UTC().Format(time.RFC3339),
|
||||
}
|
||||
if u.ExpiresAt != nil {
|
||||
resp["expires_at"] = u.ExpiresAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
if u.DeactivatedAt != nil {
|
||||
resp["deactivated_at"] = u.DeactivatedAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func dmVars(u *user.User, domain, frontend string) map[string]string {
|
||||
expires := "lifetime"
|
||||
if u.ExpiresAt != nil {
|
||||
expires = u.ExpiresAt.Format("2006-01-02")
|
||||
}
|
||||
return map[string]string{
|
||||
"username": u.Username,
|
||||
"npub": nostr.HexToNpub(u.Pubkey),
|
||||
"pubkey": u.Pubkey,
|
||||
"domain": domain,
|
||||
"expires_at": expires,
|
||||
"days_remaining": "",
|
||||
"frontend_url": frontend,
|
||||
"subscription_type": string(u.SubscriptionType),
|
||||
}
|
||||
}
|
||||
|
||||
func hookData(u *user.User, domain string) map[string]any {
|
||||
d := map[string]any{
|
||||
"pubkey": u.Pubkey,
|
||||
"npub": nostr.HexToNpub(u.Pubkey),
|
||||
"username": u.Username,
|
||||
"subscription_type": string(u.SubscriptionType),
|
||||
}
|
||||
if u.ExpiresAt != nil {
|
||||
d["expires_at"] = u.ExpiresAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
return d
|
||||
}
|
||||
166
internal/http/handlers/admin_users.go
Normal file
166
internal/http/handlers/admin_users.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/noderunners/nip05api/internal/audit"
|
||||
"github.com/noderunners/nip05api/internal/dm"
|
||||
"github.com/noderunners/nip05api/internal/nostr"
|
||||
"github.com/noderunners/nip05api/internal/user"
|
||||
"github.com/noderunners/nip05api/internal/webhook"
|
||||
)
|
||||
|
||||
type AdminUsers struct {
|
||||
Users *user.Service
|
||||
DMs *dm.Service
|
||||
Hooks *webhook.Service
|
||||
Audit *audit.Logger
|
||||
Domain string
|
||||
Frontend string
|
||||
}
|
||||
|
||||
type adminAddReq struct {
|
||||
Pubkey string `json:"pubkey"`
|
||||
Username string `json:"username"`
|
||||
SubscriptionType string `json:"subscription_type"`
|
||||
Years int `json:"years"`
|
||||
}
|
||||
|
||||
func (h *AdminUsers) Add(w http.ResponseWriter, r *http.Request) {
|
||||
var body adminAddReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
WriteError(w, http.StatusBadRequest, "ValidationError", "invalid JSON")
|
||||
return
|
||||
}
|
||||
hexpk, err := nostr.NormalizePubkey(body.Pubkey)
|
||||
if err != nil {
|
||||
WriteError(w, http.StatusBadRequest, "ValidationError", "Invalid pubkey format")
|
||||
return
|
||||
}
|
||||
sub := user.SubscriptionType(body.SubscriptionType)
|
||||
if !sub.Valid() {
|
||||
WriteError(w, http.StatusBadRequest, "ValidationError", "invalid subscription_type")
|
||||
return
|
||||
}
|
||||
years := body.Years
|
||||
if years <= 0 {
|
||||
years = 1
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
u, err := h.Users.CreateOrActivate(r.Context(), hexpk, body.Username, sub, years, true)
|
||||
if err != nil {
|
||||
if errors.Is(err, user.ErrInvalidUsername) {
|
||||
WriteError(w, http.StatusBadRequest, "ValidationError", "invalid username")
|
||||
return
|
||||
}
|
||||
WriteError(w, http.StatusInternalServerError, "InternalError", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
vars := dmVars(u, h.Domain, h.Frontend)
|
||||
_ = h.DMs.Send(r.Context(), dm.EventWelcome, u.Pubkey, vars)
|
||||
_ = h.Hooks.Enqueue(r.Context(), webhook.EventUserAdded, hookData(u, h.Domain))
|
||||
h.Audit.Log(r.Context(), audit.ActionUserAdded, audit.ActorAdmin, u.Pubkey, map[string]any{
|
||||
"username": u.Username,
|
||||
"subscription_type": string(u.SubscriptionType),
|
||||
"years": years,
|
||||
})
|
||||
|
||||
WriteJSON(w, http.StatusCreated, userResponse(u))
|
||||
}
|
||||
|
||||
func (h *AdminUsers) List(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
activeOnly := q.Get("active") == "true"
|
||||
limit, _ := strconv.Atoi(q.Get("limit"))
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
users, err := h.Users.Repo().List(r.Context(), user.ListFilter{
|
||||
ActiveOnly: activeOnly,
|
||||
Search: q.Get("q"),
|
||||
Limit: limit,
|
||||
})
|
||||
if err != nil {
|
||||
WriteError(w, http.StatusInternalServerError, "InternalError", err.Error())
|
||||
return
|
||||
}
|
||||
out := make([]map[string]any, 0, len(users))
|
||||
for _, u := range users {
|
||||
out = append(out, userResponse(u))
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (h *AdminUsers) Update(w http.ResponseWriter, r *http.Request) {
|
||||
hexpk, err := nostr.NormalizePubkey(chi.URLParam(r, "pubkey"))
|
||||
if err != nil {
|
||||
WriteError(w, http.StatusBadRequest, "ValidationError", "Invalid pubkey format")
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Username string `json:"username"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
WriteError(w, http.StatusBadRequest, "ValidationError", "invalid JSON")
|
||||
return
|
||||
}
|
||||
u, err := h.Users.SetUsername(r.Context(), hexpk, body.Username)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, user.ErrUserNotFound):
|
||||
WriteError(w, http.StatusNotFound, "NotFound", "user not found")
|
||||
case errors.Is(err, user.ErrInvalidUsername):
|
||||
WriteError(w, http.StatusBadRequest, "ValidationError", "invalid username")
|
||||
case errors.Is(err, user.ErrUsernameTaken):
|
||||
WriteError(w, http.StatusConflict, "Conflict", "username taken")
|
||||
default:
|
||||
WriteError(w, http.StatusInternalServerError, "InternalError", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
h.Audit.Log(r.Context(), audit.ActionUserUsernameChanged, audit.ActorAdmin, u.Pubkey, map[string]any{
|
||||
"username": u.Username,
|
||||
})
|
||||
WriteJSON(w, http.StatusOK, userResponse(u))
|
||||
}
|
||||
|
||||
func (h *AdminUsers) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
hexpk, err := nostr.NormalizePubkey(chi.URLParam(r, "pubkey"))
|
||||
if err != nil {
|
||||
WriteError(w, http.StatusBadRequest, "ValidationError", "Invalid pubkey format")
|
||||
return
|
||||
}
|
||||
u, err := h.Users.Repo().GetByPubkey(r.Context(), hexpk)
|
||||
if errors.Is(err, user.ErrUserNotFound) {
|
||||
WriteError(w, http.StatusNotFound, "NotFound", "user not found")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
WriteError(w, http.StatusInternalServerError, "InternalError", err.Error())
|
||||
return
|
||||
}
|
||||
if err := h.Users.Delete(r.Context(), hexpk); err != nil {
|
||||
WriteError(w, http.StatusInternalServerError, "InternalError", err.Error())
|
||||
return
|
||||
}
|
||||
_ = h.Hooks.Enqueue(r.Context(), webhook.EventUserRemoved, hookData(u, h.Domain))
|
||||
h.Audit.Log(r.Context(), audit.ActionUserDeleted, audit.ActorAdmin, u.Pubkey, map[string]any{
|
||||
"username": u.Username,
|
||||
})
|
||||
WriteJSON(w, http.StatusOK, map[string]bool{"deleted": true})
|
||||
}
|
||||
|
||||
26
internal/http/handlers/health.go
Normal file
26
internal/http/handlers/health.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/noderunners/nip05api/internal/db"
|
||||
)
|
||||
|
||||
type Health struct {
|
||||
DB *db.DB
|
||||
Version string
|
||||
}
|
||||
|
||||
func (h *Health) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.DB.Ping(r.Context()); err != nil {
|
||||
WriteJSON(w, http.StatusServiceUnavailable, map[string]string{
|
||||
"status": "down",
|
||||
"version": h.Version,
|
||||
})
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, map[string]string{
|
||||
"status": "ok",
|
||||
"version": h.Version,
|
||||
})
|
||||
}
|
||||
109
internal/http/handlers/invoices.go
Normal file
109
internal/http/handlers/invoices.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/noderunners/nip05api/internal/invoice"
|
||||
"github.com/noderunners/nip05api/internal/nostr"
|
||||
"github.com/noderunners/nip05api/internal/user"
|
||||
)
|
||||
|
||||
type Invoices struct {
|
||||
Service *invoice.Service
|
||||
LightningEnabled bool
|
||||
}
|
||||
|
||||
type createInvoiceReq struct {
|
||||
Username string `json:"username"`
|
||||
Pubkey string `json:"pubkey"`
|
||||
SubscriptionType string `json:"subscription_type"`
|
||||
Years int `json:"years"`
|
||||
}
|
||||
|
||||
func (h *Invoices) Create(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.LightningEnabled {
|
||||
WriteError(w, http.StatusServiceUnavailable, "LightningDisabled", "lightning payments are disabled")
|
||||
return
|
||||
}
|
||||
var body createInvoiceReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
WriteError(w, http.StatusBadRequest, "ValidationError", "invalid JSON")
|
||||
return
|
||||
}
|
||||
hexpk, err := nostr.NormalizePubkey(body.Pubkey)
|
||||
if err != nil {
|
||||
WriteError(w, http.StatusBadRequest, "ValidationError", "Invalid pubkey format")
|
||||
return
|
||||
}
|
||||
subStr := strings.TrimSpace(body.SubscriptionType)
|
||||
if subStr == "" {
|
||||
subStr = string(user.SubLifetime)
|
||||
}
|
||||
sub := user.SubscriptionType(subStr)
|
||||
if !sub.Valid() {
|
||||
WriteError(w, http.StatusBadRequest, "ValidationError", "invalid subscription_type")
|
||||
return
|
||||
}
|
||||
years := body.Years
|
||||
if sub == user.SubYearly && years <= 0 {
|
||||
years = 1
|
||||
}
|
||||
p, err := h.Service.Create(r.Context(), invoice.CreateRequest{
|
||||
Username: body.Username,
|
||||
Pubkey: hexpk,
|
||||
SubscriptionType: sub,
|
||||
Years: years,
|
||||
})
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, invoice.ErrLifetimeAccess):
|
||||
WriteError(w, http.StatusForbidden, "User already has lifetime access", "")
|
||||
case errors.Is(err, invoice.ErrPendingInvoiceExists):
|
||||
WriteError(w, http.StatusConflict, "Conflict", err.Error())
|
||||
case errors.Is(err, invoice.ErrUsernameTaken),
|
||||
errors.Is(err, user.ErrUsernameTaken):
|
||||
WriteError(w, http.StatusConflict, "Conflict", "username unavailable")
|
||||
case errors.Is(err, invoice.ErrUsernameMismatch):
|
||||
WriteError(w, http.StatusConflict, "Conflict", err.Error())
|
||||
case errors.Is(err, user.ErrInvalidUsername),
|
||||
errors.Is(err, invoice.ErrInvalidYears):
|
||||
WriteError(w, http.StatusBadRequest, "ValidationError", err.Error())
|
||||
case errors.Is(err, invoice.ErrLNbits):
|
||||
WriteError(w, http.StatusServiceUnavailable, "LightningError", err.Error())
|
||||
default:
|
||||
WriteError(w, http.StatusInternalServerError, "InternalError", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"payment_hash": p.PaymentHash,
|
||||
"payment_request": p.PaymentRequest,
|
||||
"amount_sats": p.AmountSats,
|
||||
"expires_at": p.ExpiresAt.UTC().Format(time.RFC3339),
|
||||
"username": p.Username,
|
||||
"is_renewal": p.IsRenewal,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Invoices) Get(w http.ResponseWriter, r *http.Request) {
|
||||
hash := chi.URLParam(r, "payment_hash")
|
||||
p, err := h.Service.Repo().Get(r.Context(), hash)
|
||||
if errors.Is(err, invoice.ErrInvoiceNotFound) {
|
||||
WriteError(w, http.StatusNotFound, "NotFound", "invoice not found")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
WriteError(w, http.StatusInternalServerError, "InternalError", err.Error())
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"payment_hash": p.PaymentHash,
|
||||
"status": string(p.Status()),
|
||||
"username": p.Username,
|
||||
})
|
||||
}
|
||||
41
internal/http/handlers/nostrjson.go
Normal file
41
internal/http/handlers/nostrjson.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/noderunners/nip05api/internal/user"
|
||||
)
|
||||
|
||||
type NostrJSON struct {
|
||||
Users *user.Service
|
||||
Relays []string
|
||||
}
|
||||
|
||||
func (h *NostrJSON) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "public, max-age=60")
|
||||
|
||||
names, err := h.Users.Repo().ActiveByName(r.Context())
|
||||
if err != nil {
|
||||
WriteError(w, http.StatusInternalServerError, "InternalError", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if q := r.URL.Query().Get("name"); q != "" {
|
||||
filtered := map[string]string{}
|
||||
if pk, ok := names[q]; ok {
|
||||
filtered[q] = pk
|
||||
}
|
||||
names = filtered
|
||||
}
|
||||
|
||||
relays := map[string][]string{}
|
||||
if len(h.Relays) > 0 {
|
||||
for _, pk := range names {
|
||||
relays[pk] = h.Relays
|
||||
}
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"names": names,
|
||||
"relays": relays,
|
||||
})
|
||||
}
|
||||
17
internal/http/handlers/pricing.go
Normal file
17
internal/http/handlers/pricing.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package handlers
|
||||
|
||||
import "net/http"
|
||||
|
||||
type Pricing struct {
|
||||
YearlySats int64
|
||||
LifetimeSats int64
|
||||
LightningEnabled bool
|
||||
}
|
||||
|
||||
func (h *Pricing) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"yearly_sats": h.YearlySats,
|
||||
"lifetime_sats": h.LifetimeSats,
|
||||
"lightning_enabled": h.LightningEnabled,
|
||||
})
|
||||
}
|
||||
19
internal/http/handlers/respond.go
Normal file
19
internal/http/handlers/respond.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func WriteJSON(w http.ResponseWriter, code int, body any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
if body == nil {
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(body)
|
||||
}
|
||||
|
||||
func WriteError(w http.ResponseWriter, code int, kind, detail string) {
|
||||
WriteJSON(w, code, map[string]string{"error": kind, "detail": detail})
|
||||
}
|
||||
40
internal/http/handlers/usernames.go
Normal file
40
internal/http/handlers/usernames.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/noderunners/nip05api/internal/user"
|
||||
)
|
||||
|
||||
type Usernames struct{ Users *user.Service }
|
||||
|
||||
func (h *Usernames) Available(w http.ResponseWriter, r *http.Request) {
|
||||
name := user.NormalizeUsername(chi.URLParam(r, "name"))
|
||||
if err := user.ValidateUsername(name, h.Users.Reserved()); err != nil {
|
||||
WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"username": name,
|
||||
"available": false,
|
||||
"reason": "invalid_or_reserved",
|
||||
})
|
||||
return
|
||||
}
|
||||
avail, err := h.Users.IsAvailable(r.Context(), name)
|
||||
if err != nil {
|
||||
if errors.Is(err, user.ErrInvalidUsername) {
|
||||
WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"username": name,
|
||||
"available": false,
|
||||
"reason": "invalid",
|
||||
})
|
||||
return
|
||||
}
|
||||
WriteError(w, http.StatusInternalServerError, "InternalError", err.Error())
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"username": name,
|
||||
"available": avail,
|
||||
})
|
||||
}
|
||||
63
internal/http/handlers/users.go
Normal file
63
internal/http/handlers/users.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/noderunners/nip05api/internal/nostr"
|
||||
"github.com/noderunners/nip05api/internal/user"
|
||||
)
|
||||
|
||||
type Users struct {
|
||||
Users *user.Service
|
||||
GraceDays int
|
||||
}
|
||||
|
||||
func (h *Users) Get(w http.ResponseWriter, r *http.Request) {
|
||||
hexpk, err := nostr.NormalizePubkey(chi.URLParam(r, "pubkey"))
|
||||
if err != nil {
|
||||
WriteError(w, http.StatusBadRequest, "ValidationError", "Invalid pubkey format")
|
||||
return
|
||||
}
|
||||
u, err := h.Users.Repo().GetByPubkey(r.Context(), hexpk)
|
||||
if errors.Is(err, user.ErrUserNotFound) {
|
||||
WriteError(w, http.StatusNotFound, "NotFound", "user not registered")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
WriteError(w, http.StatusInternalServerError, "InternalError", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp := map[string]any{
|
||||
"pubkey": u.Pubkey,
|
||||
"npub": nostr.HexToNpub(u.Pubkey),
|
||||
}
|
||||
if u.IsActive {
|
||||
resp["is_whitelisted"] = true
|
||||
resp["username"] = u.Username
|
||||
if u.ExpiresAt != nil {
|
||||
resp["expires_at"] = u.ExpiresAt.UTC().Format(time.RFC3339)
|
||||
} else {
|
||||
resp["expires_at"] = nil
|
||||
}
|
||||
resp["subscription_type"] = string(u.SubscriptionType)
|
||||
} else {
|
||||
resp["is_whitelisted"] = false
|
||||
if u.ExpiresAt != nil {
|
||||
resp["expired_at"] = u.ExpiresAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
if u.DeactivatedAt != nil {
|
||||
cutoff := u.DeactivatedAt.Add(time.Duration(h.GraceDays) * 24 * time.Hour)
|
||||
if time.Now().UTC().Before(cutoff) {
|
||||
resp["in_grace"] = true
|
||||
resp["reserved_username"] = u.Username
|
||||
} else {
|
||||
resp["in_grace"] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
Reference in New Issue
Block a user