mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-27 16:08:58 -07:00
[www] Manage Avatar Studio and screen-sharing access (#144)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user