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:
Daniel Ledda
2020-07-17 11:40:53 +02:00
parent cef6249c09
commit 4542036b77
33 changed files with 784 additions and 852 deletions

View 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;

View 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;

View 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;

View File

@@ -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;

View File

@@ -0,0 +1,7 @@
export type ActiveRecordId = string;
interface ActiveRecord {
getId(): ActiveRecordId;
}
export default ActiveRecord;

View 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
View 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
View 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;

View File

@@ -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;

View File

@@ -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];
} }
} }

View File

@@ -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});
};

View 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});
};

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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";
}
}

View File

@@ -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"));
});

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
});
};

View File

@@ -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

View File

@@ -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;

View 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();
}
};

View File

@@ -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}));
}