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