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 {AccountStats, OutcomeType, PlayerGameResults} from "../models/Stats";
|
||||
import {AccountStatsMongoData, OutcomeType, PlayerGameResults} from "./DefaultStatsMongoData";
|
||||
import {UpdateError} from "../errors";
|
||||
import StatsUpdater from "./StatsUpdater";
|
||||
|
||||
export class AccountStatsUpdater {
|
||||
private data?: AccountStats;
|
||||
class AccountStats {
|
||||
private data?: AccountStatsMongoData;
|
||||
private readonly updater: StatsUpdater;
|
||||
constructor(data?: AccountStats) {
|
||||
constructor(data?: AccountStatsMongoData) {
|
||||
if (data) {
|
||||
this.data = data;
|
||||
}
|
||||
this.updater = new StatsUpdater();
|
||||
}
|
||||
|
||||
use(data: AccountStats) {
|
||||
use(data: AccountStatsMongoData) {
|
||||
this.data = 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 StatsUpdater, {OutcomeType, PlayerGameResults, PlayerStats} from "../models/Stats";
|
||||
import {OutcomeType, PlayerGameResults, PlayerStatsMongoData} from "./DefaultStatsMongoData";
|
||||
import StatsUpdater from "./StatsUpdater";
|
||||
|
||||
class PlayerStatsUpdater {
|
||||
private data?: PlayerStats;
|
||||
class PlayerStats {
|
||||
private data?: PlayerStatsMongoData;
|
||||
private readonly updater: StatsUpdater;
|
||||
constructor(data?: PlayerStats) {
|
||||
constructor(data?: PlayerStatsMongoData) {
|
||||
if (data) {
|
||||
this.data = data;
|
||||
}
|
||||
this.updater = new StatsUpdater();
|
||||
}
|
||||
|
||||
use(data: PlayerStats) {
|
||||
use(data: PlayerStatsMongoData) {
|
||||
this.data = 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 {FieldType} from "../enums";
|
||||
import {
|
||||
BaseStats,
|
||||
BestableFieldStats,
|
||||
BoolFieldStats,
|
||||
CellStats,
|
||||
BaseStatsMongoData,
|
||||
BestableFieldStatsMongoData,
|
||||
BoolFieldStatsMongoData,
|
||||
CellStatsMongoData,
|
||||
OutcomeType,
|
||||
PlayerGameResults, RulesetStats,
|
||||
TotalFieldStats
|
||||
} from "../models/Stats";
|
||||
PlayerGameResults, RulesetStatsMongoData,
|
||||
TotalFieldStatsMongoData
|
||||
} from "./DefaultStatsMongoData";
|
||||
|
||||
class StatsUpdater {
|
||||
private data?: BaseStats;
|
||||
private data?: BaseStatsMongoData;
|
||||
private validationRuleset?: Ruleset;
|
||||
private calculator?: ScoreCalculator;
|
||||
private currentStatsObject?: RulesetStats;
|
||||
private currentStatsObject?: RulesetStatsMongoData;
|
||||
constructor() {
|
||||
}
|
||||
|
||||
use(data: BaseStats) {
|
||||
use(data: BaseStatsMongoData) {
|
||||
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.worst = total < statsObject.worst ? total : statsObject.worst;
|
||||
statsObject.runningTotal += total;
|
||||
@@ -69,10 +69,10 @@ class StatsUpdater {
|
||||
const cellFieldType = this.validationRuleset?.blocks[ids.blockId].cells[ids.cellId].fieldType;
|
||||
const cellScore = this.calculator!.getCellScoreByLocation({...ids});
|
||||
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) {
|
||||
const bestableStats = (cellStats as BestableFieldStats);
|
||||
const bestableStats = (cellStats as BestableFieldStatsMongoData);
|
||||
if (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];
|
||||
}
|
||||
}
|
||||
@@ -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 {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) => {
|
||||
res.render("login.ejs");
|
||||
@@ -21,7 +24,7 @@ export const showRegistrationPage: RequestHandler = (req, res) => {
|
||||
export const registerNewUser: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const loginDetails: LoginDetails = req.body as LoginDetails;
|
||||
const newUser = await StoredUsers.registerUser(loginDetails);
|
||||
const newUser = await KadiUserCollection.registerUser(loginDetails);
|
||||
req.login(newUser, (err) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
@@ -49,3 +52,29 @@ export const logoutUser: RequestHandler = (req, res) => {
|
||||
req.logout();
|
||||
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 Settings from "../server-config.json";
|
||||
import {GenericPersistenceError, MongoError} from "../errors";
|
||||
import Settings from "./server-config.json";
|
||||
import {GenericPersistenceError, MongoError} from "./errors";
|
||||
|
||||
let SessionDbClient: Db;
|
||||
|
||||
export async function initMongoSessionClient() {
|
||||
if (SessionDbClient === undefined) {
|
||||
const client = await MongoClient.connect(Settings.mongodb_uri, {useUnifiedTopology: true});
|
||||
@@ -10,6 +11,7 @@ export async function initMongoSessionClient() {
|
||||
}
|
||||
return SessionDbClient;
|
||||
}
|
||||
|
||||
export function getMongoObjectCollection(collectionName: string) {
|
||||
if (SessionDbClient === undefined) {
|
||||
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>;
|
||||
|
||||
export const tryQuery: CallbackWrapper = async (cb) => {
|
||||
try {
|
||||
return cb();
|
||||
@@ -27,3 +27,14 @@ export class ModelParameterError extends GenericPersistenceError {
|
||||
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";
|
||||
}
|
||||
}
|
||||
35
src/index.ts
35
src/index.ts
@@ -1,23 +1,18 @@
|
||||
import express from "express";
|
||||
import {initialisePassport, requireAuthenticated, requireNotAuthenticated} from "./passport-config";
|
||||
import mongoose from "mongoose";
|
||||
import express, {NextFunction, Request, Response} from "express";
|
||||
import Settings from "./server-config.json";
|
||||
import flash from "express-flash";
|
||||
import passport from "passport";
|
||||
import session from "express-session";
|
||||
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
|
||||
mongoose.connect(Settings.mongodb_uri, (err: any) => {
|
||||
if (err) {
|
||||
console.log(err.message);
|
||||
}
|
||||
else {
|
||||
console.log("Successfully connected to mongoDB!");
|
||||
}
|
||||
});
|
||||
|
||||
// Express app config
|
||||
async function startApp() {
|
||||
await initMongoSessionClient();
|
||||
passport.use(new LocalStrategy({ usernameField: "email" }, authenticateKadiUser));
|
||||
passport.serializeUser(serializeKadiUser);
|
||||
passport.deserializeUser(deserializeKadiUser);
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.set("port", process.env.PORT || 3000);
|
||||
@@ -34,15 +29,13 @@ app.use(session({
|
||||
app.locals = {
|
||||
rootUrl: Settings.serverRoot
|
||||
};
|
||||
|
||||
// Passport init
|
||||
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"));
|
||||
app.listen(app.get("port"), () => {
|
||||
console.log("Kadi running on http://localhost:%d", app.get("port"));
|
||||
});
|
||||
}
|
||||
|
||||
startApp();
|
||||
@@ -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 {requireAuthenticated} from "../passport-config";
|
||||
import * as statsController from "../controllers/statsController";
|
||||
import * as dbUserController from "../controllers/dbUserController"
|
||||
import * as dbUserController from "../controllers/kadiUserController"
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Basic User Settings
|
||||
router.get("/user", dbUserController.whoAmI);
|
||||
router.get("/user", dbUserController.currentUserDetails);
|
||||
router.put("/lang", requireAuthenticated, dbUserController.changeLang);
|
||||
|
||||
// Guests
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import express from "express";
|
||||
import {requireAuthenticated} from "../passport-config";
|
||||
import express, {NextFunction} from "express";
|
||||
import routers from "./routers";
|
||||
import {ModelParameterError} from "../models/utils";
|
||||
import {LoginDetails} from "../models/StoredUser";
|
||||
import {LoginDetails} from "../Objects/KadiUser";
|
||||
import {requireAuthenticated} from "./routerMiddleware";
|
||||
import {GenericPersistenceError, KadiError} from "../errors";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -21,16 +21,18 @@ router.get("/**", requireAuthenticated, (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
const genericErrorHandler: express.ErrorRequestHandler =
|
||||
(err, req, res, next) => {
|
||||
if (err instanceof ModelParameterError) {
|
||||
res.status(500).send({message: "An internal error occurred in the database."});
|
||||
const topLevelErrorHandler: express.ErrorRequestHandler = (err, req, res, next) => {
|
||||
if (err instanceof GenericPersistenceError) {
|
||||
res.status(500).send({message: "An internal error occurred accessing the database."});
|
||||
}
|
||||
else if (err instanceof KadiError) {
|
||||
res.status(500).send({message: "An error occurred in the app."});
|
||||
}
|
||||
else {
|
||||
res.status(500).send({message: "An unknown error occurred."});
|
||||
}
|
||||
};
|
||||
|
||||
router.use(genericErrorHandler);
|
||||
router.use(topLevelErrorHandler);
|
||||
|
||||
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";
|
||||
const defaultDiceCount = 5;
|
||||
|
||||
const gameSchemas: Ruleset[] = [
|
||||
{
|
||||
export const DEFAULT_RULESET: Ruleset = {
|
||||
id: DEFAULT_RULESET_NAME,
|
||||
label: "Standard Kadi Rules (en)",
|
||||
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