Massive restructuring, most of the progress on reorganising Data Access Layer etc. finished. Gotta get the app back up and running now
This commit is contained in:
114
src/ObjectCollections/KadiUserCollection.ts
Normal file
114
src/ObjectCollections/KadiUserCollection.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import MongoStoredObjectCollection from "./MongoStoredObjectCollection";
|
||||||
|
import mongo from "mongodb";
|
||||||
|
import {CredentialsTakenError, GenericPersistenceError} from "../errors";
|
||||||
|
import StoredPlayers from "../ObjectCollections/PlayerCollection";
|
||||||
|
import {SupportedLang} from "../enums";
|
||||||
|
import {AccountStatsMongoData, defaultAccountStatsMongoData} from "../Objects/DefaultStatsMongoData";
|
||||||
|
import bcrypt from "bcrypt";
|
||||||
|
import {getMongoObjectCollection} from "../database";
|
||||||
|
import KadiUser, {LoginDetails} from "../Objects/KadiUser";
|
||||||
|
import {ActiveRecordId} from "../Objects/ActiveRecord";
|
||||||
|
import {SavedGameData} from "../Objects/savedGame";
|
||||||
|
|
||||||
|
export interface KadiUserMongoData {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
lang: SupportedLang;
|
||||||
|
friends: ActiveRecordId[];
|
||||||
|
player: ActiveRecordId;
|
||||||
|
guests: ActiveRecordId[];
|
||||||
|
accountStats: AccountStatsMongoData;
|
||||||
|
savedGames: SavedGameData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class KadiUserCollection extends MongoStoredObjectCollection<KadiUserMongoData> {
|
||||||
|
constructor(collectionClient: mongo.Collection) {
|
||||||
|
super(collectionClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
private storedUserFrom(data: KadiUserMongoData): KadiUser {
|
||||||
|
return new KadiUser(
|
||||||
|
data.id,
|
||||||
|
data.username,
|
||||||
|
data.email,
|
||||||
|
data.password,
|
||||||
|
data.lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
async read(id: string): Promise<KadiUser | null> {
|
||||||
|
const foundUser = await this.mongoRead(id);
|
||||||
|
if (foundUser) {
|
||||||
|
return this.storedUserFrom(foundUser);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByEmail(emailQuery: string): Promise<KadiUser | null> {
|
||||||
|
const foundUser = await this.mongoFindByAttribute("email", emailQuery);
|
||||||
|
if (foundUser) {
|
||||||
|
return this.storedUserFrom(foundUser);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async registerUser(loginDetails: LoginDetails): Promise<KadiUser> {
|
||||||
|
const usernameTaken = await this.userWithUsernameExists(loginDetails.username);
|
||||||
|
const emailTaken = await this.userWithEmailExists(loginDetails.email);
|
||||||
|
if (usernameTaken || emailTaken) {
|
||||||
|
throw new CredentialsTakenError(usernameTaken, emailTaken);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return this.addNewUser({...loginDetails})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async addNewUser(loginDetails: LoginDetails): Promise<KadiUser> {
|
||||||
|
const newPlayer = await StoredPlayers.create(loginDetails.username);
|
||||||
|
const securePassword = await this.makePasswordSecure(loginDetails.password);
|
||||||
|
const newUser = await this.mongoCreate({
|
||||||
|
username: loginDetails.username,
|
||||||
|
email: loginDetails.email,
|
||||||
|
password: securePassword,
|
||||||
|
lang: SupportedLang.gb,
|
||||||
|
player: newPlayer.getId(),
|
||||||
|
accountStats: defaultAccountStatsMongoData(),
|
||||||
|
friends: [],
|
||||||
|
guests: [],
|
||||||
|
savedGames: [],
|
||||||
|
});
|
||||||
|
return this.storedUserFrom(newUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
async userWithEmailExists(email: string): Promise<boolean> {
|
||||||
|
const object = await this.mongoFindByAttribute("email", email);
|
||||||
|
return object !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async userWithUsernameExists(username: string): Promise<boolean> {
|
||||||
|
const object = await this.mongoFindByAttribute("username", username);
|
||||||
|
return object !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSerializedAuthUser(id: string): Promise<KadiUser> {
|
||||||
|
const foundUser = await this.mongoRead(id);
|
||||||
|
if (foundUser) {
|
||||||
|
return this.storedUserFrom(foundUser);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new GenericPersistenceError("User not found!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async makePasswordSecure(password: string): Promise<string> {
|
||||||
|
return bcrypt.hash(password, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const KadiUserCollectionSingleton = new KadiUserCollection(getMongoObjectCollection("users"));
|
||||||
|
export default KadiUserCollectionSingleton;
|
||||||
55
src/ObjectCollections/MongoStoredObjectCollection.ts
Normal file
55
src/ObjectCollections/MongoStoredObjectCollection.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import mongo from "mongodb";
|
||||||
|
import {tryQuery} from "./database";
|
||||||
|
import {MongoError} from "../errors";
|
||||||
|
import ActiveRecord, {ActiveRecordId} from "../Objects/ActiveRecord";
|
||||||
|
|
||||||
|
|
||||||
|
abstract class MongoStoredObjectCollection<IRawData extends {id: ActiveRecordId}> {
|
||||||
|
|
||||||
|
protected constructor(protected mongoDbClientCollection: mongo.Collection) {}
|
||||||
|
|
||||||
|
protected async mongoCreate(objectData: Omit<IRawData, "id">): Promise<IRawData> {
|
||||||
|
return tryQuery(async () => {
|
||||||
|
const insertOneWriteOpResult = await this.mongoDbClientCollection.insertOne(objectData);
|
||||||
|
if (insertOneWriteOpResult.result.ok === 1) {
|
||||||
|
return insertOneWriteOpResult.ops[0]
|
||||||
|
} else {
|
||||||
|
throw new MongoError(`Error creating the object: ${JSON.stringify(objectData)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async mongoRead(id: string): Promise<IRawData | null> {
|
||||||
|
return tryQuery(async () =>
|
||||||
|
await this.mongoDbClientCollection.findOne({_id: id})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async mongoFindByAttribute(attribute: string, value: any): Promise<IRawData | null> {
|
||||||
|
return tryQuery(async () =>
|
||||||
|
await this.mongoDbClientCollection.findOne({[attribute]: value})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async mongoDelete(objectId: ActiveRecordId, returnObject?: boolean): Promise<IRawData | null | void> {
|
||||||
|
let deletedObject;
|
||||||
|
if (returnObject ?? true) {
|
||||||
|
deletedObject = await this.mongoRead(objectId);
|
||||||
|
}
|
||||||
|
const deleteWriteOpResult = await this.mongoDbClientCollection.deleteOne({_id: objectId});
|
||||||
|
if (deleteWriteOpResult.result.ok === 1) {
|
||||||
|
return deletedObject;
|
||||||
|
} else {
|
||||||
|
throw new MongoError(`Error deleting the object with id: ${JSON.stringify(objectId)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async mongoSave(object: IRawData) {
|
||||||
|
await tryQuery(() =>
|
||||||
|
this.mongoDbClientCollection.findOneAndUpdate({_id: object.id}, {$set: {...object, id: undefined}})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default MongoStoredObjectCollection;
|
||||||
30
src/ObjectCollections/PlayerCollection.ts
Normal file
30
src/ObjectCollections/PlayerCollection.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import MongoStoredObjectCollection from "./MongoStoredObjectCollection";
|
||||||
|
import mongo from "mongodb";
|
||||||
|
import {defaultPlayerStatsMongoData, PlayerStatsMongoData} from "../Objects/DefaultStatsMongoData";
|
||||||
|
import {getMongoObjectCollection} from "./database";
|
||||||
|
import Player from "../Objects/Player";
|
||||||
|
import PlayerStats from "../Objects/PlayerStats";
|
||||||
|
|
||||||
|
export interface MongoStoredPlayerData {
|
||||||
|
id: string;
|
||||||
|
nick: string;
|
||||||
|
stats?: PlayerStatsMongoData;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlayerCollection extends MongoStoredObjectCollection<MongoStoredPlayerData> {
|
||||||
|
constructor(collectionClient: mongo.Collection) {
|
||||||
|
super(collectionClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
private storedPlayerFrom(data: MongoStoredPlayerData): Player {
|
||||||
|
return new Player(data.id, data.nick, data.stats ? new PlayerStats(data.stats) : undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(nick: string): Promise<Player> {
|
||||||
|
const newPlayer = {nick, stats: defaultPlayerStatsMongoData()};
|
||||||
|
return this.storedPlayerFrom(await this.mongoCreate(newPlayer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const StoredPlayers = new PlayerCollection(getMongoObjectCollection("players"));
|
||||||
|
export default StoredPlayers;
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
import {Ruleset} from "../rulesets";
|
import {Ruleset} from "../rulesets";
|
||||||
import {AccountStats, OutcomeType, PlayerGameResults} from "../models/Stats";
|
import {AccountStatsMongoData, OutcomeType, PlayerGameResults} from "./DefaultStatsMongoData";
|
||||||
import {UpdateError} from "../errors";
|
import {UpdateError} from "../errors";
|
||||||
import StatsUpdater from "./StatsUpdater";
|
import StatsUpdater from "./StatsUpdater";
|
||||||
|
|
||||||
export class AccountStatsUpdater {
|
class AccountStats {
|
||||||
private data?: AccountStats;
|
private data?: AccountStatsMongoData;
|
||||||
private readonly updater: StatsUpdater;
|
private readonly updater: StatsUpdater;
|
||||||
constructor(data?: AccountStats) {
|
constructor(data?: AccountStatsMongoData) {
|
||||||
if (data) {
|
if (data) {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
}
|
}
|
||||||
this.updater = new StatsUpdater();
|
this.updater = new StatsUpdater();
|
||||||
}
|
}
|
||||||
|
|
||||||
use(data: AccountStats) {
|
use(data: AccountStatsMongoData) {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.updater.use(data);
|
this.updater.use(data);
|
||||||
}
|
}
|
||||||
@@ -29,3 +29,5 @@ export class AccountStatsUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default AccountStats;
|
||||||
7
src/Objects/ActiveRecord.ts
Normal file
7
src/Objects/ActiveRecord.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export type ActiveRecordId = string;
|
||||||
|
|
||||||
|
interface ActiveRecord {
|
||||||
|
getId(): ActiveRecordId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ActiveRecord;
|
||||||
163
src/Objects/DefaultStatsMongoData.ts
Executable file
163
src/Objects/DefaultStatsMongoData.ts
Executable file
@@ -0,0 +1,163 @@
|
|||||||
|
import {CellDef, DEFAULT_RULESET, DEFAULT_RULESET_NAME, Ruleset} from "../rulesets";
|
||||||
|
import {FieldType} from "../enums";
|
||||||
|
|
||||||
|
|
||||||
|
export type OutcomeType = "win" | "loss" | "runnerUp" | "draw";
|
||||||
|
export interface PlayerStatsMongoData extends BaseStatsMongoData {}
|
||||||
|
export interface AccountStatsMongoData extends BaseStatsMongoData {
|
||||||
|
timesNoWinner: number;
|
||||||
|
}
|
||||||
|
export interface BaseStatsMongoData {
|
||||||
|
statsByRuleset: Record<string, RulesetStatsMongoData>
|
||||||
|
gamesPlayed: number;
|
||||||
|
}
|
||||||
|
export interface RulesetStatsMongoData {
|
||||||
|
blockStats: Record<string, BlockStatsMongoData>;
|
||||||
|
wins: number;
|
||||||
|
runnerUps: number;
|
||||||
|
draws: number;
|
||||||
|
losses: number;
|
||||||
|
grandTotal: TotalFieldStatsMongoData;
|
||||||
|
}
|
||||||
|
export interface BlockStatsMongoData {
|
||||||
|
cellStats: Record<string, CellStatsMongoData>;
|
||||||
|
timesHadBonus?: number;
|
||||||
|
total: TotalFieldStatsMongoData;
|
||||||
|
}
|
||||||
|
export interface BaseCellStatsMongoData {
|
||||||
|
runningTotal: number;
|
||||||
|
}
|
||||||
|
export interface StrikeableFieldStatsMongoData extends BaseCellStatsMongoData {
|
||||||
|
timesStruck: number;
|
||||||
|
}
|
||||||
|
export interface BestableFieldStatsMongoData extends BaseCellStatsMongoData {
|
||||||
|
best: number;
|
||||||
|
worst: number;
|
||||||
|
}
|
||||||
|
export type TotalFieldStatsMongoData = BestableFieldStatsMongoData;
|
||||||
|
export type BoolFieldStatsMongoData = StrikeableFieldStatsMongoData & { total: number };
|
||||||
|
export type NumberFieldStatsMongoData = StrikeableFieldStatsMongoData & BestableFieldStatsMongoData;
|
||||||
|
export type MultiplierFieldStatsMongoData = NumberFieldStatsMongoData;
|
||||||
|
export type SuperkadiFieldStatsMongoData = NumberFieldStatsMongoData;
|
||||||
|
export type CellStatsMongoData = BoolFieldStatsMongoData | NumberFieldStatsMongoData | MultiplierFieldStatsMongoData | SuperkadiFieldStatsMongoData;
|
||||||
|
|
||||||
|
|
||||||
|
export interface PlayerGameResults {
|
||||||
|
blocks: Record<string, BlockResults>;
|
||||||
|
}
|
||||||
|
export interface BlockResults {
|
||||||
|
cells: Record<string, CellResults>
|
||||||
|
}
|
||||||
|
export interface CellResults {
|
||||||
|
value: CellValue;
|
||||||
|
}
|
||||||
|
export type CellValue = "cellFlagStrike" | number | boolean;
|
||||||
|
|
||||||
|
|
||||||
|
function defaultTotalFieldStatsMongoData(): TotalFieldStatsMongoData {
|
||||||
|
return {
|
||||||
|
best: 0,
|
||||||
|
worst: -1,
|
||||||
|
runningTotal: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultBoolFieldStatsMongoData(): BoolFieldStatsMongoData {
|
||||||
|
return {
|
||||||
|
timesStruck: 0,
|
||||||
|
runningTotal: 0,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultNumberFieldStatsMongoData(): NumberFieldStatsMongoData {
|
||||||
|
return {
|
||||||
|
timesStruck: 0,
|
||||||
|
runningTotal: 0,
|
||||||
|
best: 0,
|
||||||
|
worst: -1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultMultiplierFieldStatsMongoData(): MultiplierFieldStatsMongoData {
|
||||||
|
return {
|
||||||
|
timesStruck: 0,
|
||||||
|
runningTotal: 0,
|
||||||
|
best: 0,
|
||||||
|
worst: -1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultSuperkadiFieldStatsMongoData(): SuperkadiFieldStatsMongoData {
|
||||||
|
return {
|
||||||
|
timesStruck: 0,
|
||||||
|
runningTotal: 0,
|
||||||
|
best: 0,
|
||||||
|
worst: -1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultBlockStatsMongoData(cellSchemas: Record<string, CellDef>, hasBonus: boolean): BlockStatsMongoData {
|
||||||
|
const cellStatsRecord: Record<string, CellStatsMongoData> = {};
|
||||||
|
for (const cellLabel in cellSchemas) {
|
||||||
|
switch (cellSchemas[cellLabel].fieldType) {
|
||||||
|
case FieldType.number:
|
||||||
|
cellStatsRecord[cellLabel] = defaultNumberFieldStatsMongoData();
|
||||||
|
break;
|
||||||
|
case FieldType.bool:
|
||||||
|
cellStatsRecord[cellLabel] = defaultBoolFieldStatsMongoData();
|
||||||
|
break;
|
||||||
|
case FieldType.multiplier:
|
||||||
|
cellStatsRecord[cellLabel] = defaultMultiplierFieldStatsMongoData();
|
||||||
|
break;
|
||||||
|
case FieldType.superkadi:
|
||||||
|
cellStatsRecord[cellLabel] = defaultSuperkadiFieldStatsMongoData();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const stats = {
|
||||||
|
total: defaultTotalFieldStatsMongoData(),
|
||||||
|
timesHadBonus: 0,
|
||||||
|
cellStats: cellStatsRecord,
|
||||||
|
};
|
||||||
|
if (hasBonus) {
|
||||||
|
return {...stats, timesHadBonus: 0};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultRulesetStatsMongoData(ruleset: Ruleset): RulesetStatsMongoData {
|
||||||
|
const blockStatsRecord: Record<string, BlockStatsMongoData> = {};
|
||||||
|
for (const blockLabel in ruleset.blocks) {
|
||||||
|
blockStatsRecord[blockLabel] = defaultBlockStatsMongoData(ruleset.blocks[blockLabel].cells, ruleset.blocks[blockLabel].hasBonus);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
blockStats: blockStatsRecord,
|
||||||
|
wins: 0,
|
||||||
|
draws: 0,
|
||||||
|
losses: 0,
|
||||||
|
runnerUps: 0,
|
||||||
|
grandTotal: defaultTotalFieldStatsMongoData(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultBaseStatsMongoData(): BaseStatsMongoData {
|
||||||
|
return {
|
||||||
|
statsByRuleset: {
|
||||||
|
[DEFAULT_RULESET_NAME]: defaultRulesetStatsMongoData(DEFAULT_RULESET),
|
||||||
|
},
|
||||||
|
gamesPlayed: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultAccountStatsMongoData(): AccountStatsMongoData {
|
||||||
|
return {...defaultBaseStatsMongoData(), timesNoWinner: 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultPlayerStatsMongoData(): PlayerStatsMongoData {
|
||||||
|
return defaultBaseStatsMongoData();
|
||||||
|
}
|
||||||
36
src/Objects/KadiUser.ts
Normal file
36
src/Objects/KadiUser.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import {SupportedLang} from "../enums";
|
||||||
|
import ActiveRecord, {ActiveRecordId} from "./ActiveRecord";
|
||||||
|
|
||||||
|
export type LoginDetails = { username: string, email: string, password: string };
|
||||||
|
|
||||||
|
class KadiUser implements ActiveRecord {
|
||||||
|
constructor(
|
||||||
|
private id: string,
|
||||||
|
private username: string,
|
||||||
|
private email: string,
|
||||||
|
private password: string,
|
||||||
|
private lang: SupportedLang,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getLoginDetails(): LoginDetails {
|
||||||
|
return {username: this.username, email: this.email, password: this.password};
|
||||||
|
}
|
||||||
|
|
||||||
|
preferredLang(): SupportedLang {
|
||||||
|
return this.lang;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeLang(lang: SupportedLang): void {
|
||||||
|
this.lang = lang;
|
||||||
|
}
|
||||||
|
|
||||||
|
getId(): ActiveRecordId {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUsername(): string {
|
||||||
|
return this.username;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KadiUser;
|
||||||
43
src/Objects/Player.ts
Executable file
43
src/Objects/Player.ts
Executable file
@@ -0,0 +1,43 @@
|
|||||||
|
import {CellValue} from "../controllers/statsController";
|
||||||
|
import {Ruleset} from "../rulesets";
|
||||||
|
import {ActiveRecordId} from "./ActiveRecord";
|
||||||
|
import {UpdateError} from "../errors";
|
||||||
|
import {OutcomeType, PlayerGameResults} from "./DefaultStatsMongoData";
|
||||||
|
import PlayerStats from "./PlayerStats";
|
||||||
|
|
||||||
|
export interface CellDetails {
|
||||||
|
id: string;
|
||||||
|
value: CellValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Player {
|
||||||
|
constructor(
|
||||||
|
private id: ActiveRecordId,
|
||||||
|
private nick: string,
|
||||||
|
private stats?: PlayerStats
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getId(): ActiveRecordId {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
getNick(): string {
|
||||||
|
return this.nick;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setNick(newNick: string): Promise<void> {
|
||||||
|
this.nick = newNick;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset) {
|
||||||
|
if (this.stats) {
|
||||||
|
this.stats.updateStats(playerGameResults, ruleset);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new UpdateError(`The player hasn't loaded with stats. The player's stats have to be loaded first in
|
||||||
|
order for them to then be updated.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Player;
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
import {Ruleset} from "../rulesets";
|
import {Ruleset} from "../rulesets";
|
||||||
import StatsUpdater, {OutcomeType, PlayerGameResults, PlayerStats} from "../models/Stats";
|
import {OutcomeType, PlayerGameResults, PlayerStatsMongoData} from "./DefaultStatsMongoData";
|
||||||
|
import StatsUpdater from "./StatsUpdater";
|
||||||
|
|
||||||
class PlayerStatsUpdater {
|
class PlayerStats {
|
||||||
private data?: PlayerStats;
|
private data?: PlayerStatsMongoData;
|
||||||
private readonly updater: StatsUpdater;
|
private readonly updater: StatsUpdater;
|
||||||
constructor(data?: PlayerStats) {
|
constructor(data?: PlayerStatsMongoData) {
|
||||||
if (data) {
|
if (data) {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
}
|
}
|
||||||
this.updater = new StatsUpdater();
|
this.updater = new StatsUpdater();
|
||||||
}
|
}
|
||||||
|
|
||||||
use(data: PlayerStats) {
|
use(data: PlayerStatsMongoData) {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.updater.use(data);
|
this.updater.use(data);
|
||||||
}
|
}
|
||||||
@@ -21,4 +22,4 @@ class PlayerStatsUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PlayerStatsUpdater;
|
export default PlayerStats;
|
||||||
@@ -3,24 +3,24 @@ import ScoreCalculator, {ScoreCardJSONRepresentation} from "./ScoreCalculator";
|
|||||||
import {UpdateError} from "../errors";
|
import {UpdateError} from "../errors";
|
||||||
import {FieldType} from "../enums";
|
import {FieldType} from "../enums";
|
||||||
import {
|
import {
|
||||||
BaseStats,
|
BaseStatsMongoData,
|
||||||
BestableFieldStats,
|
BestableFieldStatsMongoData,
|
||||||
BoolFieldStats,
|
BoolFieldStatsMongoData,
|
||||||
CellStats,
|
CellStatsMongoData,
|
||||||
OutcomeType,
|
OutcomeType,
|
||||||
PlayerGameResults, RulesetStats,
|
PlayerGameResults, RulesetStatsMongoData,
|
||||||
TotalFieldStats
|
TotalFieldStatsMongoData
|
||||||
} from "../models/Stats";
|
} from "./DefaultStatsMongoData";
|
||||||
|
|
||||||
class StatsUpdater {
|
class StatsUpdater {
|
||||||
private data?: BaseStats;
|
private data?: BaseStatsMongoData;
|
||||||
private validationRuleset?: Ruleset;
|
private validationRuleset?: Ruleset;
|
||||||
private calculator?: ScoreCalculator;
|
private calculator?: ScoreCalculator;
|
||||||
private currentStatsObject?: RulesetStats;
|
private currentStatsObject?: RulesetStatsMongoData;
|
||||||
constructor() {
|
constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
use(data: BaseStats) {
|
use(data: BaseStatsMongoData) {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ class StatsUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateTotalFieldStats(statsObject: TotalFieldStats, total: number) {
|
private updateTotalFieldStats(statsObject: TotalFieldStatsMongoData, total: number) {
|
||||||
statsObject.best = total > statsObject.best ? total : statsObject.best;
|
statsObject.best = total > statsObject.best ? total : statsObject.best;
|
||||||
statsObject.worst = total < statsObject.worst ? total : statsObject.worst;
|
statsObject.worst = total < statsObject.worst ? total : statsObject.worst;
|
||||||
statsObject.runningTotal += total;
|
statsObject.runningTotal += total;
|
||||||
@@ -69,10 +69,10 @@ class StatsUpdater {
|
|||||||
const cellFieldType = this.validationRuleset?.blocks[ids.blockId].cells[ids.cellId].fieldType;
|
const cellFieldType = this.validationRuleset?.blocks[ids.blockId].cells[ids.cellId].fieldType;
|
||||||
const cellScore = this.calculator!.getCellScoreByLocation({...ids});
|
const cellScore = this.calculator!.getCellScoreByLocation({...ids});
|
||||||
if (cellScore > 0 && cellFieldType === FieldType.bool) {
|
if (cellScore > 0 && cellFieldType === FieldType.bool) {
|
||||||
(cellStats as BoolFieldStats).total += 1;
|
(cellStats as BoolFieldStatsMongoData).total += 1;
|
||||||
}
|
}
|
||||||
else if (cellFieldType === FieldType.multiplier || FieldType.superkadi || FieldType.number) {
|
else if (cellFieldType === FieldType.multiplier || FieldType.superkadi || FieldType.number) {
|
||||||
const bestableStats = (cellStats as BestableFieldStats);
|
const bestableStats = (cellStats as BestableFieldStatsMongoData);
|
||||||
if (bestableStats.best < cellScore) {
|
if (bestableStats.best < cellScore) {
|
||||||
bestableStats.best = cellScore;
|
bestableStats.best = cellScore;
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ class StatsUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCellStatsByIds(ids: {cellId: string, blockId: string, rulesetId: string}): CellStats {
|
private getCellStatsByIds(ids: {cellId: string, blockId: string, rulesetId: string}): CellStatsMongoData {
|
||||||
return this.data!.statsByRuleset[ids.rulesetId].blockStats[ids.blockId].cellStats[ids.cellId];
|
return this.data!.statsByRuleset[ids.rulesetId].blockStats[ids.blockId].cellStats[ids.cellId];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import DbUser, {IDbUser, IDbUserDoc} from "../models/dbUser_old";
|
|
||||||
import {RequestHandler} from "express";
|
|
||||||
import {IPlayer} from "../models/StoredPlayer";
|
|
||||||
|
|
||||||
export const whoAmI: RequestHandler = async (req, res) => {
|
|
||||||
if (req.isAuthenticated()) {
|
|
||||||
const user = req.user as IDbUser;
|
|
||||||
res.json({loggedIn: true, username: user.username, lang: user.lang});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
res.json({loggedIn: false});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const changeLang: RequestHandler = async (req, res) => {
|
|
||||||
const user = (req.user as IDbUser);
|
|
||||||
await user.changeLang(req.body.lang);
|
|
||||||
res.send({
|
|
||||||
username: user.username,
|
|
||||||
updatedLang: req.body.lang,
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addGuest: RequestHandler = async (req, res) => {
|
|
||||||
const user = (req.user as IDbUser);
|
|
||||||
if (req.body.guestName) {
|
|
||||||
const newGuest: IPlayer = await user.addGuest(req.body.guestName);
|
|
||||||
res.send({
|
|
||||||
username: user.username,
|
|
||||||
userId: user.id,
|
|
||||||
newGuest: {
|
|
||||||
id: newGuest.id,
|
|
||||||
name: newGuest.nick,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
res.status(400).send({message: "This request requires the parameter 'guestName'"});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateGuest: RequestHandler = async (req, res) => {
|
|
||||||
const user = (req.user as IDbUser);
|
|
||||||
const {id: guestId} = req.params;
|
|
||||||
if (req.body.newName) {
|
|
||||||
const {newName} = req.body;
|
|
||||||
const updatedGuest = await user.updateGuest({id: guestId, newNick: newName});
|
|
||||||
res.status(200).send({
|
|
||||||
userId: user.id,
|
|
||||||
username: user.username,
|
|
||||||
updatedGuest: {
|
|
||||||
id: updatedGuest.id,
|
|
||||||
nick: updatedGuest.nick,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
res.status(400).send({message: "This request requires the parameter 'newName'"});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getGuest: RequestHandler = async (req, res) => {
|
|
||||||
const user = (req.user as IDbUser);
|
|
||||||
const {id: guestId} = req.params;
|
|
||||||
const guest = await user.getGuest(guestId);
|
|
||||||
res.status(200).send({
|
|
||||||
userId: user.id,
|
|
||||||
username: user.username,
|
|
||||||
guest: guest,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteGuest: RequestHandler = async (req, res) => {
|
|
||||||
const user = (req.user as IDbUser);
|
|
||||||
const {id: guestId} = req.params;
|
|
||||||
const deletedGuest = await user.deleteGuest(guestId);
|
|
||||||
res.status(200).send({
|
|
||||||
userId: user.id,
|
|
||||||
username: user.username,
|
|
||||||
deletedGuest: deletedGuest,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getGuests: RequestHandler = async (req, res) => {
|
|
||||||
const user = (req.user as IDbUser);
|
|
||||||
const guests = await user.getGuests();
|
|
||||||
res.status(200).send({
|
|
||||||
userId: user.id,
|
|
||||||
username: user.username,
|
|
||||||
guests: guests,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAllPlayersAssociatedWithAccount: RequestHandler = async (req, res) => {
|
|
||||||
const user = (req.user as IDbUser);
|
|
||||||
const guests = await user.getGuests();
|
|
||||||
const mainPlayer = await user.getMainPlayerInfo();
|
|
||||||
res.status(200).send({guests, mainPlayer});
|
|
||||||
};
|
|
||||||
101
src/controllers/kadiUserController.ts
Executable file
101
src/controllers/kadiUserController.ts
Executable file
@@ -0,0 +1,101 @@
|
|||||||
|
import {RequestHandler} from "express";
|
||||||
|
import KadiUser from "../Objects/KadiUser";
|
||||||
|
import Player from "../Objects/Player";
|
||||||
|
import KadiUserCollection from "../ObjectCollections/KadiUserCollection";
|
||||||
|
|
||||||
|
export const currentUserDetails: RequestHandler = async (req, res) => {
|
||||||
|
if (req.isAuthenticated()) {
|
||||||
|
const user = req.user as KadiUser;
|
||||||
|
res.json({loggedIn: true, username: user.getUsername(), lang: user.preferredLang()});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
res.json({loggedIn: false});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const changeLang: RequestHandler = async (req, res) => {
|
||||||
|
const user = (req.user as KadiUser);
|
||||||
|
await user.changeLang(req.body.lang);
|
||||||
|
res.send({
|
||||||
|
username: user.getUsername(),
|
||||||
|
updatedLang: req.body.lang,
|
||||||
|
userId: user.getId(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addGuest: RequestHandler = async (req, res) => {
|
||||||
|
const user = (req.user as KadiUser);
|
||||||
|
if (req.body.guestName) {
|
||||||
|
const newGuest: Player = await KadiUserCollection.addGuestForAccount(req.body.guestName);
|
||||||
|
res.send({
|
||||||
|
username: user.getUsername(),
|
||||||
|
userId: user.getId(),
|
||||||
|
newGuest: {
|
||||||
|
id: newGuest.getId(),
|
||||||
|
name: newGuest.getNick(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
res.status(400).send({message: "This request requires the parameter 'guestName'"});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateGuest: RequestHandler = async (req, res) => {
|
||||||
|
const user = (req.user as KadiUser);
|
||||||
|
const {id: guestId} = req.params;
|
||||||
|
if (req.body.newName) {
|
||||||
|
const {newName} = req.body;
|
||||||
|
const updatedGuest = await KadiUserCollection.updateGuestForAccount({id: guestId, newNick: newName});
|
||||||
|
res.status(200).send({
|
||||||
|
userId: user.getId(),
|
||||||
|
username: user.getUsername(),
|
||||||
|
updatedGuest: {
|
||||||
|
id: updatedGuest.getId(),
|
||||||
|
nick: updatedGuest.getNick(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
res.status(400).send({message: "This request requires the parameter 'newName'"});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getGuest: RequestHandler = async (req, res) => {
|
||||||
|
const user = (req.user as KadiUser);
|
||||||
|
const {id: guestId} = req.params;
|
||||||
|
const guest = await KadiUserCollection.getGuestForAccount(guestId, user.getId());
|
||||||
|
res.status(200).send({
|
||||||
|
userId: user.getId(),
|
||||||
|
username: user.getUsername(),
|
||||||
|
guest: guest,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteGuest: RequestHandler = async (req, res) => {
|
||||||
|
const user = (req.user as KadiUser);
|
||||||
|
const {id: guestId} = req.params;
|
||||||
|
const deletedGuest = await KadiUserCollection.deleteGuestForAccount(guestId, user.getId());
|
||||||
|
res.status(200).send({
|
||||||
|
userId: user.getId(),
|
||||||
|
username: user.getUsername(),
|
||||||
|
deletedGuest: deletedGuest,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getGuests: RequestHandler = async (req, res) => {
|
||||||
|
const user = (req.user as KadiUser);
|
||||||
|
const guests = await KadiUserCollection.getGuestsForAccount(user.getId());
|
||||||
|
res.status(200).send({
|
||||||
|
userId: user.getId(),
|
||||||
|
username: user.getUsername(),
|
||||||
|
guests: guests,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAllPlayersAssociatedWithAccount: RequestHandler = async (req, res) => {
|
||||||
|
const user = (req.user as KadiUser);
|
||||||
|
const guests = await KadiUserCollection.getAllGuestsForAccount(user.getId());
|
||||||
|
const mainPlayer = await KadiUserCollection.getMainPlayerForAccount(user.getId());
|
||||||
|
res.status(200).send({guests, mainPlayer});
|
||||||
|
};
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import passport from "passport";
|
import passport from "passport";
|
||||||
import {RequestHandler} from "express";
|
import {RequestHandler} from "express";
|
||||||
import StoredUsers, {LoginDetails} from "../models/StoredUser";
|
import {VerifyFunction} from "passport-local";
|
||||||
|
import KadiUserCollection from "../ObjectCollections/KadiUserCollection";
|
||||||
|
import bcrypt from "bcrypt";
|
||||||
|
import KadiUser, {LoginDetails} from "../Objects/KadiUser";
|
||||||
|
|
||||||
export const showLoginPage: RequestHandler = (req, res) => {
|
export const showLoginPage: RequestHandler = (req, res) => {
|
||||||
res.render("login.ejs");
|
res.render("login.ejs");
|
||||||
@@ -21,7 +24,7 @@ export const showRegistrationPage: RequestHandler = (req, res) => {
|
|||||||
export const registerNewUser: RequestHandler = async (req, res) => {
|
export const registerNewUser: RequestHandler = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const loginDetails: LoginDetails = req.body as LoginDetails;
|
const loginDetails: LoginDetails = req.body as LoginDetails;
|
||||||
const newUser = await StoredUsers.registerUser(loginDetails);
|
const newUser = await KadiUserCollection.registerUser(loginDetails);
|
||||||
req.login(newUser, (err) => {
|
req.login(newUser, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
throw err;
|
throw err;
|
||||||
@@ -49,3 +52,29 @@ export const logoutUser: RequestHandler = (req, res) => {
|
|||||||
req.logout();
|
req.logout();
|
||||||
res.redirect(req.baseUrl + "/login");
|
res.redirect(req.baseUrl + "/login");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const authenticateKadiUser: VerifyFunction = async (email, password, done) => {
|
||||||
|
const user = await KadiUserCollection.findByEmail(email);
|
||||||
|
if (!user) {
|
||||||
|
return done(null, false, { message: "A user with that email does not exist."} );
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (await bcrypt.compare(password, (await user.getLoginDetails()).password)) {
|
||||||
|
return done(null, user);
|
||||||
|
} else {
|
||||||
|
return done(null, false, {message: "Password incorrect"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return done(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function serializeKadiUser(user: KadiUser, done: (err: any, id?: unknown) => void): Promise<void> {
|
||||||
|
done(null, user.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deserializeKadiUser(id: string, done: (err: any, id?: unknown) => void): Promise<void> {
|
||||||
|
const user: KadiUser | null = await KadiUserCollection.getSerializedAuthUser(id);
|
||||||
|
done(null, user);
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import {MongoClient, Db} from "mongodb";
|
import {MongoClient, Db} from "mongodb";
|
||||||
import Settings from "../server-config.json";
|
import Settings from "./server-config.json";
|
||||||
import {GenericPersistenceError, MongoError} from "../errors";
|
import {GenericPersistenceError, MongoError} from "./errors";
|
||||||
|
|
||||||
let SessionDbClient: Db;
|
let SessionDbClient: Db;
|
||||||
|
|
||||||
export async function initMongoSessionClient() {
|
export async function initMongoSessionClient() {
|
||||||
if (SessionDbClient === undefined) {
|
if (SessionDbClient === undefined) {
|
||||||
const client = await MongoClient.connect(Settings.mongodb_uri, {useUnifiedTopology: true});
|
const client = await MongoClient.connect(Settings.mongodb_uri, {useUnifiedTopology: true});
|
||||||
@@ -10,6 +11,7 @@ export async function initMongoSessionClient() {
|
|||||||
}
|
}
|
||||||
return SessionDbClient;
|
return SessionDbClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMongoObjectCollection(collectionName: string) {
|
export function getMongoObjectCollection(collectionName: string) {
|
||||||
if (SessionDbClient === undefined) {
|
if (SessionDbClient === undefined) {
|
||||||
throw new MongoError("Cannot retrieve a collection before the session client has been initialised!");
|
throw new MongoError("Cannot retrieve a collection before the session client has been initialised!");
|
||||||
@@ -20,7 +22,6 @@ export function getMongoObjectCollection(collectionName: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CallbackWrapper = <T>(query: () => T) => Promise<T>;
|
type CallbackWrapper = <T>(query: () => T) => Promise<T>;
|
||||||
|
|
||||||
export const tryQuery: CallbackWrapper = async (cb) => {
|
export const tryQuery: CallbackWrapper = async (cb) => {
|
||||||
try {
|
try {
|
||||||
return cb();
|
return cb();
|
||||||
@@ -27,3 +27,14 @@ export class ModelParameterError extends GenericPersistenceError {
|
|||||||
this.name = "ModelParameterError";
|
this.name = "ModelParameterError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CredentialsTakenError extends KadiError {
|
||||||
|
public emailExists: boolean;
|
||||||
|
public usernameExists: boolean;
|
||||||
|
constructor(usernameExists: boolean, emailExists: boolean) {
|
||||||
|
super("Registration failure:" + usernameExists + emailExists);
|
||||||
|
this.usernameExists = usernameExists;
|
||||||
|
this.emailExists = emailExists;
|
||||||
|
this.name = "CredentialsTakenError";
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/index.ts
65
src/index.ts
@@ -1,48 +1,41 @@
|
|||||||
import express from "express";
|
import express, {NextFunction, Request, Response} from "express";
|
||||||
import {initialisePassport, requireAuthenticated, requireNotAuthenticated} from "./passport-config";
|
|
||||||
import mongoose from "mongoose";
|
|
||||||
import Settings from "./server-config.json";
|
import Settings from "./server-config.json";
|
||||||
import flash from "express-flash";
|
import flash from "express-flash";
|
||||||
import passport from "passport";
|
import passport from "passport";
|
||||||
import session from "express-session";
|
import session from "express-session";
|
||||||
import MainRouter from "./routers/mainRouter";
|
import MainRouter from "./routers/mainRouter";
|
||||||
|
import {initMongoSessionClient} from "./database";
|
||||||
|
import {Strategy as LocalStrategy} from "passport-local";
|
||||||
|
import {authenticateKadiUser, deserializeKadiUser, serializeKadiUser} from "./controllers/signupController";
|
||||||
|
|
||||||
// MongoDB Setup
|
async function startApp() {
|
||||||
mongoose.connect(Settings.mongodb_uri, (err: any) => {
|
await initMongoSessionClient();
|
||||||
if (err) {
|
passport.use(new LocalStrategy({ usernameField: "email" }, authenticateKadiUser));
|
||||||
console.log(err.message);
|
passport.serializeUser(serializeKadiUser);
|
||||||
}
|
passport.deserializeUser(deserializeKadiUser);
|
||||||
else {
|
const app = express();
|
||||||
console.log("Successfully connected to mongoDB!");
|
app.use(express.json());
|
||||||
}
|
app.set("port", process.env.PORT || 3000);
|
||||||
});
|
app.set("view-engine", "ejs");
|
||||||
|
app.set("views", "views");
|
||||||
// Express app config
|
app.use(express.urlencoded({ extended: false}));
|
||||||
const app = express();
|
app.use(flash());
|
||||||
app.use(express.json());
|
app.use(session({
|
||||||
app.set("port", process.env.PORT || 3000);
|
|
||||||
app.set("view-engine", "ejs");
|
|
||||||
app.set("views", "views");
|
|
||||||
app.use(express.urlencoded({ extended: false}));
|
|
||||||
app.use(flash());
|
|
||||||
app.use(session({
|
|
||||||
// TODO hide the secret
|
// TODO hide the secret
|
||||||
secret: "secret",
|
secret: "secret",
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
resave: false
|
resave: false
|
||||||
}));
|
}));
|
||||||
app.locals = {
|
app.locals = {
|
||||||
rootUrl: Settings.serverRoot
|
rootUrl: Settings.serverRoot
|
||||||
};
|
};
|
||||||
|
app.use(passport.initialize());
|
||||||
|
app.use(passport.session());
|
||||||
|
app.use(Settings.serverRoot + "/static", express.static("static"));
|
||||||
|
app.use(Settings.serverRoot, MainRouter);
|
||||||
|
app.listen(app.get("port"), () => {
|
||||||
|
console.log("Kadi running on http://localhost:%d", app.get("port"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Passport init
|
startApp();
|
||||||
initialisePassport();
|
|
||||||
app.use(passport.initialize());
|
|
||||||
app.use(passport.session());
|
|
||||||
|
|
||||||
app.use(Settings.serverRoot + "/static", express.static("static"));
|
|
||||||
app.use(Settings.serverRoot, MainRouter);
|
|
||||||
|
|
||||||
const server = app.listen(app.get("port"), () => {
|
|
||||||
console.log("App is running on http://localhost:%d", app.get("port"));
|
|
||||||
});
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import StoredObject from "./StoredObject";
|
|
||||||
|
|
||||||
abstract class MongoStoredObject<RawDataInterface> implements StoredObject {
|
|
||||||
protected constructor(protected data: {_id: string} & RawDataInterface) {}
|
|
||||||
|
|
||||||
id(): string {
|
|
||||||
return this.data._id;
|
|
||||||
}
|
|
||||||
|
|
||||||
rawData(): RawDataInterface {
|
|
||||||
return this.data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MongoStoredObject;
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import StoredObject, {StoredObjectConstructor, StoredObjectId} from "./StoredObject";
|
|
||||||
import mongo from "mongodb";
|
|
||||||
import {tryQuery} from "./utils";
|
|
||||||
import {MongoError} from "../errors";
|
|
||||||
import StoredObjectCollection from "./StoredObjectCollection";
|
|
||||||
|
|
||||||
|
|
||||||
abstract class MongoStoredObjectCollection<IRawData, IStoredObject extends StoredObject>
|
|
||||||
implements StoredObjectCollection<IStoredObject> {
|
|
||||||
|
|
||||||
protected mongoDbClientCollection: mongo.Collection;
|
|
||||||
protected StoredObjectConstructor: StoredObjectConstructor<IRawData, IStoredObject>;
|
|
||||||
|
|
||||||
protected constructor(
|
|
||||||
collectionClient: mongo.Collection,
|
|
||||||
objectConstructor: StoredObjectConstructor<IRawData, IStoredObject>
|
|
||||||
) {
|
|
||||||
this.mongoDbClientCollection = collectionClient;
|
|
||||||
this.StoredObjectConstructor = objectConstructor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async create(objectData: Omit<IRawData, "_id">): Promise<IRawData> {
|
|
||||||
return tryQuery(async () => {
|
|
||||||
const insertOneWriteOpResult = await this.mongoDbClientCollection.insertOne(objectData);
|
|
||||||
if (insertOneWriteOpResult.result.ok === 1) {
|
|
||||||
return insertOneWriteOpResult.ops[0]
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw new MongoError(`Error creating the object: ${JSON.stringify(objectData)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async newObject(objectData: Omit<IRawData, "_id">): Promise<IStoredObject> {
|
|
||||||
return new this.StoredObjectConstructor(await this.create(objectData));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async findObjectById(id: string): Promise<IRawData | null> {
|
|
||||||
return tryQuery(async () =>
|
|
||||||
await this.mongoDbClientCollection.findOne({_id: id})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async findObjectByAttribute(attribute: string, value: any): Promise<IRawData | null> {
|
|
||||||
return tryQuery(async () =>
|
|
||||||
await this.mongoDbClientCollection.findOne({[attribute]: value})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteById(objectId: StoredObjectId, returnObject?: boolean): Promise<IStoredObject | null | void> {
|
|
||||||
let deletedObject;
|
|
||||||
if (returnObject ?? true) {
|
|
||||||
deletedObject = await this.findById(objectId);
|
|
||||||
}
|
|
||||||
const deleteWriteOpResult = await this.mongoDbClientCollection.deleteOne({_id: objectId});
|
|
||||||
if (deleteWriteOpResult.result.ok === 1) {
|
|
||||||
return deletedObject;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw new MongoError(`Error deleting the object with id: ${JSON.stringify(objectId)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async findById(id: string): Promise<IStoredObject | null> {
|
|
||||||
const data = await this.findObjectById(id);
|
|
||||||
if (data) {
|
|
||||||
return new this.StoredObjectConstructor(data);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
async save(...objects: IStoredObject[]): Promise<void> {
|
|
||||||
await tryQuery(async () => {
|
|
||||||
for (const object of objects) {
|
|
||||||
await this.mongoDbClientCollection.findOneAndUpdate({_id: object.id()}, {...object.rawData()});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MongoStoredObjectCollection;
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import {DEFAULT_RULESET_NAME, DefaultRulesetStats} from "../rulesets";
|
|
||||||
|
|
||||||
|
|
||||||
export type OutcomeType = "win" | "loss" | "runnerUp" | "draw";
|
|
||||||
export interface PlayerStats extends BaseStats {}
|
|
||||||
export interface AccountStats extends BaseStats {
|
|
||||||
timesNoWinner: number;
|
|
||||||
}
|
|
||||||
interface BaseStats {
|
|
||||||
statsByRuleset: Record<string, RulesetStats> & { [DEFAULT_RULESET_NAME]: DefaultRulesetStats }
|
|
||||||
gamesPlayed: number;
|
|
||||||
}
|
|
||||||
export interface RulesetStats {
|
|
||||||
blockStats: Record<string, BlockStats>;
|
|
||||||
wins: number;
|
|
||||||
runnerUps: number;
|
|
||||||
draws: number;
|
|
||||||
losses: number;
|
|
||||||
grandTotal: TotalFieldStats;
|
|
||||||
}
|
|
||||||
interface BlockStats {
|
|
||||||
cellStats: Record<string, CellStats>;
|
|
||||||
timesHadBonus?: number;
|
|
||||||
total: TotalFieldStats;
|
|
||||||
}
|
|
||||||
interface BaseCellStats {
|
|
||||||
runningTotal: number;
|
|
||||||
}
|
|
||||||
interface StrikeableFieldStats extends BaseCellStats {
|
|
||||||
timesStruck: number;
|
|
||||||
}
|
|
||||||
interface BestableFieldStats extends BaseCellStats {
|
|
||||||
best: number;
|
|
||||||
worst: number;
|
|
||||||
}
|
|
||||||
type TotalFieldStats = BestableFieldStats;
|
|
||||||
type BoolFieldStats = StrikeableFieldStats & { total: number };
|
|
||||||
type NumberFieldStats = StrikeableFieldStats & BestableFieldStats;
|
|
||||||
type MultiplierFieldStats = NumberFieldStats;
|
|
||||||
type SuperkadiFieldStats = NumberFieldStats;
|
|
||||||
type CellStats = BoolFieldStats | NumberFieldStats | MultiplierFieldStats | SuperkadiFieldStats;
|
|
||||||
|
|
||||||
|
|
||||||
export interface PlayerGameResults {
|
|
||||||
blocks: Record<string, BlockResults>;
|
|
||||||
}
|
|
||||||
interface BlockResults {
|
|
||||||
cells: Record<string, CellResults>
|
|
||||||
}
|
|
||||||
interface CellResults {
|
|
||||||
value: CellValue;
|
|
||||||
}
|
|
||||||
type CellValue = "cellFlagStrike" | number | boolean;
|
|
||||||
|
|
||||||
|
|
||||||
function default
|
|
||||||
|
|
||||||
function defaultBoolFieldStats(): BoolFieldStats {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultNumberFieldStats(): NumberFieldStats {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultMultiplierFieldStats(): MultiplierFieldStats {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultSuperkadiFieldStats(): SuperkadiFieldStats {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultBaseStatsData(): BaseStats {
|
|
||||||
return {
|
|
||||||
statsByRuleset: {
|
|
||||||
[DEFAULT_RULESET_NAME]: {}
|
|
||||||
wins: 0,
|
|
||||||
runnerUps: 0,
|
|
||||||
draws: 0,
|
|
||||||
losses: 0,
|
|
||||||
grandTotal: {},
|
|
||||||
},
|
|
||||||
gamesPlayed: 0;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultAccountStatsData(): AccountStats {
|
|
||||||
return {...defaultBaseStatsData(), timesNoWinner: 0};
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultPlayerStatsData(): PlayerStats {
|
|
||||||
return defaultBaseStatsData();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default {
|
|
||||||
defaultPlayerStatsData: defaultPlayerStatsData,
|
|
||||||
defaultAccountStatsData: defaultAccountStatsData,
|
|
||||||
};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export type StoredObjectConstructor<Data, Object> = new (data: Data, ...args: any[]) => Object;
|
|
||||||
export type StoredObjectId = string;
|
|
||||||
|
|
||||||
interface StoredObject {
|
|
||||||
id(): string;
|
|
||||||
rawData(): any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StoredObject;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import StoredObject, {StoredObjectId} from "./StoredObject";
|
|
||||||
|
|
||||||
interface StoredObjectCollection<StoredObjectInterface extends StoredObject> {
|
|
||||||
findById(id: string): Promise<StoredObjectInterface | null>;
|
|
||||||
deleteById(objectId: StoredObjectId, returnObject?: boolean): Promise<StoredObjectInterface | null | void>;
|
|
||||||
save(...objects: StoredObjectInterface[]): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StoredObjectCollection;
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import Stats, {OutcomeType, PlayerGameResults, PlayerStats} from "./Stats";
|
|
||||||
import {getMongoObjectCollection} from "./utils";
|
|
||||||
import {CellValue} from "../controllers/statsController";
|
|
||||||
import mongo from "mongodb";
|
|
||||||
import {Ruleset} from "../rulesets";
|
|
||||||
import StoredObjectCollection from "./StoredObjectCollection";
|
|
||||||
import MongoStoredObjectCollection from "./MongoStoredObjectCollection";
|
|
||||||
import StoredObject from "./StoredObject";
|
|
||||||
import PlayerStatsUpdater from "../classes/PlayerStatsUpdater";
|
|
||||||
import MongoStoredObject from "./MongoStoredObject";
|
|
||||||
|
|
||||||
export interface CellDetails {
|
|
||||||
id: string;
|
|
||||||
value: CellValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StoredPlayerData {
|
|
||||||
_id: string;
|
|
||||||
nick: string;
|
|
||||||
stats: PlayerStats;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StoredPlayerCollection extends StoredObjectCollection<StoredPlayer> {
|
|
||||||
newPlayer(nick: string): Promise<StoredPlayer>;
|
|
||||||
}
|
|
||||||
|
|
||||||
class MongoStoredPlayerCollection
|
|
||||||
extends MongoStoredObjectCollection<StoredPlayerData, StoredPlayer>
|
|
||||||
implements StoredPlayerCollection {
|
|
||||||
|
|
||||||
private updater: PlayerStatsUpdater;
|
|
||||||
constructor(collectionClient: mongo.Collection) {
|
|
||||||
super(collectionClient, MongoStoredPlayer);
|
|
||||||
this.updater = new PlayerStatsUpdater();
|
|
||||||
}
|
|
||||||
|
|
||||||
async newPlayer(nick: string): Promise<StoredPlayer> {
|
|
||||||
const newPlayer = {nick, stats: Stats.makeBlankDataFields()};
|
|
||||||
return this.newObject(newPlayer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StoredPlayer extends StoredObject {
|
|
||||||
nick(): string;
|
|
||||||
setNick(newNick: string): Promise<void>;
|
|
||||||
updateStats(results: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MongoStoredPlayer extends MongoStoredObject<StoredPlayerData> implements StoredPlayer {
|
|
||||||
constructor(data: StoredPlayerData) {
|
|
||||||
super(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
nick(): string {
|
|
||||||
return this.data.nick;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setNick(newNick: string): Promise<void> {
|
|
||||||
this.data.nick = newNick;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateStats(playerGameResults: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset) {
|
|
||||||
const statsUpdater = new PlayerStatsUpdater(this.data.stats);
|
|
||||||
await statsUpdater.updateStats(playerGameResults, ruleset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const StoredPlayers = new MongoStoredPlayerCollection(getMongoObjectCollection("players"));
|
|
||||||
export default StoredPlayers;
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
import {SupportedLang} from "../enums";
|
|
||||||
import {StoredPlayer} from "./StoredPlayer";
|
|
||||||
import {AccountStats} from "./Stats";
|
|
||||||
import {SavedGameData, StoredSavedGame} from "./savedGame";
|
|
||||||
import {
|
|
||||||
GenericPersistenceError, getMongoObjectCollection,
|
|
||||||
MongoStoredObject,
|
|
||||||
MongoStoredObjectCollection,
|
|
||||||
StoredObject,
|
|
||||||
StoredObjectCollection,
|
|
||||||
StoredObjectId,
|
|
||||||
tryQuery
|
|
||||||
} from "./utils";
|
|
||||||
import mongo from "mongodb";
|
|
||||||
import StoredPlayers from "./StoredPlayer";
|
|
||||||
import bcrypt from "bcrypt";
|
|
||||||
|
|
||||||
export class CredentialsTakenError extends Error {
|
|
||||||
public emailExists: boolean;
|
|
||||||
public usernameExists: boolean;
|
|
||||||
constructor(usernameExists: boolean, emailExists: boolean) {
|
|
||||||
super("Registration failure:" + usernameExists + emailExists);
|
|
||||||
this.usernameExists = usernameExists;
|
|
||||||
this.emailExists = emailExists;
|
|
||||||
this.name = "CredentialsTakenError";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StoredUserData {
|
|
||||||
_id: string;
|
|
||||||
username: string;
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
lang: SupportedLang;
|
|
||||||
friends: string[];
|
|
||||||
player: StoredObjectId;
|
|
||||||
guests: StoredObjectId[];
|
|
||||||
accountStats: AccountStats;
|
|
||||||
savedGames: SavedGameData[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StoredUser extends StoredObject {
|
|
||||||
getLoginDetails(): Promise<LoginDetails>
|
|
||||||
preferredLang(): Promise<SupportedLang>;
|
|
||||||
getFriends(): Promise<StoredUser[]>;
|
|
||||||
getGuests(): Promise<StoredPlayer[]>;
|
|
||||||
getAccountStats(): Promise<AccountStats>;
|
|
||||||
getSavedGames(): Promise<StoredSavedGame[]>;
|
|
||||||
getMainPlayerInfo(): Promise<StoredPlayer>;
|
|
||||||
findGuestByNick(nick: string): Promise<StoredPlayer | null>;
|
|
||||||
changeLang(lang: SupportedLang): Promise<void>;
|
|
||||||
addGame(game: any): Promise<void>;
|
|
||||||
getGuestById(guestId: string): Promise<StoredPlayer>;
|
|
||||||
addGuest(nick: string): Promise<StoredPlayer>;
|
|
||||||
updateGuest(guestParams: GuestUpdateParams): Promise<StoredPlayer>;
|
|
||||||
deleteGuest(guestId: string): Promise<StoredPlayer | null>;
|
|
||||||
}
|
|
||||||
|
|
||||||
type GuestUpdateParams = { id: string, newNick: string };
|
|
||||||
export type LoginDetails = { username: string, email: string, password: string };
|
|
||||||
|
|
||||||
class MongoStoredUser extends MongoStoredObject<StoredUserData> implements StoredUser {
|
|
||||||
constructor(data: StoredUserData) {
|
|
||||||
super(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getLoginDetails(): Promise<LoginDetails> {
|
|
||||||
return {username: this.data.username, email: this.data.email, password: this.data.password};
|
|
||||||
}
|
|
||||||
|
|
||||||
async preferredLang(): Promise<SupportedLang> {
|
|
||||||
return this.data.lang;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getFriends(): Promise<StoredUser[]> {
|
|
||||||
const friends: StoredUser[] = [];
|
|
||||||
for (const friendId in this.data.guests) {
|
|
||||||
const foundFriend = await StoredUsers.findById(friendId) as StoredUser;
|
|
||||||
friends.push(foundFriend);
|
|
||||||
}
|
|
||||||
return friends;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getGuests(): Promise<StoredPlayer[]> {
|
|
||||||
const guests: StoredPlayer[] = [];
|
|
||||||
for (const guestId in this.data.guests) {
|
|
||||||
const foundGuest = await StoredPlayers.findById(guestId) as StoredPlayer;
|
|
||||||
guests.push(foundGuest);
|
|
||||||
}
|
|
||||||
return guests;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAccountStats(): Promise<AccountStats> {
|
|
||||||
return this.data.accountStats;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSavedGames(): Promise<StoredSavedGame[]> {
|
|
||||||
return this.data.savedGames.map(savedGame => new MongoStoredSavedGame(savedGame));
|
|
||||||
}
|
|
||||||
|
|
||||||
async getMainPlayerInfo(): Promise<StoredPlayer> {
|
|
||||||
return StoredPlayers.findById(this.data.player) as Promise<StoredPlayer>;
|
|
||||||
}
|
|
||||||
|
|
||||||
async findGuestByNick(nick: string): Promise<StoredPlayer | null> {
|
|
||||||
const guests = await this.getGuests();
|
|
||||||
for (const guest of guests) {
|
|
||||||
if (guest.nick() == nick) {
|
|
||||||
return guest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async changeLang(lang: SupportedLang): Promise<void> {
|
|
||||||
this.data.lang = lang;
|
|
||||||
}
|
|
||||||
|
|
||||||
async addGame(game: SavedGameData): Promise<void> {
|
|
||||||
this.data.savedGames.push(game);
|
|
||||||
}
|
|
||||||
|
|
||||||
getGuestById(guestId: string): Promise<StoredPlayer> {
|
|
||||||
return StoredPlayers.findById(guestId) as Promise<StoredPlayer>;
|
|
||||||
}
|
|
||||||
|
|
||||||
async addGuest(nick: string): Promise<StoredPlayer> {
|
|
||||||
const newGuest = await StoredPlayers.newPlayer(nick);
|
|
||||||
this.data.guests.push(newGuest.id());
|
|
||||||
return newGuest;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateGuest(guestParams: GuestUpdateParams): Promise<StoredPlayer> {
|
|
||||||
const guest = await StoredPlayers.findById(guestParams.id) as StoredPlayer;
|
|
||||||
await guest.setNick(guestParams.newNick);
|
|
||||||
await StoredPlayers.save(guest);
|
|
||||||
return guest;
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteGuest(guestId: string): Promise<StoredPlayer | null> {
|
|
||||||
return StoredPlayers.deleteById(guestId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StoredUserCollection extends StoredObjectCollection<StoredUser> {
|
|
||||||
findByEmail(emailQuery: string): Promise<StoredUser | null>;
|
|
||||||
registerUser(loginDetails: LoginDetails): Promise<StoredUser>;
|
|
||||||
userWithEmailExists(email: string): Promise<boolean>;
|
|
||||||
userWithUsernameExists(username: string): Promise<boolean>;
|
|
||||||
getSerializedAuthUser(id: string): Promise<StoredUser>;
|
|
||||||
}
|
|
||||||
|
|
||||||
class MongoStoredUserCollection extends MongoStoredObjectCollection<StoredUserData, StoredUser> implements StoredUserCollection {
|
|
||||||
constructor(collectionClient: mongo.Collection) {
|
|
||||||
super(collectionClient, MongoStoredUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
findById(id: string): Promise<StoredUser | null> {
|
|
||||||
return this.findObjectById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
findByEmail(emailQuery: string): Promise<StoredUser | null> {
|
|
||||||
return tryQuery(async () =>
|
|
||||||
await this.findObjectByAttribute("email", emailQuery)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async registerUser(loginDetails: LoginDetails): Promise<StoredUser> {
|
|
||||||
const usernameTaken = await this.userWithUsernameExists(loginDetails.username);
|
|
||||||
const emailTaken = await this.userWithEmailExists(loginDetails.email);
|
|
||||||
if (usernameTaken || emailTaken) {
|
|
||||||
throw new CredentialsTakenError(usernameTaken, emailTaken);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return this.addNewUser({...loginDetails})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async addNewUser(loginDetails: LoginDetails): Promise<StoredUser> {
|
|
||||||
const newPlayer = await StoredPlayers.newPlayer(loginDetails.username);
|
|
||||||
return tryQuery(async () =>
|
|
||||||
this.create({
|
|
||||||
username: loginDetails.username,
|
|
||||||
email: loginDetails.email,
|
|
||||||
password: await this.makePasswordSecure(loginDetails.password),
|
|
||||||
lang: SupportedLang.gb,
|
|
||||||
player: newPlayer.id()
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async userWithEmailExists(email: string): Promise<boolean> {
|
|
||||||
const object = await this.findObjectByAttribute("email", email);
|
|
||||||
return object !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async userWithUsernameExists(username: string): Promise<boolean> {
|
|
||||||
const object = await this.findObjectByAttribute("username", username);
|
|
||||||
return object !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSerializedAuthUser(id: string): Promise<StoredUser> {
|
|
||||||
const dbResult = await this.findById(id);
|
|
||||||
if (dbResult) {
|
|
||||||
return dbResult;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw new GenericPersistenceError("User not found!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async makePasswordSecure(password: string): Promise<string> {
|
|
||||||
return bcrypt.hash(password, 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const StoredUsers = new MongoStoredUserCollection(getMongoObjectCollection("users"));
|
|
||||||
export default StoredUsers;
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import passport from "passport";
|
|
||||||
import {Strategy as LocalStrategy, VerifyFunction} from "passport-local";
|
|
||||||
import bcrypt from "bcrypt";
|
|
||||||
import {NextFunction, Request, Response} from "express";
|
|
||||||
import StoredUsers, {StoredUser} from "./models/StoredUser";
|
|
||||||
|
|
||||||
export const requireAuthenticated = (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
if (req.isAuthenticated()) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
res.redirect(req.baseUrl + "/account/login");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const requireNotAuthenticated = (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
if (req.isAuthenticated()) {
|
|
||||||
res.redirect(req.app.locals.rootUrl + "/");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const authenticateUser: VerifyFunction = async (email, password, done) => {
|
|
||||||
const user = await StoredUsers.findByEmail(email);
|
|
||||||
if (!user) {
|
|
||||||
return done(null, false, { message: "A user with that email does not exist."} );
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (await bcrypt.compare(password, (await user.getLoginDetails()).password)) {
|
|
||||||
return done(null, user);
|
|
||||||
} else {
|
|
||||||
return done(null, false, {message: "Password incorrect"});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
return done(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const initialisePassport = () => {
|
|
||||||
passport.use(new LocalStrategy({ usernameField: "email" }, authenticateUser));
|
|
||||||
passport.serializeUser((user: StoredUser, done) => {
|
|
||||||
done(null, user.id())
|
|
||||||
});
|
|
||||||
passport.deserializeUser(async (id: string, done) => {
|
|
||||||
const user: StoredUser | null = await StoredUsers.getSerializedAuthUser(id);
|
|
||||||
done(null, user);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import {requireAuthenticated} from "../passport-config";
|
import {requireAuthenticated} from "../passport-config";
|
||||||
import * as statsController from "../controllers/statsController";
|
import * as statsController from "../controllers/statsController";
|
||||||
import * as dbUserController from "../controllers/dbUserController"
|
import * as dbUserController from "../controllers/kadiUserController"
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Basic User Settings
|
// Basic User Settings
|
||||||
router.get("/user", dbUserController.whoAmI);
|
router.get("/user", dbUserController.currentUserDetails);
|
||||||
router.put("/lang", requireAuthenticated, dbUserController.changeLang);
|
router.put("/lang", requireAuthenticated, dbUserController.changeLang);
|
||||||
|
|
||||||
// Guests
|
// Guests
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import express from "express";
|
import express, {NextFunction} from "express";
|
||||||
import {requireAuthenticated} from "../passport-config";
|
|
||||||
import routers from "./routers";
|
import routers from "./routers";
|
||||||
import {ModelParameterError} from "../models/utils";
|
import {LoginDetails} from "../Objects/KadiUser";
|
||||||
import {LoginDetails} from "../models/StoredUser";
|
import {requireAuthenticated} from "./routerMiddleware";
|
||||||
|
import {GenericPersistenceError, KadiError} from "../errors";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -21,16 +21,18 @@ router.get("/**", requireAuthenticated, (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const genericErrorHandler: express.ErrorRequestHandler =
|
const topLevelErrorHandler: express.ErrorRequestHandler = (err, req, res, next) => {
|
||||||
(err, req, res, next) => {
|
if (err instanceof GenericPersistenceError) {
|
||||||
if (err instanceof ModelParameterError) {
|
res.status(500).send({message: "An internal error occurred accessing the database."});
|
||||||
res.status(500).send({message: "An internal error occurred in the database."});
|
}
|
||||||
|
else if (err instanceof KadiError) {
|
||||||
|
res.status(500).send({message: "An error occurred in the app."});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
res.status(500).send({message: "An unknown error occurred."});
|
res.status(500).send({message: "An unknown error occurred."});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
router.use(genericErrorHandler);
|
router.use(topLevelErrorHandler);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
19
src/routers/routerMiddleware.ts
Normal file
19
src/routers/routerMiddleware.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import {NextFunction, Request, Response} from "express";
|
||||||
|
|
||||||
|
export const requireAuthenticated = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
if (req.isAuthenticated()) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
res.redirect(req.baseUrl + "/account/login");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requireNotAuthenticated = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
if (req.isAuthenticated()) {
|
||||||
|
res.redirect(req.app.locals.rootUrl + "/");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -71,8 +71,7 @@ interface DefaultCellDef {
|
|||||||
export const DEFAULT_RULESET_NAME = "DEFAULT_RULESET";
|
export const DEFAULT_RULESET_NAME = "DEFAULT_RULESET";
|
||||||
const defaultDiceCount = 5;
|
const defaultDiceCount = 5;
|
||||||
|
|
||||||
const gameSchemas: Ruleset[] = [
|
export const DEFAULT_RULESET: Ruleset = {
|
||||||
{
|
|
||||||
id: DEFAULT_RULESET_NAME,
|
id: DEFAULT_RULESET_NAME,
|
||||||
label: "Standard Kadi Rules (en)",
|
label: "Standard Kadi Rules (en)",
|
||||||
blocks: {
|
blocks: {
|
||||||
@@ -171,23 +170,4 @@ const gameSchemas: Ruleset[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
];
|
|
||||||
|
|
||||||
export function getGameSchemaById(schemaId: string): Ruleset {
|
|
||||||
for (const schema of gameSchemas) {
|
|
||||||
if (schema.id === schemaId) {
|
|
||||||
return schema;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new RangeError("No such GameSchema with id '" + schemaId + "'!");
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SchemaListing {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSchemaListings(): SchemaListing[] {
|
|
||||||
return gameSchemas.map((s) => ({id: s.id, label: s.label}));
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user