Enable dev mode for React app with proxy support; refine build, styles, and UI.
- Adjusted `package.json` scripts for Bun dev server and build flow. - Added `dev.html` for standalone web development with hot-reload enabled. - Introduced `WebDisableEmbedded` and `WebDevProxyURL` configurations to support proxying non-API paths. - Refactored server logic to handle reverse proxy for development mode. - Updated `App.jsx` structure, styles, and layout for responsiveness and dynamic padding. - Improved login interface with logo support and cleaner design. - Enhanced development flow documentation in `README.md`.
This commit is contained in:
@@ -42,6 +42,10 @@ type C struct {
|
|||||||
ACLMode string `env:"ORLY_ACL_MODE" usage:"ACL mode: follows,none" default:"none"`
|
ACLMode string `env:"ORLY_ACL_MODE" usage:"ACL mode: follows,none" default:"none"`
|
||||||
SpiderMode string `env:"ORLY_SPIDER_MODE" usage:"spider mode: none,follow" default:"none"`
|
SpiderMode string `env:"ORLY_SPIDER_MODE" usage:"spider mode: none,follow" default:"none"`
|
||||||
SpiderFrequency time.Duration `env:"ORLY_SPIDER_FREQUENCY" usage:"spider frequency in seconds" default:"1h"`
|
SpiderFrequency time.Duration `env:"ORLY_SPIDER_FREQUENCY" usage:"spider frequency in seconds" default:"1h"`
|
||||||
|
|
||||||
|
// Web UI and dev mode settings
|
||||||
|
WebDisableEmbedded bool `env:"ORLY_WEB_DISABLE" default:"false" usage:"disable serving the embedded web UI; useful for hot-reload during development"`
|
||||||
|
WebDevProxyURL string `env:"ORLY_WEB_DEV_PROXY_URL" usage:"when ORLY_WEB_DISABLE is true, reverse-proxy non-API paths to this dev server URL (e.g. http://localhost:5173)"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates and initializes a new configuration object for the relay
|
// New creates and initializes a new configuration object for the relay
|
||||||
|
|||||||
161
app/server.go
161
app/server.go
@@ -4,7 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -27,7 +30,10 @@ type Server struct {
|
|||||||
publishers *publish.S
|
publishers *publish.S
|
||||||
Admins [][]byte
|
Admins [][]byte
|
||||||
*database.D
|
*database.D
|
||||||
|
|
||||||
|
// optional reverse proxy for dev web server
|
||||||
|
devProxy *httputil.ReverseProxy
|
||||||
|
|
||||||
// Challenge storage for HTTP UI authentication
|
// Challenge storage for HTTP UI authentication
|
||||||
challengeMutex sync.RWMutex
|
challengeMutex sync.RWMutex
|
||||||
challenges map[string][]byte
|
challenges map[string][]byte
|
||||||
@@ -37,31 +43,40 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Set CORS headers for all responses
|
// Set CORS headers for all responses
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
w.Header().Set(
|
||||||
|
"Access-Control-Allow-Headers", "Content-Type, Authorization",
|
||||||
|
)
|
||||||
|
|
||||||
// Handle preflight OPTIONS requests
|
// Handle preflight OPTIONS requests
|
||||||
if r.Method == "OPTIONS" {
|
if r.Method == "OPTIONS" {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// log.T.C(
|
// If this is a websocket request, only intercept the relay root path.
|
||||||
// func() string {
|
// This allows other websocket paths (e.g., Vite HMR) to be handled by the dev proxy when enabled.
|
||||||
// return fmt.Sprintf("path %v header %v", r.URL, r.Header)
|
|
||||||
// },
|
|
||||||
// )
|
|
||||||
if r.Header.Get("Upgrade") == "websocket" {
|
if r.Header.Get("Upgrade") == "websocket" {
|
||||||
s.HandleWebsocket(w, r)
|
if s.mux != nil && s.Config != nil && s.Config.WebDisableEmbedded && s.Config.WebDevProxyURL != "" && r.URL.Path != "/" {
|
||||||
} else if r.Header.Get("Accept") == "application/nostr+json" {
|
// forward to mux (which will proxy to dev server)
|
||||||
s.HandleRelayInfo(w, r)
|
|
||||||
} else {
|
|
||||||
if s.mux == nil {
|
|
||||||
http.Error(w, "Upgrade required", http.StatusUpgradeRequired)
|
|
||||||
} else {
|
|
||||||
s.mux.ServeHTTP(w, r)
|
s.mux.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
s.HandleWebsocket(w, r)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r.Header.Get("Accept") == "application/nostr+json" {
|
||||||
|
s.HandleRelayInfo(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.mux == nil {
|
||||||
|
http.Error(w, "Upgrade required", http.StatusUpgradeRequired)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.mux.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) ServiceURL(req *http.Request) (st string) {
|
func (s *Server) ServiceURL(req *http.Request) (st string) {
|
||||||
host := req.Header.Get("X-Forwarded-Host")
|
host := req.Header.Get("X-Forwarded-Host")
|
||||||
if host == "" {
|
if host == "" {
|
||||||
@@ -98,17 +113,43 @@ func (s *Server) UserInterface() {
|
|||||||
if s.mux == nil {
|
if s.mux == nil {
|
||||||
s.mux = http.NewServeMux()
|
s.mux = http.NewServeMux()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If dev proxy is configured, initialize it
|
||||||
|
if s.Config != nil && s.Config.WebDisableEmbedded && s.Config.WebDevProxyURL != "" {
|
||||||
|
proxyURL := s.Config.WebDevProxyURL
|
||||||
|
// Add default scheme if missing to avoid: proxy error: unsupported protocol scheme ""
|
||||||
|
if !strings.Contains(proxyURL, "://") {
|
||||||
|
proxyURL = "http://" + proxyURL
|
||||||
|
}
|
||||||
|
if target, err := url.Parse(proxyURL); !chk.E(err) {
|
||||||
|
if target.Scheme == "" || target.Host == "" {
|
||||||
|
// invalid URL, disable proxy
|
||||||
|
log.Printf(
|
||||||
|
"invalid ORLY_WEB_DEV_PROXY_URL: %q — disabling dev proxy\n",
|
||||||
|
s.Config.WebDevProxyURL,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
s.devProxy = httputil.NewSingleHostReverseProxy(target)
|
||||||
|
// Ensure Host header points to upstream for dev servers that care
|
||||||
|
origDirector := s.devProxy.Director
|
||||||
|
s.devProxy.Director = func(req *http.Request) {
|
||||||
|
origDirector(req)
|
||||||
|
req.Host = target.Host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize challenge storage if not already done
|
// Initialize challenge storage if not already done
|
||||||
if s.challenges == nil {
|
if s.challenges == nil {
|
||||||
s.challengeMutex.Lock()
|
s.challengeMutex.Lock()
|
||||||
s.challenges = make(map[string][]byte)
|
s.challenges = make(map[string][]byte)
|
||||||
s.challengeMutex.Unlock()
|
s.challengeMutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve the main login interface
|
// Serve the main login interface (and static assets) or proxy in dev mode
|
||||||
s.mux.HandleFunc("/", s.handleLoginInterface)
|
s.mux.HandleFunc("/", s.handleLoginInterface)
|
||||||
|
|
||||||
// API endpoints for authentication
|
// API endpoints for authentication
|
||||||
s.mux.HandleFunc("/api/auth/challenge", s.handleAuthChallenge)
|
s.mux.HandleFunc("/api/auth/challenge", s.handleAuthChallenge)
|
||||||
s.mux.HandleFunc("/api/auth/login", s.handleAuthLogin)
|
s.mux.HandleFunc("/api/auth/login", s.handleAuthLogin)
|
||||||
@@ -119,10 +160,20 @@ func (s *Server) UserInterface() {
|
|||||||
|
|
||||||
// handleLoginInterface serves the main user interface for login
|
// handleLoginInterface serves the main user interface for login
|
||||||
func (s *Server) handleLoginInterface(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleLoginInterface(w http.ResponseWriter, r *http.Request) {
|
||||||
// Create a file server handler for the embedded React app
|
// In dev mode with proxy configured, forward to dev server
|
||||||
|
if s.Config != nil && s.Config.WebDisableEmbedded && s.devProxy != nil {
|
||||||
|
s.devProxy.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// If embedded UI is disabled but no proxy configured, return a helpful message
|
||||||
|
if s.Config != nil && s.Config.WebDisableEmbedded {
|
||||||
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
w.Write([]byte("Web UI disabled (ORLY_WEB_DISABLE=true). Run the web app in standalone dev mode (e.g., npm run dev) or set ORLY_WEB_DEV_PROXY_URL to proxy through this server."))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Default: serve embedded React app
|
||||||
fileServer := http.FileServer(GetReactAppFS())
|
fileServer := http.FileServer(GetReactAppFS())
|
||||||
|
|
||||||
// Serve the React app files
|
|
||||||
fileServer.ServeHTTP(w, r)
|
fileServer.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,16 +183,16 @@ func (s *Server) handleAuthChallenge(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a proper challenge using the auth package
|
// Generate a proper challenge using the auth package
|
||||||
challenge := auth.GenerateChallenge()
|
challenge := auth.GenerateChallenge()
|
||||||
challengeHex := hex.Enc(challenge)
|
challengeHex := hex.Enc(challenge)
|
||||||
|
|
||||||
// Store the challenge using the hex value as the key for easy lookup
|
// Store the challenge using the hex value as the key for easy lookup
|
||||||
s.challengeMutex.Lock()
|
s.challengeMutex.Lock()
|
||||||
s.challenges[challengeHex] = challenge
|
s.challenges[challengeHex] = challenge
|
||||||
s.challengeMutex.Unlock()
|
s.challengeMutex.Unlock()
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.Write([]byte(`{"challenge": "` + challengeHex + `"}`))
|
w.Write([]byte(`{"challenge": "` + challengeHex + `"}`))
|
||||||
}
|
}
|
||||||
@@ -152,49 +203,49 @@ func (s *Server) handleAuthLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
// Read the request body
|
// Read the request body
|
||||||
body, err := io.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
if chk.E(err) {
|
if chk.E(err) {
|
||||||
w.Write([]byte(`{"success": false, "error": "Failed to read request body"}`))
|
w.Write([]byte(`{"success": false, "error": "Failed to read request body"}`))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the signed event
|
// Parse the signed event
|
||||||
var evt event.E
|
var evt event.E
|
||||||
if err = json.Unmarshal(body, &evt); chk.E(err) {
|
if err = json.Unmarshal(body, &evt); chk.E(err) {
|
||||||
w.Write([]byte(`{"success": false, "error": "Invalid event format"}`))
|
w.Write([]byte(`{"success": false, "error": "Invalid event format"}`))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the challenge from the event to look up the stored challenge
|
// Extract the challenge from the event to look up the stored challenge
|
||||||
challengeTag := evt.Tags.GetFirst([]byte("challenge"))
|
challengeTag := evt.Tags.GetFirst([]byte("challenge"))
|
||||||
if challengeTag == nil {
|
if challengeTag == nil {
|
||||||
w.Write([]byte(`{"success": false, "error": "Challenge tag missing from event"}`))
|
w.Write([]byte(`{"success": false, "error": "Challenge tag missing from event"}`))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
challengeHex := string(challengeTag.Value())
|
challengeHex := string(challengeTag.Value())
|
||||||
|
|
||||||
// Retrieve the stored challenge
|
// Retrieve the stored challenge
|
||||||
s.challengeMutex.RLock()
|
s.challengeMutex.RLock()
|
||||||
_, exists := s.challenges[challengeHex]
|
_, exists := s.challenges[challengeHex]
|
||||||
s.challengeMutex.RUnlock()
|
s.challengeMutex.RUnlock()
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
w.Write([]byte(`{"success": false, "error": "Invalid or expired challenge"}`))
|
w.Write([]byte(`{"success": false, "error": "Invalid or expired challenge"}`))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up the used challenge
|
// Clean up the used challenge
|
||||||
s.challengeMutex.Lock()
|
s.challengeMutex.Lock()
|
||||||
delete(s.challenges, challengeHex)
|
delete(s.challenges, challengeHex)
|
||||||
s.challengeMutex.Unlock()
|
s.challengeMutex.Unlock()
|
||||||
|
|
||||||
relayURL := s.ServiceURL(r)
|
relayURL := s.ServiceURL(r)
|
||||||
|
|
||||||
// Validate the authentication event with the correct challenge
|
// 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
|
// 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)
|
ok, err := auth.Validate(&evt, []byte(challengeHex), relayURL)
|
||||||
@@ -206,7 +257,7 @@ func (s *Server) handleAuthLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write([]byte(`{"success": false, "error": "` + errorMsg + `"}`))
|
w.Write([]byte(`{"success": false, "error": "` + errorMsg + `"}`))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authentication successful: set a simple session cookie with the pubkey
|
// Authentication successful: set a simple session cookie with the pubkey
|
||||||
cookie := &http.Cookie{
|
cookie := &http.Cookie{
|
||||||
Name: "orly_auth",
|
Name: "orly_auth",
|
||||||
@@ -246,14 +297,16 @@ func (s *Server) handleAuthLogout(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Expire the cookie
|
// Expire the cookie
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(
|
||||||
Name: "orly_auth",
|
w, &http.Cookie{
|
||||||
Value: "",
|
Name: "orly_auth",
|
||||||
Path: "/",
|
Value: "",
|
||||||
MaxAge: -1,
|
Path: "/",
|
||||||
HttpOnly: true,
|
MaxAge: -1,
|
||||||
SameSite: http.SameSiteLaxMode,
|
HttpOnly: true,
|
||||||
})
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
},
|
||||||
|
)
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.Write([]byte(`{"success": true}`))
|
w.Write([]byte(`{"success": true}`))
|
||||||
}
|
}
|
||||||
@@ -264,40 +317,42 @@ func (s *Server) handlePermissions(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract pubkey from URL path
|
// Extract pubkey from URL path
|
||||||
pubkeyHex := strings.TrimPrefix(r.URL.Path, "/api/permissions/")
|
pubkeyHex := strings.TrimPrefix(r.URL.Path, "/api/permissions/")
|
||||||
if pubkeyHex == "" || pubkeyHex == "/" {
|
if pubkeyHex == "" || pubkeyHex == "/" {
|
||||||
http.Error(w, "Invalid pubkey", http.StatusBadRequest)
|
http.Error(w, "Invalid pubkey", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert hex to binary pubkey
|
// Convert hex to binary pubkey
|
||||||
pubkey, err := hex.Dec(pubkeyHex)
|
pubkey, err := hex.Dec(pubkeyHex)
|
||||||
if chk.E(err) {
|
if chk.E(err) {
|
||||||
http.Error(w, "Invalid pubkey format", http.StatusBadRequest)
|
http.Error(w, "Invalid pubkey format", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get access level using acl registry
|
// Get access level using acl registry
|
||||||
permission := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
|
permission := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
|
||||||
|
|
||||||
// Set content type and write JSON response
|
// Set content type and write JSON response
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
// Format response as proper JSON
|
// Format response as proper JSON
|
||||||
response := struct {
|
response := struct {
|
||||||
Permission string `json:"permission"`
|
Permission string `json:"permission"`
|
||||||
}{
|
}{
|
||||||
Permission: permission,
|
Permission: permission,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marshal and write the response
|
// Marshal and write the response
|
||||||
jsonData, err := json.Marshal(response)
|
jsonData, err := json.Marshal(response)
|
||||||
if chk.E(err) {
|
if chk.E(err) {
|
||||||
http.Error(w, "Error generating response", http.StatusInternalServerError)
|
http.Error(
|
||||||
|
w, "Error generating response", http.StatusInternalServerError,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Write(jsonData)
|
w.Write(jsonData)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,41 @@ This is a React web application that uses Bun for building and bundling, and is
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
To run the development server:
|
There are two ways to develop the web app:
|
||||||
|
|
||||||
|
1) Standalone (recommended for hot reload)
|
||||||
|
- Start the Go relay with the embedded web UI disabled so the React app can run on its own dev server with HMR.
|
||||||
|
- Configure the relay via environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In another shell at repo root
|
||||||
|
export ORLY_WEB_DISABLE=true
|
||||||
|
# Optional: if you want same-origin URLs, you can set a proxy target and access the relay on the same port
|
||||||
|
# export ORLY_WEB_DEV_PROXY_URL=http://localhost:5173
|
||||||
|
|
||||||
|
# Start the relay as usual
|
||||||
|
go run .
|
||||||
|
```
|
||||||
|
|
||||||
|
- Then start the React dev server:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd app/web
|
cd app/web
|
||||||
bun install
|
bun install
|
||||||
bun run dev
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
When ORLY_WEB_DISABLE=true is set, the Go server still serves the API and websocket endpoints and sends permissive CORS headers, so the dev server can access them cross-origin. If ORLY_WEB_DEV_PROXY_URL is set, the Go server will reverse-proxy non-/api paths to the dev server so you can use the same origin.
|
||||||
|
|
||||||
|
2) Embedded (no hot reload)
|
||||||
|
- Build the web app and run the Go server with defaults:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd app/web
|
||||||
|
bun install
|
||||||
|
bun run build
|
||||||
|
cd ../../
|
||||||
|
go run .
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --hot src/index.jsx",
|
"dev": "bun --hot --port 5173 public/dev.html",
|
||||||
"build": "bun build ./src/index.jsx --outdir ./dist --minify && mkdir -p dist && cp -r public/* dist/",
|
"build": "rm -rf dist && bun build ./public/index.html --outdir ./dist --minify --splitting && cp -r public/tailwind.min.css dist/",
|
||||||
"start": "bun run dist/index.js"
|
"preview": "bun x serve dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|||||||
13
app/web/public/dev.html
Normal file
13
app/web/public/dev.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Nostr Relay (Dev)</title>
|
||||||
|
<link rel="stylesheet" href="tailwind.min.css" />
|
||||||
|
</head>
|
||||||
|
<body class="bg-white">
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
@@ -8,6 +8,23 @@ function App() {
|
|||||||
|
|
||||||
const [checkingAuth, setCheckingAuth] = useState(true);
|
const [checkingAuth, setCheckingAuth] = useState(true);
|
||||||
|
|
||||||
|
// Login view layout measurements
|
||||||
|
const titleRef = useRef(null);
|
||||||
|
const [loginPadding, setLoginPadding] = useState(16); // default fallback padding in px
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function updatePadding() {
|
||||||
|
if (titleRef.current) {
|
||||||
|
const h = titleRef.current.offsetHeight || 0;
|
||||||
|
// Pad area around the text by half the title text height
|
||||||
|
setLoginPadding(Math.max(0, Math.round(h / 2)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updatePadding();
|
||||||
|
window.addEventListener('resize', updatePadding);
|
||||||
|
return () => window.removeEventListener('resize', updatePadding);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check authentication status on page load
|
// Check authentication status on page load
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -357,22 +374,35 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// Not logged in view - shows the login form
|
// Not logged in view - shows the login form
|
||||||
<div className="max-w-3xl mx-auto mt-5 p-6 bg-gray-100 rounded">
|
<div className="w-full min-h-screen bg-gray-100">
|
||||||
<h1 className="text-2xl font-bold mb-2">Nostr Relay Authentication</h1>
|
<div
|
||||||
<p className="mb-4">Connect to this Nostr relay using your private key or browser extension.</p>
|
className="max-w-full"
|
||||||
|
style={{ padding: `${loginPadding}px` }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
<img
|
||||||
|
src="/orly.png"
|
||||||
|
alt="Orly logo"
|
||||||
|
className="object-contain"
|
||||||
|
style={{ width: '4rem', height: '4rem' }}
|
||||||
|
onError={(e) => {
|
||||||
|
// fallback to repo docs image if public asset missing
|
||||||
|
e.currentTarget.onerror = null;
|
||||||
|
e.currentTarget.src = "/docs/orly.png";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<h1 ref={titleRef} className="text-2xl font-bold p-2">ORLY🦉 Dashboard Login</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={statusClassName()}>
|
<p className="mb-4">Connect to this Nostr relay using your browser extension.</p>
|
||||||
{status}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-5">
|
<div className={statusClassName()}>
|
||||||
<button className="bg-blue-600 text-white px-5 py-3 rounded hover:bg-blue-700" onClick={loginWithExtension}>Login with Browser Extension (NIP-07)</button>
|
{status}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
<label className="block mb-1 font-bold" htmlFor="nsec">Or login with private key (nsec):</label>
|
<button className="bg-blue-600 text-white px-5 py-3 rounded hover:bg-blue-700" onClick={loginWithExtension}>Login with Browser Extension (NIP-07)</button>
|
||||||
<input className="w-full p-2 border border-gray-300 rounded" type="password" id="nsec" placeholder="nsec1..." />
|
</div>
|
||||||
<button className="mt-2 bg-red-600 text-white px-5 py-2 rounded hover:bg-red-700" onClick={() => updateStatus('Private key login not implemented in this basic interface. Please use a proper Nostr client or extension.', 'error')}>Login with Private Key</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
max-width: 800px;
|
margin: 0;
|
||||||
margin: 0 auto;
|
padding: 0;
|
||||||
padding: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
|||||||
Reference in New Issue
Block a user