Browse Source

feat: Stream Keys, Channels, Activities

tags/v0.1.0
Dale 1 year ago
parent
commit
7306d143f4
39 changed files with 687 additions and 331 deletions
  1. +1
    -1
      bin/resetDatabase.sh
  2. +4
    -0
      default.nix
  3. +24
    -0
      migrations/20190726182600_streaming-key-table.js
  4. +2
    -0
      package.json
  5. +13
    -8
      src/server/api/v1/handlers/activities.ts
  6. +16
    -16
      src/server/api/v1/handlers/auth.ts
  7. +67
    -0
      src/server/api/v1/handlers/channels.ts
  8. +58
    -0
      src/server/api/v1/handlers/streamkeys.ts
  9. +15
    -10
      src/server/api/v1/handlers/users.ts
  10. +12
    -8
      src/server/api/v1/index.ts
  11. +10
    -3
      src/server/api/v1/interfaces/IActivity.ts
  12. +2
    -2
      src/server/api/v1/interfaces/IChannel.ts
  13. +5
    -5
      src/server/api/v1/interfaces/ISession.ts
  14. +10
    -0
      src/server/api/v1/interfaces/IStreamKey.ts
  15. +1
    -1
      src/server/api/v1/middleware.ts
  16. +0
    -1
      src/server/api/v1/schemas/activity/createActivity.json
  17. +11
    -0
      src/server/api/v1/schemas/channel/createChannel.json
  18. +9
    -0
      src/server/api/v1/schemas/streamkeys/createStreamkey.json
  19. +2
    -3
      src/server/config.ts
  20. +29
    -11
      src/server/db/Activities.ts
  21. +32
    -31
      src/server/db/Channels.ts
  22. +10
    -11
      src/server/db/Sessions.ts
  23. +40
    -0
      src/server/db/Streamkeys.ts
  24. +24
    -19
      src/server/db/Users.ts
  25. +6
    -4
      src/server/lib/crypto.ts
  26. +2
    -2
      src/server/lib/db/interfaces.ts
  27. +0
    -88
      src/server/lib/http/handler/APIEndpoint.ts
  28. +0
    -3
      src/server/lib/http/handler/generic.ts
  29. +3
    -2
      src/server/lib/http/interfaces.ts
  30. +32
    -33
      src/server/lib/http/middleware.ts
  31. +2
    -2
      src/server/lib/http/request.ts
  32. +21
    -20
      src/server/lib/http/response.ts
  33. +18
    -18
      src/server/lib/validation/errors.ts
  34. +2
    -2
      src/server/routes/api.ts
  35. +1
    -2
      src/server/server.ts
  36. +15
    -15
      src/shared/lib/const.ts
  37. +4
    -4
      src/types/typings.d.ts
  38. +9
    -0
      tslint.json
  39. +175
    -6
      yarn.lock

+ 1
- 1
bin/resetDatabase.sh View File

@@ -4,4 +4,4 @@ PG_USER=${1-$USER};

psql postgres -U $PG_USER -c "drop database miracle;";
psql postgres -U $PG_USER -c "create database miracle;";
yarn migration:local;
yarn migrate:local;

+ 4
- 0
default.nix View File

@@ -5,6 +5,10 @@ stdenv.mkDerivation {
version = "0.1.0";
system = builtins.currentSystem;

shellHook = ''
export PATH="$PATH:$PWD/node_modules/.bin"
'';

buildInputs = [
yarn automake autoconf m4 git bash
nodejs-8_x libpng libGL gcc


+ 24
- 0
migrations/20190726182600_streaming-key-table.js View File

@@ -0,0 +1,24 @@
exports.up = function (knex) {
return knex.schema.createTableIfNotExists('streamkeys', function (t) {
t.uuid('id').primary().unique();
t.uuid('user_id');
t.integer('valid_for');
t.foreign('user_id')
.references("id")
.inTable("users");
t.uuid('created_by_id');
t.foreign('created_by_id')
.references("id")
.inTable("users");
t.uuid('channel_id');
t.foreign('channel_id')
.references('id')
.inTable('channels');
t.timestamps();
});
};

exports.down = function (knex) {
return knex.schema.dropTableIfExists('streamkeys');
};


+ 2
- 0
package.json View File

@@ -41,6 +41,8 @@
"ts-loader": "^6.0.1",
"ts-node": "^8.1.0",
"tsconfig-paths": "^3.8.0",
"tslint": "^5.18.0",
"tslint-config-airbnb": "^5.11.1",
"typescript": "^3.4.5",
"webpack": "^4.32.0",
"webpack-cli": "^3.3.2",


+ 13
- 8
src/server/api/v1/handlers/activities.ts View File

@@ -2,20 +2,25 @@ import { Response, Router } from 'express';

import { Request } from 'server/lib/http/interfaces';
import Activities from 'server/db/Activities';
import { sendData, dataOrNotFound, sendCreated, sendUnauthorized, sendNoContent } from 'server/lib/http/response';
import {
sendData,
dataOrNotFound,
sendCreated,
sendUnauthorized,
sendNoContent,
} from 'server/lib/http/response';
import { getIdFromParams } from 'server/lib/http/request';
import { isAdmin, isMod } from 'server/lib/crypto';

import { authenticateUser, acceptsSchema } from 'server/lib/http/middleware';
import createActivitySchema from 'server/api/v1/schemas/activity/createActivity.json';
import createActivityJson from 'server/api/v1/schemas/activity/createActivity.json';

function getList (req: Request, res: Response) {
function getList (_: any, res: Response) {
Activities.getActivities().then(sendData(res));
}

function getId (req: Request, res: Response) {
const id = getIdFromParams(req);
console.log(id)
Activities.getActivity(id)
.then(dataOrNotFound(res, { id }));
}
@@ -31,7 +36,7 @@ function create (req: Request, res: Response) {
function update (req: Request, res: Response) {
const id = getIdFromParams(req);
const currentUser = req.session.user;
Activities.getActivity(id).then(activity => {
Activities.getActivity(id).then((activity) => {
if (activity.created_by_id === currentUser.id || isMod(currentUser) || isAdmin(currentUser)) {
Activities.updateActivity(id, { ...activity, ...req.body }).then(sendData(res));
} else {
@@ -43,7 +48,7 @@ function update (req: Request, res: Response) {
function del (req: Request, res: Response) {
const id = getIdFromParams(req);
const currentUser = req.session.user;
Activities.getActivity(id).then(activity => {
Activities.getActivity(id).then((activity) => {
if (activity.created_by_id === currentUser.id || isMod(currentUser) || isAdmin(currentUser)) {
Activities.deleteActivity(id).then(() => sendNoContent(res));
} else {
@@ -55,9 +60,9 @@ function del (req: Request, res: Response) {
const router = Router();

router.get('/', getList);
router.post('/', authenticateUser, acceptsSchema(createActivitySchema), create);
router.post('/', authenticateUser, acceptsSchema(createActivityJson), create);
router.get('/:id', getId);
router.put('/:id', authenticateUser, acceptsSchema(createActivitySchema), update);
router.put('/:id', authenticateUser, acceptsSchema(createActivityJson), update);
router.delete('/:id', authenticateUser, del);

export default router;

+ 16
- 16
src/server/api/v1/handlers/auth.ts View File

@@ -1,5 +1,5 @@
import Users from 'server/db/Users';
import Sessions from 'server/db/Sessions'
import Sessions from 'server/db/Sessions';
import { Router, Response } from 'express';

import ISession from 'server/api/v1/interfaces/ISession';
@@ -12,42 +12,42 @@ import { Request } from 'server/lib/http/interfaces';
import { sendData, sendDuplicate, sendUnauthorized, sendNoContent } from 'server/lib/http/response';
import { checkPassword } from 'server/lib/crypto';

function login(req: Request, res: Response) {
function login (req: Request, res: Response) {
Users.getV1UserByUsername(req.body.username)
.then(user => {
.then((user) => {
if (user) {
const passwordsMatch = checkPassword(
req.body.password, user.passwordHash, user.passwordSalt
req.body.password, user.passwordHash, user.passwordSalt,
);
if (passwordsMatch) {
Sessions.createSession({ user_id: user.id })
.then(session => sendData(res)({ token: session.id }))
.then(session => sendData(res)({ token: session.id }));
} else {
sendUnauthorized(res)(req.body)
sendUnauthorized(res)(req.body);
}
} else {
sendUnauthorized(res)(req.body)
sendUnauthorized(res)(req.body);
}
})
});
}

function logout(req: Request, res: Response) {
function logout (req: Request, res: Response) {
if (req.session) {
Sessions.destroySession(req.session.id).then(() => sendNoContent(res))
Sessions.destroySession(req.session.id).then(() => sendNoContent(res));
}
}

function createSession(user_id: string): PromiseLike<ISession> {
function createSession (user_id: string): PromiseLike<ISession> {
return Sessions.createSession({ user_id });
}

function signUp(req: Request, res: Response) {
function signUp (req: Request, res: Response) {
Users.checkIfUserExists(req.body.username, req.body.email)
.then((exists) => {
if (!exists) {
Users.createV1User(req.body)
.then(user => createSession(user.id))
.then(session => sendData(res)({ token: session.id }))
.then(session => sendData(res)({ token: session.id }));
} else {
const { password, ...probableCause } = req.body;
sendDuplicate(res)(probableCause);
@@ -56,8 +56,8 @@ function signUp(req: Request, res: Response) {
}

const router = Router();
router.post("/login", acceptsSchema(loginSchema), login);
router.delete("/login", authenticateUser, logout);
router.post("/sign-up", acceptsSchema(createUserSchema), signUp);
router.post('/login', acceptsSchema(loginSchema), login);
router.delete('/login', authenticateUser, logout);
router.post('/sign-up', acceptsSchema(createUserSchema), signUp);

export default router;

+ 67
- 0
src/server/api/v1/handlers/channels.ts View File

@@ -0,0 +1,67 @@
import { Response, Router } from 'express';

import { Request } from 'server/lib/http/interfaces';
import Channels from 'server/db/Channels';
import {
sendData,
dataOrNotFound,
sendCreated,
sendUnauthorized,
sendNoContent,
} from 'server/lib/http/response';

import createChannelJson from 'server/api/v1/schemas/channel/createChannel.json';
import { authenticateUser, acceptsSchema } from 'server/lib/http/middleware';
import { getIdFromParams } from 'server/lib/http/request';
import { isAdmin, isMod } from 'server/lib/crypto';

function getList (_: any, res: Response) {
Channels.getChannels().then(sendData(res));
}

function getId (req: Request, res: Response) {
const id = getIdFromParams(req);
Channels.getChannel(id).then(dataOrNotFound(res, { id }));
}

function create (req: Request, res: Response) {
Channels
.createChannel(req.body, req.session.user.id)
.then(sendCreated(res));
}

function update (req: Request, res: Response) {
const id = getIdFromParams(req);
const currentUser = req.session.user;
Channels.getChannel(id).then((channel) => {
if (channel.created_by_id === currentUser.id || isMod(currentUser) || isAdmin(currentUser)) {
Channels.updateChannel(
id, { ...channel, ...req.body },
req.session.user.id,
).then(sendData(res));
} else {
sendUnauthorized(res)({ id: currentUser.id });
}
});
}

function del (req: Request, res: Response) {
const id = getIdFromParams(req);
const currentUser = req.session.user;
Channels.getChannel(id).then((channel) => {
if (channel.created_by_id === currentUser.id || isMod(currentUser) || isAdmin(currentUser)) {
Channels.deleteChannel(id).then(() => sendNoContent(res));
} else {
sendUnauthorized(res)({ id: currentUser.id });
}
});
}

const router = Router();
router.get('/', getList);
router.get('/:id', getId);
router.post('/', authenticateUser, acceptsSchema(createChannelJson), create);
router.put('/:id', authenticateUser, acceptsSchema(createChannelJson), update);
router.delete('/:id', authenticateUser, del);

export default router;

+ 58
- 0
src/server/api/v1/handlers/streamkeys.ts View File

@@ -0,0 +1,58 @@
import { Router, Response } from 'express';

import { Request } from 'server/lib/http/interfaces';
import Streamkeys from 'server/db/Streamkeys';
import { getIdFromParams } from 'server/lib/http/request';
import { sendData, sendUnauthorized, sendNoContent } from 'server/lib/http/response';
import Channels from 'server/db/Channels';
import { authenticateUser } from 'server/lib/http/middleware';
import IStreamKey from '../interfaces/IStreamKey';

function getKeysByChannel (req: Request, res: Response) {
const id = getIdFromParams(req);
Streamkeys
.getStreamKeysByChannelId(id, req.session.user.id)
.then(sendData(res));
}

function getKeysByUser (req: Request, res: Response) {
const id = getIdFromParams(req);
Streamkeys
.getStreamKeysByUserId(id, req.session.user.id)
.then(sendData(res));
}

function getKeysByMe (req: Request, res: Response) {
console.log(req.session);
Streamkeys
.getStreamKeysByCreatorId(req.session.user.id)
.then(sendData(res));
}

function createKey (req: Request<IStreamKey>, res: Response) {
const newKey = req.body;
Channels.getChannel(newKey.channel_id).then((channel) => {
if (channel && (channel.created_by_id === req.session.user.id)) {
Streamkeys
.createStreamKey(newKey.user_id, newKey.channel_id, req.session.user.id)
.then(sendData(res));
} else {
sendUnauthorized(res)({ id: req.session.user.id });
}
});
}

function deleteKey (req: Request, res: Response) {
const id = getIdFromParams(req);
Streamkeys.deleteStreamKey(id, req.session.user.id).then(() => sendNoContent(res));
}

const router = Router();

router.get('/', authenticateUser, getKeysByMe);
router.post('/', authenticateUser, createKey);
router.delete('/:id', authenticateUser, deleteKey);
router.get('/channel/:id', authenticateUser, getKeysByChannel);
router.get('/user/:id', authenticateUser, getKeysByUser);

export default router;

+ 15
- 10
src/server/api/v1/handlers/users.ts View File

@@ -3,26 +3,29 @@ import { Response, Router } from 'express';
import IUser from 'server/api/v1/interfaces/IUser';

import { Request } from 'server/lib/http/interfaces';
import { sendData, sendCreated, dataOrNotFound, sendDuplicate, sendUnauthorized } from 'server/lib/http/response';
import {
sendData,
sendCreated,
dataOrNotFound,
sendDuplicate,
sendUnauthorized,
} from 'server/lib/http/response';

import * as createUserSchema from 'server/api/v1/schemas/user/createUser.json';
import * as updateUserSchema from 'server/api/v1/schemas/user/updateUser.json'
import * as createUserJson from 'server/api/v1/schemas/user/createUser.json';
import * as updateUserJson from 'server/api/v1/schemas/user/updateUser.json';

import Users from 'server/db/Users';
import { acceptsSchema, authenticateUser, authorizeRoles } from 'server/lib/http/middleware';
import { getIdFromParams } from 'server/lib/http/request';
import { isAdmin } from 'server/lib/crypto';


export function getList (_: Request, res: Response) {
Users
.getV1UsersSafe()
.then(sendData(res));
}

export function getId (req: Request, res: Response) {
const id = getIdFromParams(req)
console.log(id);
const id = getIdFromParams(req);
Users.getV1UserSafe(id).then(dataOrNotFound(res, { id }));
}

@@ -32,7 +35,7 @@ export function create (req: Request, res: Response) {
if (!user) {
Users.createV1User(req.body).then(sendCreated(res));
} else {
sendDuplicate(res, { email })
sendDuplicate(res, { email });
}
});
}
@@ -63,9 +66,11 @@ export function del (req: Request, res: Response) {
const router = Router();

router.get('/', getList);
router.post('/', authenticateUser, authorizeRoles(['admin']), acceptsSchema(createUserSchema), create);
router.post(
'/', authenticateUser, authorizeRoles(['admin']), acceptsSchema(createUserJson), create,
);
router.get('/:id', getId);
router.put('/:id', authenticateUser, acceptsSchema(updateUserSchema), update);
router.put('/:id', authenticateUser, acceptsSchema(updateUserJson), update);
router.delete('/:id', authenticateUser, del);

export default router;

+ 12
- 8
src/server/api/v1/index.ts View File

@@ -1,16 +1,20 @@
import { Router, json } from 'express';

import { ApiV1CommonHeaders } from 'server/api/v1/middleware';
import userRoutes from 'server/api/v1/handlers/users';
import authRoutes from 'server/api/v1/handlers/auth';
import activityRoutes from 'server/api/v1/handlers/activities';
import { apiV1CommonHeaders } from 'server/api/v1/middleware';
import users from 'server/api/v1/handlers/users';
import auth from 'server/api/v1/handlers/auth';
import activities from 'server/api/v1/handlers/activities';
import channels from 'server/api/v1/handlers/channels';
import streamkeys from 'server/api/v1/handlers/streamkeys';

const v1Router = Router();

v1Router.use(ApiV1CommonHeaders);
v1Router.use(apiV1CommonHeaders);
v1Router.use(json());
v1Router.use('/auth', authRoutes);
v1Router.use('/users', userRoutes);
v1Router.use('/activities', activityRoutes);
v1Router.use('/auth', auth);
v1Router.use('/users', users);
v1Router.use('/activities', activities);
v1Router.use('/channels', channels);
v1Router.use('/streamkeys', streamkeys);

export default v1Router;

+ 10
- 3
src/server/api/v1/interfaces/IActivity.ts View File

@@ -1,14 +1,21 @@
import { IUserSafe } from "./IUser";
import { IWithTimestamps, IWithId } from "server/lib/db/interfaces";
import { IUserSafe } from './IUser';
import { IWithTimestamps, IWithId } from 'server/lib/db/interfaces';

export interface IActivityRaw extends IWithTimestamps, IWithId {
interface IActivityFields {
name: string;
verb: string;
created_by_id: string;
}

export interface IActivityRaw extends IWithTimestamps, IWithId, IActivityFields {}

interface IActivity extends IActivityRaw {
created_by: IUserSafe;
}

export interface IActivityForChannel extends IWithId {
name: string;
verb: string;
}

export default IActivity;

+ 2
- 2
src/server/api/v1/interfaces/IChannel.ts View File

@@ -1,5 +1,5 @@
import { IUserSafe } from 'server/api/v1/interfaces/IUser';
import { IActivityRaw } from './IActivity';
import { IActivityForChannel } from './IActivity';

export interface IChannelRaw {
id?: string;
@@ -13,5 +13,5 @@ export interface IChannelRaw {
}
export default interface IChannel extends IChannelRaw {
created_by: IUserSafe;
activity: IActivityRaw;
activity: IActivityForChannel;
}

+ 5
- 5
src/server/api/v1/interfaces/ISession.ts View File

@@ -1,11 +1,11 @@
import IUser from 'server/api/v1/interfaces/IUser';

export default interface ISession {
id?: string;
user: IUser;
id?: string;
user: IUser;
}

export interface ISessionRaw {
id?: string;
user_id: string;
}
id?: string;
user_id: string;
}

+ 10
- 0
src/server/api/v1/interfaces/IStreamKey.ts View File

@@ -0,0 +1,10 @@
import { IWithId, IWithTimestamps } from 'server/lib/db/interfaces';

interface IStreamKeyFields {
user_id: string;
channel_id: string;
created_by_id: string;
valid_for: number;
}

export default interface IStreamKey extends IStreamKeyFields, IWithId, IWithTimestamps {}

+ 1
- 1
src/server/api/v1/middleware.ts View File

@@ -1,6 +1,6 @@
import { Request, Response } from 'express';

export function ApiV1CommonHeaders (req: Request, res: Response, next: () => void) {
export function apiV1CommonHeaders (req: Request, res: Response, next: () => void) {
res.setHeader('X-Miracle-Api-Ver', '1');
next();
}

+ 0
- 1
src/server/api/v1/schemas/activity/createActivity.json View File

@@ -1,4 +1,3 @@

{
"type": "object",
"properties": {


+ 11
- 0
src/server/api/v1/schemas/channel/createChannel.json View File

@@ -0,0 +1,11 @@
{
"type": "object",
"properties": {
"title": { "type": "string" },
"slug": { "type": "string" },
"description": { "type": "string" },
"activity_id": { "type": "string" }
},
"additionalProperties": false,
"required": [ "title", "slug", "activity_id" ]
}

+ 9
- 0
src/server/api/v1/schemas/streamkeys/createStreamkey.json View File

@@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"user": { "type": "string" },
"channel": { "type": "string" }
},
"additionalProperties": false,
"required": [ "channel" ]
}

+ 2
- 3
src/server/config.ts View File

@@ -1,5 +1,5 @@
import { pathOr } from 'ramda';
import * as fs from 'fs'
import * as fs from 'fs';
import minimist from 'minimist';

interface IDBConfig {
@@ -10,7 +10,7 @@ interface IDBConfig {
username?: string,
database?: string,
filename?: string,
}
};
}

interface IConfig {
@@ -21,4 +21,3 @@ const args = minimist(process.argv.slice(2));
const configPath = pathOr(defaultConfigPath, ['configPath'], args) || defaultConfigPath;
const config: IConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
export default config;


+ 29
- 11
src/server/db/Activities.ts View File

@@ -2,25 +2,37 @@ import R from 'ramda';
import uuidv4 from 'uuid/v4';

import db from 'server/db';
import IActivity, { IActivityRaw } from 'server/api/v1/interfaces/IActivity';
import IActivity, { IActivityRaw, IActivityForChannel } from 'server/api/v1/interfaces/IActivity';
import Users from './Users';
import { IUserSafe } from 'server/api/v1/interfaces/IUser';

export type IActivityMap = {
[key: string]: IActivity;
}
};

export type IActivityForChannelMap = {
[key: string]: IActivityForChannel;
};

class Activities {

convertActivityForChannel = ({ id, name, verb }: IActivityRaw): IActivityForChannel =>
({ id, name, verb })

mergeActivity = (activity: IActivityRaw, user: IUserSafe): IActivity => ({
...activity,
created_by: user,
});
})

getActivityMap = (activities: IActivity[]): IActivityMap =>
R.reduce<IActivity, IActivityMap>(
(acc, activity) => ({ ...acc, [activity.id]: activity }), {}, activities
);
(acc, activity) => ({ ...acc, [activity.id]: activity }), {}, activities,
)

getActivityMapForChannel = (activities: IActivityForChannel[]): IActivityForChannelMap =>
R.reduce<IActivityForChannel, IActivityForChannelMap>(
(acc, activity) => ({ ...acc, [activity.id]: activity }), {}, activities,
)

getActivityIds = (activities: IActivityRaw[] | IActivity[]): string[] =>
R.map<IActivityRaw | IActivity, string>(R.path(['id']))(activities)
@@ -28,22 +40,21 @@ class Activities {
mergeActivities = (activities: IActivityRaw[], users: IUserSafe[]): IActivity[] => {
const userMap = Users.getUserSafeMap(users);
return activities.map(
activity => this.mergeActivity(activity, userMap[activity.created_by_id])
activity => this.mergeActivity(activity, userMap[activity.created_by_id]),
);
}

augmentActivity = (activity?: IActivityRaw): PromiseLike<IActivity | null> => {
if (activity) {
return Users.getV1UserSafe(activity.created_by_id)
.then(R.curry(this.mergeActivity)(activity))
} else {
Promise.resolve(null);
.then(R.curry(this.mergeActivity)(activity));
}
return Promise.resolve(null);
}

augmentActivities = (activities: IActivityRaw[]): PromiseLike<IActivity[]> => {
const userIds = activities.map<string>(R.path(['created_by_id']));
return Users.getUsersByIds(userIds).then(users => this.mergeActivities(activities, users))
return Users.getUsersByIdsSafe(userIds).then(users => this.mergeActivities(activities, users));
}

getActivities = (): PromiseLike<IActivity[]> => {
@@ -56,11 +67,18 @@ class Activities {
.then(this.augmentActivities)

getActivity = (id: string): PromiseLike<IActivity | null> => {
console.log(id);
return db('activities').where({ id }).get(0)
.then(this.augmentActivity);
}

getActivityForChannel = (id: string): PromiseLike<IActivityForChannel | null> =>
db('activities').where({ id }).get(0)
.then(this.convertActivityForChannel)

getActivitiesForChannelByIds = (ids: string[]): PromiseLike<IActivityForChannel[]> =>
db('activities').whereIn('id', ids)
.then(R.map(this.convertActivityForChannel))

createActivity = (activity: IActivityRaw): PromiseLike<IActivity> => {
return db('activities')
.insert({ ...activity, id: uuidv4() }).returning('*').get(0)


src/server/db/Channel.ts → src/server/db/Channels.ts View File

@@ -1,4 +1,4 @@
import R from 'ramda';
import ramda from 'ramda';
import uuidv4 from 'uuid/v4';

import db from 'server/db';
@@ -6,60 +6,60 @@ import IChannel, { IChannelRaw } from 'server/api/v1/interfaces/IChannel';
import Users from './Users';
import Activities from './Activities';
import { IUserSafe } from 'server/api/v1/interfaces/IUser';
import IActivity from 'server/api/v1/interfaces/IActivity';
import IActivity, { IActivityForChannel } from 'server/api/v1/interfaces/IActivity';

class Channels {

mergeChannel = (channel: IChannelRaw, user: IUserSafe, activity: IActivity): IChannel => {
return {
...channel,
activity,
created_by: user,
};
}
mergeChannel =
(channel: IChannelRaw, user: IUserSafe, activity: IActivityForChannel): IChannel => {
return {
...channel,
activity,
created_by: user,
};
}

mergeChannels = (channels: IChannelRaw[], users: IUserSafe[], activities: IActivity[]) => {
const userMap = Users.getUserSafeMap(users);
const activityMap = Activities.getActivityMap(activities);
return channels.map(channel => this.mergeChannel(
channel,
R.path([channel.created_by_id], userMap),
R.path([channel.activity_id], activityMap),
));
}
mergeChannels =
(channels: IChannelRaw[], users: IUserSafe[], activities: IActivityForChannel[]) => {
const userMap = Users.getUserSafeMap(users);
const activityMap = Activities.getActivityMapForChannel(activities);
return channels.map(channel => this.mergeChannel(
channel,
ramda.path([channel.created_by_id], userMap),
ramda.path([channel.activity_id], activityMap),
));
}

augmentChannel = (channel?: IChannelRaw): PromiseLike<IChannel | null> => {
if (channel) {
return Promise.all([
Users.getV1UserSafe(channel.created_by_id),
Activities.getActivity(channel.activity_id),
Activities.getActivityForChannel(channel.activity_id),
]).then(([user, activity]) => this.mergeChannel(channel, user, activity));
} else {
Promise.resolve(null);
}
return Promise.resolve(null);
}

augmentChannels = (channels: IChannelRaw[]): PromiseLike<IChannel[]> => {
const userIds: string[] = channels
.map(R.path<string>(['created_by_id'])).filter(R.identity);
.map(ramda.path<string>(['created_by_id'])).filter(ramda.identity);
const activityIds: string[] = channels
.map(R.path<string>(['activity_id'])).filter(R.identity);
.map(ramda.path<string>(['activity_id'])).filter(ramda.identity);
return Promise.all([
Users.getUsersByIds(userIds),
Activities.getActivitiesByIds(activityIds),
Users.getUsersByIds(userIds).then(users => users.map(Users.sanitzieUser)),
Activities.getActivitiesForChannelByIds(activityIds),
]).then(([users, activities]) => this.mergeChannels(channels, users, activities));
}

getChannels = (): PromiseLike<IChannel[]> =>
db('channels').select('*')
.then(this.augmentChannels);
.then(this.augmentChannels)

getChannelsByIds = (ids: string[]): PromiseLike<IChannel[]> =>
db('channels').select('*').whereIn('id', ids)
.then(this.augmentChannels);
.then(this.augmentChannels)

getChannel = (id: string): PromiseLike<IChannel | null> => {
console.log(id);
return db('channels').where({ id }).get(0)
.then(this.augmentChannel);
}
@@ -70,9 +70,10 @@ class Channels {
.then(this.augmentChannel);
}

updateActivity = (id: string, channel: IChannelRaw, user_id: string): PromiseLike<IChannel> => {
return db('channel').where({ id })
.update({ ...channel, id, created_by_id: user_id }, '*').get(0)
updateChannel = (id: string, channel: IChannel, user_id: string): PromiseLike<IChannel> => {
const { activity, created_by, ...sanitizedChannel } = channel;
return db('channels').where({ id })
.update({ ...sanitizedChannel, id, created_by_id: user_id }, '*').get(0)
.then(this.augmentChannel);
}


+ 10
- 11
src/server/db/Sessions.ts View File

@@ -7,28 +7,27 @@ import ISession, { ISessionRaw } from 'server/api/v1/interfaces/ISession';
import { hashPassword } from 'server/lib/crypto';

class Sessions {
augmentSession(rawSession?: ISessionRaw): PromiseLike<ISession|null> {
augmentSession (rawSession?: ISessionRaw): PromiseLike<ISession|null> {
if (rawSession) {
return db('users').where({ id: rawSession.user_id }).get(0)
.then((user) => ({ id: rawSession.id, user }))
} else {
return Promise.resolve(null)
.then(user => ({ user, id: rawSession.id }));
}
return Promise.resolve(null);
}

getSession(id: string): PromiseLike<ISession|null> {
return db('sessions').where({ id }).get(0).then(this.augmentSession)
getSession (id: string): PromiseLike<ISession|null> {
return db('sessions').where({ id }).get(0).then(this.augmentSession);
}

createSession(rawSession: ISessionRaw): PromiseLike<ISession> {
createSession (rawSession: ISessionRaw): PromiseLike<ISession> {
return db('sessions').insert({
...rawSession, id: uuidv4(),
}).returning("*").get(0).then(
(session: ISessionRaw) => this.augmentSession(session)
...rawSession, id: uuidv4(),
}).returning('*').get(0).then(
(session: ISessionRaw) => this.augmentSession(session),
);
}

destroySession(id: string): PromiseLike<boolean> {
destroySession (id: string): PromiseLike<boolean> {
return db('sessions').where({ id }).delete();
}
}


+ 40
- 0
src/server/db/Streamkeys.ts View File

@@ -0,0 +1,40 @@
import uuidv4 from 'uuid/v4';

import db from 'server/db';
import IStreamKey from 'server/api/v1/interfaces/IStreamKey';

class Streamkeys {
getStreamKeysByChannelId =
(channel_id: string, created_by_id: string): PromiseLike<IStreamKey[]> =>
db('streamkeys').where({ channel_id, created_by_id })

getStreamKeysByUserId =
(user_id: string, created_by_id: string): PromiseLike<IStreamKey[]> =>
db('streamkeys').where({ user_id, created_by_id })

getStreamKeysByCreatorId =
(created_by_id: string): PromiseLike<IStreamKey[]> =>
db('streamkeys').where({ created_by_id })

createStreamKey = (
user_id: string,
channel_id: string,
created_by_id: string,
valid_for: number = 0,
): PromiseLike<IStreamKey | null> =>
db('streamkeys').insert(
{
created_by_id,
channel_id,
valid_for,
id: uuidv4(),
user_id: user_id || created_by_id,
},
'*',
).get(0)

deleteStreamKey = (id: string, created_by_id: string): PromiseLike<boolean> =>
db('streamkeys').where({ id, created_by_id }).delete().then()
}

export default new Streamkeys;

+ 24
- 19
src/server/db/Users.ts View File

@@ -1,4 +1,4 @@
import R from 'ramda';
import ramda from 'ramda';
import uuidv4 from 'uuid/v4';

import db from 'server/db';
@@ -7,7 +7,7 @@ import IUser, { IUserCreation, IUserSafe } from 'server/api/v1/interfaces/IUser'
import { hashPassword } from 'server/lib/crypto';

export interface IUserSafeMap {
[key: string]: IUserSafe
[key: string]: IUserSafe;
}

class Users {
@@ -21,58 +21,63 @@ class Users {
...userSafe
} = user;
return userSafe;
} else {
return null;
}
return null;
}

getUserSafeMap = (users: IUserSafe[]): IUserSafeMap =>
R.reduce<IUserSafe, IUserSafeMap>(
(acc, user) => ({ ...acc, [user.id]: user }), {}, users
);
ramda.reduce<IUserSafe, IUserSafeMap>(
(acc, user) => ({ ...acc, [user.id]: user }), {}, users,
)

getUserIds = (users: IUserSafe[] | IUser[]): string[] =>
R.map<IUser | IUserSafe, string>(R.path(['id']))(users)
ramda.map<IUser | IUserSafe, string>(ramda.path(['id']))(users)

checkIfUserExists = (username: string, email: string): PromiseLike<boolean> => {
return db("users").where(function() {
return db('users').where(function () {
return this.where({ username }).orWhere({ email });
}).count().get(0).then(R.pipe(R.pathOr(0, ['count']), R.lt(0)));
}).count().get(0).then(ramda.pipe(ramda.pathOr(0, ['count']), ramda.lt(0)));
}

getV1Users = (): PromiseLike<IUser[]> =>
db('users').select('*')
.then((users: IUser[]) => users.map((user: any) => user as IUser));
.then((users: IUser[]) => users.map((user: any) => user as IUser))

getUsersByIds = (ids: string[]): PromiseLike<IUser[]> =>
db('users').select('*').whereIn('id', ids)
.then((users: IUser[]) => users.map((user: any) => user as IUser));
.then((users: IUser[]) => users.map((user: any) => user as IUser))

getUsersByIdsSafe = (ids: string[]): PromiseLike<IUserSafe[]> =>
db('users').select('*').whereIn('id', ids)
.then((users: IUser[]) => users
.map((user: any) => user as IUser)
.map(this.sanitzieUser))

getV1UsersSafe = (): PromiseLike<IUserSafe[]> =>
this.getV1Users().then(users => users.map(this.sanitzieUser))

getV1UserSafe = (id: string): PromiseLike<IUserSafe> =>
this.getV1User(id).then(this.sanitzieUser);
this.getV1User(id).then(this.sanitzieUser)

getV1User = (id: string): PromiseLike<IUser> =>
db('users').select('*').where({ id }).get(0);
db('users').select('*').where({ id }).get(0)

getV1UserByUsername = (username: string): PromiseLike<IUser> =>
db('users').select('*').where({ username }).get(0);
db('users').select('*').where({ username }).get(0)

getV1UserByEmail = (email: string): PromiseLike<IUser> =>
db('users').select('*').where({ email }).get(0);
db('users').select('*').where({ email }).get(0)

createV1User = (user: IUserCreation): PromiseLike<IUserSafe> => {
const { password, ...sanitizedUser } = user;
const { passwordSalt, passwordHash } = hashPassword(password);
const userObject = {
passwordSalt,
passwordHash,
id: uuidv4(),
...sanitizedUser,
singleChannelMode: true,
role: 'user',
passwordSalt,
passwordHash,
};
return db('users').insert(userObject).returning('*').get(0).then(this.sanitzieUser);
}
@@ -82,7 +87,7 @@ class Users {
}

deleteV1User = (id: string): PromiseLike<Boolean> =>
db('users').where({ id }).delete().then();
db('users').where({ id }).delete().then()
}

export default new Users;

+ 6
- 4
src/server/lib/crypto.ts View File

@@ -16,7 +16,9 @@ export function hashPassword (password: string): IHashResult {
return { passwordHash, passwordSalt };
}

export function checkPassword(password: string, passwordHash: string, passwordSalt: string): boolean {
export function checkPassword (
password: string, passwordHash: string, passwordSalt: string,
): boolean {
const hashedChallenge = crypto
.pbkdf2Sync(password, passwordSalt, 1000, 64, 'sha512')
.toString('base64');
@@ -24,14 +26,14 @@ export function checkPassword(password: string, passwordHash: string, passwordSa
return passwordHash === hashedChallenge;
}

export function isAdmin(user: IUser): boolean {
export function isAdmin (user: IUser): boolean {
return user.role === ADMIN_ROLE;
}

export function isMod(user: IUser): boolean {
export function isMod (user: IUser): boolean {
return user.role === MODERATOR_ROLE;
}

export function isUser(user: IUser): boolean {
export function isUser (user: IUser): boolean {
return user.role === USER_ROLE;
}

+ 2
- 2
src/server/lib/db/interfaces.ts View File

@@ -3,6 +3,6 @@ export interface IWithId {
}

export interface IWithTimestamps {
created_at?: Date,
updated_at?: Date,
created_at?: Date;
updated_at?: Date;
}

+ 0
- 88
src/server/lib/http/handler/APIEndpoint.ts View File

@@ -1,88 +0,0 @@
import { Response, Router, Request } from 'express';

import { sendParams, sendNotImplemented, sendNoContent } from 'server/lib/http/response';
import { acceptsSchema, authenticateUser, authorizeRoles } from '../middleware';

interface IDocumentationObject {
title?: string;
description?: string;
};

interface EndpointOptions {
middleware?: any;
schema?: object;
postSchema?: object;
putIdSchema?: object;
documentation?: IDocumentationObject;
}

type Handler = (req: Request, res: Response) => void;
type Handlers = { [key: string]: Handler };

interface IEndpointSchemas {
post?: any,
put?: any,
patch?: any,
postId?: any,
putId?: any,
patchId?: any,
}

export default class APIEndpoint {
router: Router;
documentation?: IDocumentationObject;

authenticateUsers?: boolean;
authorizeRole?: string | string[];

get?: Handler
post?: Handler
put?: Handler
delete?: Handler
options?: Handler
schemas: IEndpointSchemas = {}

constructor({
middleware = [],
documentation = {}
}: EndpointOptions = {}) {
this.documentation = documentation;
this.router = Router();
middleware.forEach(this.router.use)
}

optionsHandler = (_: any, res: Response): void => {
sendParams(res)({ schema: this.schemas });
}

defaultHandler = (_: any, res: Response): void => {
sendNotImplemented(res)({ message: "Nope" });
}

getHandlers = (): Handlers => {
return {
'get': this.get || this.defaultHandler,
'post': this.post || this.defaultHandler,
'put': this.put || this.defaultHandler,
'delete': this.delete || this.defaultHandler,
'options': this.options || this.defaultHandler,
};
}



getRouter = (): Router => {
const handlers: Handlers = this.getHandlers();
if (this.authenticateUsers) {
this.router.use(authenticateUser);
}
if (this.authenticateUsers && this.authorizeRole) {
this.router.use(authorizeRoles(this.authorizeRole));
}
return this.router;
}

getRoutes = (): any => {
console.log(this.router);
}
}

+ 0
- 3
src/server/lib/http/handler/generic.ts View File

@@ -1,3 +0,0 @@
import { Response } from "express";

export const getEntityByField = (tableName: string, fieldName: string) => (req: Request, res: Response)

+ 3
- 2
src/server/lib/http/interfaces.ts View File

@@ -2,7 +2,7 @@ import { Request } from 'express';
import ISession from 'server/api/v1/interfaces/ISession';

export interface ErrorsObject {
[key: string]: string,
[key: string]: string;
}

export interface ResponseObject<TData = any> {
@@ -14,7 +14,8 @@ export interface ResponseObject<TData = any> {
message?: string;
}

export interface Request extends Request {
export interface Request<T = any> extends Request {
session?: ISession;
body: T;
entity?: any;
}

+ 32
- 33
src/server/lib/http/middleware.ts View File

@@ -1,50 +1,49 @@
import * as R from 'ramda';
import uuidv4 from 'uuid/v4';
import ramda from 'ramda';
import { Response } from 'express';
import AJV from 'ajv';

import { convertErrors } from 'server/lib/validation/errors';
import { request } from 'http';
import Sessions from 'server/db/Sessions';
import { Request } from './interfaces';
import { sendUnauthenticated, sendUnauthorized } from './response';

export function JsonApiHeaders (req: Request, res: Response, next: () => void) {
export function jsonApiHeaders (_: any, res: Response, next: () => void) {
res.setHeader('Content-Type', 'application/json');
res.setHeader('X-Powered-By', 'miracle-tv');
next();
}

export const acceptsSchema = (schema: object) => (req: Request, res: Response, next: () => void) => {
const ajv = new AJV({ allErrors: true });
const validate = ajv.compile(schema);
const isValid = validate(req.body);
if (isValid) {
next();
} else {
const errorsAndMessages = convertErrors(validate.errors);
const errors = Object.keys(errorsAndMessages).reduce((acc, key) => {
return { ...acc, [key]: errorsAndMessages[key].errorCode };
}, {});
const errorMessages = Object.keys(errorsAndMessages).reduce((acc, key) => {
return { ...acc, [key]: errorsAndMessages[key].message };
}, {});
res.status(400);
res.send({
status: 'error',
message: 'Validation Error',
errors,
errorMessages
});
}
};
export const acceptsSchema =
(schema: object) => (req: Request, res: Response, next: () => void) => {
const ajv = new AJV({ allErrors: true });
const validate = ajv.compile(schema);
const isValid = validate(req.body);
if (isValid) {
next();
} else {
const errorsAndMessages = convertErrors(validate.errors);
const errors = Object.keys(errorsAndMessages).reduce((acc, key) => {
return { ...acc, [key]: errorsAndMessages[key].errorCode };
}, {});
const errorMessages = Object.keys(errorsAndMessages).reduce((acc, key) => {
return { ...acc, [key]: errorsAndMessages[key].message };
}, {});
res.status(400);
res.send({
errors,
errorMessages,
status: 'error',
message: 'Validation Error',
});
}
};

export function authenticateUser(req: Request, res: Response, next: any) {
export function authenticateUser (req: Request, res: Response, next: any) {
if (!req.headers.authorization) {
sendUnauthenticated(res)({ token: '' });
return;
}
const token = R.last(req.headers.authorization.split(' '));
const token = ramda.last(req.headers.authorization.split(' '));
Sessions.getSession(token).then(
(session) => {
if (session) {
@@ -55,12 +54,12 @@ export function authenticateUser(req: Request, res: Response, next: any) {
}
},
() => sendUnauthenticated(res)({ token }),
)
);
}

export const authorizeRoles =
(roles: string | string[]) => (req: Request, res: Response, next: any) => {
if (R.is(String, roles)) {
if (ramda.is(String, roles)) {
const hasAccess = req.session.user.role === roles;
if (hasAccess) {
next();
@@ -68,11 +67,11 @@ export const authorizeRoles =
sendUnauthorized(res)({ token: req.session.id });
}
} else {
const hasAccess = R.includes(req.session.user.role, roles);
const hasAccess = ramda.includes(req.session.user.role, roles);
if (hasAccess) {
next();
} else {
sendUnauthorized(res)({ token: req.session.id });
}
}
}
};

+ 2
- 2
src/server/lib/http/request.ts View File

@@ -1,4 +1,4 @@
import R from 'ramda';
import ramda from 'ramda';

export const getIdFromParams =
R.pathOr<string>(null, ['params', 'id'])
ramda.pathOr<string>(null, ['params', 'id']);

+ 21
- 20
src/server/lib/http/response.ts View File

@@ -1,59 +1,60 @@
import * as R from 'ramda';
import ramda from 'ramda';
import { Response } from 'express';

import { HTTP_CODES } from 'shared/lib/const';

export const sendData = R.curry((res: Response, data: any) => {
export const sendData = ramda.curry((res: Response, data: any) => {
res.json({ data, status: 'ok' });
});

export const dataOrCode = R.curry((code: number, res: Response, params: Object, data: Object) => {
if (!R.isNil(data)) {
sendData(res, data);
} else {
res.status(code).json({ data: params, code, status: 'error' });
}
});
export const dataOrCode =
ramda.curry((code: number, res: Response, params: Object, data: Object) => {
if (!ramda.isNil(data)) {
sendData(res, data);
} else {
res.status(code).json({ code, data: params, status: 'error' });
}
});

export const dataOrNotFound = dataOrCode(HTTP_CODES.NOT_FOUND);
export const dataOrBadRequest = dataOrCode(HTTP_CODES.BAD_REQUEST);

export const sendUnauthorized = R.curry((res: Response, data: Object) => {
export const sendUnauthorized = ramda.curry((res: Response, data: Object) => {
res.status(HTTP_CODES.UNAUTHORIZED).json({ data, status: 'error' });
});

export const sendUnauthenticated = R.curry((res: Response, data: Object) => {
export const sendUnauthenticated = ramda.curry((res: Response, data: Object) => {
res.status(HTTP_CODES.UNAUTHENTICATED).json({ data, status: 'error' });
});

export const sendParamError = R.curry((res: Response, data: Object) => {
export const sendParamError = ramda.curry((res: Response, data: Object) => {
res.status(HTTP_CODES.BAD_REQUEST).json({ data, status: 'error' });
});

export const sendServerError = R.curry((res: Response, data: Object) => {
export const sendServerError = ramda.curry((res: Response, data: Object) => {
res.status(HTTP_CODES.SERVER_ERROR).json({ data, status: 'error' });
});

export const sendNotImplemented = R.curry((res: Response, data: Object) => {
export const sendNotImplemented = ramda.curry((res: Response, data: Object) => {
res.status(HTTP_CODES.NOT_IMPLEMENTED).json({ data, status: 'error' });
});

export const sendNotFound = R.curry((res: Response, data: Object) => {
export const sendNotFound = ramda.curry((res: Response, data: Object) => {
res.status(HTTP_CODES.NOT_FOUND).json({ data, status: 'error' });
});

export const sendDuplicate = R.curry((res: Response, data: Object) => {
res.status(HTTP_CODES.DUPLICATE).json({ data, status: 'error'});
export const sendDuplicate = ramda.curry((res: Response, data: Object) => {
res.status(HTTP_CODES.DUPLICATE).json({ data, status: 'error' });
});

export const sendCreated = R.curry((res: Response, data: any) => {
export const sendCreated = ramda.curry((res: Response, data: any) => {
res.status(HTTP_CODES.CREATED).json({ data, status: 'ok' });
});

export const sendNoContent = R.curry((res: Response) => {
export const sendNoContent = ramda.curry((res: Response) => {
res.status(HTTP_CODES.NO_CONTENT).end();
});

export const sendRedirect = R.curry((res: Response, url: string) => {
export const sendRedirect = ramda.curry((res: Response, url: string) => {
res.redirect(url);
});

+ 18
- 18
src/server/lib/validation/errors.ts View File

@@ -6,49 +6,49 @@ import {
ERROR_CODE_TYPE,
ERROR_CODE_MAX_NUM, ERROR_CODE_MIN_NUM,
ERROR_CODE_MAX_STRING, ERROR_CODE_MIN_STRING,
} from 'shared/lib/const'
} from 'shared/lib/const';

const propertyNameMap: { [key: string] : string } = {
'required': 'missingProperty',
'additionalProperties': 'additionalProperty',
}
required: 'missingProperty',
additionalProperties: 'additionalProperty',
};

const keywordErrorMap: { [key: string]: string } = {
'required': ERROR_CODE_MISSING_PROPERTY,
'additionalProperties': ERROR_CODE_EXTRA_PROPERTY,
'type': ERROR_CODE_TYPE,
'maximum': ERROR_CODE_MAX_NUM,
'minimum': ERROR_CODE_MIN_NUM,
'maxLength': ERROR_CODE_MAX_STRING,
'minLength': ERROR_CODE_MIN_STRING,
}
required: ERROR_CODE_MISSING_PROPERTY,
additionalProperties: ERROR_CODE_EXTRA_PROPERTY,
type: ERROR_CODE_TYPE,
maximum: ERROR_CODE_MAX_NUM,
minimum: ERROR_CODE_MIN_NUM,
maxLength: ERROR_CODE_MAX_STRING,
minLength: ERROR_CODE_MIN_STRING,
};

function commonErrorHandler (error: any) {
const propertyNameKey: string = propertyNameMap[error.keyword];
const propertyName: string = error.params[propertyNameKey];
const message: string = error.message;
const parentPath: string[] = error.schemaPath.split('/').filter(
(key:string) => key !== '#' && key !== error.keyword && key !== 'properties'
(key:string) => key !== '#' && key !== error.keyword && key !== 'properties',
);
const fullPath = propertyName
? uniq([...parentPath, propertyName]).filter(identity).join('.')
: error.dataPath.split('.').filter(identity).join('.');
return {
message,
path: fullPath,
errorCode: keywordErrorMap[error.keyword] || error.keyword,
message,
};
};
}

function defaultHandler(error: any) {
function defaultHandler (error: any) {
return error;
}

export function convertErrors(validationErrors: any): any {
export function convertErrors (validationErrors: any): any {
const errorsByPath = validationErrors.reduce((acc: any, error: any) => {
const errorHandler = commonErrorHandler;
const { path, ...convertedError } = errorHandler(error);
return { ...acc, [path]: convertedError };
}, {})
}, {});
return errorsByPath;
}

+ 2
- 2
src/server/routes/api.ts View File

@@ -1,10 +1,10 @@
import { Router } from 'express';

import { JsonApiHeaders } from 'server/lib/http/middleware';
import { jsonApiHeaders } from 'server/lib/http/middleware';
import v1Router from 'server/api/v1';

const apiRouter = Router();

apiRouter.use(JsonApiHeaders);
apiRouter.use(jsonApiHeaders);
apiRouter.use('/v1', v1Router);
export default apiRouter;

+ 1
- 2
src/server/server.ts View File

@@ -1,5 +1,4 @@
import Express from 'express';
import { Request, Response } from 'express';
import Express, { Request, Response } from 'express';

import apiRouter from 'server/routes/api';



+ 15
- 15
src/shared/lib/const.ts View File

@@ -6,16 +6,16 @@ export const ERROR_CODE_MIN_NUM = 'errors.schema.field.minimumNumber';
export const ERROR_CODE_MAX_STRING = 'errors.schema.field.maximumStringLength';
export const ERROR_CODE_MIN_STRING = 'errors.schema.field.minimumStringLength';

export const HTTP_CODE_OK = 200
export const HTTP_CODE_CREATED = 201
export const HTTP_CODE_NO_CONTENT = 204
export const HTTP_CODE_BAD_REQUEST = 400
export const HTTP_CODE_UNAUTHENTICATED = 401
export const HTTP_CODE_UNAUTHORIZED = 403
export const HTTP_CODE_NOT_FOUND = 404
export const HTTP_CODE_SERVER_ERROR = 500
export const HTTP_CODE_DUPLICATE = 409
export const HTTP_CODE_NOT_IMPLEMENTED = 501
export const HTTP_CODE_OK = 200;
export const HTTP_CODE_CREATED = 201;
export const HTTP_CODE_NO_CONTENT = 204;
export const HTTP_CODE_BAD_REQUEST = 400;
export const HTTP_CODE_UNAUTHENTICATED = 401;
export const HTTP_CODE_UNAUTHORIZED = 403;
export const HTTP_CODE_NOT_FOUND = 404;
export const HTTP_CODE_SERVER_ERROR = 500;
export const HTTP_CODE_DUPLICATE = 409;
export const HTTP_CODE_NOT_IMPLEMENTED = 501;
export const HTTP_CODES = {
OK: HTTP_CODE_OK,
CREATED: HTTP_CODE_CREATED,
@@ -29,24 +29,24 @@ export const HTTP_CODES = {
NOT_IMPLEMENTED: HTTP_CODE_NOT_IMPLEMENTED,
};

interface IROLE_OPTION {
interface IRoleOption {
label: string;
value: string;
}

interface IROLE_LABEL_MAP {
interface IRoleLabelMap {
[key: string]: string;
}

export const ADMIN_ROLE = 'admin';
export const MODERATOR_ROLE = 'mod';
export const USER_ROLE = 'user';
export const ROLE_OPTIONS: IROLE_OPTION[] = [
export const ROLE_OPTIONS: IRoleOption[] = [
{ label: 'Admin', value: ADMIN_ROLE },
{ label: 'Moderator', value: MODERATOR_ROLE },
{ label: 'User', value: USER_ROLE },
];

export const ROLE_LABEL_MAP: IROLE_LABEL_MAP = ROLE_OPTIONS
.reduce((acc: object, opt: IROLE_OPTION) => ({ ...acc, [opt.value]: opt.label }), {})
export const ROLE_LABEL_MAP: IRoleLabelMap = ROLE_OPTIONS
.reduce((acc: object, opt: IRoleOption) => ({ ...acc, [opt.value]: opt.label }), {});
export const ROLE_LIST: string[] = ROLE_OPTIONS.map(opt => opt.value);

+ 4
- 4
src/types/typings.d.ts View File

@@ -1,6 +1,6 @@
declare module "*.json";
declare module "server/db/knexfile";
declare module "uuid/v4" {
function uuid4(): string;
declare module '*.json';
declare module 'server/db/knexfile';
declare module 'uuid/v4' {
function uuid4 (): string;
export = uuid4;
}

+ 9
- 0
tslint.json View File

@@ -0,0 +1,9 @@
{
"extends": "tslint-config-airbnb",
"rules": {
"space-before-function-paren": { "options": ["always"] },
"import-name": false,
"variable-name": false,
"align": false
}
}

+ 175
- 6
yarn.lock View File

@@ -2,6 +2,22 @@
# yarn lockfile v1


"@babel/code-frame@^7.0.0":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d"
integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==
dependencies:
"@babel/highlight" "^7.0.0"

"@babel/highlight@^7.0.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.5.0.tgz#56d11312bd9248fa619591d02472be6e8cb32540"
integrity sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==
dependencies:
chalk "^2.0.0"
esutils "^2.0.2"
js-tokens "^4.0.0"

"@babel/polyfill@^7.4.3":
version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.4.4.tgz#78801cf3dbe657844eeabf31c1cae3828051e893"
@@ -10,6 +26,25 @@
core-js "^2.6.5"
regenerator-runtime "^0.13.2"

"@fimbul/bifrost@^0.17.0":
version "0.17.0"
resolved "https://registry.yarnpkg.com/@fimbul/bifrost/-/bifrost-0.17.0.tgz#f0383ba7e40992e3193dc87e2ddfde2ad62a9cf4"
integrity sha512-gVTkJAOef5HtN6LPmrtt5fAUmBywwlgmObsU3FBhPoNeXPLaIl2zywXkJEtvvVLQnaFmtff3x+wIj5lHRCDE3Q==
dependencies:
"@fimbul/ymir" "^0.17.0"
get-caller-file "^2.0.0"
tslib "^1.8.1"
tsutils "^3.5.0"

"@fimbul/ymir@^0.17.0":
version "0.17.0"
resolved "https://registry.yarnpkg.com/@fimbul/ymir/-/ymir-0.17.0.tgz#4f28389b9f804d1cd202e11983af1743488b7815"
integrity sha512-xMXM9KTXRLHLVS6dnX1JhHNEkmWHcAVCQ/4+DA1KKwC/AFnGHzu/7QfQttEPgw3xplT+ILf9e3i64jrFwB3JtA==
dependencies:
inversify "^5.0.0"
reflect-metadata "^0.1.12"
tslib "^1.8.1"

"@types/ajv@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/ajv/-/ajv-1.0.0.tgz#4fb2440742f2f6c30e7fb0797b839fc6f696682a"
@@ -677,6 +712,11 @@ buffer@^4.3.0:
ieee754 "^1.1.4"
isarray "^1.0.0"

builtin-modules@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=

builtin-status-codes@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
@@ -754,7 +794,7 @@ chai@^4.2.0:
pathval "^1.1.0"
type-detect "^4.0.5"

chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.1:
chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.1:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -873,7 +913,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
dependencies:
delayed-stream "~1.0.0"

commander@^2.19.0, commander@^2.20.0:
commander@^2.12.1, commander@^2.19.0, commander@^2.20.0:
version "2.20.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
@@ -1193,7 +1233,7 @@ detect-libc@^1.0.2:
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=

diff@3.5.0, diff@^3.1.0:
diff@3.5.0, diff@^3.1.0, diff@^3.2.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
@@ -1207,6 +1247,14 @@ diffie-hellman@^5.0.0:
miller-rabin "^4.0.0"
randombytes "^2.0.0"

doctrine@0.7.2:
version "0.7.2"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-0.7.2.tgz#7cb860359ba3be90e040b26b729ce4bfa654c523"
integrity sha1-fLhgNZujvpDgQLJrcpzkv6ZUxSM=
dependencies:
esutils "^1.1.6"
isarray "0.0.1"

domain-browser@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
@@ -1354,6 +1402,16 @@ estraverse@^4.1.0, estraverse@^4.1.1:
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=

esutils@^1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-1.1.6.tgz#c01ccaa9ae4b897c6d0c3e210ae52f3c7a844375"
integrity sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U=

esutils@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=

etag@~1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
@@ -1715,7 +1773,7 @@ get-caller-file@^1.0.1:
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==

get-caller-file@^2.0.1:
get-caller-file@^2.0.0, get-caller-file@^2.0.1:
version "2.0.5"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
@@ -1774,7 +1832,7 @@ glob@7.1.3:
once "^1.3.0"
path-is-absolute "^1.0.0"

glob@^7.1.3:
glob@^7.1.1, glob@^7.1.3:
version "7.1.4"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==
@@ -2048,6 +2106,11 @@ interpret@^1.1.0, interpret@^1.2.0:
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==

inversify@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/inversify/-/inversify-5.0.1.tgz#500d709b1434896ce5a0d58915c4a4210e34fb6e"
integrity sha512-Ieh06s48WnEYGcqHepdsJUIJUXpwH5o5vodAX+DK2JA/gjy4EbEcQZxw+uFfzysmKjiLXGYwNG3qDZsKVMcINQ==

invert-kv@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
@@ -2291,6 +2354,11 @@ is-wsl@^1.1.0:
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=

isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=

isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@@ -2318,7 +2386,12 @@ isstream@~0.1.2:
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=

js-yaml@3.13.1:
js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==

js-yaml@3.13.1, js-yaml@^3.13.1:
version "3.13.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
@@ -3554,6 +3627,11 @@ rechoir@^0.6.2:
dependencies:
resolve "^1.1.6"

reflect-metadata@^0.1.12:
version "0.1.13"
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==

regenerator-runtime@^0.13.2:
version "0.13.2"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz#32e59c9a6fb9b1a4aff09b4930ca2d4477343447"
@@ -3670,6 +3748,13 @@ resolve@^1.1.6, resolve@^1.1.7:
dependencies:
path-parse "^1.0.6"

resolve@^1.3.2:
version "1.11.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.1.tgz#ea10d8110376982fef578df8fc30b9ac30a07a3e"
integrity sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==
dependencies:
path-parse "^1.0.6"

ret@~0.1.10:
version "0.1.15"
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
@@ -4274,11 +4359,95 @@ tsconfig-paths@^3.8.0:
minimist "^1.2.0"
strip-bom "^3.0.0"

tslib@1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8"
integrity sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==

tslib@^1.7.1, tslib@^1.8.0, tslib@^1.8.1:
version "1.10.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==

tslib@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==

tslint-config-airbnb@^5.11.1:
version "5.11.1"
resolved "https://registry.yarnpkg.com/tslint-config-airbnb/-/tslint-config-airbnb-5.11.1.tgz#51a27fbb8bf24c144d064a274a71da47e7ece617"
integrity sha512-hkaittm2607vVMe8eotANGN1CimD5tor7uoY3ypg2VTtEcDB/KGWYbJOz58t8LI4cWSyWtgqYQ5F0HwKxxhlkQ==
dependencies:
tslint-consistent-codestyle "^1.14.1"
tslint-eslint-rules "^5.4.0"
tslint-microsoft-contrib "~5.2.1"

tslint-consistent-codestyle@^1.14.1:
version "1.15.1"
resolved "https://registry.yarnpkg.com/tslint-consistent-codestyle/-/tslint-consistent-codestyle-1.15.1.tgz#a0c5cd5a5860d40b659c490d8013c5732e02af8c"
integrity sha512-38Y3Dz4zcABe/PlPAQSGNEWPGVq0OzcIQR7SEU6dNujp/SgvhxhJOhIhI9gY4r0I3/TNtvVQwARWor9O9LPZWg==
dependencies:
"@fimbul/bifrost" "^0.17.0"
tslib "^1.7.1"
tsutils "^2.29.0"

tslint-eslint-rules@^5.4.0:
version "5.4.0"
resolved "https://registry.yarnpkg.com/tslint-eslint-rules/-/tslint-eslint-rules-5.4.0.tgz#e488cc9181bf193fe5cd7bfca213a7695f1737b5"
integrity sha512-WlSXE+J2vY/VPgIcqQuijMQiel+UtmXS+4nvK4ZzlDiqBfXse8FAvkNnTcYhnQyOTW5KFM+uRRGXxYhFpuBc6w==
dependencies:
doctrine "0.7.2"
tslib "1.9.0"
tsutils "^3.0.0"

tslint-microsoft-contrib@~5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/tslint-microsoft-contrib/-/tslint-microsoft-contrib-5.2.1.tgz#a6286839f800e2591d041ea2800c77487844ad81"
integrity sha512-PDYjvpo0gN9IfMULwKk0KpVOPMhU6cNoT9VwCOLeDl/QS8v8W2yspRpFFuUS7/c5EIH/n8ApMi8TxJAz1tfFUA==
dependencies:
tsutils "^2.27.2 <2.29.0"

tslint@^5.18.0:
version "5.18.0"
resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.18.0.tgz#f61a6ddcf372344ac5e41708095bbf043a147ac6"
integrity sha512-Q3kXkuDEijQ37nXZZLKErssQVnwCV/+23gFEMROi8IlbaBG6tXqLPQJ5Wjcyt/yHPKBC+hD5SzuGaMora+ZS6w==
dependencies:
"@babel/code-frame" "^7.0.0"
builtin-modules "^1.1.1"
chalk "^2.3.0"
commander "^2.12.1"
diff "^3.2.0"
glob "^7.1.1"
js-yaml "^3.13.1"
minimatch "^3.0.4"
mkdirp "^0.5.1"
resolve "^1.3.2"
semver "^5.3.0"
tslib "^1.8.0"
tsutils "^2.29.0"

"tsutils@^2.27.2 <2.29.0":
version "2.28.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.28.0.tgz#6bd71e160828f9d019b6f4e844742228f85169a1"
integrity sha512-bh5nAtW0tuhvOJnx1GLRn5ScraRLICGyJV5wJhtRWOLsxW70Kk5tZtpK3O/hW6LDnqKS9mlUMPZj9fEMJ0gxqA==
dependencies:
tslib "^1.8.1"

tsutils@^2.29.0:
version "2.29.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99"
integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==
dependencies:
tslib "^1.8.1"

tsutils@^3.0.0, tsutils@^3.5.0:
version "3.14.1"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.14.1.tgz#f1d2b93d2a0876481f2f1f98c25ba42bbd7ee860"
integrity sha512-kiuZzD1uUA5DxGj/uxbde+ymp6VVdAxdzOIlAFbYKrPyla8/uiJ9JLBm1QsPhOm4Muj0/+cWEDP99yoCUcSl6Q==
dependencies:
tslib "^1.8.1"

tty-browserify@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"


Loading…
Cancel
Save