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.

This commit is contained in:
Daniel Ledda
2020-05-25 22:57:36 +02:00
parent 3a7e7de3d4
commit 4ccbdf599e
15 changed files with 483 additions and 140 deletions

View File

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

View File

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

View File

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

View File

@@ -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<IPlayer>;
findGuestByNick(nick: string): Promise<IPlayer | null>;
changeLang(lang: SupportedLang): void;
addGame(game: any): Promise<string | null>;
getGuests(): Promise<IPlayer[]>;
getGuest(guestId: string): Promise<IPlayer>;
addGuest(nick: string): Promise<IPlayer>;
updateGuest(guestParams: GuestUpdateParams): Promise<IPlayer>;
deleteGuest(guestId: string): Promise<IPlayer>;
}
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<ISavedGameDoc>;
getMainPlayerInfo(): Promise<IPlayer>;
findGuestByNick(nick: string): Promise<IPlayer | null>;
changeLang(lang: SupportedLang): void;
addGame(game: any): Promise<string | null>;
getGuests(): Promise<IPlayer[]>;
getGuest(guestId: string): Promise<IPlayer>;
addGuest(nick: string): Promise<IPlayer>;
updateGuest(guestParams: GuestUpdateParams): Promise<IPlayer>;
deleteGuest(guestId: string): Promise<IPlayer>;
}
export interface IDbUserModel extends mongoose.Model<IDbUserDoc> {
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<boolean>;
userWithUsernameExists(username: string): Promise<boolean>;
getSerializedAuthUser(id: string): Promise<IDbUser>;
}
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<boolean> {
return tryQuery(() => this.exists({email}));
};
DbUserSchema.statics.userWithUsernameExists = async function (username: string) {
return this.exists({username});
DbUserSchema.statics.userWithUsernameExists = async function (username: string): Promise<boolean> {
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<IDbUser> {
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<IPlayer[]> {
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<IPlayer> {
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<IPlayer | null> {
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<IPlayer> {
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<IPlayer> {
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<IPlayer> {
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<IPlayer> {
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<ISavedGame> {
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<void> {
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<IPlayer> {
const user = await tryQuery(() =>
DbUser.findById(this.id, {"player.nick": 1, "player._id": 1}).exec()
);
return user.player;
};
const DbUser = mongoose.model<IDbUserDoc, IDbUserModel>("DbUser", DbUserSchema);

View File

@@ -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<IPlayerDoc> {
@@ -18,7 +21,7 @@ export interface IPlayerModel extends mongoose.Model<IPlayerDoc> {
export const PlayerSchema = new mongoose.Schema({
nick: { type: String, required: true },
stats: { type: PlayerStatsSchema, required: true, default: () => ({}) },
});
}, {...globalSchemaOptions});
const Player = mongoose.model<IPlayerDoc, IPlayerModel>("Player", PlayerSchema);
export default Player;

View File

@@ -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<mongoose.Types.ObjectId>;
results: mongoose.Types.Array<mongoose.Types.Subdocument>;
}
export interface ISavedGameModel extends mongoose.Model<ISavedGameDoc> {
// virtual static methods
createFromGameSubmission(submission: GameSubmission): Promise<ISavedGameDoc>;
}
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<ISavedGameDoc, ISavedGameModel>("SavedGame", SavedGameSchema);
export default SavedGame;

View File

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

41
src/models/utils.ts Normal file
View File

@@ -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 = <T>(query: () => T) => Promise<any>;
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;
},
}
};

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
{
"mongodb_uri": "mongodb://127.0.0.1:27017/test",
"mongodb_uri": "mongodb://127.0.0.1:27017/local",
"serverRoot": "/kadi"
}