feat: move EncryptID to auth.rspace.online, rebrand as rStack Identity
Traefik routes auth.rspace.online (priority 150) with encryptid.jeffemmett.com fallback. Landing page rebranded as rStack Identity with rStack.online ecosystem tagline. Registration form now includes optional email for account recovery. JWT issuer and recovery URL updated. 14 r* apps listed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fa80968b7f
commit
e65cfffefd
|
|
@ -21,12 +21,13 @@ services:
|
|||
- SMTP_USER=${SMTP_USER:-noreply@jeffemmett.com}
|
||||
- SMTP_PASS=${SMTP_PASS}
|
||||
- SMTP_FROM=${SMTP_FROM:-EncryptID <noreply@jeffemmett.com>}
|
||||
- RECOVERY_URL=${RECOVERY_URL:-https://encryptid.jeffemmett.com/recover}
|
||||
- RECOVERY_URL=${RECOVERY_URL:-https://auth.rspace.online/recover}
|
||||
labels:
|
||||
# Traefik auto-discovery
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.encryptid.rule=Host(`encryptid.jeffemmett.com`)"
|
||||
- "traefik.http.routers.encryptid.rule=Host(`auth.rspace.online`) || Host(`encryptid.jeffemmett.com`)"
|
||||
- "traefik.http.routers.encryptid.entrypoints=web"
|
||||
- "traefik.http.routers.encryptid.priority=150"
|
||||
- "traefik.http.services.encryptid.loadbalancer.server.port=3000"
|
||||
# Also serve from root domain for .well-known (WebAuthn Related Origins)
|
||||
- "traefik.http.routers.encryptid-wellknown.rule=Host(`jeffemmett.com`) && PathPrefix(`/.well-known/webauthn`)"
|
||||
|
|
|
|||
|
|
@ -64,8 +64,9 @@ const CONFIG = {
|
|||
pass: process.env.SMTP_PASS || '',
|
||||
from: process.env.SMTP_FROM || 'EncryptID <noreply@jeffemmett.com>',
|
||||
},
|
||||
recoveryUrl: process.env.RECOVERY_URL || 'https://encryptid.jeffemmett.com/recover',
|
||||
recoveryUrl: process.env.RECOVERY_URL || 'https://auth.rspace.online/recover',
|
||||
allowedOrigins: [
|
||||
'https://auth.rspace.online',
|
||||
'https://encryptid.jeffemmett.com',
|
||||
'https://jeffemmett.com',
|
||||
'https://rspace.online',
|
||||
|
|
@ -132,11 +133,11 @@ async function sendRecoveryEmail(to: string, token: string, username: string): P
|
|||
await smtpTransport.sendMail({
|
||||
from: CONFIG.smtp.from,
|
||||
to,
|
||||
subject: 'EncryptID — Account Recovery',
|
||||
subject: 'rStack — Account Recovery',
|
||||
text: [
|
||||
`Hi ${username},`,
|
||||
'',
|
||||
'A recovery request was made for your EncryptID account.',
|
||||
'A recovery request was made for your rStack account.',
|
||||
'Use the link below to add a new passkey:',
|
||||
'',
|
||||
recoveryLink,
|
||||
|
|
@ -144,7 +145,7 @@ async function sendRecoveryEmail(to: string, token: string, username: string): P
|
|||
'This link expires in 30 minutes.',
|
||||
'If you did not request this, you can safely ignore this email.',
|
||||
'',
|
||||
'— EncryptID',
|
||||
'— rStack Identity',
|
||||
].join('\n'),
|
||||
html: `
|
||||
<!DOCTYPE html>
|
||||
|
|
@ -156,11 +157,11 @@ async function sendRecoveryEmail(to: string, token: string, username: string): P
|
|||
<table width="480" cellpadding="0" cellspacing="0" style="background:#16213e;border-radius:12px;border:1px solid rgba(255,255,255,0.1);">
|
||||
<tr><td style="padding:32px 32px 24px;text-align:center;">
|
||||
<div style="font-size:36px;margin-bottom:8px;">🔒</div>
|
||||
<h1 style="margin:0;font-size:24px;background:linear-gradient(90deg,#00d4ff,#7c3aed);-webkit-background-clip:text;-webkit-text-fill-color:transparent;">EncryptID</h1>
|
||||
<h1 style="margin:0;font-size:24px;background:linear-gradient(90deg,#00d4ff,#7c3aed);-webkit-background-clip:text;-webkit-text-fill-color:transparent;">rStack Identity</h1>
|
||||
</td></tr>
|
||||
<tr><td style="padding:0 32px 24px;color:#e2e8f0;font-size:15px;line-height:1.6;">
|
||||
<p>Hi <strong>${username}</strong>,</p>
|
||||
<p>A recovery request was made for your EncryptID account. Click below to add a new passkey:</p>
|
||||
<p>A recovery request was made for your rStack account. Click below to add a new passkey:</p>
|
||||
</td></tr>
|
||||
<tr><td style="padding:0 32px 32px;text-align:center;">
|
||||
<a href="${recoveryLink}" style="display:inline-block;padding:12px 32px;background:linear-gradient(90deg,#00d4ff,#7c3aed);color:#fff;text-decoration:none;border-radius:8px;font-weight:600;font-size:15px;">Recover Account</a>
|
||||
|
|
@ -281,7 +282,7 @@ app.post('/api/register/start', async (c) => {
|
|||
* Complete registration - verify and store credential
|
||||
*/
|
||||
app.post('/api/register/complete', async (c) => {
|
||||
const { challenge, credential, userId, username } = await c.req.json();
|
||||
const { challenge, credential, userId, username, email } = await c.req.json();
|
||||
|
||||
// Verify challenge
|
||||
const challengeRecord = await getChallenge(challenge);
|
||||
|
|
@ -301,6 +302,11 @@ app.post('/api/register/complete', async (c) => {
|
|||
const did = `did:key:${userId.slice(0, 32)}`;
|
||||
await createUser(userId, username, username, did);
|
||||
|
||||
// Set recovery email if provided during registration
|
||||
if (email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
await setUserEmail(userId, email);
|
||||
}
|
||||
|
||||
const storedCredential: StoredCredential = {
|
||||
credentialId: credential.credentialId,
|
||||
publicKey: credential.publicKey,
|
||||
|
|
@ -643,7 +649,7 @@ async function generateSessionToken(userId: string, username: string): Promise<s
|
|||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const payload = {
|
||||
iss: 'https://encryptid.jeffemmett.com',
|
||||
iss: 'https://auth.rspace.online',
|
||||
sub: userId,
|
||||
aud: CONFIG.allowedOrigins,
|
||||
iat: now,
|
||||
|
|
@ -674,7 +680,7 @@ app.get('/recover', (c) => {
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>EncryptID — Account Recovery</title>
|
||||
<title>rStack Identity — Account Recovery</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
|
|
@ -724,7 +730,7 @@ app.get('/recover', (c) => {
|
|||
<div class="card">
|
||||
<div class="logo">🔒</div>
|
||||
<h1>Account Recovery</h1>
|
||||
<p class="subtitle">Verify your recovery link and add a new passkey</p>
|
||||
<p class="subtitle">Verify your recovery link and add a new passkey to your rStack account</p>
|
||||
|
||||
<div id="status" class="status loading">Verifying recovery token...</div>
|
||||
|
||||
|
|
@ -967,7 +973,7 @@ app.get('/', (c) => {
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>EncryptID - Unified Identity for the r-Ecosystem</title>
|
||||
<title>rStack Identity — One Passkey for the r* Ecosystem</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
|
|
@ -1082,8 +1088,8 @@ app.get('/', (c) => {
|
|||
<body>
|
||||
<div class="page">
|
||||
<div class="header">
|
||||
<h1>EncryptID</h1>
|
||||
<p class="tagline">Unified Identity for the r-Ecosystem</p>
|
||||
<h1>rStack Identity</h1>
|
||||
<p class="tagline">One passkey for the <a href="https://rstack.online" style="color:#00d4ff;text-decoration:none">rStack.online</a> ecosystem</p>
|
||||
</div>
|
||||
|
||||
<!-- Auth card: login/register when signed out, profile when signed in -->
|
||||
|
|
@ -1091,7 +1097,7 @@ app.get('/', (c) => {
|
|||
<!-- Login/Register form -->
|
||||
<div id="auth-form">
|
||||
<h2>Get started</h2>
|
||||
<p class="sub">Sign in with your passkey or create a new account. No passwords, no tracking.</p>
|
||||
<p class="sub">Sign in with your passkey or create a new account. No passwords, no tracking, no third parties.</p>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab active" id="tab-signin" onclick="switchTab('signin')">Sign In</button>
|
||||
|
|
@ -1107,6 +1113,10 @@ app.get('/', (c) => {
|
|||
<label for="username">Username</label>
|
||||
<input id="username" type="text" placeholder="Choose a username" autocomplete="username" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="reg-email">Email <span style="color:#475569;font-weight:400">(optional — for account recovery)</span></label>
|
||||
<input id="reg-email" type="email" placeholder="you@example.com" autocomplete="email" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn-primary" id="auth-btn" onclick="handleAuth()">Sign In with Passkey</button>
|
||||
|
|
@ -1153,8 +1163,8 @@ app.get('/', (c) => {
|
|||
<div class="feature-desc">Hardware-backed, phishing-resistant login. No passwords ever.</div>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature-title">Social Recovery</div>
|
||||
<div class="feature-desc">Recover your account via email. No seed phrases needed.</div>
|
||||
<div class="feature-title">Email Recovery</div>
|
||||
<div class="feature-desc">Optional email for account recovery. No seed phrases needed.</div>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature-title">E2E Encryption</div>
|
||||
|
|
@ -1162,22 +1172,26 @@ app.get('/', (c) => {
|
|||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature-title">Cross-App Identity</div>
|
||||
<div class="feature-desc">One passkey works across every app in the r-suite.</div>
|
||||
<div class="feature-desc">One passkey works across every app in the rStack ecosystem.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="apps">
|
||||
<div class="apps-title">One identity across the r-Ecosystem</div>
|
||||
<div class="apps-title">One identity across the <a href="https://rstack.online" style="color:#64748b;text-decoration:none">rStack.online</a> ecosystem</div>
|
||||
<div class="app-links">
|
||||
<a href="https://rspace.online">rSpace</a>
|
||||
<a href="https://rnotes.online">rNotes</a>
|
||||
<a href="https://rfiles.online">rFiles</a>
|
||||
<a href="https://rcart.online">rCart</a>
|
||||
<a href="https://rfunds.online">rFunds</a>
|
||||
<a href="https://rwallet.online">rWallet</a>
|
||||
<a href="https://rauctions.online">rAuctions</a>
|
||||
<a href="https://rpubs.online">rPubs</a>
|
||||
<a href="https://rvote.online">rVote</a>
|
||||
<a href="https://rmaps.online">rMaps</a>
|
||||
<a href="https://rtrips.online">rTrips</a>
|
||||
<a href="https://rtube.online">rTube</a>
|
||||
<a href="https://rinbox.online">rInbox</a>
|
||||
<a href="https://rstack.online">rStack</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1228,20 +1242,23 @@ app.get('/', (c) => {
|
|||
if (currentMode === 'register') {
|
||||
const username = document.getElementById('username').value.trim();
|
||||
if (!username) { showError('Username is required'); btn.disabled = false; return; }
|
||||
const email = document.getElementById('reg-email').value.trim();
|
||||
|
||||
btn.textContent = 'Creating passkey...';
|
||||
const credential = await registerPasskey(username, username);
|
||||
|
||||
// Complete registration with server
|
||||
const regBody = {
|
||||
challenge: credential.challenge,
|
||||
credential: credential.credential,
|
||||
userId: credential.userId,
|
||||
username,
|
||||
};
|
||||
if (email) regBody.email = email;
|
||||
const res = await fetch('/api/register/complete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
challenge: credential.challenge,
|
||||
credential: credential.credential,
|
||||
userId: credential.userId,
|
||||
username,
|
||||
}),
|
||||
body: JSON.stringify(regBody),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'Registration failed');
|
||||
|
|
@ -1386,9 +1403,9 @@ setInterval(() => {
|
|||
console.log(`
|
||||
╔═══════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ 🔐 EncryptID Server ║
|
||||
║ 🔐 rStack Identity (EncryptID) ║
|
||||
║ ║
|
||||
║ Unified Identity for the r-Ecosystem ║
|
||||
║ One passkey for the rStack.online ecosystem ║
|
||||
║ ║
|
||||
║ Port: ${CONFIG.port} ║
|
||||
║ RP ID: ${CONFIG.rpId} ║
|
||||
|
|
|
|||
Loading…
Reference in New Issue