mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 22:51:30 -07:00
more routes
This commit is contained in:
@@ -3,12 +3,12 @@
|
||||
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
data TEXT NOT NULL,
|
||||
account_id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.AccountId')) VIRTUAL,
|
||||
username_lower TEXT GENERATED ALWAYS AS (lower(json_extract(data, '$.Username'))) VIRTUAL
|
||||
account_id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.accountId')) VIRTUAL,
|
||||
username_lower TEXT GENERATED ALWAYS AS (lower(json_extract(data, '$.username'))) VIRTUAL
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_account_id ON accounts (account_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_username_lower ON accounts (username_lower);
|
||||
|
||||
-- Seed the system account (uid 0) and Coach (uid 1).
|
||||
INSERT OR IGNORE INTO accounts (data) VALUES ('{"AccountId":0,"Username":"RecRoom","DisplayName":"Rec Room","ProfileImage":"DefaultProfileImage.jpg","IsJunior":false,"Platforms":0,"PersonalPronouns":0,"IdentityFlags":0,"CreatedAt":"2016-01-01T00:00:00Z"}');
|
||||
INSERT OR IGNORE INTO accounts (data) VALUES ('{"AccountId":1,"Username":"Coach","DisplayName":"Coach","ProfileImage":"DefaultProfileImage.jpg","IsJunior":false,"Platforms":0,"PersonalPronouns":0,"IdentityFlags":0,"CreatedAt":"2016-01-01T00:00:00Z"}');
|
||||
INSERT OR IGNORE INTO accounts (data) VALUES ('{"accountId":0,"username":"RecRoom","displayName":"Rec Room","profileImage":"DefaultProfileImage.jpg","isJunior":false,"platforms":0,"personalPronouns":0,"identityFlags":0,"createdAt":"2016-01-01T00:00:00Z"}');
|
||||
INSERT OR IGNORE INTO accounts (data) VALUES ('{"accountId":1,"username":"Coach","displayName":"Coach","profileImage":"DefaultProfileImage.jpg","isJunior":false,"platforms":0,"personalPronouns":0,"identityFlags":0,"createdAt":"2016-01-01T00:00:00Z"}');
|
||||
|
||||
@@ -15,24 +15,24 @@ export const SCHEMA_DDL: string[] = [
|
||||
`CREATE TABLE IF NOT EXISTS accounts (
|
||||
data TEXT NOT NULL,
|
||||
avatar TEXT,
|
||||
account_id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.AccountId')) VIRTUAL,
|
||||
username_lower TEXT GENERATED ALWAYS AS (lower(json_extract(data, '$.Username'))) VIRTUAL
|
||||
account_id INTEGER GENERATED ALWAYS AS (json_extract(data, '$.accountId')) VIRTUAL,
|
||||
username_lower TEXT GENERATED ALWAYS AS (lower(json_extract(data, '$.username'))) VIRTUAL
|
||||
)`,
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_account_id ON accounts (account_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_accounts_username_lower ON accounts (username_lower)`,
|
||||
]
|
||||
|
||||
/** Client-facing account shape (PascalCase, as the client expects). */
|
||||
/** Client-facing account shape (camelCase, exactly as the client's AccountDTO). */
|
||||
export interface Account {
|
||||
AccountId: number
|
||||
Username: string
|
||||
DisplayName: string
|
||||
ProfileImage: string
|
||||
IsJunior: boolean
|
||||
Platforms: number
|
||||
PersonalPronouns: number
|
||||
IdentityFlags: number
|
||||
CreatedAt: string
|
||||
accountId: number
|
||||
username: string
|
||||
displayName: string
|
||||
profileImage: string
|
||||
isJunior: boolean
|
||||
platforms: number
|
||||
personalPronouns: number
|
||||
identityFlags: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
interface AccountRow {
|
||||
@@ -68,15 +68,15 @@ export function randomUsername(): string {
|
||||
*/
|
||||
export function defaultAccount(id: number, overrides: Partial<Account> = {}): Account {
|
||||
return {
|
||||
AccountId: id,
|
||||
Username: `Player${id}`,
|
||||
DisplayName: `Player${id}`,
|
||||
ProfileImage: 'DefaultProfileImage.jpg',
|
||||
IsJunior: false,
|
||||
Platforms: 0,
|
||||
PersonalPronouns: 0,
|
||||
IdentityFlags: 0,
|
||||
CreatedAt: new Date().toISOString(),
|
||||
accountId: id,
|
||||
username: `Player${id}`,
|
||||
displayName: `Player${id}`,
|
||||
profileImage: 'DefaultProfileImage.jpg',
|
||||
isJunior: false,
|
||||
platforms: 0,
|
||||
personalPronouns: 0,
|
||||
identityFlags: 0,
|
||||
createdAt: new Date().toISOString(),
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
@@ -112,8 +112,8 @@ export async function createAccount(
|
||||
.prepare('SELECT COALESCE(MAX(account_id), 1) + 1 AS next FROM accounts')
|
||||
.first<{ next: number }>()
|
||||
const id = row?.next ?? 2
|
||||
const username = overrides.Username ?? randomUsername()
|
||||
const account = defaultAccount(id, { Username: username, DisplayName: username, ...overrides })
|
||||
const username = overrides.username ?? randomUsername()
|
||||
const account = defaultAccount(id, { username, displayName: username, ...overrides })
|
||||
await db.prepare('INSERT INTO accounts (data) VALUES (?1)').bind(JSON.stringify(account)).run()
|
||||
return account
|
||||
}
|
||||
|
||||
+19
-18
@@ -109,20 +109,14 @@ const app = new Hono<App>()
|
||||
// EAC challenge — a fresh GUID, JSON-quoted, served as plain text.
|
||||
.get('/eac/challenge', (c) => c.text(`"AA=="`))
|
||||
|
||||
// Cached logins for a platform id. No DB binding yet — always empty.
|
||||
// Cached logins for a platform id. No CachedLogins storage yet, so there's never
|
||||
// a cached account — return []. The client then goes through a fresh login /
|
||||
// create_account instead of auto-logging into a stub account.
|
||||
.get('/cachedlogin/forplatformid/:platform/:id', (c) => {
|
||||
const { platform, id } = c.req.param()
|
||||
logger.info('cached login lookup', { platform, id })
|
||||
// TODO: query CachedLogins once a DB binding exists.
|
||||
return c.json([
|
||||
{
|
||||
accountId: 1,
|
||||
platform: '0',
|
||||
platformId: '0',
|
||||
lastLoginTime: '2026-06-10T00:00:00Z',
|
||||
requirePassword: false,
|
||||
},
|
||||
])
|
||||
// TODO: query CachedLogins once they're persisted.
|
||||
return c.json([])
|
||||
})
|
||||
|
||||
// Bulk cached-login lookup by platform id (friends resolution). The client
|
||||
@@ -141,17 +135,24 @@ const app = new Hono<App>()
|
||||
const platform = Number.isNaN(platformInt) ? '' : (PLATFORM_TYPES[platformInt] ?? '')
|
||||
|
||||
// grant_type=create_account mints + persists a brand-new account (with an
|
||||
// auto-assigned random username — players don't choose one initially). The
|
||||
// token's `sub` is the new account's id. Otherwise use the posted account_id,
|
||||
// falling back to "1" (the cachedlogin stub hands the client account 1).
|
||||
// auto-assigned random username — players don't choose one initially) and the
|
||||
// token's `sub` is its id. Otherwise the request MUST post a valid account_id —
|
||||
// never fall back to a stub account (issuing account 1 to anyone would be bad).
|
||||
let accountId: string
|
||||
if (grantType === 'create_account') {
|
||||
const account = await createAccount(c.env.DB, { Platforms: platformInt || 0 })
|
||||
accountId = String(account.AccountId)
|
||||
const account = await createAccount(c.env.DB, { platforms: platformInt || 0 })
|
||||
accountId = String(account.accountId)
|
||||
// Place the new player in Orientation (they don't matchmake into it).
|
||||
await placeNewPlayerInOrientation(c.env, account.AccountId)
|
||||
await placeNewPlayerInOrientation(c.env, account.accountId)
|
||||
} else {
|
||||
accountId = typeof body.account_id === 'string' && body.account_id ? body.account_id : '1'
|
||||
const posted = typeof body.account_id === 'string' ? body.account_id.trim() : ''
|
||||
if (!/^\d+$/.test(posted)) {
|
||||
return c.json(
|
||||
{ error: 'invalid_request', error_description: 'account_id is required' },
|
||||
400
|
||||
)
|
||||
}
|
||||
accountId = posted
|
||||
}
|
||||
|
||||
const accessToken = await generateToken(accountId, platformId, platform)
|
||||
|
||||
@@ -67,11 +67,10 @@ describe('auth worker routes', () => {
|
||||
expect(await res.text()).toBe('"AA=="')
|
||||
})
|
||||
|
||||
test('GET /cachedlogin/forplatformid/:platform/:id returns a cached login', async () => {
|
||||
test('GET /cachedlogin/forplatformid/:platform/:id returns [] (no cached login)', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/cachedlogin/forplatformid/1/abc123`)
|
||||
expect(res.status).toBe(200)
|
||||
const logins = (await res.json()) as Array<{ accountId: number }>
|
||||
expect(logins[0]).toMatchObject({ accountId: 1 })
|
||||
expect(await res.json()).toEqual([])
|
||||
})
|
||||
|
||||
test('POST /connect/token issues a bearer token with role/scope claims', async () => {
|
||||
@@ -107,19 +106,19 @@ describe('auth worker routes', () => {
|
||||
expect(payload.scope).toContain('rn.api')
|
||||
})
|
||||
|
||||
test('POST /connect/token falls back to account 1 when no account_id is posted', async () => {
|
||||
test('POST /connect/token 400s when no account_id is posted (never defaults to 1)', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/connect/token`, { method: 'POST' })
|
||||
expect(res.status).toBe(200)
|
||||
const { access_token } = (await res.json()) as { access_token: string }
|
||||
const payload = JSON.parse(
|
||||
new TextDecoder().decode(
|
||||
Uint8Array.from(
|
||||
atob(access_token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')),
|
||||
(ch) => ch.charCodeAt(0)
|
||||
)
|
||||
)
|
||||
) as { sub: string }
|
||||
expect(payload.sub).toBe('1')
|
||||
expect(res.status).toBe(400)
|
||||
expect((await res.json()) as { error: string }).toMatchObject({ error: 'invalid_request' })
|
||||
})
|
||||
|
||||
test('POST /connect/token 400s on a non-numeric account_id', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/connect/token`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: 'account_id=notanumber',
|
||||
})
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
test('POST /connect/token grant_type=create_account persists a new account', async () => {
|
||||
@@ -132,8 +131,8 @@ describe('auth worker routes', () => {
|
||||
.bind(sub)
|
||||
.first<{ data: string }>()
|
||||
expect(row).not.toBeNull()
|
||||
const account = JSON.parse(row!.data) as { Username: string }
|
||||
expect(account.Username).not.toMatch(/^Player\d+$/)
|
||||
const account = JSON.parse(row!.data) as { username: string }
|
||||
expect(account.username).not.toMatch(/^Player\d+$/)
|
||||
})
|
||||
|
||||
test('POST /connect/token create_account seeds the new player into Orientation', async () => {
|
||||
|
||||
Reference in New Issue
Block a user