userinfo.go raw

   1  package handler
   2  
   3  import (
   4  	"encoding/json"
   5  	"log"
   6  	"net/http"
   7  	"strings"
   8  
   9  	"github.com/mlekudev/gitea-nostr-auth/internal/nostr"
  10  )
  11  
  12  type UserInfoResponse struct {
  13  	Sub               string `json:"sub"`
  14  	Name              string `json:"name,omitempty"`
  15  	PreferredUsername string `json:"preferred_username"`
  16  	Email             string `json:"email,omitempty"`
  17  	EmailVerified     bool   `json:"email_verified,omitempty"`
  18  	Picture           string `json:"picture,omitempty"`
  19  	Profile           string `json:"profile,omitempty"`
  20  	Website           string `json:"website,omitempty"`
  21  	Error             string `json:"error,omitempty"`
  22  	ErrorDesc         string `json:"error_description,omitempty"`
  23  }
  24  
  25  func (h *Handler) UserInfo(w http.ResponseWriter, r *http.Request) {
  26  	w.Header().Set("Content-Type", "application/json")
  27  
  28  	// Extract Bearer token
  29  	authHeader := r.Header.Get("Authorization")
  30  	if !strings.HasPrefix(authHeader, "Bearer ") {
  31  		w.WriteHeader(http.StatusUnauthorized)
  32  		json.NewEncoder(w).Encode(UserInfoResponse{
  33  			Error:     "invalid_token",
  34  			ErrorDesc: "missing or invalid Authorization header",
  35  		})
  36  		return
  37  	}
  38  
  39  	token := strings.TrimPrefix(authHeader, "Bearer ")
  40  
  41  	// Look up access token
  42  	accessToken, err := h.store.GetAccessToken(token)
  43  	if err != nil || accessToken == nil {
  44  		w.WriteHeader(http.StatusUnauthorized)
  45  		json.NewEncoder(w).Encode(UserInfoResponse{
  46  			Error:     "invalid_token",
  47  			ErrorDesc: "token is invalid or expired",
  48  		})
  49  		return
  50  	}
  51  
  52  	pubkey := accessToken.Pubkey
  53  	npub := nostr.PubkeyToNpub(pubkey)
  54  
  55  	// Fetch profile from relays (this also fetches relay list first)
  56  	log.Printf("Fetching profile for %s from relays...", nostr.TruncateNpub(npub))
  57  	profile := h.fetcher.FetchProfile(r.Context(), pubkey)
  58  
  59  	// Build response with profile data or fallbacks
  60  	response := UserInfoResponse{
  61  		Sub: pubkey,
  62  	}
  63  
  64  	if profile != nil {
  65  		log.Printf("Got profile for %s: name=%s, nip05=%s", nostr.TruncateNpub(npub), profile.Name, profile.Nip05)
  66  
  67  		// Use profile data
  68  		response.Name = profile.GetDisplayName()
  69  		response.PreferredUsername = profile.GetUsername()
  70  		response.Picture = profile.Picture
  71  		response.Website = profile.Website
  72  		response.Profile = profile.About
  73  
  74  		// Use NIP-05 as email if available (it's verified in the Nostr sense)
  75  		if profile.Nip05 != "" {
  76  			// NIP-05 format: name@domain.com or _@domain.com
  77  			response.Email = profile.Nip05
  78  			response.EmailVerified = false // We haven't verified it ourselves
  79  		} else {
  80  			response.Email = nostr.GeneratePlaceholderEmail(pubkey)
  81  		}
  82  	} else {
  83  		log.Printf("No profile found for %s, using defaults", nostr.TruncateNpub(npub))
  84  
  85  		// Fallback to generated values
  86  		response.PreferredUsername = nostr.GenerateUsername(pubkey)
  87  		response.Name = nostr.TruncateNpub(npub)
  88  		response.Email = nostr.GeneratePlaceholderEmail(pubkey)
  89  	}
  90  
  91  	json.NewEncoder(w).Encode(response)
  92  }
  93