Merge remote-tracking branch 'origin/master'

pull/5/head
Trivernis 5 years ago
commit 88b9937387

40
Jenkinsfile vendored

@ -0,0 +1,40 @@
pipeline {
agent any
stages {
stage('Dependencies') {
steps {
echo 'Installing Dependencies...'
nodejs(nodeJSInstallationName: 'Node 12.x') {
sh 'yarn install'
}
}
}
stage('Stylecheck') {
steps {
echo 'Checking Style...'
nodejs(nodeJSInstallationName: 'Node 12.x') {
sh 'tslint "src/**/*.ts"'
}
}
}
stage('Build') {
steps {
echo 'Building...'
nodejs(nodeJSInstallationName: 'Node 12.x') {
sh 'gulp'
}
sh '/bin/tar -zcvf greenvironment-server.tar.gz dist'
archiveArtifacts artifacts: 'greenvironment-server.tar.gz', fingerprint: true
}
}
stage('Test') {
steps {
echo 'Testing...'
nodejs(nodeJSInstallationName: 'Node 12.x') {
sh 'yarn test'
}
}
}
}
}

@ -9,3 +9,4 @@ 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".
Additionally the server needs a working redis server to connect to.

7954
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -4,7 +4,7 @@
"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": "mocha dist/tests/**/*Test.js",
"build": "gulp", "build": "gulp",
"start": "node ./dist/index.js" "start": "node ./dist/index.js"
}, },
@ -21,6 +21,7 @@
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@types/bluebird": "^3.5.27", "@types/bluebird": "^3.5.27",
"@types/chai": "^4.2.7",
"@types/compression": "^1.0.1", "@types/compression": "^1.0.1",
"@types/connect-pg-simple": "^4.2.0", "@types/connect-pg-simple": "^4.2.0",
"@types/cookie-parser": "^1.4.2", "@types/cookie-parser": "^1.4.2",
@ -30,28 +31,30 @@
"@types/express-session": "^1.15.14", "@types/express-session": "^1.15.14",
"@types/express-socket.io-session": "^1.3.2", "@types/express-socket.io-session": "^1.3.2",
"@types/fs-extra": "^8.0.0", "@types/fs-extra": "^8.0.0",
"@types/graphql": "^14.2.3",
"@types/http-status": "^0.2.30", "@types/http-status": "^0.2.30",
"@types/js-yaml": "^3.12.1", "@types/js-yaml": "^3.12.1",
"@types/markdown-it": "0.0.9", "@types/markdown-it": "0.0.9",
"@types/mocha": "^5.2.7",
"@types/node": "^12.7.12", "@types/node": "^12.7.12",
"@types/pg": "^7.11.0", "@types/pg": "^7.11.0",
"@types/sequelize": "^4.28.5", "@types/sequelize": "^4.28.5",
"@types/socket.io": "^2.1.2", "@types/socket.io": "^2.1.2",
"@types/socket.io-redis": "^1.0.25",
"@types/validator": "^10.11.3", "@types/validator": "^10.11.3",
"@types/winston": "^2.4.4", "chai": "^4.2.0",
"delete": "^1.1.0", "delete": "^1.1.0",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-minify": "^3.1.0", "gulp-minify": "^3.1.0",
"gulp-sass": "^4.0.2", "gulp-sass": "^4.0.2",
"gulp-typescript": "^5.0.1", "gulp-typescript": "^5.0.1",
"mocha": "^6.2.2",
"ts-lint": "^4.5.1", "ts-lint": "^4.5.1",
"tsc": "^1.20150623.0", "tsc": "^1.20150623.0",
"tslint": "^5.19.0", "tslint": "^5.19.0",
"typescript": "^3.5.3" "typescript": "^3.7.2"
}, },
"dependencies": { "dependencies": {
"@types/socket.io-redis": "^1.0.25", "@types/uuid": "^3.4.6",
"compression": "^1.7.4", "compression": "^1.7.4",
"connect-session-sequelize": "^6.0.0", "connect-session-sequelize": "^6.0.0",
"cookie-parser": "^1.4.4", "cookie-parser": "^1.4.4",
@ -75,6 +78,7 @@
"socket.io": "^2.2.0", "socket.io": "^2.2.0",
"socket.io-redis": "^5.2.0", "socket.io-redis": "^5.2.0",
"sqlite3": "^4.1.0", "sqlite3": "^4.1.0",
"uuid": "^3.3.3",
"winston": "^3.2.1", "winston": "^3.2.1",
"winston-daily-rotate-file": "^4.2.1" "winston-daily-rotate-file": "^4.2.1"
} }

@ -16,7 +16,7 @@ import {Sequelize} from "sequelize-typescript";
import * as socketIo from "socket.io"; import * as socketIo from "socket.io";
import * as socketIoRedis from "socket.io-redis"; import * as socketIoRedis from "socket.io-redis";
import {resolver} from "./graphql/resolvers"; import {resolver} from "./graphql/resolvers";
import dataaccess from "./lib/dataaccess"; import dataaccess from "./lib/dataAccess";
import globals from "./lib/globals"; import globals from "./lib/globals";
import routes from "./routes"; import routes from "./routes";
@ -41,12 +41,13 @@ class App {
/** /**
* initializes everything that needs to be initialized asynchronous. * initializes everything that needs to be initialized asynchronous.
*/ */
public async init() { public async init(): Promise<void> {
await dataaccess.init(this.sequelize); await dataaccess.init(this.sequelize);
const appSession = session({ const appSession = session({
cookie: { cookie: {
maxAge: Number(globals.config.session.cookieMaxAge) || 604800000, maxAge: Number(globals.config.session.cookieMaxAge) || 604800000,
// @ts-ignore
secure: "auto", secure: "auto",
}, },
resave: false, resave: false,
@ -56,12 +57,15 @@ class App {
}); });
const force = fsx.existsSync("sqz-force"); const force = fsx.existsSync("sqz-force");
logger.info(`Sequelize Table force: ${force}`); logger.info(`Syncinc database. Sequelize Table force: ${force}.`);
await this.sequelize.sync({force, logging: (msg) => logger.silly(msg)}); await this.sequelize.sync({force, logging: (msg) => logger.silly(msg)});
this.sequelize.options.logging = (msg) => logger.silly(msg);
logger.info("Setting up socket.io");
await routes.ioListeners(this.io); await routes.ioListeners(this.io);
this.io.adapter(socketIoRedis()); this.io.adapter(socketIoRedis());
this.io.use(sharedsession(appSession, {autoSave: true})); this.io.use(sharedsession(appSession, {autoSave: true}));
logger.info("Configuring express app.");
this.app.set("views", path.join(__dirname, "views")); this.app.set("views", path.join(__dirname, "views"));
this.app.set("view engine", "pug"); this.app.set("view engine", "pug");
this.app.set("trust proxy", 1); this.app.set("trust proxy", 1);
@ -73,11 +77,24 @@ class App {
this.app.use(cookieParser()); this.app.use(cookieParser());
this.app.use(appSession); this.app.use(appSession);
// enable cross origin requests if enabled in the config // enable cross origin requests if enabled in the config
if (globals.config.server.cors) { if (globals.config.server?.cors) {
this.app.use(cors()); this.app.use(cors());
} }
// handle authentification via bearer in the Authorization header
this.app.use(async (req, res, next) => {
if (!req.session.userId && req.headers.authorization) {
const bearer = req.headers.authorization.split("Bearer ")[1];
if (bearer) {
const user = await dataaccess.getUserByToken(bearer);
// @ts-ignore
req.session.userId = user.id;
}
}
next();
});
this.app.use((req, res, next) => { this.app.use((req, res, next) => {
logger.verbose(`${req.method} ${req.url}`); logger.verbose(`${req.method} ${req.url}`);
process.send({cmd: "notifyRequest"});
next(); next();
}); });
this.app.use(routes.router); this.app.use(routes.router);
@ -117,13 +134,14 @@ class App {
res.status(httpStatus.INTERNAL_SERVER_ERROR); res.status(httpStatus.INTERNAL_SERVER_ERROR);
res.render("errors/500.pug"); res.render("errors/500.pug");
}); });
logger.info("Server configured.");
} }
/** /**
* Starts the web server. * Starts the web server.
*/ */
public start() { public start(): void {
if (globals.config.server.port) { if (globals.config.server?.port) {
logger.info(`Starting server...`); logger.info(`Starting server...`);
this.app.listen(globals.config.server.port); this.app.listen(globals.config.server.port);
logger.info(`Server running on port ${globals.config.server.port}`); logger.info(`Server running on port ${globals.config.server.port}`);

@ -1,6 +1,7 @@
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 * as yaml from "js-yaml";
import dataaccess from "../lib/dataAccess";
import {NotLoggedInGqlError, PostNotFoundGqlError} from "../lib/errors/graphqlErrors"; import {NotLoggedInGqlError, PostNotFoundGqlError} from "../lib/errors/graphqlErrors";
import globals from "../lib/globals"; import globals from "../lib/globals";
import {InternalEvents} from "../lib/InternalEvents"; import {InternalEvents} from "../lib/InternalEvents";
@ -100,6 +101,23 @@ export function resolver(req: any, res: any): any {
return new NotLoggedInGqlError(); return new NotLoggedInGqlError();
} }
}, },
async getToken({email, passwordHash}: {email: string, passwordHash: string}) {
if (email && passwordHash) {
try {
const user = await dataaccess.getUserByLogin(email, passwordHash);
return {
expires: Number(user.authExpire),
value: user.token(),
};
} catch (err) {
res.status(400);
return err.graphqlError;
}
} else {
res.status(400);
return new GraphQLError("No email or password specified.");
}
},
async register({username, email, passwordHash}: { username: string, email: string, passwordHash: string }) { async register({username, email, passwordHash}: { username: string, email: string, passwordHash: string }) {
if (username && email && passwordHash) { if (username && email && passwordHash) {
if (!is.email(email)) { if (!is.email(email)) {
@ -121,6 +139,22 @@ export function resolver(req: any, res: any): any {
return new GraphQLError("No username, email or password given."); return new GraphQLError("No username, email or password given.");
} }
}, },
async setUserSettings({settings}: {settings: string}) {
if (req.session.userId) {
const user = await models.User.findByPk(req.session.userId);
try {
user.frontendSettings = yaml.safeLoad(settings);
await user.save();
return user.settings;
} catch (err) {
res.status(400);
return new GraphQLError("Invalid settings json.");
}
} else {
res.status(status.UNAUTHORIZED);
return new NotLoggedInGqlError();
}
},
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) {

@ -25,6 +25,9 @@ type Query {
"returns the post filtered by the sort type with pagination." "returns the post filtered by the sort type with pagination."
getPosts(first: Int=20, offset: Int=0, sort: SortType = NEW): [Post] getPosts(first: Int=20, offset: Int=0, sort: SortType = NEW): [Post]
"Returns an access token for the user that can be used in requests. To user the token in requests, it has to be set in the HTTP header 'Authorization' with the format Bearer <token>."
getToken(email: String!, passwordHash: String!): Token!
} }
type Mutation { type Mutation {
@ -32,11 +35,14 @@ type Mutation {
acceptCookies: Boolean acceptCookies: Boolean
"Login of the user. The passwordHash should be a sha512 hash of the password." "Login of the user. The passwordHash should be a sha512 hash of the password."
login(email: String, passwordHash: String): Profile login(email: String!, passwordHash: String!): Profile
"Registers the user." "Registers the user."
register(username: String, email: String, passwordHash: String): Profile register(username: String, email: String, passwordHash: String): Profile
"Sets the user settings to the specified settings string. The settings parameter should be a valid yaml."
setUserSettings(settings: String!): String!
"Logout of the user." "Logout of the user."
logout: Boolean logout: Boolean
@ -247,6 +253,9 @@ type Profile implements UserData {
"the levels of the user depending on the points" "the levels of the user depending on the points"
level: Int! level: Int!
"the custom settings for the frontend"
settings: String!
} }
"represents a single user post" "represents a single user post"
@ -274,7 +283,7 @@ type Post {
createdAt: String! createdAt: String!
"the type of vote the user performed on the post" "the type of vote the user performed on the post"
userVote: VoteType userVote(userId: ID!): VoteType
} }
"represents a request of any type" "represents a request of any type"
@ -368,6 +377,12 @@ type Event {
participants(first: Int=10, offset: Int=0): [User!]! participants(first: Int=10, offset: Int=0): [User!]!
} }
"respresents an access token entry with the value as the acutal token and expires as the date the token expires."
type Token {
value: String!
expires: String!
}
"represents the type of vote performed on a post" "represents the type of vote performed on a post"
enum VoteType { enum VoteType {
UPVOTE UPVOTE

@ -3,21 +3,91 @@ import * as cluster from "cluster";
import App from "./app"; import App from "./app";
const numCPUs = require("os").cpus().length; const numCPUs = require("os").cpus().length;
interface IResourceUsage {
mem: {rss: number, heapTotal: number, heapUsed: number, external: number};
cpu: {user: number, system: number};
}
interface IClusterData {
reqCount: number;
workerCount: () => number;
workerRes: {[key: string]: IResourceUsage};
}
if (cluster.isMaster) { if (cluster.isMaster) {
console.log(`[CLUSTER] Master ${process.pid} is running`); console.log(`[CLUSTER-M] Master ${process.pid} is running`);
const clusterData: IClusterData = {
reqCount: 0,
workerCount: () => Object.keys(cluster.workers).length,
// @ts-ignore
workerRes: {},
};
setInterval(() => {
clusterData.workerRes.M = {
cpu: process.cpuUsage(),
mem: process.memoryUsage(),
};
}, 1000);
const log = (msg: string) => {
process.stdout.write(" ".padEnd(100) + "\r");
process.stdout.write(msg);
process.stdout.write(
`W: ${clusterData.workerCount()},R: ${clusterData.reqCount},M: ${(() => {
let usageString = "";
for (const [key, value] of Object.entries(clusterData.workerRes)) {
usageString += `${
Math.round((value as IResourceUsage).mem.heapUsed / 100000) / 10}MB,`.padEnd(8);
}
return usageString;
})()}`.padEnd(99) + "\r");
};
cluster.settings.silent = true;
cluster.on("exit", (worker, code, signal) => {
log(`[CLUSTER-M] Worker ${worker.process.pid} died!\n`);
delete clusterData.workerRes[worker.id];
log("[CLUSTER-M] Starting new worker\n");
cluster.fork();
});
cluster.on("online", (worker) => {
worker.process.stdout.on("data", (data) => {
log(`[CLUSTER-${worker.id}] ${data}`);
});
});
cluster.on("message", (worker, message) => {
switch (message.cmd) {
case "notifyRequest":
clusterData.reqCount++;
log("");
break;
case "notifyResources":
// @ts-ignore
clusterData.workerRes[worker.id] = message.data;
log("");
break;
default:
break;
}
});
for (let i = 0; i < numCPUs; i++) { for (let i = 0; i < numCPUs; i++) {
cluster.fork(); cluster.fork();
} }
cluster.on("exit", (worker, code, signal) => {
console.log(`[CLUSTER] Worker ${worker.process.pid} died!`);
});
} else { } else {
/** /**
* async main function wrapper. * async main function wrapper.
*/ */
(async () => { (async () => {
const app = new App(process.pid); setInterval(() => {
process.send({cmd: "notifyResources", data: {
cpu: process.cpuUsage(),
mem: process.memoryUsage(),
}});
}, 1000);
const app = new App(cluster.worker.id);
await app.init(); await app.init();
app.start(); app.start();
})(); })();

@ -35,7 +35,7 @@ namespace dataaccess {
/** /**
* Initializes everything that needs to be initialized asynchronous. * Initializes everything that needs to be initialized asynchronous.
*/ */
export async function init(seq: Sequelize) { export async function init(seq: Sequelize): Promise<void> {
sequelize = seq; sequelize = seq;
try { try {
await sequelize.addModels([ await sequelize.addModels([
@ -93,6 +93,14 @@ namespace dataaccess {
} }
} }
/**
* Returns the user by auth token.
* @param token
*/
export async function getUserByToken(token: string): Promise<models.User> {
return models.User.findOne({where: {authToken: token}});
}
/** /**
* Registers a user with a username and password returning a user * Registers a user with a username and password returning a user
* @param username * @param username
@ -131,7 +139,7 @@ namespace dataaccess {
* @param offset * @param offset
* @param sort * @param sort
*/ */
export async function getPosts(first: number, offset: number, sort: SortType) { export async function getPosts(first: number, offset: number, sort: SortType): Promise<models.Post[]> {
if (sort === SortType.NEW) { if (sort === SortType.NEW) {
return models.Post.findAll({ return models.Post.findAll({
include: [{association: "rVotes"}], include: [{association: "rVotes"}],
@ -140,6 +148,7 @@ namespace dataaccess {
order: [["createdAt", "DESC"]], order: [["createdAt", "DESC"]],
}); });
} else { } else {
// more performant way to get the votes with plain sql
return await sequelize.query( return await sequelize.query(
`SELECT * FROM ( `SELECT * FROM (
SELECT *, SELECT *,

@ -1,4 +1,4 @@
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 {

@ -1,12 +1,13 @@
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 path from "path";
import * as winston from "winston"; import * as winston from "winston";
require("winston-daily-rotate-file"); require("winston-daily-rotate-file");
const configPath = "config.yaml"; const configPath = "config.yaml";
const defaultConfig = __dirname + "/../default-config.yaml"; const defaultConfig = path.join(__dirname, "/../default-config.yaml");
// ensure that the config exists by copying the default config. // ensure that the config exists by copying the default config.
if (!(fsx.pathExistsSync(configPath))) { if (!(fsx.pathExistsSync(configPath))) {
@ -21,9 +22,9 @@ if (!(fsx.pathExistsSync(configPath))) {
* Defines global variables to be used. * Defines global variables to be used.
*/ */
namespace globals { namespace globals {
export const config = yaml.safeLoad(fsx.readFileSync("config.yaml", "utf-8")); export const config: IConfig = yaml.safeLoad(fsx.readFileSync("config.yaml", "utf-8"));
// @ts-ignore // @ts-ignore
export const logger = winston.createLogger({ export const logger: winston.Logger = winston.createLogger({
transports: [ transports: [
new winston.transports.Console({ new winston.transports.Console({
format: winston.format.combine( format: winston.format.combine(
@ -33,7 +34,7 @@ namespace globals {
return `${timestamp} ${level}: ${message}`; return `${timestamp} ${level}: ${message}`;
}), }),
), ),
level: config.logging.level, level: config.logging?.level ?? "info",
}), }),
// @ts-ignore // @ts-ignore
new (winston.transports.DailyRotateFile)({ new (winston.transports.DailyRotateFile)({
@ -46,13 +47,13 @@ namespace globals {
}), }),
), ),
json: false, json: false,
level: config.logging.level, level: config.logging?.level ?? "info",
maxFiles: "7d", maxFiles: "7d",
zippedArchive: true, zippedArchive: true,
}), }),
], ],
}); });
export const internalEmitter = new EventEmitter(); export const internalEmitter: EventEmitter = new EventEmitter();
} }
export default globals; export default globals;

@ -0,0 +1,67 @@
/**
* An interface for the configuration file
*/
interface IConfig {
/**
* Database connection info
*/
database: {
/**
* A connection uri for the database. <type>://<user>:<password>@<ip/domain>/<database>
*/
connectionUri: string;
};
/**
* Configuration for the http server
*/
server?: {
/**
* The port to listen on
*/
port?: number;
/**
* If cross origin requests should be enabled
*/
cors?: false;
};
/**
* The session configuration
*/
session: {
/**
* A secure secret to be used for sessions
*/
secret: string;
/**
* The maximum cookie age before the session gets deleted
*/
cookieMaxAge: number;
};
/**
* Configuration for markdown parsing
*/
markdown?: {
/**
* The plugins to use for parsing
*/
plugins: string[];
};
/**
* Logging configuration
*/
logging?: {
/**
* The loglevel that is used for the console and logfiles
*/
level?: ("silly" | "debug" | "verbose" | "info" | "warn" | "error");
};
/**
* The frontend configuration
*/
frontend?: {
/**
* Points to the index.html which is loaded as a fallback for angular to work
*/
angularIndex?: string;
};
}

@ -20,7 +20,7 @@ namespace markdown {
* 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): string {
return md.renderInline(markdownString); return md.renderInline(markdownString);
} }
@ -28,7 +28,7 @@ namespace markdown {
* Renders the markdown string. * Renders the markdown string.
* @param markdownString * @param markdownString
*/ */
export function render(markdownString: string) { export function render(markdownString: string): string {
return md.render(markdownString); return md.render(markdownString);
} }
} }

@ -1,4 +1,4 @@
import {Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript"; import {Column, ForeignKey, Model, NotNull, Table} from "sequelize-typescript";
import {ChatRoom} from "./ChatRoom"; import {ChatRoom} from "./ChatRoom";
import {User} from "./User"; import {User} from "./User";

@ -1,5 +1,5 @@
import * as sqz from "sequelize"; import * as sqz from "sequelize";
import {BelongsTo, Column, CreatedAt, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript"; import {BelongsTo, Column, CreatedAt, ForeignKey, Model, NotNull, Table} from "sequelize-typescript";
import markdown from "../markdown"; import markdown from "../markdown";
import {ChatRoom} from "./ChatRoom"; import {ChatRoom} from "./ChatRoom";
import {User} from "./User"; import {User} from "./User";

@ -1,4 +1,4 @@
import {BelongsToMany, CreatedAt, HasMany, Model, Table,} from "sequelize-typescript"; import {BelongsToMany, CreatedAt, HasMany, Model, Table} from "sequelize-typescript";
import {ChatMember} from "./ChatMember"; import {ChatMember} from "./ChatMember";
import {ChatMessage} from "./ChatMessage"; import {ChatMessage} from "./ChatMessage";
import {User} from "./User"; import {User} from "./User";

@ -29,8 +29,8 @@ export class Event extends Model<Event> {
} }
public async participants({first, offset}: {first: number, offset: number}): Promise<User[]> { public async participants({first, offset}: {first: number, offset: number}): Promise<User[]> {
const limit = first || 10; const limit = first ?? 10;
offset = offset || 0; offset = offset ?? 0;
return await this.$get("rParticipants", {limit, offset}) as User[]; return await this.$get("rParticipants", {limit, offset}) as User[];
} }
} }

@ -41,14 +41,14 @@ export class Group extends Model<Group> {
} }
public async admins({first, offset}: { first: number, offset: number }): Promise<User[]> { public async admins({first, offset}: { first: number, offset: number }): Promise<User[]> {
const limit = first || 10; const limit = first ?? 10;
offset = offset || 0; offset = offset ?? 0;
return await this.$get("rAdmins", {limit, offset}) as User[]; return await this.$get("rAdmins", {limit, offset}) as User[];
} }
public async members({first, offset}: { first: number, offset: number }): Promise<User[]> { public async members({first, offset}: { first: number, offset: number }): Promise<User[]> {
const limit = first || 10; const limit = first ?? 10;
offset = offset || 0; offset = offset ?? 0;
return await this.$get("rMembers", {limit, offset}) as User[]; return await this.$get("rMembers", {limit, offset}) as User[];
} }
@ -57,8 +57,8 @@ export class Group extends Model<Group> {
} }
public async events({first, offset}: { first: number, offset: number }): Promise<Event[]> { public async events({first, offset}: { first: number, offset: number }): Promise<Event[]> {
const limit = first || 10; const limit = first ?? 10;
offset = offset || 0; offset = offset ?? 0;
return await this.$get("rEvents", {limit, offset}) as Event[]; return await this.$get("rEvents", {limit, offset}) as Event[];
} }
} }

@ -1,4 +1,4 @@
import {Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript"; import {Column, ForeignKey, Model, NotNull, Table} from "sequelize-typescript";
import {Group} from "./Group"; import {Group} from "./Group";
import {User} from "./User"; import {User} from "./User";

@ -1,4 +1,4 @@
import {Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript"; import {Column, ForeignKey, Model, NotNull, Table} from "sequelize-typescript";
import {Group} from "./Group"; import {Group} from "./Group";
import {User} from "./User"; import {User} from "./User";

@ -1,5 +1,5 @@
import * as sqz from "sequelize"; import * as sqz from "sequelize";
import {BelongsTo, BelongsToMany, Column, CreatedAt, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript"; import {BelongsTo, BelongsToMany, Column, CreatedAt, ForeignKey, Model, NotNull, Table} from "sequelize-typescript";
import markdown from "../markdown"; import markdown from "../markdown";
import {PostVote, VoteType} from "./PostVote"; import {PostVote, VoteType} from "./PostVote";
import {User} from "./User"; import {User} from "./User";
@ -24,35 +24,55 @@ export class Post extends Model<Post> {
@CreatedAt @CreatedAt
public readonly createdAt!: Date; public readonly createdAt!: Date;
/**
* Returns the author of a post
*/
public async author(): Promise<User> { public async author(): Promise<User> {
return await this.$get("rAuthor") as User; return await this.$get("rAuthor") as User;
} }
/**
* Returns the votes on a post
*/
public async votes(): Promise<Array<User & {PostVote: PostVote}>> { public async votes(): Promise<Array<User & {PostVote: PostVote}>> {
return await this.$get("rVotes") as Array<User & {PostVote: PostVote}>; return await this.$get("rVotes") as Array<User & {PostVote: PostVote}>;
} }
/**
* Returns the markdown-rendered html content of the post
*/
public get htmlContent() { public get htmlContent() {
return markdown.render(this.getDataValue("content")); return markdown.render(this.getDataValue("content"));
} }
/**
* Returns the number of upvotes on the post
*/
public async upvotes() { public async upvotes() {
return (await this.votes()).filter((v) => v.PostVote.voteType === VoteType.UPVOTE).length; return (await this.votes()).filter((v) => v.PostVote.voteType === VoteType.UPVOTE).length;
} }
/**
* Returns the number of downvotes on the post
*/
public async downvotes() { public async downvotes() {
return (await this.votes()).filter((v) => v.PostVote.voteType === VoteType.DOWNVOTE).length; return (await this.votes()).filter((v) => v.PostVote.voteType === VoteType.DOWNVOTE).length;
} }
/**
* Toggles the vote of the user.
* @param userId
* @param type
*/
public async vote(userId: number, type: VoteType): Promise<VoteType> { public async vote(userId: number, type: VoteType): Promise<VoteType> {
type = type || VoteType.UPVOTE; type = type ?? VoteType.UPVOTE;
let votes = await this.$get("rVotes", {where: {id: userId}}) as Array<User & {PostVote: PostVote}>; let votes = await this.$get("rVotes", {where: {id: userId}}) as Array<User & {PostVote: PostVote}>;
let vote = votes[0] || null; let vote = votes[0] ?? null;
let created = false; let created = false;
if (!vote) { if (!vote) {
await this.$add("rVote", userId); await this.$add("rVote", userId);
votes = await this.$get("rVotes", {where: {id: userId}}) as Array<User & {PostVote: PostVote}>; votes = await this.$get("rVotes", {where: {id: userId}}) as Array<User & {PostVote: PostVote}>;
vote = votes[0] || null; vote = votes[0] ?? null;
created = true; created = true;
} }
if (vote) { if (vote) {
@ -67,4 +87,13 @@ export class Post extends Model<Post> {
return vote.PostVote.voteType; return vote.PostVote.voteType;
} }
/**
* Returns the type of vote that was performend on the post by the user specified by the user id.
* @param userId
*/
public async userVote({userId}: {userId: number}): Promise<VoteType> {
const votes = await this.$get("rVotes", {where: {id: userId}}) as Array<User & {PostVote: PostVote}>;
return votes[0]?.PostVote?.voteType;
}
} }

@ -1,5 +1,5 @@
import * as sqz from "sequelize"; import * as sqz from "sequelize";
import {Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript"; import {Column, ForeignKey, Model, NotNull, Table} from "sequelize-typescript";
import {Post} from "./Post"; import {Post} from "./Post";
import {User} from "./User"; import {User} from "./User";

@ -1,5 +1,5 @@
import * as sqz from "sequelize"; import * as sqz from "sequelize";
import {BelongsTo, Column, ForeignKey, Model, NotNull, Table,} from "sequelize-typescript"; import {BelongsTo, Column, ForeignKey, Model, NotNull, Table} from "sequelize-typescript";
import {User} from "./User"; import {User} from "./User";
export enum RequestType { export enum RequestType {

@ -10,6 +10,7 @@ import {
Unique, Unique,
UpdatedAt, UpdatedAt,
} from "sequelize-typescript"; } from "sequelize-typescript";
import * as uuidv4 from "uuid/v4";
import {RequestNotFoundError} from "../errors/RequestNotFoundError"; import {RequestNotFoundError} from "../errors/RequestNotFoundError";
import {UserNotFoundError} from "../errors/UserNotFoundError"; import {UserNotFoundError} from "../errors/UserNotFoundError";
import {ChatMember} from "./ChatMember"; import {ChatMember} from "./ChatMember";
@ -49,6 +50,17 @@ export class User extends Model<User> {
@Column({defaultValue: 0, allowNull: false}) @Column({defaultValue: 0, allowNull: false})
public rankpoints: number; public rankpoints: number;
@NotNull
@Column({defaultValue: {}, allowNull: false, type: sqz.JSON})
public frontendSettings: any;
@Unique
@Column({defaultValue: uuidv4, unique: true})
public authToken: string;
@Column({defaultValue: () => Date.now() + 7200000})
public authExpire: Date;
@BelongsToMany(() => User, () => Friendship, "userId") @BelongsToMany(() => User, () => Friendship, "userId")
public rFriends: User[]; public rFriends: User[];
@ -119,14 +131,33 @@ export class User extends Model<User> {
return Math.ceil(this.rankpoints / 100); return Math.ceil(this.rankpoints / 100);
} }
/**
* returns the settings of the user as a jston string
*/
public get settings(): string {
return JSON.stringify(this.getDataValue("frontendSettings"));
}
/**
* Returns the token for the user that can be used as a bearer in requests
*/
public async token(): Promise<string> {
if (this.getDataValue("authExpire") < new Date(Date.now())) {
this.authToken = null;
this.authExpire = null;
await this.save();
}
return this.getDataValue("authToken");
}
/** /**
* All friends of the user * All friends of the user
* @param first * @param first
* @param offset * @param offset
*/ */
public async friends({first, offset}: { first: number, offset: number }): Promise<User[]> { public async friends({first, offset}: { first: number, offset: number }): Promise<User[]> {
const limit = first || 10; const limit = first ?? 10;
offset = offset || 0; offset = offset ?? 0;
return await this.$get("rFriendOf", {limit, offset}) as User[]; return await this.$get("rFriendOf", {limit, offset}) as User[];
} }
@ -143,8 +174,8 @@ export class User extends Model<User> {
* @param offset * @param offset
*/ */
public async chats({first, offset}: { first: number, offset: number }): Promise<ChatRoom[]> { public async chats({first, offset}: { first: number, offset: number }): Promise<ChatRoom[]> {
const limit = first || 10; const limit = first ?? 10;
offset = offset || 0; offset = offset ?? 0;
return await this.$get("rChats", {limit, offset}) as ChatRoom[]; return await this.$get("rChats", {limit, offset}) as ChatRoom[];
} }
@ -170,8 +201,8 @@ export class User extends Model<User> {
} }
public async posts({first, offset}: { first: number, offset: number }): Promise<Post[]> { public async posts({first, offset}: { first: number, offset: number }): Promise<Post[]> {
const limit = first || 10; const limit = first ?? 10;
offset = offset || 0; offset = offset ?? 0;
return await this.$get("rPosts", {limit, offset}) as Post[]; return await this.$get("rPosts", {limit, offset}) as Post[];
} }
@ -210,8 +241,8 @@ export class User extends Model<User> {
* @param offset * @param offset
*/ */
public async groups({first, offset}: { first: number, offset: number }): Promise<Group[]> { public async groups({first, offset}: { first: number, offset: number }): Promise<Group[]> {
const limit = first || 10; const limit = first ?? 10;
offset = offset || 0; offset = offset ?? 0;
return await this.$get("rGroups", {limit, offset}) as Group[]; return await this.$get("rGroups", {limit, offset}) as Group[];
} }

@ -1,6 +1,6 @@
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 globals from "../lib/globals"; import globals from "../lib/globals";
import {InternalEvents} from "../lib/InternalEvents"; import {InternalEvents} from "../lib/InternalEvents";
import {ChatMessage, ChatRoom, Post, Request, User} from "../lib/models"; import {ChatMessage, ChatRoom, Post, Request, User} from "../lib/models";

@ -0,0 +1,26 @@
import {expect} from "chai";
import {describe, it} from "mocha";
import markdown from "../../lib/markdown";
describe("markdown", () => {
describe("renderInline", () => {
it("renders markdown inline expressions", () => {
const result = markdown.renderInline("**Hello**");
expect(result).to.equal("<strong>Hello</strong>");
});
it("renders markdown emoji", () => {
const result = markdown.renderInline(":smile:");
expect(result).to.equal("😄");
});
});
describe("render", () => {
it("renders markdown block expressions", () => {
const result = markdown.render("#header\n```\n```");
expect(result).to.equal("<p>#header</p>\n<pre><code></code></pre>\n");
});
it("renders markdown emoji", () => {
const result = markdown.render(":smile:");
expect(result).to.equal("<p>😄</p>\n");
});
});
});

@ -0,0 +1,20 @@
import {expect} from "chai";
import {describe, it} from "mocha";
import {is} from "../../lib/regex";
describe("regex", () => {
describe("email", () => {
it("identifies right emails", () => {
const result = is.email("trivernis@mail.com");
expect(result).to.equal(true);
});
it("identifies non-email urls", () => {
const result = is.email("notanemail.com");
expect(result).to.equal(false);
});
it("identifies malformed emails", () => {
const result = is.email("trivernis@mail.");
expect(result).to.equal(false);
});
});
});

@ -2,13 +2,16 @@
"compileOnSave": true, "compileOnSave": true,
"compilerOptions": { "compilerOptions": {
"noImplicitAny": true, "noImplicitAny": true,
"noImplicitThis": true,
"removeComments": true, "removeComments": true,
"preserveConstEnums": true, "preserveConstEnums": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"outDir": "./dist", "outDir": "./dist",
"sourceMap": true, "sourceMap": true,
"target": "es2018", "target": "es2018",
"allowJs": true, "allowJs": false,
"forceConsistentCasingInFileNames": true,
"strictFunctionTypes": true,
"moduleResolution": "node", "moduleResolution": "node",
"module": "commonjs", "module": "commonjs",
"experimentalDecorators": true, "experimentalDecorators": true,

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save