From d59918deb0b933f159102573c881728d7f44004b Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Mon, 8 Jun 2026 19:27:17 -0400 Subject: [PATCH] cleanup and get nostr-login-lite working with this project --- app.js.old | 343 ------------------------------------ app.js.old2 | 356 ------------------------------------- app_working.js | 454 ------------------------------------------------ views/login.ejs | 125 ++++++++++--- 4 files changed, 101 insertions(+), 1177 deletions(-) delete mode 100644 app.js.old delete mode 100644 app.js.old2 delete mode 100644 app_working.js diff --git a/app.js.old b/app.js.old deleted file mode 100644 index 605d4b8..0000000 --- a/app.js.old +++ /dev/null @@ -1,343 +0,0 @@ -import express from 'express'; -import { Provider } from 'oidc-provider'; -import { nip19, getEventHash, getSignature, verifyEvent } from 'nostr-tools'; -import crypto from 'crypto'; - -const app = express(); -const PORT = 3001; - -// In-memory storage for demo purposes -const users = new Map(); -const sessions = new Map(); -const authCodes = new Map(); - -// Production OIDC configuration -const configuration = { - clients: [{ - client_id: 'gitea', - client_secret: 'gitea-secret', - redirect_uris: ['https://git.laantungir.net/user/oauth2/nostr/callback'], - response_types: ['code'], - grant_types: ['authorization_code'], - scope: 'openid profile email', - }], - claims: { - openid: ['sub'], - profile: ['name', 'preferred_username'], - email: ['email'], - }, - features: { - devInteractions: { enabled: false }, - introspection: { enabled: true }, - revocation: { enabled: true }, - }, - ttl: { - AccessToken: 1 * 60 * 60, // 1 hour - AuthorizationCode: 10 * 60, // 10 minutes - IdToken: 1 * 60 * 60, // 1 hour - DeviceCode: 10 * 60, // 10 minutes - RefreshToken: 1 * 24 * 60 * 60, // 1 day - }, - issueRefreshToken: async (ctx, token, client) => { - return client.grantTypes.includes('refresh_token') && token.scopes.has('offline_access'); - }, - interactions: { - url: async (ctx, interaction) => { - return `/interaction/${interaction.uid}`; - }, - }, - findAccount: async (ctx, sub, token) => { - const user = users.get(sub); - if (!user) return undefined; - - return { - accountId: sub, - claims: async (use, scope) => { - return { - sub: user.pubkey, - name: user.name || user.pubkey.slice(0, 16), - preferred_username: user.name || user.pubkey.slice(0, 16), - email: user.email || `${user.pubkey.slice(0, 16)}@nostr.local`, - }; - }, - }; - }, -}; - -// Initialize OIDC provider with production issuer -const oidc = new Provider('https://auth.laantungir.net', configuration); - -// Custom interaction handling -oidc.use(async (ctx, next) => { - if (ctx.path.startsWith('/interaction/')) { - const uid = ctx.path.split('/')[2]; - const interaction = await oidc.interactionDetails(ctx.req, ctx.res); - - if (ctx.method === 'GET') { - // Show login form - const challenge = crypto.randomBytes(32).toString('hex'); - sessions.set(uid, { challenge, interaction }); - - // Render login page - ctx.type = 'html'; - ctx.body = await renderLoginPage(challenge, uid, interaction); - return; - } - - if (ctx.method === 'POST') { - // Handle login submission - const body = await parseBody(ctx); - const sessionData = sessions.get(uid); - - if (!sessionData) { - ctx.status = 400; - ctx.body = { error: 'Session expired' }; - return; - } - - try { - let nostrEvent; - - if (body.signature && body.pubkey) { - // Manual signing - nostrEvent = { - kind: 1, - pubkey: body.pubkey, - created_at: Math.floor(Date.now() / 1000), - tags: [], - content: `Authenticating to ${sessionData.challenge}`, - id: '', - sig: body.signature - }; - nostrEvent.id = getEventHash(nostrEvent); - } else if (body.event) { - // Extension signing - nostrEvent = JSON.parse(body.event); - } else { - throw new Error('No signature provided'); - } - - // Verify the event - if (!verifyEvent(nostrEvent)) { - throw new Error('Invalid signature'); - } - - // Check if challenge matches - if (!nostrEvent.content.includes(sessionData.challenge)) { - throw new Error('Challenge mismatch'); - } - - // Create or get user - const pubkey = nostrEvent.pubkey; - const npub = nip19.npubEncode(pubkey); - - if (!users.has(pubkey)) { - users.set(pubkey, { - pubkey, - npub, - name: npub.slice(0, 16), - email: `${pubkey.slice(0, 16)}@nostr.local`, - }); - } - - const user = users.get(pubkey); - - // Complete the interaction - const result = { - login: { - account: pubkey, - }, - }; - - await oidc.interactionFinished(ctx.req, ctx.res, result, { - mergeWithLastSubmission: false, - }); - - sessions.delete(uid); - return; - - } catch (error) { - console.error('Authentication error:', error); - ctx.status = 400; - ctx.body = { error: error.message }; - return; - } - } - } - - await next(); -}); - -// Helper function to parse POST body -async function parseBody(ctx) { - return new Promise((resolve, reject) => { - let body = ''; - ctx.req.on('data', chunk => { - body += chunk.toString(); - }); - ctx.req.on('end', () => { - try { - const parsed = new URLSearchParams(body); - const result = {}; - for (const [key, value] of parsed) { - result[key] = value; - } - resolve(result); - } catch (error) { - reject(error); - } - }); - }); -} - -// Helper function to render login page -async function renderLoginPage(challenge, uid, interaction) { - return ` - - - - Nostr OIDC Bridge - - - - - -
-

🔐 Sign in with Nostr

-

To authenticate, please sign the following challenge with your Nostr key:

- -
- Challenge:
- Authenticating to ${challenge} -
- -
-

🔌 Method 1: Browser Extension (Recommended)

-

If you have a Nostr browser extension (nos2x, Alby, etc.), click below:

- -
-
- -
-

🔑 Method 2: Manual Signing

-

If you don't have an extension, you can manually sign the challenge:

- -
- - - - - - - -
-
-
- - - -`; -} - -// Middleware setup -app.use(express.static('public')); -app.use(oidc.callback()); - -app.listen(PORT, '0.0.0.0', () => { - console.log(`🚀 Nostr OIDC Bridge running on http://0.0.0.0:${PORT}`); - console.log(`🔗 Discovery URL: https://auth.laantungir.net/.well-known/openid-configuration`); -}); diff --git a/app.js.old2 b/app.js.old2 deleted file mode 100644 index ff2ec76..0000000 --- a/app.js.old2 +++ /dev/null @@ -1,356 +0,0 @@ -import express from 'express'; -import { Provider } from 'oidc-provider'; -import { nip19, getEventHash, verifyEvent, finalizeEvent, getPublicKey } from 'nostr-tools'; -import crypto from 'crypto'; - -const app = express(); -const PORT = 3001; - -// In-memory storage for demo purposes -const users = new Map(); -const sessions = new Map(); -const authCodes = new Map(); - -// Production OIDC configuration -const configuration = { - clients: [{ - client_id: 'GITEA', - client_secret: 'gitea-secret', - redirect_uris: ['https://git.laantungir.net/user/oauth2/NOSTR/callback'], - response_types: ['code'], - grant_types: ['authorization_code'], - scope: 'openid profile email', - }], - claims: { - openid: ['sub'], - profile: ['name', 'preferred_username'], - email: ['email'], - }, - // Add proxy-related settings - cookies: { - secure: true, - sameSite: 'none' - }, - features: { - devInteractions: { enabled: false }, - introspection: { enabled: true }, - revocation: { enabled: true }, - }, - ttl: { - AccessToken: 1 * 60 * 60, // 1 hour - AuthorizationCode: 10 * 60, // 10 minutes - IdToken: 1 * 60 * 60, // 1 hour - DeviceCode: 10 * 60, // 10 minutes - RefreshToken: 1 * 24 * 60 * 60, // 1 day - }, - issueRefreshToken: async (ctx, token, client) => { - return client.grantTypes.includes('refresh_token') && token.scopes.has('offline_access'); - }, - interactions: { - url: async (ctx, interaction) => { - return `/interaction/${interaction.uid}`; - }, - }, - findAccount: async (ctx, sub, token) => { - const user = users.get(sub); - if (!user) return undefined; - - return { - accountId: sub, - claims: async (use, scope) => { - return { - sub: user.pubkey, - name: user.name || user.pubkey.slice(0, 16), - preferred_username: user.name || user.pubkey.slice(0, 16), - email: user.email || `${user.pubkey.slice(0, 16)}@nostr.local`, - }; - }, - }; - }, -}; - -// Initialize OIDC provider with production issuer -const oidc = new Provider('https://auth.laantungir.net', configuration); - -// Add middleware to handle proxy forwarding -oidc.use(async (ctx, next) => { - ctx.secure = true; // Force secure context - ctx.protocol = 'https'; // Force HTTPS protocol - await next(); -}); - -// Custom interaction handling -oidc.use(async (ctx, next) => { - if (ctx.path.startsWith('/interaction/')) { - const uid = ctx.path.split('/')[2]; - const interaction = await oidc.interactionDetails(ctx.req, ctx.res); - - if (ctx.method === 'GET') { - // Show login form - const challenge = crypto.randomBytes(32).toString('hex'); - sessions.set(uid, { challenge, interaction }); - - // Render login page - ctx.type = 'html'; - ctx.body = await renderLoginPage(challenge, uid, interaction); - return; - } - - if (ctx.method === 'POST') { - // Handle login submission - const body = await parseBody(ctx); - const sessionData = sessions.get(uid); - - if (!sessionData) { - ctx.status = 400; - ctx.body = { error: 'Session expired' }; - return; - } - - try { - let nostrEvent; - - if (body.signature && body.pubkey) { - // Manual signing - nostrEvent = { - kind: 1, - pubkey: body.pubkey, - created_at: Math.floor(Date.now() / 1000), - tags: [], - content: `Authenticating to ${sessionData.challenge}`, - id: '', - sig: body.signature - }; - nostrEvent.id = getEventHash(nostrEvent); - } else if (body.event) { - // Extension signing - nostrEvent = JSON.parse(body.event); - } else { - throw new Error('No signature provided'); - } - - // Verify the event - if (!verifyEvent(nostrEvent)) { - throw new Error('Invalid signature'); - } - - // Check if challenge matches - if (!nostrEvent.content.includes(sessionData.challenge)) { - throw new Error('Challenge mismatch'); - } - - // Create or get user - const pubkey = nostrEvent.pubkey; - const npub = nip19.npubEncode(pubkey); - - if (!users.has(pubkey)) { - users.set(pubkey, { - pubkey, - npub, - name: npub.slice(0, 16), - email: `${pubkey.slice(0, 16)}@nostr.local`, - }); - } - - const user = users.get(pubkey); - - // Complete the interaction - const result = { - login: { - account: pubkey, - }, - }; - - await oidc.interactionFinished(ctx.req, ctx.res, result, { - mergeWithLastSubmission: false, - }); - - sessions.delete(uid); - return; - - } catch (error) { - console.error('Authentication error:', error); - ctx.status = 400; - ctx.body = { error: error.message }; - return; - } - } - } - - await next(); -}); - -// Helper function to parse POST body -async function parseBody(ctx) { - return new Promise((resolve, reject) => { - let body = ''; - ctx.req.on('data', chunk => { - body += chunk.toString(); - }); - ctx.req.on('end', () => { - try { - const parsed = new URLSearchParams(body); - const result = {}; - for (const [key, value] of parsed) { - result[key] = value; - } - resolve(result); - } catch (error) { - reject(error); - } - }); - }); -} - -// Helper function to render login page -async function renderLoginPage(challenge, uid, interaction) { - return ` - - - - Nostr OIDC Bridge - - - - - -
-

🔐 Sign in with Nostr

-

To authenticate, please sign the following challenge with your Nostr key:

- -
- Challenge:
- Authenticating to ${challenge} -
- -
-

🔌 Method 1: Browser Extension (Recommended)

-

If you have a Nostr browser extension (nos2x, Alby, etc.), click below:

- -
-
- -
-

🔑 Method 2: Manual Signing

-

If you don't have an extension, you can manually sign the challenge:

- -
- - - - - - - -
-
-
- - - -`; -} - -// Middleware setup -app.use(express.static('public')); -app.set('trust proxy', true); // Trust proxy headers -app.use(oidc.callback()); - -app.listen(PORT, '0.0.0.0', () => { - console.log(`🚀 Nostr OIDC Bridge running on http://0.0.0.0:${PORT}`); - console.log(`🔗 Discovery URL: https://auth.laantungir.net/.well-known/openid-configuration`); -}); diff --git a/app_working.js b/app_working.js deleted file mode 100644 index 28a410e..0000000 --- a/app_working.js +++ /dev/null @@ -1,454 +0,0 @@ -import express from 'express'; -import crypto from 'crypto'; -import jwt from 'jsonwebtoken'; -import { nip19, getEventHash, verifyEvent, SimplePool } from 'nostr-tools'; - -const app = express(); -const PORT = 3001; - -// In-memory storage for demo purposes -const users = new Map(); -const authCodes = new Map(); -const sessions = new Map(); - -// JWT signing key (in production, use a proper key) -const JWT_SECRET = 'your-super-secret-jwt-signing-key-change-in-production'; -const ISSUER = 'https://auth.laantungir.net'; // Production HTTPS URL -const CLIENT_ID = 'GITEA'; -const CLIENT_SECRET = 'gitea-secret'; - -// Nostr relays for fetching user metadata -const NOSTR_RELAYS = [ - 'wss://relay.damus.io', - 'wss://nos.lol', - 'wss://relay.nostr.band', - 'wss://nostr-pub.wellorder.net' -]; - -// Initialize Nostr pool -const pool = new SimplePool(); - -// NIP-05 related functions -async function fetchUserMetadata(pubkey) { - try { - console.log('🔍 Fetching metadata for pubkey:', pubkey); - - const filter = { - kinds: [0], - authors: [pubkey], - limit: 1 - }; - - const events = await pool.querySync(NOSTR_RELAYS, filter); - - if (events.length > 0) { - const metadata = JSON.parse(events[0].content); - console.log('✅ Found metadata:', metadata); - return metadata; - } - - console.log('❌ No metadata found'); - return null; - } catch (error) { - console.error('❌ Error fetching metadata:', error); - return null; - } -} - -async function validateNip05(nip05, pubkey) { - try { - console.log('🔍 Validating NIP-05:', nip05); - - // Parse the NIP-05 identifier - let localPart, domain; - if (nip05.includes('@')) { - [localPart, domain] = nip05.split('@'); - } else { - // Handle _@domain.com format (root identifier) - localPart = '_'; - domain = nip05; - } - - // Fetch the well-known endpoint - const url = `https://${domain}/.well-known/nostr.json?name=${localPart}`; - console.log('🔍 Fetching well-known:', url); - - const response = await fetch(url, { - timeout: 5000, - headers: { - 'User-Agent': 'Nostr-OIDC-Bridge/1.0' - } - }); - - if (!response.ok) { - console.log('❌ Well-known fetch failed:', response.status); - return false; - } - - const data = await response.json(); - console.log('✅ Well-known response:', data); - - // Check if the pubkey matches - const expectedPubkey = data.names?.[localPart]; - const isValid = expectedPubkey === pubkey; - - console.log(`${isValid ? '✅' : '❌'} NIP-05 validation:`, { - expected: expectedPubkey, - actual: pubkey, - valid: isValid - }); - - return isValid; - } catch (error) { - console.error('❌ Error validating NIP-05:', error); - return false; - } -} - -function generateUsernameAndEmail(nip05, npub, pubkey) { - let username, email; - - if (nip05) { - if (nip05.includes('@')) { - // For user@domain.com format, use local part as username - const [local, domain] = nip05.split('@'); - - // Use local part as username (truncate if needed) - username = local.length <= 40 ? local : local.substring(0, 40); - - // Use full NIP-05 as email - email = nip05; - } else { - // For root identifier (domain.com), use domain as username - username = nip05.length <= 40 ? nip05 : nip05.substring(0, 40); - email = `_@${nip05}`; // Convert to standard email format - } - } else { - // Fallback to shortened npub - username = npub.length <= 40 ? npub.substring(0, 40) : npub.substring(0, 40); - email = `${pubkey.substring(0, 8)}@nostr.local`; - } - - return { username, email }; -} - -// Trust proxy for production deployment behind nginx -app.set('trust proxy', true); - -// Middleware -app.use(express.urlencoded({ extended: true })); -app.use(express.json()); -app.set('view engine', 'ejs'); -app.set('views', './views'); - -// Debug logging middleware -app.use((req, res, next) => { - console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`); - if (req.query && Object.keys(req.query).length > 0) { - console.log('Query params:', req.query); - } - next(); -}); - -// OIDC Discovery endpoint - manually controlled for production -app.get('/.well-known/openid-configuration', (req, res) => { - res.json({ - issuer: ISSUER, - authorization_endpoint: `${ISSUER}/auth`, - token_endpoint: `${ISSUER}/token`, - userinfo_endpoint: `${ISSUER}/userinfo`, - jwks_uri: `${ISSUER}/jwks`, - scopes_supported: ['openid', 'profile', 'email'], - response_types_supported: ['code'], - grant_types_supported: ['authorization_code'], - subject_types_supported: ['public'], - id_token_signing_alg_values_supported: ['HS256'], - claims_supported: ['sub', 'name', 'preferred_username', 'email'], - }); -}); - -// Authorization endpoint -app.get('/auth', (req, res) => { - const { client_id, redirect_uri, response_type, scope, state } = req.query; - - console.log('🔵 Authorization request:', { client_id, redirect_uri, response_type, scope, state }); - - // Validate client - if (client_id !== CLIENT_ID) { - const errorUrl = `${redirect_uri}?error=invalid_client&state=${state}`; - console.log('❌ Invalid client, redirecting to:', errorUrl); - return res.redirect(errorUrl); - } - - // Generate session - const sessionId = crypto.randomBytes(32).toString('hex'); - const challenge = crypto.randomBytes(32).toString('hex'); - - sessions.set(sessionId, { - client_id, - redirect_uri, - scope, - state, - challenge, - created_at: Date.now() - }); - - console.log('✅ Created session:', sessionId); - - // Render login page - res.render('login', { - challenge, - uid: sessionId, - returnTo: `/complete-auth/${sessionId}` - }); -}); - -// Handle Nostr authentication -app.post('/complete-auth/:sessionId', async (req, res) => { - try { - const { sessionId } = req.params; - const { npub, event_json } = req.body; - - const session = sessions.get(sessionId); - if (!session) { - throw new Error('Session not found or expired'); - } - - // Verify the Nostr event - const event = JSON.parse(event_json); - - if (!verifyEvent(event)) { - throw new Error('Invalid signature'); - } - - if (event.content !== session.challenge) { - throw new Error('Challenge mismatch'); - } - - const { data: pubkey } = nip19.decode(npub); - if (event.pubkey !== pubkey) { - throw new Error('Public key mismatch'); - } - - console.log('✅ Nostr authentication successful for:', pubkey); - - // Create or get user - let user = users.get(pubkey); - if (!user) { - // Try to fetch NIP-05 information - let nip05 = null; - let validatedNip05 = false; - - try { - console.log('🔍 Attempting to fetch NIP-05 for user...'); - const metadata = await fetchUserMetadata(pubkey); - - if (metadata && metadata.nip05) { - console.log('🔍 Found NIP-05 in metadata:', metadata.nip05); - validatedNip05 = await validateNip05(metadata.nip05, pubkey); - - if (validatedNip05) { - nip05 = metadata.nip05; - console.log('✅ NIP-05 validated successfully:', nip05); - } else { - console.log('❌ NIP-05 validation failed'); - } - } else { - console.log('❌ No NIP-05 found in metadata'); - } - } catch (error) { - console.error('❌ Error during NIP-05 lookup:', error); - } - - // Generate the best username and email - const { username, email } = generateUsernameAndEmail(validatedNip05 ? nip05 : null, npub, pubkey); - - user = { - id: pubkey, - name: nip05 && validatedNip05 ? nip05 : npub, - username: username, - email: email, - nip05: validatedNip05 ? nip05 : null, - }; - users.set(pubkey, user); - console.log('✅ Created new user:', user); - } - - // Generate authorization code - const authCode = crypto.randomBytes(32).toString('hex'); - authCodes.set(authCode, { - user_id: pubkey, - client_id: session.client_id, - redirect_uri: session.redirect_uri, - scope: session.scope, - created_at: Date.now(), - expires_at: Date.now() + (10 * 60 * 1000) // 10 minutes - }); - - console.log('✅ Generated auth code:', authCode); - - // Clean up session - sessions.delete(sessionId); - - // Redirect back to Gitea with auth code - const redirectUrl = `${session.redirect_uri}?code=${authCode}&state=${session.state}`; - console.log('✅ Redirecting to Gitea:', redirectUrl); - - res.redirect(redirectUrl); - - } catch (error) { - console.error('❌ Authentication error:', error); - const session = sessions.get(req.params.sessionId); - const challenge = session ? session.challenge : crypto.randomBytes(32).toString('hex'); - - res.render('login', { - error: error.message, - challenge, - uid: req.params.sessionId, - returnTo: `/complete-auth/${req.params.sessionId}` - }); - } -}); - -// Token endpoint -app.post('/token', (req, res) => { - try { - const { grant_type, code, client_id, client_secret, redirect_uri } = req.body; - - console.log('🔵 Token request:', { grant_type, code, client_id, redirect_uri }); - - // Validate grant type - if (grant_type !== 'authorization_code') { - return res.status(400).json({ error: 'unsupported_grant_type' }); - } - - // Validate client - if (client_id !== CLIENT_ID || client_secret !== CLIENT_SECRET) { - return res.status(401).json({ error: 'invalid_client' }); - } - - // Get auth code - const authData = authCodes.get(code); - if (!authData) { - return res.status(400).json({ error: 'invalid_grant' }); - } - - // Check expiration - if (Date.now() > authData.expires_at) { - authCodes.delete(code); - return res.status(400).json({ error: 'invalid_grant' }); - } - - // Validate redirect URI - if (authData.redirect_uri !== redirect_uri) { - return res.status(400).json({ error: 'invalid_grant' }); - } - - const user = users.get(authData.user_id); - if (!user) { - return res.status(400).json({ error: 'invalid_grant' }); - } - - // Generate tokens - const now = Math.floor(Date.now() / 1000); - - const accessToken = jwt.sign({ - sub: user.id, - aud: client_id, - iss: ISSUER, - iat: now, - exp: now + 3600, // 1 hour - scope: authData.scope - }, JWT_SECRET); - - const idToken = jwt.sign({ - sub: user.id, - aud: client_id, - iss: ISSUER, - iat: now, - exp: now + 3600, - name: user.name, - preferred_username: user.username, - email: user.email, - }, JWT_SECRET); - - // Clean up auth code (one-time use) - authCodes.delete(code); - - console.log('✅ Issued tokens for user:', user.id); - - res.json({ - access_token: accessToken, - token_type: 'Bearer', - expires_in: 3600, - id_token: idToken, - scope: authData.scope - }); - - } catch (error) { - console.error('❌ Token error:', error); - res.status(500).json({ error: 'server_error' }); - } -}); - -// Userinfo endpoint function -const handleUserInfo = (req, res) => { - try { - const authHeader = req.headers.authorization; - if (!authHeader || !authHeader.startsWith('Bearer ')) { - return res.status(401).json({ error: 'invalid_token' }); - } - - const token = authHeader.substring(7); - const decoded = jwt.verify(token, JWT_SECRET); - - const user = users.get(decoded.sub); - if (!user) { - return res.status(401).json({ error: 'invalid_token' }); - } - - console.log('✅ UserInfo request for user:', user.id); - - res.json({ - sub: user.id, - name: user.name, - preferred_username: user.username, - email: user.email, - }); - - } catch (error) { - console.error('❌ Userinfo error:', error); - res.status(401).json({ error: 'invalid_token' }); - } -}; - -// Userinfo endpoints (both /userinfo and /me for compatibility) -app.get('/userinfo', handleUserInfo); -app.get('/me', handleUserInfo); - -// JWKS endpoint (simplified) -app.get('/jwks', (req, res) => { - // This is a simplified implementation - // In production, use proper JWK format - res.json({ keys: [] }); -}); - -// Health check -app.get('/health', (req, res) => { - res.json({ status: 'ok', timestamp: new Date().toISOString() }); -}); - -// Error handler -app.use((err, req, res, next) => { - console.error('❌ Express Error:', err); - if (res.headersSent) { - return next(err); - } - res.status(500).json({ error: 'Internal server error', message: err.message }); -}); - -// Start server -app.listen(PORT, '0.0.0.0', () => { - console.log(`🚀 Simple Nostr OIDC Bridge running on http://0.0.0.0:${PORT}`); - console.log(`🔗 OIDC Discovery: ${ISSUER}/.well-known/openid-configuration`); -}); diff --git a/views/login.ejs b/views/login.ejs index aa88621..2a7c970 100644 --- a/views/login.ejs +++ b/views/login.ejs @@ -26,19 +26,22 @@ <% } %> - +
- +
+ +
+ - - - + + + +