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:
2025-09-21 10:29:17 +01:00
parent 6f71b95734
commit 24b742bd20
7 changed files with 205 additions and 75 deletions

View File

@@ -42,6 +42,10 @@ type C struct {
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"`
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

View File

@@ -4,7 +4,10 @@ import (
"context"
"encoding/json"
"io"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strconv"
"strings"
"sync"
@@ -28,6 +31,9 @@ type Server struct {
Admins [][]byte
*database.D
// optional reverse proxy for dev web server
devProxy *httputil.ReverseProxy
// Challenge storage for HTTP UI authentication
challengeMutex sync.RWMutex
challenges map[string][]byte
@@ -37,7 +43,9 @@ 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")
w.Header().Set(
"Access-Control-Allow-Headers", "Content-Type, Authorization",
)
// Handle preflight OPTIONS requests
if r.Method == "OPTIONS" {
@@ -45,23 +53,30 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
// log.T.C(
// func() string {
// return fmt.Sprintf("path %v header %v", r.URL, r.Header)
// },
// )
// If this is a websocket request, only intercept the relay root path.
// This allows other websocket paths (e.g., Vite HMR) to be handled by the dev proxy when enabled.
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 {
if s.mux != nil && s.Config != nil && s.Config.WebDisableEmbedded && s.Config.WebDevProxyURL != "" && r.URL.Path != "/" {
// forward to mux (which will proxy to dev server)
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) {
host := req.Header.Get("X-Forwarded-Host")
if host == "" {
@@ -99,6 +114,32 @@ func (s *Server) UserInterface() {
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
if s.challenges == nil {
s.challengeMutex.Lock()
@@ -106,7 +147,7 @@ func (s *Server) UserInterface() {
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)
// API endpoints for authentication
@@ -119,10 +160,20 @@ func (s *Server) UserInterface() {
// 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
// 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())
// Serve the React app files
fileServer.ServeHTTP(w, r)
}
@@ -246,14 +297,16 @@ func (s *Server) handleAuthLogout(w http.ResponseWriter, r *http.Request) {
return
}
// Expire the cookie
http.SetCookie(w, &http.Cookie{
Name: "orly_auth",
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
http.SetCookie(
w, &http.Cookie{
Name: "orly_auth",
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
},
)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"success": true}`))
}
@@ -295,7 +348,9 @@ func (s *Server) handlePermissions(w http.ResponseWriter, r *http.Request) {
// Marshal and write the response
jsonData, err := json.Marshal(response)
if chk.E(err) {
http.Error(w, "Error generating response", http.StatusInternalServerError)
http.Error(
w, "Error generating response", http.StatusInternalServerError,
)
return
}

View File

@@ -9,12 +9,41 @@ This is a React web application that uses Bun for building and bundling, and is
## 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
cd app/web
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

View File

@@ -4,9 +4,9 @@
"private": true,
"type": "module",
"scripts": {
"dev": "bun run --hot src/index.jsx",
"build": "bun build ./src/index.jsx --outdir ./dist --minify && mkdir -p dist && cp -r public/* dist/",
"start": "bun run dist/index.js"
"dev": "bun --hot --port 5173 public/dev.html",
"build": "rm -rf dist && bun build ./public/index.html --outdir ./dist --minify --splitting && cp -r public/tailwind.min.css dist/",
"preview": "bun x serve dist"
},
"dependencies": {
"react": "^18.2.0",

13
app/web/public/dev.html Normal file
View 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>

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
function App() {
const [user, setUser] = useState(null);
@@ -8,6 +8,23 @@ function App() {
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(() => {
// Check authentication status on page load
(async () => {
@@ -357,22 +374,35 @@ function App() {
</div>
) : (
// Not logged in view - shows the login form
<div className="max-w-3xl mx-auto mt-5 p-6 bg-gray-100 rounded">
<h1 className="text-2xl font-bold mb-2">Nostr Relay Authentication</h1>
<p className="mb-4">Connect to this Nostr relay using your private key or browser extension.</p>
<div className="w-full min-h-screen bg-gray-100">
<div
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()}>
{status}
</div>
<p className="mb-4">Connect to this Nostr relay using your browser extension.</p>
<div className="mb-5">
<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>
</div>
<div className={statusClassName()}>
{status}
</div>
<div className="mb-5">
<label className="block mb-1 font-bold" htmlFor="nsec">Or login with private key (nsec):</label>
<input className="w-full p-2 border border-gray-300 rounded" type="password" id="nsec" placeholder="nsec1..." />
<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 className="mb-5">
<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>
</div>
</div>
</div>
)}

View File

@@ -1,8 +1,7 @@
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
margin: 0;
padding: 0;
}
.container {