Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
36fcb07
Update docker compose with external ports
digitalbase Jan 6, 2026
fce4987
Update docker compose with external ports
digitalbase Jan 6, 2026
f18d43e
Merge remote-tracking branch 'origin/main'
digitalbase Jan 10, 2026
2e5efc6
Add basic-auth package
digitalbase Jan 10, 2026
764d25a
Pnpm lockfile with basic auth
digitalbase Jan 10, 2026
36a6476
Run express server secured + add message to log when running in auth …
digitalbase Jan 10, 2026
f8283ab
Update .env example
digitalbase Jan 10, 2026
f841fcc
Update docker compose
digitalbase Jan 10, 2026
3018e9b
Update docker build command
digitalbase Jan 10, 2026
626fef6
Bring ports to standard
digitalbase Jan 10, 2026
ee08753
Add api token to config and types
digitalbase Jan 10, 2026
55c4ddc
Pass api token to daemon
digitalbase Jan 10, 2026
6c51650
Extract API token from config to pass it from UI
digitalbase Jan 10, 2026
bd7ea4d
Extend daemon middleware to also accept apiToken
digitalbase Jan 10, 2026
2cebac2
Update HttpServerConfig type
digitalbase Jan 10, 2026
28e660a
Fix the hover background color of 'generate key'
digitalbase Jan 10, 2026
09a6a0e
Add dotenv dependency to add support for .env file
digitalbase Jan 10, 2026
04211ea
Load .env in daemon and UI
digitalbase Jan 10, 2026
44ddfc9
Drop authHost from project
digitalbase Jan 10, 2026
7461eb6
Drop authPort from project
digitalbase Jan 10, 2026
35f06c4
Update .env.example
digitalbase Jan 10, 2026
8e4b2ee
Improve naming from _HOST to _ADDRESS
digitalbase Jan 10, 2026
c33ab53
Rename port to BIND_PORT too as we need an external port too
digitalbase Jan 10, 2026
e7245ec
Rename DAEMON_URL to SIGNET_URL
digitalbase Jan 10, 2026
191e633
Drop support for env.BASE_URL
digitalbase Jan 10, 2026
f7975ef
Replace EXTERNAL_URL with UI_URL
digitalbase Jan 10, 2026
a5c1ee5
Align docker-compose vars + update healthcheck to use bind variables
digitalbase Jan 10, 2026
bdee3dd
Load dotenv dynamically. And do not fail in production
digitalbase Jan 10, 2026
820d9b3
Use require as top-level await requires ES2022 or newer
digitalbase Jan 10, 2026
231f3be
Add signet_URL to UI (for proxying)
digitalbase Jan 10, 2026
e5dcfca
Replace config api token by env variable + update docs
digitalbase Jan 10, 2026
33a1bf6
Update env example
digitalbase Jan 10, 2026
aa9c6ec
Introduce proxy authentication middleware to not bypass existing JWT/…
digitalbase Jan 10, 2026
27e9fe5
Add express rate limiter to protect Basic-Auth from bruce-force attempts
digitalbase Jan 10, 2026
44c72e3
Update lockfile
digitalbase Jan 10, 2026
ed4eff5
Update author. You should also update LICENSE
digitalbase Jan 10, 2026
e7e6048
Update docker compose with external ports
digitalbase Jan 6, 2026
9413d6d
Add basic-auth package
digitalbase Jan 10, 2026
bce703f
Pnpm lockfile with basic auth
digitalbase Jan 10, 2026
bb3f094
Run express server secured + add message to log when running in auth …
digitalbase Jan 10, 2026
884d596
Update .env example
digitalbase Jan 10, 2026
97d4d24
Update docker compose
digitalbase Jan 10, 2026
19555bf
Update docker build command
digitalbase Jan 10, 2026
9194263
Bring ports to standard
digitalbase Jan 10, 2026
1c0637d
Add api token to config and types
digitalbase Jan 10, 2026
ac5eea5
Pass api token to daemon
digitalbase Jan 10, 2026
3a24b47
Extract API token from config to pass it from UI
digitalbase Jan 10, 2026
abb2b05
Extend daemon middleware to also accept apiToken
digitalbase Jan 10, 2026
1b005ef
Update HttpServerConfig type
digitalbase Jan 10, 2026
2a2c1a0
Fix the hover background color of 'generate key'
digitalbase Jan 10, 2026
8391b38
Add dotenv dependency to add support for .env file
digitalbase Jan 10, 2026
bce88dd
Load .env in daemon and UI
digitalbase Jan 10, 2026
d58e8d8
Drop authHost from project
digitalbase Jan 10, 2026
5dec61e
Drop authPort from project
digitalbase Jan 10, 2026
f21c256
Update .env.example
digitalbase Jan 10, 2026
dc2bcee
Improve naming from _HOST to _ADDRESS
digitalbase Jan 10, 2026
a170a7a
Rename port to BIND_PORT too as we need an external port too
digitalbase Jan 10, 2026
eb0ebdc
Rename DAEMON_URL to SIGNET_URL
digitalbase Jan 10, 2026
a05ca32
Drop support for env.BASE_URL
digitalbase Jan 10, 2026
b2d25da
Replace EXTERNAL_URL with UI_URL
digitalbase Jan 10, 2026
78574c7
Align docker-compose vars + update healthcheck to use bind variables
digitalbase Jan 10, 2026
d043cb4
Load dotenv dynamically. And do not fail in production
digitalbase Jan 10, 2026
a986944
Use require as top-level await requires ES2022 or newer
digitalbase Jan 10, 2026
f5a59e2
Add signet_URL to UI (for proxying)
digitalbase Jan 10, 2026
86ee4fc
Replace config api token by env variable + update docs
digitalbase Jan 10, 2026
da81357
Update env example
digitalbase Jan 10, 2026
19b9119
Introduce proxy authentication middleware to not bypass existing JWT/…
digitalbase Jan 10, 2026
4a1fcef
Add express rate limiter to protect Basic-Auth from bruce-force attempts
digitalbase Jan 10, 2026
ab1c44c
Update lockfile
digitalbase Jan 10, 2026
732bca5
Update startWebAuth
digitalbase Jan 12, 2026
bcf462f
Merge remote-tracking branch 'origin/feature/ui-authentication' into …
digitalbase Jan 12, 2026
5ee59d3
Disable authRateLimited (it's kicking in)
digitalbase Jan 12, 2026
6dee14e
Merge branch 'Letdown2491:main' into main
digitalbase Feb 12, 2026
f834e45
Merge remote-tracking branch 'origin/main' into feature/ui-authentica…
digitalbase Feb 12, 2026
700e669
Fix conflicts + repair auth in dev server
digitalbase Feb 12, 2026
fd2d8a3
Fix hard coded sqlite dir
digitalbase Feb 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,35 @@
# Optional environment overrides for docker compose
# Copy to .env only if you need to change the defaults

# API port (default: 3000)
AUTH_PORT=3000
# Bind addresses (which network interface to listen on)
# Signet Daemon port (default: 3174)
SIGNET_BIND_PORT=3174
# Daemon bind address (default: all interfaces)
# Examples: 0.0.0.0 (all interfaces), 127.0.0.1 (localhost only), 100.x.x.x (Tailscale)
SIGNET_BIND_ADDRESS=0.0.0.0

# UI port (default: 4174)
UI_PORT=4174
UI_BIND_PORT=4174
# UI bind address (default: all interfaces)
# Examples: 0.0.0.0 (all interfaces), 127.0.0.1 (localhost only), 100.x.x.x (Tailscale)
UI_BIND_ADDRESS=0.0.0.0

# External addresses (if you want to expose parts of signet)
# Signet Url (default: BIND_PORT:BIND_ADDRESS)
# Where daemon is accessible (default: http://signet:3000 in Docker)
# SIGNET_URL=

# UI Host (default: localhost)
# Where UI is accessible (default: http://UI_BIND_PORT:UI_BIND_ADDRESS)
#UI_URL=localhost


# UI Basic Authentication (disabled by default)
# Set both values to enable authentication
# UI_AUTH_USERNAME=admin
# UI_AUTH_PASSWORD=your_secure_password

# API Token for UI-to-daemon communication
# Generate a secure token with: openssl rand -hex 32
# Both the daemon and UI need this token for secure proxying
SIGNET_API_TOKEN=
8 changes: 4 additions & 4 deletions apps/signet-ui/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ ENV NODE_ENV=production
COPY --from=build /app/apps/signet-ui/dist ./dist
COPY --from=build /app/apps/signet-ui/server.mjs ./server.mjs
# Install server dependencies (pinned to match package.json)
RUN npm install --no-save express@4 http-proxy-middleware@3
RUN npm install --no-save express@4 express-rate-limit@7 http-proxy-middleware@3 basic-auth@2

# Environment variables (can be overridden at runtime)
ENV UI_PORT=4174
ENV UI_HOST=0.0.0.0
ENV DAEMON_URL=http://signet:3000
ENV UI_BIND_PORT=4174
ENV UI_BIND_ADDRESS=0.0.0.0
ENV SIGNET_URL=http://signet:3000

EXPOSE 4174
CMD ["node", "server.mjs"]
2 changes: 2 additions & 0 deletions apps/signet-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
},
"dependencies": {
"@signet/types": "workspace:*",
"basic-auth": "^2.0.1",
"debug": "^4.3.4",
"dotenv": "^16.6.1",
"express": "^4.22.1",
"focus-trap-react": "^11.0.6",
"html5-qrcode": "^2.3.8",
Expand Down
80 changes: 74 additions & 6 deletions apps/signet-ui/server.mjs
Original file line number Diff line number Diff line change
@@ -1,17 +1,58 @@
import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';
import rateLimit from 'express-rate-limit';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import auth from 'basic-auth';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Load .env from repository root (two levels up) in development
// In production (NODE_ENV=production), dotenv may not be installed
if (process.env.NODE_ENV !== 'production') {
try {
const { config } = await import('dotenv');
config({ path: path.resolve(__dirname, '../../.env') });
} catch {
// dotenv not available, skip .env loading (production mode)
}
}

const app = express();

// Support both new (UI_*) and legacy (PORT/HOST) env var names
const port = Number.parseInt(process.env.UI_PORT ?? process.env.PORT ?? '4174', 10);
const host = process.env.UI_HOST ?? process.env.HOST ?? '0.0.0.0';
const daemonUrl = process.env.DAEMON_URL ?? 'http://localhost:3000';
const port = Number.parseInt(process.env.UI_BIND_PORT ?? process.env.PORT ?? '4174', 10);
const host = process.env.UI_BIND_ADDRESS ?? '0.0.0.0';
const signetHost = process.env.SIGNET_HOST ?? 'localhost';
const signetPort = process.env.SIGNET_PORT ?? '3000';
const signetUrl = process.env.SIGNET_URL ?? `http://${signetHost}:${signetPort}`;

// Basic auth configuration (disabled by default)
const authUsername = process.env.UI_AUTH_USERNAME;
const authPassword = process.env.UI_AUTH_PASSWORD;
const isAuthEnabled = authUsername && authPassword;

// Rate limiting for basic auth failures (prevent brute force)
// Allows 10 failed attempts per 15 minutes per IP
const authRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // 10 attempts
skipSuccessfulRequests: true, // Only count failed auth attempts
standardHeaders: true, // Return rate limit info in `RateLimit-*` headers
legacyHeaders: false, // Disable `X-RateLimit-*` headers
handler: (req, res) => {
res.status(429).send('Too many failed authentication attempts. Please try again later.');
},
});

// Load API token from environment variable
const apiToken = process.env.SIGNET_API_TOKEN;
if (apiToken) {
console.log('✓ Using API token from SIGNET_API_TOKEN environment variable');
} else {
console.warn('⚠️ No SIGNET_API_TOKEN set - requests to daemon may fail if authentication is required');
}

// Shared error handler for proxies
const onProxyError = (err, req, res) => {
Expand Down Expand Up @@ -43,7 +84,7 @@ const apiPaths = [

// SSE proxy for /events endpoint (no timeout, streaming)
const sseProxy = createProxyMiddleware({
target: daemonUrl,
target: signetUrl,
changeOrigin: true,
proxyTimeout: 0,
timeout: 0,
Expand All @@ -53,6 +94,9 @@ const sseProxy = createProxyMiddleware({
proxyReq.setHeader('Accept', 'text/event-stream');
proxyReq.setHeader('Cache-Control', 'no-cache');
proxyReq.setHeader('Connection', 'keep-alive');
if (apiToken) {
proxyReq.setHeader('X-API-Token', apiToken);
}
},
proxyRes(proxyRes) {
proxyRes.headers['x-accel-buffering'] = 'no';
Expand All @@ -64,15 +108,38 @@ const sseProxy = createProxyMiddleware({

// API proxy for standard endpoints
const apiProxy = createProxyMiddleware({
target: daemonUrl,
target: signetUrl,
changeOrigin: true,
proxyTimeout: 10_000,
pathFilter: apiPaths,
on: {
proxyReq(proxyReq) {
if (apiToken) {
proxyReq.setHeader('X-API-Token', apiToken);
}
},
error: onProxyError
}
});

// Basic authentication middleware with rate limiting
if (isAuthEnabled) {
// Apply rate limiter first
//app.use(authRateLimiter);

// Then check credentials
app.use((req, res, next) => {
const credentials = auth(req);

if (!credentials || credentials.name !== authUsername || credentials.pass !== authPassword) {
res.set('WWW-Authenticate', 'Basic realm="Signet UI"');
return res.status(401).send('Authentication required');
}

next();
});
}

// Mount proxies at root - pathFilter handles routing
app.use(sseProxy);
app.use(apiProxy);
Expand All @@ -87,5 +154,6 @@ app.get('*', (_req, res) => {
});

app.listen(port, host, () => {
console.log(`Signet UI listening on http://${host}:${port} (proxying ${daemonUrl})`);
const authStatus = isAuthEnabled ? ' [Basic Auth Enabled]' : '';
console.log(`Signet UI listening on http://${host}:${port} (proxying ${signetUrl})${authStatus}`);
});
1 change: 1 addition & 0 deletions apps/signet-ui/src/design-system.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

/* Semantic colors */
--success: #22c55e;
--success-hover: #4cd67d;
--success-muted: rgba(34, 197, 94, 0.15);
--success-border: rgba(34, 197, 94, 0.3);

Expand Down
38 changes: 36 additions & 2 deletions apps/signet-ui/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,46 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import { defineConfig, type Plugin } from 'vite';
import react from '@vitejs/plugin-react';
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { config as dotenvConfig } from 'dotenv';

// Load .env from repository root (two levels up)
dotenvConfig({ path: resolve(__dirname, '../../.env') });

const packageJson = JSON.parse(readFileSync('./package.json', 'utf-8'));

function basicAuthPlugin(): Plugin {
return {
name: 'basic-auth',
configureServer(server) {
const username = process.env.UI_AUTH_USERNAME;
const password = process.env.UI_AUTH_PASSWORD;
if (!username || !password) return;

console.log('Basic Auth enabled for dev server');

server.middlewares.use((req, res, next) => {
const header = req.headers.authorization;
if (header) {
const match = header.match(/^Basic\s+(.+)$/);
if (match) {
const [user, pass] = Buffer.from(match[1], 'base64').toString().split(':');
if (user === username && pass === password) {
return next();
}
}
}
res.setHeader('WWW-Authenticate', 'Basic realm="Signet UI"');
res.statusCode = 401;
res.end('Authentication required');
});
},
};
}

export default defineConfig({
plugins: [react()],
plugins: [basicAuthPlugin(), react()],
define: {
__APP_VERSION__: JSON.stringify(packageJson.version),
},
Expand Down
6 changes: 3 additions & 3 deletions apps/signet/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ ENV CI=true
RUN pnpm install --frozen-lockfile

# Ensure better-sqlite3 native bindings are installed (prebuild-install downloads prebuilt binaries)
RUN cd /app/node_modules/.pnpm/better-sqlite3@12.5.0/node_modules/better-sqlite3 && \
RUN cd /app/node_modules/.pnpm/better-sqlite3@*/node_modules/better-sqlite3 && \
npx --yes prebuild-install -d

# Copy source files
Expand Down Expand Up @@ -60,8 +60,8 @@ WORKDIR /app/apps/signet

# Environment variables (can be overridden at runtime)
ENV DATABASE_URL="file:/app/config/signet.db"
ENV SIGNET_PORT=3000
ENV SIGNET_HOST=0.0.0.0
ENV SIGNET_BIND_PORT=3000
ENV SIGNET_BIND_ADDRESS=0.0.0.0

EXPOSE 3000

Expand Down
2 changes: 1 addition & 1 deletion apps/signet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@
"handlebars": "^4.7.8",
"isomorphic-ws": "^5.0.0",
"nostr-tools": "^2.22.1",
"prisma": "^7.3.0",
"qrcode": "^1.5.4",
"websocket-polyfill": "^0.0.3",
"ws": "^8.19.0",
Expand All @@ -68,6 +67,7 @@
"@types/node": "^20.19.0",
"@types/qrcode": "^1.5.6",
"@vitest/coverage-v8": "^4.0.18",
"prisma": "^7.4.0",
"ts-node": "^10.9.2",
"tsup": "^8.5.1",
"typescript": "^5.9.3",
Expand Down
2 changes: 1 addition & 1 deletion apps/signet/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export async function loadConfig(configPath: string): Promise<ConfigFile> {
authPort: 3000,
authHost: '127.0.0.1',
baseUrl: 'http://localhost:4174',
requireAuth: false,
requireAuth: true,
};
needsSave = true;
} else {
Expand Down
4 changes: 2 additions & 2 deletions apps/signet/src/daemon/authorize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ async function resolveBaseUrl(connectionManager: ConnectionManager): Promise<str
}

const config = await connectionManager.config();
// Support both new (EXTERNAL_URL) and legacy (BASE_URL) env var names
const baseUrl = config.baseUrl ?? process.env.EXTERNAL_URL ?? process.env.BASE_URL ?? null;
// Support both new (UI_URL) and legacy (BASE_URL) env var names
const baseUrl = config.baseUrl ?? process.env.UI_URL ?? null;
cachedBaseUrl = baseUrl;
return baseUrl;
}
Expand Down
Loading