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
executing "gulp" in the terminal. To run the server you need
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",
"main": "./dist/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"test": "mocha dist/tests/**/*Test.js",
"build": "gulp",
"start": "node ./dist/index.js"
},
@ -21,6 +21,7 @@
"license": "ISC",
"devDependencies": {
"@types/bluebird": "^3.5.27",
"@types/chai": "^4.2.7",
"@types/compression": "^1.0.1",
"@types/connect-pg-simple": "^4.2.0",
"@types/cookie-parser": "^1.4.2",
@ -30,28 +31,30 @@
"@types/express-session": "^1.15.14",
"@types/express-socket.io-session": "^1.3.2",
"@types/fs-extra": "^8.0.0",
"@types/graphql": "^14.2.3",
"@types/http-status": "^0.2.30",
"@types/js-yaml": "^3.12.1",
"@types/markdown-it": "0.0.9",
"@types/mocha": "^5.2.7",
"@types/node": "^12.7.12",
"@types/pg": "^7.11.0",
"@types/sequelize": "^4.28.5",
"@types/socket.io": "^2.1.2",
"@types/socket.io-redis": "^1.0.25",
"@types/validator": "^10.11.3",
"@types/winston": "^2.4.4",
"chai": "^4.2.0",
"delete": "^1.1.0",
"gulp": "^4.0.2",
"gulp-minify": "^3.1.0",
"gulp-sass": "^4.0.2",
"gulp-typescript": "^5.0.1",
"mocha": "^6.2.2",
"ts-lint": "^4.5.1",
"tsc": "^1.20150623.0",
"tslint": "^5.19.0",
"typescript": "^3.5.3"
"typescript": "^3.7.2"
},
"dependencies": {
"@types/socket.io-redis": "^1.0.25",
"@types/uuid": "^3.4.6",
"compression": "^1.7.4",
"connect-session-sequelize": "^6.0.0",
"cookie-parser": "^1.4.4",
@ -75,6 +78,7 @@
"socket.io": "^2.2.0",
"socket.io-redis": "^5.2.0",
"sqlite3": "^4.1.0",
"uuid": "^3.3.3",
"winston": "^3.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 socketIoRedis from "socket.io-redis";
import {resolver} from "./graphql/resolvers";
import dataaccess from "./lib/dataaccess";
import dataaccess from "./lib/dataAccess";
import globals from "./lib/globals";
import routes from "./routes";
@ -41,12 +41,13 @@ class App {
/**
* initializes everything that needs to be initialized asynchronous.
*/
public async init() {
public async init(): Promise<void> {
await dataaccess.init(this.sequelize);
const appSession = session({
cookie: {
maxAge: Number(globals.config.session.cookieMaxAge) || 604800000,
// @ts-ignore
secure: "auto",
},
resave: false,
@ -56,12 +57,15 @@ class App {
});
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)});
this.sequelize.options.logging = (msg) => logger.silly(msg);
logger.info("Setting up socket.io");
await routes.ioListeners(this.io);
this.io.adapter(socketIoRedis());
this.io.use(sharedsession(appSession, {autoSave: true}));
logger.info("Configuring express app.");
this.app.set("views", path.join(__dirname, "views"));
this.app.set("view engine", "pug");
this.app.set("trust proxy", 1);
@ -73,11 +77,24 @@ class App {
this.app.use(cookieParser());
this.app.use(appSession);
// enable cross origin requests if enabled in the config
if (globals.config.server.cors) {
if (globals.config.server?.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) => {
logger.verbose(`${req.method} ${req.url}`);
process.send({cmd: "notifyRequest"});
next();
});
this.app.use(routes.router);
@ -117,13 +134,14 @@ class App {
res.status(httpStatus.INTERNAL_SERVER_ERROR);
res.render("errors/500.pug");
});
logger.info("Server configured.");
}
/**
* Starts the web server.
*/
public start() {
if (globals.config.server.port) {
public start(): void {
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}`);

@ -1,6 +1,7 @@
import {GraphQLError} from "graphql";
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 globals from "../lib/globals";
import {InternalEvents} from "../lib/InternalEvents";
@ -100,6 +101,23 @@ export function resolver(req: any, res: any): any {
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 }) {
if (username && email && passwordHash) {
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.");
}
},
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 }) {
if (postId && type) {
if (req.session.userId) {

@ -25,6 +25,9 @@ type Query {
"returns the post filtered by the sort type with pagination."
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 {
@ -32,11 +35,14 @@ type Mutation {
acceptCookies: Boolean
"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."
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: Boolean
@ -247,6 +253,9 @@ type Profile implements UserData {
"the levels of the user depending on the points"
level: Int!
"the custom settings for the frontend"
settings: String!
}
"represents a single user post"
@ -274,7 +283,7 @@ type Post {
createdAt: String!
"the type of vote the user performed on the post"
userVote: VoteType
userVote(userId: ID!): VoteType
}
"represents a request of any type"
@ -368,6 +377,12 @@ type Event {
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"
enum VoteType {
UPVOTE

@ -3,21 +3,91 @@ import * as cluster from "cluster";
import App from "./app";
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) {
console.log(`[CLUSTER] Master ${process.pid} is running`);
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
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) => {
console.log(`[CLUSTER] Worker ${worker.process.pid} died!`);
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++) {
cluster.fork();
}
} else {
/**
* async main function wrapper.
*/
(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();
app.start();
})();

@ -35,7 +35,7 @@ namespace dataaccess {
/**
* Initializes everything that needs to be initialized asynchronous.
*/
export async function init(seq: Sequelize) {
export async function init(seq: Sequelize): Promise<void> {
sequelize = seq;
try {
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
* @param username
@ -131,7 +139,7 @@ namespace dataaccess {
* @param offset
* @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) {
return models.Post.findAll({
include: [{association: "rVotes"}],
@ -140,6 +148,7 @@ namespace dataaccess {
order: [["createdAt", "DESC"]],
});
} else {
// more performant way to get the votes with plain sql
return await sequelize.query(
`SELECT * FROM (
SELECT *,

@ -1,4 +1,4 @@
import dataaccess from "../dataaccess";
import dataaccess from "../dataAccess";
import {BaseError} from "./BaseError";
export class RequestNotFoundError extends BaseError {

@ -1,12 +1,13 @@
import {EventEmitter} from "events";
import * as fsx from "fs-extra";
import * as yaml from "js-yaml";
import * as path from "path";
import * as winston from "winston";
require("winston-daily-rotate-file");
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.
if (!(fsx.pathExistsSync(configPath))) {
@ -21,9 +22,9 @@ if (!(fsx.pathExistsSync(configPath))) {
* Defines global variables to be used.
*/
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
export const logger = winston.createLogger({
export const logger: winston.Logger = winston.createLogger({
transports: [
new winston.transports.Console({
format: winston.format.combine(
@ -33,7 +34,7 @@ namespace globals {
return `${timestamp} ${level}: ${message}`;
}),
),
level: config.logging.level,
level: config.logging?.level ?? "info",
}),
// @ts-ignore
new (winston.transports.DailyRotateFile)({
@ -46,13 +47,13 @@ namespace globals {
}),
),
json: false,
level: config.logging.level,
level: config.logging?.level ?? "info",
maxFiles: "7d",
zippedArchive: true,
}),
],
});
export const internalEmitter = new EventEmitter();
export const internalEmitter: EventEmitter = new EventEmitter();
}
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).
* @param markdownString
*/
export function renderInline(markdownString: string) {
export function renderInline(markdownString: string): string {
return md.renderInline(markdownString);
}
@ -28,7 +28,7 @@ namespace markdown {
* Renders the markdown string.
* @param markdownString
*/
export function render(markdownString: string) {
export function render(markdownString: string): string {
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 {User} from "./User";

@ -1,5 +1,5 @@
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 {ChatRoom} from "./ChatRoom";
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 {ChatMessage} from "./ChatMessage";
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[]> {
const limit = first || 10;
offset = offset || 0;
const limit = first ?? 10;
offset = offset ?? 0;
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[]> {
const limit = first || 10;
offset = offset || 0;
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;
const limit = first ?? 10;
offset = offset ?? 0;
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[]> {
const limit = first || 10;
offset = offset || 0;
const limit = first ?? 10;
offset = offset ?? 0;
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 {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 {User} from "./User";

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

@ -1,5 +1,5 @@
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";
export enum RequestType {

@ -10,6 +10,7 @@ import {
Unique,
UpdatedAt,
} from "sequelize-typescript";
import * as uuidv4 from "uuid/v4";
import {RequestNotFoundError} from "../errors/RequestNotFoundError";
import {UserNotFoundError} from "../errors/UserNotFoundError";
import {ChatMember} from "./ChatMember";
@ -49,6 +50,17 @@ export class User extends Model<User> {
@Column({defaultValue: 0, allowNull: false})
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")
public rFriends: User[];
@ -119,14 +131,33 @@ export class User extends Model<User> {
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
* @param first
* @param offset
*/
public async friends({first, offset}: { first: number, offset: number }): Promise<User[]> {
const limit = first || 10;
offset = offset || 0;
const limit = first ?? 10;
offset = offset ?? 0;
return await this.$get("rFriendOf", {limit, offset}) as User[];
}
@ -143,8 +174,8 @@ export class User extends Model<User> {
* @param offset
*/
public async chats({first, offset}: { first: number, offset: number }): Promise<ChatRoom[]> {
const limit = first || 10;
offset = offset || 0;
const limit = first ?? 10;
offset = offset ?? 0;
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[]> {
const limit = first || 10;
offset = offset || 0;
const limit = first ?? 10;
offset = offset ?? 0;
return await this.$get("rPosts", {limit, offset}) as Post[];
}
@ -210,8 +241,8 @@ export class User extends Model<User> {
* @param offset
*/
public async groups({first, offset}: { first: number, offset: number }): Promise<Group[]> {
const limit = first || 10;
offset = offset || 0;
const limit = first ?? 10;
offset = offset ?? 0;
return await this.$get("rGroups", {limit, offset}) as Group[];
}

@ -1,6 +1,6 @@
import {Router} from "express";
import {Namespace, Server} from "socket.io";
import dataaccess from "../lib/dataaccess";
import dataaccess from "../lib/dataAccess";
import globals from "../lib/globals";
import {InternalEvents} from "../lib/InternalEvents";
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,
"compilerOptions": {
"noImplicitAny": true,
"noImplicitThis": true,
"removeComments": true,
"preserveConstEnums": true,
"allowSyntheticDefaultImports": true,
"outDir": "./dist",
"sourceMap": true,
"target": "es2018",
"allowJs": true,
"allowJs": false,
"forceConsistentCasingInFileNames": true,
"strictFunctionTypes": true,
"moduleResolution": "node",
"module": "commonjs",
"experimentalDecorators": true,

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