seed-demo.js (9742B)
1 import crypto from 'node:crypto'; 2 import { loadConfig } from '../src/lib/config.js'; 3 import { hashPassword } from '../src/lib/password.js'; 4 import { hashClientSecret } from '../src/lib/oidc/clients.js'; 5 import { 6 addClientRealm, 7 addOidcClientHost, 8 addRealmEmailDomain, 9 closeRealms, 10 createRealm, 11 findClientRealm, 12 findRealmBySlug, 13 getSystemDatabase, 14 initRealms, 15 openRealmDatabase, 16 } from '../src/lib/realm.js'; 17 import { 18 addUserEmail, 19 assignHomeRealmForEmail, 20 assignRole, 21 createUser, 22 ensureMembership, 23 findUserByEmail, 24 findUserByUsername, 25 setPasswordCredential, 26 setSystemAdmin, 27 } from '../src/lib/users.js'; 28 29 const DEMO_PASSWORD = 'demo'; 30 31 async function ensureRealm({ slug, displayName, hosts, branding, allowSessionDurationChoice = true }) { 32 let realm = await findRealmBySlug(slug); 33 if (!realm) { 34 realm = await createRealm({ 35 slug, 36 displayName, 37 hosts, 38 branding, 39 allowSessionDurationChoice, 40 }); 41 } 42 return realm; 43 } 44 45 async function ensureRealmDomain(realmId, domain) { 46 const systemDb = getSystemDatabase(); 47 const existing = await systemDb.get( 48 'SELECT realm_id FROM realm_email_domains WHERE domain = {domain}', 49 { domain } 50 ); 51 if (existing) return; 52 await addRealmEmailDomain(realmId, domain, { verified: true }); 53 } 54 55 async function ensureRealmUser(realmSlug, userId, roles = []) { 56 const realm = await findRealmBySlug(realmSlug); 57 if (!realm) throw new Error(`unknown realm: ${realmSlug}`); 58 const db = await openRealmDatabase(realm); 59 await ensureMembership(db, userId, 'active'); 60 for (const role of roles) { 61 await assignRole(db, userId, role); 62 } 63 } 64 65 async function ensureUser({ 66 username, 67 email, 68 displayName, 69 password = DEMO_PASSWORD, 70 isSystemAdmin = false, 71 }) { 72 const systemDb = getSystemDatabase(); 73 const existing = 74 (await findUserByEmail(systemDb, email)) || (await findUserByUsername(systemDb, username)); 75 if (existing) { 76 await setPasswordCredential(systemDb, existing.id, await hashPassword(password)); 77 if (isSystemAdmin) await setSystemAdmin(systemDb, existing.id, true); 78 return existing.id; 79 } 80 81 const id = await createUser(systemDb, { username, displayName, isSystemAdmin }); 82 await addUserEmail(systemDb, id, email, { verified: true, primary: true }); 83 await assignHomeRealmForEmail(systemDb, id, email); 84 await setPasswordCredential(systemDb, id, await hashPassword(password)); 85 return id; 86 } 87 88 async function ensureProvider(db, { slug, displayName, issuer, clientId, clientSecret }) { 89 const existing = await db.get('SELECT id FROM identity_providers WHERE slug = {slug}', { slug }); 90 if (existing) return existing.id; 91 const now = Date.now(); 92 const id = crypto.randomUUID(); 93 await db.run( 94 `INSERT INTO identity_providers 95 (id, slug, display_name, issuer, client_id, client_secret, scopes, enabled, created_at, updated_at) 96 VALUES 97 ({id}, {slug}, {displayName}, {issuer}, {clientId}, {clientSecret}, 'openid email profile', 1, {createdAt}, {updatedAt})`, 98 { 99 id, 100 slug, 101 displayName, 102 issuer, 103 clientId, 104 clientSecret: clientSecret || null, 105 createdAt: now, 106 updatedAt: now, 107 } 108 ); 109 return id; 110 } 111 112 async function ensureProviderDomain(db, domain, providerId) { 113 const existing = await db.get('SELECT domain FROM email_domain_providers WHERE domain = {domain}', { 114 domain, 115 }); 116 if (existing) return; 117 await db.run( 118 'INSERT INTO email_domain_providers (domain, provider_id, created_at) VALUES ({domain}, {providerId}, {createdAt})', 119 { domain, providerId, createdAt: Date.now() } 120 ); 121 } 122 123 async function ensureClient( 124 realm, 125 db, 126 { 127 clientId, 128 name, 129 accessMode, 130 roles = [], 131 redirectUris = [], 132 postLogoutRedirectUris = [], 133 tokenEndpointAuthMethod = 'none', 134 clientSecret = null, 135 branding = {}, 136 requirePkce = true, 137 } 138 ) { 139 const mapping = await findClientRealm(clientId); 140 if (mapping && mapping.realm_id !== realm.id) { 141 throw new Error(`client id is already assigned: ${clientId}`); 142 } 143 144 let client = await db.get('SELECT id FROM oidc_clients WHERE client_id = {clientId}', { clientId }); 145 if (!client) { 146 const now = Date.now(); 147 client = { id: crypto.randomUUID() }; 148 await db.run( 149 `INSERT INTO oidc_clients 150 (id, client_id, name, redirect_uris, scopes, access_mode, enabled, branding, 151 client_secret_hash, token_endpoint_auth_method, require_pkce, 152 post_logout_redirect_uris, created_at, updated_at) 153 VALUES 154 ({id}, {clientId}, {name}, {redirectUris}, 'openid profile email', {accessMode}, 1, {branding}, 155 {clientSecretHash}, {tokenEndpointAuthMethod}, {requirePkce}, 156 {postLogoutRedirectUris}, {createdAt}, {updatedAt})`, 157 { 158 id: client.id, 159 clientId, 160 name, 161 redirectUris: JSON.stringify(redirectUris), 162 accessMode, 163 branding: JSON.stringify(branding), 164 clientSecretHash: clientSecret ? hashClientSecret(clientSecret) : null, 165 tokenEndpointAuthMethod, 166 requirePkce: requirePkce ? 1 : 0, 167 postLogoutRedirectUris: JSON.stringify(postLogoutRedirectUris), 168 createdAt: now, 169 updatedAt: now, 170 } 171 ); 172 } 173 174 await addClientRealm(clientId, realm.id); 175 176 for (const slug of roles) { 177 const role = await db.get('SELECT id FROM roles WHERE slug = {slug}', { slug }); 178 if (!role) continue; 179 const existing = await db.get( 180 'SELECT client_id FROM oidc_client_roles WHERE client_id = {clientId} AND role_id = {roleId}', 181 { clientId: client.id, roleId: role.id } 182 ); 183 if (existing) continue; 184 await db.run( 185 'INSERT INTO oidc_client_roles (client_id, role_id) VALUES ({clientId}, {roleId})', 186 { clientId: client.id, roleId: role.id } 187 ); 188 } 189 } 190 191 const cfg = loadConfig(); 192 const publicUrl = new URL(cfg.server.public_url); 193 const portSuffix = publicUrl.port ? `:${publicUrl.port}` : ''; 194 const clientOrigin = (host) => `${publicUrl.protocol}//${host}${portSuffix}`; 195 196 await initRealms(cfg); 197 198 const acme = await ensureRealm({ 199 slug: 'acme', 200 displayName: 'Acme', 201 hosts: ['acme.localhost'], 202 branding: { primary_color: '#25A' }, 203 }); 204 const globex = await ensureRealm({ 205 slug: 'globex', 206 displayName: 'Globex', 207 hosts: ['globex.localhost'], 208 branding: { primary_color: '#25A' }, 209 }); 210 211 await ensureRealmDomain(acme.id, 'acme.test'); 212 await ensureRealmDomain(acme.id, 'sso.acme.test'); 213 await ensureRealmDomain(globex.id, 'globex.test'); 214 215 const alice = await ensureUser({ 216 username: 'alice', 217 email: 'alice@acme.test', 218 displayName: 'Alice', 219 }); 220 const acmeAdmin = await ensureUser({ 221 username: 'acme-it', 222 email: 'it@acme.test', 223 displayName: 'Acme IT', 224 }); 225 const guest = await ensureUser({ 226 username: 'guest', 227 email: 'guest@personal.test', 228 displayName: 'Guest User', 229 }); 230 const contractor = await ensureUser({ 231 username: 'contractor', 232 email: 'contractor@personal.test', 233 displayName: 'Contractor', 234 }); 235 const systemAdmin = await ensureUser({ 236 username: 'admin', 237 email: 'admin@system.test', 238 displayName: 'System Admin', 239 isSystemAdmin: true, 240 }); 241 242 await ensureRealmUser('acme', alice); 243 await ensureRealmUser('globex', alice); 244 await ensureRealmUser('acme', acmeAdmin, ['realm-admin']); 245 await ensureRealmUser('acme', guest); 246 await ensureRealmUser('globex', contractor, ['realm-admin']); 247 248 const acmeDb = await openRealmDatabase(acme); 249 const providerId = await ensureProvider(acmeDb, { 250 slug: 'example-sso', 251 displayName: 'Example SSO', 252 issuer: 'https://idp.example.com', 253 clientId: 'demo', 254 clientSecret: 'demo-secret', 255 }); 256 await ensureProviderDomain(acmeDb, 'sso.acme.test', providerId); 257 258 await ensureClient(acme, acmeDb, { 259 clientId: 'internal-app', 260 name: 'Internal App', 261 accessMode: 'internal', 262 redirectUris: [`${clientOrigin('app.acme.localhost')}/callback`], 263 postLogoutRedirectUris: [`${clientOrigin('app.acme.localhost')}/`], 264 }); 265 await ensureClient(acme, acmeDb, { 266 clientId: 'acme-members-app', 267 name: 'Acme Members App', 268 accessMode: 'members', 269 redirectUris: [`${clientOrigin('members.localhost')}/callback`], 270 }); 271 await ensureClient(acme, acmeDb, { 272 clientId: 'acme-public-app', 273 name: 'Acme Public App', 274 accessMode: 'public', 275 redirectUris: [`${clientOrigin('public.localhost')}/callback`], 276 }); 277 await ensureClient(acme, acmeDb, { 278 clientId: 'confidential-app', 279 name: 'Confidential App', 280 accessMode: 'members', 281 redirectUris: [`${clientOrigin('confidential.localhost')}/callback`], 282 tokenEndpointAuthMethod: 'client_secret_basic', 283 clientSecret: 'demo-secret', 284 }); 285 286 const globexDb = await openRealmDatabase(globex); 287 await ensureClient(globex, globexDb, { 288 clientId: 'globex-members-app', 289 name: 'Globex Members App', 290 accessMode: 'members', 291 redirectUris: [`${clientOrigin('members.localhost')}/callback`], 292 }); 293 await ensureClient(globex, globexDb, { 294 clientId: 'globex-public-app', 295 name: 'Globex Public App', 296 accessMode: 'public', 297 redirectUris: [`${clientOrigin('public.localhost')}/callback`], 298 }); 299 300 await addOidcClientHost('internal-app', 'app.acme.localhost'); 301 302 console.log('seeded realms acme and globex'); 303 console.log('password for all demo users: demo'); 304 console.log('home users: alice@acme.test, it@acme.test, admin@system.test (system admin)'); 305 console.log('guest users: guest@personal.test (acme), contractor@personal.test (globex realm-admin)'); 306 console.log('upstream mapping: sso.acme.test -> Example SSO'); 307 console.log( 308 'clients: internal-app, acme-members-app, acme-public-app, confidential-app, ' + 309 'globex-members-app, globex-public-app' 310 ); 311 console.log('client host: app.acme.localhost -> acme/internal-app'); 312 console.log('virtual clients: admin, account'); 313 314 await closeRealms();