Release v0.0.1 - Initial OAuth2 server implementation
- Add Nostr OAuth2 server with NIP-98 authentication support - Implement OAuth2 authorization and token endpoints - Add .well-known/openid-configuration discovery endpoint - Include Dockerfile for containerized deployment - Add Claude Code release command for version management - Create example configuration file 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
92
internal/handler/userinfo.go
Normal file
92
internal/handler/userinfo.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.mleku.dev/mleku/gitea-nostr-auth/internal/nostr"
|
||||
)
|
||||
|
||||
type UserInfoResponse struct {
|
||||
Sub string `json:"sub"`
|
||||
Name string `json:"name,omitempty"`
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
Email string `json:"email,omitempty"`
|
||||
EmailVerified bool `json:"email_verified,omitempty"`
|
||||
Picture string `json:"picture,omitempty"`
|
||||
Profile string `json:"profile,omitempty"`
|
||||
Website string `json:"website,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
ErrorDesc string `json:"error_description,omitempty"`
|
||||
}
|
||||
|
||||
func (h *Handler) UserInfo(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
// Extract Bearer token
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(UserInfoResponse{
|
||||
Error: "invalid_token",
|
||||
ErrorDesc: "missing or invalid Authorization header",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
|
||||
// Look up access token
|
||||
accessToken, err := h.store.GetAccessToken(token)
|
||||
if err != nil || accessToken == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(UserInfoResponse{
|
||||
Error: "invalid_token",
|
||||
ErrorDesc: "token is invalid or expired",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
pubkey := accessToken.Pubkey
|
||||
npub := nostr.PubkeyToNpub(pubkey)
|
||||
|
||||
// Fetch profile from relays (this also fetches relay list first)
|
||||
log.Printf("Fetching profile for %s from relays...", nostr.TruncateNpub(npub))
|
||||
profile := h.fetcher.FetchProfile(r.Context(), pubkey)
|
||||
|
||||
// Build response with profile data or fallbacks
|
||||
response := UserInfoResponse{
|
||||
Sub: pubkey,
|
||||
}
|
||||
|
||||
if profile != nil {
|
||||
log.Printf("Got profile for %s: name=%s, nip05=%s", nostr.TruncateNpub(npub), profile.Name, profile.Nip05)
|
||||
|
||||
// Use profile data
|
||||
response.Name = profile.GetDisplayName()
|
||||
response.PreferredUsername = profile.GetUsername()
|
||||
response.Picture = profile.Picture
|
||||
response.Website = profile.Website
|
||||
response.Profile = profile.About
|
||||
|
||||
// Use NIP-05 as email if available (it's verified in the Nostr sense)
|
||||
if profile.Nip05 != "" {
|
||||
// NIP-05 format: name@domain.com or _@domain.com
|
||||
response.Email = profile.Nip05
|
||||
response.EmailVerified = false // We haven't verified it ourselves
|
||||
} else {
|
||||
response.Email = nostr.GeneratePlaceholderEmail(pubkey)
|
||||
}
|
||||
} else {
|
||||
log.Printf("No profile found for %s, using defaults", nostr.TruncateNpub(npub))
|
||||
|
||||
// Fallback to generated values
|
||||
response.PreferredUsername = nostr.GenerateUsername(pubkey)
|
||||
response.Name = nostr.TruncateNpub(npub)
|
||||
response.Email = nostr.GeneratePlaceholderEmail(pubkey)
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
Reference in New Issue
Block a user