diff --git a/src/entities/player-alias.ts b/src/entities/player-alias.ts index 033634af..5783dc1e 100644 --- a/src/entities/player-alias.ts +++ b/src/entities/player-alias.ts @@ -4,6 +4,9 @@ import Redis from 'ioredis' import { v4 } from 'uuid' import GameChannel, { GameChannelLeavingReason } from './game-channel' import Socket from '../socket' +import Integration, { IntegrationType } from './integration' +import Game from './game' +import { Request } from 'koa-clay' export enum PlayerAliasService { STEAM = 'steam', @@ -44,6 +47,27 @@ export default class PlayerAlias { @Property({ onUpdate: () => new Date() }) updatedAt: Date = new Date() + static async resolveIdentifier( + req: Request, + game: Game, + service: string, + identifier: string + ): Promise { + if (service.trim() === PlayerAliasService.STEAM) { + const em = req.ctx.em as EntityManager + const integration = await em.repo(Integration).findOne({ + game, + type: IntegrationType.STEAMWORKS + }) + + if (integration) { + return integration.getPlayerIdentifier(req, identifier) + } + } + + return identifier.trim() + } + async createSocketToken(redis: Redis): Promise { const token = v4() await redis.set(`socketTokens.${this.id}`, token, 'EX', 3600) diff --git a/src/services/api/player-api.service.ts b/src/services/api/player-api.service.ts index 377ae39b..2ae92cc2 100644 --- a/src/services/api/player-api.service.ts +++ b/src/services/api/player-api.service.ts @@ -10,31 +10,10 @@ import { uniqWith } from 'lodash' import PlayerAPIDocs from '../../docs/player-api.docs' import PlayerGameStat from '../../entities/player-game-stat' import checkScope from '../../policies/checkScope' -import Integration, { IntegrationType } from '../../entities/integration' import { validateAuthSessionToken } from '../../middleware/player-auth-middleware' import { setCurrentPlayerState } from '../../middleware/current-player-middleware' import { ClickHouseClient } from '@clickhouse/client' -async function getRealIdentifier( - req: Request, - key: APIKey, - service: string, - identifier: string -): Promise { - if (service === PlayerAliasService.STEAM) { - const integration = await (req.ctx.em as EntityManager).repo(Integration).findOne({ - game: key.game, - type: IntegrationType.STEAMWORKS - }) - - if (integration) { - return integration.getPlayerIdentifier(req, identifier) - } - } - - return identifier.trim() -} - export async function findAliasFromIdentifyRequest( req: Request, key: APIKey, @@ -43,7 +22,7 @@ export async function findAliasFromIdentifyRequest( ) { return (req.ctx.em as EntityManager).repo(PlayerAlias).findOne({ service: service.trim(), - identifier: await getRealIdentifier(req, key, service, identifier), + identifier, player: { game: key.game } @@ -59,7 +38,7 @@ export async function createPlayerFromIdentifyRequest( if (checkScope(key, APIKeyScope.WRITE_PLAYERS)) { const res = await forwardRequest<{ player: Player }>(req, { body: { - aliases: [{ service, identifier: await getRealIdentifier(req, key, service, identifier) }], + aliases: [{ service, identifier }], props: req.ctx.state.initialPlayerProps } }) @@ -122,12 +101,14 @@ export default class PlayerAPIService extends APIService { let justCreated = false try { - alias = await findAliasFromIdentifyRequest(req, key, service, identifier) + const realIdentifier = await PlayerAlias.resolveIdentifier(req, key.game, service, identifier) + + alias = await findAliasFromIdentifyRequest(req, key, service, realIdentifier) if (!alias) { if (service === PlayerAliasService.TALO) { req.ctx.throw(404, 'Player not found: Talo aliases must be created using the /v1/players/auth API') } else { - const player = await createPlayerFromIdentifyRequest(req, key, service, identifier) + const player = await createPlayerFromIdentifyRequest(req, key, service, realIdentifier) alias = player?.aliases[0] justCreated = true } diff --git a/src/services/api/player-auth-api.service.ts b/src/services/api/player-auth-api.service.ts index 54c7d66a..b0741522 100644 --- a/src/services/api/player-auth-api.service.ts +++ b/src/services/api/player-auth-api.service.ts @@ -46,8 +46,9 @@ export default class PlayerAuthAPIService extends APIService { const em: EntityManager = req.ctx.em const key = await this.getAPIKey(req.ctx) + const realIdentifier = await PlayerAlias.resolveIdentifier(req, key.game, PlayerAliasService.TALO, identifier) - const player = await createPlayerFromIdentifyRequest(req, key, PlayerAliasService.TALO, identifier) + const player = await createPlayerFromIdentifyRequest(req, key, PlayerAliasService.TALO, realIdentifier) const alias = player?.aliases[0] alias.player.auth = new PlayerAuth() @@ -118,8 +119,9 @@ export default class PlayerAuthAPIService extends APIService { const em: EntityManager = req.ctx.em const key = await this.getAPIKey(req.ctx) + const realIdentifier = await PlayerAlias.resolveIdentifier(req, key.game, PlayerAliasService.TALO, identifier) - const alias = await findAliasFromIdentifyRequest(req, key, PlayerAliasService.TALO, identifier) + const alias = await findAliasFromIdentifyRequest(req, key, PlayerAliasService.TALO, realIdentifier) if (!alias) return this.handleFailedLogin(req) await em.populate(alias, ['player.auth']) diff --git a/tests/services/_api/player-api/steamworksIdentify.test.ts b/tests/services/_api/player-api/steamworksIdentify.test.ts index 1caf1d37..c9edade9 100644 --- a/tests/services/_api/player-api/steamworksIdentify.test.ts +++ b/tests/services/_api/player-api/steamworksIdentify.test.ts @@ -77,9 +77,9 @@ describe('Player API service - identify - steamworks auth', () => { .auth(token, { type: 'bearer' }) .expect(200) - expect(authenticateTicketMock).toHaveBeenCalledTimes(1) - expect(verifyOwnershipMock).toHaveBeenCalledTimes(1) - expect(playerSummaryMock).toHaveBeenCalledTimes(1) + expect(authenticateTicketMock).toHaveBeenCalledOnce() + expect(verifyOwnershipMock).toHaveBeenCalledOnce() + expect(playerSummaryMock).toHaveBeenCalledOnce() expect(res.body.alias.identifier).toBe(steamId) expect(res.body.alias.player.id).toBe(player.id) @@ -168,9 +168,9 @@ describe('Player API service - identify - steamworks auth', () => { .auth(token, { type: 'bearer' }) .expect(200) - expect(authenticateTicketMock).toHaveBeenCalledTimes(2) // check + create - expect(verifyOwnershipMock).toHaveBeenCalledTimes(2) - expect(playerSummaryMock).toHaveBeenCalledTimes(2) + expect(authenticateTicketMock).toHaveBeenCalledOnce() + expect(verifyOwnershipMock).toHaveBeenCalledOnce() + expect(playerSummaryMock).toHaveBeenCalledOnce() expect(res.body.alias.identifier).toBe(steamId) expect(res.body.alias.player.props).toStrictEqual([ @@ -436,9 +436,9 @@ describe('Player API service - identify - steamworks auth', () => { .auth(token, { type: 'bearer' }) .expect(200) - expect(authenticateTicketMock).toHaveBeenCalledTimes(2) - expect(verifyOwnershipMock).toHaveBeenCalledTimes(2) - expect(playerSummaryMock).toHaveBeenCalledTimes(2) + expect(authenticateTicketMock).toHaveBeenCalledOnce() + expect(verifyOwnershipMock).toHaveBeenCalledOnce() + expect(playerSummaryMock).toHaveBeenCalledOnce() expect(res.body.alias.identifier).toBe(steamId) // should not include persona name or avatar hash props @@ -516,9 +516,9 @@ describe('Player API service - identify - steamworks auth', () => { .auth(token, { type: 'bearer' }) .expect(200) - expect(authenticateTicketMock).toHaveBeenCalledTimes(2) - expect(verifyOwnershipMock).toHaveBeenCalledTimes(2) - expect(playerSummaryMock).toHaveBeenCalledTimes(2) + expect(authenticateTicketMock).toHaveBeenCalledOnce() + expect(verifyOwnershipMock).toHaveBeenCalledOnce() + expect(playerSummaryMock).toHaveBeenCalledOnce() expect(res.body.alias.identifier).toBe(steamId) // should not include persona name or avatar hash props @@ -598,9 +598,9 @@ describe('Player API service - identify - steamworks auth', () => { .auth(token, { type: 'bearer' }) .expect(200) - expect(authenticateTicketMock).toHaveBeenCalledTimes(2) - expect(verifyOwnershipMock).toHaveBeenCalledTimes(2) - expect(playerSummaryMock).toHaveBeenCalledTimes(2) + expect(authenticateTicketMock).toHaveBeenCalledOnce() + expect(verifyOwnershipMock).toHaveBeenCalledOnce() + expect(playerSummaryMock).toHaveBeenCalledOnce() expect(res.body.alias.identifier).toBe(steamId) // should not include persona name or avatar hash props since player wasn't found @@ -686,9 +686,9 @@ describe('Player API service - identify - steamworks auth', () => { .auth(token, { type: 'bearer' }) .expect(200) - expect(authenticateTicketMock).toHaveBeenCalledTimes(1) - expect(verifyOwnershipMock).toHaveBeenCalledTimes(1) - expect(playerSummaryMock).toHaveBeenCalledTimes(1) + expect(authenticateTicketMock).toHaveBeenCalledOnce() + expect(verifyOwnershipMock).toHaveBeenCalledOnce() + expect(playerSummaryMock).toHaveBeenCalledOnce() expect(res.body.alias.identifier).toBe(steamId) expect(res.body.alias.player.id).toBe(player.id) diff --git a/tests/services/_api/player-auth-api/login.test.ts b/tests/services/_api/player-auth-api/login.test.ts index 6eb3c744..ef53d85f 100644 --- a/tests/services/_api/player-auth-api/login.test.ts +++ b/tests/services/_api/player-auth-api/login.test.ts @@ -154,4 +154,27 @@ describe('Player auth API service - login', () => { }) expect(activity).not.toBeNull() }) + + it('should login successfully when identifier has whitespace', async () => { + const [apiKey, token] = await createAPIKeyAndToken([APIKeyScope.READ_PLAYERS, APIKeyScope.WRITE_PLAYERS]) + + const player = await new PlayerFactory([apiKey.game]).withTaloAlias().state(async () => ({ + auth: await new PlayerAuthFactory().state(async () => ({ + password: await bcrypt.hash('password', 10), + verificationEnabled: false + })).one() + })).one() + const alias = player.aliases[0] + + await em.persistAndFlush(player) + + const res = await request(app) + .post('/v1/players/auth/login') + .send({ identifier: ` ${alias.identifier} `, password: 'password' }) + .auth(token, { type: 'bearer' }) + .expect(200) + + expect(res.body.alias.identifier).toBe(alias.identifier) + expect(res.body.sessionToken).toBeTruthy() + }) }) diff --git a/tests/services/_api/player-auth-api/register.test.ts b/tests/services/_api/player-auth-api/register.test.ts index f5138871..dfddaa8b 100644 --- a/tests/services/_api/player-auth-api/register.test.ts +++ b/tests/services/_api/player-auth-api/register.test.ts @@ -133,4 +133,20 @@ describe('Player auth API service - register', () => { errorCode: 'INVALID_EMAIL' }) }) + + it('should trim whitespace from identifiers before storing', async () => { + const [, token] = await createAPIKeyAndToken([APIKeyScope.READ_PLAYERS, APIKeyScope.WRITE_PLAYERS]) + + const identifier = randUserName() + const identifierWithSpaces = ` ${identifier} ` + + const res = await request(app) + .post('/v1/players/auth/register') + .send({ identifier: identifierWithSpaces, password: 'password' }) + .auth(token, { type: 'bearer' }) + .expect(200) + + expect(res.body.alias.identifier).toBe(identifier) + expect(res.body.alias.identifier).not.toBe(identifierWithSpaces) + }) })