Merge branch 'develop' of Software_Engineering_I/greenvironment-server into master

pull/2/head^2
Julius 5 years ago committed by Gitea
commit 56614f6338

20
.gitignore vendored

@ -1,8 +1,12 @@
.nyc_output/ .nyc_output/
coverage/ coverage/
node_modules/ node_modules/
npm-debug.log npm-debug.log
test/*.log test/*.log
dist dist
.idea .idea
config.yaml config.yaml
sqz-force
greenvironment.db
logs
logs*

@ -1,15 +1,19 @@
# Changelog # Changelog
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
### Added ### Added
- Connection to Postgres Database - Graphql Schema
- Graphql Schema - default-config file and generation of config file on startup
- default-config file and generation of config file on startup - DTOs
- DTOs - Home Route
- Home Route - session management
- Sequelize models and integration
- Sequelize-typescript integration
- error pages
- pagination for most list types

@ -1,12 +1,11 @@
FROM node:current-alpine FROM node:current-alpine
COPY . /home/node/green COPY . /home/node/green
WORKDIR /home/node/green WORKDIR /home/node/green
RUN npm install -g gulp RUN npm install -g gulp
RUN npm install --save-dev RUN npm install --save-dev
RUN npm rebuild node-sass RUN npm rebuild node-sass
RUN gulp RUN gulp
COPY . . COPY . .
EXPOSE 8080 EXPOSE 8080
EXPOSE 5432 CMD ["npm" , "run"]
CMD ["npm" , "run"]

@ -1,11 +1,11 @@
# greenvironment-server # greenvironment-server
Server of the greenvironment social network. Server of the greenvironment social network.
## Install ## Install
You need to install a nodejs runtime to run the greenvironment server. You need to install a nodejs runtime to run the greenvironment server.
Then you need to install all requirements. To do so, open a terminal in the Then you need to install all requirements. To do so, open a terminal in the
greenvironment project folder and execute "npm i". You can build the project by greenvironment project folder and execute "npm i". You can build the project by
executing "gulp" in the terminal. To run the server you need executing "gulp" in the terminal. To run the server you need
to execute "node ./dist". to execute "node ./dist".

@ -1,15 +1,11 @@
version: "3" version: "3"
services: services:
greenvironment: greenvironment:
build: build: .
context: . user: "root"
dockerfile: ./Dockerfile working_dir: /home/node/green
user: "node" environment:
working_dir: /home/node/green - NODE_ENV=production
environment: ports:
- NODE_ENV=production - "8080:8080"
volumes: command: "npm start"
- ./:/home/node/green
ports:
- "8080:8080"
command: "npm start"

@ -1,47 +1,47 @@
const {src, dest, watch, series, task} = require('gulp'); const {src, dest, watch, series, task} = require('gulp');
const sass = require('gulp-sass'); 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'); const gulp = require('gulp');
function clearDist(cb) { function clearDist(cb) {
del('dist/*', cb); del('dist/*', cb);
} }
function compileTypescript() { function compileTypescript() {
let tsProject = ts.createProject('tsconfig.json'); let tsProject = ts.createProject('tsconfig.json');
let tsResult = tsProject.src().pipe(tsProject()); let tsResult = tsProject.src().pipe(tsProject());
return tsResult return tsResult
//.pipe(minify()) //.pipe(minify())
.pipe(dest('dist')); .pipe(dest('dist'));
} }
function minifyJs() { function minifyJs() {
return src('src/public/javascripts/**/*.js') return src('src/public/javascripts/**/*.js')
.pipe(minify({ .pipe(minify({
ext: { ext: {
src: '-debug.js', src: '-debug.js',
min: '.js' min: '.js'
} }
})) }))
.pipe(dest('dist/public/javascripts')); .pipe(dest('dist/public/javascripts'));
} }
function compileSass() { function compileSass() {
return src('src/public/stylesheets/sass/**/style.sass') return src('src/public/stylesheets/sass/**/style.sass')
.pipe(sass().on('error', sass.logError)) .pipe(sass().on('error', sass.logError))
.pipe(dest('dist/public/stylesheets')); .pipe(dest('dist/public/stylesheets'));
} }
function moveRemaining() { function moveRemaining() {
return src(['src/**/*', '!src/**/*.ts', '!src/**/*.sass', '!src/**/*.js']) return src(['src/**/*', '!src/**/*.ts', '!src/**/*.sass', '!src/**/*.js'])
.pipe(dest('dist')); .pipe(dest('dist'));
} }
task('default', series(clearDist, compileTypescript, minifyJs, compileSass, moveRemaining)); task('default', series(clearDist, compileTypescript, minifyJs, compileSass, moveRemaining));
task('watch', () => { task('watch', () => {
watch('src/public/stylesheets/sass/**/*.sass', compileSass); watch('src/public/stylesheets/sass/**/*.sass', compileSass);
watch('**/*.js', minifyJs); watch('**/*.js', minifyJs);
watch('**/*.ts', compileTypescript); watch('**/*.ts', compileTypescript);
}); });

15440
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,70 +1,79 @@
{ {
"name": "greenvironment-server", "name": "greenvironment-server",
"version": "0.1.0", "version": "0.1.0",
"description": "Server for greenvironment network", "description": "Server for greenvironment network",
"main": "./dist/index.js", "main": "./dist/index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"build": "gulp", "build": "gulp",
"start": "node ./dist/index.js" "start": "node ./dist/index.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.trivernis.net/Software_Engineering_I/greenvironment-server.git" "url": "https://git.trivernis.net/Software_Engineering_I/greenvironment-server.git"
}, },
"keywords": [ "keywords": [
"server", "server",
"nodejs", "nodejs",
"express" "express"
], ],
"author": "SoftEngI", "author": "SoftEngI",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@types/compression": "^1.0.1", "@types/bluebird": "^3.5.27",
"@types/connect-pg-simple": "^4.2.0", "@types/compression": "^1.0.1",
"@types/cookie-parser": "^1.4.2", "@types/connect-pg-simple": "^4.2.0",
"@types/express": "^4.17.1", "@types/cookie-parser": "^1.4.2",
"@types/express-graphql": "^0.8.0", "@types/cors": "^2.8.6",
"@types/express-session": "^1.15.14", "@types/express": "^4.17.1",
"@types/express-socket.io-session": "^1.3.2", "@types/express-graphql": "^0.8.0",
"@types/fs-extra": "^8.0.0", "@types/express-session": "^1.15.14",
"@types/graphql": "^14.2.3", "@types/express-socket.io-session": "^1.3.2",
"@types/http-status": "^0.2.30", "@types/fs-extra": "^8.0.0",
"@types/js-yaml": "^3.12.1", "@types/graphql": "^14.2.3",
"@types/markdown-it": "0.0.9", "@types/http-status": "^0.2.30",
"@types/node": "^12.7.8", "@types/js-yaml": "^3.12.1",
"@types/pg": "^7.11.0", "@types/markdown-it": "0.0.9",
"@types/socket.io": "^2.1.2", "@types/node": "^12.7.12",
"@types/winston": "^2.4.4", "@types/pg": "^7.11.0",
"delete": "^1.1.0", "@types/sequelize": "^4.28.5",
"gulp": "^4.0.2", "@types/socket.io": "^2.1.2",
"gulp-minify": "^3.1.0", "@types/validator": "^10.11.3",
"gulp-sass": "^4.0.2", "@types/winston": "^2.4.4",
"gulp-typescript": "^5.0.1", "delete": "^1.1.0",
"ts-lint": "^4.5.1", "gulp": "^4.0.2",
"tsc": "^1.20150623.0", "gulp-minify": "^3.1.0",
"tslint": "^5.19.0", "gulp-sass": "^4.0.2",
"typescript": "^3.5.3" "gulp-typescript": "^5.0.1",
}, "ts-lint": "^4.5.1",
"dependencies": { "tsc": "^1.20150623.0",
"compression": "^1.7.4", "tslint": "^5.19.0",
"connect-pg-simple": "^6.0.1", "typescript": "^3.5.3"
"cookie-parser": "^1.4.4", },
"express": "^4.17.1", "dependencies": {
"express-graphql": "^0.9.0", "compression": "^1.7.4",
"express-session": "^1.16.2", "connect-session-sequelize": "^6.0.0",
"express-socket.io-session": "^1.3.5", "cookie-parser": "^1.4.4",
"fs-extra": "^8.1.0", "cors": "^2.8.5",
"g": "^2.0.1", "express": "^4.17.1",
"graphql": "^14.4.2", "express-graphql": "^0.9.0",
"graphql-import": "^0.7.1", "express-session": "^1.16.2",
"http-status": "^1.3.2", "express-socket.io-session": "^1.3.5",
"js-yaml": "^3.13.1", "fs-extra": "^8.1.0",
"markdown-it": "^10.0.0", "graphql": "^14.4.2",
"markdown-it-emoji": "^1.4.0", "graphql-import": "^0.7.1",
"pg": "^7.12.1", "http-status": "^1.3.2",
"pug": "^2.0.4", "js-yaml": "^3.13.1",
"socket.io": "^2.2.0", "markdown-it": "^10.0.0",
"winston": "^3.2.1" "markdown-it-emoji": "^1.4.0",
} "pg": "^7.12.1",
} "pug": "^2.0.4",
"reflect-metadata": "^0.1.13",
"sequelize": "^5.19.6",
"sequelize-typescript": "^1.0.0",
"socket.io": "^2.2.0",
"sqlite3": "^4.1.0",
"winston": "^3.2.1",
"winston-daily-rotate-file": "^4.2.1"
}
}

@ -1,97 +1,118 @@
import * as compression from "compression"; import * as compression from "compression";
import connectPgSimple = require("connect-pg-simple"); import * as cookieParser from "cookie-parser";
import * as cookieParser from "cookie-parser"; import * as cors from "cors";
import * as express from "express"; import {Request, Response} from "express";
import * as graphqlHTTP from "express-graphql"; import * as express from "express";
import * as session from "express-session"; import * as graphqlHTTP from "express-graphql";
import sharedsession = require("express-socket.io-session"); import * as session from "express-session";
import {buildSchema} from "graphql"; import sharedsession = require("express-socket.io-session");
import {importSchema} from "graphql-import"; import * as fsx from "fs-extra";
import * as http from "http"; import {buildSchema} from "graphql";
import * as path from "path"; import {importSchema} from "graphql-import";
import * as socketIo from "socket.io"; import * as http from "http";
import {resolver} from "./graphql/resolvers"; import * as httpStatus from "http-status";
import dataaccess, {queryHelper} from "./lib/dataaccess"; import * as path from "path";
import globals from "./lib/globals"; import {Sequelize} from "sequelize-typescript";
import routes from "./routes"; import * as socketIo from "socket.io";
import {resolver} from "./graphql/resolvers";
const logger = globals.logger; import dataaccess from "./lib/dataaccess";
import globals from "./lib/globals";
const PgSession = connectPgSimple(session); import routes from "./routes";
class App { const SequelizeStore = require("connect-session-sequelize")(session.Store);
public app: express.Application; const logger = globals.logger;
public io: socketIo.Server;
public server: http.Server; class App {
public app: express.Application;
constructor() { public io: socketIo.Server;
this.app = express(); public server: http.Server;
this.server = new http.Server(this.app); public readonly sequelize: Sequelize;
this.io = socketIo(this.server);
} constructor() {
this.app = express();
/** this.server = new http.Server(this.app);
* initializes everything that needs to be initialized asynchronous. this.io = socketIo(this.server);
*/ this.sequelize = new Sequelize(globals.config.database.connectionUri );
public async init() { }
await dataaccess.init();
await routes.ioListeners(this.io); /**
* initializes everything that needs to be initialized asynchronous.
const appSession = session({ */
cookie: { public async init() {
maxAge: Number(globals.config.session.cookieMaxAge) || 604800000, await dataaccess.init(this.sequelize);
secure: "auto",
}, const appSession = session({
resave: false, cookie: {
saveUninitialized: false, maxAge: Number(globals.config.session.cookieMaxAge) || 604800000,
secret: globals.config.session.secret, secure: "auto",
store: new PgSession({ },
pool: dataaccess.pool, resave: false,
tableName: "user_sessions", saveUninitialized: false,
}), secret: globals.config.session.secret,
}); store: new SequelizeStore({db: this.sequelize}),
});
this.io.use(sharedsession(appSession, {autoSave: true}));
const force = fsx.existsSync("sqz-force");
this.app.set("views", path.join(__dirname, "views")); logger.info(`Sequelize Table force: ${force}`);
this.app.set("view engine", "pug"); await this.sequelize.sync({force, logging: (msg) => logger.silly(msg)});
this.app.set("trust proxy", 1); await routes.ioListeners(this.io);
this.app.use(compression()); this.io.use(sharedsession(appSession, {autoSave: true}));
this.app.use(express.json());
this.app.use(express.urlencoded({extended: false})); this.app.set("views", path.join(__dirname, "views"));
this.app.use(express.static(path.join(__dirname, "public"))); this.app.set("view engine", "pug");
this.app.use(cookieParser()); this.app.set("trust proxy", 1);
this.app.use(appSession);
this.app.use((req, res, next) => { this.app.use(compression());
logger.verbose(`${req.method} ${req.url}`); this.app.use(express.json());
next(); this.app.use(express.urlencoded({extended: false}));
}); this.app.use(express.static(path.join(__dirname, "public")));
this.app.use(routes.router); this.app.use(cookieParser());
this.app.use("/graphql", graphqlHTTP((request, response) => { this.app.use(appSession);
return { if (globals.config.server.cors) {
// @ts-ignore all this.app.use(cors());
context: {session: request.session}, }
graphiql: true, this.app.use((req, res, next) => {
rootValue: resolver(request, response), logger.verbose(`${req.method} ${req.url}`);
schema: buildSchema(importSchema(path.join(__dirname, "./graphql/schema.graphql"))), next();
}; });
})); this.app.use(routes.router);
} this.app.use("/graphql", graphqlHTTP((request, response) => {
return {
/** // @ts-ignore all
* Starts the web server. context: {session: request.session},
*/ graphiql: true,
public start() { rootValue: resolver(request, response),
if (globals.config.server.port) { schema: buildSchema(importSchema(path.join(__dirname, "./graphql/schema.graphql"))),
logger.info(`Starting server...`); };
this.app.listen(globals.config.server.port); }));
logger.info(`Server running on port ${globals.config.server.port}`); this.app.use((req: any, res: Response) => {
} else { if (globals.config.frontend.angularIndex) {
logger.error("No port specified in the config." + res.sendFile(path.join(__dirname, globals.config.frontend.angularIndex));
"Please configure a port in the config.yaml."); } else {
} res.status(httpStatus.NOT_FOUND);
} res.render("errors/404.pug", {url: req.url});
} }
});
export default App; this.app.use((err, req: Request, res: Response) => {
res.status(httpStatus.INTERNAL_SERVER_ERROR);
res.render("errors/500.pug");
});
}
/**
* Starts the web server.
*/
public start() {
if (globals.config.server.port) {
logger.info(`Starting server...`);
this.app.listen(globals.config.server.port);
logger.info(`Server running on port ${globals.config.server.port}`);
} else {
logger.error("No port specified in the config." +
"Please configure a port in the config.yaml.");
}
}
}
export default App;

@ -1,22 +1,25 @@
# database connection info # database connection info
database: database:
host: connectionUri: "sqlite://greenvironment.db"
port:
user: # http server configuration
password: server:
database: port: 8080
cors: false
# http server configuration
server: # configuration of sessions
port: 8080 session:
secret: REPLACE WITH SAFE RANDOM GENERATED SECRET
session: cookieMaxAge: 604800000 # 7 days
secret: REPLACE WITH SAFE RANDOM GENERATED SECRET
cookieMaxAge: 604800000 # 7 days # configuration of the markdown parser
markdown:
markdown: plugins:
plugins: - 'markdown-it-emoji'
- 'markdown-it-emoji'
# configuration for logging
logging: logging:
level: info level: info
frontend:
angularIndex:

@ -1,13 +1,10 @@
import {GraphQLError} from "graphql"; import {GraphQLError} from "graphql";
import * as status from "http-status"; import * as status from "http-status";
import dataaccess from "../lib/dataaccess"; import dataaccess from "../lib/dataaccess";
import {Chatroom} from "../lib/dataaccess/Chatroom"; import {NotLoggedInGqlError, PostNotFoundGqlError} from "../lib/errors/graphqlErrors";
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 globals from "../lib/globals";
import {InternalEvents} from "../lib/InternalEvents"; import {InternalEvents} from "../lib/InternalEvents";
import * as models from "../lib/models";
import {is} from "../lib/regex"; import {is} from "../lib/regex";
/** /**
@ -17,9 +14,9 @@ import {is} from "../lib/regex";
*/ */
export function resolver(req: any, res: any): any { export function resolver(req: any, res: any): any {
return { return {
getSelf() { async getSelf() {
if (req.session.userId) { if (req.session.userId) {
return new Profile(req.session.userId); return models.User.findByPk(req.session.userId);
} else { } else {
res.status(status.UNAUTHORIZED); res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError(); return new NotLoggedInGqlError();
@ -29,7 +26,7 @@ export function resolver(req: any, res: any): any {
if (handle) { if (handle) {
return await dataaccess.getUserByHandle(handle); return await dataaccess.getUserByHandle(handle);
} else if (userId) { } else if (userId) {
return new User(userId); return models.User.findByPk(userId);
} else { } else {
res.status(status.BAD_REQUEST); res.status(status.BAD_REQUEST);
return new GraphQLError("No userId or handle provided."); return new GraphQLError("No userId or handle provided.");
@ -45,12 +42,28 @@ export function resolver(req: any, res: any): any {
}, },
async getChat({chatId}: { chatId: number }) { async getChat({chatId}: { chatId: number }) {
if (chatId) { if (chatId) {
return new Chatroom(chatId); return models.ChatRoom.findByPk(chatId);
} else { } else {
res.status(status.BAD_REQUEST); res.status(status.BAD_REQUEST);
return new GraphQLError("No chatId given."); return new GraphQLError("No chatId given.");
} }
}, },
async getGroup({groupId}: { groupId: number }) {
if (groupId) {
return models.Group.findByPk(groupId);
} else {
res.status(status.BAD_REQUEST);
return new GraphQLError("No group id given.");
}
},
async getRequest({requestId}: { requestId: number }) {
if (requestId) {
return models.Request.findByPk(requestId);
} else {
res.status(status.BAD_REQUEST);
return new GraphQLError("No requestId given.");
}
},
acceptCookies() { acceptCookies() {
req.session.cookiesAccepted = true; req.session.cookiesAccepted = true;
return true; return true;
@ -73,8 +86,14 @@ export function resolver(req: any, res: any): any {
} }
}, },
logout() { logout() {
if (req.session.user) { if (req.session.userId) {
delete req.session.user; delete req.session.userId;
req.session.save((err: any) => {
if (err) {
globals.logger.error(err.message);
globals.logger.debug(err.stack);
}
});
return true; return true;
} else { } else {
res.status(status.UNAUTHORIZED); res.status(status.UNAUTHORIZED);
@ -105,7 +124,13 @@ export function resolver(req: any, res: any): any {
async vote({postId, type}: { postId: number, type: dataaccess.VoteType }) { async vote({postId, type}: { postId: number, type: dataaccess.VoteType }) {
if (postId && type) { if (postId && type) {
if (req.session.userId) { if (req.session.userId) {
return await (new Post(postId)).vote(req.session.userId, type); const post = await models.Post.findByPk(postId);
if (post) {
return await post.vote(req.session.userId, type);
} else {
res.status(status.BAD_REQUEST);
return new PostNotFoundGqlError(postId);
}
} else { } else {
res.status(status.UNAUTHORIZED); res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError(); return new NotLoggedInGqlError();
@ -118,9 +143,13 @@ export function resolver(req: any, res: any): any {
async createPost({content}: { content: string }) { async createPost({content}: { content: string }) {
if (content) { if (content) {
if (req.session.userId) { if (req.session.userId) {
const post = await dataaccess.createPost(content, req.session.userId); if (content.length > 2048) {
globals.internalEmitter.emit(InternalEvents.GQLPOSTCREATE, post); return new GraphQLError("Content too long.");
return post; } else {
const post = await dataaccess.createPost(content, req.session.userId);
globals.internalEmitter.emit(InternalEvents.GQLPOSTCREATE, post);
return post;
}
} else { } else {
res.status(status.UNAUTHORIZED); res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError(); return new NotLoggedInGqlError();
@ -132,8 +161,8 @@ export function resolver(req: any, res: any): any {
}, },
async deletePost({postId}: { postId: number }) { async deletePost({postId}: { postId: number }) {
if (postId) { if (postId) {
const post = new Post(postId); const post = await models.Post.findByPk(postId, {include: [models.User]});
if ((await post.author()).id === req.session.userId) { if (post.rAuthor.id === req.session.userId) {
return await dataaccess.deletePost(post.id); return await dataaccess.deletePost(post.id);
} else { } else {
res.status(status.FORBIDDEN); res.status(status.FORBIDDEN);
@ -194,8 +223,8 @@ export function resolver(req: any, res: any): any {
return new NotLoggedInGqlError(); return new NotLoggedInGqlError();
} }
if (sender && type) { if (sender && type) {
const profile = new Profile(req.session.userId); const user = await models.User.findByPk(req.session.userId);
await profile.denyRequest(sender, type); await user.denyRequest(sender, type);
return true; return true;
} else { } else {
res.status(status.BAD_REQUEST); res.status(status.BAD_REQUEST);
@ -209,8 +238,8 @@ export function resolver(req: any, res: any): any {
} }
if (sender && type) { if (sender && type) {
try { try {
const profile = new Profile(req.session.userId); const user = await models.User.findByPk(req.session.userId);
await profile.acceptRequest(sender, type); await user.acceptRequest(sender, type);
return true; return true;
} catch (err) { } catch (err) {
globals.logger.warn(err.message); globals.logger.warn(err.message);
@ -223,5 +252,129 @@ export function resolver(req: any, res: any): any {
return new GraphQLError("No sender or type given."); return new GraphQLError("No sender or type given.");
} }
}, },
async removeFriend({friendId}: { friendId: number }) {
if (req.session.userId) {
const self = await models.User.findByPk(req.session.userId);
return await self.removeFriend(friendId);
} else {
res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError();
}
},
async getPosts({first, offset, sort}: { first: number, offset: number, sort: dataaccess.SortType }) {
return await dataaccess.getPosts(first, offset, sort);
},
async createGroup({name, members}: { name: string, members: number[] }) {
if (req.session.userId) {
return await dataaccess.createGroup(name, req.session.userId, members);
} else {
return new NotLoggedInGqlError();
}
},
async joinGroup({id}: { id: number }) {
if (req.session.userId) {
try {
return await dataaccess
.changeGroupMembership(id, req.session.userId, dataaccess.MembershipChangeAction.ADD);
} catch (err) {
res.status(status.BAD_REQUEST);
return err.graphqlError;
}
} else {
res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError();
}
},
async leaveGroup({id}: { id: number }) {
if (req.session.userId) {
try {
return await dataaccess
.changeGroupMembership(id, req.session.userId, dataaccess.MembershipChangeAction.REMOVE);
} catch (err) {
res.status(status.BAD_REQUEST);
return err.graphqlError;
}
} else {
res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError();
}
},
async addGroupAdmin({groupId, userId}: { groupId: number, userId: number }) {
if (req.session.userId) {
const group = await models.Group.findByPk(groupId);
const self = await models.User.findByPk(req.session.userId);
if (group && !(await group.$has("rAdmins", self)) && (await group.creator()) !== self.id) {
res.status(status.FORBIDDEN);
return new GraphQLError("You are not a group admin!");
}
try {
return await dataaccess
.changeGroupMembership(groupId, userId, dataaccess.MembershipChangeAction.OP);
} catch (err) {
res.status(status.BAD_REQUEST);
return err.graphqlError;
}
} else {
res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError();
}
},
async removeGroupAdmin({groupId, userId}: { groupId: number, userId: number }) {
if (req.session.userId) {
const group = await models.Group.findByPk(groupId);
const isCreator = Number(group.creatorId) === Number(req.session.userId);
const userIsCreator = Number(group.creatorId) === Number(userId);
if (group && !isCreator && Number(userId) !== Number(req.session.userId)) {
res.status(status.FORBIDDEN);
return new GraphQLError("You are not the group creator!");
} else if (userIsCreator) {
res.status(status.FORBIDDEN);
return new GraphQLError("You are not allowed to remove a creator as an admin.");
}
try {
return await dataaccess
.changeGroupMembership(groupId, userId, dataaccess.MembershipChangeAction.DEOP);
} catch (err) {
res.status(status.BAD_REQUEST);
return err.graphqlError;
}
} else {
res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError();
}
},
async createEvent({name, dueDate, groupId}: { name: string, dueDate: string, groupId: number }) {
if (req.session.userId) {
const date = new Date(dueDate);
const group = await models.Group.findByPk(groupId);
return group.$create<models.Event>("rEvent", {name, dueDate: date});
} else {
res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError();
}
},
async joinEvent({eventId}: { eventId: number }) {
if (req.session.userId) {
const event = await models.Event.findByPk(eventId);
const self = await models.User.findByPk(req.session.userId);
await event.$add("rParticipants", self);
return event;
} else {
res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError();
}
},
async leaveEvent({eventId}: { eventId: number }) {
if (req.session.userId) {
const event = await models.Event.findByPk(eventId);
const self = await models.User.findByPk(req.session.userId);
await event.$remove("rParticipants", self);
return event;
} else {
res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError();
}
},
}; };
} }

@ -1,236 +1,390 @@
type Query { type Query {
"returns the user object for a given user id or a handle (only one required)" "returns the user object for a given user id or a handle (only one required)"
getUser(userId: ID, handle: String): User getUser(userId: ID, handle: String): User
"returns the logged in user" "returns the logged in user"
getSelf: Profile 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
"find a post by the posted date or content" "return shte group object for its id"
findPost(first: Int, offset: Int, text: String!, postedDate: String): [Post] getGroup(groupId: ID!): Group
"find a user by user name or handle" "returns the request object for its id"
findUser(first: Int, offset: Int, name: String!, handle: String!): [User] getRequest(requestId: ID!): Request
}
"find a post by the posted date or content"
type Mutation { findPost(first: Int, offset: Int, text: String!, postedDate: String): [Post]
"Accepts the usage of cookies."
acceptCookies: Boolean "find a user by user name or handle"
findUser(first: Int, offset: Int, name: String, handle: String): [User]
"Login of the user. The passwordHash should be a sha512 hash of the password."
login(email: String, passwordHash: String): Profile "returns the post filtered by the sort type with pagination."
getPosts(first: Int=20, offset: Int=0, sort: SortType = NEW): [Post]
"Registers the user." }
register(username: String, email: String, passwordHash: String): Profile
type Mutation {
"Logout of the user." "Accepts the usage of cookies."
logout: Boolean acceptCookies: Boolean
"Upvote/downvote a Post" "Login of the user. The passwordHash should be a sha512 hash of the password."
vote(postId: ID!, type: VoteType!): VoteType login(email: String, passwordHash: String): Profile
"Report the post" "Registers the user."
report(postId: ID!): Boolean register(username: String, email: String, passwordHash: String): Profile
"send a request" "Logout of the user."
sendRequest(receiver: ID!, type: RequestType): Request logout: Boolean
"lets you accept a request for a given request id" "Upvote/downvote a Post"
acceptRequest(sender: ID!, type: RequestType): Boolean vote(postId: ID!, type: VoteType!): VoteType
"lets you deny a request for a given request id" "Report the post"
denyRequest(requestId: ID!): Boolean report(postId: ID!): Boolean
"send a message in a Chatroom" "send a request"
sendMessage(chatId: ID!, content: String!): ChatMessage sendRequest(receiver: ID!, type: RequestType): Request
"create the post" "lets you accept a request for a given request id"
createPost(content: String!): Post acceptRequest(sender: ID!, type: RequestType): Boolean
"delete the post for a given post id" "lets you deny a request for a given request id"
deletePost(postId: ID!): Boolean denyRequest(requestId: ID!): Boolean
"Creates a chat between the user (and optional an other user)" "removes a friend"
createChat(members: [ID!]): ChatRoom removeFriend(friendId: ID!): Boolean
}
"send a message in a Chatroom"
interface UserData { sendMessage(chatId: ID!, content: String!): ChatMessage
"url for the Profile picture of the User"
profilePicture: String "create the post"
createPost(content: String!): Post!
"name of the User"
name: String! "delete the post for a given post id"
deletePost(postId: ID!): Boolean!
"unique identifier name from the User"
handle: String! "Creates a chat between the user (and optional an other user)"
createChat(members: [ID!]): ChatRoom!
"Id of the User"
id: ID! "Creates a new group with a given name and additional members"
createGroup(name: String!, members: [ID!]): Group!
"the total number of posts the user posted"
numberOfPosts: Int "Joins a group with the given id"
joinGroup(id: ID!): Group
"returns a given number of posts of a user"
posts(first: Int=10, offset: Int): [Post] "leaves the group with the given id"
leaveGroup(id: ID!): Group
"creation date of the user account"
joinedAt: String! "adds an admin to the group"
addGroupAdmin(groupId: ID!, userId: ID!): Group
"all friends of the user"
friends: [User] "removes an admin from the group"
} removeGroupAdmin(groupId: ID!, userId: ID!): Group
"represents a single user account" "Creates a new event with a epoch due date on a group."
type User implements UserData{ createEvent(name: String, dueDate: String, groupId: ID!): Event
"url for the Profile picture of the User"
profilePicture: String "Joins a event."
joinEvent(eventId: ID!): Event
"name of the User"
name: String! "Leaves a event."
leaveEvent(eventId: ID!): Event
"unique identifier name from the User" }
handle: String!
interface UserData {
"Id of the User" "url for the Profile picture of the User"
id: ID! profilePicture: String
"the total number of posts the user posted" "name of the User"
numberOfPosts: Int name: String!
"returns a given number of posts of a user" "unique identifier name from the User"
posts(first: Int=10, offset: Int): [Post] handle: String!
"creation date of the user account" "Id of the User"
joinedAt: String! id: ID!
"all friends of the user" "DEPRECATED! the total number of posts the user posted"
friends: [User] numberOfPosts: Int!
}
"the number of posts the user has created"
type Profile implements UserData { postCount: Int!
"url for the Profile picture of the User"
profilePicture: String "returns a given number of posts of a user"
posts(first: Int=10, offset: Int=0): [Post]
"name of the User"
name: String! "creation date of the user account"
joinedAt: String!
"returns the chatrooms the user joined."
chats(first: Int=10, offset: Int): [ChatRoom] "all friends of the user"
friends(first: Int=10, offset: Int=0): [User]
"unique identifier name from the User"
handle: String! "The number of friends the user has"
friendCount: Int!
"Id of the User"
id: ID! "The groups the user has joined"
groups(first: Int=10, offset: Int=0): [Group]
"the total number of posts the user posted"
numberOfPosts: Int "The numbef of groups the user has joined"
groupCount: Int!
"returns a given number of posts of a user"
posts(first: Int=10, offset: Int): [Post] "the points of the user"
points: Int!
"creation date of the user account"
joinedAt: String! "the levels of the user depending on the points"
level: Int!
"all friends of the user" }
friends: [User]
"represents a single user account"
"all sent request for groupChats/friends/events" type User implements UserData{
sentRequests: [Request] "url for the Profile picture of the User"
profilePicture: String
"all received request for groupChats/friends/events"
receivedRequests: [Request] "name of the User"
} name: String!
"represents a single user post" "unique identifier name from the User"
type Post { handle: String!
"The id of the post." "Id of the User"
id: ID! id: ID!
"the text of the post" "the total number of posts the user posted"
content: String numberOfPosts: Int!
"the content of the post rendered by markdown-it" "returns a given number of posts of a user"
htmlContent: String posts(first: Int=10, offset: Int): [Post]
"upvotes of the Post" "the number of posts the user has created"
upvotes: Int! postCount: Int!
"downvotes of the Post" "creation date of the user account"
downvotes: Int! joinedAt: String!
"the user that is the author of the Post" "all friends of the user"
author: User! friends(first: Int=10, offset: Int=0): [User]
"date the post was created" "The number of friends the user has"
createdAt: String! friendCount: Int!
"the type of vote the user performed on the post" "the points of the user"
userVote: VoteType points: Int!
}
"the groups the user has joined"
"represents a request of any type" groups(first: Int=10, offset: Int=0): [Group]
type Request {
"Id of the user who sended the request" "The numbef of groups the user has joined"
sender: User! groupCount: Int!
"Id of the user who received the request" "the levels of the user depending on the points"
receiver: User! level: Int!
}
"type of the request"
type: RequestType! type Profile implements UserData {
} "url for the Profile picture of the User"
profilePicture: String
"represents a chatroom"
type ChatRoom { "name of the User"
"the socket.io namespace for the chatroom" name: String!
namespace: String
"the email of the user"
"the members of the chatroom" email: String!
members: [User!]
"returns the chatrooms the user joined."
"return a specfic range of messages posted in the chat" chats(first: Int=10, offset: Int): [ChatRoom]
messages(first: Int = 10, offset: Int, containing: String): [ChatMessage]!
"the count of the users chats"
"id of the chat" chatCount: Int!
id: ID!
} "unique identifier name from the User"
handle: String!
type ChatMessage {
"The author of the chat message." "Id of the User"
author: User! id: ID!
"The chatroom the message was posted in" "the total number of posts the user posted"
chat: ChatRoom! numberOfPosts: Int!
"The timestamp when the message was posted (epoch)." "the number of posts the user has created"
createdAt: String! postCount: Int!
"The content of the message." "returns a given number of posts of a user"
content: String! posts(first: Int=10, offset: Int): [Post!]!
"The content of the message rendered by markdown-it." "creation date of the user account"
htmlContent: String joinedAt: String!
}
"all friends of the user"
"represents the type of vote performed on a post" friends(first: Int=10, offset: Int=0): [User!]!
enum VoteType {
UPVOTE "The number of friends the user has"
DOWNVOTE friendCount: Int!
}
"all sent request for groupChats/friends/events"
""" sentRequests: [Request!]!
represents the type of request that the user has received
Currently on Friend Requests are implemented. "all received request for groupChats/friends/events"
""" receivedRequests: [Request!]!
enum RequestType {
FRIENDREQUEST "all groups the user is an admin of"
GROUPINVITE administratedGroups: [Group!]!
EVENTINVITE
} "all groups the user has created"
createdGroups: [Group!]!
"all groups the user has joined"
groups(first: Int=10, offset: Int=0): [Group!]!
"The numbef of groups the user has joined"
groupCount: Int!
"the points of the user"
points: Int!
"the levels of the user depending on the points"
level: Int!
}
"represents a single user post"
type Post {
"The id of the post."
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: Int!
"downvotes of the Post"
downvotes: Int!
"the user that is the author of the Post"
author: User!
"date the post was created"
createdAt: String!
"the type of vote the user performed on the post"
userVote: VoteType
}
"represents a request of any type"
type Request {
"Id of the request."
id: ID!
"Id of the user who sended the request"
sender: User!
"Id of the user who received the request"
receiver: User!
"type of the request"
type: RequestType!
}
"represents a chatroom"
type ChatRoom {
"the socket.io namespace for the chatroom"
namespace: String
"the members of the chatroom"
members(first: Int=10, offset: Int=0): [User!]
"return a specfic range of messages posted in the chat"
messages(first: Int = 10, offset: Int, containing: String): [ChatMessage]!
"id of the chat"
id: ID!
}
type ChatMessage {
"Id of the chat message"
id: ID!
"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
}
type Group {
"ID of the group"
id: ID!
"name of the group"
name: String!
"the creator of the group"
creator: User
"all admins of the group"
admins(first: Int=10, offset: Int=0): [User]!
"the members of the group with pagination"
members(first: Int = 10, offset: Int = 0): [User]!
"the groups chat"
chat: ChatRoom
"the events of the group"
events(first: Int=10, offset: Int=0): [Event!]!
}
type Event {
"ID of the event"
id: ID!
"Name of the event"
name: String!
"The date of the event."
dueDate: String!
"The group the event belongs to."
group: Group!
"The participants of the event."
participants(first: Int=10, offset: Int=0): [User!]!
}
"represents the type of vote performed on a post"
enum VoteType {
UPVOTE
DOWNVOTE
}
"""
represents the type of request that the user has received
Currently on Friend Requests are implemented.
"""
enum RequestType {
FRIENDREQUEST
GROUPINVITE
EVENTINVITE
}
enum SortType {
TOP
NEW
}

@ -1,11 +1,11 @@
import App from "./app"; import App from "./app";
/** /**
* async main function wrapper. * async main function wrapper.
*/ */
(async () => { (async () => {
const app = new App(); const app = new App();
await app.init(); await app.init();
app.start(); app.start();
})(); })();

@ -1,8 +1,11 @@
export enum InternalEvents { /**
CHATCREATE = "chatCreate", * Events that are emitted and processsed internally on the server
CHATMESSAGE = "chatMessage", */
GQLCHATMESSAGE = "graphqlChatMessage", export enum InternalEvents {
REQUESTCREATE = "requestCreate", CHATCREATE = "chatCreate",
POSTCREATE = "postCreate", CHATMESSAGE = "chatMessage",
GQLPOSTCREATE = "graphqlPostCreate", GQLCHATMESSAGE = "graphqlChatMessage",
} REQUESTCREATE = "requestCreate",
POSTCREATE = "postCreate",
GQLPOSTCREATE = "graphqlPostCreate",
}

@ -1,73 +0,0 @@
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("sha1");
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];
}
}
}
}

@ -1,212 +0,0 @@
/**
* @author Trivernis
* @remarks
*
* Taken from {@link https://github.com/Trivernis/whooshy}
*/
import * as fsx from "fs-extra";
import {Pool, PoolClient, QueryConfig, QueryResult} from "pg";
import globals from "./globals";
const logger = globals.logger;
export interface IAdvancedQueryConfig extends QueryConfig {
cache?: boolean;
}
/**
* Transaction class to wrap SQL transactions.
*/
export class SqlTransaction {
/**
* Constructor.
* @param client
*/
constructor(private client: PoolClient) {
}
/**
* Begins the transaction.
*/
public async begin() {
return await this.client.query("BEGIN");
}
/**
* Commits the transaction
*/
public async commit() {
return await this.client.query("COMMIT");
}
/**
* Rolls back the transaction
*/
public async rollback() {
return await this.client.query("ROLLBACK");
}
/**
* Executes a query inside the transaction.
* @param query
*/
public async query(query: QueryConfig) {
return await this.client.query(query);
}
/**
* Releases the client back to the pool.
*/
public release() {
this.client.release();
}
}
/**
* Query helper for easyer fetching of a specific row count.
*/
export class QueryHelper {
private pool: Pool;
/**
* Constructor.
* @param pgPool
* @param [tableCreationFile]
* @param [tableUpdateFile]
*/
constructor(pgPool: Pool, private tableCreationFile?: string, private tableUpdateFile?: string) {
this.pool = pgPool;
}
/**
* Async init function
*/
public async init() {
await this.pool.connect();
await this.createTables();
await this.updateTableDefinitions();
}
/**
* creates all tables needed if a filepath was given with the constructor
*/
public async createTables() {
if (this.tableCreationFile) {
logger.info("Creating nonexistent tables...");
const tableSql = await fsx.readFile(this.tableCreationFile, "utf-8");
const trans = await this.createTransaction();
await trans.begin();
try {
await trans.query({text: tableSql});
await trans.commit();
} catch (err) {
globals.logger.error(`Error on table creation ${err.message}`);
globals.logger.debug(err.stack);
await trans.rollback();
} finally {
trans.release();
}
}
}
/**
* Updates the definition of the tables if the table update file was passed in the constructor
*/
public async updateTableDefinitions() {
if (this.tableUpdateFile) {
logger.info("Updating table definitions...");
const tableSql = await fsx.readFile(this.tableUpdateFile, "utf-8");
const trans = await this.createTransaction();
await trans.begin();
try {
await trans.query({text: tableSql});
await trans.commit();
} catch (err) {
globals.logger.error(`Error on table update ${err.message}`);
globals.logger.debug(err.stack);
await trans.rollback();
} finally {
trans.release();
}
}
}
/**
* executes the sql query with values and returns all results.
* @param query
*/
public async all(query: IAdvancedQueryConfig): Promise<any[]> {
const result = await this.query(query);
return result.rows;
}
/**
* executes the sql query with values and returns the first result.
* @param query
*/
public async first(query: IAdvancedQueryConfig): Promise<any> {
const result = await this.query(query);
if (result.rows && result.rows.length > 0) {
return result.rows[0];
}
}
/**
* Creates a new Transaction to be uses with error handling.
*/
public async createTransaction() {
const client: PoolClient = await this.pool.connect();
return new SqlTransaction(client);
}
/**
* Queries the database with error handling.
* @param query - the sql and values to execute
*/
private async query(query: IAdvancedQueryConfig): Promise<QueryResult|{rows: any}> {
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);
}
} catch (err) {
logger.debug(`Error on query "${JSON.stringify(query)}".`);
logger.error(`Sql query failed: ${err}`);
logger.verbose(err.stack);
return {
rows: null,
};
}
}
}
/**
* Returns the parameterized value sql for inserting
* @param columnCount
* @param rowCount
* @param [offset]
*/
export function buildSqlParameters(columnCount: number, rowCount: number, offset?: number): string {
let sql = "";
for (let i = 0; i < rowCount; i++) {
sql += "(";
for (let j = 0; j < columnCount; j++) {
sql += `$${(i * columnCount) + j + 1 + offset},`;
}
sql = sql.replace(/,$/, "") + "),";
}
return sql.replace(/,$/, "");
}

@ -1,26 +1,20 @@
/** import {Router} from "express";
* @author Trivernis import {Namespace, Server} from "socket.io";
* @remarks
* /**
* Taken from {@link https://github.com/Trivernis/whooshy} * Abstract Route class to be implemented by each route.
*/ * This class contains the socket-io Server, router and resolver
* for each route.
import {Router} from "express"; */
import {Namespace, Server} from "socket.io"; abstract class Route {
/** public router?: Router;
* Abstract Route class to be implemented by each route. protected io?: Server;
* This class contains the socket-io Server, router and resolver protected ions?: Namespace;
* for each route.
*/ public abstract async init(...params: any): Promise<any>;
abstract class Route {
public abstract async destroy(...params: any): Promise<any>;
public router?: Router; }
protected io?: Server;
protected ions?: Namespace; export default Route;
public abstract async init(...params: any): Promise<any>;
public abstract async destroy(...params: any): Promise<any>;
}
export default Route;

@ -0,0 +1,317 @@
import * as crypto from "crypto";
import * as sqz from "sequelize";
import {Sequelize} from "sequelize-typescript";
import {ChatNotFoundError} from "./errors/ChatNotFoundError";
import {EmailAlreadyRegisteredError} from "./errors/EmailAlreadyRegisteredError";
import {GroupNotFoundError} from "./errors/GroupNotFoundError";
import {InvalidLoginError} from "./errors/InvalidLoginError";
import {NoActionSpecifiedError} from "./errors/NoActionSpecifiedError";
import {UserNotFoundError} from "./errors/UserNotFoundError";
import globals from "./globals";
import {InternalEvents} from "./InternalEvents";
import * as models from "./models";
/**
* Generates a new handle from the username and a base64 string of the current time.
* @param username
*/
async function generateHandle(username: string) {
username = username.toLowerCase().replace(/\s/g, "_");
const count = await models.User.count({where: {handle: {[sqz.Op.like]: `%${username}%`}}});
if (count > 0) {
return `${username}${count}`;
} else {
return username;
}
}
/**
* Namespace with functions to fetch initial data for wrapping.
*/
namespace dataaccess {
let sequelize: Sequelize;
/**
* Initializes everything that needs to be initialized asynchronous.
*/
export async function init(seq: Sequelize) {
sequelize = seq;
try {
await sequelize.addModels([
models.ChatMember,
models.ChatMessage,
models.ChatRoom,
models.Friendship,
models.Post,
models.PostVote,
models.Request,
models.User,
models.Group,
models.GroupAdmin,
models.GroupMember,
models.EventParticipant,
models.Event,
]);
} catch (err) {
globals.logger.error(err.message);
globals.logger.debug(err.stack);
}
}
/**
* Returns the user by handle.
* @param userHandle
*/
export async function getUserByHandle(userHandle: string): Promise<models.User> {
const user = await models.User.findOne({where: {handle: userHandle}});
if (user) {
return user;
} 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<models.User> {
const hash = crypto.createHash("sha512");
hash.update(password);
password = hash.digest("hex");
const user = await models.User.findOne({where: {email}});
if (user) {
if (user.password === password) {
return user;
} else {
throw new InvalidLoginError(email);
}
} 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): Promise<models.User> {
const hash = crypto.createHash("sha512");
hash.update(password);
password = hash.digest("hex");
const existResult = !!(await models.User.findOne({where: {username, email, password}}));
const handle = await generateHandle(username);
if (!existResult) {
return models.User.create({username, email, password, handle});
} else {
throw new EmailAlreadyRegisteredError(email);
}
}
/**
* Returns a post for a given postId.s
* @param postId
*/
export async function getPost(postId: number): Promise<models.Post> {
const post = await models.Post.findByPk(postId);
if (post) {
return post;
} else {
return null;
}
}
/**
* Returns all posts sorted by new or top with pagination.
* @param first
* @param offset
* @param sort
*/
export async function getPosts(first: number, offset: number, sort: SortType) {
if (sort === SortType.NEW) {
return models.Post.findAll({
include: [{association: "rVotes"}],
limit: first,
offset,
order: [["createdAt", "DESC"]],
});
} else {
return await sequelize.query(
`SELECT * FROM (
SELECT *,
(SELECT count(*) FROM post_votes WHERE vote_type = 'UPVOTE' AND post_id = posts.id) AS upvotes ,
(SELECT count(*) FROM post_votes WHERE vote_type = 'DOWNVOTE' AND post_id = posts.id) AS downvotes
FROM posts) AS a ORDER BY (a.upvotes - a.downvotes) DESC LIMIT ? OFFSET ?`,
{replacements: [first, offset], mapToModel: true, model: models.Post}) as models.Post[];
}
}
/**
* Creates a post
* @param content
* @param authorId
* @param type
*/
export async function createPost(content: string, authorId: number, type?: string): Promise<models.Post> {
type = type || "MISC";
const post = await models.Post.create({content, authorId});
globals.internalEmitter.emit(InternalEvents.POSTCREATE, post);
return post;
}
/**
* Deletes a post
* @param postId
*/
export async function deletePost(postId: number): Promise<boolean> {
await (await models.Post.findByPk(postId)).destroy();
return true;
}
/**
* Creates a chatroom containing two users
* @param members
*/
export async function createChat(...members: number[]): Promise<models.ChatRoom> {
return sequelize.transaction(async (t) => {
const chat = await models.ChatRoom.create({}, {transaction: t, include: [models.User]});
for (const member of members) {
const user = await models.User.findByPk(member);
await chat.$add("rMember", user, {transaction: t});
}
await chat.save({transaction: t});
globals.internalEmitter.emit(InternalEvents.CHATCREATE, chat);
return chat;
});
}
/**
* Sends a message into a chat.
* @param authorId
* @param chatId
* @param content
*/
export async function sendChatMessage(authorId: number, chatId: number, content: string) {
const chat = await models.ChatRoom.findByPk(chatId);
if (chat) {
const message = await chat.$create("rMessage", {content, authorId}) as models.ChatMessage;
globals.internalEmitter.emit(InternalEvents.CHATMESSAGE, message);
return message;
} else {
throw new ChatNotFoundError(chatId);
}
}
/**
* Returns all rChats.
*/
export async function getAllChats(): Promise<models.ChatRoom[]> {
return models.ChatRoom.findAll();
}
/**
* Sends a request to a user.
* @param sender
* @param receiver
* @param requestType
*/
export async function createRequest(sender: number, receiver: number, requestType?: RequestType) {
requestType = requestType || RequestType.FRIENDREQUEST;
const request = await models.Request.create({senderId: sender, receiverId: receiver, requestType});
globals.internalEmitter.emit(InternalEvents.REQUESTCREATE, request);
return request;
}
/**
* Create a new group.
* @param name
* @param creator
* @param members
*/
export async function createGroup(name: string, creator: number, members: number[]): Promise<models.Group> {
members = members || [];
return sequelize.transaction(async (t) => {
members.push(creator);
const groupChat = await createChat(...members);
const group = await models.Group.create({name, creatorId: creator, chatId: groupChat.id}, {transaction: t});
const creatorUser = await models.User.findByPk(creator, {transaction: t});
await group.$add("rAdmins", creatorUser, {transaction: t});
for (const member of members) {
const user = await models.User.findByPk(member, {transaction: t});
await group.$add("rMembers", user, {transaction: t});
}
return group;
});
}
/**
* Changes the membership of a user
* @param groupId
* @param userId
* @param action
*/
export async function changeGroupMembership(groupId: number, userId: number, action: MembershipChangeAction):
Promise<models.Group> {
const group = await models.Group.findByPk(groupId);
if (group) {
const user = await models.User.findByPk(userId);
if (user) {
if (action === MembershipChangeAction.ADD) {
await group.$add("rMembers", user);
} else if (action === MembershipChangeAction.REMOVE) {
await group.$remove("rMembers", user);
} else if (action === MembershipChangeAction.OP) {
await group.$add("rAdmins", user);
} else if (action === MembershipChangeAction.DEOP) {
await group.$remove("rAdmins", user);
} else {
throw new NoActionSpecifiedError(MembershipChangeAction);
}
return group;
} else {
throw new UserNotFoundError(userId);
}
} else {
throw new GroupNotFoundError(groupId);
}
}
/**
* Enum representing the types of votes that can be performed on a post.
*/
export enum VoteType {
UPVOTE = "UPVOTE",
DOWNVOTE = "DOWNVOTE",
}
/**
* Enum representing the types of request that can be created.
*/
export enum RequestType {
FRIENDREQUEST = "FRIENDREQUEST",
GROUPINVITE = "GROUPINVITE",
EVENTINVITE = "EVENTINVITE",
}
/**
* Enum representing the types of sorting in the feed.
*/
export enum SortType {
TOP = "TOP",
NEW = "NEW",
}
export enum MembershipChangeAction {
ADD,
REMOVE,
OP,
DEOP,
}
}
export default dataaccess;

@ -1,31 +0,0 @@
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);
}
/**
* Returns resolved and rendered content of the chat message.
*/
public resolvedContent() {
return {
author: this.author.id,
chat: this.chat.id,
content: this.content,
createdAt: this.createdAt,
htmlContent: this.htmlContent(),
};
}
}

@ -1,76 +0,0 @@
import globals from "../globals";
import {ChatMessage} from "./ChatMessage";
import {queryHelper} from "./index";
import {User} from "./User";
export class Chatroom {
public namespace: string;
constructor(public readonly id: number) {
this.id = Number(id);
this.namespace = `/chat/${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,40 +0,0 @@
/**
* abstact DataObject class
*/
import {EventEmitter} from "events";
export abstract class DataObject extends EventEmitter {
protected dataLoaded: boolean = false;
private loadingData: boolean = false;
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>;
/**
* Loads data from the database if data has not been loaded
*/
protected async loadDataIfNotExists() {
if (!this.dataLoaded && !this.loadingData) {
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,159 +0,0 @@
import markdown from "../markdown";
import {DataObject} from "./DataObject";
import {queryHelper} from "./index";
import dataaccess from "./index";
import {User} from "./User";
export class Post extends DataObject {
public readonly id: number;
private $createdAt: string;
private $content: string;
private $author: number;
private $type: string;
/**
* Returns the resolved data of the post.
*/
public async resolvedData() {
await this.loadDataIfNotExists();
return {
authorId: this.$author,
content: this.$content,
createdAt: this.$createdAt,
id: this.id,
type: this.$type,
};
}
/**
* Returns the upvotes of a post.
*/
public async upvotes(): Promise<number> {
const result = await queryHelper.first({
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
*/
public async downvotes(): Promise<number> {
const result = await queryHelper.first({
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)
*/
public async content(): Promise<string> {
await this.loadDataIfNotExists();
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.
*/
public async createdAt(): Promise<string> {
await this.loadDataIfNotExists();
return this.$createdAt;
}
/**
* The autor of the post.
*/
public async author(): Promise<User> {
await this.loadDataIfNotExists();
return new User(this.$author);
}
/**
* Deletes the post.
*/
public async delete(): Promise<void> {
const query = await queryHelper.first({
text: "DELETE FROM posts WHERE id = $1",
values: [this.id],
});
}
/**
* The type of vote the user performed on the post.
*/
public async userVote(userId: number): Promise<dataaccess.VoteType> {
const result = await queryHelper.first({
cache: true,
text: "SELECT vote_type FROM votes WHERE user_id = $1 AND item_id = $2",
values: [userId, this.id],
});
if (result) {
return result.vote_type;
} else {
return null;
}
}
/**
* 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.
*/
protected async loadData(): Promise<void> {
let result: any;
if (this.row) {
result = this.row;
} else {
result = await queryHelper.first({
cache: true,
text: "SELECT * FROM posts WHERE posts.id = $1",
values: [this.id],
});
}
if (result) {
this.$author = result.author;
this.$content = result.content;
this.$createdAt = result.created_at;
this.$type = result.type;
this.dataLoaded = true;
}
}
}

@ -1,163 +0,0 @@
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;
}
}

@ -1,24 +0,0 @@
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) {
}
/**
* Returns the resolved request data.
*/
public resolvedData() {
return {
receiverId: this.receiver.id,
senderId: this.sender.id,
type: this.type,
};
}
}

@ -1,128 +0,0 @@
import globals from "../globals";
import {DataObject} from "./DataObject";
import {queryHelper} from "./index";
import {Post} from "./Post";
export class User extends DataObject {
private $name: string;
private $handle: string;
private $email: string;
private $greenpoints: number;
private $joinedAt: string;
private $exists: boolean;
/**
* The name of the user
*/
public async name(): Promise<string> {
await this.loadDataIfNotExists();
return this.$name;
}
/**
* The unique handle of the user.
*/
public async handle(): Promise<string> {
await this.loadDataIfNotExists();
return this.$handle;
}
/**
* The email of the user
*/
public async email(): Promise<string> {
await this.loadDataIfNotExists();
return this.$email;
}
/**
* The number of greenpoints of the user
*/
public async greenpoints(): Promise<number> {
await this.loadDataIfNotExists();
return this.$greenpoints;
}
/**
* Returns the number of posts the user created
*/
public async numberOfPosts(): Promise<number> {
const result = await queryHelper.first({
cache: true,
text: "SELECT COUNT(*) count FROM posts WHERE author = $1",
values: [this.id],
});
return result.count;
}
/**
* The date the user joined the platform
*/
public async joinedAt(): Promise<Date> {
await this.loadDataIfNotExists();
return new Date(this.$joinedAt);
}
/**
* Returns all friends of the user.
*/
public async friends(): Promise<User[]> {
const result = await queryHelper.all({
cache: true,
text: "SELECT * FROM user_friends WHERE user_id = $1 OR friend_id = $1",
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 = [];
for (const row of result) {
posts.push(new Post(row.id, row));
}
return posts;
}
/**
* Fetches the data for the user.
*/
protected async loadData(): Promise<void> {
let result: any;
if (this.row) {
result = this.row;
} else {
result = await queryHelper.first({
cache: true,
text: "SELECT * FROM users WHERE users.id = $1",
values: [this.id],
});
}
if (result) {
this.$name = result.name;
this.$handle = result.handle;
this.$email = result.email;
this.$greenpoints = result.greenpoints;
this.$joinedAt = result.joined_at;
this.dataLoaded = true;
}
}
}

@ -1,258 +0,0 @@
import {Pool} from "pg";
import {ChatNotFoundError} from "../errors/ChatNotFoundError";
import {EmailAlreadyRegisteredError} from "../errors/EmailAlreadyRegisteredError";
import {UserNotFoundError} from "../errors/UserNotFoundError";
import globals from "../globals";
import {InternalEvents} from "../InternalEvents";
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";
const config = globals.config;
const tableCreationFile = __dirname + "/../../sql/create-tables.sql";
const tableUpdateFile = __dirname + "/../../sql/update-tables.sql";
const dbClient: Pool = new Pool({
database: config.database.database,
host: config.database.host,
password: config.database.password,
port: config.database.port,
user: config.database.user,
});
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 {
export const pool: Pool = dbClient;
/**
* Initializes everything that needs to be initialized asynchronous.
*/
export async function init() {
try {
await queryHelper.init();
} catch (err) {
globals.logger.error(err.message);
globals.logger.debug(err.stack);
}
}
/**
* Returns the user by handle.
* @param userHandle
*/
export async function getUserByHandle(userHandle: string): Promise<User> {
const result = await queryHelper.first({
text: "SELECT * FROM users WHERE users.handle = $1",
values: [userHandle],
});
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 existResult = await queryHelper.first({
text: "SELECT email FROM users WHERE email = $1;",
values: [email],
});
if (!existResult || !existResult.email) {
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);
} else {
throw new EmailAlreadyRegisteredError(email);
}
}
/**
* 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],
});
const post = new Post(result.id, result);
globals.internalEmitter.emit(InternalEvents.POSTCREATE, post);
return post;
}
/**
* 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();
}
const chat = new Chatroom(id);
globals.internalEmitter.emit(InternalEvents.CHATCREATE, chat);
return chat;
}
/**
* 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],
});
const message = new ChatMessage(new User(result.author), chat, result.created_at, result.content);
globals.internalEmitter.emit(InternalEvents.CHATMESSAGE, message);
return message;
} else {
throw new ChatNotFoundError(chatId);
}
}
/**
* Returns all chats.
*/
export async function getAllChats(): Promise<Chatroom[]> {
const result = await queryHelper.all({
text: "SELECT id FROM chats;",
});
const chats = [];
for (const row of result) {
chats.push(new Chatroom(row.id));
}
return chats;
}
/**
* 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],
});
const request = new Request(new User(result.sender), new User(result.receiver), result.type);
globals.internalEmitter.emit(InternalEvents.REQUESTCREATE, Request);
return request;
}
/**
* Enum representing the types of votes that can be performed on a post.
*/
export enum VoteType {
UPVOTE = "UPVOTE",
DOWNVOTE = "DOWNVOTE",
}
/**
* Enum representing the types of request that can be created.
*/
export enum RequestType {
FRIENDREQUEST = "FRIENDREQUEST",
GROUPINVITE = "GROUPINVITE",
EVENTINVITE = "EVENTINVITE",
}
}
export default dataaccess;

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

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

@ -1,8 +1,8 @@
import {BaseError} from "./BaseError"; import {BaseError} from "./BaseError";
export class EmailAlreadyRegisteredError extends BaseError { export class EmailAlreadyRegisteredError extends BaseError {
constructor(email: string) { constructor(email: string) {
super(`A user for '${email}' does already exist.`); super(`A user for '${email}' does already exist.`);
} }
} }

@ -0,0 +1,8 @@
import {BaseError} from "./BaseError";
export class GroupNotFoundError extends BaseError {
constructor(groupId: number) {
super(`Group ${groupId} not found!`);
}
}

@ -0,0 +1,7 @@
import {BaseError} from "./BaseError";
export class InvalidLoginError extends BaseError {
constructor(email: (string)) {
super(`Invalid login data for ${email}.`);
}
}

@ -0,0 +1,11 @@
import {BaseError} from "./BaseError";
export class NoActionSpecifiedError extends BaseError {
constructor(actions?: any) {
if (actions) {
super(`No action of '${Object.keys(actions).join(", ")}'`);
} else {
super("No action specified!");
}
}
}

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

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

@ -1,9 +1,19 @@
import {GraphQLError} from "graphql"; import {GraphQLError} from "graphql";
import {BaseError} from "./BaseError";
export class NotLoggedInGqlError extends GraphQLError {
export class NotLoggedInGqlError extends GraphQLError { constructor() {
super("Not logged in");
constructor() { }
super("Not logged in"); }
}
} export class PostNotFoundGqlError extends GraphQLError {
constructor(postId: number) {
super(`Post '${postId}' not found!`);
}
}
export class GroupNotFoundGqlError extends GraphQLError {
constructor(groupId: number) {
super(`Group '${groupId}' not found!`);
}
}

@ -1,15 +1,9 @@
/**
* @author Trivernis
* @remarks
*
* Partly taken from {@link https://github.com/Trivernis/whooshy}
*/
import {EventEmitter} from "events"; import {EventEmitter} from "events";
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";
require("winston-daily-rotate-file");
const configPath = "config.yaml"; const configPath = "config.yaml";
const defaultConfig = __dirname + "/../default-config.yaml"; const defaultConfig = __dirname + "/../default-config.yaml";
@ -28,25 +22,37 @@ 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); // @ts-ignore
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, timestamp }) => { winston.format.printf(({level, message, timestamp}) => {
return `${timestamp} ${level}: ${message}`;
}),
),
level: config.logging.level,
}),
// @ts-ignore
new (winston.transports.DailyRotateFile)({
dirname: "logs",
filename: "gv-%DATE%.log",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.printf(({level, message, timestamp}) => {
return `${timestamp} ${level}: ${message}`; return `${timestamp} ${level}: ${message}`;
}), }),
), ),
json: false,
level: config.logging.level, level: config.logging.level,
maxFiles: "7d",
zippedArchive: true,
}), }),
], ],
}); });
export const internalEmitter = new EventEmitter(); export const internalEmitter = new EventEmitter();
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;

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

@ -0,0 +1,16 @@
import {Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
import {ChatRoom} from "./ChatRoom";
import {User} from "./User";
@Table({underscored: true})
export class ChatMember extends Model<ChatMember> {
@ForeignKey(() => User)
@NotNull
@Column({allowNull: false})
public userId: number;
@ForeignKey(() => ChatRoom)
@NotNull
@Column({allowNull: false})
public chatId: number;
}

@ -0,0 +1,44 @@
import * as sqz from "sequelize";
import {BelongsTo, Column, CreatedAt, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
import markdown from "../markdown";
import {ChatRoom} from "./ChatRoom";
import {User} from "./User";
@Table({underscored: true})
export class ChatMessage extends Model<ChatMessage> {
@NotNull
@Column({type: sqz.STRING(512), allowNull: false})
public content: string;
@ForeignKey(() => ChatRoom)
@NotNull
@Column({allowNull: false})
public chatId: number;
@ForeignKey(() => User)
@NotNull
@Column({allowNull: false})
public authorId: number;
@BelongsTo(() => ChatRoom, "chatId")
public rChat: ChatRoom;
@BelongsTo(() => User, "authorId")
public rAuthor: User;
@CreatedAt
public createdAt: Date;
public async chat(): Promise<ChatRoom> {
return await this.$get("rChat") as ChatRoom;
}
public async author(): Promise<User> {
return await this.$get("rAuthor") as User;
}
public get htmlContent(): string {
return markdown.renderInline(this.getDataValue("content"));
}
}

@ -0,0 +1,28 @@
import {BelongsToMany, CreatedAt, HasMany, Model, Table,} from "sequelize-typescript";
import {ChatMember} from "./ChatMember";
import {ChatMessage} from "./ChatMessage";
import {User} from "./User";
@Table({underscored: true})
export class ChatRoom extends Model<ChatRoom> {
@BelongsToMany(() => User, () => ChatMember)
public rMembers: User[];
@HasMany(() => ChatMessage, "chatId")
public rMessages: ChatMessage[];
@CreatedAt
public readonly createdAt!: Date;
public async members(): Promise<User[]> {
return await this.$get("rMembers") as User[];
}
public async messages(): Promise<ChatMessage[]> {
return await this.$get("rMessages") as ChatMessage[];
}
public get namespace(): string {
return "/chats/" + this.getDataValue("id");
}
}

@ -0,0 +1,36 @@
import {BelongsTo, BelongsToMany, Column, ForeignKey, Model, NotNull, Table} from "sequelize-typescript";
import {EventParticipant} from "./EventParticipant";
import {Group} from "./Group";
import {User} from "./User";
@Table({underscored: true})
export class Event extends Model<Event> {
@NotNull
@Column({allowNull: false})
public name: string;
@NotNull
@Column({allowNull: false})
public dueDate: Date;
@NotNull
@ForeignKey(() => Group)
@Column({allowNull: false})
public groupId: number;
@BelongsTo(() => Group, "groupId")
public rGroup: Group;
@BelongsToMany(() => User, () => EventParticipant)
public rParticipants: User[];
public async group(): Promise<Group> {
return await this.$get("rGroup") as Group;
}
public async participants({first, offset}: {first: number, offset: number}): Promise<User[]> {
const limit = first || 10;
offset = offset || 0;
return await this.$get("rParticipants", {limit, offset}) as User[];
}
}

@ -0,0 +1,16 @@
import {BelongsTo, BelongsToMany, Column, ForeignKey, Model, NotNull, Table} from "sequelize-typescript";
import {Event} from "./Event";
import {User} from "./User";
@Table({underscored: true})
export class EventParticipant extends Model<EventParticipant> {
@NotNull
@ForeignKey(() => User)
@Column({allowNull: false})
public userId: number;
@NotNull
@ForeignKey(() => Event)
@Column({allowNull: false})
public eventId: number;
}

@ -0,0 +1,18 @@
import {Column, ForeignKey, Model, NotNull, PrimaryKey, Table} from "sequelize-typescript";
import {User} from "./User";
@Table({underscored: true})
export class Friendship extends Model<Friendship> {
@ForeignKey(() => User)
@PrimaryKey
@NotNull
@Column({allowNull: false})
public userId: number;
@ForeignKey(() => User)
@PrimaryKey
@NotNull
@Column({allowNull: false})
public friendId: number;
}

@ -0,0 +1,64 @@
import {BelongsTo, BelongsToMany, Column, ForeignKey, HasMany, Model, NotNull, Table} from "sequelize-typescript";
import {ChatRoom} from "./ChatRoom";
import {Event} from "./Event";
import {GroupAdmin} from "./GroupAdmin";
import {GroupMember} from "./GroupMember";
import {User} from "./User";
@Table({underscored: true})
export class Group extends Model<Group> {
@NotNull
@Column({allowNull: false})
public name: string;
@NotNull
@ForeignKey(() => User)
@Column({allowNull: false})
public creatorId: number;
@NotNull
@ForeignKey(() => ChatRoom)
@Column({allowNull: false})
public chatId: number;
@BelongsTo(() => User, "creatorId")
public rCreator: User;
@BelongsToMany(() => User, () => GroupAdmin)
public rAdmins: User[];
@BelongsToMany(() => User, () => GroupMember)
public rMembers: User[];
@BelongsTo(() => ChatRoom)
public rChat: ChatRoom;
@HasMany(() => Event, "groupId")
public rEvents: Event[];
public async creator(): Promise<User> {
return await this.$get("rCreator") as User;
}
public async admins({first, offset}: { first: number, offset: number }): Promise<User[]> {
const limit = first || 10;
offset = offset || 0;
return await this.$get("rAdmins", {limit, offset}) as User[];
}
public async members({first, offset}: { first: number, offset: number }): Promise<User[]> {
const limit = first || 10;
offset = offset || 0;
return await this.$get("rMembers", {limit, offset}) as User[];
}
public async chat(): Promise<ChatRoom> {
return await this.$get("rChat") as ChatRoom;
}
public async events({first, offset}: { first: number, offset: number }): Promise<Event[]> {
const limit = first || 10;
offset = offset || 0;
return await this.$get("rEvents", {limit, offset}) as Event[];
}
}

@ -0,0 +1,16 @@
import {Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
import {Group} from "./Group";
import {User} from "./User";
@Table({underscored: true})
export class GroupAdmin extends Model<GroupAdmin> {
@NotNull
@ForeignKey(() => User)
@Column({allowNull: false})
public userId: number;
@NotNull
@ForeignKey(() => Group)
@Column({allowNull: false})
public groupId: number;
}

@ -0,0 +1,16 @@
import {Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
import {Group} from "./Group";
import {User} from "./User";
@Table({underscored: true})
export class GroupMember extends Model<GroupMember> {
@NotNull
@ForeignKey(() => User)
@Column({allowNull: false})
public userId: number;
@NotNull
@ForeignKey(() => Group)
@Column({allowNull: false})
public groupId: number;
}

@ -0,0 +1,70 @@
import * as sqz from "sequelize";
import {BelongsTo, BelongsToMany, Column, CreatedAt, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
import markdown from "../markdown";
import {PostVote, VoteType} from "./PostVote";
import {User} from "./User";
@Table({underscored: true})
export class Post extends Model<Post> {
@NotNull
@Column({type: sqz.STRING(2048), allowNull: false})
public content: string;
@ForeignKey(() => User)
@NotNull
@Column({allowNull: false})
public authorId: number;
@BelongsTo(() => User, "authorId")
public rAuthor: User;
@BelongsToMany(() => User, () => PostVote)
public rVotes: Array<User & {PostVote: PostVote}>;
@CreatedAt
public readonly createdAt!: Date;
public async author(): Promise<User> {
return await this.$get("rAuthor") as User;
}
public async votes(): Promise<Array<User & {PostVote: PostVote}>> {
return await this.$get("rVotes") as Array<User & {PostVote: PostVote}>;
}
public get htmlContent() {
return markdown.render(this.getDataValue("content"));
}
public async upvotes() {
return (await this.votes()).filter((v) => v.PostVote.voteType === VoteType.UPVOTE).length;
}
public async downvotes() {
return (await this.votes()).filter((v) => v.PostVote.voteType === VoteType.DOWNVOTE).length;
}
public async vote(userId: number, type: VoteType): Promise<VoteType> {
type = type || VoteType.UPVOTE;
let votes = await this.$get("rVotes", {where: {id: userId}}) as Array<User & {PostVote: PostVote}>;
let vote = votes[0] || null;
let created = false;
if (!vote) {
await this.$add("rVote", userId);
votes = await this.$get("rVotes", {where: {id: userId}}) as Array<User & {PostVote: PostVote}>;
vote = votes[0] || null;
created = true;
}
if (vote) {
if (vote.PostVote.voteType === type && !created) {
await vote.PostVote.destroy();
return null;
} else {
vote.PostVote.voteType = type;
await vote.PostVote.save();
}
}
return vote.PostVote.voteType;
}
}

@ -0,0 +1,26 @@
import * as sqz from "sequelize";
import {Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
import {Post} from "./Post";
import {User} from "./User";
export enum VoteType {
UPVOTE = "UPVOTE",
DOWNVOTE = "DOWNVOTE",
}
@Table({underscored: true})
export class PostVote extends Model<PostVote> {
@NotNull
@Column({type: sqz.ENUM, values: ["UPVOTE", "DOWNVOTE"], defaultValue: "UPVOTE", allowNull: false})
public voteType: VoteType;
@ForeignKey(() => User)
@NotNull
@Column({allowNull: false})
public userId: number;
@ForeignKey(() => Post)
@NotNull
@Column({allowNull: false})
public postId: number;
}

@ -0,0 +1,45 @@
import * as sqz from "sequelize";
import {BelongsTo, Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript";
import {User} from "./User";
export enum RequestType {
FRIENDREQUEST = "FRIENDREQUEST",
GROUPINVITE = "GROUPINVITE",
EVENTINVITE = "EVENTINVITE",
}
@Table({underscored: true})
export class Request extends Model<Request> {
@NotNull
@Column({
allowNull: false,
defaultValue: "FRIENDREQUEST",
type: sqz.ENUM,
values: ["FRIENDREQUEST", "GROUPINVITE", "EVENTINVITE"],
})
public requestType: RequestType;
@ForeignKey(() => User)
@NotNull
@Column({allowNull: false})
public senderId: number;
@BelongsTo(() => User, "senderId")
public rSender: User;
@ForeignKey(() => User)
@NotNull
@Column({allowNull: false})
public receiverId: number;
@BelongsTo(() => User, "receiverId")
public rReceiver: User;
public async receiver(): Promise<User> {
return await this.$get("rReceiver") as User;
}
public async sender(): Promise<User> {
return await this.$get("rSender") as User;
}
}

@ -0,0 +1,288 @@
import * as sqz from "sequelize";
import {
BelongsToMany,
Column,
CreatedAt,
HasMany,
Model,
NotNull,
Table,
Unique,
UpdatedAt,
} from "sequelize-typescript";
import {RequestNotFoundError} from "../errors/RequestNotFoundError";
import {UserNotFoundError} from "../errors/UserNotFoundError";
import {ChatMember} from "./ChatMember";
import {ChatMessage} from "./ChatMessage";
import {ChatRoom} from "./ChatRoom";
import {Event} from "./Event";
import {EventParticipant} from "./EventParticipant";
import {Friendship} from "./Friendship";
import {Group} from "./Group";
import {GroupAdmin} from "./GroupAdmin";
import {GroupMember} from "./GroupMember";
import {Post} from "./Post";
import {PostVote} from "./PostVote";
import {Request, RequestType} from "./Request";
@Table({underscored: true})
export class User extends Model<User> {
@NotNull
@Column({type: sqz.STRING(128), allowNull: false})
public username: string;
@NotNull
@Unique
@Column({type: sqz.STRING(128), allowNull: false, unique: true})
public handle: string;
@Unique
@NotNull
@Column({type: sqz.STRING(128), allowNull: false, unique: true})
public email: string;
@NotNull
@Column({type: sqz.STRING(128), allowNull: false})
public password: string;
@NotNull
@Column({defaultValue: 0, allowNull: false})
public rankpoints: number;
@BelongsToMany(() => User, () => Friendship, "userId")
public rFriends: User[];
@BelongsToMany(() => User, () => Friendship, "friendId")
public rFriendOf: User[];
@BelongsToMany(() => Post, () => PostVote)
public votes: Array<Post & { PostVote: PostVote }>;
@BelongsToMany(() => ChatRoom, () => ChatMember)
public rChats: ChatRoom[];
@BelongsToMany(() => Group, () => GroupAdmin)
public rAdministratedGroups: Group[];
@BelongsToMany(() => Event, () => EventParticipant)
public rEvents: Event[];
@BelongsToMany(() => Group, () => GroupMember)
public rGroups: Group[];
@HasMany(() => Post, "authorId")
public rPosts: Post[];
@HasMany(() => Request, "senderId")
public rSentRequests: Request[];
@HasMany(() => Request, "receiverId")
public rReceivedRequests: Request[];
@HasMany(() => ChatMessage, "authorId")
public messages: ChatMessage[];
@HasMany(() => Group, "creatorId")
public rCreatedGroups: Group[];
@CreatedAt
public readonly createdAt!: Date;
@UpdatedAt
public readonly updatedAt!: Date;
/**
* The name of the user
*/
public get name(): string {
return this.getDataValue("username");
}
/**
* The date the user joined the network
*/
public get joinedAt(): Date {
return this.getDataValue("createdAt");
}
/**
* The points of the user
*/
public get points(): number {
return this.rankpoints;
}
/**
* The level of the user which is the points divided by 100
*/
public get level(): number {
return Math.ceil(this.rankpoints / 100);
}
/**
* All friends of the user
* @param first
* @param offset
*/
public async friends({first, offset}: { first: number, offset: number }): Promise<User[]> {
const limit = first || 10;
offset = offset || 0;
return await this.$get("rFriendOf", {limit, offset}) as User[];
}
/**
* The total number of the users friends.
*/
public async friendCount(): Promise<number> {
return this.$count("rFriends");
}
/**
* The chats the user has joined
* @param first
* @param offset
*/
public async chats({first, offset}: { first: number, offset: number }): Promise<ChatRoom[]> {
const limit = first || 10;
offset = offset || 0;
return await this.$get("rChats", {limit, offset}) as ChatRoom[];
}
/**
* the number of chats the user has
*/
public async chatCount(): Promise<number> {
return this.$count("rChats");
}
/**
* All active requests the user ha ssent
*/
public async sentRequests(): Promise<Request[]> {
return await this.$get("rSentRequests") as Request[];
}
/**
* All requests the user has received
*/
public async receivedRequests(): Promise<Request[]> {
return await this.$get("rReceivedRequests") as Request[];
}
public async posts({first, offset}: { first: number, offset: number }): Promise<Post[]> {
const limit = first || 10;
offset = offset || 0;
return await this.$get("rPosts", {limit, offset}) as Post[];
}
/**
* @deprecated
* use {@link postCount} instead
*/
public async numberOfPosts(): Promise<number> {
return this.postCount();
}
/**
* number of posts the user created
*/
public async postCount(): Promise<number> {
return this.$count("rPosts");
}
/**
* Groups the user is the admin of
*/
public async administratedGroups(): Promise<Group[]> {
return await this.$get("rAdministratedGroups") as Group[];
}
/**
* Groups the user has created
*/
public async createdGroups(): Promise<Group[]> {
return await this.$get("rCreatedGroups") as Group[];
}
/**
* Groups the user is a member of
* @param first
* @param offset
*/
public async groups({first, offset}: { first: number, offset: number }): Promise<Group[]> {
const limit = first || 10;
offset = offset || 0;
return await this.$get("rGroups", {limit, offset}) as Group[];
}
/**
* The number of groups the user has joined
*/
public async groupCount(): Promise<number> {
return this.$count("rGroups");
}
/**
* Events the user has joined
*/
public async events(): Promise<Event[]> {
return await this.$get("rEvents") as Event[];
}
/**
* The number of events the user is participating in.
*/
public async eventCount(): Promise<number> {
return this.$count("rEvents");
}
/**
* Denys a request the user has received
* @param sender
* @param type
*/
public async denyRequest(sender: number, type: RequestType) {
const request = await this.$get("rReceivedRequests",
{where: {senderId: sender, requestType: type}}) as Request[];
if (request[0]) {
await request[0].destroy();
}
}
/**
* Accepts a request the user has received
* @param sender
* @param type
*/
public async acceptRequest(sender: number, type: RequestType) {
const requests = await this.$get("rReceivedRequests",
{where: {senderId: sender, requestType: type}}) as Request[];
if (requests.length > 0) {
const request = requests[0];
if (request.requestType === RequestType.FRIENDREQUEST) {
await Friendship.bulkCreate([
{userId: this.id, friendId: sender},
{userId: sender, friendId: this.id},
], {ignoreDuplicates: true});
await request.destroy();
}
} else {
throw new RequestNotFoundError(sender, this.id, type);
}
}
/**
* Removes a user from the users friends
* @param friendId
*/
public async removeFriend(friendId: number) {
const friend = await User.findByPk(friendId);
if (friend) {
await this.$remove("rFriends", friend);
await this.$remove("rFriendOf", friend);
return true;
} else {
throw new UserNotFoundError(friendId);
}
}
}

@ -0,0 +1,13 @@
export {ChatMember} from "./ChatMember";
export {ChatMessage} from "./ChatMessage";
export {ChatRoom} from "./ChatRoom";
export {Friendship} from "./Friendship";
export {Post} from "./Post";
export {PostVote} from "./PostVote";
export {Request} from "./Request";
export {User} from "./User";
export {Group} from "./Group";
export {GroupAdmin} from "./GroupAdmin";
export {GroupMember} from "./GroupMember";
export {Event} from "./Event";
export {EventParticipant} from "./EventParticipant";

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

@ -1,5 +0,0 @@
@mixin gridPosition($rowStart, $rowEnd, $columnStart, $columnEnd)
grid-row-start: $rowStart
grid-row-end: $rowEnd
grid-column-start: $columnStart
grid-column-end: $columnEnd

@ -1,108 +1,10 @@
@import "vars" body
@import "mixins" font-family: Arial, serif
body #server-error
font-family: Arial, serif *
margin-left: auto
button margin-right: auto
border: 2px solid $cPrimary text-align: center
margin-top: 0.125em code
padding: 0.125em font-size: 2em
background-color: $cPrimary
color: $cPrimarySurface
font-weight: bold
transition-duration: 0.25s
button:hover
background-color: lighten($cPrimary, 10%)
cursor: pointer
button:active
background-color: darken($cPrimary, 5%)
box-shadow: inset 0.25em 0.25em 0.1em rgba(0, 0, 0, 0.25)
.stylebar
@include gridPosition(1, 2, 1, 4)
display: grid
grid-template: 100% /25% 50% 25%
background-color: $cPrimary
color: $cPrimarySurface
h1
@include gridPosition(1, 2, 1, 2)
text-align: center
margin: auto
#content
grid-template: 7.5% 92.5% / 25% 50% 25%
display: grid
width: 100%
height: 100%
#friendscontainer
@include gridPosition(2, 3, 1, 2)
background-color: $cPrimaryBackground
#input-login
margin-top: 1em
@include gridPosition(2,3,2,3)
grid-template: 7.5% 7.5% 7.5% 7.5% 72%/ 100%
display: grid
background-color: $cPrimaryBackground
input
margin: 0.25em
.loginButton
margin: 0.25em
#input-register
margin-top: 1em
@include gridPosition(2,3,2,3)
grid-template: 7.5% 7.5% 7.5% 7.5% 7.5% 7.5% 58%/ 100%
display: grid
background-color: $cPrimaryBackground
input
margin: 0.25em
.registerButton
margin: 0.25em
#feedcontainer
@include gridPosition(2, 3, 2, 3)
background-color: $cSecondaryBackground
.postinput
margin: 0.5em
input
width: 100%
border-radius: 0.25em
border: 1px solid $cPrimary
padding: 0.125em
height: 2em
button.submitbutton
border-radius: 0.25em
height: 2em
.feeditem
background-color: $cPrimaryBackground
min-height: 2em
margin: 0.5em
padding: 0.25em
border-radius: 0.25em
.itemhead
align-items: flex-start
.title, .handle, .date
margin: 0.125em
.title
font-weight: bold
.handle, .date
color: $cInactiveText
.handle a
text-decoration: none
color: $cInactiveText
font-style: normal
.handle a:hover
text-decoration: underline

@ -1,5 +0,0 @@
$cPrimaryBackground: #fff
$cSecondaryBackground: #ddd
$cInactiveText: #555
$cPrimary: #0d6b14
$cPrimarySurface: #fff

@ -1,101 +1,98 @@
import {Router} from "express"; import {Router} from "express";
import {Namespace, Server} from "socket.io"; import {Namespace, Server} from "socket.io";
import dataaccess from "../lib/dataaccess"; import dataaccess from "../lib/dataaccess";
import {ChatMessage} from "../lib/dataaccess/ChatMessage"; import globals from "../lib/globals";
import {Chatroom} from "../lib/dataaccess/Chatroom"; import {InternalEvents} from "../lib/InternalEvents";
import {Post} from "../lib/dataaccess/Post"; import {ChatMessage, ChatRoom, Post, Request, User} from "../lib/models";
import {Request} from "../lib/dataaccess/Request"; import Route from "../lib/Route";
import globals from "../lib/globals";
import {InternalEvents} from "../lib/InternalEvents"; /**
import Route from "../lib/Route"; * list of chatroom socket namespaces.
*/
/** const chatRooms: Namespace[] = [];
* list of chatroom socket namespaces.
*/ /**
const chatRooms: Namespace[] = []; * Class for the home route.
*/
/** class HomeRoute extends Route {
* Class for the home route. /**
*/ * Constructor, creates new router.
class HomeRoute extends Route { */
/** constructor() {
* Constructor, creates new router. super();
*/ this.router = Router();
constructor() { }
super();
this.router = Router(); /**
} * Asynchronous init for socket.io.
* @param io - the io instance
/** */
* Asynchronous init for socket.io. public async init(io: Server) {
* @param io - the io instance this.io = io;
*/
public async init(io: Server) { io.on("connection", (socket) => {
this.io = io; socket.on("postCreate", async (content) => {
if (socket.handshake.session.userId) {
io.on("connection", (socket) => { const post = await dataaccess.createPost(content, socket.handshake.session.userId);
socket.on("postCreate", async (content) => { io.emit("post", Object.assign(post, {htmlContent: post.htmlContent}));
if (socket.handshake.session.userId) { } else {
const post = await dataaccess.createPost(content, socket.handshake.session.userId); socket.emit("error", "Not logged in!");
io.emit("post", await post.resolvedData()); }
} else { });
socket.emit("error", "Not logged in!"); globals.internalEmitter.on(InternalEvents.REQUESTCREATE, async (request: Request) => {
} if ((await request.$get("sender") as User).id === socket.handshake.session.userId) {
}); socket.emit("request", request);
globals.internalEmitter.on(InternalEvents.REQUESTCREATE, (request: Request) => { }
if (request.receiver.id === socket.handshake.session.userId) { });
socket.emit("request", request.resolvedData()); globals.internalEmitter.on(InternalEvents.GQLPOSTCREATE, async (post: Post) => {
} socket.emit("post", Object.assign(post, {htmlContent: post.htmlContent}));
}); });
globals.internalEmitter.on(InternalEvents.GQLPOSTCREATE, async (post: Post) => { });
socket.emit("post", await post.resolvedData());
}); const chats = await dataaccess.getAllChats();
}); for (const chat of chats) {
chatRooms[chat.id] = this.getChatSocketNamespace(chat.id);
const chats = await dataaccess.getAllChats(); }
for (const chat of chats) { globals.internalEmitter.on(InternalEvents.CHATCREATE, (chat: ChatRoom) => {
chatRooms[chat.id] = this.getChatSocketNamespace(chat.id); chatRooms[chat.id] = this.getChatSocketNamespace(chat.id);
} });
globals.internalEmitter.on(InternalEvents.CHATCREATE, (chat: Chatroom) => { }
chatRooms[chat.id] = this.getChatSocketNamespace(chat.id);
}); /**
} * Destroys the instance by dereferencing the router and resolver.
*/
/** public async destroy(): Promise<void> {
* Destroys the instance by dereferencing the router and resolver. this.router = null;
*/ }
public async destroy(): Promise<void> {
this.router = null; /**
} * Returns the namespace socket for a chat socket.
* @param chatId
/** */
* Returns the namespace socket for a chat socket. private getChatSocketNamespace(chatId: number) {
* @param chatId if (chatRooms[chatId]) {
*/ return chatRooms[chatId];
private getChatSocketNamespace(chatId: number) { }
if (chatRooms[chatId]) { const chatNs = this.io.of(`/chat/${chatId}`);
return chatRooms[chatId]; chatNs.on("connection", (socket) => {
} socket.on("chatMessage", async (content) => {
const chatNs = this.io.of(`/chat/${chatId}`); if (socket.handshake.session.userId) {
chatNs.on("connection", (socket) => { const userId = socket.handshake.session.userId;
socket.on("chatMessage", async (content) => { const message = await dataaccess.sendChatMessage(userId, chatId, content);
if (socket.handshake.session.userId) { socket.broadcast.emit("chatMessage", Object.assign(message, {htmlContent: message.htmlContent}));
const userId = socket.handshake.session.userId; socket.emit("chatMessageSent", Object.assign(message, {htmlContent: message.htmlContent}));
const message = await dataaccess.sendChatMessage(userId, chatId, content); } else {
socket.broadcast.emit("chatMessage", message.resolvedContent()); socket.emit("error", "Not logged in!");
socket.emit("chatMessageSent", message.resolvedContent()); }
} else { });
socket.emit("error", "Not logged in!"); globals.internalEmitter.on(InternalEvents.GQLCHATMESSAGE, async (message: ChatMessage) => {
} if ((await message.$get("chat") as ChatRoom).id === chatId) {
}); socket.emit("chatMessage", Object.assign(message, {htmlContent: message.htmlContent}));
globals.internalEmitter.on(InternalEvents.GQLCHATMESSAGE, (message: ChatMessage) => { }
if (message.chat.id === chatId) { });
socket.emit("chatMessage", message.resolvedContent()); });
} return chatNs;
}); }
}); }
return chatNs;
} export default HomeRoute;
}
export default HomeRoute;

@ -1,34 +1,34 @@
/** /**
* @author Trivernis * @author Trivernis
* @remarks * @remarks
* *
* Taken from {@link https://github.com/Trivernis/whooshy} * Taken from {@link https://github.com/Trivernis/whooshy}
*/ */
import {Router} from "express"; import {Router} from "express";
import {Server} from "socket.io"; import {Server} from "socket.io";
import HomeRoute from "./home"; import HomeRoute from "./home";
const homeRoute = new HomeRoute(); const homeRoute = new HomeRoute();
/** /**
* Namespace to manage the routes of the server. * Namespace to manage the routes of the server.
* Allows easier assignments of graphql endpoints, socket.io connections and routers when * Allows easier assignments of graphql endpoints, socket.io connections and routers when
* used with {@link Route}. * used with {@link Route}.
*/ */
namespace routes { namespace routes {
export const router = Router(); export const router = Router();
router.use("/", homeRoute.router); router.use("/", homeRoute.router);
/** /**
* Assigns the io listeners or namespaces to the routes * Assigns the io listeners or namespaces to the routes
* @param io * @param io
*/ */
export const ioListeners = async (io: Server) => { export const ioListeners = async (io: Server) => {
await homeRoute.init(io); await homeRoute.init(io);
}; };
} }
export default routes; export default routes;

@ -1,137 +0,0 @@
--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;
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 functions relying on types
DO $$ BEGIN
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 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,
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,19 +0,0 @@
DO $$ BEGIN
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 $$;

@ -0,0 +1,12 @@
html
head
link(href="/stylesheets/style.css" rel="stylesheet" type="text/css")
body
div#server-error
div
h1 Page not found!
div
code 404
div
h1 The page "#{url}" was not found.

@ -0,0 +1,13 @@
html
head
link(href="/stylesheets/style.css" rel="stylesheet" type="text/css")
body
div#server-error
div
h1 Internal server error!
div
code 500
div
h2 Oops the server couldn't handle that.
div
p You might want to report this.

@ -1,21 +1,24 @@
{ {
"compileOnSave": true, "compileOnSave": true,
"compilerOptions": { "compilerOptions": {
"noImplicitAny": true, "noImplicitAny": true,
"removeComments": true, "removeComments": true,
"preserveConstEnums": true, "preserveConstEnums": true,
"outDir": "./dist", "allowSyntheticDefaultImports": true,
"sourceMap": true, "outDir": "./dist",
"target": "es2018", "sourceMap": true,
"allowJs": true, "target": "es2018",
"moduleResolution": "node", "allowJs": true,
"module": "commonjs" "moduleResolution": "node",
}, "module": "commonjs",
"include": [ "experimentalDecorators": true,
"src/**/*" "emitDecoratorMetadata": true
], },
"exclude": [ "include": [
"node_modules", "src/**/*"
"**/*.spec.ts" ],
] "exclude": [
} "node_modules",
"**/*.spec.ts"
]
}

@ -1,31 +1,32 @@
{ {
"extends": "tslint:recommended", "extends": "tslint:recommended",
"rulesDirectory": [], "rulesDirectory": [],
"rules": { "rules": {
"max-line-length": { "max-line-length": {
"options": [120] "options": [120]
}, },
"new-parens": true, "new-parens": true,
"no-arg": true, "no-arg": true,
"no-bitwise": true, "no-bitwise": true,
"no-conditional-assignment": true, "no-conditional-assignment": true,
"no-consecutive-blank-lines": false, "no-consecutive-blank-lines": false,
"cyclomatic-complexity": true, "cyclomatic-complexity": true,
"brace-style": "1tbs", "brace-style": "1tbs",
"semicolon": true, "semicolon": true,
"indent": [true, "spaces", 4], "indent": [true, "spaces", 4],
"no-shadowed-variable": true, "no-shadowed-variable": true,
"no-console": { "no-console": {
"severity": "warning", "severity": "warning",
"options": ["debug", "info", "log", "time", "timeEnd", "trace"] "options": ["debug", "info", "log", "time", "timeEnd", "trace"]
}, },
"no-namespace": false, "no-namespace": false,
"no-internal-module": false, "no-internal-module": false,
"max-classes-per-file": false "max-classes-per-file": false,
}, "no-var-requires": false
"jsRules": { },
"max-line-length": { "jsRules": {
"options": [120] "max-line-length": {
} "options": [120]
} }
} }
}

Loading…
Cancel
Save