Merge branch 'julius-dev' of Software_Engineering_I/greenvironment-server into develop

pull/1/head
Trivernis 5 years ago committed by Gitea
commit 6687baecce

@ -3,7 +3,7 @@ const sass = require('gulp-sass');
const ts = require('gulp-typescript'); const ts = require('gulp-typescript');
const minify = require('gulp-minify'); const minify = require('gulp-minify');
const del = require('delete'); const del = require('delete');
const gulp = require('gulp');
function clearDist(cb) { function clearDist(cb) {
del('dist/*', cb); del('dist/*', cb);

155
package-lock.json generated

@ -55,6 +55,15 @@
"@types/node": "*" "@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": { "@types/connect": {
"version": "3.4.32", "version": "3.4.32",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz",
@ -150,10 +159,32 @@
"integrity": "sha512-UoCovaxbJIxagCvVfalfK7YaNhmxj3BQFRQ2RHQKLiu+9wNXhJnlbspsLHt/YQM99IaLUUFJNzCwzc6W0ypMeQ==", "integrity": "sha512-UoCovaxbJIxagCvVfalfK7YaNhmxj3BQFRQ2RHQKLiu+9wNXhJnlbspsLHt/YQM99IaLUUFJNzCwzc6W0ypMeQ==",
"dev": true "dev": true
}, },
"@types/http-status": {
"version": "0.2.30",
"resolved": "https://registry.npmjs.org/@types/http-status/-/http-status-0.2.30.tgz",
"integrity": "sha512-wcBc5XEOMmhuoWfNhwnpw8+tVAsueUeARxCTcRQ0BCN5V/dyKQBJNWdxmvcZW5IJWoeU47UWQ+ACCg48KKnqyA==",
"dev": true
},
"@types/js-yaml": { "@types/js-yaml": {
"version": "3.12.1", "version": "3.12.1",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.1.tgz", "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/linkify-it": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-2.1.0.tgz",
"integrity": "sha512-Q7DYAOi9O/+cLLhdaSvKdaumWyHbm7HAk/bFwwyTuU0arR5yyCeW5GOoqt4tJTpDRxhpx9Q8kQL6vMpuw9hDSw==",
"dev": true
},
"@types/markdown-it": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-0.0.9.tgz",
"integrity": "sha512-IFSepyZXbF4dgSvsk8EsgaQ/8Msv1I5eTL0BZ0X3iGO9jw6tCVtPG8HchIPm3wrkmGdqZOD42kE0zplVi1gYDA==",
"dev": true,
"requires": {
"@types/linkify-it": "*"
}
}, },
"@types/mime": { "@types/mime": {
"version": "2.0.1", "version": "2.0.1",
@ -162,9 +193,9 @@
"dev": true "dev": true
}, },
"@types/node": { "@types/node": {
"version": "12.7.2", "version": "12.7.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.8.tgz",
"integrity": "sha512-dyYO+f6ihZEtNPDcWNR1fkoTDf3zAK3lAABDze3mz6POyIercH0lEUawUFXlG8xaQZmm1yEBON/4TsYv/laDYg==", "integrity": "sha512-FMdVn84tJJdV+xe+53sYiZS4R5yn1mAIxfj+DVoNiQjTYz1+OYmjwEZr1ev9nU0axXwda0QDbYl06QHanRVH3A==",
"dev": true "dev": true
}, },
"@types/pg": { "@types/pg": {
@ -215,6 +246,7 @@
"version": "2.4.4", "version": "2.4.4",
"resolved": "https://registry.npmjs.org/@types/winston/-/winston-2.4.4.tgz", "resolved": "https://registry.npmjs.org/@types/winston/-/winston-2.4.4.tgz",
"integrity": "sha512-BVGCztsypW8EYwJ+Hq+QNYiT/MUyCif0ouBH+flrY66O5W+KIXAMML6E/0fJpm7VjIzgangahl5S03bJJQGrZw==", "integrity": "sha512-BVGCztsypW8EYwJ+Hq+QNYiT/MUyCif0ouBH+flrY66O5W+KIXAMML6E/0fJpm7VjIzgangahl5S03bJJQGrZw==",
"dev": true,
"requires": { "requires": {
"winston": "*" "winston": "*"
} }
@ -1310,6 +1342,40 @@
"resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
"integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" "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": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -1361,9 +1427,9 @@
} }
}, },
"connect-pg-simple": { "connect-pg-simple": {
"version": "6.0.0", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-6.0.0.tgz", "resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-6.0.1.tgz",
"integrity": "sha512-6pQnRSGFyswyHMdKQp5C+g78fjU/1/6eY05VeixXwMixw5KYhAcoOCXyf8TdPE1IzRLNDBMQi64vojXK/HMXVw==", "integrity": "sha512-zW5AOtRNOLcXxphSmQ+oYj0snlLs1Je3u5K2NWyF7WhMVoPvnQXraK2wzS8f7qLwhMcmYukah2ymu0Gdxf7Qsg==",
"requires": { "requires": {
"pg": "^7.4.3" "pg": "^7.4.3"
} }
@ -1843,6 +1909,11 @@
"has-binary2": "~1.0.2" "has-binary2": "~1.0.2"
} }
}, },
"entities": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
"integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw=="
},
"env-variable": { "env-variable": {
"version": "0.0.5", "version": "0.0.5",
"resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.5.tgz", "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.5.tgz",
@ -2481,8 +2552,7 @@
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
@ -2503,14 +2573,12 @@
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@ -2525,20 +2593,17 @@
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@ -2655,8 +2720,7 @@
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@ -2668,7 +2732,6 @@
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@ -2683,7 +2746,6 @@
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@ -2795,8 +2857,7 @@
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@ -2808,7 +2869,6 @@
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@ -2930,7 +2990,6 @@
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@ -2950,7 +3009,6 @@
"version": "3.0.1", "version": "3.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
@ -2994,8 +3052,7 @@
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.0.3", "version": "3.0.3",
@ -3633,6 +3690,11 @@
"sshpk": "^1.7.0" "sshpk": "^1.7.0"
} }
}, },
"http-status": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/http-status/-/http-status-1.3.2.tgz",
"integrity": "sha512-vR1YTaDyi2BukI0UiH01xy92oiZi4in7r0dmSPnrZg72Vu1SzyOLalwWP5NUk1rNiB2L+XVK2lcSVOqaertX8A=="
},
"iconv-lite": { "iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -4166,6 +4228,14 @@
"resolve": "^1.1.7" "resolve": "^1.1.7"
} }
}, },
"linkify-it": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
"integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
"requires": {
"uc.micro": "^1.0.1"
}
},
"load-json-file": { "load-json-file": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
@ -4272,6 +4342,23 @@
"object-visit": "^1.0.0" "object-visit": "^1.0.0"
} }
}, },
"markdown-it": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz",
"integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==",
"requires": {
"argparse": "^1.0.7",
"entities": "~2.0.0",
"linkify-it": "^2.0.0",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
}
},
"markdown-it-emoji": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz",
"integrity": "sha1-m+4OmpkKljupbfaYDE/dsF37Tcw="
},
"matchdep": { "matchdep": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz",
@ -4312,6 +4399,11 @@
"resolve-dir": "^1.0.0" "resolve-dir": "^1.0.0"
} }
}, },
"mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
"integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4="
},
"media-typer": { "media-typer": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -6849,6 +6941,11 @@
"integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==",
"dev": true "dev": true
}, },
"uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
},
"uglify-js": { "uglify-js": {
"version": "2.8.29", "version": "2.8.29",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",

@ -18,6 +18,7 @@
"author": "SoftEngI", "author": "SoftEngI",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@types/compression": "^1.0.1",
"@types/connect-pg-simple": "^4.2.0", "@types/connect-pg-simple": "^4.2.0",
"@types/cookie-parser": "^1.4.2", "@types/cookie-parser": "^1.4.2",
"@types/express": "^4.17.1", "@types/express": "^4.17.1",
@ -26,9 +27,13 @@
"@types/express-socket.io-session": "^1.3.2", "@types/express-socket.io-session": "^1.3.2",
"@types/fs-extra": "^8.0.0", "@types/fs-extra": "^8.0.0",
"@types/graphql": "^14.2.3", "@types/graphql": "^14.2.3",
"@types/node": "^12.7.2", "@types/http-status": "^0.2.30",
"@types/js-yaml": "^3.12.1",
"@types/markdown-it": "0.0.9",
"@types/node": "^12.7.8",
"@types/pg": "^7.11.0", "@types/pg": "^7.11.0",
"@types/socket.io": "^2.1.2", "@types/socket.io": "^2.1.2",
"@types/winston": "^2.4.4",
"delete": "^1.1.0", "delete": "^1.1.0",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-minify": "^3.1.0", "gulp-minify": "^3.1.0",
@ -40,9 +45,8 @@
"typescript": "^3.5.3" "typescript": "^3.5.3"
}, },
"dependencies": { "dependencies": {
"@types/js-yaml": "^3.12.1", "compression": "^1.7.4",
"@types/winston": "^2.4.4", "connect-pg-simple": "^6.0.1",
"connect-pg-simple": "^6.0.0",
"cookie-parser": "^1.4.4", "cookie-parser": "^1.4.4",
"express": "^4.17.1", "express": "^4.17.1",
"express-graphql": "^0.9.0", "express-graphql": "^0.9.0",
@ -52,7 +56,10 @@
"g": "^2.0.1", "g": "^2.0.1",
"graphql": "^14.4.2", "graphql": "^14.4.2",
"graphql-import": "^0.7.1", "graphql-import": "^0.7.1",
"http-status": "^1.3.2",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"markdown-it": "^10.0.0",
"markdown-it-emoji": "^1.4.0",
"pg": "^7.12.1", "pg": "^7.12.1",
"pug": "^2.0.4", "pug": "^2.0.4",
"socket.io": "^2.2.0", "socket.io": "^2.2.0",

@ -1,11 +1,23 @@
import * as compression from "compression";
import connectPgSimple = require("connect-pg-simple");
import * as cookieParser from "cookie-parser";
import * as express from "express"; 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 http from "http";
import * as path from "path"; import * as path from "path";
import * as socketIo from "socket.io"; import * as socketIo from "socket.io";
import dataaccess from "./lib/dataaccess"; import dataaccess, {queryHelper} from "./lib/dataaccess";
import globals from "./lib/globals"; import globals from "./lib/globals";
import routes from "./routes"; import routes from "./routes";
const logger = globals.logger;
const PgSession = connectPgSimple(session);
class App { class App {
public app: express.Application; public app: express.Application;
public io: socketIo.Server; public io: socketIo.Server;
@ -23,10 +35,47 @@ class App {
public async init() { public async init() {
await dataaccess.init(); await dataaccess.init();
await routes.ioListeners(this.io); await routes.ioListeners(this.io);
const appSession = session({
cookie: {
maxAge: Number(globals.config.session.cookieMaxAge) || 604800000,
secure: "auto",
},
resave: false,
saveUninitialized: false,
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("views", path.join(__dirname, "views"));
this.app.set("view engine", "pug"); 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(express.static(path.join(__dirname, "public")));
this.app.use(cookieParser());
this.app.use(appSession);
this.app.use((req, res, next) => {
logger.verbose(`${req.method} ${req.url}`);
next();
});
this.app.use(routes.router); this.app.use(routes.router);
this.app.use("/graphql", graphqlHTTP((request, response) => {
return {
// @ts-ignore all
context: {session: request.session},
graphiql: true,
rootValue: routes.resolvers(request, response),
schema: buildSchema(importSchema(path.join(__dirname, "./public/graphql/schema.graphql"))),
};
}));
} }
/** /**
@ -34,11 +83,11 @@ class App {
*/ */
public start() { public start() {
if (globals.config.server.port) { if (globals.config.server.port) {
globals.logger.info(`Starting server...`); logger.info(`Starting server...`);
this.app.listen(globals.config.server.port); this.app.listen(globals.config.server.port);
globals.logger.info(`Server running on port ${globals.config.server.port}`); logger.info(`Server running on port ${globals.config.server.port}`);
} else { } else {
globals.logger.error("No port specified in the config." + logger.error("No port specified in the config." +
"Please configure a port in the config.yaml."); "Please configure a port in the config.yaml.");
} }
} }

@ -9,3 +9,14 @@ database:
# http server configuration # http server configuration
server: server:
port: 8080 port: 8080
session:
secret: REPLACE WITH SAFE RANDOM GENERATED SECRET
cookieMaxAge: 604800000 # 7 days
markdown:
plugins:
- 'markdown-it-emoji'
logging:
level: info

@ -0,0 +1,73 @@
import * as crypto from "crypto";
import {EventEmitter} from "events";
export class MemoryCache extends EventEmitter {
private cacheItems: any = {};
private cacheExpires: any = {};
private expireCheck: NodeJS.Timeout;
/**
* Creates interval function.
* @param ttl
*/
constructor(private ttl: number = 500) {
super();
this.expireCheck = setInterval(() => this.checkExpires(), ttl / 2);
}
/**
* Creates a md5 hash of the given key.
* @param key
*/
public hashKey(key: string): string {
const hash = crypto.createHash("md5");
const data = hash.update(key, "utf8");
return data.digest("hex");
}
/**
* Sets an entry.
* @param key
* @param value
*/
public set(key: string, value: any) {
this.cacheItems[key] = value;
this.cacheExpires[key] = Date.now() + this.ttl;
this.emit("set", key, value);
}
/**
* Returns the entry stored with the given key.
* @param key
*/
public get(key: string) {
if (this.cacheItems.hasOwnProperty(key)) {
this.emit("hit", key, this.cacheItems[key]);
return this.cacheItems[key];
} else {
this.emit("miss", key);
}
}
/**
* Deletes a cache item.
* @param key
*/
public delete(key: string) {
this.emit("delete", key);
delete this.cacheItems[key];
}
/**
* Checks expires and clears items that are over the expire value.
*/
private checkExpires() {
for (const [key, value] of Object.entries(this.cacheExpires)) {
if (value < Date.now()) {
this.emit("delete", key);
delete this.cacheItems[key];
delete this.cacheExpires[key];
}
}
}
}

@ -11,6 +11,10 @@ import globals from "./globals";
const logger = globals.logger; const logger = globals.logger;
export interface IAdvancedQueryConfig extends QueryConfig {
cache?: boolean;
}
/** /**
* Transaction class to wrap SQL transactions. * Transaction class to wrap SQL transactions.
*/ */
@ -54,7 +58,7 @@ export class SqlTransaction {
/** /**
* Releases the client back to the pool. * Releases the client back to the pool.
*/ */
public async release() { public release() {
this.client.release(); this.client.release();
} }
} }
@ -101,7 +105,7 @@ export class QueryHelper {
* executes the sql query with values and returns all results. * executes the sql query with values and returns all results.
* @param query * @param query
*/ */
public async all(query: QueryConfig): Promise<any[]> { public async all(query: IAdvancedQueryConfig): Promise<any[]> {
const result = await this.query(query); const result = await this.query(query);
return result.rows; return result.rows;
} }
@ -110,7 +114,7 @@ export class QueryHelper {
* executes the sql query with values and returns the first result. * executes the sql query with values and returns the first result.
* @param query * @param query
*/ */
public async first(query: QueryConfig): Promise<any> { public async first(query: IAdvancedQueryConfig): Promise<any> {
const result = await this.query(query); const result = await this.query(query);
if (result.rows && result.rows.length > 0) { if (result.rows && result.rows.length > 0) {
return result.rows[0]; return result.rows[0];
@ -129,11 +133,26 @@ export class QueryHelper {
* Queries the database with error handling. * Queries the database with error handling.
* @param query - the sql and values to execute * @param query - the sql and values to execute
*/ */
private async query(query: QueryConfig): Promise<QueryResult|{rows: any}> { private async query(query: IAdvancedQueryConfig): Promise<QueryResult|{rows: any}> {
try { try {
return await this.pool.query(query); query.text = query.text.replace(/[\r\n]/g, " ");
globals.logger.silly(`Executing sql '${JSON.stringify(query)}'`);
if (query.cache) {
const key = globals.cache.hashKey(JSON.stringify(query));
const cacheResult = globals.cache.get(key);
if (cacheResult) {
return cacheResult;
} else {
const result = await this.pool.query(query);
globals.cache.set(key, result);
return result;
}
} else {
return await this.pool.query(query);
}
} catch (err) { } catch (err) {
logger.debug(`Error on query "${query}".`); logger.debug(`Error on query "${JSON.stringify(query)}".`);
logger.error(`Sql query failed: ${err}`); logger.error(`Sql query failed: ${err}`);
logger.verbose(err.stack); logger.verbose(err.stack);
return { return {

@ -0,0 +1,18 @@
import markdown from "../markdown";
import {Chatroom} from "./Chatroom";
import {User} from "./User";
export class ChatMessage {
constructor(
public readonly author: User,
public readonly chat: Chatroom,
public readonly createdAt: number,
public readonly content: string) {}
/**
* The content rendered by markdown-it.
*/
public htmlContent(): string {
return markdown.renderInline(this.content);
}
}

@ -0,0 +1,74 @@
import globals from "../globals";
import {ChatMessage} from "./ChatMessage";
import {queryHelper} from "./index";
import {User} from "./User";
export class Chatroom {
constructor(private readonly id: number) {
this.id = Number(id);
}
/**
* Returns if the chat exists.
*/
public async exists(): Promise<boolean> {
const result = await queryHelper.first({
text: "SELECT id FROM chats WHERE id = $1",
values: [this.id],
});
return !!result.id;
}
/**
* Returns all members of a chatroom.
*/
public async members(): Promise<User[]> {
const result = await queryHelper.all({
cache: true,
text: `SELECT * FROM chat_members
JOIN users ON (chat_members.member = users.id)
WHERE chat_members.chat = $1;`,
values: [this.id],
});
const chatMembers = [];
for (const row of result) {
const user = new User(row.id, row);
chatMembers.push(user);
}
return chatMembers;
}
/**
* Returns messages of the chat
* @param limit - the limit of messages to return
* @param offset - the offset of messages to return
* @param containing - filter by containing
*/
public async messages({first, offset, containing}: {first?: number, offset?: number, containing?: string}) {
const lim = first || 16;
const offs = offset || 0;
const result = await queryHelper.all({
cache: true,
text: "SELECT * FROM chat_messages WHERE chat = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3",
values: [this.id, lim, offs],
});
const messages = [];
const users: any = {};
for (const row of result) {
if (!users[row.author]) {
const user = new User(row.author);
await user.exists();
users[row.author] = user;
}
messages.push(new ChatMessage(users[row.author], this, row.created_at, row.content));
}
if (containing) {
return messages.filter((x) => x.content.includes(containing));
} else {
return messages;
}
}
}

@ -1,10 +1,23 @@
/** /**
* abstact DataObject class * abstact DataObject class
*/ */
export abstract class DataObject { import {EventEmitter} from "events";
export abstract class DataObject extends EventEmitter {
protected dataLoaded: boolean = false; protected dataLoaded: boolean = false;
private loadingData: boolean = false;
constructor(public id: number, protected row?: any) { constructor(public id: number, protected row?: any) {
super();
this.id = Number(id);
}
/**
* Returns if the object extists by trying to load data.
*/
public async exists() {
await this.loadDataIfNotExists();
return this.dataLoaded;
} }
protected abstract loadData(): Promise<void>; protected abstract loadData(): Promise<void>;
@ -12,9 +25,16 @@ export abstract class DataObject {
/** /**
* Loads data from the database if data has not been loaded * Loads data from the database if data has not been loaded
*/ */
protected loadDataIfNotExists() { protected async loadDataIfNotExists() {
if (this.dataLoaded) { if (!this.dataLoaded && !this.loadingData) {
this.loadData(); this.loadingData = true;
await this.loadData();
this.loadingData = false;
this.emit("loaded");
} else if (this.loadingData) {
return new Promise((res) => {
this.on("loaded", () => res());
});
} }
} }
} }

@ -1,3 +1,4 @@
import markdown from "../markdown";
import {DataObject} from "./DataObject"; import {DataObject} from "./DataObject";
import {queryHelper} from "./index"; import {queryHelper} from "./index";
import dataaccess from "./index"; import dataaccess from "./index";
@ -5,8 +6,6 @@ import {User} from "./User";
export class Post extends DataObject { export class Post extends DataObject {
public readonly id: number; public readonly id: number;
private $upvotes: number;
private $downvotes: number;
private $createdAt: string; private $createdAt: string;
private $content: string; private $content: string;
private $author: number; private $author: number;
@ -16,31 +15,47 @@ export class Post extends DataObject {
* Returns the upvotes of a post. * Returns the upvotes of a post.
*/ */
public async upvotes(): Promise<number> { public async upvotes(): Promise<number> {
this.loadDataIfNotExists(); const result = await queryHelper.first({
return this.$upvotes; cache: true,
text: "SELECT COUNT(*) count FROM votes WHERE item_id = $1 AND vote_type = 'UPVOTE'",
values: [this.id],
});
return result.count;
} }
/** /**
* Returns the downvotes of the post * Returns the downvotes of the post
*/ */
public async downvotes(): Promise<number> { public async downvotes(): Promise<number> {
this.loadDataIfNotExists(); const result = await queryHelper.first({
return this.$downvotes; cache: true,
text: "SELECT COUNT(*) count FROM votes WHERE item_id = $1 AND vote_type = 'DOWNVOTE'",
values: [this.id],
});
return result.count;
} }
/** /**
* The content of the post (markdown) * The content of the post (markdown)
*/ */
public async content(): Promise<string> { public async content(): Promise<string> {
this.loadDataIfNotExists(); await this.loadDataIfNotExists();
return this.$content; return this.$content;
} }
/**
* the content rendered by markdown-it.
*/
public async htmlContent(): Promise<string> {
await this.loadDataIfNotExists();
return markdown.render(this.$content);
}
/** /**
* The date the post was created at. * The date the post was created at.
*/ */
public async createdAt(): Promise<string> { public async createdAt(): Promise<string> {
this.loadDataIfNotExists(); await this.loadDataIfNotExists();
return this.$createdAt; return this.$createdAt;
} }
@ -48,7 +63,7 @@ export class Post extends DataObject {
* The autor of the post. * The autor of the post.
*/ */
public async author(): Promise<User> { public async author(): Promise<User> {
this.loadDataIfNotExists(); await this.loadDataIfNotExists();
return new User(this.$author); return new User(this.$author);
} }
@ -67,6 +82,7 @@ export class Post extends DataObject {
*/ */
public async userVote(userId: number): Promise<dataaccess.VoteType> { public async userVote(userId: number): Promise<dataaccess.VoteType> {
const result = await queryHelper.first({ const result = await queryHelper.first({
cache: true,
text: "SELECT vote_type FROM votes WHERE user_id = $1 AND item_id = $2", text: "SELECT vote_type FROM votes WHERE user_id = $1 AND item_id = $2",
values: [userId, this.id], values: [userId, this.id],
}); });
@ -77,6 +93,34 @@ export class Post extends DataObject {
} }
} }
/**
* Performs a vote on a post.
* @param userId
* @param type
*/
public async vote(userId: number, type: dataaccess.VoteType): Promise<dataaccess.VoteType> {
const uVote = await this.userVote(userId);
if (uVote === type) {
await queryHelper.first({
text: "DELETE FROM votes WHERE item_id = $1 AND user_id = $2",
values: [this.id, userId],
});
} else {
if (uVote) {
await queryHelper.first({
text: "UPDATE votes SET vote_type = $1 WHERE user_id = $2 AND item_id = $3",
values: [type, userId, this.id],
});
} else {
await queryHelper.first({
text: "INSERT INTO votes (user_id, item_id, vote_type) values ($1, $2, $3)",
values: [userId, this.id, type],
});
}
return type;
}
}
/** /**
* Loads the data from the database if needed. * Loads the data from the database if needed.
*/ */
@ -86,6 +130,7 @@ export class Post extends DataObject {
result = this.row; result = this.row;
} else { } else {
result = await queryHelper.first({ result = await queryHelper.first({
cache: true,
text: "SELECT * FROM posts WHERE posts.id = $1", text: "SELECT * FROM posts WHERE posts.id = $1",
values: [this.id], values: [this.id],
}); });
@ -93,8 +138,6 @@ export class Post extends DataObject {
if (result) { if (result) {
this.$author = result.author; this.$author = result.author;
this.$content = result.content; this.$content = result.content;
this.$downvotes = result.downvotes;
this.$upvotes = result.upvotes;
this.$createdAt = result.created_at; this.$createdAt = result.created_at;
this.$type = result.type; this.$type = result.type;
this.dataLoaded = true; this.dataLoaded = true;

@ -0,0 +1,163 @@
import {RequestNotFoundError} from "../errors/RequestNotFoundError";
import {Chatroom} from "./Chatroom";
import dataaccess, {queryHelper} from "./index";
import {User} from "./User";
import {Request} from "./Request";
export class Profile extends User {
/**
* Returns all chatrooms (with pagination).
* Skips the query if the user doesn't exist.
* @param first
* @param offset
*/
public async chats({first, offset}: {first: number, offset?: number}): Promise<Chatroom[]> {
if (!(await this.exists())) {
return [];
}
first = first || 10;
offset = offset || 0;
const result = await queryHelper.all({
text: "SELECT chat FROM chat_members WHERE member = $1 LIMIT $2 OFFSET $3",
values: [this.id, first, offset],
});
if (result) {
return result.map((row) => new Chatroom(row.chat));
} else {
return [];
}
}
/**
* Returns all open requests the user has send.
*/
public async sentRequests() {
const result = await queryHelper.all({
cache: true,
text: "SELECT * FROM requests WHERE sender = $1",
values: [this.id],
});
return this.getRequests(result);
}
/**
* Returns all received requests of the user.
*/
public async receivedRequests() {
const result = await queryHelper.all({
cache: true,
text: "SELECT * FROM requests WHERE receiver = $1",
values: [this.id],
});
return this.getRequests(result);
}
/**
* Sets the greenpoints of a user.
* @param points
*/
public async setGreenpoints(points: number): Promise<number> {
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<string> {
const result = await queryHelper.first({
text: "UPDATE 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<string> {
const result = await queryHelper.first({
text: "UPDATE 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<string> {
const result = await queryHelper.first({
text: "UPDATE users SET name = $1 WHERE id = $2",
values: [name, this.id],
});
return result.name;
}
/**
* Denys a request.
* @param sender
* @param type
*/
public async denyRequest(sender: number, type: dataaccess.RequestType) {
await queryHelper.first({
text: "DELETE FROM requests WHERE receiver = $1 AND sender = $2 AND type = $3",
values: [this.id, sender, type],
});
}
/**
* Accepts a request.
* @param sender
* @param type
*/
public async acceptRequest(sender: number, type: dataaccess.RequestType) {
const exists = await queryHelper.first({
cache: true,
text: "SELECT 1 FROM requests WHERE receiver = $1 AND sender = $2 AND type = $3",
values: [this.id, sender, type],
});
if (exists) {
if (type === dataaccess.RequestType.FRIENDREQUEST) {
await queryHelper.first({
text: "INSERT INTO user_friends (user_id, friend_id) VALUES ($1, $2)",
values: [this.id, sender],
});
}
} else {
throw new RequestNotFoundError(sender, this.id, type);
}
}
/**
* Returns request wrapper for a row database request result.
* @param rows
*/
private getRequests(rows: any) {
const requests = [];
const requestUsers: any = {};
for (const row of rows) {
let sender = requestUsers[row.sender];
if (!sender) {
sender = new User(row.sender);
requestUsers[row.sender] = sender;
}
let receiver = requestUsers[row.receiver];
if (!receiver) {
receiver = new User(row.receiver);
requestUsers[row.receiver] = receiver;
}
requests.push(new Request(sender, receiver, row.type));
}
return requests;
}
}

@ -0,0 +1,12 @@
import dataaccess from "./index";
import {User} from "./User";
/**
* Represents a request to a user.
*/
export class Request {
constructor(
public readonly sender: User,
public readonly receiver: User,
public readonly type: dataaccess.RequestType) {}
}

@ -1,3 +1,4 @@
import globals from "../globals";
import {DataObject} from "./DataObject"; import {DataObject} from "./DataObject";
import {queryHelper} from "./index"; import {queryHelper} from "./index";
import {Post} from "./Post"; import {Post} from "./Post";
@ -8,102 +9,91 @@ export class User extends DataObject {
private $email: string; private $email: string;
private $greenpoints: number; private $greenpoints: number;
private $joinedAt: string; private $joinedAt: string;
private $exists: boolean;
/** /**
* The name of the user * The name of the user
*/ */
public async name(): Promise<string> { public async name(): Promise<string> {
this.loadDataIfNotExists(); await this.loadDataIfNotExists();
return this.$name; return this.$name;
} }
/**
* Sets the username of the user
* @param name
*/
public async setName(name: string): Promise<string> {
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. * The unique handle of the user.
*/ */
public async handle(): Promise<string> { public async handle(): Promise<string> {
this.loadDataIfNotExists(); await this.loadDataIfNotExists();
return this.$handle; return this.$handle;
} }
/**
* Updates the handle of the user
*/
public async setHandle(handle: string): Promise<string> {
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 * The email of the user
*/ */
public async email(): Promise<string> { public async email(): Promise<string> {
this.loadDataIfNotExists(); await this.loadDataIfNotExists();
return this.$email; return this.$email;
} }
/**
* Sets the email of the user
* @param email
*/
public async setEmail(email: string): Promise<string> {
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 * The number of greenpoints of the user
*/ */
public async greenpoints(): Promise<number> { public async greenpoints(): Promise<number> {
this.loadDataIfNotExists(); await this.loadDataIfNotExists();
return this.$greenpoints; return this.$greenpoints;
} }
/** /**
* Sets the greenpoints of a user. * Returns the number of posts the user created
* @param points
*/ */
public async setGreenpoints(points: number): Promise<number> { public async numberOfPosts(): Promise<number> {
const result = await queryHelper.first({ const result = await queryHelper.first({
text: "UPDATE users SET greenpoints = $1 WHERE id = $2 RETURNING greenpoints", cache: true,
values: [points, this.id], text: "SELECT COUNT(*) count FROM posts WHERE author = $1",
values: [this.id],
}); });
return result.greenpoints; return result.count;
} }
/** /**
* The date the user joined the platform * The date the user joined the platform
*/ */
public async joinedAt(): Promise<Date> { public async joinedAt(): Promise<Date> {
this.loadDataIfNotExists(); await this.loadDataIfNotExists();
return new Date(this.$joinedAt); return new Date(this.$joinedAt);
} }
/** /**
* Returns all posts for a user. * Returns all friends of the user.
*/ */
public async posts(): Promise<Post[]> { public async friends(): Promise<User[]> {
const result = await queryHelper.all({ const result = await queryHelper.all({
text: "SELECT * FROM posts WHERE author = $1", cache: true,
text: "SELECT * FROM user_friends WHERE user_id = $1 OR friend_id = $1",
values: [this.id], values: [this.id],
}); });
const userFriends = [];
for (const row of result) {
if (row.user_id === this.id) {
userFriends.push(new User(row.friend_id));
} else {
userFriends.push(new User(row.user_id));
}
}
return userFriends;
}
/**
* Returns all posts for a user.
*/
public async posts({first, offset}: {first: number, offset: number}): Promise<Post[]> {
first = first || 10;
offset = offset || 0;
const result = await queryHelper.all({
cache: true,
text: "SELECT * FROM posts WHERE author = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3",
values: [this.id, first, offset],
});
const posts = []; const posts = [];
for (const row of result) { for (const row of result) {
@ -121,7 +111,8 @@ export class User extends DataObject {
result = this.row; result = this.row;
} else { } else {
result = await queryHelper.first({ result = await queryHelper.first({
text: "SELECT * FROM users WHERE user.id = $1", cache: true,
text: "SELECT * FROM users WHERE users.id = $1",
values: [this.id], values: [this.id],
}); });
} }

@ -1,6 +1,13 @@
import {Pool} from "pg"; import {Pool} from "pg";
import {ChatNotFoundError} from "../errors/ChatNotFoundError";
import {UserNotFoundError} from "../errors/UserNotFoundError";
import globals from "../globals"; import globals from "../globals";
import {QueryHelper} from "../QueryHelper"; import {QueryHelper} from "../QueryHelper";
import {ChatMessage} from "./ChatMessage";
import {Chatroom} from "./Chatroom";
import {Post} from "./Post";
import {Profile} from "./Profile";
import {Request} from "./Request";
import {User} from "./User"; import {User} from "./User";
const config = globals.config; const config = globals.config;
@ -16,33 +23,182 @@ const dbClient: Pool = new Pool({
}); });
export const queryHelper = new QueryHelper(dbClient, tableCreationFile, tableUpdateFile); 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 with functions to fetch initial data for wrapping.
*/
namespace dataaccess { namespace dataaccess {
export const pool: Pool = dbClient;
/** /**
* Initializes everything that needs to be initialized asynchronous. * Initializes everything that needs to be initialized asynchronous.
*/ */
export async function init() { export async function init() {
await queryHelper.updateTableDefinitions();
await queryHelper.createTables(); await queryHelper.createTables();
} await queryHelper.updateTableDefinitions();
/**
* Returns the user by id
* @param userId
*/
export function getUser(userId: number) {
return new User(userId);
} }
/** /**
* Returns the user by handle. * Returns the user by handle.
* @param userHandle * @param userHandle
*/ */
export async function getUserByHandle(userHandle: string) { export async function getUserByHandle(userHandle: string): Promise<User> {
const result = await this.queryHelper.first({ const result = await queryHelper.first({
text: "SELECT * FROM users WHERE users.handle = $1", text: "SELECT * FROM users WHERE users.handle = $1",
values: [userHandle], values: [userHandle],
}); });
return new User(result.id, result); if (result) {
return new User(result.id, result);
} else {
throw new UserNotFoundError(userHandle);
}
}
/**
* Returns the user by email and password
* @param email
* @param password
*/
export async function getUserByLogin(email: string, password: string): Promise<Profile> {
const result = await queryHelper.first({
text: "SELECT * FROM users WHERE email = $1 AND password = $2",
values: [email, password],
});
if (result) {
return new Profile(result.id, result);
} else {
throw new UserNotFoundError(email);
}
}
/**
* 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 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);
}
/**
* Returns a post for a given postId.s
* @param postId
*/
export async function getPost(postId: number): Promise<Post> {
const result = await queryHelper.first({
text: "SELECT * FROM posts WHERE id = $1",
values: [postId],
});
if (result) {
return new Post(result.id, result);
} else {
return null;
}
}
/**
* Creates a post
* @param content
* @param authorId
* @param type
*/
export async function createPost(content: string, authorId: number, type?: string): Promise<Post> {
type = type || "MISC";
const result = await queryHelper.first({
text: "INSERT INTO posts (content, author, type) VALUES ($1, $2, $3) RETURNING *",
values: [content, authorId, type],
});
return new Post(result.id, result);
}
/**
* Deletes a post
* @param postId
*/
export async function deletePost(postId: number): Promise<boolean> {
const result = await queryHelper.first({
text: "DELETE FROM posts WHERE posts.id = $1",
values: [postId],
});
return true;
}
/**
* Creates a chatroom containing two users
* @param members
*/
export async function createChat(...members: number[]): Promise<Chatroom> {
const idResult = await queryHelper.first({
text: "INSERT INTO chats (id) values (default) RETURNING *;",
});
const id = idResult.id;
const transaction = await queryHelper.createTransaction();
try {
await transaction.begin();
for (const member of members) {
await transaction.query({
name: "chat-member-insert",
text: "INSERT INTO chat_members (chat, member) VALUES ($1, $2);",
values: [id, member],
});
}
await transaction.commit();
} catch (err) {
globals.logger.warn(`Failed to insert chatmember into database: ${err.message}`);
globals.logger.debug(err.stack);
await transaction.rollback();
} finally {
transaction.release();
}
return new Chatroom(id);
}
/**
* Sends a message into a chat.
* @param authorId
* @param chatId
* @param content
*/
export async function sendChatMessage(authorId: number, chatId: number, content: string) {
const chat = new Chatroom(chatId);
if ((await chat.exists())) {
const result = await queryHelper.first({
text: "INSERT INTO chat_messages (chat, author, content) values ($1, $2, $3) RETURNING *",
values: [chatId, authorId, content],
});
return new ChatMessage(new User(result.author), chat, result.created_at, result.content);
} else {
throw new ChatNotFoundError(chatId);
}
}
/**
* Sends a request to a user.
* @param sender
* @param receiver
* @param type
*/
export async function createRequest(sender: number, receiver: number, type?: RequestType) {
type = type || RequestType.FRIENDREQUEST;
const result = await queryHelper.first({
text: "INSERT INTO requests (sender, receiver, type) VALUES ($1, $2, $3) RETURNING *",
values: [sender, receiver, type],
});
return new Request(new User(result.sender), new User(result.receiver), result.type);
} }
/** /**

@ -0,0 +1,13 @@
import {GraphQLError} from "graphql";
/**
* Base error class.
*/
export class BaseError extends Error {
public readonly graphqlError: GraphQLError;
constructor(message?: string, friendlyMessage?: string) {
super(message);
this.graphqlError = new GraphQLError(friendlyMessage || message);
}
}

@ -0,0 +1,7 @@
import {BaseError} from "./BaseError";
export class ChatNotFoundError extends BaseError {
constructor(chatId: number) {
super(`Chat with id ${chatId} not found.`);
}
}

@ -0,0 +1,9 @@
import dataaccess from "../dataaccess";
import {BaseError} from "./BaseError";
export class RequestNotFoundError extends BaseError {
constructor(sender: number, receiver: number, type: dataaccess.RequestType) {
super(`Request with sender '${sender}' and receiver '${receiver}' of type '${type}' not found.`);
}
}

@ -0,0 +1,7 @@
import {BaseError} from "./BaseError";
export class UserNotFoundError extends BaseError {
constructor(username: string) {
super(`User ${username} not found!`);
}
}

@ -0,0 +1,9 @@
import {GraphQLError} from "graphql";
import {BaseError} from "./BaseError";
export class NotLoggedInGqlError extends GraphQLError {
constructor() {
super("Not logged in");
}
}

@ -8,6 +8,7 @@
import * as fsx from "fs-extra"; import * as fsx from "fs-extra";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import * as winston from "winston"; import * as winston from "winston";
import {MemoryCache} from "./MemoryCache";
const configPath = "config.yaml"; const configPath = "config.yaml";
const defaultConfig = __dirname + "/../default-config.yaml"; const defaultConfig = __dirname + "/../default-config.yaml";
@ -15,6 +16,10 @@ const defaultConfig = __dirname + "/../default-config.yaml";
// ensure that the config exists by copying the default config. // ensure that the config exists by copying the default config.
if (!(fsx.pathExistsSync(configPath))) { if (!(fsx.pathExistsSync(configPath))) {
fsx.copySync(defaultConfig, configPath); fsx.copySync(defaultConfig, configPath);
} else {
const conf = yaml.safeLoad(fsx.readFileSync(configPath, "utf-8"));
const defConf = yaml.safeLoad(fsx.readFileSync(defaultConfig, "utf-8"));
fsx.writeFileSync(configPath, yaml.safeDump(Object.assign(defConf, conf)));
} }
/** /**
@ -22,19 +27,24 @@ if (!(fsx.pathExistsSync(configPath))) {
*/ */
namespace globals { namespace globals {
export const config = yaml.safeLoad(fsx.readFileSync("config.yaml", "utf-8")); export const config = yaml.safeLoad(fsx.readFileSync("config.yaml", "utf-8"));
export const cache = new MemoryCache(1200);
export const logger = winston.createLogger({ export const logger = winston.createLogger({
transports: [ transports: [
new winston.transports.Console({ new winston.transports.Console({
format: winston.format.combine( format: winston.format.combine(
winston.format.timestamp(), winston.format.timestamp(),
winston.format.colorize(), winston.format.colorize(),
winston.format.printf(({ level, message, label, timestamp }) => { winston.format.printf(({ level, message, timestamp }) => {
return `${timestamp} ${level}: ${message}`; return `${timestamp} ${level}: ${message}`;
}), }),
), ),
level: config.logging.level,
}), }),
], ],
}); });
cache.on("set", (key) => logger.debug(`Caching '${key}'.`));
cache.on("miss", (key) => logger.debug(`Cache miss for '${key}'`));
cache.on("hit", (key) => logger.debug(`Cache hit for '${key}'`));
} }
export default globals; export default globals;

@ -0,0 +1,36 @@
import * as MarkdownIt from "markdown-it/lib";
import globals from "./globals";
namespace markdown {
const md = new MarkdownIt();
for (const pluginName of globals.config.markdown.plugins) {
try {
const plugin = require(pluginName);
if (plugin) {
md.use(plugin);
}
} catch (err) {
globals.logger.warn(`Markdown-it plugin '${pluginName}' not found!`);
}
}
/**
* Renders the markdown string inline (without blocks).
* @param markdownString
*/
export function renderInline(markdownString: string) {
return md.renderInline(markdownString);
}
/**
* Renders the markdown string.
* @param markdownString
*/
export function render(markdownString: string) {
return md.render(markdownString);
}
}
export default markdown;

@ -0,0 +1,11 @@
export namespace is {
const emailRegex = /\S+?@\S+?(\.\S+?)?\.\w{2,3}(.\w{2-3})?/g
/**
* Tests if a string is a valid email.
* @param testString
*/
export function email(testString: string) {
return emailRegex.test(testString)
}
}

@ -1,15 +1,15 @@
type Query { type Query {
"returns the user object for a given user id" "returns the user object for a given user id or a handle (only one required)"
getUser(userId: ID): User getUser(userId: ID, handle: String): User
"returns the logged in user"
getSelf: Profile
"returns the post object for a post id" "returns the post object for a post id"
getPost(postId: ID): Post getPost(postId: ID!): Post
"returns the chat object for a chat id" "returns the chat object for a chat id"
getChat(chatId: ID): ChatRoom getChat(chatId: ID!): ChatRoom
"returns the request object for a request id"
getRequest(requestId: ID): Request
"find a post by the posted date or content" "find a post by the posted date or content"
findPost(first: Int, offset: Int, text: String!, postedDate: String): [Post] findPost(first: Int, offset: Int, text: String!, postedDate: String): [Post]
@ -19,35 +19,76 @@ type Query {
} }
type Mutation { type Mutation {
"Accepts the usage of cookies."
acceptCookies: Boolean
"Login of the user. The passwordHash should be a sha512 hash of the password."
login(email: String, passwordHash: String): Profile
"Registers the user."
register(username: String, email: String, passwordHash: String): Profile
"Logout of the user."
logout: Boolean
"Upvote/downvote a Post" "Upvote/downvote a Post"
vote(postId: ID!, type: [VoteType!]!): Boolean vote(postId: ID!, type: VoteType!): VoteType
"Report the post" "Report the post"
report(postId: ID!): Boolean report(postId: ID!): Boolean
"send a request" "send a request"
sendRequest(reciever: ID!, type: RequestType): Boolean sendRequest(receiver: ID!, type: RequestType): Request
"lets you accept a request for a given request id" "lets you accept a request for a given request id"
acceptRequest(requestId: ID!): Boolean acceptRequest(sender: ID!, type: RequestType): Boolean
"lets you deny a request for a given request id" "lets you deny a request for a given request id"
denyRequest(requestId: ID!): Boolean denyRequest(requestId: ID!): Boolean
"send a message in a Chatroom" "send a message in a Chatroom"
sendMessage(chatId: ID!, content: String!): Boolean sendMessage(chatId: ID!, content: String!): ChatMessage
"create the post" "create the post"
createPost(text: String, picture: String, tags: [String]): Boolean createPost(content: String!): Post
"delete the post for a given post id" "delete the post for a given post id"
deletePost(postId: ID!): Boolean deletePost(postId: ID!): Boolean
"Creates a chat between the user (and optional an other user)"
createChat(members: [ID!]): ChatRoom
}
interface UserData {
"url for the Profile picture of the User"
profilePicture: String
"name of the User"
name: String!
"unique identifier name from the User"
handle: String!
"Id of the User"
id: ID!
"the total number of posts the user posted"
numberOfPosts: Int
"returns a given number of posts of a user"
posts(first: Int=10, offset: Int): [Post]
"creation date of the user account"
joinedAt: String!
"all friends of the user"
friends: [User]
} }
"represents a single user account" "represents a single user account"
type User { type User implements UserData{
"url for the Profile picture of the User" "url for the Profile picture of the User"
profilePicture: String! profilePicture: String
"name of the User" "name of the User"
name: String! name: String!
@ -62,28 +103,61 @@ type User {
numberOfPosts: Int numberOfPosts: Int
"returns a given number of posts of a user" "returns a given number of posts of a user"
getAllPosts(first: Int=10, offset: Int): [Post] posts(first: Int=10, offset: Int): [Post]
"creation date of the user account" "creation date of the user account"
joinedDate: String! joinedAt: String!
"returns chats the user pinned" "all friends of the user"
pinnedChats: [ChatRoom] friends: [User]
}
type Profile implements UserData {
"url for the Profile picture of the User"
profilePicture: String
"returns all friends of the user" "name of the User"
name: String!
"returns the chatrooms the user joined."
chats(first: Int=10, offset: Int): [ChatRoom]
"unique identifier name from the User"
handle: String!
"Id of the User"
id: ID!
"the total number of posts the user posted"
numberOfPosts: Int
"returns a given number of posts of a user"
posts(first: Int=10, offset: Int): [Post]
"creation date of the user account"
joinedAt: String!
"all friends of the user"
friends: [User] friends: [User]
"all request for groupChats/friends/events" "all sent request for groupChats/friends/events"
requests: [Request] sentRequests: [Request]
"all received request for groupChats/friends/events"
receivedRequests: [Request]
} }
"represents a single user post" "represents a single user post"
type Post { type Post {
"returns the path to the posts picture if it has one"
picture: String
"returns the text of the post" "The id of the post."
text: String id: ID!
"the text of the post"
content: String
"the content of the post rendered by markdown-it"
htmlContent: String
"upvotes of the Post" "upvotes of the Post"
upvotes: Int! upvotes: Int!
@ -95,20 +169,14 @@ type Post {
author: User! author: User!
"date the post was created" "date the post was created"
creationDate: String! createdAt: String!
"returns the type of vote the user performed on the post" "the type of vote the user performed on the post"
userVote: VoteType userVote: VoteType
"returns the tags of the post"
tags: [String]
} }
"represents a request of any type" "represents a request of any type"
type Request { type Request {
"id of the request"
id: ID!
"Id of the user who sended the request" "Id of the user who sended the request"
sender: User! sender: User!
@ -116,7 +184,7 @@ type Request {
receiver: User! receiver: User!
"type of the request" "type of the request"
requestType: RequestType! type: RequestType!
} }
"represents a chatroom" "represents a chatroom"
@ -125,19 +193,39 @@ type ChatRoom {
members: [User!] members: [User!]
"return a specfic range of messages posted in the chat" "return a specfic range of messages posted in the chat"
getMessages(first: Int, offset: Int): [String] messages(first: Int = 10, offset: Int, containing: String): [ChatMessage]!
"id of the chat" "id of the chat"
id: ID! id: ID!
} }
type ChatMessage {
"The author of the chat message."
author: User!
"The chatroom the message was posted in"
chat: ChatRoom!
"The timestamp when the message was posted (epoch)."
createdAt: String!
"The content of the message."
content: String!
"The content of the message rendered by markdown-it."
htmlContent: String
}
"represents the type of vote performed on a post" "represents the type of vote performed on a post"
enum VoteType { enum VoteType {
UPVOTE UPVOTE
DOWNVOTE DOWNVOTE
} }
"represents the type of request that the user has received" """
represents the type of request that the user has received
Currently on Friend Requests are implemented.
"""
enum RequestType { enum RequestType {
FRIENDREQUEST FRIENDREQUEST
GROUPINVITE GROUPINVITE

@ -1,5 +1,15 @@
import {Router} from "express"; import {Router} from "express";
import {GraphQLError} from "graphql";
import * as status from "http-status";
import {Server} from "socket.io"; import {Server} from "socket.io";
import dataaccess from "../lib/dataaccess";
import {Chatroom} from "../lib/dataaccess/Chatroom";
import {Post} from "../lib/dataaccess/Post";
import {Profile} from "../lib/dataaccess/Profile";
import {User} from "../lib/dataaccess/User";
import {NotLoggedInGqlError} from "../lib/errors/graphqlErrors";
import globals from "../lib/globals";
import {is} from "../lib/regex";
import Route from "../lib/Route"; import Route from "../lib/Route";
/** /**
@ -12,7 +22,6 @@ class HomeRoute extends Route {
constructor() { constructor() {
super(); super();
this.router = Router(); this.router = Router();
this.configure();
} }
/** /**
@ -36,25 +45,205 @@ class HomeRoute extends Route {
* @param req - the request object * @param req - the request object
* @param res - the response object * @param res - the response object
*/ */
public async resolver(req: any, res: any): Promise<object> { public resolver(req: any, res: any): any {
return { return {
// TODO: Define grapql resolvers getSelf() {
}; if (req.session.userId) {
} return new Profile(req.session.userId);
} else {
res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError();
}
},
async getUser({userId, handle}: {userId: number, handle: string}) {
if (handle) {
return await dataaccess.getUserByHandle(handle);
} else if (userId) {
return new User(userId);
} else {
res.status(status.BAD_REQUEST);
return new GraphQLError("No userId or handle provided.");
}
},
async getPost({postId}: {postId: number}) {
if (postId) {
return await dataaccess.getPost(postId);
} else {
res.status(status.BAD_REQUEST);
return new GraphQLError("No postId given.");
}
},
async getChat({chatId}: {chatId: number}) {
if (chatId) {
return new Chatroom(chatId);
} else {
res.status(status.BAD_REQUEST);
return new GraphQLError("No chatId given.");
}
},
acceptCookies() {
req.session.cookiesAccepted = true;
return true;
},
async login({email, passwordHash}: {email: string, passwordHash: string}) {
if (email && passwordHash) {
try {
const user = await dataaccess.getUserByLogin(email, passwordHash);
req.session.userId = user.id;
return user;
} catch (err) {
globals.logger.verbose(`Failed to login user '${email}'`);
res.status(status.BAD_REQUEST);
return err.graphqlError;
}
} else {
res.status(status.BAD_REQUEST);
return new GraphQLError("No email or password given.");
}
},
logout() {
if (req.session.user) {
delete req.session.user;
return true;
} else {
res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError();
}
},
async register({username, email, passwordHash}: {username: string, email: string, passwordHash: string}) {
if (username && email && passwordHash) {
if (!is.email(email)) {
res.status(status.BAD_REQUEST);
return new GraphQLError(`'${email}' is not a valid email address!`);
}
const user = await dataaccess.registerUser(username, email, passwordHash);
if (user) {
req.session.userId = user.id;
return user;
} else {
res.status(status.INTERNAL_SERVER_ERROR);
return new GraphQLError("Failed to create account.");
}
} else {
res.status(status.BAD_REQUEST);
return new GraphQLError("No username, email or password given.");
}
},
async vote({postId, type}: {postId: number, type: dataaccess.VoteType}) {
if (postId && type) {
if (req.session.userId) {
return await (new Post(postId)).vote(req.session.userId, type);
} else {
res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError();
}
} else {
res.status(status.BAD_REQUEST);
return new GraphQLError("No postId or type given.");
}
},
async createPost({content}: {content: string}) {
if (content) {
if (req.session.userId) {
return await dataaccess.createPost(content, req.session.userId);
} else {
res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError();
}
} else {
res.status(status.BAD_REQUEST);
return new GraphQLError("Can't create empty post.");
}
},
async deletePost({postId}: {postId: number}) {
if (postId) {
const post = new Post(postId);
if ((await post.author()).id === req.session.userId) {
return await dataaccess.deletePost(post.id);
} else {
res.status(status.FORBIDDEN);
return new GraphQLError("User is not author of the post.");
}
} else {
return new GraphQLError("No postId given.");
}
},
async createChat({members}: {members: number[]}) {
if (req.session.userId) {
const chatMembers = [req.session.userId];
if (members) {
chatMembers.push(...members);
}
return await dataaccess.createChat(...chatMembers);
/** } else {
* Configures the route. res.status(status.UNAUTHORIZED);
*/ return new NotLoggedInGqlError();
private configure() { }
this.router.get("/", (req, res) => { },
res.render("home"); async sendMessage({chatId, content}: {chatId: number, content: string}) {
}); if (!req.session.userId) {
this.router.get("/login", (req, res) => { res.status(status.UNAUTHORIZED);
res.render("login"); return new NotLoggedInGqlError();
}); }
this.router.get("/register", (req, res) => { if (chatId && content) {
res.render("register"); try {
}); return await dataaccess.sendChatMessage(req.session.userId, chatId, content);
} catch (err) {
res.status(status.BAD_REQUEST);
return err.graphqlError;
}
} else {
res.status(status.BAD_REQUEST);
return new GraphQLError("No chatId or content given.");
}
},
async sendRequest({receiver, type}: {receiver: number, type: dataaccess.RequestType}) {
if (!req.session.userId) {
res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError();
}
if (receiver && type) {
return await dataaccess.createRequest(req.session.userId, receiver, type);
} else {
res.status(status.BAD_REQUEST);
return new GraphQLError("No receiver or type given.");
}
},
async denyRequest({sender, type}: {sender: number, type: dataaccess.RequestType}) {
if (!req.session.userId) {
res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError();
}
if (sender && type) {
const profile = new Profile(req.session.userId);
await profile.denyRequest(sender, type);
return true;
} else {
res.status(status.BAD_REQUEST);
return new GraphQLError("No sender or type given.");
}
},
async acceptRequest({sender, type}: {sender: number, type: dataaccess.RequestType}) {
if (!req.session.userId) {
res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError();
}
if (sender && type) {
try {
const profile = new Profile(req.session.userId);
await profile.acceptRequest(sender, type);
return true;
} catch (err) {
res.status(status.BAD_REQUEST);
return err.graphqlError;
}
} else {
res.status(status.BAD_REQUEST);
return new GraphQLError("No sender or type given.");
}
},
};
} }
} }

@ -28,9 +28,9 @@ namespace routes {
* @param request * @param request
* @param response * @param response
*/ */
export const resolvers = async (request: any, response: any): Promise<object> => { export function resolvers(request: any, response: any): Promise<object> {
return homeRoute.resolver(request, response); return homeRoute.resolver(request, response);
}; }
/** /**
* Assigns the io listeners or namespaces to the routes * Assigns the io listeners or namespaces to the routes

@ -1,58 +1,131 @@
CREATE TABLE IF NOT EXISTS users ( --create functions
id SERIAL PRIMARY KEY, DO $$BEGIN
name varchar(128) NOT NULL,
handle varchar(128) UNIQUE NOT NULL, IF NOT EXISTS(SELECT 1 from pg_proc WHERE proname = 'function_exists') THEN
password varchar(1024) NOT NULL, CREATE FUNCTION function_exists(text) RETURNS boolean LANGUAGE plpgsql AS $BODY$
email varchar(128) UNIQUE NOT NULL, BEGIN
greenpoints INTEGER DEFAULT 0, RETURN EXISTS(SELECT 1 from pg_proc WHERE proname = $1);
joined_at TIMESTAMP DEFAULT now() END $BODY$;
); END IF;
CREATE TABLE IF NOT EXISTS posts ( IF NOT function_exists('type_exists') THEN
id BIGSERIAL PRIMARY KEY, CREATE FUNCTION type_exists(text) RETURNS boolean LANGUAGE plpgsql AS $BODY$
upvotes INTEGER DEFAULT 0, BEGIN
downvotes INTEGER DEFAULT 0, RETURN EXISTS (SELECT 1 FROM pg_type WHERE typname = $1);
created_at TIMESTAMP DEFAULT now(), END $BODY$;
content text, END IF;
author SERIAL REFERENCES users (id) ON DELETE CASCADE,
type varchar(16) NOT NULL IF NOT function_exists('cast_to_votetype') THEN
); CREATE FUNCTION cast_to_votetype(text) RETURNS votetype LANGUAGE plpgsql AS $BODY$
BEGIN
CREATE TABLE IF NOT EXISTS votes ( RETURN CASE WHEN $1::votetype IS NULL THEN 'UPVOTE' ELSE $1::votetype END;
user_id SERIAL REFERENCES users (id) ON DELETE CASCADE, END $BODY$;
item_id BIGSERIAL REFERENCES posts (id) ON DELETE CASCADE, END IF;
vote_type varchar(8) DEFAULT 'upvote'
); IF NOT function_exists('cast_to_posttype') THEN
CREATE FUNCTION cast_to_posttype(text) RETURNS posttype LANGUAGE plpgsql AS $BODY$
CREATE TABLE IF NOT EXISTS events ( BEGIN
id BIGSERIAL PRIMARY KEY, RETURN CASE WHEN $1::posttype IS NULL THEN 'MISC' ELSE $1::posttype END;
time TIMESTAMP, END $BODY$;
owner SERIAL REFERENCES users (id) END IF;
);
END$$;
CREATE TABLE IF NOT EXISTS event_members (
event BIGSERIAL REFERENCES events (id), --create types
member SERIAL REFERENCES users (id) DO $$ BEGIN
);
IF NOT type_exists('votetype') THEN
CREATE TABLE IF NOT EXISTS chats ( CREATE TYPE votetype AS enum ('DOWNVOTE', 'UPVOTE');
id BIGSERIAL PRIMARY KEY END IF;
);
IF NOT type_exists('posttype') THEN
CREATE TABLE IF NOT EXISTS chat_messages ( CREATE TYPE posttype AS enum ('MISC', 'ACTION', 'IMAGE', 'TEXT');
chat BIGSERIAL REFERENCES chats (id) ON DELETE CASCADE, END IF;
author SERIAL REFERENCES users (id) ON DELETE SET NULL,
content VARCHAR(1024) NOT NULL, IF NOT type_exists('requesttype') THEN
created_at TIMESTAMP DEFAULT now(), CREATE TYPE requesttype AS enum ('FRIENDREQUEST');
PRIMARY KEY (chat, author, created_at) END IF;
);
END$$;
CREATE TABLE IF NOT EXISTS chat_members (
chat BIGSERIAL REFERENCES chats (id) ON DELETE CASCADE, -- create tables
member SERIAL REFERENCES users (id) ON DELETE CASCADE DO $$ BEGIN
);
CREATE TABLE IF NOT EXISTS "user_sessions" (
CREATE TABLE IF NOT EXISTS user_friends ( "sid" varchar NOT NULL,
user_id SERIAL REFERENCES users (id) ON DELETE CASCADE, "sess" json NOT NULL,
friend_id SERIAL REFERENCES users (id) ON DELETE CASCADE "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,
handle varchar(128) UNIQUE NOT NULL,
password varchar(1024) NOT NULL,
email varchar(128) UNIQUE NOT NULL,
greenpoints INTEGER DEFAULT 0,
joined_at TIMESTAMP DEFAULT now()
);
CREATE TABLE IF NOT EXISTS posts (
id BIGSERIAL PRIMARY KEY,
upvotes INTEGER DEFAULT 0,
downvotes INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT now(),
content text,
author SERIAL REFERENCES users (id) ON DELETE CASCADE,
type posttype NOT NULL DEFAULT 'MISC'
);
CREATE TABLE IF NOT EXISTS votes (
user_id SERIAL REFERENCES users (id) ON DELETE CASCADE,
item_id BIGSERIAL REFERENCES posts (id) ON DELETE CASCADE,
vote_type votetype DEFAULT 'DOWNVOTE',
PRIMARY KEY (user_id, item_id)
);
CREATE TABLE IF NOT EXISTS events (
id BIGSERIAL PRIMARY KEY,
time TIMESTAMP,
owner SERIAL REFERENCES users (id)
);
CREATE TABLE IF NOT EXISTS event_members (
event BIGSERIAL REFERENCES events (id),
member SERIAL REFERENCES users (id),
PRIMARY KEY (event, member)
);
CREATE TABLE IF NOT EXISTS chats (
id BIGSERIAL PRIMARY KEY
);
CREATE TABLE IF NOT EXISTS chat_messages (
chat BIGSERIAL REFERENCES chats (id) ON DELETE CASCADE,
author SERIAL REFERENCES users (id) ON DELETE SET NULL,
content VARCHAR(1024) NOT NULL,
created_at TIMESTAMP DEFAULT now(),
PRIMARY KEY (chat, author, created_at)
);
CREATE TABLE IF NOT EXISTS chat_members (
chat BIGSERIAL REFERENCES chats (id) ON DELETE CASCADE,
member SERIAL REFERENCES users (id) ON DELETE CASCADE,
PRIMARY KEY (chat, member)
);
CREATE TABLE IF NOT EXISTS user_friends (
user_id SERIAL REFERENCES users (id) ON DELETE CASCADE,
friend_id SERIAL REFERENCES users (id) ON DELETE CASCADE,
PRIMARY KEY (user_id, friend_id)
);
CREATE TABLE IF NOT EXISTS requests (
sender SERIAL REFERENCES users (id) ON DELETE CASCADE,
receiver SERIAL REFERENCES users (id) ON DELETE CASCADE,
type requesttype DEFAULT 'FRIENDREQUEST',
PRIMARY KEY (sender, receiver, type)
);
END $$;

@ -1,3 +1,19 @@
ALTER TABLE IF EXISTS votes DO $$ BEGIN
ADD COLUMN IF NOT EXISTS vote_type varchar(8) DEFAULT 'upvote',
ALTER COLUMN vote_type SET DEFAULT 'upvote'; ALTER TABLE IF EXISTS votes
ADD COLUMN IF NOT EXISTS vote_type votetype DEFAULT 'UPVOTE',
ALTER COLUMN vote_type TYPE votetype USING cast_to_votetype(vote_type::text),
ALTER COLUMN vote_type DROP DEFAULT,
ALTER COLUMN vote_type SET DEFAULT 'UPVOTE';
ALTER TABLE IF EXISTS posts
ALTER COLUMN type TYPE posttype USING cast_to_posttype(type::text),
ALTER COLUMN type DROP DEFAULT,
ALTER COLUMN type SET DEFAULT 'MISC',
DROP COLUMN IF EXISTS upvotes,
DROP COLUMN IF EXISTS downvotes;
ALTER TABLE requests
ADD COLUMN IF NOT EXISTS type requesttype DEFAULT 'FRIENDREQUEST';
END $$;

@ -1,13 +0,0 @@
div#feedcontainer
div.postinput
input(type=text placeholder='Post something')
button.submitbutton Submit
div.feeditem
div.itemhead
span.title Testuser
span.handle
a(href='#') @testuser
span.date 23.09.19 10:07
p.text
| Example Test text.
| This is a test

@ -1 +0,0 @@
div#friendscontainer

@ -1,9 +0,0 @@
html
head
title Greenvironment Network
include ../includes/head
body
div#content
include stylebar
include feed
include friends

@ -1,2 +0,0 @@
div.stylebar
h1 Greenvironment

@ -1 +0,0 @@
link(rel='stylesheet' href='stylesheets/style.css')

@ -1,8 +0,0 @@
html
head
title Greenvironment Network Login
include ../includes/head
body
div#content
include stylebar
include login

@ -1,6 +0,0 @@
div#input-login
input(type=text placeholder='username')
input(type=text placeholder='password')
button.loginButton Login
a(href="/register" )
| You aren´t part of greenvironment yet? - create a new account

@ -1,2 +0,0 @@
div.stylebar
h1 Greenvironment

@ -1,8 +0,0 @@
html
head
title Greenvironment Network Register
include ../includes/head
body
div#content
include stylebar
include register

@ -1,8 +0,0 @@
div#input-register
input(type=text placeholder='username')
input(type=text placeholder='email')
input(type=text placeholder='password')
input(type=text placeholder='repeat password')
button.registerButton Register
a(href="/login" )
| You are already part of greenvironment? - login

@ -1,2 +0,0 @@
div.stylebar
h1 Greenvironment
Loading…
Cancel
Save