[www] Manage Avatar Studio and screen-sharing access (#144)

This commit is contained in:
dukedotdev
2026-10-26 20:05:27 -04:00
committed by GitHub
parent 44704fec51
commit f6214fb824
8 changed files with 356 additions and 67 deletions
+73 -9
View File
@@ -1,4 +1,6 @@
import {
AVATAR_STUDIO_ACCESS,
avatarStudioFlags,
getAccount,
getAccountByUsername,
listStudioAccounts,
@@ -33,17 +35,44 @@ 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: await hasStudioAccess(c.env.DB, account, c.env.STUDIO_ROLLOUT) })
const flags = await avatarStudioFlags(c.env.DB, accountId)
return c.json({
...Object.fromEntries(
Object.entries(AVATAR_STUDIO_ACCESS).map(([kind, mask]) => [kind, (flags & mask) === mask])
),
granted: await hasStudioAccess(c.env.DB, account, c.env.STUDIO_ROLLOUT),
avatar: (flags & 7) !== 0,
screenshare: account?.hasScreenshare === true,
})
}
type AccessKind = 'studio' | 'avatar' | 'screenshare' | keyof typeof AVATAR_STUDIO_ACCESS
function accessKind(value: unknown): AccessKind | null {
if (value === undefined) return 'studio'
if (value === 'studio' || value === 'avatar' || value === 'screenshare') return value
return typeof value === 'string' && Object.hasOwn(AVATAR_STUDIO_ACCESS, value)
? (value as AccessKind)
: null
}
function avatarMask(kind: AccessKind): number {
return kind === 'avatar'
? 7
: Object.hasOwn(AVATAR_STUDIO_ACCESS, kind)
? AVATAR_STUDIO_ACCESS[kind as keyof typeof AVATAR_STUDIO_ACCESS]
: 0
}
/** Every account with the flag set. */
export async function listStudioAccessHandler(c: Context<App>) {
const accounts = await listStudioAccounts(c.env.DB)
const kind = accessKind(c.req.query('kind'))
if (!kind) return c.json({ error: 'Unknown access type' }, 400)
const accounts = await listStudioAccounts(c.env.DB, kind)
return c.json({
accounts: accounts.map((account) => ({
accountId: account.accountId,
username: account.username,
displayName: account.displayName,
enabled: kind !== 'screenshare' || account.hasScreenshare === true,
})),
})
}
@@ -58,7 +87,9 @@ export async function grantStudioAccessHandler(c: Context<App>) {
if (body === null || typeof body !== 'object' || Array.isArray(body)) {
return c.json({ error: 'Expected a JSON body' }, 400)
}
const posted = body as { username?: unknown; accountId?: unknown }
const posted = body as { username?: unknown; accountId?: unknown; kind?: unknown }
const kind = accessKind(posted.kind)
if (!kind) return c.json({ error: 'Unknown access type' }, 400)
const username =
typeof posted.username === 'string' ? posted.username.trim().replace(/^@/, '') : ''
const accountId = postedAccountId(posted.accountId)
@@ -77,9 +108,15 @@ export async function grantStudioAccessHandler(c: Context<App>) {
return c.json({ error: 'Name a player by username or account id' }, 400)
}
const alreadyGranted = account.hasStudio === true
if (!alreadyGranted) await updateAccount(c.env.DB, account.accountId, { hasStudio: true })
await recordAudit(c, 'grant_studio_access', {
const alreadyGranted =
avatarMask(kind) !== 0
? ((await avatarStudioFlags(c.env.DB, account.accountId)) & avatarMask(kind)) ===
avatarMask(kind)
: kind === 'screenshare'
? account.hasScreenshare === true
: account.hasStudio === true
await setAccess(c, account.accountId, kind, true)
await recordAudit(c, `grant_${kind}_access`, {
playerId: account.accountId,
username: account.username,
alreadyGranted,
@@ -98,15 +135,22 @@ export async function grantStudioAccessHandler(c: Context<App>) {
* account is a 404.
*/
export async function revokeStudioAccessHandler(c: Context<App>) {
const kind = accessKind(c.req.query('kind'))
if (!kind) return c.json({ error: 'Unknown access type' }, 400)
const accountId = Number(c.req.param('id'))
if (!Number.isInteger(accountId) || accountId <= 0) {
return c.json({ error: 'A numeric player id is required' }, 400)
}
const account = await getAccount(c.env.DB, accountId)
if (!account) return c.json({ error: 'No player has that account id' }, 404)
const removed = account.hasStudio === true
if (removed) await updateAccount(c.env.DB, accountId, { hasStudio: false })
await recordAudit(c, 'revoke_studio_access', {
const removed =
avatarMask(kind) !== 0
? ((await avatarStudioFlags(c.env.DB, accountId)) & avatarMask(kind)) !== 0
: kind === 'screenshare'
? account.hasScreenshare === true
: account.hasStudio === true
await setAccess(c, accountId, kind, false)
await recordAudit(c, `revoke_${kind}_access`, {
playerId: accountId,
username: account.username,
removed,
@@ -114,6 +158,26 @@ export async function revokeStudioAccessHandler(c: Context<App>) {
return c.json({ accountId, granted: false, removed })
}
async function setAccess(c: Context<App>, accountId: number, kind: AccessKind, enabled: boolean) {
const mask = avatarMask(kind)
if (mask !== 0) {
const now = new Date().toISOString()
await c.env.DB.prepare(
`INSERT INTO avatar_studio_access (account_id, flags, created_at, modified_at)
VALUES (?, ?, ?, ?) ON CONFLICT(account_id) DO UPDATE SET
flags = ${enabled ? 'flags | ?' : 'flags & ~?'}, modified_at = excluded.modified_at`
)
.bind(accountId, enabled ? mask : 0, now, now, mask)
.run()
} else {
await updateAccount(
c.env.DB,
accountId,
kind === 'screenshare' ? { hasScreenshare: enabled } : { hasStudio: enabled }
)
}
}
/** A positive integer account id from a JSON field, or null when it isn't one. */
function postedAccountId(value: unknown): number | null {
if (typeof value === 'number' && Number.isInteger(value) && value > 0) return value