Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add endpoint for toggling verification and extra auth activities #314

Merged
merged 4 commits into from
Jul 26, 2024
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
39 changes: 39 additions & 0 deletions src/docs/player-auth-api.docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,45 @@ const PlayerAuthAPIDocs: APIDocs<PlayerAuthAPIService> = {
}
}
]
},
toggleVerification: {
description: 'Toggle if verification is required for a player account',
params: {
headers: {
'x-talo-player': 'The ID of the player',
'x-talo-alias': 'The ID of the player\'s alias',
'x-talo-session': 'The session token'
},
body: {
currentPassword: 'The current password of the player',
verificationEnabled: 'The new verification status for the player account',
email: 'Required when attempting to enable verification if the player does not currently have an email address set'
}
},
samples: [
{
title: 'Sample request (disabling verification)',
sample: {
currentPassword: 'password',
verificationEnabled: false
}
},
{
title: 'Sample request (enabling verification, player does not have an email address)',
sample: {
currentPassword: 'password',
email: 'boz@mail.com',
verificationEnabled: true
}
},
{
title: 'Sample request (enabling verification, player has an email address)',
sample: {
currentPassword: 'password',
verificationEnabled: true
}
}
]
}
}

Expand Down
14 changes: 13 additions & 1 deletion src/entities/player-auth-activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ export enum PlayerAuthActivityType {
CHANGED_PASSWORD,
CHANGED_EMAIL,
PASSWORD_RESET_REQUESTED,
PASSWORD_RESET_COMPLETED
PASSWORD_RESET_COMPLETED,
VERFICIATION_TOGGLED,
CHANGE_PASSWORD_FAILED,
CHANGE_EMAIL_FAILED,
TOGGLE_VERIFICATION_FAILED
}

@Entity()
Expand Down Expand Up @@ -64,6 +68,14 @@ export default class PlayerAuthActivity {
return `A password reset request was made for ${authAlias.identifier}'s account`
case PlayerAuthActivityType.PASSWORD_RESET_COMPLETED:
return `A password reset was completed for ${authAlias.identifier}'s account`
case PlayerAuthActivityType.VERFICIATION_TOGGLED:
return `${authAlias.identifier} toggled verification`
case PlayerAuthActivityType.CHANGE_PASSWORD_FAILED:
return `${authAlias.identifier} failed to change their password`
case PlayerAuthActivityType.CHANGE_EMAIL_FAILED:
return `${authAlias.identifier} failed to change their email`
case PlayerAuthActivityType.TOGGLE_VERIFICATION_FAILED:
return `${authAlias.identifier} failed to toggle verification`
default:
return ''
}
Expand Down
3 changes: 2 additions & 1 deletion src/entities/player-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export enum PlayerAuthErrorCode {
INVALID_SESSION = 'INVALID_SESSION',
NEW_PASSWORD_MATCHES_CURRENT_PASSWORD = 'NEW_PASSWORD_MATCHES_CURRENT_PASSWORD',
NEW_EMAIL_MATCHES_CURRENT_EMAIL = 'NEW_EMAIL_MATCHES_CURRENT_EMAIL',
PASSWORD_RESET_CODE_INVALID = 'PASSWORD_RESET_CODE_INVALID'
PASSWORD_RESET_CODE_INVALID = 'PASSWORD_RESET_CODE_INVALID',
VERIFICATION_EMAIL_REQUIRED = 'VERIFICATION_EMAIL_REQUIRED'
}

@Entity()
Expand Down
4 changes: 4 additions & 0 deletions src/policies/api/player-auth-api.policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,8 @@ export default class PlayerAuthAPIPolicy extends Policy {
async resetPassword(): Promise<PolicyResponse> {
return await this.hasScopes([APIKeyScope.READ_PLAYERS, APIKeyScope.WRITE_PLAYERS])
}

async toggleVerification(): Promise<PolicyResponse> {
return await this.hasScopes([APIKeyScope.READ_PLAYERS, APIKeyScope.WRITE_PLAYERS])
}
}
100 changes: 99 additions & 1 deletion src/services/api/player-auth-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ import { PlayerAuthActivityType } from '../../entities/player-auth-activity'
path: '/reset_password',
handler: 'resetPassword',
docs: PlayerAuthAPIDocs.resetPassword
},
{
method: 'PATCH',
path: '/toggle_verification',
handler: 'toggleVerification',
docs: PlayerAuthAPIDocs.toggleVerification
}
])
export default class PlayerAuthAPIService extends APIService {
Expand Down Expand Up @@ -214,7 +220,6 @@ export default class PlayerAuthAPIService extends APIService {
createPlayerAuthActivity(req, alias.player, {
type: PlayerAuthActivityType.VERIFICATION_FAILED
})

await em.flush()

req.ctx.throw(403, {
Expand Down Expand Up @@ -281,6 +286,14 @@ export default class PlayerAuthAPIService extends APIService {

const passwordMatches = await bcrypt.compare(currentPassword, alias.player.auth.password)
if (!passwordMatches) {
createPlayerAuthActivity(req, alias.player, {
type: PlayerAuthActivityType.CHANGE_PASSWORD_FAILED,
extra: {
errorCode: PlayerAuthErrorCode.INVALID_CREDENTIALS
}
})
await em.flush()

req.ctx.throw(403, {
message: 'Current password is incorrect',
errorCode: PlayerAuthErrorCode.INVALID_CREDENTIALS
Expand All @@ -289,6 +302,14 @@ export default class PlayerAuthAPIService extends APIService {

const isSamePassword = await bcrypt.compare(newPassword, alias.player.auth.password)
if (isSamePassword) {
createPlayerAuthActivity(req, alias.player, {
type: PlayerAuthActivityType.CHANGE_PASSWORD_FAILED,
extra: {
errorCode: PlayerAuthErrorCode.NEW_PASSWORD_MATCHES_CURRENT_PASSWORD
}
})
await em.flush()

req.ctx.throw(400, {
message: 'Please choose a different password',
errorCode: PlayerAuthErrorCode.NEW_PASSWORD_MATCHES_CURRENT_PASSWORD
Expand Down Expand Up @@ -323,6 +344,14 @@ export default class PlayerAuthAPIService extends APIService {

const passwordMatches = await bcrypt.compare(currentPassword, alias.player.auth.password)
if (!passwordMatches) {
createPlayerAuthActivity(req, alias.player, {
type: PlayerAuthActivityType.CHANGE_EMAIL_FAILED,
extra: {
errorCode: PlayerAuthErrorCode.INVALID_CREDENTIALS
}
})
await em.flush()

req.ctx.throw(403, {
message: 'Current password is incorrect',
errorCode: PlayerAuthErrorCode.INVALID_CREDENTIALS
Expand All @@ -331,6 +360,14 @@ export default class PlayerAuthAPIService extends APIService {

const isSameEmail = newEmail === alias.player.auth.email
if (isSameEmail) {
createPlayerAuthActivity(req, alias.player, {
type: PlayerAuthActivityType.CHANGE_EMAIL_FAILED,
extra: {
errorCode: PlayerAuthErrorCode.NEW_EMAIL_MATCHES_CURRENT_EMAIL
}
})
await em.flush()

req.ctx.throw(400, {
message: 'Please choose a different email address',
errorCode: PlayerAuthErrorCode.NEW_EMAIL_MATCHES_CURRENT_EMAIL
Expand Down Expand Up @@ -438,4 +475,65 @@ export default class PlayerAuthAPIService extends APIService {
status: 204
}
}

@Validate({
body: ['currentPassword', 'verificationEnabled']
})
@HasPermission(PlayerAuthAPIPolicy, 'toggleVerification')
async toggleVerification(req: Request): Promise<Response> {
const { currentPassword, verificationEnabled, email } = req.body
const em: EntityManager = req.ctx.em

const alias = await em.getRepository(PlayerAlias).findOne(req.ctx.state.currentAliasId, {
populate: ['player.auth']
})

if (verificationEnabled && !alias.player.auth.email && !email) {
createPlayerAuthActivity(req, alias.player, {
type: PlayerAuthActivityType.TOGGLE_VERIFICATION_FAILED,
extra: {
errorCode: PlayerAuthErrorCode.VERIFICATION_EMAIL_REQUIRED,
verificationEnabled: Boolean(verificationEnabled)
}
})
await em.flush()

req.ctx.throw(400, {
message: 'An email address is required to enable verification',
errorCode: PlayerAuthErrorCode.VERIFICATION_EMAIL_REQUIRED
})
}

const passwordMatches = await bcrypt.compare(currentPassword, alias.player.auth.password)
if (!passwordMatches) {
createPlayerAuthActivity(req, alias.player, {
type: PlayerAuthActivityType.TOGGLE_VERIFICATION_FAILED,
extra: {
errorCode: PlayerAuthErrorCode.INVALID_CREDENTIALS,
verificationEnabled: Boolean(verificationEnabled)
}
})
await em.flush()

req.ctx.throw(403, {
message: 'Current password is incorrect',
errorCode: PlayerAuthErrorCode.INVALID_CREDENTIALS
})
}

alias.player.auth.verificationEnabled = Boolean(verificationEnabled)

createPlayerAuthActivity(req, alias.player, {
type: PlayerAuthActivityType.VERFICIATION_TOGGLED,
extra: {
verificationEnabled: alias.player.auth.verificationEnabled
}
})

await em.flush()

return {
status: 204
}
}
}
7 changes: 1 addition & 6 deletions tests/fixtures/PlayerAuthActivityFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,8 @@ export default class PlayerAuthActivityFactory extends Factory<PlayerAuthActivit
type: casual.random_element([
PlayerAuthActivityType.REGISTERED,
PlayerAuthActivityType.VERIFICATION_STARTED,
PlayerAuthActivityType.VERIFICATION_FAILED,
PlayerAuthActivityType.LOGGED_IN,
PlayerAuthActivityType.LOGGED_OUT,
PlayerAuthActivityType.CHANGED_PASSWORD,
PlayerAuthActivityType.CHANGED_EMAIL,
PlayerAuthActivityType.PASSWORD_RESET_REQUESTED,
PlayerAuthActivityType.PASSWORD_RESET_COMPLETED
PlayerAuthActivityType.LOGGED_OUT
]),
player: await new PlayerFactory([this.game]).state('with talo alias').one()
}
Expand Down
10 changes: 7 additions & 3 deletions tests/services/_api/player-api/patch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,14 @@ describe('Player API service - patch', () => {
const [apiKey, token] = await createAPIKeyAndToken([APIKeyScope.WRITE_PLAYERS])

const player = await new PlayerFactory([apiKey.game]).one()
player.setProps([
{
key: casual.word,
value: casual.word
}
])
await (<EntityManager>global.em).persistAndFlush(player)

const propsLength = player.props.length

const res = await request(global.app)
.patch(`/v1/players/${player.id}`)
.send({
Expand All @@ -176,6 +180,6 @@ describe('Player API service - patch', () => {
.auth(token, { type: 'bearer' })
.expect(200)

expect(res.body.player.props).toHaveLength(propsLength + 1)
expect(res.body.player.props).toHaveLength(2)
})
})
18 changes: 18 additions & 0 deletions tests/services/_api/player-auth-api/changeEmail.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ describe('Player auth API service - change email', () => {
message: 'Current password is incorrect',
errorCode: 'INVALID_CREDENTIALS'
})

const activity = await (<EntityManager>global.em).getRepository(PlayerAuthActivity).findOne({
type: PlayerAuthActivityType.CHANGE_EMAIL_FAILED,
player: player.id,
extra: {
errorCode: 'INVALID_CREDENTIALS'
}
})
expect(activity).not.toBeNull()
})

it('should not change a player\'s email if the current email is the same as the new email', async () => {
Expand Down Expand Up @@ -132,5 +141,14 @@ describe('Player auth API service - change email', () => {
message: 'Please choose a different email address',
errorCode: 'NEW_EMAIL_MATCHES_CURRENT_EMAIL'
})

const activity = await (<EntityManager>global.em).getRepository(PlayerAuthActivity).findOne({
type: PlayerAuthActivityType.CHANGE_EMAIL_FAILED,
player: player.id,
extra: {
errorCode: 'NEW_EMAIL_MATCHES_CURRENT_EMAIL'
}
})
expect(activity).not.toBeNull()
})
})
18 changes: 18 additions & 0 deletions tests/services/_api/player-auth-api/changePassword.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ describe('Player auth API service - change password', () => {
message: 'Current password is incorrect',
errorCode: 'INVALID_CREDENTIALS'
})

const activity = await (<EntityManager>global.em).getRepository(PlayerAuthActivity).findOne({
type: PlayerAuthActivityType.CHANGE_PASSWORD_FAILED,
player: player.id,
extra: {
errorCode: 'INVALID_CREDENTIALS'
}
})
expect(activity).not.toBeNull()
})

it('should not change a player\'s password if the current password is the same as the new password', async () => {
Expand Down Expand Up @@ -129,5 +138,14 @@ describe('Player auth API service - change password', () => {
message: 'Please choose a different password',
errorCode: 'NEW_PASSWORD_MATCHES_CURRENT_PASSWORD'
})

const activity = await (<EntityManager>global.em).getRepository(PlayerAuthActivity).findOne({
type: PlayerAuthActivityType.CHANGE_PASSWORD_FAILED,
player: player.id,
extra: {
errorCode: 'NEW_PASSWORD_MATCHES_CURRENT_PASSWORD'
}
})
expect(activity).not.toBeNull()
})
})
Loading