- Added CORS support in server responses for cross-origin requests (`Access-Control-Allow-Origin`, etc.). - Improved header panel behavior with a sticky position and refined CSS styling. - Integrated profile data fetching (Kind 0 metadata) for user personalization. - Enhanced login functionality to support dynamic profile display based on fetched metadata. - Updated `index.html` to include Tailwind CSS for better design consistency.
267 lines
7.1 KiB
Go
267 lines
7.1 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"lol.mleku.dev/chk"
|
|
"next.orly.dev/app/config"
|
|
"next.orly.dev/pkg/acl"
|
|
"next.orly.dev/pkg/database"
|
|
"next.orly.dev/pkg/encoders/event"
|
|
"next.orly.dev/pkg/encoders/hex"
|
|
"next.orly.dev/pkg/protocol/auth"
|
|
"next.orly.dev/pkg/protocol/publish"
|
|
)
|
|
|
|
type Server struct {
|
|
mux *http.ServeMux
|
|
Config *config.C
|
|
Ctx context.Context
|
|
remote string
|
|
publishers *publish.S
|
|
Admins [][]byte
|
|
*database.D
|
|
|
|
// Challenge storage for HTTP UI authentication
|
|
challengeMutex sync.RWMutex
|
|
challenges map[string][]byte
|
|
}
|
|
|
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
// Set CORS headers for all responses
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
|
|
// Handle preflight OPTIONS requests
|
|
if r.Method == "OPTIONS" {
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
// log.T.C(
|
|
// func() string {
|
|
// return fmt.Sprintf("path %v header %v", r.URL, r.Header)
|
|
// },
|
|
// )
|
|
if r.Header.Get("Upgrade") == "websocket" {
|
|
s.HandleWebsocket(w, r)
|
|
} else if r.Header.Get("Accept") == "application/nostr+json" {
|
|
s.HandleRelayInfo(w, r)
|
|
} else {
|
|
if s.mux == nil {
|
|
http.Error(w, "Upgrade required", http.StatusUpgradeRequired)
|
|
} else {
|
|
s.mux.ServeHTTP(w, r)
|
|
}
|
|
}
|
|
}
|
|
func (s *Server) ServiceURL(req *http.Request) (st string) {
|
|
host := req.Header.Get("X-Forwarded-Host")
|
|
if host == "" {
|
|
host = req.Host
|
|
}
|
|
proto := req.Header.Get("X-Forwarded-Proto")
|
|
if proto == "" {
|
|
if host == "localhost" {
|
|
proto = "ws"
|
|
} else if strings.Contains(host, ":") {
|
|
// has a port number
|
|
proto = "ws"
|
|
} else if _, err := strconv.Atoi(
|
|
strings.ReplaceAll(
|
|
host, ".",
|
|
"",
|
|
),
|
|
); chk.E(err) {
|
|
// it's a naked IP
|
|
proto = "ws"
|
|
} else {
|
|
proto = "wss"
|
|
}
|
|
} else if proto == "https" {
|
|
proto = "wss"
|
|
} else if proto == "http" {
|
|
proto = "ws"
|
|
}
|
|
return proto + "://" + host
|
|
}
|
|
|
|
// UserInterface sets up a basic Nostr NDK interface that allows users to log into the relay user interface
|
|
func (s *Server) UserInterface() {
|
|
if s.mux == nil {
|
|
s.mux = http.NewServeMux()
|
|
}
|
|
|
|
// Initialize challenge storage if not already done
|
|
if s.challenges == nil {
|
|
s.challengeMutex.Lock()
|
|
s.challenges = make(map[string][]byte)
|
|
s.challengeMutex.Unlock()
|
|
}
|
|
|
|
// Serve the main login interface
|
|
s.mux.HandleFunc("/", s.handleLoginInterface)
|
|
|
|
// API endpoints for authentication
|
|
s.mux.HandleFunc("/api/auth/challenge", s.handleAuthChallenge)
|
|
s.mux.HandleFunc("/api/auth/login", s.handleAuthLogin)
|
|
s.mux.HandleFunc("/api/auth/status", s.handleAuthStatus)
|
|
s.mux.HandleFunc("/api/permissions/", s.handlePermissions)
|
|
}
|
|
|
|
// handleLoginInterface serves the main user interface for login
|
|
func (s *Server) handleLoginInterface(w http.ResponseWriter, r *http.Request) {
|
|
// Create a file server handler for the embedded React app
|
|
fileServer := http.FileServer(GetReactAppFS())
|
|
|
|
// Serve the React app files
|
|
fileServer.ServeHTTP(w, r)
|
|
}
|
|
|
|
// handleAuthChallenge generates and returns an authentication challenge
|
|
func (s *Server) handleAuthChallenge(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Generate a proper challenge using the auth package
|
|
challenge := auth.GenerateChallenge()
|
|
challengeHex := hex.Enc(challenge)
|
|
|
|
// Store the challenge using the hex value as the key for easy lookup
|
|
s.challengeMutex.Lock()
|
|
s.challenges[challengeHex] = challenge
|
|
s.challengeMutex.Unlock()
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{"challenge": "` + challengeHex + `"}`))
|
|
}
|
|
|
|
// handleAuthLogin processes authentication requests
|
|
func (s *Server) handleAuthLogin(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
// Read the request body
|
|
body, err := io.ReadAll(r.Body)
|
|
if chk.E(err) {
|
|
w.Write([]byte(`{"success": false, "error": "Failed to read request body"}`))
|
|
return
|
|
}
|
|
|
|
// Parse the signed event
|
|
var evt event.E
|
|
if err = json.Unmarshal(body, &evt); chk.E(err) {
|
|
w.Write([]byte(`{"success": false, "error": "Invalid event format"}`))
|
|
return
|
|
}
|
|
|
|
// Extract the challenge from the event to look up the stored challenge
|
|
challengeTag := evt.Tags.GetFirst([]byte("challenge"))
|
|
if challengeTag == nil {
|
|
w.Write([]byte(`{"success": false, "error": "Challenge tag missing from event"}`))
|
|
return
|
|
}
|
|
|
|
challengeHex := string(challengeTag.Value())
|
|
|
|
// Retrieve the stored challenge
|
|
s.challengeMutex.RLock()
|
|
_, exists := s.challenges[challengeHex]
|
|
s.challengeMutex.RUnlock()
|
|
|
|
if !exists {
|
|
w.Write([]byte(`{"success": false, "error": "Invalid or expired challenge"}`))
|
|
return
|
|
}
|
|
|
|
// Clean up the used challenge
|
|
s.challengeMutex.Lock()
|
|
delete(s.challenges, challengeHex)
|
|
s.challengeMutex.Unlock()
|
|
|
|
relayURL := s.ServiceURL(r)
|
|
|
|
// Validate the authentication event with the correct challenge
|
|
// The challenge in the event tag is hex-encoded, so we need to pass the hex string as bytes
|
|
ok, err := auth.Validate(&evt, []byte(challengeHex), relayURL)
|
|
if chk.E(err) || !ok {
|
|
errorMsg := "Authentication validation failed"
|
|
if err != nil {
|
|
errorMsg = err.Error()
|
|
}
|
|
w.Write([]byte(`{"success": false, "error": "` + errorMsg + `"}`))
|
|
return
|
|
}
|
|
|
|
// Authentication successful
|
|
w.Write([]byte(`{"success": true, "pubkey": "` + hex.Enc(evt.Pubkey) + `", "message": "Authentication successful"}`))
|
|
}
|
|
|
|
// handleAuthStatus returns the current authentication status
|
|
func (s *Server) handleAuthStatus(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{"authenticated": false}`))
|
|
}
|
|
|
|
// handlePermissions returns the permission level for a given pubkey
|
|
func (s *Server) handlePermissions(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Extract pubkey from URL path
|
|
pubkeyHex := strings.TrimPrefix(r.URL.Path, "/api/permissions/")
|
|
if pubkeyHex == "" || pubkeyHex == "/" {
|
|
http.Error(w, "Invalid pubkey", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Convert hex to binary pubkey
|
|
pubkey, err := hex.Dec(pubkeyHex)
|
|
if chk.E(err) {
|
|
http.Error(w, "Invalid pubkey format", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get access level using acl registry
|
|
permission := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
|
|
|
|
// Set content type and write JSON response
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
// Format response as proper JSON
|
|
response := struct {
|
|
Permission string `json:"permission"`
|
|
}{
|
|
Permission: permission,
|
|
}
|
|
|
|
// Marshal and write the response
|
|
jsonData, err := json.Marshal(response)
|
|
if chk.E(err) {
|
|
http.Error(w, "Error generating response", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Write(jsonData)
|
|
}
|