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