diff --git a/src/ObjectCollections/KadiUserCollection.ts b/src/ObjectCollections/KadiUserCollection.ts new file mode 100644 index 0000000..3103fd2 --- /dev/null +++ b/src/ObjectCollections/KadiUserCollection.ts @@ -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 { + 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 { + const foundUser = await this.mongoRead(id); + if (foundUser) { + return this.storedUserFrom(foundUser); + } + else { + return null; + } + } + + async findByEmail(emailQuery: string): Promise { + const foundUser = await this.mongoFindByAttribute("email", emailQuery); + if (foundUser) { + return this.storedUserFrom(foundUser); + } + else { + return null; + } + } + + async registerUser(loginDetails: LoginDetails): Promise { + 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 { + 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 { + const object = await this.mongoFindByAttribute("email", email); + return object !== null; + } + + async userWithUsernameExists(username: string): Promise { + const object = await this.mongoFindByAttribute("username", username); + return object !== null; + } + + async getSerializedAuthUser(id: string): Promise { + const foundUser = await this.mongoRead(id); + if (foundUser) { + return this.storedUserFrom(foundUser); + } + else { + throw new GenericPersistenceError("User not found!"); + } + } + + async makePasswordSecure(password: string): Promise { + return bcrypt.hash(password, 10); + } +} + +const KadiUserCollectionSingleton = new KadiUserCollection(getMongoObjectCollection("users")); +export default KadiUserCollectionSingleton; \ No newline at end of file diff --git a/src/ObjectCollections/MongoStoredObjectCollection.ts b/src/ObjectCollections/MongoStoredObjectCollection.ts new file mode 100644 index 0000000..96c6194 --- /dev/null +++ b/src/ObjectCollections/MongoStoredObjectCollection.ts @@ -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 { + + protected constructor(protected mongoDbClientCollection: mongo.Collection) {} + + protected async mongoCreate(objectData: Omit): Promise { + 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 { + return tryQuery(async () => + await this.mongoDbClientCollection.findOne({_id: id}) + ); + } + + protected async mongoFindByAttribute(attribute: string, value: any): Promise { + return tryQuery(async () => + await this.mongoDbClientCollection.findOne({[attribute]: value}) + ); + } + + protected async mongoDelete(objectId: ActiveRecordId, returnObject?: boolean): Promise { + 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; \ No newline at end of file diff --git a/src/ObjectCollections/PlayerCollection.ts b/src/ObjectCollections/PlayerCollection.ts new file mode 100644 index 0000000..c01c891 --- /dev/null +++ b/src/ObjectCollections/PlayerCollection.ts @@ -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 { + 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 { + const newPlayer = {nick, stats: defaultPlayerStatsMongoData()}; + return this.storedPlayerFrom(await this.mongoCreate(newPlayer)); + } +} + +const StoredPlayers = new PlayerCollection(getMongoObjectCollection("players")); +export default StoredPlayers; \ No newline at end of file diff --git a/src/models/ruleset.ts b/src/ObjectCollections/ruleset.ts similarity index 100% rename from src/models/ruleset.ts rename to src/ObjectCollections/ruleset.ts diff --git a/src/classes/AccountStatsUpdater.ts b/src/Objects/AccountStats.ts similarity index 73% rename from src/classes/AccountStatsUpdater.ts rename to src/Objects/AccountStats.ts index 89d9a7d..618be8a 100644 --- a/src/classes/AccountStatsUpdater.ts +++ b/src/Objects/AccountStats.ts @@ -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); } @@ -28,4 +28,6 @@ export class AccountStatsUpdater { to analyse.`); } } -} \ No newline at end of file +} + +export default AccountStats; \ No newline at end of file diff --git a/src/Objects/ActiveRecord.ts b/src/Objects/ActiveRecord.ts new file mode 100644 index 0000000..236f2db --- /dev/null +++ b/src/Objects/ActiveRecord.ts @@ -0,0 +1,7 @@ +export type ActiveRecordId = string; + +interface ActiveRecord { + getId(): ActiveRecordId; +} + +export default ActiveRecord; \ No newline at end of file diff --git a/src/Objects/DefaultStatsMongoData.ts b/src/Objects/DefaultStatsMongoData.ts new file mode 100755 index 0000000..5d6d41f --- /dev/null +++ b/src/Objects/DefaultStatsMongoData.ts @@ -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 + gamesPlayed: number; +} +export interface RulesetStatsMongoData { + blockStats: Record; + wins: number; + runnerUps: number; + draws: number; + losses: number; + grandTotal: TotalFieldStatsMongoData; +} +export interface BlockStatsMongoData { + cellStats: Record; + 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; +} +export interface BlockResults { + cells: Record +} +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, hasBonus: boolean): BlockStatsMongoData { + const cellStatsRecord: Record = {}; + 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 = {}; + 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(); +} \ No newline at end of file diff --git a/src/Objects/KadiUser.ts b/src/Objects/KadiUser.ts new file mode 100644 index 0000000..74d7dea --- /dev/null +++ b/src/Objects/KadiUser.ts @@ -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; \ No newline at end of file diff --git a/src/Objects/Player.ts b/src/Objects/Player.ts new file mode 100755 index 0000000..c350d5b --- /dev/null +++ b/src/Objects/Player.ts @@ -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 { + 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; \ No newline at end of file diff --git a/src/classes/PlayerStatsUpdater.ts b/src/Objects/PlayerStats.ts similarity index 58% rename from src/classes/PlayerStatsUpdater.ts rename to src/Objects/PlayerStats.ts index f537d67..8bad20f 100644 --- a/src/classes/PlayerStatsUpdater.ts +++ b/src/Objects/PlayerStats.ts @@ -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; \ No newline at end of file +export default PlayerStats; \ No newline at end of file diff --git a/src/classes/ScoreBlockCalculator.ts b/src/Objects/ScoreBlockCalculator.ts similarity index 100% rename from src/classes/ScoreBlockCalculator.ts rename to src/Objects/ScoreBlockCalculator.ts diff --git a/src/classes/ScoreCalculator.ts b/src/Objects/ScoreCalculator.ts similarity index 100% rename from src/classes/ScoreCalculator.ts rename to src/Objects/ScoreCalculator.ts diff --git a/src/classes/ScoreCellCalculator.ts b/src/Objects/ScoreCellCalculator.ts similarity index 100% rename from src/classes/ScoreCellCalculator.ts rename to src/Objects/ScoreCellCalculator.ts diff --git a/src/classes/StatsUpdater.ts b/src/Objects/StatsUpdater.ts similarity index 85% rename from src/classes/StatsUpdater.ts rename to src/Objects/StatsUpdater.ts index 785f2a3..d771da1 100644 --- a/src/classes/StatsUpdater.ts +++ b/src/Objects/StatsUpdater.ts @@ -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]; } } diff --git a/src/models/savedGame.ts b/src/Objects/savedGame.ts similarity index 100% rename from src/models/savedGame.ts rename to src/Objects/savedGame.ts diff --git a/src/controllers/dbUserController.ts b/src/controllers/dbUserController.ts deleted file mode 100755 index 8fa68bf..0000000 --- a/src/controllers/dbUserController.ts +++ /dev/null @@ -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}); -}; \ No newline at end of file diff --git a/src/controllers/kadiUserController.ts b/src/controllers/kadiUserController.ts new file mode 100755 index 0000000..ac725c0 --- /dev/null +++ b/src/controllers/kadiUserController.ts @@ -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}); +}; \ No newline at end of file diff --git a/src/controllers/signupController.ts b/src/controllers/signupController.ts index 3b8cef5..7d442ac 100755 --- a/src/controllers/signupController.ts +++ b/src/controllers/signupController.ts @@ -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; @@ -48,4 +51,30 @@ export const registerNewUser: RequestHandler = async (req, res) => { export const logoutUser: RequestHandler = (req, res) => { req.logout(); res.redirect(req.baseUrl + "/login"); -}; \ No newline at end of file +}; + +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 { + done(null, user.getId()); +} + +export async function deserializeKadiUser(id: string, done: (err: any, id?: unknown) => void): Promise { + const user: KadiUser | null = await KadiUserCollection.getSerializedAuthUser(id); + done(null, user); +} \ No newline at end of file diff --git a/src/models/utils.ts b/src/database.ts similarity index 88% rename from src/models/utils.ts rename to src/database.ts index bdf755a..c90d072 100644 --- a/src/models/utils.ts +++ b/src/database.ts @@ -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 = (query: () => T) => Promise; - export const tryQuery: CallbackWrapper = async (cb) => { try { return cb(); diff --git a/src/errors.ts b/src/errors.ts index b75fb38..e0d75b6 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -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"; + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index f2f296b..d1eff1a 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,48 +1,41 @@ -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!"); - } -}); +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); + 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 + secret: "secret", + saveUninitialized: false, + resave: false + })); + app.locals = { + 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")); + }); +} -// Express app config -const app = express(); -app.use(express.json()); -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 - secret: "secret", - saveUninitialized: false, - resave: false -})); -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")); -}); \ No newline at end of file +startApp(); \ No newline at end of file diff --git a/src/models/MongoStoredObject.ts b/src/models/MongoStoredObject.ts deleted file mode 100644 index fbbedbe..0000000 --- a/src/models/MongoStoredObject.ts +++ /dev/null @@ -1,15 +0,0 @@ -import StoredObject from "./StoredObject"; - -abstract class MongoStoredObject implements StoredObject { - protected constructor(protected data: {_id: string} & RawDataInterface) {} - - id(): string { - return this.data._id; - } - - rawData(): RawDataInterface { - return this.data; - } -} - -export default MongoStoredObject; \ No newline at end of file diff --git a/src/models/MongoStoredObjectCollection.ts b/src/models/MongoStoredObjectCollection.ts deleted file mode 100644 index 52801d0..0000000 --- a/src/models/MongoStoredObjectCollection.ts +++ /dev/null @@ -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 - implements StoredObjectCollection { - - protected mongoDbClientCollection: mongo.Collection; - protected StoredObjectConstructor: StoredObjectConstructor; - - protected constructor( - collectionClient: mongo.Collection, - objectConstructor: StoredObjectConstructor - ) { - this.mongoDbClientCollection = collectionClient; - this.StoredObjectConstructor = objectConstructor; - } - - private async create(objectData: Omit): Promise { - 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): Promise { - return new this.StoredObjectConstructor(await this.create(objectData)); - } - - protected async findObjectById(id: string): Promise { - return tryQuery(async () => - await this.mongoDbClientCollection.findOne({_id: id}) - ); - } - - protected async findObjectByAttribute(attribute: string, value: any): Promise { - return tryQuery(async () => - await this.mongoDbClientCollection.findOne({[attribute]: value}) - ); - } - - async deleteById(objectId: StoredObjectId, returnObject?: boolean): Promise { - 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 { - const data = await this.findObjectById(id); - if (data) { - return new this.StoredObjectConstructor(data); - } - else { - return null; - } - }; - - async save(...objects: IStoredObject[]): Promise { - await tryQuery(async () => { - for (const object of objects) { - await this.mongoDbClientCollection.findOneAndUpdate({_id: object.id()}, {...object.rawData()}); - } - }); - } -} - -export default MongoStoredObjectCollection; \ No newline at end of file diff --git a/src/models/Stats.ts b/src/models/Stats.ts deleted file mode 100755 index a431811..0000000 --- a/src/models/Stats.ts +++ /dev/null @@ -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 & { [DEFAULT_RULESET_NAME]: DefaultRulesetStats } - gamesPlayed: number; -} -export interface RulesetStats { - blockStats: Record; - wins: number; - runnerUps: number; - draws: number; - losses: number; - grandTotal: TotalFieldStats; -} -interface BlockStats { - cellStats: Record; - 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; -} -interface BlockResults { - cells: Record -} -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, -}; \ No newline at end of file diff --git a/src/models/StoredObject.ts b/src/models/StoredObject.ts deleted file mode 100644 index 8b1bbb7..0000000 --- a/src/models/StoredObject.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type StoredObjectConstructor = new (data: Data, ...args: any[]) => Object; -export type StoredObjectId = string; - -interface StoredObject { - id(): string; - rawData(): any; -} - -export default StoredObject; \ No newline at end of file diff --git a/src/models/StoredObjectCollection.ts b/src/models/StoredObjectCollection.ts deleted file mode 100644 index 9be9334..0000000 --- a/src/models/StoredObjectCollection.ts +++ /dev/null @@ -1,9 +0,0 @@ -import StoredObject, {StoredObjectId} from "./StoredObject"; - -interface StoredObjectCollection { - findById(id: string): Promise; - deleteById(objectId: StoredObjectId, returnObject?: boolean): Promise; - save(...objects: StoredObjectInterface[]): Promise; -} - -export default StoredObjectCollection; \ No newline at end of file diff --git a/src/models/StoredPlayer.ts b/src/models/StoredPlayer.ts deleted file mode 100755 index 7eb1cfa..0000000 --- a/src/models/StoredPlayer.ts +++ /dev/null @@ -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 { - newPlayer(nick: string): Promise; -} - -class MongoStoredPlayerCollection - extends MongoStoredObjectCollection - implements StoredPlayerCollection { - - private updater: PlayerStatsUpdater; - constructor(collectionClient: mongo.Collection) { - super(collectionClient, MongoStoredPlayer); - this.updater = new PlayerStatsUpdater(); - } - - async newPlayer(nick: string): Promise { - const newPlayer = {nick, stats: Stats.makeBlankDataFields()}; - return this.newObject(newPlayer); - } -} - -export interface StoredPlayer extends StoredObject { - nick(): string; - setNick(newNick: string): Promise; - updateStats(results: PlayerGameResults & {outcome: OutcomeType}, ruleset: Ruleset): Promise; -} - -export class MongoStoredPlayer extends MongoStoredObject implements StoredPlayer { - constructor(data: StoredPlayerData) { - super(data); - } - - nick(): string { - return this.data.nick; - } - - async setNick(newNick: string): Promise { - 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; \ No newline at end of file diff --git a/src/models/StoredUser.ts b/src/models/StoredUser.ts deleted file mode 100644 index 06fc979..0000000 --- a/src/models/StoredUser.ts +++ /dev/null @@ -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 - preferredLang(): Promise; - getFriends(): Promise; - getGuests(): Promise; - getAccountStats(): Promise; - getSavedGames(): Promise; - getMainPlayerInfo(): Promise; - findGuestByNick(nick: string): Promise; - changeLang(lang: SupportedLang): Promise; - addGame(game: any): Promise; - getGuestById(guestId: string): Promise; - addGuest(nick: string): Promise; - updateGuest(guestParams: GuestUpdateParams): Promise; - deleteGuest(guestId: string): Promise; -} - -type GuestUpdateParams = { id: string, newNick: string }; -export type LoginDetails = { username: string, email: string, password: string }; - -class MongoStoredUser extends MongoStoredObject implements StoredUser { - constructor(data: StoredUserData) { - super(data); - } - - async getLoginDetails(): Promise { - return {username: this.data.username, email: this.data.email, password: this.data.password}; - } - - async preferredLang(): Promise { - return this.data.lang; - } - - async getFriends(): Promise { - 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 { - 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 { - return this.data.accountStats; - } - - async getSavedGames(): Promise { - return this.data.savedGames.map(savedGame => new MongoStoredSavedGame(savedGame)); - } - - async getMainPlayerInfo(): Promise { - return StoredPlayers.findById(this.data.player) as Promise; - } - - async findGuestByNick(nick: string): Promise { - const guests = await this.getGuests(); - for (const guest of guests) { - if (guest.nick() == nick) { - return guest; - } - } - return null; - } - - async changeLang(lang: SupportedLang): Promise { - this.data.lang = lang; - } - - async addGame(game: SavedGameData): Promise { - this.data.savedGames.push(game); - } - - getGuestById(guestId: string): Promise { - return StoredPlayers.findById(guestId) as Promise; - } - - async addGuest(nick: string): Promise { - const newGuest = await StoredPlayers.newPlayer(nick); - this.data.guests.push(newGuest.id()); - return newGuest; - } - - async updateGuest(guestParams: GuestUpdateParams): Promise { - 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 { - return StoredPlayers.deleteById(guestId); - } -} - -export interface StoredUserCollection extends StoredObjectCollection { - findByEmail(emailQuery: string): Promise; - registerUser(loginDetails: LoginDetails): Promise; - userWithEmailExists(email: string): Promise; - userWithUsernameExists(username: string): Promise; - getSerializedAuthUser(id: string): Promise; -} - -class MongoStoredUserCollection extends MongoStoredObjectCollection implements StoredUserCollection { - constructor(collectionClient: mongo.Collection) { - super(collectionClient, MongoStoredUser); - } - - findById(id: string): Promise { - return this.findObjectById(id); - } - - findByEmail(emailQuery: string): Promise { - return tryQuery(async () => - await this.findObjectByAttribute("email", emailQuery) - ); - } - - async registerUser(loginDetails: LoginDetails): Promise { - 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 { - 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 { - const object = await this.findObjectByAttribute("email", email); - return object !== null; - } - - async userWithUsernameExists(username: string): Promise { - const object = await this.findObjectByAttribute("username", username); - return object !== null; - } - - async getSerializedAuthUser(id: string): Promise { - const dbResult = await this.findById(id); - if (dbResult) { - return dbResult; - } - else { - throw new GenericPersistenceError("User not found!"); - } - } - - async makePasswordSecure(password: string): Promise { - return bcrypt.hash(password, 10); - } -} - -const StoredUsers = new MongoStoredUserCollection(getMongoObjectCollection("users")); -export default StoredUsers; \ No newline at end of file diff --git a/src/passport-config.ts b/src/passport-config.ts deleted file mode 100755 index ecf1299..0000000 --- a/src/passport-config.ts +++ /dev/null @@ -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); - }); -}; \ No newline at end of file diff --git a/src/routers/apiRouter.ts b/src/routers/apiRouter.ts index f62e53f..47e2bc7 100755 --- a/src/routers/apiRouter.ts +++ b/src/routers/apiRouter.ts @@ -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 diff --git a/src/routers/mainRouter.ts b/src/routers/mainRouter.ts index 8c44ac2..f633118 100755 --- a/src/routers/mainRouter.ts +++ b/src/routers/mainRouter.ts @@ -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."}); - } - else { - res.status(500).send({message: "An unknown error occurred."}); - } - }; +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; \ No newline at end of file diff --git a/src/routers/routerMiddleware.ts b/src/routers/routerMiddleware.ts new file mode 100644 index 0000000..3bcd453 --- /dev/null +++ b/src/routers/routerMiddleware.ts @@ -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(); + } +}; \ No newline at end of file diff --git a/src/rulesets.ts b/src/rulesets.ts index e6c2b80..ebe4c1f 100755 --- a/src/rulesets.ts +++ b/src/rulesets.ts @@ -71,123 +71,103 @@ interface DefaultCellDef { export const DEFAULT_RULESET_NAME = "DEFAULT_RULESET"; const defaultDiceCount = 5; -const gameSchemas: Ruleset[] = [ - { - id: DEFAULT_RULESET_NAME, - label: "Standard Kadi Rules (en)", - blocks: { - top: { - label: "Upper", - hasBonus: true, - bonusScore: 35, - bonusFor: 63, - cells: { - aces: { - fieldType: FieldType.multiplier, - label: "Aces", - multiplier: 1, - maxMultiples: defaultDiceCount, - }, - - twos: { - fieldType: FieldType.multiplier, - label: "Twos", - multiplier: 2, - maxMultiples: defaultDiceCount, - }, - - threes: { - fieldType: FieldType.multiplier, - label: "Threes", - multiplier: 3, - maxMultiples: defaultDiceCount, - }, - - fours: { - fieldType: FieldType.multiplier, - label: "Fours", - multiplier: 4, - maxMultiples: defaultDiceCount, - }, - - fives: { - fieldType: FieldType.multiplier, - label: "Fives", - multiplier: 5, - maxMultiples: defaultDiceCount, - }, - - sixes: { - fieldType: FieldType.multiplier, - label: "Sixes", - multiplier: 6, - maxMultiples: defaultDiceCount, - }, +export const DEFAULT_RULESET: Ruleset = { + id: DEFAULT_RULESET_NAME, + label: "Standard Kadi Rules (en)", + blocks: { + top: { + label: "Upper", + hasBonus: true, + bonusScore: 35, + bonusFor: 63, + cells: { + aces: { + fieldType: FieldType.multiplier, + label: "Aces", + multiplier: 1, + maxMultiples: defaultDiceCount, }, - }, - bottom: { - label: "Lower", - hasBonus: false, - cells: { - threeKind: { - fieldType: FieldType.number, - label: "Three of a Kind", - }, - fourKind: { - fieldType: FieldType.number, - label: "Four of a Kind", - }, + twos: { + fieldType: FieldType.multiplier, + label: "Twos", + multiplier: 2, + maxMultiples: defaultDiceCount, + }, - fullHouse: { - fieldType: FieldType.bool, - label: "Full House", - score: 25, - }, + threes: { + fieldType: FieldType.multiplier, + label: "Threes", + multiplier: 3, + maxMultiples: defaultDiceCount, + }, - smlStraight: { - fieldType: FieldType.bool, - label: "Small Straight", - score: 30, - }, + fours: { + fieldType: FieldType.multiplier, + label: "Fours", + multiplier: 4, + maxMultiples: defaultDiceCount, + }, - lgSraight: { - fieldType: FieldType.bool, - label: "Large Straight", - score: 40, - }, + fives: { + fieldType: FieldType.multiplier, + label: "Fives", + multiplier: 5, + maxMultiples: defaultDiceCount, + }, - superkadi: { - fieldType: FieldType.superkadi, - label: "Super Kadis", - score: 50, - maxSuperkadis: 5, - }, - - chance: { - fieldType: FieldType.number, - label: "Chance", - }, + sixes: { + fieldType: FieldType.multiplier, + label: "Sixes", + multiplier: 6, + maxMultiples: defaultDiceCount, }, }, }, - } -]; + bottom: { + label: "Lower", + hasBonus: false, + cells: { + threeKind: { + fieldType: FieldType.number, + label: "Three of a Kind", + }, -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 + "'!"); -} + fourKind: { + fieldType: FieldType.number, + label: "Four of a Kind", + }, -export interface SchemaListing { - id: string; - label: string; -} + fullHouse: { + fieldType: FieldType.bool, + label: "Full House", + score: 25, + }, -export function getSchemaListings(): SchemaListing[] { - return gameSchemas.map((s) => ({id: s.id, label: s.label})); -} + smlStraight: { + fieldType: FieldType.bool, + label: "Small Straight", + score: 30, + }, + + lgSraight: { + fieldType: FieldType.bool, + label: "Large Straight", + score: 40, + }, + + superkadi: { + fieldType: FieldType.superkadi, + label: "Super Kadis", + score: 50, + maxSuperkadis: 5, + }, + + chance: { + fieldType: FieldType.number, + label: "Chance", + }, + }, + }, + }, +}; \ No newline at end of file