Player accounts (auth slice 1): register/login, JWT issuance, isolation
GUPI-issued local auth (path A): osint.users (handle + scrypt password_hash + display_name + avatar_url; external_id reserved for a later glitch.university key-exchange). POST /api/auth/register|login set the auth_token cookie via a new signPlayerToken (sub=user id, role=player); /logout and /me added. Because playthroughs already bind to resolveUserId, two players each "Begin" and stay fully isolated. Verification stays issuer-agnostic for the external swap later. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+33
-1
@@ -8,7 +8,8 @@ import { fileURLToPath } from 'node:url'
|
||||
import multer from 'multer'
|
||||
import pg from 'pg'
|
||||
import type { CaseState } from '../src/types.js'
|
||||
import { authenticateJwt, createDevelopmentAdminToken, hasAdminClaim, requireAdmin, resolvePlayerName, resolveUserId } from './auth.js'
|
||||
import { authenticateJwt, createDevelopmentAdminToken, hasAdminClaim, requireAdmin, resolvePlayerName, resolveUserId, signPlayerToken } from './auth.js'
|
||||
import { createUserRepository } from './userRepository.js'
|
||||
import { submitCaseReport } from './caseReports.js'
|
||||
import { createLevelRepository } from './levelRepository.js'
|
||||
import { createEvidenceJudgeFromEnv } from './evidenceJudge.js'
|
||||
@@ -33,6 +34,8 @@ const evidenceJudge = createEvidenceJudgeFromEnv()
|
||||
const levels = createLevelRepository(pool, editingEnabled, objectStorage, evidenceJudge)
|
||||
const narrative = createNarrativeRepository(pool, objectStorage)
|
||||
const storyGraph = createStoryGraphRepository(pool)
|
||||
const users = createUserRepository(pool)
|
||||
const AUTH_COOKIE = { httpOnly: true, sameSite: 'lax' as const, path: '/', maxAge: 30 * 24 * 60 * 60 * 1000 }
|
||||
const STORY_NODE_TYPES: StoryNodeType[] = ['cutscene', 'dialogue', 'level', 'det_gate', 'llm_gate', 'merit', 'phone']
|
||||
|
||||
function wantsEdit(req: express.Request) {
|
||||
@@ -59,6 +62,35 @@ app.get('/api/health', async (_req, res) => {
|
||||
catch { res.status(503).json({ ok: false, database: 'unavailable' }) }
|
||||
})
|
||||
app.get('/api/session', (req, res) => res.json({ authenticated: Boolean(req.authClaims), isAdmin: hasAdminClaim(req), playerName:resolvePlayerName(req) }))
|
||||
|
||||
// Player accounts (path A: GUPI issues the token). register/login set the auth_token
|
||||
// cookie; every game write then binds to this user via resolveUserId.
|
||||
app.post('/api/auth/register', async (req, res, next) => {
|
||||
try {
|
||||
const body = req.body || {}
|
||||
const result = await users.registerUser({ handle: String(body.handle || ''), password: String(body.password || ''), displayName: String(body.displayName || '') })
|
||||
if (result.error || !result.user) return res.status(result.error === 'That handle is taken' ? 409 : 400).json({ error: result.error || 'Registration failed' })
|
||||
res.cookie('auth_token', signPlayerToken(result.user), AUTH_COOKIE)
|
||||
res.status(201).json({ user: result.user })
|
||||
} catch (error) { next(error) }
|
||||
})
|
||||
app.post('/api/auth/login', async (req, res, next) => {
|
||||
try {
|
||||
const body = req.body || {}
|
||||
const user = await users.authenticateUser(String(body.handle || ''), String(body.password || ''))
|
||||
if (!user) return res.status(401).json({ error: 'Invalid handle or password' })
|
||||
res.cookie('auth_token', signPlayerToken(user), AUTH_COOKIE)
|
||||
res.json({ user })
|
||||
} catch (error) { next(error) }
|
||||
})
|
||||
app.post('/api/auth/logout', (_req, res) => { res.clearCookie('auth_token', { path: '/' }); res.json({ ok: true }) })
|
||||
app.get('/api/auth/me', async (req, res, next) => {
|
||||
try {
|
||||
const sub = req.authClaims?.sub
|
||||
const user = typeof sub === 'string' ? await users.getUser(sub) : null
|
||||
user ? res.json({ user }) : res.status(204).end()
|
||||
} catch (error) { next(error) }
|
||||
})
|
||||
if (process.env.NODE_ENV !== 'production') app.get('/api/dev/admin-session', (req, res) => {
|
||||
const requestedReturn = String(req.query.returnTo || '/')
|
||||
const returnTo = requestedReturn.startsWith('/') && !requestedReturn.startsWith('//') ? requestedReturn : '/'
|
||||
|
||||
Reference in New Issue
Block a user