mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
add game config configuration
This commit is contained in:
@@ -15,7 +15,7 @@ import {
|
||||
setPasswordHash,
|
||||
setPresence,
|
||||
} from '@repo/domain'
|
||||
import { logger, withNotFound, withOnError } from '@repo/hono-helpers'
|
||||
import { intVar, logger, withNotFound, withOnError } from '@repo/hono-helpers'
|
||||
import { generateToken, TOKEN_TTL_SECONDS, validateAndGetAccountId } from '@repo/jwt'
|
||||
|
||||
import { hashPassword, verifyPassword } from './password'
|
||||
@@ -57,9 +57,15 @@ const PLATFORM_TYPES: Record<number, string> = {
|
||||
* legitimate players behind one address, so this WILL be the arm that produces false
|
||||
* positives. It counts `signupIp` (immutable), so an abuser can't reset their own
|
||||
* count by hopping networks.
|
||||
*
|
||||
* These are the defaults. An operator overrides either arm with the matching worker var
|
||||
* (`MAX_ACCOUNTS_PER_PLATFORM_ID` / `MAX_ACCOUNTS_PER_IP` in wrangler.jsonc `vars`), and
|
||||
* setting one to 0 disables that arm entirely — which a small private server that trusts
|
||||
* everyone it invites will want, and which the IP arm in particular is worth reaching for
|
||||
* if a shared network is being locked out.
|
||||
*/
|
||||
const MAX_ACCOUNTS_PER_PLATFORM_ID = 3
|
||||
const MAX_ACCOUNTS_PER_IP = 3
|
||||
const DEFAULT_MAX_ACCOUNTS_PER_PLATFORM_ID = 3
|
||||
const DEFAULT_MAX_ACCOUNTS_PER_IP = 3
|
||||
|
||||
/** New players start in the Orientation room (RoomId 13) — the new-user flow. */
|
||||
const ORIENTATION_ROOM_ID = 13
|
||||
@@ -301,12 +307,19 @@ const app = new Hono<App>()
|
||||
let accountId: string
|
||||
if (grantType === 'create_account') {
|
||||
// Signup caps. Checked before minting anything, so a rejected signup leaves no
|
||||
// account behind. Each arm is skipped when its identity is unknown (no verified
|
||||
// platform id / no client IP) — an unattributable signup can't be counted against
|
||||
// anyone, and lumping them together would lock out real players.
|
||||
// account behind. Each arm is skipped when it's disabled (var <= 0) or when its
|
||||
// identity is unknown (no verified platform id / no client IP) — an unattributable
|
||||
// signup can't be counted against anyone, and lumping them together would lock out
|
||||
// real players. The disabled check comes first so a disabled arm costs no D1 read.
|
||||
const maxPerPlatformId = intVar(
|
||||
c.env.MAX_ACCOUNTS_PER_PLATFORM_ID,
|
||||
DEFAULT_MAX_ACCOUNTS_PER_PLATFORM_ID
|
||||
)
|
||||
const maxPerIp = intVar(c.env.MAX_ACCOUNTS_PER_IP, DEFAULT_MAX_ACCOUNTS_PER_IP)
|
||||
if (
|
||||
maxPerPlatformId > 0 &&
|
||||
verifiedSteamId !== null &&
|
||||
(await countAccountsByPlatformId(c.env.DB, verifiedSteamId)) >= MAX_ACCOUNTS_PER_PLATFORM_ID
|
||||
(await countAccountsByPlatformId(c.env.DB, verifiedSteamId)) >= maxPerPlatformId
|
||||
) {
|
||||
logger.info('signup rejected: platform account limit', { platformId: verifiedSteamId })
|
||||
return c.json(
|
||||
@@ -318,8 +331,9 @@ const app = new Hono<App>()
|
||||
)
|
||||
}
|
||||
if (
|
||||
maxPerIp > 0 &&
|
||||
clientIp !== '' &&
|
||||
(await countAccountsBySignupIp(c.env.DB, clientIp)) >= MAX_ACCOUNTS_PER_IP
|
||||
(await countAccountsBySignupIp(c.env.DB, clientIp)) >= maxPerIp
|
||||
) {
|
||||
logger.info('signup rejected: per-IP account limit', { ip: clientIp })
|
||||
return c.json(
|
||||
|
||||
@@ -12,6 +12,13 @@ export type Env = SharedHonoEnv & {
|
||||
// signed here verify in all of them. Provisioned via `wrangler secrets-store`;
|
||||
// the store id is spliced into wrangler.jsonc at deploy time (RECFLARE_SECRETS_STORE).
|
||||
JWT_SECRET: SecretsStoreSecret
|
||||
// Signup caps, both optional (see auth.app.ts for what each arm counts and why).
|
||||
// Unset falls back to the DEFAULT_MAX_ACCOUNTS_* constants there; 0 disables that arm.
|
||||
// Typed `string | number` because a var declared in wrangler.jsonc `vars` arrives as a
|
||||
// number while the same var set from the dashboard or `--var` arrives as a string —
|
||||
// read them through `intVar`, never as a bare number.
|
||||
MAX_ACCOUNTS_PER_PLATFORM_ID?: string | number
|
||||
MAX_ACCOUNTS_PER_IP?: string | number
|
||||
}
|
||||
|
||||
/** Variables can be extended */
|
||||
|
||||
@@ -419,10 +419,36 @@ describe('auth worker routes', () => {
|
||||
expect(capped.json.error_description).toMatch(/network/)
|
||||
|
||||
// A different IP is unaffected, and the capped IP can still LOG IN to what it has.
|
||||
const other = await postToken('grant_type=create_account&platform_id=steam-cap4', '198.51.100.23')
|
||||
const other = await postToken(
|
||||
'grant_type=create_account&platform_id=steam-cap4',
|
||||
'198.51.100.23'
|
||||
)
|
||||
expect(other.status).toBe(200)
|
||||
})
|
||||
|
||||
test('the signup caps come from vars, and 0 disables an arm', async () => {
|
||||
// The cap an operator actually runs is the `MAX_ACCOUNTS_PER_IP` var; the constant in
|
||||
// auth.app.ts is only the fallback. `env` is shared by every test in this file, so the
|
||||
// override is restored in `finally` rather than leaking a cap of 0 into the tests above.
|
||||
const original = env.MAX_ACCOUNTS_PER_IP
|
||||
try {
|
||||
env.MAX_ACCOUNTS_PER_IP = 1
|
||||
const ip = '198.51.100.30'
|
||||
const first = await postToken('grant_type=create_account&platform_id=steam-var0', ip)
|
||||
expect(first.status).toBe(200)
|
||||
const capped = await postToken('grant_type=create_account&platform_id=steam-var1', ip)
|
||||
expect(capped.status).toBe(400)
|
||||
expect(capped.json.error_description).toMatch(/network/)
|
||||
|
||||
// 0 disables the arm entirely: the IP that was just capped can sign up again.
|
||||
env.MAX_ACCOUNTS_PER_IP = 0
|
||||
const uncapped = await postToken('grant_type=create_account&platform_id=steam-var2', ip)
|
||||
expect(uncapped.status).toBe(200)
|
||||
} finally {
|
||||
env.MAX_ACCOUNTS_PER_IP = original
|
||||
}
|
||||
})
|
||||
|
||||
test('POST /connect/token does not cap logins, only signups', async () => {
|
||||
// Account 42's owner may be over the signup cap; that must never lock them out of
|
||||
// an account they already have.
|
||||
|
||||
Reference in New Issue
Block a user