diff --git a/packages/core/src/steam/index.ts b/packages/core/src/steam/index.ts index fc580407..811cdf47 100644 --- a/packages/core/src/steam/index.ts +++ b/packages/core/src/steam/index.ts @@ -6,6 +6,7 @@ import { ErrorCodes, VisibleError } from '../error.js'; import { fn } from '../fn.js'; import { Identifier } from '../id.js'; import { Fingerprint } from '../user/fingerprint.js'; +import { Identity } from '../user/identity.js'; import { User } from '../user/index.js'; import { LinkedAccount } from '../user/linked-account.js'; @@ -163,6 +164,15 @@ async function resolveSshIdentityOnce( } export namespace Steam { + /** + * Connect a Steam account to whoever is asking. + * + * Works out who that is and then hands over to the one place the rules + * live. It used to write the row itself, which meant the cap on how many + * accounts one person may connect held on the sign-in path and not on + * this one — and this is the path a settings screen uses, so it is the + * one that would have been over the limit. + */ export const link = fn( z.object({ steamId: z.string(), @@ -170,34 +180,21 @@ export namespace Steam { userId: z.string().optional() }), async (input) => { - return Database.transaction(async () => { - const existing = await LinkedAccount.findByProvider({ - provider: 'steam', - providerAccountId: input.steamId - }); - if (existing) { - return existing.id; - } - const actor = Actor.use(); - const uid = - input.userId ?? - (actor.type === 'user' || actor.type === 'member' ? actor.properties.userID : undefined); - if (!uid) { - throw new VisibleError( - 'forbidden', - ErrorCodes.Permission.INSUFFICIENT_PERMISSIONS, - 'Cannot link Steam account without a user ID' - ); - } - const id = Identifier.ascending('linkedAccount'); - await LinkedAccount.create({ - id, - userId: uid, - provider: 'steam', - providerAccountId: input.steamId, - profile: input.profile ?? null - }); - return id; + const actor = Actor.use(); + const uid = + input.userId ?? + (actor.type === 'user' || actor.type === 'member' ? actor.properties.userID : undefined); + if (!uid) { + throw new VisibleError( + 'forbidden', + ErrorCodes.Permission.INSUFFICIENT_PERMISSIONS, + 'Cannot link Steam account without a user ID' + ); + } + return Identity.linkSteam({ + userId: uid, + steamId: input.steamId, + profile: input.profile }); } ); diff --git a/packages/core/src/user/identity.test.ts b/packages/core/src/user/identity.test.ts index d42305f7..6c70472a 100644 --- a/packages/core/src/user/identity.test.ts +++ b/packages/core/src/user/identity.test.ts @@ -1,7 +1,9 @@ import { afterAll, beforeEach, describe, expect, test } from 'bun:test'; +import { Actor } from '../actor.js'; import { testDb } from '../db/test.js'; import { Identifier } from '../id.js'; +import { Steam } from '../steam/index.js'; import { Identity } from './identity.js'; import { User } from './index.js'; import { LinkedAccount } from './linked-account.js'; @@ -187,6 +189,27 @@ describe('Identity.linkSteam', () => { expect(thrown.type).toBe('already_exists'); }); + test('the cap holds on the path the settings screen uses', async () => { + const { userID } = await Identity.fromVerifiedEmail({ email: email(7) }); + track(userID); + for (let n = 50; n < 54; n++) { + await Identity.linkSteam({ userId: userID, steamId: steamID(n) }); + } + + let thrown: any = null; + await Actor.with({ type: 'user', properties: { userID, linkedAccountID: '' } }, async () => { + try { + await Steam.link({ steamId: steamID(54) }); + } catch (err) { + thrown = err; + } + }); + + expect(thrown).not.toBeNull(); + expect(thrown.code).toBe('invalid_state'); + expect(await Identity.listSteam(userID)).toHaveLength(Identity.MAX_STEAM_ACCOUNTS); + }); + test('a legacy user is claimed by attaching an email, and keeps its Steam link', async () => { const legacy = await legacySteamUser(40);