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>
106 lines
3.9 KiB
JavaScript
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 */
|
|
}
|
|
})
|