Browse Source

feat: Stream Keys, Channels, Activities

develop
Dale 2 years 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


+ 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"