Skip to content

Commit

Permalink
Merge pull request #316 from TaloDev/develop
Browse files Browse the repository at this point in the history
Release 0.37.0
  • Loading branch information
tudddorrr authored Aug 1, 2024
2 parents 3bceed8 + 6956a34 commit 94811ed
Show file tree
Hide file tree
Showing 38 changed files with 942 additions and 39 deletions.
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "game-services",
"version": "0.36.1",
"version": "0.37.0",
"description": "",
"main": "src/index.ts",
"scripts": {
Expand Down
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: '[email protected]',
verificationEnabled: true
}
},
{
title: 'Sample request (enabling verification, player has an email address)',
sample: {
currentPassword: 'password',
verificationEnabled: true
}
}
]
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/entities/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import PlayerAuthActivity from './player-auth-activity'
import PlayerAuth from './player-auth'
import GameFeedback from './game-feedback'
import Invite from './invite'
Expand Down Expand Up @@ -33,6 +34,7 @@ import PlayerGroup from './player-group'
import GameSecret from './game-secret'

export default [
PlayerAuthActivity,
PlayerAuth,
GameFeedback,
GameSecret,
Expand Down
94 changes: 94 additions & 0 deletions src/entities/player-auth-activity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Entity, Enum, ManyToOne, PrimaryKey, Property } from '@mikro-orm/mysql'
import Player from './player'
import PlayerAlias, { PlayerAliasService } from './player-alias'

export enum PlayerAuthActivityType {
REGISTERED,
VERIFICATION_STARTED,
VERIFICATION_FAILED,
LOGGED_IN,
LOGGED_OUT,
CHANGED_PASSWORD,
CHANGED_EMAIL,
PASSWORD_RESET_REQUESTED,
PASSWORD_RESET_COMPLETED,
VERFICIATION_TOGGLED,
CHANGE_PASSWORD_FAILED,
CHANGE_EMAIL_FAILED,
TOGGLE_VERIFICATION_FAILED
}

@Entity()
export default class PlayerAuthActivity {
@PrimaryKey()
id: number

@ManyToOne(() => Player, { eager: true })
player: Player

@Enum(() => PlayerAuthActivityType)
type: PlayerAuthActivityType

@Property({ type: 'json' })
extra: {
[key: string]: unknown
} = {}

@Property()
createdAt: Date = new Date()

constructor(player: Player) {
this.player = player
}

private getAuthAlias(): PlayerAlias {
return this.player.aliases.find((alias) => alias.service === PlayerAliasService.TALO)
}

/* v8 ignore start */
private getActivity(): string {
const authAlias = this.getAuthAlias()

switch (this.type) {
case PlayerAuthActivityType.REGISTERED:
return `${authAlias.identifier} created their account`
case PlayerAuthActivityType.VERIFICATION_STARTED:
return `${authAlias.identifier} started verification`
case PlayerAuthActivityType.VERIFICATION_FAILED:
return `${authAlias.identifier} failed verification`
case PlayerAuthActivityType.LOGGED_IN:
return `${authAlias.identifier} logged in`
case PlayerAuthActivityType.LOGGED_OUT:
return `${authAlias.identifier} logged out`
case PlayerAuthActivityType.CHANGED_PASSWORD:
return `${authAlias.identifier} changed their password`
case PlayerAuthActivityType.CHANGED_EMAIL:
return `${authAlias.identifier} changed their email`
case PlayerAuthActivityType.PASSWORD_RESET_REQUESTED:
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 ''
}
}
/* v8 ignore stop */

toJSON() {
return {
id: this.id,
type: this.type,
description: this.getActivity(),
extra: this.extra,
createdAt: this.createdAt
}
}
}
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
2 changes: 1 addition & 1 deletion src/lib/logging/createGameActivity.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { EntityManager } from '@mikro-orm/mysql'
import GameActivity from '../../entities/game-activity'

export default async function createGameActivity(em: EntityManager, data: Partial<GameActivity>): Promise<GameActivity> {
export default function createGameActivity(em: EntityManager, data: Partial<GameActivity>): GameActivity {
const activity = new GameActivity(data.game, data.user)
activity.type = data.type
activity.extra = data.extra ?? {}
Expand Down
24 changes: 24 additions & 0 deletions src/lib/logging/createPlayerAuthActivity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import PlayerAuthActivity from '../../entities/player-auth-activity'
import Player from '../../entities/player'
import { Request } from 'koa-clay'
import { EntityManager } from '@mikro-orm/mysql'

export default function createPlayerAuthActivity(
req: Request,
player: Player,
data: Pick<Partial<PlayerAuthActivity>, 'type' | 'extra'>
): PlayerAuthActivity {
const em: EntityManager = req.ctx.em

const activity = new PlayerAuthActivity(player)
activity.type = data.type
activity.extra = {
...(data.extra ?? {}),
userAgent: req.headers['user-agent'],
ip: req.ctx.request.ip
}

em.persist(activity)

return activity
}
89 changes: 89 additions & 0 deletions src/migrations/.snapshot-gs_dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,95 @@
},
"nativeEnums": {}
},
{
"columns": {
"id": {
"name": "id",
"type": "int",
"unsigned": true,
"autoincrement": true,
"primary": true,
"nullable": false,
"mappedType": "integer"
},
"player_id": {
"name": "player_id",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "string"
},
"type": {
"name": "type",
"type": "tinyint",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "enum"
},
"extra": {
"name": "extra",
"type": "json",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "json"
},
"created_at": {
"name": "created_at",
"type": "datetime",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 0,
"mappedType": "datetime"
}
},
"name": "player_auth_activity",
"indexes": [
{
"columnNames": [
"player_id"
],
"composite": false,
"keyName": "player_auth_activity_player_id_index",
"constraint": false,
"primary": false,
"unique": false
},
{
"keyName": "PRIMARY",
"columnNames": [
"id"
],
"composite": false,
"constraint": true,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {
"player_auth_activity_player_id_foreign": {
"constraintName": "player_auth_activity_player_id_foreign",
"columnNames": [
"player_id"
],
"localTableName": "player_auth_activity",
"referencedColumnNames": [
"id"
],
"referencedTableName": "player",
"updateRule": "cascade"
}
},
"nativeEnums": {}
},
{
"columns": {
"id": {
Expand Down
16 changes: 16 additions & 0 deletions src/migrations/20240725183402CreatePlayerAuthActivityTable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Migration } from '@mikro-orm/migrations'

export class CreatePlayerAuthActivityTable extends Migration {

async up(): Promise<void> {
this.addSql('create table `player_auth_activity` (`id` int unsigned not null auto_increment primary key, `player_id` varchar(255) not null, `type` tinyint not null, `extra` json not null, `created_at` datetime not null) default character set utf8mb4 engine = InnoDB;')
this.addSql('alter table `player_auth_activity` add index `player_auth_activity_player_id_index`(`player_id`);')

this.addSql('alter table `player_auth_activity` add constraint `player_auth_activity_player_id_foreign` foreign key (`player_id`) references `player` (`id`) on update cascade;')
}

async down(): Promise<void> {
this.addSql('drop table if exists `player_auth_activity`;')
}

}
5 changes: 5 additions & 0 deletions src/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { AddAPIKeyLastUsedAtColumn } from './20230205220925AddAPIKeyLastUsedAtCo
import { CreateGameFeedbackAndCategoryTables } from './20240606165637CreateGameFeedbackAndCategoryTables'
import { AddAPIKeyUpdatedAtColumn } from './20240614122547AddAPIKeyUpdatedAtColumn'
import { CreatePlayerAuthTable } from './20240628155142CreatePlayerAuthTable'
import { CreatePlayerAuthActivityTable } from './20240725183402CreatePlayerAuthActivityTable'

export default [
{
Expand Down Expand Up @@ -139,5 +140,9 @@ export default [
{
name: 'CreatePlayerAuthTable',
class: CreatePlayerAuthTable
},
{
name: 'CreatePlayerAuthActivityTable',
class: CreatePlayerAuthActivityTable
}
]
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])
}
}
10 changes: 10 additions & 0 deletions src/policies/player.policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,14 @@ export default class PlayerPolicy extends Policy {

return await this.canAccessGame(player.game.id)
}

@UserTypeGate([UserType.ADMIN], 'view player auth activities')
async getAuthActivities(req: Request): Promise<PolicyResponse> {
const { id } = req.params

const player = await this.getPlayer(id)
if (!player) return new PolicyDenial({ message: 'Player not found' }, 404)

return await this.canAccessGame(player.game.id)
}
}
Loading

0 comments on commit 94811ed

Please sign in to comment.