Files
website/scripts/dev/seed-sso-provider.js
wtclaude 70849f96ee chore(dev): add stub OAuth IdP + seed + bridge smoketest for native SSO
Dev environments have no real OAuth provider configured, so GET /auth/providers
returns [] and the native mobile SSO flow cannot be exercised locally. Add
dependency-free dev tooling under scripts/dev/:

- stub-idp.js: stub OAuth2/OIDC IdP (authorize picker, token, userinfo)
- seed-sso-provider.js: registers a 'devstub' auth_providers row + pre-links
  each principal's sub to a dev account (SSO is link-only)
- sso-bridge-smoketest.js: drives the full app flow headless (PKCE → start →
  IdP → callback → deep link → exchange) and asserts a bearer pair
- README.md: host + emulator usage

Verified end-to-end against the local site: player and admin principals both
sign in and receive the correct role. DEV ONLY — never deploy the stub.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 14:56:43 -05:00

106 lines
3.9 KiB
JavaScript

#!/usr/bin/env node
/*
* Seed a dev SSO provider + pre-linked identities — DEV ONLY.
*
* Registers an `auth_providers` row pointing at the local `stub-idp.js`, so
* `GET /auth/providers` returns a provider and the native mobile SSO flow becomes
* exercisable. Because SSO is link-only (identities are never auto-provisioned),
* it also pre-links each stub principal's `sub` to an existing dev account.
*
* Idempotent: re-running upserts the provider and skips already-linked identities.
*
* Run (loads website/server/.env for DB creds):
* node website/scripts/dev/seed-sso-provider.js
*
* Env overrides:
* STUB_PROVIDER_ID provider slug (default 'devstub')
* STUB_IDP_PUBLIC_URL browser-facing base (default http://127.0.0.1:9099)
* STUB_IDP_INTERNAL_URL server-facing base (default = STUB_IDP_PUBLIC_URL)
*
* The authorize URL is followed by the browser (Custom Tab); token/userinfo are
* called server-side by the website. On an emulator set PUBLIC to the host's
* reachable address (e.g. http://10.0.2.2:9099) and INTERNAL to http://127.0.0.1:9099.
*/
'use strict'
const fs = require('fs')
const path = require('path')
// Load website/server/.env into process.env WITHOUT the dotenv dependency (it
// lives in server/node_modules and wouldn't resolve from this scripts/ location).
// The file is simple KEY=value; that is all we need for the DB credentials.
function loadEnv(envPath) {
if (!fs.existsSync(envPath)) return
for (const line of fs.readFileSync(envPath, 'utf8').split(/\r?\n/)) {
const m = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line)
if (!m) continue
const key = m[1]
let val = m[2].trim()
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
val = val.slice(1, -1)
}
if (process.env[key] === undefined) process.env[key] = val
}
}
loadEnv(path.join(__dirname, '..', '..', 'server', '.env'))
const authProviders = require('../../server/src/model/authProviders/authProviders.model')
const userIdentities = require('../../server/src/model/userIdentities/userIdentities.model')
const { close } = require('../../server/src/utils/db')
const PROVIDER_ID = process.env.STUB_PROVIDER_ID || 'devstub'
const PUBLIC_URL = (process.env.STUB_IDP_PUBLIC_URL || 'http://127.0.0.1:9099').replace(/\/$/, '')
const INTERNAL_URL = (process.env.STUB_IDP_INTERNAL_URL || PUBLIC_URL).replace(/\/$/, '')
// sub → dev account id. Keep the subs in sync with stub-idp.js PRINCIPALS.
const PRINCIPALS = [
{ sub: 'stub-admin', email: 'admin@dev.local', userId: 1 },
{ sub: 'stub-colby', email: 'colby@dev.local', userId: 14 },
]
async function main() {
// eslint-disable-next-line no-console
const log = (...a) => console.log('[seed-sso]', ...a)
await authProviders.save(PROVIDER_ID, {
kind: 'oauth2',
name: 'Dev Stub IdP',
enabled: true,
clientId: 'stub-client',
secret: 'stub-secret',
authorizeUrl: `${PUBLIC_URL}/authorize`,
tokenUrl: `${INTERNAL_URL}/token`,
userinfoUrl: `${INTERNAL_URL}/userinfo`,
scopes: 'openid email profile',
priority: 50,
})
log(`provider '${PROVIDER_ID}' upserted (authorize=${PUBLIC_URL}/authorize, token/userinfo=${INTERNAL_URL})`)
for (const p of PRINCIPALS) {
const existing = await userIdentities.findByProviderSubject(PROVIDER_ID, p.sub)
if (existing) {
log(`identity ${PROVIDER_ID}:${p.sub} already linked to user ${existing.user_id} — skip`)
continue
}
await userIdentities.link({ userId: p.userId, provider: PROVIDER_ID, subject: p.sub, email: p.email })
log(`linked ${PROVIDER_ID}:${p.sub} → user ${p.userId}`)
}
log('done.')
}
main()
.catch((err) => {
// eslint-disable-next-line no-console
console.error('[seed-sso] FAILED', err)
process.exitCode = 1
})
.finally(async () => {
try {
await close()
} catch {
/* ignore shutdown errors */
}
})