[studio] rollout support

This commit is contained in:
Devin Zuczek
2026-10-24 15:00:49 -04:00
parent 4e2cdd87eb
commit 8397997234
12 changed files with 218 additions and 37 deletions
+8
View File
@@ -144,6 +144,14 @@ export type Env = SharedHonoEnv & {
* all closes the claim, exactly as an unset one does.
*/
DISCORD_BENEFITS_ROLE_IDS?: string
/**
* Discord role ids whose holders have Rec Room Studio upload access without the
* account's `hasStudio` flag (`RECFLARE_STUDIO_ROLLOUT` in the root `.env`; the var
* `auth` stamps `betastudio` from). `/api/studio-access` answers `granted` through the
* same `hasStudioAccess`, so the Studio settings page agrees with the token. The staff
* list and its add/remove still manage the flag alone.
*/
STUDIO_ROLLOUT?: string
/**
* The bot user's token for the scheduled role refresh (discord-roles.ts). Optional in
* effect: the binding must exist for the deploy, but a placeholder value just leaves the
+8 -3
View File
@@ -8,6 +8,8 @@ import {
import { logger } from '@repo/hono-helpers'
import { validateAndGetAccountId } from '@repo/jwt'
import { hasStudioAccess } from '../../auth/src/platform-db'
import type { Context } from 'hono'
import type { Account } from '@repo/domain'
import type { App } from './context'
@@ -20,15 +22,18 @@ import type { App } from './context'
* the signed-in player. Staff add and remove people through the routes under
* `/api/staff/studio-access`, behind the same `requireStaff` gate as moderation.
* The grant is the account's `hasStudio` flag; `auth` stamps `betastudio` from it
* on the next Studio sign-in or refresh. Who granted what is on `audit_log`.
* on the next Studio sign-in or refresh. Who granted what is on `audit_log`. A Discord
* role in the `STUDIO_ROLLOUT` var grants the same access without the flag
* (`hasStudioAccess`); the status below reports it, the staff list does not — that list
* is the accounts staff added by hand.
*/
/** The signed-in player's own flag, and nothing else. 401 with no session. */
/** Whether the signed-in player can upload — flag or rollout role. 401 with no session. */
export async function studioAccessStatusHandler(c: Context<App>) {
const accountId = await validateAndGetAccountId(c.req.raw, await c.env.JWT_SECRET.get())
if (accountId === null) return c.json({ error: 'Unauthorized' }, 401)
const account = await getAccount(c.env.DB, accountId)
return c.json({ granted: account?.hasStudio === true })
return c.json({ granted: await hasStudioAccess(c.env.DB, account, c.env.STUDIO_ROLLOUT) })
}
/** Every account with the flag set. */
+21
View File
@@ -3094,6 +3094,27 @@ it('tells a signed-in player whether they can upload, and nobody else', async ()
})
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ granted: false })
// A Discord role named in STUDIO_ROLLOUT grants the same access without the flag, and the
// page reads it from the same helper `auth` stamps `betastudio` from, so the two agree.
// The account has to exist: a link alone, with no account behind it, grants nothing.
await updateAccount(env.DB, 8601, { username: 'StudioFan' })
await linkPlatformIdentity(env.DB, 8601, PlatformType.Discord, '500000000000008601', [
'1077000000000000031',
])
const original = env.STUDIO_ROLLOUT
try {
env.STUDIO_ROLLOUT = '1077000000000000031'
const rolled = await SELF.fetch('https://example.com/api/studio-access', {
headers: { authorization: `Bearer ${await tokenFor(8601, ['gameClient'])}` },
})
expect(await rolled.json()).toEqual({ granted: true })
// The flag itself never moved; the staff list is still empty of this account.
expect((await getAccount(env.DB, 8601))?.hasStudio).not.toBe(true)
} finally {
if (original === undefined) delete env.STUDIO_ROLLOUT
else env.STUDIO_ROLLOUT = original
}
})
it('lets staff add, list, and remove studio upload access', async () => {