Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/entities/player-alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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<string> {
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<string> {
const token = v4()
await redis.set(`socketTokens.${this.id}`, token, 'EX', 3600)
Expand Down
31 changes: 6 additions & 25 deletions src/services/api/player-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
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,
Expand All @@ -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
}
Expand All @@ -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
}
})
Expand Down Expand Up @@ -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
}
Expand Down
6 changes: 4 additions & 2 deletions src/services/api/player-auth-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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'])
Expand Down
36 changes: 18 additions & 18 deletions tests/services/_api/player-api/steamworksIdentify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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([
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
23 changes: 23 additions & 0 deletions tests/services/_api/player-auth-api/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})
16 changes: 16 additions & 0 deletions tests/services/_api/player-auth-api/register.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
Loading