diff --git a/package-lock.json b/package-lock.json index 7baeb51..99cd62f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,15 @@ "@types/node": "*" } }, + "@types/compression": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.0.1.tgz", + "integrity": "sha512-GuoIYzD70h+4JUqUabsm31FGqvpCYHGKcLtor7nQ/YvUyNX0o9SJZ9boFI5HjFfbOda5Oe/XOvNK6FES8Y/79w==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, "@types/connect": { "version": "3.4.32", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", @@ -153,7 +162,8 @@ "@types/js-yaml": { "version": "3.12.1", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.1.tgz", - "integrity": "sha512-SGGAhXLHDx+PK4YLNcNGa6goPf9XRWQNAUUbffkwVGGXIxmDKWyGGL4inzq2sPmExu431Ekb9aEMn9BkPqEYFA==" + "integrity": "sha512-SGGAhXLHDx+PK4YLNcNGa6goPf9XRWQNAUUbffkwVGGXIxmDKWyGGL4inzq2sPmExu431Ekb9aEMn9BkPqEYFA==", + "dev": true }, "@types/mime": { "version": "2.0.1", @@ -215,6 +225,7 @@ "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/winston/-/winston-2.4.4.tgz", "integrity": "sha512-BVGCztsypW8EYwJ+Hq+QNYiT/MUyCif0ouBH+flrY66O5W+KIXAMML6E/0fJpm7VjIzgangahl5S03bJJQGrZw==", + "dev": true, "requires": { "winston": "*" } @@ -1310,6 +1321,40 @@ "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" }, + "compressible": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz", + "integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==", + "requires": { + "mime-db": ">= 1.40.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1361,9 +1406,9 @@ } }, "connect-pg-simple": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-6.0.0.tgz", - "integrity": "sha512-6pQnRSGFyswyHMdKQp5C+g78fjU/1/6eY05VeixXwMixw5KYhAcoOCXyf8TdPE1IzRLNDBMQi64vojXK/HMXVw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-6.0.1.tgz", + "integrity": "sha512-zW5AOtRNOLcXxphSmQ+oYj0snlLs1Je3u5K2NWyF7WhMVoPvnQXraK2wzS8f7qLwhMcmYukah2ymu0Gdxf7Qsg==", "requires": { "pg": "^7.4.3" } @@ -2691,12 +2736,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -2715,6 +2762,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -2894,7 +2942,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -3000,7 +3049,8 @@ "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, diff --git a/package.json b/package.json index 0fb8448..664e5a3 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "author": "SoftEngI", "license": "ISC", "devDependencies": { + "@types/compression": "^1.0.1", "@types/connect-pg-simple": "^4.2.0", "@types/cookie-parser": "^1.4.2", "@types/express": "^4.17.1", @@ -26,9 +27,11 @@ "@types/express-socket.io-session": "^1.3.2", "@types/fs-extra": "^8.0.0", "@types/graphql": "^14.2.3", + "@types/js-yaml": "^3.12.1", "@types/node": "^12.7.2", "@types/pg": "^7.11.0", "@types/socket.io": "^2.1.2", + "@types/winston": "^2.4.4", "delete": "^1.1.0", "gulp": "^4.0.2", "gulp-minify": "^3.1.0", @@ -40,9 +43,8 @@ "typescript": "^3.5.3" }, "dependencies": { - "@types/js-yaml": "^3.12.1", - "@types/winston": "^2.4.4", - "connect-pg-simple": "^6.0.0", + "compression": "^1.7.4", + "connect-pg-simple": "^6.0.1", "cookie-parser": "^1.4.4", "express": "^4.17.1", "express-graphql": "^0.9.0", diff --git a/src/app.ts b/src/app.ts index c7079ac..6758970 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,4 +1,12 @@ +import * as compression from "compression"; +import connectPgSimple = require("connect-pg-simple"); +import * as cookieParser from "cookie-parser"; import * as express from "express"; +import * as graphqlHTTP from "express-graphql"; +import * as session from "express-session"; +import sharedsession = require("express-socket.io-session"); +import {buildSchema} from "graphql"; +import {importSchema} from "graphql-import"; import * as http from "http"; import * as path from "path"; import * as socketIo from "socket.io"; @@ -6,6 +14,8 @@ import dataaccess from "./lib/dataaccess"; import globals from "./lib/globals"; import routes from "./routes"; +const PgSession = connectPgSimple(session); + class App { public app: express.Application; public io: socketIo.Server; @@ -23,10 +33,43 @@ class App { public async init() { await dataaccess.init(); await routes.ioListeners(this.io); + + const appSession = session({ + cookie: { + maxAge: Number(globals.config.session.cookieMaxAge), + secure: "auto", + }, + resave: false, + saveUninitialized: true, // TODO: Set to false and only save when accepted by user. + secret: globals.config.session.secret, + store: new PgSession({ + pool: dataaccess.pool, + tableName: "user_sessions", + }), + }); + + this.io.use(sharedsession(appSession, {autoSave: true})); + this.app.set("views", path.join(__dirname, "views")); this.app.set("view engine", "pug"); + this.app.set("trust proxy", 1); + + this.app.use(compression()); + this.app.use(express.json()); + this.app.use(express.urlencoded({extended: false})); this.app.use(express.static(path.join(__dirname, "public"))); + this.app.use(cookieParser()); + this.app.use(appSession); this.app.use(routes.router); + this.app.use("/graphql", graphqlHTTP(async (request, response) => { + return { + // @ts-ignore all + context: {session: request.session}, + graphiql: true, + rootValue: await routes.resolvers(request, response), + schema: buildSchema(importSchema("./public/graphql/schema.graphql")), + }; + })); } /** diff --git a/src/default-config.yaml b/src/default-config.yaml index 417ccda..e232512 100644 --- a/src/default-config.yaml +++ b/src/default-config.yaml @@ -9,3 +9,7 @@ database: # http server configuration server: port: 8080 + +session: + secret: REPLACE WITH SAFE RANDOM GENERATED SECRET + cookieMaxAge: 604800000‬ # 7 days diff --git a/src/lib/dataaccess/Profile.ts b/src/lib/dataaccess/Profile.ts new file mode 100644 index 0000000..a3f3a46 --- /dev/null +++ b/src/lib/dataaccess/Profile.ts @@ -0,0 +1,51 @@ +import {queryHelper} from "./index"; +import {User} from "./User"; + +export class Profile extends User { + /** + * Sets the greenpoints of a user. + * @param points + */ + public async setGreenpoints(points: number): Promise { + const result = await queryHelper.first({ + text: "UPDATE users SET greenpoints = $1 WHERE id = $2 RETURNING greenpoints", + values: [points, this.id], + }); + return result.greenpoints; + } + + /** + * Sets the email of the user + * @param email + */ + public async setEmail(email: string): Promise { + const result = await queryHelper.first({ + text: "UPDATE TABLE users SET email = $1 WHERE users.id = $2 RETURNING email", + values: [email, this.id], + }); + return result.email; + } + + /** + * Updates the handle of the user + */ + public async setHandle(handle: string): Promise { + const result = await queryHelper.first({ + text: "UPDATE TABLE users SET handle = $1 WHERE id = $2", + values: [handle, this.id], + }); + return result.handle; + } + + /** + * Sets the username of the user + * @param name + */ + public async setName(name: string): Promise { + const result = await queryHelper.first({ + text: "UPDATE TABLE users SET name = $1 WHERE id = $2", + values: [name, this.id], + }); + return result.name; + } +} diff --git a/src/lib/dataaccess/User.ts b/src/lib/dataaccess/User.ts index bb680c4..394c062 100644 --- a/src/lib/dataaccess/User.ts +++ b/src/lib/dataaccess/User.ts @@ -17,18 +17,6 @@ export class User extends DataObject { return this.$name; } - /** - * Sets the username of the user - * @param name - */ - public async setName(name: string): Promise { - const result = await queryHelper.first({ - text: "UPDATE TABLE users SET name = $1 WHERE id = $2", - values: [name, this.id], - }); - return result.name; - } - /** * The unique handle of the user. */ @@ -37,17 +25,6 @@ export class User extends DataObject { return this.$handle; } - /** - * Updates the handle of the user - */ - public async setHandle(handle: string): Promise { - const result = await queryHelper.first({ - text: "UPDATE TABLE users SET handle = $1 WHERE id = $2", - values: [handle, this.id], - }); - return result.handle; - } - /** * The email of the user */ @@ -56,18 +33,6 @@ export class User extends DataObject { return this.$email; } - /** - * Sets the email of the user - * @param email - */ - public async setEmail(email: string): Promise { - const result = await queryHelper.first({ - text: "UPDATE TABLE users SET email = $1 WHERE users.id = $2 RETURNING email", - values: [email, this.id], - }); - return result.email; - } - /** * The number of greenpoints of the user */ @@ -76,18 +41,6 @@ export class User extends DataObject { return this.$greenpoints; } - /** - * Sets the greenpoints of a user. - * @param points - */ - public async setGreenpoints(points: number): Promise { - const result = await queryHelper.first({ - text: "UPDATE users SET greenpoints = $1 WHERE id = $2 RETURNING greenpoints", - values: [points, this.id], - }); - return result.greenpoints; - } - /** * The date the user joined the platform */ diff --git a/src/lib/dataaccess/index.ts b/src/lib/dataaccess/index.ts index 1d610ef..5696547 100644 --- a/src/lib/dataaccess/index.ts +++ b/src/lib/dataaccess/index.ts @@ -1,6 +1,7 @@ import {Pool} from "pg"; import globals from "../globals"; import {QueryHelper} from "../QueryHelper"; +import {Profile} from "./Profile"; import {User} from "./User"; const config = globals.config; @@ -16,7 +17,18 @@ const dbClient: Pool = new Pool({ }); export const queryHelper = new QueryHelper(dbClient, tableCreationFile, tableUpdateFile); +/** + * Generates a new handle from the username and a base64 string of the current time. + * @param username + */ +function generateHandle(username: string) { + return `${username}.${Buffer.from(Date.now().toString()).toString("base64")}`; +} + namespace dataaccess { + + export const pool: Pool = dbClient; + /** * Initializes everything that needs to be initialized asynchronous. */ @@ -45,6 +57,33 @@ namespace dataaccess { return new User(result.id, result); } + /** + * Returns the user by email and password + * @param email + * @param password + */ + export async function getUserByLogin(email: string, password: string) { + const result = await this.queryHelper.first({ + text: "SELECT * FROM users WHERE email = $1 AND password = $2", + values: [email, password], + }); + return new Profile(result.id, result); + } + + /** + * Registers a user with a username and password returning a user + * @param username + * @param email + * @param password + */ + export async function registerUser(username: string, email: string, password: string) { + const result = await this.queryHelper.first({ + text: "INSERT INTO users (name, handle, password, email) VALUES ($1, $2, $3, $4) RETURNING *", + values: [username, generateHandle(username), password, email], + }); + return new Profile(result.id, result); + } + /** * Enum representing the types of votes that can be performed on a post. */ diff --git a/src/routes/index.ts b/src/routes/index.ts index 84cc7c5..e512ee6 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -28,9 +28,9 @@ namespace routes { * @param request * @param response */ - export const resolvers = async (request: any, response: any): Promise => { + export async function resolvers(request: any, response: any): Promise { return homeRoute.resolver(request, response); - }; + } /** * Assigns the io listeners or namespaces to the routes diff --git a/src/sql/create-tables.sql b/src/sql/create-tables.sql index 7c1c97a..faf2137 100644 --- a/src/sql/create-tables.sql +++ b/src/sql/create-tables.sql @@ -1,3 +1,10 @@ +CREATE TABLE IF NOT EXISTS "user_sessions" ( + "sid" varchar NOT NULL COLLATE "default", + "sess" json NOT NULL, + "expire" timestamp(6) NOT NULL, + PRIMARY KEY ("sid") NOT DEFERRABLE INITIALLY IMMEDIATE +) WITH (OIDS=FALSE); + CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, name varchar(128) NOT NULL,