first commit to new repo

This commit is contained in:
Daniel Ledda
2020-05-10 15:16:01 +02:00
commit a57ad3af42
478 changed files with 90510 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
import passport from "passport";
import DbUser, {IDbUser, IDbUserDoc} from "../models/dbUser";
import {RequestHandler} from "express";
import SavedGame from "../models/savedGame";
export const whoAmI: RequestHandler = async (req, res) => {
if (req.isAuthenticated()) {
const user = req.user as IDbUserDoc;
res.json({loggedIn: true, username: user.username});
}
else {
res.json({loggedIn: false});
}
};

View File

@@ -0,0 +1,50 @@
import passport from "passport";
import DbUser from "../models/dbUser";
import {RequestHandler} from "express";
export const showLoginPage: RequestHandler = (req, res) => {
res.render("login.ejs");
};
export const loginUser: RequestHandler = (req, res) => {
passport.authenticate('local', {
successRedirect: req.app.locals.rootUrl + "/",
failureRedirect: req.baseUrl + "/login",
failureFlash: true,
})(req, res);
};
export const showRegistrationPage: RequestHandler = (req, res) => {
res.render("register.ejs");
};
export const registerNewUser: RequestHandler = async (req, res) => {
try {
const newUser = await DbUser.registerUser(req.body.username, req.body.email, req.body.password);
req.login(newUser, (err) => {
if (err) {
throw err;
}
else {
res.redirect(req.baseUrl + "/");
}
});
}
catch (error) {
const errors: string[] = [];
if (error.name === "CredentialsTakenError") {
error.usernameExists && errors.push("That username is already taken");
error.emailExists && errors.push("That email address is already taken");
}
else {
errors.push("An error occurred during the registration.");
}
req.flash("errors", errors);
res.render("register.ejs");
}
};
export const logoutUser: RequestHandler = (req, res) => {
req.logout();
res.redirect(req.baseUrl + "/login");
};

View File

@@ -0,0 +1,40 @@
import passport from "passport";
import DbUser, {IDbUser, IDbUserDoc} from "../models/dbUser";
import {RequestHandler} from "express";
import SavedGame from "../models/savedGame";
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});
if (dbUser) {
res.json({games: dbUser.savedGames});
}
else {
res.sendStatus(404);
}
};
export const saveGame: RequestHandler = async (req, res) => {
const user = (req.user as IDbUserDoc);
const handleError = (err: string) => {
res.send({error: true, message: err});
};
DbUser.findById(user, (err, user) => {
if (err) {
handleError(err);
}
else if (user) {
const newGame = new SavedGame();
newGame.game = req.body;
user.savedGames.push(newGame);
user.save((err) => {
if (err) {
return handleError(err);
}
else {
res.send("Thanks for submitting your game, " + user.username + "!");
}
})
}
});
};

47
src/index.ts Executable file
View File

@@ -0,0 +1,47 @@
import express from "express";
import {initialisePassport, requireAuthenticated, requireNotAuthenticated} from "./passport-config";
import mongoose from "mongoose";
import Settings from "./server-config.json";
import flash from "express-flash";
import passport from "passport";
import session from "express-session";
import * as path from "path";
import MainRouter from "./routers/mainRouter";
// MongoDB Setup
mongoose.connect(Settings.mongodb_uri, (err: any) => {
if (err) {
console.log(err.message);
}
else {
console.log("Successfully connected to mongoDB!");
}
});
// Express app config
const app = express();
app.use(express.json());
app.set("port", process.env.PORT || 3000);
app.set("view-engine", "ejs");
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, MainRouter);
const server = app.listen(app.get("port"), () => {
console.log("App is running on http://localhost:%d", app.get("port"));
});

94
src/models/dbUser.ts Executable file
View File

@@ -0,0 +1,94 @@
import mongoose from "mongoose";
import Player, {IPlayer, IPlayerDoc, PlayerSchema} from "./player";
import {AccountStats, AccountStatsSchema, IAccountStats, IAccountStatsDoc} from "./stats";
import {ISavedGame, ISavedGameDoc, SavedGameSchema} from "./savedGame";
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 IDbUser {
username: string;
email: string;
password: string;
friends?: IDbUser[];
player?: IPlayer;
guests?: IPlayer[];
accountStats?: IAccountStats;
savedGames?: ISavedGame[];
}
export interface IDbUserDoc extends mongoose.Document {
username: string;
email: string;
password: string;
friends: IDbUserDoc[];
player: IPlayerDoc;
guests: IPlayerDoc[];
accountStats: IAccountStatsDoc;
savedGames: ISavedGameDoc[];
}
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;
}
export const DbUserSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
friends: {type: [mongoose.Schema.Types.ObjectId], required: true, default: []},
player: {type: PlayerSchema, required: true, unique: true},
guests: {type: [PlayerSchema], required: true, default: []},
accountStats: {type: AccountStatsSchema, required: true, default: () => ({}) },
savedGames: {type: [SavedGameSchema], required: true, default: []},
});
DbUserSchema.statics.findByEmail = async function (emailQuery: string) {
return 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,
player
});
};
DbUserSchema.statics.registerUser = async function (username: string, email: string, password: string) {
const usernameTaken = await this.userWithUsernameExists(username);
const emailTaken = await this.userWithEmailExists(email);
if (usernameTaken || emailTaken) {
throw new CredentialsTakenError(usernameTaken, emailTaken);
}
else {
const hashedPassword = await bcrypt.hash(password, 10);
return this.addNewUser({username, email, password: hashedPassword});
}
};
DbUserSchema.statics.userWithEmailExists = async function (email: string) {
return this.exists({email});
};
DbUserSchema.statics.userWithUsernameExists = async function (username: string) {
return this.exists({username});
};
const DbUser = mongoose.model<IDbUserDoc, IDbUserModel>("DbUser", DbUserSchema);
export default DbUser;

24
src/models/player.ts Executable file
View File

@@ -0,0 +1,24 @@
import mongoose from "mongoose";
import {IPlayerStats, IPlayerStatsDoc, PlayerStatsSchema} from "./stats";
export interface IPlayer {
nick: string;
stats: IPlayerStats;
}
export interface IPlayerDoc extends mongoose.Document {
nick: string;
stats: IPlayerStats;
}
export interface IPlayerModel extends mongoose.Model<IPlayerDoc> {
// virtual static methods
}
export const PlayerSchema = new mongoose.Schema({
nick: { type: String, required: true },
stats: { type: PlayerStatsSchema, required: true, default: () => ({}) },
});
const Player = mongoose.model<IPlayerDoc, IPlayerModel>("Player", PlayerSchema);
export default Player;

0
src/models/ruleset.ts Executable file
View File

24
src/models/savedGame.ts Executable file
View File

@@ -0,0 +1,24 @@
import mongoose from "mongoose";
import {IPlayer} from "./player";
export interface ISavedGame {
//players: IPlayer[],
game: any,
}
export interface ISavedGameDoc extends mongoose.Document {
//players: mongoose.Types.ObjectId[],
game: mongoose.Types.Subdocument,
}
export interface ISavedGameModel extends mongoose.Model<ISavedGameDoc> {
// virtual static methods
}
export const SavedGameSchema = new mongoose.Schema({
//players: [mongoose.Schema.Types.ObjectId],
game: mongoose.Schema.Types.Mixed,
}, {timestamps: true});
const SavedGame = mongoose.model<ISavedGameDoc, ISavedGameModel>("SavedGame", SavedGameSchema);
export default SavedGame;

205
src/models/stats.ts Executable file
View File

@@ -0,0 +1,205 @@
import mongoose from "mongoose";
// Interfaces and types for business logic
export interface IPlayerStats extends IBaseStats {
wins: number;
runnerUps: number;
draws: number;
losses: number;
}
export interface IAccountStats extends IBaseStats {
timesNoWinner: number;
}
export interface IBaseStats {
one: IMultiplierFieldStats;
two: IMultiplierFieldStats;
three: IMultiplierFieldStats;
four: IMultiplierFieldStats;
five: IMultiplierFieldStats;
six: IMultiplierFieldStats;
upperBonus: IBonusFieldStats;
upperTotal: ITotalFieldStats;
threeKind: INumberFieldStats;
fourKind: INumberFieldStats;
fullHouse: IBoolFieldStats;
smlStraight: IBoolFieldStats;
lgStraight: IBoolFieldStats;
yahtzee: IYahtzeeFieldStats;
chance: INumberFieldStats;
grandTotal: ITotalFieldStats;
lowerTotal: ITotalFieldStats;
gamesPlayed: number;
}
interface IBonusFieldStats {
total: number;
}
interface ITotalFieldStats {
average: number;
best: number;
worst: number;
}
interface IBoolFieldStats {
average: number;
timesStruck: number;
total: number;
}
interface INumberFieldStats {
average: number;
timesStruck: number;
best: number;
worst: number;
}
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;
wins: number;
runnerUps: number;
draws: number;
losses: number;
}
export interface IAccountStats 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;
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 {
constructor(key: string, options: any) {
super(key, options, 'Int');
}
cast(val: any): number {
let _val = Number(val);
if (isNaN(_val)) {
throw new Error('ZeroPositiveInt: ' + val + ' is not a number');
}
_val = Math.round(_val);
return _val;
}
}
(mongoose.Schema.Types as any).Int = Int;
const BonusFieldStatsSchema = new mongoose.Schema( {
average: {type: Number, required: true, default: 0, min: 0},
total: {type: Int, required: true, default: 0, min: 0}
}, { _id: false });
const TotalFieldStatsSchema = new mongoose.Schema( {
average: {type: Number, required: true, default: 0, min: 0},
best: {type: Int, required: true, default: 0, min: 0},
worst: {type: Int, required: true, default: 0, min: 0},
}, { _id: false });
const BoolFieldStatsSchema = new mongoose.Schema( {
average: {type: Number, required: true, default: 0, min: 0, max: 1},
timesStruck: {type: Int, required: true, default: 0, min: 0},
total: {type: Int, required: true, default: 0, min: 0},
}, { _id: false });
const NumberFieldStatsSchema = new mongoose.Schema( {
average: {type: Number, required: true, default: 0, min: 0},
timesStruck: {type: Int, required: true, default: 0, min: 0},
best: {type: Int, required: true, default: 0, min: 0},
worst: {type: Int, required: true, default: 0, min: 0},
}, { _id: false });
const MultiplierFieldStatsSchema = new mongoose.Schema( {
average: {type: Number, required: true, default: 0, min: 0},
timesStruck: {type: Int, required: true, default: 0, min: 0},
best: {type: Int, required: true, default: 0, min: 0},
worst: {type: Int, required: true, default: 0, min: 0},
}, { _id: false });
const YahtzeeFieldStatsSchema = new mongoose.Schema( {
average: {type: Number, required: true, default: 0, min: 0},
timesStruck: {type: Int, required: true, default: 0, min: 0},
best: {type: Int, required: true, default: 0, min: 0},
worst: {type: Int, required: true, default: 0, min: 0},
}, { _id: false });
export const PlayerStatsSchema = new mongoose.Schema( {
one: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
two: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
three: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
four: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
five: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
six: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
upperBonus: { type: BonusFieldStatsSchema, required: true, default: () => ({}) },
upperTotal: { type: TotalFieldStatsSchema, required: true, default: () => ({}) },
threeKind: { type: NumberFieldStatsSchema, required: true, default: () => ({}) },
fourKind: { type: NumberFieldStatsSchema, required: true, default: () => ({}) },
fullHouse: { type: BoolFieldStatsSchema, required: true, default: () => ({}) },
smlStraight: { type: BoolFieldStatsSchema, required: true, default: () => ({}) },
lgStraight: { type: BoolFieldStatsSchema, required: true, default: () => ({}) },
yahtzee: { type: YahtzeeFieldStatsSchema, required: true, default: () => ({}) },
chance: { type: NumberFieldStatsSchema, required: true, default: () => ({}) },
grandTotal: { type: TotalFieldStatsSchema, required: true, default: () => ({}) },
lowerTotal: { type: TotalFieldStatsSchema, required: true, default: () => ({}) },
gamesPlayed: { type: Int, required: true, default: 0, min: 0 },
wins: { type: Int, required: true, default: 0, min: 0 },
runnerUps: { type: Int, required: true, default: 0, min: 0 },
draws: { type: Int, required: true, default: 0, min: 0 },
losses: { type: Int, required: true, default: 0, min: 0 },
}, { _id: false });
export const AccountStatsSchema = new mongoose.Schema( {
one: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
two: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
three: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
four: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
five: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
six: { type: MultiplierFieldStatsSchema, required: true, default: () => ({}) },
upperBonus: { type: BonusFieldStatsSchema, required: true, default: () => ({}) },
upperTotal: { type: TotalFieldStatsSchema, required: true, default: () => ({}) },
threeKind: { type: NumberFieldStatsSchema, required: true, default: () => ({}) },
fourKind: { type: NumberFieldStatsSchema, required: true, default: () => ({}) },
fullHouse: { type: BoolFieldStatsSchema, required: true, default: () => ({}) },
smlStraight: { type: BoolFieldStatsSchema, required: true, default: () => ({}) },
lgStraight: { type: BoolFieldStatsSchema, required: true, default: () => ({}) },
yahtzee: { type: YahtzeeFieldStatsSchema, required: true, default: () => ({}) },
chance: { type: NumberFieldStatsSchema, required: true, default: () => ({}) },
grandTotal: { type: TotalFieldStatsSchema, required: true, default: () => ({}) },
lowerTotal: { type: TotalFieldStatsSchema, required: true, default: () => ({}) },
gamesPlayed: { type: Int, required: true, default: 0, min: 0 },
timesNoWinner: { type: Int, required: true, default: 0, min: 0 },
}, { _id: false });
export const PlayerStats = mongoose.model<IPlayerStatsDoc>("PlayerStats", PlayerStatsSchema);
export const AccountStats = mongoose.model<IAccountStatsDoc>("AccountStats", AccountStatsSchema);

51
src/passport-config.ts Executable file
View File

@@ -0,0 +1,51 @@
import passport from "passport";
import {Strategy as LocalStrategy, VerifyFunction} from "passport-local";
import bcrypt from "bcrypt";
import DbUser, {IDbUserDoc} from "./models/dbUser";
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();
}
};
const authenticateUser: VerifyFunction = async (email, password, done) => {
const user = await DbUser.findByEmail(email);
if (!user) {
return done(null, false, { message: "A user with that email does not exist."} );
}
try {
if (await bcrypt.compare(password, user.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: IDbUserDoc, done) => {
done(null, user._id)
});
passport.deserializeUser(async (id: string, done) => {
const user: IDbUserDoc | null = await DbUser.findById(id);
done(null, user);
});
};

12
src/routers/apiRouter.ts Executable file
View File

@@ -0,0 +1,12 @@
import express from "express";
import {requireAuthenticated, requireNotAuthenticated} from "../passport-config";
import * as stats from "../controllers/statsController";
import * as dbUser from "../controllers/dbUserController"
const router = express.Router();
router.get("/user", dbUser.whoAmI);
router.get("/games", requireAuthenticated, stats.listGames);
router.post("/savegame", requireAuthenticated, stats.saveGame);
export default router;

23
src/routers/mainRouter.ts Executable file
View File

@@ -0,0 +1,23 @@
import express from "express";
import {requireAuthenticated, requireNotAuthenticated} from "../passport-config";
import routers from "./routers";
import {IDbUser} from "../models/dbUser";
import * as path from "path";
const router = express.Router();
router.use("/account", routers.signup);
router.use("/api", routers.api);
router.use("/game/static", express.static(path.join(__dirname, "../game/static")));
router.use("/static", express.static(path.join(__dirname, "../frontend/static")));
router.use("/static", express.static("static"));
// General Endpoints
router.get("/game", requireAuthenticated, (req, res) => {
res.sendFile(path.join(__dirname + "/../game/index.html"));
});
router.get("", requireAuthenticated, (req, res) => {
res.sendFile(path.join(__dirname + "/../frontend/index.html"), {username: (req.user as IDbUser).username});
});
export default router;

9
src/routers/routers.ts Executable file
View File

@@ -0,0 +1,9 @@
import signupRouter from "./signupRouter";
import apiRouter from "./apiRouter";
const routers = {
signup: signupRouter,
api: apiRouter,
};
export default routers;

17
src/routers/signupRouter.ts Executable file
View File

@@ -0,0 +1,17 @@
import express from "express";
import {requireAuthenticated, requireNotAuthenticated} from "../passport-config";
import * as signup from "../controllers/signupController";
const router = express.Router();
router.get("/login", requireNotAuthenticated, signup.showLoginPage);
router.post("/login", requireNotAuthenticated, signup.loginUser);
router.get("/register", requireNotAuthenticated, signup.showRegistrationPage);
router.post("/register", requireNotAuthenticated, signup.registerNewUser);
router.get("/logout", requireAuthenticated, signup.logoutUser);
export default router;

4
src/server-config.json Executable file
View File

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