From 4ccbdf599edbf450a5b891dcbf7af9e2a2498d0f Mon Sep 17 00:00:00 2001 From: Daniel Ledda Date: Mon, 25 May 2020 22:57:36 +0200 Subject: [PATCH] Added Crud Rest API for guest players, unknown players in saved games are automatically added as guests. Games are now as such saved correctly. More decoupling from models. Updated express for better error handling. --- .idea/vcs.xml | 4 +- package-lock.json | 112 ++++++++++------ package.json | 24 ++-- src/controllers/dbUserController.ts | 91 ++++++++++++- src/controllers/statsController.ts | 33 ++++- src/index.ts | 4 - src/models/dbUser.ts | 191 +++++++++++++++++++++++----- src/models/player.ts | 7 +- src/models/savedGame.ts | 39 ++++-- src/models/stats.ts | 29 +---- src/models/utils.ts | 41 ++++++ src/passport-config.ts | 8 +- src/routers/apiRouter.ts | 23 +++- src/routers/mainRouter.ts | 15 ++- src/server-config.json | 2 +- 15 files changed, 483 insertions(+), 140 deletions(-) create mode 100644 src/models/utils.ts diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 0ca43fb..46fc5c8 100755 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,9 +2,7 @@ - - - + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a27f03a..8c7af84 100755 --- a/package-lock.json +++ b/package-lock.json @@ -133,9 +133,9 @@ "dev": true }, "@types/mongodb": { - "version": "3.5.16", - "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.5.16.tgz", - "integrity": "sha512-q12k9vFEGfQTUTC9KiN+Lf1nPpEawuPyTfIHBgGn+lSD/4ICZhePxQqEe/ukRgAjS63vdc+Td758VBr4bUGNjg==", + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.5.18.tgz", + "integrity": "sha512-fEmnRmwXt4pEFhqWB/ZlyNaDhLfQv5GALaZAlH9Pb0kEttvsCr66umJ9pfBEEP3ks1hjlwAuMtqk/+DyZDLAXQ==", "dev": true, "requires": { "@types/bson": "*", @@ -143,9 +143,9 @@ } }, "@types/mongoose": { - "version": "5.7.16", - "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.7.16.tgz", - "integrity": "sha512-QuWb8Tqjq1r/ZEpBi9MBWpuu8upe+4Co89GExyIFb0Q7TCmeMQsxG1lVfkmjk8GVm/qMIwUMdpuopVLPhmnFUA==", + "version": "5.7.21", + "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.7.21.tgz", + "integrity": "sha512-KPJ4zVHWZK5vKlnJj2YDDCTmesYkW9iGB95ATfFYcdvx394Xuyh9/Cq0NSJnJ8J8PFVlhx9iBQ+Q425tEuEJvA==", "dev": true, "requires": { "@types/mongodb": "*", @@ -466,9 +466,9 @@ "dev": true }, "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.1.tgz", + "integrity": "sha1-Qmu52oQJDBg42BLIFQryCoMx4pY=" }, "array-initial": { "version": "1.1.0", @@ -1883,9 +1883,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "ejs": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.2.tgz", - "integrity": "sha512-zFuywxrAWtX5Mk2KAuoJNkXXbfezpNA0v7i+YC971QORguPekpjpAgeOv99YWSdKXwj7JxI2QAWDeDkE8fWtXw==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.3.tgz", + "integrity": "sha512-wmtrUGyfSC23GC/B1SMv2ogAUgbQEtDmTIhfqielrG5ExIM9TP4UoYdi90jLF1aTcsWCJNEO0UrgKzP0y3nTSg==", "requires": { "jake": "^10.6.1" } @@ -2141,18 +2141,18 @@ } }, "express": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "version": "5.0.0-alpha.8", + "resolved": "https://registry.npmjs.org/express/-/express-5.0.0-alpha.8.tgz", + "integrity": "sha512-PL8wTLgaNOiq7GpXt187/yWHkrNSfbr4H0yy+V0fpqJt5wpUzBi9DprAkwGKBFOqWHylJ8EyPy34V5u9YArfng==", "requires": { "accepts": "~1.3.7", - "array-flatten": "1.1.1", + "array-flatten": "2.1.1", "body-parser": "1.19.0", "content-disposition": "0.5.3", "content-type": "~1.0.4", "cookie": "0.4.0", "cookie-signature": "1.0.6", - "debug": "2.6.9", + "debug": "3.1.0", "depd": "~1.1.2", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", @@ -2163,10 +2163,11 @@ "methods": "~1.1.2", "on-finished": "~2.3.0", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-is-absolute": "1.0.1", "proxy-addr": "~2.0.5", "qs": "6.7.0", "range-parser": "~1.2.1", + "router": "2.0.0-alpha.1", "safe-buffer": "5.1.2", "send": "0.17.1", "serve-static": "1.14.1", @@ -2175,6 +2176,16 @@ "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } } }, "express-flash": { @@ -7147,9 +7158,9 @@ } }, "jake": { - "version": "10.6.1", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.6.1.tgz", - "integrity": "sha512-pHUK3+V0BjOb1XSi95rbBksrMdIqLVC9bJqDnshVyleYsET3H0XAq+3VB2E3notcYvv4wRdRHn13p7vobG+wfQ==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.7.1.tgz", + "integrity": "sha512-FUkLZXms1LSTQop5EJBdXVzbM0q6yYWMM4vo/TiLQeHJ4UMJVO8DBTZFiAgMBJctin9q92xnr2vdH7Wrpn7tTQ==", "requires": { "async": "0.9.x", "chalk": "^2.4.2", @@ -8039,9 +8050,9 @@ } }, "mongoose": { - "version": "5.9.13", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.9.13.tgz", - "integrity": "sha512-MsFdJAaCTVbDA3gYskUEpUN1kThL7sp4zh8N9rGt0+9vYMn28q92NLK90vGssM9qjOGWp8HqLeT1fBgfMZDnKA==", + "version": "5.9.15", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.9.15.tgz", + "integrity": "sha512-dGIDqaQkAJoLl7lsRLy70mDg+VcL1IPOHr/0f23MLF45PtnM5exRdmienfyVjdrSVGgTus+1sMUKef6vSnrDZg==", "requires": { "bson": "^1.1.4", "kareem": "2.3.1", @@ -8245,9 +8256,9 @@ } }, "nodemon": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.3.tgz", - "integrity": "sha512-lLQLPS90Lqwc99IHe0U94rDgvjo+G9I4uEIxRG3evSLROcqQ9hwc0AxlSHKS4T1JW/IMj/7N5mthiN58NL/5kw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.4.tgz", + "integrity": "sha512-Ltced+hIfTmaS28Zjv1BM552oQ3dbwPqI4+zI0SLgq+wpJhSyqgYude/aZa/3i31VCQWMfXJVxvu86abcam3uQ==", "dev": true, "requires": { "chokidar": "^3.2.2", @@ -9059,9 +9070,9 @@ "optional": true }, "pstree.remy": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.7.tgz", - "integrity": "sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, "pump": { @@ -9594,6 +9605,35 @@ "glob": "^7.1.3" } }, + "router": { + "version": "2.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/router/-/router-2.0.0-alpha.1.tgz", + "integrity": "sha512-fz/T/qLkJM6RTtbqGqA1+uZ88ejqJoPyKeJAeXPYjebA7HzV/UyflH4gXWqW/Y6SERnp4kDwNARjqy6se3PcOw==", + "requires": { + "array-flatten": "2.1.1", + "debug": "3.1.0", + "methods": "~1.1.2", + "parseurl": "~1.3.2", + "path-to-regexp": "0.1.7", + "setprototypeof": "1.1.0", + "utils-merge": "1.0.1" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + } + } + }, "rtlcss": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-2.5.0.tgz", @@ -10570,9 +10610,9 @@ "dev": true }, "ts-loader": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-7.0.3.tgz", - "integrity": "sha512-BXAHfPjm3J//20ibuI30M+xgLpdIng68p2H952QqbbmDk7SW72HV42k9Gop7rMxuHvrXWjazWhKuyr9D9kKe3A==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-7.0.5.tgz", + "integrity": "sha512-zXypEIT6k3oTc+OZNx/cqElrsbBtYqDknf48OZos0NQ3RTt045fBIU8RRSu+suObBzYB355aIPGOe/3kj9h7Ig==", "dev": true, "requires": { "chalk": "^2.3.0", @@ -10698,9 +10738,9 @@ } }, "typescript": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", - "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.3.tgz", + "integrity": "sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ==", "dev": true }, "uglify-js": { diff --git a/package.json b/package.json index bb72f5d..fe399a2 100755 --- a/package.json +++ b/package.json @@ -14,30 +14,30 @@ "license": "ISC", "dependencies": { "bcrypt": "^4.0.1", - "ejs": "^3.1.2", - "express": "^4.17.1", + "diff": ">=3.5.0", + "ejs": "^3.1.3", + "express": "^5.0.0-alpha.8", "express-flash": "0.0.2", "express-session": "^1.17.1", - "mongoose": "^5.9.11", + "lodash.template": ">=4.5.0", + "mongoose": "^5.9.15", "passport": "^0.4.1", - "passport-local": "^1.0.0", - "diff": ">=3.5.0", - "lodash.template": ">=4.5.0" + "passport-local": "^1.0.0" }, "devDependencies": { "@types/bcrypt": "^3.0.0", "@types/express": "^4.17.6", "@types/express-flash": "0.0.2", "@types/express-session": "^1.17.0", - "@types/mongoose": "^5.7.14", + "@types/mongoose": "^5.7.21", "@types/passport": "^1.0.3", "@types/passport-local": "^1.0.33", - "semantic-ui": "^2.4.2", - "nodemon": "^2.0.3", "concurrently": "^5.2.0", - "typescript": "^3.8.3", "gulp": "^3.9.1", - "ts-loader": "^7.0.3", - "tslint": "^6.1.1" + "nodemon": "^2.0.4", + "semantic-ui": "^2.4.2", + "ts-loader": "^7.0.5", + "tslint": "^6.1.1", + "typescript": "^3.9.3" } } diff --git a/src/controllers/dbUserController.ts b/src/controllers/dbUserController.ts index b1eeca3..9124ff4 100755 --- a/src/controllers/dbUserController.ts +++ b/src/controllers/dbUserController.ts @@ -1,9 +1,10 @@ -import {IDbUserDoc} from "../models/dbUser"; +import DbUser, {IDbUser, IDbUserDoc} from "../models/dbUser"; import {RequestHandler} from "express"; +import {IPlayer} from "../models/player"; export const whoAmI: RequestHandler = async (req, res) => { if (req.isAuthenticated()) { - const user = req.user as IDbUserDoc; + const user = req.user as IDbUser; res.json({loggedIn: true, username: user.username, lang: user.lang}); } else { @@ -12,6 +13,88 @@ export const whoAmI: RequestHandler = async (req, res) => { }; export const changeLang: RequestHandler = async (req, res) => { - const user = (req.user as IDbUserDoc); - user.changeLang(req.body.lang); + 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/statsController.ts b/src/controllers/statsController.ts index ea897e9..5d39750 100755 --- a/src/controllers/statsController.ts +++ b/src/controllers/statsController.ts @@ -2,12 +2,22 @@ import passport from "passport"; import DbUser, {IDbUser, IDbUserDoc} from "../models/dbUser"; import {RequestHandler} from "express"; import SavedGame from "../models/savedGame"; +import Player, {IPlayer} from "../models/player"; + +export interface GameSubmission { + players: {id: string, nick: string}[], + results: GameResults[]; +} + +interface GameResults { + playerId: string; + blocks: any; +} export const listGames: RequestHandler = async (req, res) => { - const user = (req.user as IDbUserDoc); - const dbUser = await DbUser.findById(user._id, {"savedGames.game": 1, "savedGames.createdAt": 1}); + const user = (req.user as IDbUser); + const dbUser = await DbUser.findById(user.id, {"savedGames._id": 1, "savedGames.results": 1, "savedGames.createdAt": 1}); if (dbUser) { - console.log(dbUser.savedGames); res.json({games: dbUser.savedGames}); } else { @@ -16,6 +26,19 @@ export const listGames: RequestHandler = async (req, res) => { }; export const saveGame: RequestHandler = async (req, res) => { - const user = (req.user as IDbUserDoc); - await user.addGame(req.body); + const user = (req.user as IDbUser); + const submission = (req.body as GameSubmission); + for (let player of submission.players) { + if (player.id === player.nick) { + const newGuest = await user.addGuest(player.nick); + player.id = newGuest.id; + const playerResult = submission.results.find(result => result.playerId === player.nick); + playerResult!.playerId = player.id; + } + } + const newGame = await user.addGame(submission); + res.send({message: "Game submitted successfully!", newGame: newGame}); +}; + +const processStatistics = (results: GameResults, account: IDbUser) => { }; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 4e1124c..22d56a8 100755 --- a/src/index.ts +++ b/src/index.ts @@ -39,10 +39,6 @@ app.locals = { initialisePassport(); app.use(passport.initialize()); app.use(passport.session()); -app.use((req, res, next) => { - console.log(req.originalUrl); - next(); -}); app.use(Settings.serverRoot + "/static", express.static("static")); app.use(Settings.serverRoot, MainRouter); diff --git a/src/models/dbUser.ts b/src/models/dbUser.ts index d1aed0a..03561a3 100755 --- a/src/models/dbUser.ts +++ b/src/models/dbUser.ts @@ -1,9 +1,11 @@ -import mongoose from "mongoose"; +import mongoose, {Model} from "mongoose"; import Player, {IPlayer, IPlayerDoc, PlayerSchema} from "./player"; -import {AccountStats, AccountStatsSchema, IAccountStats, IAccountStatsDoc} from "./stats"; +import {AccountStatsSchema, IAccountStats, IAccountStatsDoc} from "./stats"; import SavedGame, {ISavedGame, ISavedGameDoc, SavedGameSchema} from "./savedGame"; import bcrypt from "bcrypt"; import {SupportedLang} from "../enums"; +import {GenericModelError, globalSchemaOptions, ModelParameterError, tryQuery} from "./utils"; +import {GameSubmission} from "../controllers/statsController"; export class CredentialsTakenError extends Error { public emailExists: boolean; @@ -17,6 +19,7 @@ export class CredentialsTakenError extends Error { } export interface IDbUser { + id: string; username: string; email: string; password: string; @@ -26,9 +29,21 @@ export interface IDbUser { guests?: IPlayer[]; accountStats?: IAccountStats; savedGames?: ISavedGame[]; + getMainPlayerInfo(): Promise; + findGuestByNick(nick: string): Promise; + changeLang(lang: SupportedLang): void; + addGame(game: any): Promise; + getGuests(): Promise; + getGuest(guestId: string): Promise; + addGuest(nick: string): Promise; + updateGuest(guestParams: GuestUpdateParams): Promise; + deleteGuest(guestId: string): Promise; } +type GuestUpdateParams = {id: string, newNick: string}; + export interface IDbUserDoc extends mongoose.Document { + id: string; username: string; email: string; password: string; @@ -37,17 +52,25 @@ export interface IDbUserDoc extends mongoose.Document { player: IPlayerDoc; guests: IPlayerDoc[]; accountStats: IAccountStatsDoc; - savedGames: ISavedGameDoc[]; + savedGames: mongoose.Types.Array; + getMainPlayerInfo(): Promise; + findGuestByNick(nick: string): Promise; changeLang(lang: SupportedLang): void; addGame(game: any): Promise; + getGuests(): Promise; + getGuest(guestId: string): Promise; + addGuest(nick: string): Promise; + updateGuest(guestParams: GuestUpdateParams): Promise; + deleteGuest(guestId: string): Promise; } export interface IDbUserModel extends mongoose.Model { findByEmail(emailQuery: string): IDbUserDoc; addNewUser(user: IDbUser): IDbUserDoc; registerUser(username: string, email: string, password: string): IDbUserDoc; - userWithEmailExists(email: string): boolean; - userWithUsernameExists(username: string): boolean; + userWithEmailExists(email: string): Promise; + userWithUsernameExists(username: string): Promise; + getSerializedAuthUser(id: string): Promise; } export const DbUserSchema = new mongoose.Schema({ @@ -60,21 +83,25 @@ export const DbUserSchema = new mongoose.Schema({ guests: {type: [PlayerSchema], default: []}, accountStats: {type: AccountStatsSchema, default: () => ({}) }, savedGames: {type: [SavedGameSchema], default: []}, -}); +}, {...globalSchemaOptions}); DbUserSchema.statics.findByEmail = async function (emailQuery: string) { - return this.findOne({email: emailQuery}); + return tryQuery(() => + this.findOne({email: emailQuery}) + ); }; -DbUserSchema.statics.addNewUser = async function (user: IDbUser) { - const player = new Player( { nick: user.username }); - return this.create({ - username: user.username, - email: user.email, - password: user.password, - lang: SupportedLang.gb, - player - }); +DbUserSchema.statics.addNewUser = async function (username: string, email: string, hashedPw: string) { + const player = new Player( { nick: username }); + return tryQuery(() => + this.create({ + username: username, + email: email, + password: hashedPw, + lang: SupportedLang.gb, + player + }) + ); }; DbUserSchema.statics.registerUser = async function (username: string, email: string, password: string) { @@ -85,30 +112,136 @@ DbUserSchema.statics.registerUser = async function (username: string, email: str } else { const hashedPassword = await bcrypt.hash(password, 10); - return this.addNewUser({username, email, password: hashedPassword}); + return tryQuery(() => + this.addNewUser(username, email, hashedPassword) + ); } }; -DbUserSchema.statics.userWithEmailExists = async function (email: string) { - return this.exists({email}); +DbUserSchema.statics.userWithEmailExists = async function (email: string): Promise { + return tryQuery(() => this.exists({email})); }; -DbUserSchema.statics.userWithUsernameExists = async function (username: string) { - return this.exists({username}); +DbUserSchema.statics.userWithUsernameExists = async function (username: string): Promise { + return tryQuery(() => this.exists({username})); }; -DbUserSchema.methods.addGame = function (game: any): void { - const newGame = new SavedGame(); - newGame.game = game; - DbUser.updateOne(this, {$push: {savedGames: newGame}}, (err) => { - console.log(err); +DbUserSchema.statics.getSerializedAuthUser = async function (id: string): Promise { + return tryQuery(() => { + return DbUser.findById(id, {id: 1, username: 1, password: 1, lang: 1, email: 1}); }); }; -DbUserSchema.methods.changeLang = function (lang: SupportedLang): void { - if (lang in SupportedLang) { - DbUser.updateOne(this, {lang: lang}); +DbUserSchema.methods.getGuests = async function (this: IDbUser): Promise { + const user: IDbUserDoc = await tryQuery(async () => { + return DbUser.findById(this.id, {"guests.nick": 1, "guests._id": 1}).exec(); + }); + return user.guests; +}; + +DbUserSchema.methods.getGuest = async function (this: IDbUser, guestId: string): Promise { + return tryQuery(async () => { + const user = await DbUser.findById(this.id, {guests: {$elemMatch: {_id: guestId}}}); + if (user!.guests.length > 0) { + return user!.guests[0]; + } + else { + throw new ModelParameterError("Guest with ID " + guestId + " doesn't exist!"); + } + }); +}; + +DbUserSchema.methods.findGuestByNick = async function (this: IDbUser, guestNick: string): Promise { + return tryQuery(async () => { + const user = await DbUser.findById(this.id, {guests: {$elemMatch: {nick: guestNick}}}); + if (user!.guests.length > 0) { + return user!.guests[0]; + } + else { + return null; + } + }); +}; + +DbUserSchema.methods.addGuest = async function (this: IDbUser, newGuestNick: string): Promise { + if (this.username !== newGuestNick) { + const guestLookup = await this.findGuestByNick(newGuestNick); + if (!guestLookup) { + return saveGuest(this, newGuestNick); + } + else { + throw new ModelParameterError(`Cannot add a guest with the same name of another guest in this account.`); + } } + else { + throw new ModelParameterError("Cannot add a guest with the same name as the account holder's username.") + } +}; + +async function saveGuest(user: IDbUser, newGuestNick: string): Promise { + const newGuest: IPlayerDoc = new Player(); + newGuest.nick = newGuestNick; + await tryQuery(() => { + DbUser.findByIdAndUpdate(user.id, {$push: {guests: newGuest}}).exec(); + }); + return newGuest; +} + +DbUserSchema.methods.updateGuest = async function (this: IDbUser, guestParams: GuestUpdateParams): Promise { + return tryQuery(async () => { + const user = await DbUser.findById(this.id); + const updatableGuest = user!.guests.find(guest => guest.id === guestParams.id); + if (updatableGuest) { + updatableGuest.nick = guestParams.newNick; + await user!.save(); + return updatableGuest; + } + else { + throw new ModelParameterError("Guest with ID " + guestParams.id + " doesn't exist!"); + } + }); +}; + +DbUserSchema.methods.deleteGuest = async function (this: IDbUser, guestId: string): Promise { + return tryQuery(async () => { + const user = await DbUser.findById(this.id); + const deleteGuestIndex = user!.guests.findIndex(guest => guest.id === guestId); + if (deleteGuestIndex !== -1) { + const deletedGuest = user!.guests[deleteGuestIndex]; + user!.guests[deleteGuestIndex].remove(); + await user!.save(); + return deletedGuest; + } + else { + throw new ModelParameterError("Guest with ID " + guestId + " doesn't exist!"); + } + }); +}; + +DbUserSchema.methods.addGame = async function (submission: GameSubmission): Promise { + const newGame = await SavedGame.createFromGameSubmission(submission); + await tryQuery(() => { + DbUser.findByIdAndUpdate(this.id, {$push: {savedGames: newGame}}).exec(); + }); + return newGame; +}; + +DbUserSchema.methods.changeLang = async function (lang: SupportedLang): Promise { + if (lang in SupportedLang) { + await tryQuery(() => + DbUser.findByIdAndUpdate(this.id, {lang: lang}) + ); + } + else { + throw new ModelParameterError(lang + " is not a supported language code!"); + } +}; + +DbUserSchema.methods.getMainPlayerInfo = async function (): Promise { + const user = await tryQuery(() => + DbUser.findById(this.id, {"player.nick": 1, "player._id": 1}).exec() + ); + return user.player; }; const DbUser = mongoose.model("DbUser", DbUserSchema); diff --git a/src/models/player.ts b/src/models/player.ts index 54d3618..474fc37 100755 --- a/src/models/player.ts +++ b/src/models/player.ts @@ -1,14 +1,17 @@ import mongoose from "mongoose"; import {IPlayerStats, IPlayerStatsDoc, PlayerStatsSchema} from "./stats"; +import {globalSchemaOptions} from "./utils"; export interface IPlayer { + id: string; nick: string; stats: IPlayerStats; } export interface IPlayerDoc extends mongoose.Document { + id: string; nick: string; - stats: IPlayerStats; + stats: IPlayerStatsDoc; } export interface IPlayerModel extends mongoose.Model { @@ -18,7 +21,7 @@ export interface IPlayerModel extends mongoose.Model { export const PlayerSchema = new mongoose.Schema({ nick: { type: String, required: true }, stats: { type: PlayerStatsSchema, required: true, default: () => ({}) }, -}); +}, {...globalSchemaOptions}); const Player = mongoose.model("Player", PlayerSchema); export default Player; \ No newline at end of file diff --git a/src/models/savedGame.ts b/src/models/savedGame.ts index dfb5267..8b370bc 100755 --- a/src/models/savedGame.ts +++ b/src/models/savedGame.ts @@ -1,24 +1,45 @@ -import mongoose from "mongoose"; -import {IPlayer} from "./player"; +import mongoose, {Types} from "mongoose"; +import Player, {IPlayer} from "./player"; +import {GameSubmission} from "../controllers/statsController"; +import {tryQuery, globalSchemaOptions} from "./utils"; +import DbUser from "./dbUser"; export interface ISavedGame { - //players: IPlayer[], - game: any, + id: string; + //rulesetUsed?: ruleset; + players: mongoose.Types.ObjectId[]; + results: any[]; } export interface ISavedGameDoc extends mongoose.Document { - //players: mongoose.Types.ObjectId[], - game: mongoose.Types.Subdocument, + //rulesetUsed: mongoose.Types.ObjectId[]; + id: string; + players: mongoose.Types.Array; + results: mongoose.Types.Array; } export interface ISavedGameModel extends mongoose.Model { // virtual static methods + createFromGameSubmission(submission: GameSubmission): Promise; } export const SavedGameSchema = new mongoose.Schema({ - //players: [mongoose.Schema.Types.ObjectId], - game: mongoose.Schema.Types.Mixed, -}, {timestamps: true}); + //rulesetUsed: [mongoose.Schema.Types.ObjectId], + players: [mongoose.Schema.Types.ObjectId], + results: [mongoose.Schema.Types.Mixed], +}, { + timestamps: true, + ...globalSchemaOptions +}); + +SavedGameSchema.statics.createFromGameSubmission = async function(submission: GameSubmission) { + const newGame = new SavedGame(); + newGame.results.addToSet(...submission.results); + await tryQuery(async () => { + newGame.players.addToSet(...submission.players.map(player => player.id)); + }); + return newGame; +}; const SavedGame = mongoose.model("SavedGame", SavedGameSchema); export default SavedGame; \ No newline at end of file diff --git a/src/models/stats.ts b/src/models/stats.ts index c5ae3fd..8443e17 100755 --- a/src/models/stats.ts +++ b/src/models/stats.ts @@ -53,31 +53,16 @@ type IMultiplierFieldStats = INumberFieldStats; type IYahtzeeFieldStats = INumberFieldStats; // Mongoose doc interfaces and types -export interface IPlayerStats extends IBaseStats { - one: IMultiplierFieldStatsDoc; - two: IMultiplierFieldStatsDoc; - three: IMultiplierFieldStatsDoc; - four: IMultiplierFieldStatsDoc; - five: IMultiplierFieldStatsDoc; - six: IMultiplierFieldStatsDoc; - upperBonus: IBonusFieldStatsDoc; - upperTotal: ITotalFieldStatsDoc; - threeKind: INumberFieldStatsDoc; - fourKind: INumberFieldStatsDoc; - fullHouse: IBoolFieldStatsDoc; - smlStraight: IBoolFieldStatsDoc; - lgStraight: IBoolFieldStatsDoc; - yahtzee: IYahtzeeFieldStatsDoc; - chance: INumberFieldStatsDoc; - grandTotal: ITotalFieldStatsDoc; - lowerTotal: ITotalFieldStatsDoc; - gamesPlayed: number; +export interface IPlayerStatsDoc extends IBaseStatsDoc { wins: number; runnerUps: number; draws: number; losses: number; } -export interface IAccountStats extends IBaseStats { +export interface IAccountStatsDoc extends IBaseStatsDoc { + timesNoWinner: number; +} +interface IBaseStatsDoc extends mongoose.Document { one: IMultiplierFieldStatsDoc; two: IMultiplierFieldStatsDoc; three: IMultiplierFieldStatsDoc; @@ -96,16 +81,14 @@ export interface IAccountStats extends IBaseStats { grandTotal: ITotalFieldStatsDoc; lowerTotal: ITotalFieldStatsDoc; gamesPlayed: number; - timesNoWinner: number; } + type IBonusFieldStatsDoc = mongoose.Document & IBonusFieldStats; type ITotalFieldStatsDoc = mongoose.Document & ITotalFieldStats; type IBoolFieldStatsDoc = mongoose.Document & IBoolFieldStats; type INumberFieldStatsDoc = mongoose.Document & INumberFieldStats; type IMultiplierFieldStatsDoc = mongoose.Document & IMultiplierFieldStats; type IYahtzeeFieldStatsDoc = mongoose.Document & IYahtzeeFieldStats; -export type IPlayerStatsDoc = mongoose.Document & IPlayerStats; -export type IAccountStatsDoc = mongoose.Document & IAccountStats; // Mongoose schemata class Int extends mongoose.SchemaType { diff --git a/src/models/utils.ts b/src/models/utils.ts new file mode 100644 index 0000000..231178b --- /dev/null +++ b/src/models/utils.ts @@ -0,0 +1,41 @@ +export class GenericModelError extends Error { + constructor(message: string) { + super(message); + this.name = "GenericModelError"; + } +} + +export class ModelParameterError extends GenericModelError { + constructor(message: string) { + super(message); + this.name = "ModelParameterError"; + } +} + +type CallbackWrapper = (query: () => T) => Promise; + +export const tryQuery: CallbackWrapper = async (cb) => { + try { + return cb(); + } + catch (err) { + throw new GenericModelError(err); + } +}; + +export const globalSchemaOptions = { + toObject: { + transform: function (doc: any, ret: any) { + ret.id = ret._id; + delete ret._id; + delete ret.__v; + } + }, + toJSON: { + transform: function (doc: any, ret: any) { + ret.id = ret._id; + delete ret._id; + delete ret.__v; + }, + } +}; \ No newline at end of file diff --git a/src/passport-config.ts b/src/passport-config.ts index 1298842..79f7ab2 100755 --- a/src/passport-config.ts +++ b/src/passport-config.ts @@ -1,7 +1,7 @@ import passport from "passport"; import {Strategy as LocalStrategy, VerifyFunction} from "passport-local"; import bcrypt from "bcrypt"; -import DbUser, {IDbUserDoc} from "./models/dbUser"; +import DbUser, {IDbUser} from "./models/dbUser"; import {NextFunction, Request, Response} from "express"; export const requireAuthenticated = (req: Request, res: Response, next: NextFunction) => { @@ -41,11 +41,11 @@ const authenticateUser: VerifyFunction = async (email, password, done) => { export const initialisePassport = () => { passport.use(new LocalStrategy({ usernameField: "email" }, authenticateUser)); - passport.serializeUser((user: IDbUserDoc, done) => { - done(null, user._id) + passport.serializeUser((user: IDbUser, done) => { + done(null, user.id) }); passport.deserializeUser(async (id: string, done) => { - const user: IDbUserDoc | null = await DbUser.findById(id, {username: 1, password: 1, lang: 1, email: 1}); + const user: IDbUser | null = await DbUser.getSerializedAuthUser(id); done(null, user); }); }; \ No newline at end of file diff --git a/src/routers/apiRouter.ts b/src/routers/apiRouter.ts index 4e04071..f62e53f 100755 --- a/src/routers/apiRouter.ts +++ b/src/routers/apiRouter.ts @@ -1,13 +1,24 @@ import express from "express"; import {requireAuthenticated} from "../passport-config"; -import * as stats from "../controllers/statsController"; -import * as dbUser from "../controllers/dbUserController" +import * as statsController from "../controllers/statsController"; +import * as dbUserController from "../controllers/dbUserController" const router = express.Router(); -router.get("/user", dbUser.whoAmI); -router.post("/changeLang", requireAuthenticated, dbUser.changeLang); -router.get("/games", requireAuthenticated, stats.listGames); -router.post("/savegame", requireAuthenticated, stats.saveGame); +// Basic User Settings +router.get("/user", dbUserController.whoAmI); +router.put("/lang", requireAuthenticated, dbUserController.changeLang); + +// Guests +router.get("/players", requireAuthenticated, dbUserController.getAllPlayersAssociatedWithAccount); +router.get("/guests", requireAuthenticated, dbUserController.getGuests); +router.get("/guest/:id", requireAuthenticated, dbUserController.getGuest); +router.put("/guest/:id", requireAuthenticated, dbUserController.updateGuest); +router.post("/guests", requireAuthenticated, dbUserController.addGuest); +router.delete("/guest/:id", requireAuthenticated, dbUserController.deleteGuest); + +// Games +router.get("/games", requireAuthenticated, statsController.listGames); +router.post("/games", requireAuthenticated, statsController.saveGame); export default router; \ No newline at end of file diff --git a/src/routers/mainRouter.ts b/src/routers/mainRouter.ts index dca6787..d4d0a17 100755 --- a/src/routers/mainRouter.ts +++ b/src/routers/mainRouter.ts @@ -2,19 +2,18 @@ import express from "express"; import {requireAuthenticated} from "../passport-config"; import routers from "./routers"; import {IDbUser} from "../models/dbUser"; +import {ModelParameterError} from "../models/utils"; const router = express.Router(); router.use("/account", routers.signup); router.use("/api", routers.api); - router.get("/game", requireAuthenticated, (req, res) => { res.render("gameIndex.ejs", { username: (req.user as IDbUser).username, rootUrl: req.app.locals.rootUrl }); }); - router.get("/**", requireAuthenticated, (req, res) => { res.render("frontendIndex.ejs", { username: (req.user as IDbUser).username, @@ -22,4 +21,16 @@ 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."}); + } + }; + +router.use(genericErrorHandler); + export default router; \ No newline at end of file diff --git a/src/server-config.json b/src/server-config.json index e55f4f9..aef19a5 100755 --- a/src/server-config.json +++ b/src/server-config.json @@ -1,4 +1,4 @@ { - "mongodb_uri": "mongodb://127.0.0.1:27017/test", + "mongodb_uri": "mongodb://127.0.0.1:27017/local", "serverRoot": "/kadi" } \ No newline at end of file