Skip to content

Commit 6230342

Browse files
authored
Merge pull request #314 from TaloDev/toggle-verification
Add endpoint for toggling verification and extra auth activities
2 parents 03406cd + c752bd9 commit 6230342

File tree

10 files changed

+395
-12
lines changed

10 files changed

+395
-12
lines changed

src/docs/player-auth-api.docs.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,45 @@ const PlayerAuthAPIDocs: APIDocs<PlayerAuthAPIService> = {
237237
}
238238
}
239239
]
240+
},
241+
toggleVerification: {
242+
description: 'Toggle if verification is required for a player account',
243+
params: {
244+
headers: {
245+
'x-talo-player': 'The ID of the player',
246+
'x-talo-alias': 'The ID of the player\'s alias',
247+
'x-talo-session': 'The session token'
248+
},
249+
body: {
250+
currentPassword: 'The current password of the player',
251+
verificationEnabled: 'The new verification status for the player account',
252+
email: 'Required when attempting to enable verification if the player does not currently have an email address set'
253+
}
254+
},
255+
samples: [
256+
{
257+
title: 'Sample request (disabling verification)',
258+
sample: {
259+
currentPassword: 'password',
260+
verificationEnabled: false
261+
}
262+
},
263+
{
264+
title: 'Sample request (enabling verification, player does not have an email address)',
265+
sample: {
266+
currentPassword: 'password',
267+
email: 'boz@mail.com',
268+
verificationEnabled: true
269+
}
270+
},
271+
{
272+
title: 'Sample request (enabling verification, player has an email address)',
273+
sample: {
274+
currentPassword: 'password',
275+
verificationEnabled: true
276+
}
277+
}
278+
]
240279
}
241280
}
242281

src/entities/player-auth-activity.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ export enum PlayerAuthActivityType {
1111
CHANGED_PASSWORD,
1212
CHANGED_EMAIL,
1313
PASSWORD_RESET_REQUESTED,
14-
PASSWORD_RESET_COMPLETED
14+
PASSWORD_RESET_COMPLETED,
15+
VERFICIATION_TOGGLED,
16+
CHANGE_PASSWORD_FAILED,
17+
CHANGE_EMAIL_FAILED,
18+
TOGGLE_VERIFICATION_FAILED
1519
}
1620

1721
@Entity()
@@ -64,6 +68,14 @@ export default class PlayerAuthActivity {
6468
return `A password reset request was made for ${authAlias.identifier}'s account`
6569
case PlayerAuthActivityType.PASSWORD_RESET_COMPLETED:
6670
return `A password reset was completed for ${authAlias.identifier}'s account`
71+
case PlayerAuthActivityType.VERFICIATION_TOGGLED:
72+
return `${authAlias.identifier} toggled verification`
73+
case PlayerAuthActivityType.CHANGE_PASSWORD_FAILED:
74+
return `${authAlias.identifier} failed to change their password`
75+
case PlayerAuthActivityType.CHANGE_EMAIL_FAILED:
76+
return `${authAlias.identifier} failed to change their email`
77+
case PlayerAuthActivityType.TOGGLE_VERIFICATION_FAILED:
78+
return `${authAlias.identifier} failed to toggle verification`
6779
default:
6880
return ''
6981
}

src/entities/player-auth.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ export enum PlayerAuthErrorCode {
1414
INVALID_SESSION = 'INVALID_SESSION',
1515
NEW_PASSWORD_MATCHES_CURRENT_PASSWORD = 'NEW_PASSWORD_MATCHES_CURRENT_PASSWORD',
1616
NEW_EMAIL_MATCHES_CURRENT_EMAIL = 'NEW_EMAIL_MATCHES_CURRENT_EMAIL',
17-
PASSWORD_RESET_CODE_INVALID = 'PASSWORD_RESET_CODE_INVALID'
17+
PASSWORD_RESET_CODE_INVALID = 'PASSWORD_RESET_CODE_INVALID',
18+
VERIFICATION_EMAIL_REQUIRED = 'VERIFICATION_EMAIL_REQUIRED'
1819
}
1920

2021
@Entity()

src/policies/api/player-auth-api.policy.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,8 @@ export default class PlayerAuthAPIPolicy extends Policy {
3434
async resetPassword(): Promise<PolicyResponse> {
3535
return await this.hasScopes([APIKeyScope.READ_PLAYERS, APIKeyScope.WRITE_PLAYERS])
3636
}
37+
38+
async toggleVerification(): Promise<PolicyResponse> {
39+
return await this.hasScopes([APIKeyScope.READ_PLAYERS, APIKeyScope.WRITE_PLAYERS])
40+
}
3741
}

src/services/api/player-auth-api.service.ts

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ import { PlayerAuthActivityType } from '../../entities/player-auth-activity'
6464
path: '/reset_password',
6565
handler: 'resetPassword',
6666
docs: PlayerAuthAPIDocs.resetPassword
67+
},
68+
{
69+
method: 'PATCH',
70+
path: '/toggle_verification',
71+
handler: 'toggleVerification',
72+
docs: PlayerAuthAPIDocs.toggleVerification
6773
}
6874
])
6975
export default class PlayerAuthAPIService extends APIService {
@@ -214,7 +220,6 @@ export default class PlayerAuthAPIService extends APIService {
214220
createPlayerAuthActivity(req, alias.player, {
215221
type: PlayerAuthActivityType.VERIFICATION_FAILED
216222
})
217-
218223
await em.flush()
219224

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

282287
const passwordMatches = await bcrypt.compare(currentPassword, alias.player.auth.password)
283288
if (!passwordMatches) {
289+
createPlayerAuthActivity(req, alias.player, {
290+
type: PlayerAuthActivityType.CHANGE_PASSWORD_FAILED,
291+
extra: {
292+
errorCode: PlayerAuthErrorCode.INVALID_CREDENTIALS
293+
}
294+
})
295+
await em.flush()
296+
284297
req.ctx.throw(403, {
285298
message: 'Current password is incorrect',
286299
errorCode: PlayerAuthErrorCode.INVALID_CREDENTIALS
@@ -289,6 +302,14 @@ export default class PlayerAuthAPIService extends APIService {
289302

290303
const isSamePassword = await bcrypt.compare(newPassword, alias.player.auth.password)
291304
if (isSamePassword) {
305+
createPlayerAuthActivity(req, alias.player, {
306+
type: PlayerAuthActivityType.CHANGE_PASSWORD_FAILED,
307+
extra: {
308+
errorCode: PlayerAuthErrorCode.NEW_PASSWORD_MATCHES_CURRENT_PASSWORD
309+
}
310+
})
311+
await em.flush()
312+
292313
req.ctx.throw(400, {
293314
message: 'Please choose a different password',
294315
errorCode: PlayerAuthErrorCode.NEW_PASSWORD_MATCHES_CURRENT_PASSWORD
@@ -323,6 +344,14 @@ export default class PlayerAuthAPIService extends APIService {
323344

324345
const passwordMatches = await bcrypt.compare(currentPassword, alias.player.auth.password)
325346
if (!passwordMatches) {
347+
createPlayerAuthActivity(req, alias.player, {
348+
type: PlayerAuthActivityType.CHANGE_EMAIL_FAILED,
349+
extra: {
350+
errorCode: PlayerAuthErrorCode.INVALID_CREDENTIALS
351+
}
352+
})
353+
await em.flush()
354+
326355
req.ctx.throw(403, {
327356
message: 'Current password is incorrect',
328357
errorCode: PlayerAuthErrorCode.INVALID_CREDENTIALS
@@ -331,6 +360,14 @@ export default class PlayerAuthAPIService extends APIService {
331360

332361
const isSameEmail = newEmail === alias.player.auth.email
333362
if (isSameEmail) {
363+
createPlayerAuthActivity(req, alias.player, {
364+
type: PlayerAuthActivityType.CHANGE_EMAIL_FAILED,
365+
extra: {
366+
errorCode: PlayerAuthErrorCode.NEW_EMAIL_MATCHES_CURRENT_EMAIL
367+
}
368+
})
369+
await em.flush()
370+
334371
req.ctx.throw(400, {
335372
message: 'Please choose a different email address',
336373
errorCode: PlayerAuthErrorCode.NEW_EMAIL_MATCHES_CURRENT_EMAIL
@@ -438,4 +475,65 @@ export default class PlayerAuthAPIService extends APIService {
438475
status: 204
439476
}
440477
}
478+
479+
@Validate({
480+
body: ['currentPassword', 'verificationEnabled']
481+
})
482+
@HasPermission(PlayerAuthAPIPolicy, 'toggleVerification')
483+
async toggleVerification(req: Request): Promise<Response> {
484+
const { currentPassword, verificationEnabled, email } = req.body
485+
const em: EntityManager = req.ctx.em
486+
487+
const alias = await em.getRepository(PlayerAlias).findOne(req.ctx.state.currentAliasId, {
488+
populate: ['player.auth']
489+
})
490+
491+
if (verificationEnabled && !alias.player.auth.email && !email) {
492+
createPlayerAuthActivity(req, alias.player, {
493+
type: PlayerAuthActivityType.TOGGLE_VERIFICATION_FAILED,
494+
extra: {
495+
errorCode: PlayerAuthErrorCode.VERIFICATION_EMAIL_REQUIRED,
496+
verificationEnabled: Boolean(verificationEnabled)
497+
}
498+
})
499+
await em.flush()
500+
501+
req.ctx.throw(400, {
502+
message: 'An email address is required to enable verification',
503+
errorCode: PlayerAuthErrorCode.VERIFICATION_EMAIL_REQUIRED
504+
})
505+
}
506+
507+
const passwordMatches = await bcrypt.compare(currentPassword, alias.player.auth.password)
508+
if (!passwordMatches) {
509+
createPlayerAuthActivity(req, alias.player, {
510+
type: PlayerAuthActivityType.TOGGLE_VERIFICATION_FAILED,
511+
extra: {
512+
errorCode: PlayerAuthErrorCode.INVALID_CREDENTIALS,
513+
verificationEnabled: Boolean(verificationEnabled)
514+
}
515+
})
516+
await em.flush()
517+
518+
req.ctx.throw(403, {
519+
message: 'Current password is incorrect',
520+
errorCode: PlayerAuthErrorCode.INVALID_CREDENTIALS
521+
})
522+
}
523+
524+
alias.player.auth.verificationEnabled = Boolean(verificationEnabled)
525+
526+
createPlayerAuthActivity(req, alias.player, {
527+
type: PlayerAuthActivityType.VERFICIATION_TOGGLED,
528+
extra: {
529+
verificationEnabled: alias.player.auth.verificationEnabled
530+
}
531+
})
532+
533+
await em.flush()
534+
535+
return {
536+
status: 204
537+
}
538+
}
441539
}

tests/fixtures/PlayerAuthActivityFactory.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,8 @@ export default class PlayerAuthActivityFactory extends Factory<PlayerAuthActivit
1919
type: casual.random_element([
2020
PlayerAuthActivityType.REGISTERED,
2121
PlayerAuthActivityType.VERIFICATION_STARTED,
22-
PlayerAuthActivityType.VERIFICATION_FAILED,
2322
PlayerAuthActivityType.LOGGED_IN,
24-
PlayerAuthActivityType.LOGGED_OUT,
25-
PlayerAuthActivityType.CHANGED_PASSWORD,
26-
PlayerAuthActivityType.CHANGED_EMAIL,
27-
PlayerAuthActivityType.PASSWORD_RESET_REQUESTED,
28-
PlayerAuthActivityType.PASSWORD_RESET_COMPLETED
23+
PlayerAuthActivityType.LOGGED_OUT
2924
]),
3025
player: await new PlayerFactory([this.game]).state('with talo alias').one()
3126
}

tests/services/_api/player-api/patch.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,14 @@ describe('Player API service - patch', () => {
155155
const [apiKey, token] = await createAPIKeyAndToken([APIKeyScope.WRITE_PLAYERS])
156156

157157
const player = await new PlayerFactory([apiKey.game]).one()
158+
player.setProps([
159+
{
160+
key: casual.word,
161+
value: casual.word
162+
}
163+
])
158164
await (<EntityManager>global.em).persistAndFlush(player)
159165

160-
const propsLength = player.props.length
161-
162166
const res = await request(global.app)
163167
.patch(`/v1/players/${player.id}`)
164168
.send({
@@ -176,6 +180,6 @@ describe('Player API service - patch', () => {
176180
.auth(token, { type: 'bearer' })
177181
.expect(200)
178182

179-
expect(res.body.player.props).toHaveLength(propsLength + 1)
183+
expect(res.body.player.props).toHaveLength(2)
180184
})
181185
})

tests/services/_api/player-auth-api/changeEmail.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@ describe('Player auth API service - change email', () => {
101101
message: 'Current password is incorrect',
102102
errorCode: 'INVALID_CREDENTIALS'
103103
})
104+
105+
const activity = await (<EntityManager>global.em).getRepository(PlayerAuthActivity).findOne({
106+
type: PlayerAuthActivityType.CHANGE_EMAIL_FAILED,
107+
player: player.id,
108+
extra: {
109+
errorCode: 'INVALID_CREDENTIALS'
110+
}
111+
})
112+
expect(activity).not.toBeNull()
104113
})
105114

106115
it('should not change a player\'s email if the current email is the same as the new email', async () => {
@@ -132,5 +141,14 @@ describe('Player auth API service - change email', () => {
132141
message: 'Please choose a different email address',
133142
errorCode: 'NEW_EMAIL_MATCHES_CURRENT_EMAIL'
134143
})
144+
145+
const activity = await (<EntityManager>global.em).getRepository(PlayerAuthActivity).findOne({
146+
type: PlayerAuthActivityType.CHANGE_EMAIL_FAILED,
147+
player: player.id,
148+
extra: {
149+
errorCode: 'NEW_EMAIL_MATCHES_CURRENT_EMAIL'
150+
}
151+
})
152+
expect(activity).not.toBeNull()
135153
})
136154
})

tests/services/_api/player-auth-api/changePassword.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,15 @@ describe('Player auth API service - change password', () => {
9898
message: 'Current password is incorrect',
9999
errorCode: 'INVALID_CREDENTIALS'
100100
})
101+
102+
const activity = await (<EntityManager>global.em).getRepository(PlayerAuthActivity).findOne({
103+
type: PlayerAuthActivityType.CHANGE_PASSWORD_FAILED,
104+
player: player.id,
105+
extra: {
106+
errorCode: 'INVALID_CREDENTIALS'
107+
}
108+
})
109+
expect(activity).not.toBeNull()
101110
})
102111

103112
it('should not change a player\'s password if the current password is the same as the new password', async () => {
@@ -129,5 +138,14 @@ describe('Player auth API service - change password', () => {
129138
message: 'Please choose a different password',
130139
errorCode: 'NEW_PASSWORD_MATCHES_CURRENT_PASSWORD'
131140
})
141+
142+
const activity = await (<EntityManager>global.em).getRepository(PlayerAuthActivity).findOne({
143+
type: PlayerAuthActivityType.CHANGE_PASSWORD_FAILED,
144+
player: player.id,
145+
extra: {
146+
errorCode: 'NEW_PASSWORD_MATCHES_CURRENT_PASSWORD'
147+
}
148+
})
149+
expect(activity).not.toBeNull()
132150
})
133151
})

0 commit comments

Comments
 (0)