diff --git a/package.json b/package.json index 23c9e9c..6962c31 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,17 @@ "scripts": { "tsc": "tsc --p src/", "tsc-watch": "tsc --p src/ -w", - "start": "npm run build.dev && nodemon --watch build build/index.js", - "build.dev": "gulp build", + "start": "npm run build.dev && concurrently \"npm run tsc-watch\" \"nodemon --watch build build/index.js\"", + "build.dev": "rimraf build && gulp build", "serve.dev": "node build/index.js", "lint": "gulp tslint", - "test": "rimraf igo-test.db && gulp tslint && NODE_ENV=test tape ./build/**/*.test.js | tap-spec", - "initDB": "rimraf igo.db && tape ./build/**/*.test.js | tap-spec", + "test": "npm run build.dev && rimraf igo-test.db && gulp tslint && NODE_ENV=test tape ./build/**/*.test.js | tap-spec", + "testUser": "npm run build.dev && rimraf igo-test.db && gulp tslint && NODE_ENV=test tape ./build/user/*.test.js | tap-spec", + "testBookmark": "npm run build.dev && rimraf igo-test.db && gulp tslint && NODE_ENV=test tape ./build/bookmark/*.test.js | tap-spec", + "testTool": "npm run build.dev && rimraf igo-test.db && gulp tslint && NODE_ENV=test tape ./build/tool*/*.test.js | tap-spec", + "testLayer": "npm run build.dev && rimraf igo-test.db && gulp tslint && NODE_ENV=test tape ./build/layer*/*.test.js | tap-spec", + "testContext": "npm run build.dev && rimraf igo-test.db && gulp tslint && NODE_ENV=test tape ./build/context*/*.test.js | tap-spec", + "initDB": "npm run build.dev && rimraf igo.db && node build/initDB.js", "coverage": "npm run build.coverage && npm run serve.coverage", "build.coverage": "NODE_ENV=test istanbul cover tape ./build/**/*.test.js", "serve.coverage": "node_modules/live-server/live-server.js ./coverage/lcov-report/ --port=4210 --no-browser" @@ -43,10 +48,13 @@ "inert": "^4.1.0", "joi": "^10.1.0", "jsonwebtoken": "^7.2.1", + "ldapjs": "^1.0.1", "nconf": "^0.8.4", "path": "^0.12.7", "pg": "^6.1.2", "pg-hstore": "^2.3.2", + "request": "^2.81.0", + "rxjs": "^5.4.1", "sequelize": "^3.30.2", "sqlite3": "^3.1.8", "vision": "^4.1.1", @@ -63,6 +71,7 @@ "@types/nconf": "0.0.33", "@types/node": "^6.0.55", "@types/sequelize": "^4.0.40", + "concurrently": "^3.5.0", "gulp": "^3.9.1", "gulp-env": "^0.4.0", "gulp-mocha": "^4.1.0", diff --git a/src/configurations/config.dev.json b/src/configurations/config.dev.json index d853359..f30aeef 100644 --- a/src/configurations/config.dev.json +++ b/src/configurations/config.dev.json @@ -6,13 +6,19 @@ }, "server": { "port": 5000, - "jwtSecret": "random-secret-password", + "adminProfil": "admin", "jwtExpiration": "1h", + "userApi": { + "host": "localhost", + "port": 8001 + }, "plugins": [ "logger", - "jwt-auth", "sanitizer", "swagger" ] + }, + "test": { + "xConsumerId": "" } } diff --git a/src/configurations/config.test.json b/src/configurations/config.test.json index 795aec6..509e694 100644 --- a/src/configurations/config.test.json +++ b/src/configurations/config.test.json @@ -6,13 +6,19 @@ }, "server": { "port": 5000, - "jwtSecret": "random-secret-password", + "adminProfil": "admin", "jwtExpiration": "1h", + "userApi": { + "host": "localhost", + "port": 8001 + }, "plugins": [ "logger", - "jwt-auth", "sanitizer", "swagger" ] + }, + "test": { + "xConsumerId": "" } } diff --git a/src/configurations/index.ts b/src/configurations/index.ts index 8e91360..65cff30 100644 --- a/src/configurations/index.ts +++ b/src/configurations/index.ts @@ -11,11 +11,18 @@ const configs = new nconf.Provider({ } }); -export interface IServerConfigurations { +interface IUserApiConfiguration { + host: string; + port: number; +} + +export interface IServerConfiguration { port: number; plugins: Array; - jwtSecret: string; jwtExpiration: string; + userApi: IUserApiConfiguration; + googleKey?: string; + adminProfil?: string; } interface IDatabaseConfiguration { @@ -31,11 +38,26 @@ export interface IPostgresConfiguration extends IDatabaseConfiguration { } export type IDataConfiguration = ISqliteConfiguration | IPostgresConfiguration; +export interface IConsumer { + xConsumerId: string; + xConsumerUsername: string; +} + +export interface ITestConfiguration { + admin: IConsumer; + anonyme: IConsumer; + user1: IConsumer; + user2: IConsumer; +} export function getDatabaseConfig(): IDataConfiguration { - return configs.get('database'); + return configs.get('database'); +} + +export function getServerConfig(): IServerConfiguration { + return configs.get('server'); } -export function getServerConfigs(): IServerConfigurations { - return configs.get('server'); +export function getTestConfig(): ITestConfiguration { + return configs.get('test'); } diff --git a/src/context/context.controller.ts b/src/context/context.controller.ts new file mode 100644 index 0000000..332af93 --- /dev/null +++ b/src/context/context.controller.ts @@ -0,0 +1,332 @@ +import * as Hapi from 'hapi'; +import * as Boom from 'boom'; + +import { IDatabase, database } from '../database'; +import { ObjectUtils, uuid } from '../utils'; + +import { User } from '../user'; +import { TypePermission, ContextPermission } from '../contextPermission'; +import { ToolContext } from '../toolContext'; +import { LayerContext } from '../layerContext'; + +import { Context, ContextInstance, Scope } from './index'; + + +export class ContextController { + + private database: IDatabase = database; + private context: Context; + private contextPermission: ContextPermission; + private toolContext: ToolContext; + private layerContext: LayerContext; + + constructor() { + this.contextPermission = new ContextPermission(); + this.context = new Context(); + this.toolContext = new ToolContext(); + this.layerContext = new LayerContext(); + } + + public create(request: Hapi.Request, reply: Hapi.IReply) { + const newContext = request.payload; + newContext.owner = request.headers['x-consumer-username']; + + this.context.create(newContext).subscribe( + (context: ContextInstance) => { + if (newContext.tools) { + this.toolContext.bulkCreate(context.id, newContext.tools) + .subscribe( + () => { + newContext.tools = undefined; + if (!newContext.layers) { + reply(context).code(201); + } + }, + (error) => { + newContext.tools = undefined; + if (!newContext.layers) { + reply(context).code(201); + } + } + ); + } + if (newContext.layers) { + this.layerContext.bulkCreate(context.id, newContext.layers, true) + .subscribe( + () => { + newContext.layers = undefined; + if (!newContext.tools) { + reply(context).code(201); + } + }, + (error) => { + newContext.layers = undefined; + if (!newContext.tools) { + reply(context).code(201); + } + } + ); + } + + if (!newContext.tools && !newContext.layers) { + reply(context).code(201); + } + + }, + (error: Boom.BoomError) => reply(error) + ); + } + + public clone(request: Hapi.Request, reply: Hapi.IReply) { + const id = request.params['contextId']; + let properties = request.payload; + if (typeof request.payload === 'string') { + properties = JSON.parse(request.payload); + } + + this.context.getById(id, true, true).subscribe( + (context: any) => { + Object.assign(context, properties); + const newContext = { + scope: Scope[Scope.private], + uri: uuid(), + title: context.title, + icon: context.icon, + map: context.map, + tools: context.tools, + layers: context.layers + }; + request.payload = newContext; + this.create(request, reply); + }, + (error: Boom.BoomError) => reply(error) + ); + } + + public update(request: Hapi.Request, reply: Hapi.IReply) { + const id = request.params['contextId']; + const newContext = request.payload; + + this.context.update(id, newContext).subscribe( + (context: ContextInstance) => { + + if (newContext.tools) { + this.toolContext.deleteByContextId(context.id).subscribe( + (rep) => { + this.toolContext.bulkCreate(context.id, newContext.tools) + .subscribe( + () => { + newContext.tools = undefined; + if (!newContext.layers) { + reply(context); + } + }, + (error) => { + newContext.tools = undefined; + if (!newContext.layers) { + reply(context); + } + } + ); + }, + (error: Boom.BoomError) => reply(error) + ); + } + if (newContext.layers) { + this.layerContext.deleteByContextId(context.id).subscribe( + (rep) => { + this.layerContext.bulkCreate(context.id, newContext.layers, true) + .subscribe( + () => { + newContext.layers = undefined; + if (!newContext.tools) { + reply(context); + } + }, + (error) => { + newContext.layers = undefined; + if (!newContext.tools) { + reply(context); + } + } + ); + }, + (error: Boom.BoomError) => reply(error) + ); + } + + if (!newContext.tools && !newContext.layer) { + reply(context); + } + + }, + (error: Boom.BoomError) => reply(error) + ); + } + + public delete(request: Hapi.Request, reply: Hapi.IReply) { + const id = request.params['contextId']; + this.context.delete(id).subscribe( + (context: ContextInstance) => reply(context).code(204), + (error: Boom.BoomError) => reply(error) + ); + } + + public getById(request: Hapi.Request, reply: Hapi.IReply) { + const owner = request.headers['x-consumer-username']; + const id = request.params['contextId']; + + this.context.getById(id).subscribe( + (context: ContextInstance) => { + this.contextPermission.getPermission(context, owner).subscribe( + (permission) => { + if (permission) { + context.permission = TypePermission[permission]; + reply(context); + } else { + reply(Boom.unauthorized()); + } + } + ); + }, + (error: Boom.BoomError) => reply(error) + ); + } + + public get(request: Hapi.Request, reply: Hapi.IReply) { + const owner = request.headers['x-consumer-username']; + const isAnonyme = request.headers['x-anonymous-consumer']; + const id = request.headers['x-consumer-id']; + + User.getProfils(id).subscribe((profils) => { + profils = profils || []; + if (owner) { + profils.push(owner); + } + + const promises = []; + if (owner && !isAnonyme) { + promises.push(this.database.context.findAll({ + where: { + owner: owner + } + })); + } else { + promises.push(new Promise(resolve => resolve())); + } + + if (profils && profils.length) { + promises.push(this.database.context.findAll({ + include: [{ + model: this.database.contextPermission, + where: { + profil: profils + } + }], + where: { + scope: 'protected', + owner: { + $ne: owner + } + } + })); + } else { + promises.push(new Promise(resolve => resolve())); + } + + promises.push(this.database.context.findAll({ + include: [{ + model: this.database.contextPermission, + required: false, + where: { + profil: profils + } + }], + where: { + scope: 'public', + owner: { + $ne: owner + } + } + })); + + Promise.all(promises) + .then((repPromises: Array>) => { + const oursPromises = repPromises[0] || []; + const sharedPromises = repPromises[1] || []; + const publicPromises = repPromises[2] || []; + + const oursContexts = oursPromises.map( + (c) => { + const plainC = c.get(); + plainC.permission = TypePermission[TypePermission.write]; + return ObjectUtils.removeNull(plainC); + } + ); + const sharedContexts = sharedPromises.map( + (c) => { + const plainC = c.get(); + + plainC.permission = TypePermission[TypePermission.read]; + for (const cp of plainC['contextPermissions']) { + const typePerm: any = cp.typePermission; + if (typePerm === TypePermission[TypePermission.write]) { + plainC.permission = TypePermission[TypePermission.write]; + break; + } + } + + delete plainC['contextPermissions']; + return ObjectUtils.removeNull(plainC); + } + ); + const publicContexts = publicPromises.map( + (c) => { + const plainC = c.get(); + plainC.permission = TypePermission[TypePermission.read]; + for (const cp of plainC['contextPermissions']) { + const typePerm: any = cp.typePermission; + if (typePerm === TypePermission[TypePermission.write]) { + plainC.permission = TypePermission[TypePermission.write]; + break; + } + } + + delete plainC['contextPermissions']; + return ObjectUtils.removeNull(plainC); + } + ); + + const contexts = { + ours: oursContexts, + shared: sharedContexts, + public: publicContexts + }; + + reply(contexts); + }).catch((error) => { + reply(Boom.badImplementation(error)); + }); + }); + } + + public getDetailsById(request: Hapi.Request, reply: Hapi.IReply) { + const owner = request.headers['x-consumer-username']; + const id = request.params['contextId']; + + this.context.getById(id, true, true).subscribe((contextDetails: any) => { + this.contextPermission.getPermission(contextDetails, owner).subscribe( + (permission) => { + if (permission) { + contextDetails.permission = TypePermission[permission]; + reply(contextDetails); + } else { + reply(Boom.unauthorized()); + } + }, + (error: Boom.BoomError) => reply(error) + ); + }); + } + +} diff --git a/src/context/context.model.ts b/src/context/context.model.ts new file mode 100644 index 0000000..ad4fe51 --- /dev/null +++ b/src/context/context.model.ts @@ -0,0 +1,105 @@ +import * as Sequelize from 'sequelize'; + +import { TypePermission } from '../contextPermission'; + +export enum Scope { + public, + protected, + private +} + +interface Map { + view: { + center: [number, number]; + zoom: number; + projection: string; + }; +}; + +export interface IContext { + id?: string; + uri: string; + scope: Scope; + title: string; + icon: string; + map: Map; + owner: string; + permission?: TypePermission | string; +}; + +export interface ContextInstance extends Sequelize.Instance { + id: string; + createdAt: Date; + updatedAt: Date; + + uri: string; + scope: Scope; + title: string; + icon: string; + map: string; + owner: string; + permission?: TypePermission | string; +} + + +export interface ContextDetailed extends ContextInstance { + tools?: any[]; + layers?: any[]; + toolbar?: string[]; +} + +export interface ContextModel + extends Sequelize.Model { } + +export default function define(sequelize: Sequelize.Sequelize, DataTypes) { + const context = sequelize.define('context', { + 'id': { + 'type': DataTypes.INTEGER, + 'allowNull': false, + 'primaryKey': true, + 'autoIncrement': true + }, + 'uri': { + 'type': DataTypes.STRING(64), + 'allowNull': false + }, + 'title': { + 'type': DataTypes.STRING(128), + 'allowNull': false + }, + 'icon': { + 'type': DataTypes.STRING(128) + }, + 'owner': { + 'type': DataTypes.STRING(128), + 'allowNull': false + }, + 'scope': { + 'type': DataTypes.ENUM('public', 'protected', 'private'), + 'allowNull': false + }, + 'map': { + 'type': DataTypes.TEXT, + 'get': function() { + const map = this.getDataValue('map'); + return map ? JSON.parse(map) : {}; + }, + 'set': function(val) { + this.setDataValue('map', JSON.stringify(val)); + } + } + }, + { + 'indexes': [{ + 'fields': ['scope'] + }, { + 'fields': ['owner'] + }], + 'tableName': 'context', + 'timestamps': true + }); + + context.sync(); + + return context; +} diff --git a/src/context/context.test.ts b/src/context/context.test.ts new file mode 100644 index 0000000..810ebb2 --- /dev/null +++ b/src/context/context.test.ts @@ -0,0 +1,829 @@ +import * as test from 'tape'; +import * as Server from '../server'; +import * as Configs from '../configurations'; + +const serverConfigs = Configs.getServerConfig(); +const testConfigs = Configs.getTestConfig(); +const anonyme = testConfigs.anonyme; +const admin = testConfigs.admin; +const user1 = testConfigs.user1; +const user2 = testConfigs.user2; + +Server.init(serverConfigs).then((server) => { + + test('POST /contexts - context 1 ', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + uri: 'user1Private', + title: 'user1Private', + scope: 'private', + map: { + view: { + center: [-73, 46] + } + } + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.uri, 'user1Private'); + t.equal(result.title, 'user1Private'); + t.equal(result.scope, 'private'); + t.equal(result.map.view.center[1], 46); + t.equal(result.owner, user1.xConsumerUsername); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts - context 2', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + uri: 'user2Private', + title: 'user2Private', + scope: 'private', + map: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts - context 3', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + uri: 'user2Public', + title: 'user2Public', + scope: 'public', + map: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts - context 4', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + uri: 'user2PublicWrite', + title: 'user2PublicWrite', + scope: 'public', + map: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts - context 5', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + uri: 'user2Protected', + title: 'user2Protected', + scope: 'protected', + map: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts - context 6', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + uri: 'user2ProtectedWrite', + title: 'user2ProtectedWrite', + scope: 'protected', + map: {} + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.uri, 'user2ProtectedWrite'); + t.equal(result.title, 'user2ProtectedWrite'); + t.equal(result.scope, 'protected'); + t.equal(result.owner, user2.xConsumerUsername); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts/4/permissions - before context', function(t) { + const options = { + method: 'POST', + url: '/contexts/4/permissions', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + typePermission: 'write', + profil: user1.xConsumerUsername + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts/6/permissions - before context', function(t) { + const options = { + method: 'POST', + url: '/contexts/6/permissions', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + typePermission: 'write', + profil: user1.xConsumerUsername + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts - anonyme', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': anonyme.xConsumerUsername, + 'x-consumer-id': anonyme.xConsumerId, + 'x-anonymous-consumer': 'true' + }, + payload: { + uri: 'user2ProtectedWrite', + title: 'user2ProtectedWrite', + scope: 'protected', + map: {} + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must be authenticated'); + t.equal(response.statusCode, 401); + server.stop(t.end); + }); + }); + + // ========================================================= + + test('POST /contexts/1/clone - context 1 ', function(t) { + const options = { + method: 'POST', + url: '/contexts/1/clone', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.title, 'user1Private'); + t.equal(result.scope, 'private'); + t.equal(result.owner, user1.xConsumerUsername); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + + test('POST /contexts/2/clone - context 2 ', function(t) { + const options = { + method: 'POST', + url: '/contexts/2/clone', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have read permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + + test('POST /contexts/3/clone - context 3 ', function(t) { + const options = { + method: 'POST', + url: '/contexts/3/clone', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.title, 'user2Public'); + t.equal(result.scope, 'private'); + t.equal(result.owner, user1.xConsumerUsername); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts/4/clone - context 4 ', function(t) { + const options = { + method: 'POST', + url: '/contexts/4/clone', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.title, 'user2PublicWrite'); + t.equal(result.scope, 'private'); + t.equal(result.owner, user1.xConsumerUsername); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts/5/clone - context 5 ', function(t) { + const options = { + method: 'POST', + url: '/contexts/5/clone', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have read permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('POST /contexts/6/clone - context 6 ', function(t) { + const options = { + method: 'POST', + url: '/contexts/6/clone', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.title, 'user2ProtectedWrite'); + t.equal(result.scope, 'private'); + t.equal(result.owner, user1.xConsumerUsername); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + // ========================================================= + + test('Patch /contexts/7 - context 7 ', function(t) { + const options = { + method: 'Patch', + url: '/contexts/7', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + title: 'user1PrivateClone', + scope: 'public', + map: { + view: { + zoom: 11 + } + } + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('Patch /contexts/1 - context 1 ', function(t) { + const options = { + method: 'Patch', + url: '/contexts/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + map: { + view: { + zoom: 13 + } + } + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('Patch /contexts/2 - context 2 ', function(t) { + const options = { + method: 'Patch', + url: '/contexts/2', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + map: { + view: { + zoom: 12 + } + } + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('Patch /contexts/3 - context 3 ', function(t) { + const options = { + method: 'Patch', + url: '/contexts/3', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + map: { + view: { + zoom: 3 + } + } + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('Patch /contexts/4 - context 4 ', function(t) { + const options = { + method: 'Patch', + url: '/contexts/4', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + map: { + view: { + zoom: 4 + } + } + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('Patch /contexts/5 - context 5 ', function(t) { + const options = { + method: 'Patch', + url: '/contexts/5', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + map: { + view: { + zoom: 5 + } + } + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('Patch /contexts/6 - context 6 ', function(t) { + const options = { + method: 'Patch', + url: '/contexts/6', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + map: { + view: { + zoom: 6 + } + } + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + // ========================================================= + + test('GET /contexts/7 - context 7 ', function(t) { + const options = { + method: 'GET', + url: '/contexts/7', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.title, 'user1PrivateClone'); + t.equal(result.scope, 'public'); + t.equal(result.map.view.zoom, 11); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + + test('DELETE /contexts/7 - context 7 ', function(t) { + const options = { + method: 'DELETE', + url: '/contexts/7', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 204); + server.stop(t.end); + }); + }); + + test('GET /contexts/7 - after delete ', function(t) { + const options = { + method: 'GET', + url: '/contexts/7', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + // ========================================================= + + test('GET /contexts/1/details - context 1 ', function(t) { + const options = { + method: 'GET', + url: '/contexts/1/details', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.title, 'user1Private'); + t.equal(result.scope, 'private'); + t.equal(result.map.view.zoom, 13); + t.equal(result.permission, 'write'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /contexts/2/details - context 2 ', function(t) { + const options = { + method: 'GET', + url: '/contexts/2/details', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have read permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('GET /contexts/3/details - context 3 ', function(t) { + const options = { + method: 'GET', + url: '/contexts/3/details', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.title, 'user2Public'); + t.equal(result.scope, 'public'); + t.equal(result.permission, 'read'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /contexts/4/details - context 4 ', function(t) { + const options = { + method: 'GET', + url: '/contexts/4/details', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.title, 'user2PublicWrite'); + t.equal(result.scope, 'public'); + t.equal(result.map.view.zoom, 4); + t.equal(result.permission, 'write'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /contexts/5/details - context 5 ', function(t) { + const options = { + method: 'GET', + url: '/contexts/5/details', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have read permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('GET /contexts/6/details - context 6 ', function(t) { + const options = { + method: 'GET', + url: '/contexts/6/details', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.title, 'user2ProtectedWrite'); + t.equal(result.scope, 'protected'); + t.equal(result.map.view.zoom, 6); + t.equal(result.permission, 'write'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + + test('GET /contexts/4/details - anonyme 4 ', function(t) { + const options = { + method: 'GET', + url: '/contexts/4/details', + headers: { + 'x-consumer-username': anonyme.xConsumerUsername, + 'x-consumer-id': anonyme.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.title, 'user2PublicWrite'); + t.equal(result.scope, 'public'); + t.equal(result.permission, 'read'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + + test('GET /contexts/6/details - anonyme 6 ', function(t) { + const options = { + method: 'GET', + url: '/contexts/6/details', + headers: { + 'x-consumer-username': anonyme.xConsumerUsername, + 'x-consumer-id': anonyme.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have read permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + + test('GET /contexts/4/details - admin 4 ', function(t) { + const options = { + method: 'GET', + url: '/contexts/4/details', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.title, 'user2PublicWrite'); + t.equal(result.scope, 'public'); + t.equal(result.permission, 'read'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + + test('GET /contexts/6/details - admin 6 ', function(t) { + const options = { + method: 'GET', + url: '/contexts/6/details', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have read permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + // ========================================================= + + test('GET /contexts - admin ', function(t) { + const options = { + method: 'GET', + url: '/contexts', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.ours.length, 0); + t.equal(result.shared.length, 0); + t.equal(result.public.length, 2); + t.equal(result.public[0].permission, 'read'); + t.equal(result.public[1].permission, 'read'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /contexts - anonyme ', function(t) { + const options = { + method: 'GET', + url: '/contexts', + headers: { + 'x-consumer-username': anonyme.xConsumerUsername, + 'x-consumer-id': anonyme.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.ours.length, 0); + t.equal(result.shared.length, 0); + t.equal(result.public.length, 2); + t.equal(result.public[0].permission, 'read'); + t.equal(result.public[1].permission, 'read'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /contexts - user1 ', function(t) { + const options = { + method: 'GET', + url: '/contexts', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + console.log(result); + t.equal(result.ours.length, 4); + t.equal(result.shared.length, 1); + t.equal(result.public.length, 2); + t.equal(result.ours[0].permission, 'write'); + t.equal(result.shared[0].permission, 'write'); + t.equal(result.public[0].permission, 'read'); + t.equal(result.public[1].permission, 'write'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /contexts - user2 ', function(t) { + const options = { + method: 'GET', + url: '/contexts', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.ours.length, 5); + t.equal(result.shared.length, 0); + t.equal(result.public.length, 0); + t.equal(result.ours[0].permission, 'write'); + t.equal(result.ours[2].permission, 'write'); + t.equal(result.ours[4].permission, 'write'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + +}); diff --git a/src/context/context.ts b/src/context/context.ts new file mode 100644 index 0000000..95507d7 --- /dev/null +++ b/src/context/context.ts @@ -0,0 +1,143 @@ +import * as Rx from 'rxjs'; +import * as Boom from 'boom'; + +import { IDatabase, database } from '../database'; +import { ObjectUtils } from '../utils'; + +import { IContext, ContextInstance, ContextDetailed } from './context.model'; + +export class Context { + + private database: IDatabase = database; + + constructor() {} + + public create(context: IContext): Rx.Observable { + return Rx.Observable.create(observer => { + this.database.context.create(context).then((createdContext) => { + observer.next(createdContext); + observer.complete(); + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public update(id: string, context: IContext): Rx.Observable { + return Rx.Observable.create(observer => { + this.database.context.update(context, { + where: { + id: id + } + }).then((count: [number, ContextInstance[]]) => { + if (count[0]) { + observer.next({id: id}); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public delete(id: string): Rx.Observable<{}> { + return Rx.Observable.create(observer => { + this.database.context.destroy({ + where: { + id: id + } + }).then((count: number) => { + if (count) { + observer.next({}); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public get(): Rx.Observable { + return Rx.Observable.create(observer => { + this.database.context.findAll().then((contexts: ContextInstance[]) => { + const plainContexts = contexts.map( + (context) => ObjectUtils.removeNull(context.get()) + ); + observer.next(plainContexts); + observer.complete(); + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public getById(id: string, includeLayers = false, + includeTools = false): Rx.Observable { + + const include = []; + if (includeLayers) { include.push(this.database.layer); } + if (includeTools) { include.push(this.database.tool); } + + return Rx.Observable.create(observer => { + this.database.context.findOne({ + include: include, + where: { + id: id + } + }).then((context: ContextDetailed) => { + if (context) { + if (includeLayers || includeTools) { + const plainDetails = this.contextObjToPlainObj(context); + observer.next(plainDetails); + } else { + observer.next(ObjectUtils.removeNull(context.get())); + } + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + private contextObjToPlainObj(context): ContextDetailed { + + let plain: any = context.get(); + plain.layers = []; + plain.tools = []; + plain.toolbar = []; + + for (const tool of context.tools) { + const plainTool = tool.get(); + Object.assign(plainTool.options, plainTool.toolContext.options); + plainTool.toolContext = null; + plain.tools.push(plainTool); + if (plainTool.inToolbar) { + plain.toolbar.push(plainTool.name); + } + } + + for (const layer of context.layers) { + const plainLayer = layer.get(); + Object.assign(plainLayer.view, plainLayer.layerContext.view); + Object.assign(plainLayer.source, plainLayer.layerContext.source); + plainLayer.order = plainLayer.layerContext.order; + plainLayer.layerContext = null; + plain.layers.push(plainLayer); + } + + plain = ObjectUtils.removeNull(plain); + plain.layers = plain.layers.sort( + (a, b) => a.order < b.order ? -1 : a.order > b.order ? 1 : 0 + ); + + return plain; + } + +} diff --git a/src/context/context.validator.ts b/src/context/context.validator.ts new file mode 100644 index 0000000..607b81b --- /dev/null +++ b/src/context/context.validator.ts @@ -0,0 +1,60 @@ +import * as Joi from 'joi'; + +import { ToolValidator } from '../tool/tool.validator'; +import { LayerValidator } from '../layer/layer.validator'; + +const createToolModel = ToolValidator.createModel; +const updateToolModel = ToolValidator.updateModel + .keys({id: Joi.string()}); + +const createLayerModel = LayerValidator.createModel + .keys({order: Joi.number()}); +const updateLayerModel = LayerValidator.updateModel + .keys({ + id: Joi.string(), + order: Joi.number() + }); + +export class ContextValidator { + + static createModel = Joi.object().keys({ + scope: Joi.string().required().valid('public', 'protected', 'private'), + uri: Joi.string().required(), + title: Joi.string().required(), + icon: Joi.string().allow(''), + map: Joi.object().required().keys({ + view: Joi.object().keys({ + center: Joi.array().length(2).items(Joi.number()), + zoom: Joi.number(), + projection: Joi.string() + }) + }), + layers: Joi.array().items( + Joi.alternatives().try(createLayerModel, updateLayerModel) + ), + tools: Joi.array().items( + Joi.alternatives().try(createToolModel, updateToolModel) + ) + }); + + static updateModel = Joi.object().keys({ + scope: Joi.string().valid('public', 'protected', 'private'), + uri: Joi.string(), + title: Joi.string(), + icon: Joi.string().allow(''), + map: Joi.object().keys({ + view: Joi.object().keys({ + center: Joi.array().length(2).items(Joi.number()), + zoom: Joi.number(), + projection: Joi.string() + }) + }), + layers: Joi.array().items( + Joi.alternatives().try(createLayerModel, updateLayerModel) + ), + tools: Joi.array().items( + Joi.alternatives().try(createToolModel, updateToolModel) + ) + }); + +} diff --git a/src/context/index.ts b/src/context/index.ts new file mode 100644 index 0000000..4f72ad5 --- /dev/null +++ b/src/context/index.ts @@ -0,0 +1,9 @@ +import * as Hapi from 'hapi'; +import Routes from './routes'; + +export function init(server: Hapi.Server) { + Routes(server); +} + +export * from './context.model'; +export * from './context'; diff --git a/src/context/routes.ts b/src/context/routes.ts new file mode 100644 index 0000000..97ed1a4 --- /dev/null +++ b/src/context/routes.ts @@ -0,0 +1,200 @@ +import * as Hapi from 'hapi'; +import * as Joi from 'joi'; + +import { ContextController } from './context.controller'; +import { ContextValidator } from './context.validator'; +import { ContextPermissionValidator } from '../contextPermission'; +import { UserValidator } from '../user/user.validator'; + +export default function(server: Hapi.Server) { + + const contextController = new ContextController(); + server.bind(contextController); + + server.route({ + method: 'GET', + path: '/contexts/{contextId}', + config: { + handler: contextController.getById, + tags: ['api', 'contexts'], + description: 'Get contexts by id.', + validate: { + params: { + contextId: Joi.string().required() + }, + headers: ContextPermissionValidator.readPermission + }, + plugins: { + 'hapi-swagger': { + responses: { + '200': { + description: 'Context founded.' + }, + '404': { + description: 'Context does not exists.' + } + } + } + } + } + }); + + server.route({ + method: 'GET', + path: '/contexts/{contextId}/details', + config: { + handler: contextController.getDetailsById, + tags: ['api', 'tools', 'layers', 'contexts'], + description: 'Get details of context by context id.', + validate: { + params: { + contextId: Joi.string().required() + }, + headers: ContextPermissionValidator.readPermission + }, + plugins: { + 'hapi-swagger': { + responses: { + '200': { + description: 'Context founded.' + }, + '404': { + description: 'Context does not exists.' + } + } + } + } + } + }); + + server.route({ + method: 'GET', + path: '/contexts', + config: { + handler: contextController.get, + auth: false, + tags: ['api', 'contexts'], + description: 'Get all contexts.', + validate: { + headers: UserValidator.userValidator + } + } + }); + + server.route({ + method: 'DELETE', + path: '/contexts/{contextId}', + config: { + handler: contextController.delete, + tags: ['api', 'contexts'], + description: 'Delete context by id.', + validate: { + params: { + contextId: Joi.string().required() + }, + headers: ContextPermissionValidator.writePermission + }, + plugins: { + 'hapi-swagger': { + responses: { + '204': { + description: 'Deleted Context.', + }, + '401': { + description: 'Must be authenticated' + }, + '404': { + description: 'Context does not exists.' + } + } + } + } + } + }); + + server.route({ + method: 'PATCH', + path: '/contexts/{contextId}', + config: { + handler: contextController.update, + tags: ['api', 'contexts'], + description: 'Update context by id.', + validate: { + params: { + contextId: Joi.string().required() + }, + payload: ContextValidator.updateModel, + headers: ContextPermissionValidator.writePermission + }, + plugins: { + 'hapi-swagger': { + responses: { + '200': { + description: 'Deleted Context.', + }, + '401': { + description: 'Must be authenticated' + }, + '404': { + description: 'Context does not exists.' + } + } + } + } + } + }); + + server.route({ + method: 'POST', + path: '/contexts', + config: { + handler: contextController.create, + tags: ['api', 'contexts'], + description: 'Create a context.', + validate: { + payload: ContextValidator.createModel, + headers: UserValidator.authenticateValidator + }, + plugins: { + 'hapi-swagger': { + responses: { + '201': { + description: 'Created Context.' + }, + '401': { + description: 'Must be authenticated' + } + } + } + } + } + }); + + server.route({ + method: 'POST', + path: '/contexts/{contextId}/clone', + config: { + handler: contextController.clone, + tags: ['api', 'contexts'], + description: 'Clone a context.', + validate: { + params: { + contextId: Joi.string().required() + }, + headers: ContextPermissionValidator.readPermission + }, + plugins: { + 'hapi-swagger': { + responses: { + '201': { + description: 'Cloned context.' + }, + '401': { + description: 'Must be authenticated' + } + } + } + } + } + }); +} diff --git a/src/contextPermission/contextPermission.controller.ts b/src/contextPermission/contextPermission.controller.ts new file mode 100644 index 0000000..d4c3107 --- /dev/null +++ b/src/contextPermission/contextPermission.controller.ts @@ -0,0 +1,58 @@ +import * as Hapi from 'hapi'; +import * as Boom from 'boom'; + +import { + IContextPermission, + ContextPermissionInstance, + ContextPermission +} from './index'; + +export class ContextPermissionController { + + private contextPermission: ContextPermission; + + constructor() { + this.contextPermission = new ContextPermission(); + } + + public create(request: Hapi.Request, reply: Hapi.IReply) { + const newContextPermission: IContextPermission = request.payload; + newContextPermission['contextId'] = request.params['contextId']; + + this.contextPermission.create(newContextPermission).subscribe( + (cp: ContextPermissionInstance) => reply(cp).code(201), + (error: Boom.BoomError) => reply(error) + ); + } + + public update(request: Hapi.Request, reply: Hapi.IReply) { + const id = request.params['id']; + const newContextPermission: IContextPermission = request.payload; + + this.contextPermission.update(id, newContextPermission).subscribe( + (cp: ContextPermissionInstance) => reply(cp), + (error: Boom.BoomError) => reply(error) + ); + } + + public delete(request: Hapi.Request, reply: Hapi.IReply) { + const id = request.params['id']; + + this.contextPermission.delete(id).subscribe( + (cp: ContextPermissionInstance) => reply(cp).code(204), + (error: Boom.BoomError) => reply(error) + ); + } + + public getByContextId( + request: Hapi.Request, reply: Hapi.IReply) { + + const contextId = request.params['contextId']; + + this.contextPermission.getByContextId(contextId).subscribe( + (cp: ContextPermissionInstance[]) => reply(cp), + (error: Boom.BoomError) => reply(error) + ); + } + +} diff --git a/src/contextPermission/contextPermission.model.ts b/src/contextPermission/contextPermission.model.ts new file mode 100644 index 0000000..0f99be5 --- /dev/null +++ b/src/contextPermission/contextPermission.model.ts @@ -0,0 +1,78 @@ +import * as Sequelize from 'sequelize'; + +export enum TypePermission { + null, + read, + write +} + +export interface IContextPermission { + id?: string; + typePermission: TypePermission; + profil: string; + contextId: string; +}; + +export interface ContextPermissionInstance + extends Sequelize.Instance { + id: string; + createdAt: Date; + updatedAt: Date; + + profil: string; + contextId: string; + typePermission: TypePermission; +} + +export interface ContextPermissionModel + extends Sequelize.Model { } + +export default function define(sequelize: Sequelize.Sequelize, DataTypes) { + const contextPermission = + sequelize.define( + 'contextPermission', { + 'id': { + 'type': DataTypes.INTEGER, + 'allowNull': false, + 'primaryKey': true, + 'autoIncrement': true + }, + 'typePermission': { + 'type': DataTypes.ENUM('read', 'write'), + 'allowNull': false + }, + 'profil': { + 'type': DataTypes.STRING, + 'allowNull': false + }, + 'contextId': { + 'type': DataTypes.INTEGER + } + }, + { + 'indexes': [{ + 'unique': true, + 'fields': ['contextId', 'profil'] + }, { + 'fields': ['contextId'] + }, { + 'fields': ['profil'] + }], + 'tableName': 'contextPermission', + 'timestamps': true + } + ); + + const context = sequelize.models['context']; + + context.hasMany(contextPermission, { + foreignKey: { + name: 'contextId', + allowNull: false + } + }); + + contextPermission.sync(); + + return contextPermission; +} diff --git a/src/contextPermission/contextPermission.test.ts b/src/contextPermission/contextPermission.test.ts new file mode 100644 index 0000000..1f33c27 --- /dev/null +++ b/src/contextPermission/contextPermission.test.ts @@ -0,0 +1,719 @@ +import * as test from 'tape'; +import * as Server from '../server'; +import * as Configs from '../configurations'; + +const serverConfigs = Configs.getServerConfig(); +const testConfigs = Configs.getTestConfig(); +const user1 = testConfigs.user1; +const user2 = testConfigs.user2; + +Server.init(serverConfigs).then((server) => { + + test('POST /contexts - before permissionContext - ', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + uri: 'user1Private', + title: 'user1Private', + scope: 'private', + map: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts - before permissionContext - context 2', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + uri: 'user2Private', + title: 'user2Private', + scope: 'private', + map: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts - before permissionContext - context 3', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + uri: 'user2Public', + title: 'user2Public', + scope: 'public', + map: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts - before permissionContext - context 4', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + uri: 'user2PublicWrite', + title: 'user2PublicWrite', + scope: 'public', + map: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts - before permissionContext - context 5', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + uri: 'user2Protected', + title: 'user2Protected', + scope: 'protected', + map: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts - before permissionContext - context 6', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + uri: 'user2ProtectedWrite', + title: 'user2ProtectedWrite', + scope: 'protected', + map: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts/{id}/permissions - before permContext', function(t) { + const options = { + method: 'POST', + url: '/contexts/4/permissions', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + typePermission: 'write', + profil: user1.xConsumerUsername + } + }; + server.inject(options, function(response) { + server.stop(t.end); + }); + }); + + test('POST /contexts/{id}/permissions - before permContext', function(t) { + const options = { + method: 'POST', + url: '/contexts/6/permissions', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + typePermission: 'write', + profil: user1.xConsumerUsername + } + }; + server.inject(options, function(response) { + server.stop(t.end); + }); + }); + + // =========================================================== + + test('POST /contexts/1/permissions - user1', function(t) { + const options = { + method: 'POST', + url: '/contexts/1/permissions', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + typePermission: 'read', + profil: 'test' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.profil, 'test'); + t.equal(result.typePermission, 'read'); + t.equal(Number(result.contextId), 1); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts/2/permissions - user1', function(t) { + const options = { + method: 'POST', + url: '/contexts/2/permissions', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + typePermission: 'read', + profil: 'test' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('POST /contexts/3/permissions - user1', function(t) { + const options = { + method: 'POST', + url: '/contexts/3/permissions', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + typePermission: 'read', + profil: 'test' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('POST /contexts/4/permissions - user1', function(t) { + const options = { + method: 'POST', + url: '/contexts/4/permissions', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + typePermission: 'read', + profil: 'test' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.profil, 'test'); + t.equal(result.typePermission, 'read'); + t.equal(Number(result.contextId), 4); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts/5/permissions - user1', function(t) { + const options = { + method: 'POST', + url: '/contexts/5/permissions', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + typePermission: 'read', + profil: 'test' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('POST /contexts/6/permissions - user1', function(t) { + const options = { + method: 'POST', + url: '/contexts/6/permissions', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + typePermission: 'write', + profil: 'test' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.profil, 'test'); + t.equal(result.typePermission, 'write'); + t.equal(Number(result.contextId), 6); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts/10/permissions - user1', function(t) { + const options = { + method: 'POST', + url: '/contexts/1/permissions', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + typePermission: 'read', + profil: 'test' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'The pair contextId and profil must be unique.'); + t.equal(response.statusCode, 409); + server.stop(t.end); + }); + }); + + test('POST /contexts/6/permissions - user2', function(t) { + const options = { + method: 'POST', + url: '/contexts/6/permissions', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + typePermission: 'read', + profil: 'test' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'The pair contextId and profil must be unique.'); + t.equal(response.statusCode, 409); + server.stop(t.end); + }); + }); + + test('POST /contexts/6/permissions - user2', function(t) { + const options = { + method: 'POST', + url: '/contexts/6/permissions', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + typePermission: 'read', + profil: 'test' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + const message = 'The pair contextId and profil must be unique.'; + t.equal(result.message, message); + t.equal(response.statusCode, 409); + server.stop(t.end); + }); + }); + + test('POST /contexts/3/permissions - user2', function(t) { + const options = { + method: 'POST', + url: '/contexts/3/permissions', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + typePermission: 'read', + profil: 'test' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.profil, 'test'); + t.equal(result.typePermission, 'read'); + t.equal(Number(result.contextId), 3); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts/3/permissions - user2', function(t) { + const options = { + method: 'POST', + url: '/contexts/3/permissions', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + typePermission: 'test', + profil: 'test2' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + let message = 'child "typePermission" fails because '; + message += '["typePermission" must be one of [read, write]]'; + t.equal(result.message, message); + t.equal(response.statusCode, 400); + server.stop(t.end); + }); + }); + + + // =============================================== + + test('PATCH /contexts/1/permissions/1 - id not allowed', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/1/permissions/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + id: '1234' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, '"id" is not allowed'); + t.equal(response.statusCode, 400); + server.stop(t.end); + }); + }); + + test('PATCH /contexts/1/permissions/1 - user1', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/1/permissions/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + profil: 'anotherProfil', + typePermission: 'write' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(Number(result.id), 1); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('PATCH /contexts/2/permissions/1 - user1', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/2/permissions/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + typePermission: 'write' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('PATCH /contexts/3/permissions/1 - user1', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/3/permissions/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + typePermission: 'write' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('PATCH /contexts/4/permissions/2 - user1', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/4/permissions/2', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + typePermission: 'write' + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('PATCH /contexts/5/permissions/1 - user1', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/5/permissions/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + typePermission: 'write' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('PATCH /contexts/6/permissions/1 - user1', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/6/permissions/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + typePermission: 'write' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(Number(result.id), 1); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('PATCH /contexts/1/permissions/10 - user1', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/1/permissions/10', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + typePermission: 'write' + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 404); + server.stop(t.end); + }); + }); + +// =================================== + + test('GET /contexts/1/permissions - user1', function(t) { + const options = { + method: 'GET', + url: '/contexts/1/permissions', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.length, 1); + t.equal(result[0].profil, 'test'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /contexts/2/permissions - user1', function(t) { + const options = { + method: 'GET', + url: '/contexts/2/permissions', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('GET /contexts/3/permissions - user1', function(t) { + const options = { + method: 'GET', + url: '/contexts/3/permissions', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('GET /contexts/4/permissions - user1', function(t) { + const options = { + method: 'GET', + url: '/contexts/4/permissions', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('GET /contexts/5/permissions - user1', function(t) { + const options = { + method: 'GET', + url: '/contexts/5/permissions', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('GET /contexts/6/permissions - user1', function(t) { + const options = { + method: 'GET', + url: '/contexts/6/permissions', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.length, 2); + t.equal(result[0].profil, user1.xConsumerUsername); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + // ============================================ + + test('DELETE /contexts/1/permissions/3 - user1', function(t) { + const options = { + method: 'DELETE', + url: '/contexts/1/permissions/3', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 204); + server.stop(t.end); + }); + }); + + test('GET /contexts/1/permissions - user1', function(t) { + const options = { + method: 'GET', + url: '/contexts/1/permissions', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.length, 0); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + +}); diff --git a/src/contextPermission/contextPermission.ts b/src/contextPermission/contextPermission.ts new file mode 100644 index 0000000..58d74b2 --- /dev/null +++ b/src/contextPermission/contextPermission.ts @@ -0,0 +1,216 @@ +import * as Rx from 'rxjs'; +import * as Boom from 'boom'; + +import { IDatabase, database } from '../database'; +import { ObjectUtils } from '../utils'; +import { User } from '../user'; +import { ContextInstance, Scope } from '../context'; + +import { IContextPermission, ContextPermissionInstance, + TypePermission } from './index'; + +export class ContextPermission { + private database: IDatabase = database; + + constructor() {} + + public create( + contextPermission: IContextPermission + ): Rx.Observable { + + return Rx.Observable.create(observer => { + this.database.contextPermission.create(contextPermission) + .then((contextPermissionCreated) => { + observer.next(contextPermissionCreated); + observer.complete(); + }).catch((error) => { + const uniqueFields = ['contextId', 'profil']; + if (error.name === 'SequelizeUniqueConstraintError' && + error.fields.toString() === uniqueFields.toString()) { + const message = 'The pair contextId and profil must be unique.'; + observer.error(Boom.conflict(message)); + } else { + observer.error(Boom.badImplementation(error)); + } + }); + }); + } + + public update( + id: string, + contextPermission: IContextPermission + ): Rx.Observable { + + return Rx.Observable.create(observer => { + + this.database.contextPermission.update(contextPermission, { + where: { + id: id + } + }).then((count: [number, ContextPermissionInstance[]]) => { + if (count[0]) { + observer.next({id: id}); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + const uniqueFields = ['contextId', 'profil']; + if (error.name === 'SequelizeUniqueConstraintError' && + error.fields.toString() === uniqueFields.toString()) { + const message = 'The pair contextId and profil must be unique.'; + observer.error(Boom.conflict(message)); + } else { + observer.error(Boom.badImplementation(error)); + } + }); + }); + } + + public delete(id: string): Rx.Observable<{}> { + return Rx.Observable.create(observer => { + this.database.contextPermission.destroy({ + where: { + id: id + } + }).then((count: number) => { + if (count) { + observer.next({}); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public getByContextId(contextId): Rx.Observable { + return Rx.Observable.create(observer => { + this.database.contextPermission.findAll({ + where: { + contextId: contextId + } + }).then((contextPermissions: ContextPermissionInstance[]) => { + const plainContextPermissions = contextPermissions.map( + (contextPermission) => { + return ObjectUtils.removeNull(contextPermission.get()); + } + ); + observer.next(plainContextPermissions); + observer.complete(); + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public getPermission(context: ContextInstance, + user?: string): Rx.Observable { + + return Rx.Observable.create(observer => { + + if (user && context.owner === user) { + observer.next(TypePermission.write); + observer.complete(); + return; + } + + if (!user) { + if (Scope[context.scope] as any === Scope.public) { + observer.next(TypePermission.read); + } else { + observer.next(TypePermission.null); + } + observer.complete(); + return; + } + + if (Scope[context.scope] as any === Scope.private) { + observer.next(TypePermission.null); + observer.complete(); + return; + } + + this.getPermissionFromProfils(context, user).subscribe( + (permission) => { + observer.next(permission); + observer.complete(); + }, + (error: Boom.BoomError) => observer.error(error) + ); + }); + } + + public getPermissionByContextId(contextId: string, + user?: string): Rx.Observable { + + return Rx.Observable.create(observer => { + this.database.context.findOne({ + where: { + id: contextId + } + }).then((context) => { + if (context) { + this.getPermission(context, user).subscribe( + (permission) => { + observer.next(permission); + observer.complete(); + }, + (error: Boom.BoomError) => observer.error(error) + ); + } else { + observer.next(TypePermission.null); + observer.complete(); + } + }); + }); + } + + private getPermissionFromProfils(context: ContextInstance, + user?: string): Rx.Observable { + + return Rx.Observable.create(observer => { + User.getProfils(user).subscribe( + (profils) => { + if (user) { + profils.push(user); + } + this.database.context.findAll({ + include: [{ + model: this.database.contextPermission, + where: { + profil: profils + } + }], + where: { + id: context.id + } + }).then((contextFound: Array) => { + if (!contextFound.length) { + if (Scope[context.scope] as any === Scope.public) { + observer.next(TypePermission.read); + } else { + observer.next(TypePermission.null); + } + } else { + let permission = TypePermission.read; + for (const cp of contextFound[0].contextPermissions) { + const typePerm: any = TypePermission[cp.typePermission]; + if (typePerm === TypePermission.write) { + permission = TypePermission.write; + break; + } + } + observer.next(permission); + } + observer.complete(); + }); + }, + (error: Boom.BoomError) => observer.error(error) + ); + }); + } + +} diff --git a/src/contextPermission/contextPermission.validator.ts b/src/contextPermission/contextPermission.validator.ts new file mode 100644 index 0000000..b326bd6 --- /dev/null +++ b/src/contextPermission/contextPermission.validator.ts @@ -0,0 +1,67 @@ +import * as Joi from 'joi'; +import * as Boom from 'boom'; + +import { UserValidator } from '../user/user.validator'; + +import { TypePermission } from './contextPermission.model'; +import { ContextPermission } from './contextPermission'; + +export class ContextPermissionValidator { + + static createModel = Joi.object().keys({ + profil: Joi.string().required(), + typePermission: Joi.string().valid('read', 'write') + }); + + static updateModel = Joi.object().keys({ + profil: Joi.string(), + typePermission: Joi.string().valid('read', 'write') + }); + + static writePermission = (value, options, next) => { + + const valid = Joi.validate(value, UserValidator.notAnonymousValidator); + + if (valid.error) { + next(Boom.unauthorized('Must be authenticated')); + } else { + const owner = value['x-consumer-username']; + const contextId = options.context.params['contextId']; + const contextPermission = new ContextPermission(); + contextPermission.getPermissionByContextId(contextId, owner).subscribe( + (permission) => { + if (permission === TypePermission.write) { + next(null, value); + } else { + next(Boom.forbidden('Must have write permission for this context')); + } + }, + (error: Boom.BoomError) => next(error) + ); + } + } + + static readPermission = (value, options, next) => { + + const valid = Joi.validate(value, UserValidator.userValidator); + + if (valid.error) { + next(Boom.unauthorized('Must be authenticated')); + } else { + const owner = value['x-consumer-username']; + const contextId = options.context.params['contextId']; + const contextPermission = new ContextPermission(); + contextPermission.getPermissionByContextId(contextId, owner).subscribe( + (permission) => { + if (permission) { + next(null, value); + } else { + next(Boom.forbidden('Must have read permission for this context')); + } + }, + (error: Boom.BoomError) => next(error) + ); + } + } + +} diff --git a/src/contextPermission/index.ts b/src/contextPermission/index.ts new file mode 100644 index 0000000..ce09f6d --- /dev/null +++ b/src/contextPermission/index.ts @@ -0,0 +1,10 @@ +import * as Hapi from 'hapi'; +import Routes from './routes'; + +export function init(server: Hapi.Server) { + Routes(server); +} + +export * from './contextPermission.model' +export * from './contextPermission.validator' +export * from './contextPermission' diff --git a/src/contextPermission/routes.ts b/src/contextPermission/routes.ts new file mode 100644 index 0000000..6b26260 --- /dev/null +++ b/src/contextPermission/routes.ts @@ -0,0 +1,126 @@ +import * as Hapi from 'hapi'; +import * as Joi from 'joi'; + +import { ContextPermissionController } from './contextPermission.controller'; +import { ContextPermissionValidator } from './contextPermission.validator'; + +export default function(server: Hapi.Server) { + + const contextPermissionController = + new ContextPermissionController(); + + server.bind(contextPermissionController); + + server.route({ + method: 'GET', + path: '/contexts/{contextId}/permissions', + config: { + handler: contextPermissionController.getByContextId, + tags: ['api', 'contextsPermissions', 'contexts', 'permissions'], + description: 'Get permissions by contexts id.', + validate: { + params: { + contextId: Joi.string().required() + }, + headers: ContextPermissionValidator.writePermission + }, + plugins: { + 'hapi-swagger': { + responses: { + '200': { + 'description': 'permissions founded.' + }, + '404': { + 'description': 'Permission does not exists.' + } + } + } + } + } + }); + + server.route({ + method: 'DELETE', + path: '/contexts/{contextId}/permissions/{id}', + config: { + handler: contextPermissionController.delete, + tags: ['api', 'contextsPermissions', 'contexts', 'permissions'], + description: 'Delete contextPermission by id.', + validate: { + params: { + id: Joi.string().required(), + contextId: Joi.string().required() + }, + headers: ContextPermissionValidator.writePermission + }, + plugins: { + 'hapi-swagger': { + responses: { + '204': { + 'description': 'Deleted ContextPermission.', + }, + '404': { + 'description': 'ContextPermission does not exists.' + } + } + } + } + } + }); + + server.route({ + method: 'PATCH', + path: '/contexts/{contextId}/permissions/{id}', + config: { + handler: contextPermissionController.update, + tags: ['api', 'contextsPermissions', 'contexts', 'permissions'], + description: 'Update contextPermission by id.', + validate: { + params: { + id: Joi.string().required(), + contextId: Joi.string().required() + }, + payload: ContextPermissionValidator.updateModel, + headers: ContextPermissionValidator.writePermission + }, + plugins: { + 'hapi-swagger': { + responses: { + '200': { + 'description': 'Deleted ContextPermission.', + }, + '404': { + 'description': 'ContextPermission does not exists.' + } + } + } + } + } + }); + + server.route({ + method: 'POST', + path: '/contexts/{contextId}/permissions', + config: { + handler: contextPermissionController.create, + tags: ['api', 'contextsPermissions', 'contexts', 'permissions'], + description: 'Create a contextPermission.', + validate: { + params: { + contextId: Joi.string().required() + }, + payload: ContextPermissionValidator.createModel, + headers: ContextPermissionValidator.writePermission + }, + plugins: { + 'hapi-swagger': { + responses: { + '201': { + 'description': 'Created ContextPermission.' + } + } + } + } + } + }); +} diff --git a/src/contexts/context.controller.ts b/src/contexts/context.controller.ts deleted file mode 100644 index c0c2e54..0000000 --- a/src/contexts/context.controller.ts +++ /dev/null @@ -1,129 +0,0 @@ -import * as Hapi from 'hapi'; -import * as Boom from 'boom'; -import { IContext, ContextInstance } from './context.model'; -import { IDatabase } from '../database'; -import { IServerConfigurations } from '../configurations'; - -export default class ContextController { - - private database: IDatabase; - private configs: IServerConfigurations; - - constructor(configs: IServerConfigurations, database: IDatabase) { - this.configs = configs; - this.database = database; - } - - public createContext(request: Hapi.Request, reply: Hapi.IReply) { - const newContext: IContext = request.payload; - this.database.context.create(newContext).then((context) => { - reply(context).code(201); - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - - public updateContext(request: Hapi.Request, reply: Hapi.IReply) { - const id = request.params['id']; - const context: IContext = request.payload; - - this.database.context.update(context, { - where: { - id: id - } - }).then((count: [number, ContextInstance[]]) => { - if (count[0]) { - reply({}); - } else { - reply(Boom.notFound()); - } - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - - public deleteContext(request: Hapi.Request, reply: Hapi.IReply) { - const id = request.params['id']; - this.database.context.destroy({ - where: { - id: id - } - }).then((count: number) => { - if (count) { - reply({}); - } else { - reply(Boom.notFound()); - } - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - - public getContextById(request: Hapi.Request, reply: Hapi.IReply) { - const id = request.params['id']; - this.database.context.findOne({ - where: { - id: id - } - }).then((context: ContextInstance) => { - if (context) { - reply(context); - } else { - reply(Boom.notFound()); - } - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - - public getContexts(request: Hapi.Request, reply: Hapi.IReply) { - this.database.context.findAll() - .then((contexts: Array) => { - reply(contexts); - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - - public getContextDetailsById(request: Hapi.Request, reply: Hapi.IReply) { - const id = request.params['id']; - - this.database.context.findOne({ - include: [ - this.database.layer, - this.database.tool - ], - where: { - id: id - } - }).then((contextDetails: any) => { - const plainDetails = contextDetails.get(); - plainDetails.layers = []; - plainDetails.tools = []; - plainDetails.toolbar = []; - - for (const tool of contextDetails.tools) { - const plainTool = tool.get(); - Object.assign(plainTool.options, plainTool.toolContext.options); - plainTool.toolContext = undefined; - plainDetails.tools.push(plainTool); - if (plainTool.inToolbar) { - plainDetails.toolbar.push(plainTool.name); - } - } - - for (const layer of contextDetails.layers) { - const plainLayer = layer.get(); - Object.assign(plainLayer.view, plainLayer.layerContext.view); - Object.assign(plainLayer.source, plainLayer.layerContext.source); - plainLayer.layerContext = undefined; - plainDetails.layers.push(plainLayer); - } - - reply(plainDetails); - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - -} diff --git a/src/contexts/context.model.ts b/src/contexts/context.model.ts deleted file mode 100644 index 5f0e068..0000000 --- a/src/contexts/context.model.ts +++ /dev/null @@ -1,84 +0,0 @@ -import * as Sequelize from 'sequelize'; - -enum Scope { - public, - protected, - private -} - -interface Map { - view: { - center: string; - zoom: number; - }; -}; - -export interface IContext { - uri: string; - scope: Scope; - title: string; - icon: string; - map: Map; -}; - -export interface ContextInstance extends Sequelize.Instance { - id: string; - createdAt: Date; - updatedAt: Date; - - uri: string; - scope: Scope; - title: string; - icon: string; - map: string; -} - -export interface ContextModel - extends Sequelize.Model { } - -export default function define(sequelize: Sequelize.Sequelize, DataTypes) { - const context = sequelize.define('context', { - 'id': { - 'type': DataTypes.INTEGER, - 'allowNull': false, - 'primaryKey': true, - 'autoIncrement': true - }, - 'uri': { - 'type': DataTypes.STRING(64), - 'allowNull': false - }, - 'title': { - 'type': DataTypes.STRING(128), - 'allowNull': false - }, - 'icon': { - 'type': DataTypes.STRING(128) - }, - 'scope': { - 'type': DataTypes.ENUM('public', 'protected', 'private'), - 'allowNull': false - /*'unique': true, - 'validate': { - 'isEmail': true - }*/ - }, - 'map': { - 'type': DataTypes.TEXT, - 'get': function() { - return JSON.parse(this.getDataValue('map')); - }, - 'set': function(val) { - this.setDataValue('map', JSON.stringify({})); - } - } - }, - { - 'tableName': 'context', - 'timestamps': true - }); - - context.sync(); - - return context; -} diff --git a/src/contexts/context.test.ts b/src/contexts/context.test.ts deleted file mode 100644 index 00059b9..0000000 --- a/src/contexts/context.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import * as test from 'tape'; -// import ContextCont from './context.controller'; -import * as Server from '../server'; -import * as Configs from '../configurations'; - -const serverConfigs = Configs.getServerConfigs(); - -Server.init(serverConfigs).then((server) => { - - test('Basic HTTP Tests - GET /contexts', function(t) { - const options = { - method: 'GET', - url: '/contexts' - }; - server.inject(options, function(response) { - t.equal(response.statusCode, 200); - t.equal(response.result.length, 0); - server.stop(t.end); - }); - }); - - - test('Basic HTTP Tests - GET /contexts/{id}', function(t) { - const options = { - method: 'GET', - url: '/contexts/2' - }; - server.inject(options, function(response) { - t.equal(response.statusCode, 404); - server.stop(t.end); - }); - }); - - - test('Basic HTTP Tests - POST /contexts', function(t) { - const options = { - method: 'POST', - url: '/contexts', - payload: { - uri: 'dummy', - title: 'dummy', - scope: 'private', - map: {} - } - }; - server.inject(options, function(response) { - t.equal(response.statusCode, 201); - server.stop(t.end); - }); - }); - - -}); diff --git a/src/contexts/context.validator.ts b/src/contexts/context.validator.ts deleted file mode 100644 index 5dd5316..0000000 --- a/src/contexts/context.validator.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as Joi from 'joi'; - -export const createContextModel = Joi.object().keys({ - scope: Joi.string().required().valid('public', 'protected', 'private'), - uri: Joi.string().required(), - title: Joi.string().required(), - icon: Joi.string(), - map: Joi.object().required().keys({ - view: Joi.object().keys({ - center: Joi.string(), - zoom: Joi.number() - }) - }) -}); - -export const updateContextModel = Joi.object().keys({ - scope: Joi.string().valid('public', 'protected', 'private'), - uri: Joi.string(), - title: Joi.string(), - icon: Joi.string(), - map: Joi.object().keys({ - view: Joi.object().keys({ - center: Joi.string(), - zoom: Joi.number() - }) - }) -}); diff --git a/src/contexts/index.ts b/src/contexts/index.ts deleted file mode 100644 index 0cc53d9..0000000 --- a/src/contexts/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as Hapi from 'hapi'; -import Routes from './routes'; -import { IDatabase } from '../database'; -import { IServerConfigurations } from '../configurations'; - -export function init(server: Hapi.Server, - configs: IServerConfigurations, - database: IDatabase) { - Routes(server, configs, database); -} diff --git a/src/contexts/routes.ts b/src/contexts/routes.ts deleted file mode 100644 index 373fbbf..0000000 --- a/src/contexts/routes.ts +++ /dev/null @@ -1,180 +0,0 @@ -import * as Hapi from 'hapi'; -import * as Joi from 'joi'; -import ContextController from './context.controller'; -import * as ContextValidator from './context.validator'; -// import { jwtValidator } from '../users/user-validator'; -import { IDatabase } from '../database'; -import { IServerConfigurations } from '../configurations'; - -export default function (server: Hapi.Server, - configs: IServerConfigurations, - database: IDatabase) { - - const contextController = new ContextController(configs, database); - server.bind(contextController); - - server.route({ - method: 'GET', - path: '/contexts/{id}', - config: { - handler: contextController.getContextById, - // auth: 'jwt', - auth: false, - tags: ['api', 'contexts'], - description: 'Get contexts by id.', - validate: { - params: { - id: Joi.string().required() - } - // headers: jwtValidator - }, - plugins: { - 'hapi-swagger': { - responses: { - '200': { - 'description': 'Context founded.' - }, - '404': { - 'description': 'Context does not exists.' - } - } - } - } - } - }); - - server.route({ - method: 'GET', - path: '/contexts/{id}/details', - config: { - handler: contextController.getContextDetailsById, - // auth: 'jwt', - auth: false, - tags: ['api', 'tools', 'layers', 'contexts'], - description: 'Get details of context by context id.', - validate: { - params: { - id: Joi.string().required() - } - // headers: jwtValidator - }, - plugins: { - 'hapi-swagger': { - responses: { - '200': { - 'description': 'Context founded.' - }, - '404': { - 'description': 'Context does not exists.' - } - } - } - } - } - }); - - server.route({ - method: 'GET', - path: '/contexts', - config: { - handler: contextController.getContexts, - // auth: 'jwt', - auth: false, - tags: ['api', 'contexts'], - description: 'Get all contexts.', - validate: { - query: { - // top: Joi.number().default(5), - // skip: Joi.number().default(0) - } - // headers: jwtValidator - } - } - }); - - server.route({ - method: 'DELETE', - path: '/contexts/{id}', - config: { - handler: contextController.deleteContext, - // auth: 'jwt', - auth: false, - tags: ['api', 'contexts'], - description: 'Delete context by id.', - validate: { - params: { - id: Joi.string().required() - } - // headers: jwtValidator - }, - plugins: { - 'hapi-swagger': { - responses: { - '200': { - 'description': 'Deleted Context.', - }, - '404': { - 'description': 'Context does not exists.' - } - } - } - } - } - }); - - server.route({ - method: 'PUT', - path: '/contexts/{id}', - config: { - handler: contextController.updateContext, - // auth: 'jwt', - auth: false, - tags: ['api', 'contexts'], - description: 'Update context by id.', - validate: { - params: { - id: Joi.string().required() - }, - payload: ContextValidator.updateContextModel - // headers: jwtValidator - }, - plugins: { - 'hapi-swagger': { - responses: { - '200': { - 'description': 'Deleted Context.', - }, - '404': { - 'description': 'Context does not exists.' - } - } - } - } - } - }); - - server.route({ - method: 'POST', - path: '/contexts', - config: { - handler: contextController.createContext, - // auth: 'jwt', - auth: false, - tags: ['api', 'contexts'], - description: 'Create a context.', - validate: { - payload: ContextValidator.createContextModel - // headers: jwtValidator - }, - plugins: { - 'hapi-swagger': { - responses: { - '201': { - 'description': 'Created Context.' - } - } - } - } - } - }); -} diff --git a/src/database.ts b/src/database.ts index 65643b8..3dea699 100644 --- a/src/database.ts +++ b/src/database.ts @@ -1,21 +1,27 @@ import * as Sequelize from 'sequelize'; -import * as Glob from 'glob'; -import * as path from 'path'; import * as Configs from './configurations'; -import { ContextModel } from './contexts/context.model'; -import { LayerModel } from './layers/layer.model'; -import { ToolModel } from './tools/tool.model'; -import { ToolContextModel } from './toolsContexts/toolContext.model'; -import { LayerContextModel } from './layersContexts/layerContext.model'; +import { UserModel } from './user/user.model'; +import { POIModel } from './poi/poi.model'; +import { ContextModel } from './context/context.model'; +import { LayerModel } from './layer/layer.model'; +import { ToolModel } from './tool/tool.model'; +import { ToolContextModel } from './toolContext/toolContext.model'; +import { LayerContextModel } from './layerContext/layerContext.model'; +import { + ContextPermissionModel +} from './contextPermission/contextPermission.model'; export interface IDatabase { sequelize: Sequelize.Sequelize; + user: UserModel; + poi: POIModel; context: ContextModel; layer: LayerModel; tool: ToolModel; layerContext: LayerContextModel; toolContext: ToolContextModel; + contextPermission: ContextPermissionModel; } const dbConfigs = Configs.getDatabaseConfig(); @@ -36,20 +42,25 @@ if (dbPG.connectionString) { } const db = {}; -Glob.sync('./src/**/*.model.ts').forEach((file) => { - const fileName = file.replace('./src/', './').replace('.ts', ''); - const model = sequelize['import'](path.join(__dirname, fileName)); - db[model['name']] = model; -}); - -/*Object.keys(db).forEach(function(modelName) { - if (db[modelName].associate) { - db[modelName].associate(db); - } -});*/ +// Glob.sync('./src/**/*.model.ts').forEach((file) => { +// const fileName = file.replace('./src/', './').replace('.ts', ''); +// const model = sequelize['import'](path.join(__dirname, fileName)); +// db[model['name']] = model; +// }); + +db['user'] = sequelize['import']('./user/user.model'); +db['poi'] = sequelize['import']('./poi/poi.model'); +db['layer'] = sequelize['import']('./layer/layer.model'); +db['tool'] = sequelize['import']('./tool/tool.model'); +db['context'] = sequelize['import']('./context/context.model'); +db['contextPermission'] = + sequelize['import']('./contextPermission/contextPermission.model'); +db['layerContext'] = sequelize['import']('./layerContext/layerContext.model'); +db['toolContext'] = sequelize['import']('./toolContext/toolContext.model'); -db['sequelize'] = sequelize; -// db['Sequelize'] = Sequelize; +db['sequelize'] = sequelize; -export default db; +const database = db; +export default database; +export { database }; diff --git a/src/index.ts b/src/index.ts index 5ff2d4f..c59ff38 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ import * as Configs from './configurations'; console.log(`Running enviroment ${process.env.NODE_ENV || 'dev'}`); // Starting Application Server -const serverConfigs = Configs.getServerConfigs(); +const serverConfigs = Configs.getServerConfig(); Server.init(serverConfigs).then((server) => { server.start(() => { console.log('Server running at:', server.info.uri); diff --git a/src/initDB.ts b/src/initDB.ts new file mode 100644 index 0000000..1062f24 --- /dev/null +++ b/src/initDB.ts @@ -0,0 +1,180 @@ +import * as Server from './server'; +import * as Configs from './configurations'; + +const serverConfigs = Configs.getServerConfig(); +const testConfigs = Configs.getTestConfig(); +const admin = testConfigs.admin; + +Server.init(serverConfigs).then((server) => { + + const handleError = (response) => { + if (response.statusCode < 200 || response.statusCode >= 400) { + console.log('=====================ERROR==================='); + console.error(response.result); + console.log('============================================='); + } + }; + + server.inject({ + method: 'POST', + url: '/tools', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + name: 'searchResults', + inToolbar: true + } + }, handleError); + + server.inject({ + method: 'POST', + url: '/tools', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + name: 'contextManager', + inToolbar: true + } + }, handleError); + + server.inject({ + method: 'POST', + url: '/tools', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + name: 'contextEditor' + } + }, handleError); + + server.inject({ + method: 'POST', + url: '/tools', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + name: 'permissionsContextManager', + inToolbar: false + } + }, handleError); + + server.inject({ + method: 'POST', + url: '/tools', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + name: 'toolsContextManager', + inToolbar: false + } + }, handleError); + + server.inject({ + method: 'POST', + url: '/tools', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + name: 'mapDetails', + inToolbar: true + } + }, handleError); + + server.inject({ + method: 'POST', + url: '/tools', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + name: 'timeAnalysis', + inToolbar: true + } + }, handleError); + + server.inject({ + method: 'POST', + url: '/tools', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + name: 'print', + inToolbar: true + } + }, handleError); + + server.inject({ + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + uri: 'default', + title: 'Default', + scope: 'public', + map: { + view: { + projection: 'EPSG:3857', + center: [-72, 52], + zoom: 6 + } + }, + tools: [ + {id: '1'}, + {id: '2'}, + {id: '3'}, + {id: '4'}, + {id: '5'}, + {id: '6'}, + {id: '7'}, + {id: '8'} + ], + layers: [{ + title: 'MSP Base Map', + type: 'xyz', + source: { + url: 'https://geoegl.msp.gouv.qc.ca/carto/tms/1.0.0/' + + 'carte_gouv_qc_ro@EPSG_3857/{z}/{x}/{-y}.png' + } + }, { + title: 'MSP DESSERTE MUN 911', + type: 'wms', + source: { + url: '/ws/igo_gouvouvert.fcgi', + params: { + layers: 'MSP_DESSERTE_MUN_911', + version: '1.3.0' + } + } + }, { + title: 'MSP Tel. Urgence', + type: 'wms', + source: { + url: '/ws/igo_gouvouvert.fcgi', + params: { + layers: 'telephone_urg', + version: '1.3.0' + } + } + }] + } + }, handleError); + +}); diff --git a/src/layer/index.ts b/src/layer/index.ts new file mode 100644 index 0000000..41b0b7d --- /dev/null +++ b/src/layer/index.ts @@ -0,0 +1,9 @@ +import * as Hapi from 'hapi'; +import Routes from './routes'; + +export function init(server: Hapi.Server) { + Routes(server); +} + +export * from './layer.model' +export * from './layer' diff --git a/src/layer/layer.controller.ts b/src/layer/layer.controller.ts new file mode 100644 index 0000000..0e90301 --- /dev/null +++ b/src/layer/layer.controller.ts @@ -0,0 +1,58 @@ +import * as Hapi from 'hapi'; +import * as Boom from 'boom'; + +import { Layer } from './layer'; +import { ILayer, LayerInstance } from './layer.model'; + +export class LayerController { + + private layer: Layer; + + constructor() { + this.layer = new Layer(); + } + + public create(request: Hapi.Request, reply: Hapi.IReply) { + const layerToCreate: ILayer = request.payload; + + this.layer.create(layerToCreate).subscribe( + (layer: LayerInstance) => reply(layer).code(201), + (error: Boom.BoomError) => reply(error) + ); + } + + public update(request: Hapi.Request, reply: Hapi.IReply) { + const id = request.params['id']; + const layerToUpdate: ILayer = request.payload; + + this.layer.update(id, layerToUpdate).subscribe( + (layer: LayerInstance) => reply(layer), + (error: Boom.BoomError) => reply(error) + ); + } + + public delete(request: Hapi.Request, reply: Hapi.IReply) { + const id = request.params['id']; + + this.layer.delete(id).subscribe( + (layer: LayerInstance) => reply(layer).code(204), + (error: Boom.BoomError) => reply(error) + ); + } + + public getById(request: Hapi.Request, reply: Hapi.IReply) { + const id = request.params['id']; + + this.layer.getById(id).subscribe( + (layer: LayerInstance) => reply(layer), + (error: Boom.BoomError) => reply(error) + ); + } + + public get(request: Hapi.Request, reply: Hapi.IReply) { + this.layer.get().subscribe( + (layers: LayerInstance[]) => reply(layers), + (error: Boom.BoomError) => reply(error) + ); + } +} diff --git a/src/layer/layer.model.ts b/src/layer/layer.model.ts new file mode 100644 index 0000000..3a037b5 --- /dev/null +++ b/src/layer/layer.model.ts @@ -0,0 +1,83 @@ +import * as Sequelize from 'sequelize'; + +interface ViewLayer { + attribution: string; + minZoom: number; + maxZoom: number; +}; + +export interface SourceLayer { + url: string; + params?: { [key: string]: any }; +}; + +export interface ILayer { + id?: string; + title: string; + type: string; + view?: ViewLayer; + source?: SourceLayer; + order?: number; +}; + +export interface LayerInstance extends Sequelize.Instance { + id: string; + createdAt: Date; + updatedAt: Date; + + title: string; + type: string; + view?: ViewLayer; + source?: SourceLayer; +} + +export interface LayerModel + extends Sequelize.Model { } + + +export default function define(sequelize: Sequelize.Sequelize, DataTypes) { + const layer = sequelize.define('layer', { + 'id': { + 'type': DataTypes.INTEGER, + 'allowNull': false, + 'primaryKey': true, + 'autoIncrement': true + }, + 'title': { + 'type': DataTypes.STRING(64), + 'allowNull': false + }, + 'type': { + 'type': DataTypes.STRING(32), + 'allowNull': false + }, + 'view': { + 'type': DataTypes.TEXT, + 'get': function() { + const view = this.getDataValue('view'); + return view ? JSON.parse(view) : {}; + }, + 'set': function(val) { + this.setDataValue('view', JSON.stringify(val)); + } + }, + 'source': { + 'type': DataTypes.TEXT, + 'get': function() { + const source = this.getDataValue('source'); + return source ? JSON.parse(source) : {}; + }, + 'set': function(val) { + this.setDataValue('source', JSON.stringify(val)); + } + } + }, + { + 'tableName': 'layer', + 'timestamps': true + }); + + layer.sync(); + + return layer; +} diff --git a/src/layer/layer.test.ts b/src/layer/layer.test.ts new file mode 100644 index 0000000..a900c1c --- /dev/null +++ b/src/layer/layer.test.ts @@ -0,0 +1,412 @@ +import * as test from 'tape'; +// import LayerCont from './layer.controller'; +import * as Server from '../server'; +import * as Configs from '../configurations'; + +const serverConfigs = Configs.getServerConfig(); +const testConfigs = Configs.getTestConfig(); +const admin = testConfigs.admin; +const anonyme = testConfigs.anonyme; +const user1 = testConfigs.user1; + +Server.init(serverConfigs).then((server) => { + + test('POST /layers - admin', function(t) { + const options = { + method: 'POST', + url: '/layers', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + title: 'dummyTitle', + type: 'osm', + view: {}, + source: {} + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.type, 'osm'); + t.equal(result.title, 'dummyTitle'); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /layers - admin 2', function(t) { + const options = { + method: 'POST', + url: '/layers', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + title: 'dummyTitle2', + type: 'osm' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.title, 'dummyTitle2'); + t.equal(result.type, 'osm'); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /layers - admin - type missing', function(t) { + const options = { + method: 'POST', + url: '/layers', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + title: 'dummy', + view: {}, + source: {} + } + }; + server.inject(options, function(response) { + const result: any = response.result; + const message = 'child "type" fails because ["type" is required]'; + t.equal(result.message, message); + t.equal(response.statusCode, 400); + server.stop(t.end); + }); + }); + + test('POST /layers - admin - another param', function(t) { + const options = { + method: 'POST', + url: '/layers', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + title: 'dummy', + type: 'osm', + view: {}, + source: {}, + anotherParam: 'other' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, '"anotherParam" is not allowed'); + t.equal(response.statusCode, 400); + server.stop(t.end); + }); + }); + + test('POST /layers - anonyme', function(t) { + const options = { + method: 'POST', + url: '/layers', + headers: { + 'x-consumer-username': anonyme.xConsumerUsername, + 'x-consumer-id': anonyme.xConsumerId + }, + payload: { + title: 'dummyAnonyme', + type: 'wfs', + view: {}, + source: {} + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.title, 'dummyAnonyme'); + t.equal(result.type, 'wfs'); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /layers - user1', function(t) { + const options = { + method: 'POST', + url: '/layers', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + title: 'dummyUser1', + type: 'osm', + view: {}, + source: {} + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.title, 'dummyUser1'); + t.equal(result.type, 'osm'); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + // ---------------------------------------------------------------- + + test('GET /layers - admin', function(t) { + const options = { + method: 'GET', + url: '/layers', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.length, 4); + t.equal(result[0].title, 'dummyTitle'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /layers - anonyme', function(t) { + const options = { + method: 'GET', + url: '/layers', + headers: { + 'x-consumer-username': anonyme.xConsumerUsername, + 'x-consumer-id': anonyme.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.length, 4); + t.equal(result[0].title, 'dummyTitle'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /layers - user1', function(t) { + const options = { + method: 'GET', + url: '/layers', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.length, 4); + t.equal(result[0].title, 'dummyTitle'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + +// ---------------------------------------------------------------- + + test('PATCH /layers/{id} - admin', function(t) { + const options = { + method: 'PATCH', + url: '/layers/2', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + type: 'wms' + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('PATCH /layers/{id} - anonyme', function(t) { + const options = { + method: 'PATCH', + url: '/layers/2', + headers: { + 'x-consumer-username': anonyme.xConsumerUsername, + 'x-consumer-id': anonyme.xConsumerId + }, + payload: { + title: 'dummy99' + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 403); + const result: any = response.result; + t.equal(result.message, 'Must be administrator'); + server.stop(t.end); + }); + }); + + test('PATCH /layers/{id} - user1', function(t) { + const options = { + method: 'PATCH', + url: '/layers/2', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + title: 'dummy99' + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 403); + const result: any = response.result; + t.equal(result.message, 'Must be administrator'); + server.stop(t.end); + }); + }); + + // ---------------------------------------------------------------- + + test('GET /layers/{id} - admin', function(t) { + const options = { + method: 'GET', + url: '/layers/10', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 404); + server.stop(t.end); + }); + }); + + test('GET /layers/{id} - admin', function(t) { + const options = { + method: 'GET', + url: '/layers/1', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.title, 'dummyTitle'); + t.equal(result.type, 'osm'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /layers/{id} - anonyme', function(t) { + const options = { + method: 'GET', + url: '/layers/2', + headers: { + 'x-consumer-username': anonyme.xConsumerUsername, + 'x-consumer-id': anonyme.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.title, 'dummyTitle2'); + t.equal(result.type, 'wms'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /layers/{id} - user1', function(t) { + const options = { + method: 'GET', + url: '/layers/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.title, 'dummyTitle'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + // ---------------------------------------------------------------- + + + test('DELETE /layers/{id} - anonyme', function(t) { + const options = { + method: 'DELETE', + url: '/layers/3', + headers: { + 'x-consumer-username': anonyme.xConsumerUsername, + 'x-consumer-id': anonyme.xConsumerId + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 403); + const result: any = response.result; + t.equal(result.message, 'Must be administrator'); + server.stop(t.end); + }); + }); + + test('DELETE /layers/{id} - user1', function(t) { + const options = { + method: 'DELETE', + url: '/layers/3', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 403); + const result: any = response.result; + t.equal(result.message, 'Must be administrator'); + server.stop(t.end); + }); + }); + + test('DELETE /layers/{id} - admin', function(t) { + const options = { + method: 'DELETE', + url: '/layers/3', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 204); + server.stop(t.end); + }); + }); + + + test('GET /layers - admin', function(t) { + const options = { + method: 'GET', + url: '/layers', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.length, 3); + t.equal(result[0].title, 'dummyTitle'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + +}); diff --git a/src/layer/layer.ts b/src/layer/layer.ts new file mode 100644 index 0000000..816b2e3 --- /dev/null +++ b/src/layer/layer.ts @@ -0,0 +1,125 @@ +import * as Rx from 'rxjs'; +import * as Boom from 'boom'; + +import { IDatabase, database } from '../database'; +import { ObjectUtils } from '../utils'; + +import { ILayer, LayerInstance } from './layer.model'; + +export class Layer { + + private database: IDatabase = database; + + constructor() {} + + public create(layer: ILayer): Rx.Observable { + return Rx.Observable.create(observer => { + this.database.layer.create(layer).then((createdLayer) => { + observer.next(createdLayer); + observer.complete(); + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public update(id: string, layer: ILayer): Rx.Observable { + return Rx.Observable.create(observer => { + this.database.layer.update(layer, { + where: { + id: id + } + }).then((count: [number, LayerInstance[]]) => { + if (count[0]) { + observer.next({id: id}); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public delete(id: string): Rx.Observable<{}> { + return Rx.Observable.create(observer => { + this.database.layer.destroy({ + where: { + id: id + } + }).then((count: number) => { + if (count) { + observer.next({}); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public get(): Rx.Observable { + return Rx.Observable.create(observer => { + this.database.layer.findAll().then((layers: LayerInstance[]) => { + const plainLayers = layers.map( + (layer) => ObjectUtils.removeNull(layer.get()) + ); + observer.next(plainLayers); + observer.complete(); + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public getById(id: string): Rx.Observable { + return Rx.Observable.create(observer => { + this.database.layer.findOne({ + where: { + id: id + } + }).then((layer: LayerInstance) => { + if (layer) { + observer.next(ObjectUtils.removeNull(layer.get())); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public getBySource(layer: ILayer): Rx.Observable { + + const where: any = { + $or: [ + {id: layer.id}, + { + type: layer.type, + source: JSON.stringify(layer.source) + } + ] + }; + + return Rx.Observable.create(observer => { + this.database.layer.findOne({ + where: where + }).then((layerFound: LayerInstance) => { + if (layerFound) { + observer.next(ObjectUtils.removeNull(layerFound.get())); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + +} diff --git a/src/layer/layer.validator.ts b/src/layer/layer.validator.ts new file mode 100644 index 0000000..6c3d204 --- /dev/null +++ b/src/layer/layer.validator.ts @@ -0,0 +1,34 @@ +import * as Joi from 'joi'; + + +export class LayerValidator { + + static createModel = Joi.object().keys({ + title: Joi.string().required().max(64), + type: Joi.string().required().max(32), + view: Joi.object().keys({ + attribution: Joi.string().allow(''), + minZoom: Joi.number(), + maxZoom: Joi.number() + }), + source: Joi.object().keys({ + url: Joi.string().allow(''), + params: Joi.object() + }) + }); + + static updateModel = Joi.object().keys({ + title: Joi.string().max(64), + type: Joi.string().max(32), + view: Joi.object().keys({ + attribution: Joi.string().allow(''), + minZoom: Joi.number(), + maxZoom: Joi.number() + }), + source: Joi.object().keys({ + url: Joi.string().allow(''), + params: Joi.object() + }) + }); + +} diff --git a/src/layers/routes.ts b/src/layer/routes.ts similarity index 64% rename from src/layers/routes.ts rename to src/layer/routes.ts index 64b7f2f..f0b05cb 100644 --- a/src/layers/routes.ts +++ b/src/layer/routes.ts @@ -1,32 +1,27 @@ import * as Hapi from 'hapi'; import * as Joi from 'joi'; -import LayerController from './layer.controller'; -import * as LayerValidator from './layer.validator'; -// import { jwtValidator } from '../users/user-validator'; -import { IDatabase } from '../database'; -import { IServerConfigurations } from '../configurations'; -export default function (server: Hapi.Server, - configs: IServerConfigurations, - database: IDatabase) { +import { LayerController } from './layer.controller'; +import { LayerValidator } from './layer.validator'; +import { UserValidator } from '../user/user.validator'; - const layerController = new LayerController(configs, database); +export default function (server: Hapi.Server) { + + const layerController = new LayerController(); server.bind(layerController); server.route({ method: 'GET', path: '/layers/{id}', config: { - handler: layerController.getLayerById, - // auth: 'jwt', - auth: false, + handler: layerController.getById, tags: ['api', 'layers'], description: 'Get layers by id.', validate: { params: { id: Joi.string().required() - } - // headers: jwtValidator + }, + headers: UserValidator.authenticateValidator }, plugins: { 'hapi-swagger': { @@ -47,17 +42,11 @@ export default function (server: Hapi.Server, method: 'GET', path: '/layers', config: { - handler: layerController.getLayers, - // auth: 'jwt', - auth: false, + handler: layerController.get, tags: ['api', 'layers'], description: 'Get all layers.', validate: { - query: { - // top: Joi.number().default(5), - // skip: Joi.number().default(0) - } - // headers: jwtValidator + headers: UserValidator.authenticateValidator } } }); @@ -66,21 +55,19 @@ export default function (server: Hapi.Server, method: 'DELETE', path: '/layers/{id}', config: { - handler: layerController.deleteLayer, - // auth: 'jwt', - auth: false, + handler: layerController.delete, tags: ['api', 'layers'], description: 'Delete layer by id.', validate: { params: { id: Joi.string().required() - } - // headers: jwtValidator + }, + headers: UserValidator.adminValidator }, plugins: { 'hapi-swagger': { responses: { - '200': { + '204': { 'description': 'Deleted Layer.', }, '404': { @@ -93,20 +80,18 @@ export default function (server: Hapi.Server, }); server.route({ - method: 'PUT', + method: 'PATCH', path: '/layers/{id}', config: { - handler: layerController.updateLayer, - // auth: 'jwt', - auth: false, + handler: layerController.update, tags: ['api', 'layers'], description: 'Update layer by id.', validate: { params: { id: Joi.string().required() }, - payload: LayerValidator.updateLayerModel - // headers: jwtValidator + payload: LayerValidator.updateModel, + headers: UserValidator.adminValidator }, plugins: { 'hapi-swagger': { @@ -127,14 +112,12 @@ export default function (server: Hapi.Server, method: 'POST', path: '/layers', config: { - handler: layerController.createLayer, - // auth: 'jwt', - auth: false, + handler: layerController.create, tags: ['api', 'layers'], description: 'Create a layer.', validate: { - payload: LayerValidator.createLayerModel - // headers: jwtValidator + payload: LayerValidator.createModel, + headers: UserValidator.authenticateValidator }, plugins: { 'hapi-swagger': { diff --git a/src/layerContext/index.ts b/src/layerContext/index.ts new file mode 100644 index 0000000..21d18a5 --- /dev/null +++ b/src/layerContext/index.ts @@ -0,0 +1,9 @@ +import * as Hapi from 'hapi'; +import Routes from './routes'; + +export function init(server: Hapi.Server) { + Routes(server); +} + +export * from './layerContext.model' +export * from './layerContext' diff --git a/src/layerContext/layerContext.controller.ts b/src/layerContext/layerContext.controller.ts new file mode 100644 index 0000000..d5db47e --- /dev/null +++ b/src/layerContext/layerContext.controller.ts @@ -0,0 +1,64 @@ +import * as Hapi from 'hapi'; +import * as Boom from 'boom'; + +import { LayerContext, ILayerContext, LayerContextInstance } from './index'; + +export class LayerContextController { + + private layerContext: LayerContext; + + constructor() { + this.layerContext = new LayerContext(); + } + + public create(request: Hapi.Request, reply: Hapi.IReply) { + const newLayerContext: ILayerContext = request.payload; + newLayerContext['contextId'] = request.params['contextId']; + + this.layerContext.create(newLayerContext).subscribe( + (tc: LayerContextInstance) => reply(tc).code(201), + (error: Boom.BoomError) => reply(error) + ); + } + + public update(request: Hapi.Request, reply: Hapi.IReply) { + const layerId = request.params['layerId']; + const contextId = request.params['contextId']; + const layerContext: ILayerContext = request.payload; + + this.layerContext.update(contextId, layerId, layerContext).subscribe( + (cp: LayerContextInstance) => reply(cp), + (error: Boom.BoomError) => reply(error) + ); + } + + public delete(request: Hapi.Request, reply: Hapi.IReply) { + const layerId = request.params['layerId']; + const contextId = request.params['contextId']; + + this.layerContext.delete(contextId, layerId).subscribe( + (cp: LayerContextInstance) => reply(cp).code(204), + (error: Boom.BoomError) => reply(error) + ); + } + + public getById(request: Hapi.Request, reply: Hapi.IReply) { + const layerId = request.params['layerId']; + const contextId = request.params['contextId']; + + this.layerContext.getById(contextId, layerId).subscribe( + (cp: LayerContextInstance) => reply(cp), + (error: Boom.BoomError) => reply(error) + ); + } + + public getByContextId(request: Hapi.Request, reply: Hapi.IReply) { + const contextId = request.params['contextId']; + + this.layerContext.getByContextId(contextId).subscribe( + (cp: LayerContextInstance[]) => reply(cp), + (error: Boom.BoomError) => reply(error) + ); + } + +} diff --git a/src/layersContexts/layerContext.model.ts b/src/layerContext/layerContext.model.ts similarity index 70% rename from src/layersContexts/layerContext.model.ts rename to src/layerContext/layerContext.model.ts index 1fd6c83..6fbe63c 100644 --- a/src/layersContexts/layerContext.model.ts +++ b/src/layerContext/layerContext.model.ts @@ -6,13 +6,12 @@ interface ViewLayer { maxZoom: number; }; -interface SourceLayer { - url: string; -}; - export interface ILayerContext { - view: ViewLayer; - source: SourceLayer; + id?: string; + layerId?: string; + contextId?: string; + view?: ViewLayer; + order?: number; }; export interface LayerContextInstance @@ -21,8 +20,10 @@ export interface LayerContextInstance createdAt: Date; updatedAt: Date; + layerId: string; + contextId: string; view: ViewLayer; - source: SourceLayer; + order: number; } export interface LayerContextModel @@ -40,29 +41,32 @@ export default function define(sequelize: Sequelize.Sequelize, DataTypes) { 'view': { 'type': DataTypes.TEXT, 'get': function() { - return JSON.parse(this.getDataValue('view')); + const view = this.getDataValue('view'); + return view ? JSON.parse(view) : {}; }, 'set': function(val) { - this.setDataValue('view', JSON.stringify({})); + this.setDataValue('view', JSON.stringify(val)); } }, - 'source': { - 'type': DataTypes.TEXT, - 'get': function() { - return JSON.parse(this.getDataValue('source')); - }, - 'set': function(val) { - this.setDataValue('source', JSON.stringify({})); - } + order: { + type: DataTypes.INTEGER, }, - context_id: { + contextId: { type: DataTypes.INTEGER }, - layer_id: { + layerId: { type: DataTypes.INTEGER } }, { + 'indexes': [{ + 'unique': true, + 'fields': ['contextId', 'layerId'] + }, { + 'fields': ['contextId'] + }, { + 'fields': ['layerId'] + }], 'tableName': 'layerContext', 'timestamps': true } @@ -77,7 +81,7 @@ export default function define(sequelize: Sequelize.Sequelize, DataTypes) { unique: false }, foreignKey: { - name: 'layer_id', + name: 'layerId', allowNull: false } }); @@ -88,7 +92,7 @@ export default function define(sequelize: Sequelize.Sequelize, DataTypes) { unique: false }, foreignKey: { - name: 'context_id', + name: 'contextId', allowNull: false } }); diff --git a/src/layerContext/layerContext.test.ts b/src/layerContext/layerContext.test.ts new file mode 100644 index 0000000..9a36612 --- /dev/null +++ b/src/layerContext/layerContext.test.ts @@ -0,0 +1,939 @@ +import * as test from 'tape'; +import * as Server from '../server'; +import * as Configs from '../configurations'; + +const serverConfigs = Configs.getServerConfig(); +const testConfigs = Configs.getTestConfig(); +const admin = testConfigs.admin; +const user1 = testConfigs.user1; +const user2 = testConfigs.user2; + +Server.init(serverConfigs).then((server) => { + + test('POST /contexts - before layerContext - Context 1', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + uri: 'user1Private', + title: 'user1Private', + scope: 'private', + map: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts - before layerContext - context 2', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + uri: 'user2Private', + title: 'user2Private', + scope: 'private', + map: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts - before layerContext - context 3', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + uri: 'user2Public', + title: 'user2Public', + scope: 'public', + map: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts - before layerContext - context 4', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + uri: 'user2PublicWrite', + title: 'user2PublicWrite', + scope: 'public', + map: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts - before layerContext - context 5', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + uri: 'user2Protected', + title: 'user2Protected', + scope: 'protected', + map: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts - before layerContext - context 6', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + uri: 'user2ProtectedWrite', + title: 'user2ProtectedWrite', + scope: 'protected', + map: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts/4/permissions - before layerContext', function(t) { + const options = { + method: 'POST', + url: '/contexts/4/permissions', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + typePermission: 'write', + profil: user1.xConsumerUsername + } + }; + server.inject(options, function(response) { + server.stop(t.end); + }); + }); + + test('POST /contexts/6/permissions - before layerContext', function(t) { + const options = { + method: 'POST', + url: '/contexts/6/permissions', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + typePermission: 'write', + profil: user1.xConsumerUsername + } + }; + server.inject(options, function(response) { + server.stop(t.end); + }); + }); + + + test('POST /layers - before layerContext', function(t) { + const options = { + method: 'POST', + url: '/layers', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + title: 'dummyTitle', + type: 'osm', + view: {}, + source: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /layers - before layerContext', function(t) { + const options = { + method: 'POST', + url: '/layers', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + title: 'dummyTitle2', + type: 'wfs', + view: {}, + source: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + // =========================================================== + + test('POST /contexts/1/layers - user1', function(t) { + const options = { + method: 'POST', + url: '/contexts/1/layers', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + layerId: 1, + view: { + minZoom: 5 + }, + order: 2 + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.layerId, 1); + t.equal(result.order, 2); + t.equal(Number(result.contextId), 1); + t.equal(result.view.minZoom, 5); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts/1/layers - user1', function(t) { + const options = { + method: 'POST', + url: '/contexts/1/layers', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + layerId: 2, + view: { + minZoom: 4 + }, + order: 1 + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.layerId, 2); + t.equal(result.order, 1); + t.equal(Number(result.contextId), 1); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts/2/layers - user1', function(t) { + const options = { + method: 'POST', + url: '/contexts/2/layers', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + layerId: 1 + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('POST /contexts/3/layers - user1', function(t) { + const options = { + method: 'POST', + url: '/contexts/3/layers', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + layerId: 1 + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('POST /contexts/4/layers - user1', function(t) { + const options = { + method: 'POST', + url: '/contexts/4/layers', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + layerId: 1 + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.layerId, 1); + t.equal(Number(result.contextId), 4); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts/5/layers - user1', function(t) { + const options = { + method: 'POST', + url: '/contexts/5/layers', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + layerId: 1 + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('POST /contexts/6/layers - user1', function(t) { + const options = { + method: 'POST', + url: '/contexts/6/layers', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + layerId: 1 + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.layerId, 1); + t.equal(Number(result.contextId), 6); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts/10/layers - user1', function(t) { + const options = { + method: 'POST', + url: '/contexts/1/layers', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + layerId: 10, + view: { + minZoom: 5 + } + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Layer can not be found.'); + t.equal(response.statusCode, 400); + server.stop(t.end); + }); + }); + + test('POST /contexts/6/layers - user2', function(t) { + const options = { + method: 'POST', + url: '/contexts/6/layers', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + layerId: 2 + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.layerId, 2); + t.equal(Number(result.contextId), 6); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts/6/layers - user2', function(t) { + const options = { + method: 'POST', + url: '/contexts/6/layers', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + layerId: 2 + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'The pair contextId and layerId must be unique.'); + t.equal(response.statusCode, 409); + server.stop(t.end); + }); + }); + + test('POST /contexts/3/layers - user2', function(t) { + const options = { + method: 'POST', + url: '/contexts/3/layers', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + layerId: 2 + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.layerId, 2); + t.equal(Number(result.contextId), 3); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + // =============================================== + + test('PATCH /contexts/1/layers/1 - user1 = layerId not allowed', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/1/layers/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + layerId: 1, + view: { + minZoom: 5 + } + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, '"layerId" is not allowed'); + t.equal(response.statusCode, 400); + server.stop(t.end); + }); + }); + + test('PATCH /contexts/1/layers/1 - user1', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/1/layers/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + view: { + minZoom: 3 + } + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(Number(result.layerId), 1); + t.equal(Number(result.contextId), 1); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('PATCH /contexts/2/layers/1 - user1', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/2/layers/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + view: { + minZoom: 4 + } + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('PATCH /contexts/3/layers/1 - user1', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/3/layers/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + view: { + minZoom: 8 + } + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('PATCH /contexts/4/layers/1 - user1', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/4/layers/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + view: { + minZoom: 6 + } + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(Number(result.layerId), 1); + t.equal(Number(result.contextId), 4); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('PATCH /contexts/5/layers/1 - user1', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/5/layers/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + view: { + minZoom: 9 + } + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('PATCH /contexts/6/layers/1 - user1', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/6/layers/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + view: { + minZoom: 11 + } + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(Number(result.layerId), 1); + t.equal(Number(result.contextId), 6); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('PATCH /contexts/1/layers/10 - user1', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/1/layers/10', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + view: { + minZoom: 5 + } + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 404); + server.stop(t.end); + }); + }); + +// =================================== + + test('GET /contexts/1/layers - user1', function(t) { + const options = { + method: 'GET', + url: '/contexts/1/layers', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.length, 2); + t.equal(result[0].layerId, 2); + t.equal(result[1].layerId, 1); + t.equal(result[0].view.minZoom, 4); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /contexts/2/layers - user1', function(t) { + const options = { + method: 'GET', + url: '/contexts/2/layers', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have read permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('GET /contexts/3/layers - user1', function(t) { + const options = { + method: 'GET', + url: '/contexts/3/layers', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.length, 1); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /contexts/4/layers - user1', function(t) { + const options = { + method: 'GET', + url: '/contexts/4/layers', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.length, 1); + t.equal(result[0].view.minZoom, 6); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /contexts/5/layers - user1', function(t) { + const options = { + method: 'GET', + url: '/contexts/5/layers', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have read permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('GET /contexts/6/layers - user1', function(t) { + const options = { + method: 'GET', + url: '/contexts/6/layers', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.length, 2); + t.equal(result[0].view.minZoom, 11); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + // ============================================ + + test('GET /contexts/6/layers/2 - user1', function(t) { + const options = { + method: 'GET', + url: '/contexts/6/layers/2', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.layerId, 2); + t.equal(result.contextId, 6); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('DELETE /contexts/6/layers/2 - user1', function(t) { + const options = { + method: 'DELETE', + url: '/contexts/6/layers/2', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 204); + server.stop(t.end); + }); + }); + + test('GET /contexts/6/layers/2 - user1', function(t) { + const options = { + method: 'GET', + url: '/contexts/6/layers/2', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 404); + server.stop(t.end); + }); + }); + + // ====================================================== + + test('GET /contexts/1/details - layerContext 1', function(t) { + const options = { + method: 'GET', + url: '/contexts/1/details', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.layers.length, 2); + t.equal(result.layers[0].title, 'dummyTitle2'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /contexts/2/details - layerContext 2 ', function(t) { + const options = { + method: 'GET', + url: '/contexts/2/details', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.layers.length, 0); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + let idContextWithLayer; + test('POST /contexts - layerContext 1 ', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + uri: 'withLayer', + title: 'withLayer', + scope: 'public', + map: {}, + layers: [{ + id: '1', + order: '4' + }, { + id: '90' + }, { + id: '2', + order: '2' + }, { + id: '3', + order: '1' + }, { + title: 'dummyTitleLayerContext', + type: 'wms', + view: {}, + source: { + url: 'http://source.com' + }, + order: '3' + }] + } + }; + server.inject(options, function(response) { + const result: any = response.result; + idContextWithLayer = result.id; + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('GET /contexts/{id}/details - context with layer', function(t) { + const options = { + method: 'GET', + url: `/contexts/${idContextWithLayer}/details`, + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.uri, 'withLayer'); + t.equal(result.layers.length, 3); + t.equal(result.layers[2].id, 1); + t.equal(result.layers[0].id, 2); + t.equal(result.layers[1].title, 'dummyTitleLayerContext'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + let idContextClonedWithLayer; + test('POST /contexts/{id}/clone - context with layer', function(t) { + const options = { + method: 'POST', + url: `/contexts/${idContextWithLayer}/clone`, + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + idContextClonedWithLayer = result.id; + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('GET /contexts/{id}/details - context cloned with layer', function(t) { + const options = { + method: 'GET', + url: `/contexts/${idContextClonedWithLayer}/details`, + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.layers.length, 3); + t.equal(result.layers[2].id, 1); + t.equal(result.layers[0].id, 2); + t.equal(result.layers[1].title, 'dummyTitleLayerContext'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + +}); diff --git a/src/layerContext/layerContext.ts b/src/layerContext/layerContext.ts new file mode 100644 index 0000000..5366a8e --- /dev/null +++ b/src/layerContext/layerContext.ts @@ -0,0 +1,198 @@ +import * as Rx from 'rxjs'; +import * as Boom from 'boom'; +import * as async from 'async'; + +import { IDatabase, database } from '../database'; +import { ObjectUtils } from '../utils'; + +import { Layer, ILayer, LayerInstance } from '../layer'; +import { ILayerContext, LayerContextInstance } from './layerContext.model'; + +export class LayerContext { + + private database: IDatabase = database; + private layer: Layer = new Layer(); + + constructor() {} + + public create( + layerContext: ILayerContext): Rx.Observable { + + return Rx.Observable.create(observer => { + this.database.layerContext.create(layerContext) + .then((createdLayerContext) => { + observer.next(createdLayerContext); + observer.complete(); + }).catch((error) => { + const uniqueFields = ['contextId', 'layerId']; + if (error.name === 'SequelizeUniqueConstraintError' && + error.fields.toString() === uniqueFields.toString()) { + const message = 'The pair contextId and layerId must be unique.'; + observer.error(Boom.conflict(message)); + } else if (error.name === 'SequelizeForeignKeyConstraintError') { + const message = 'Layer can not be found.'; + observer.error(Boom.badRequest(message)); + } else { + observer.error(Boom.badImplementation(error)); + } + }); + }); + } + + public update(contextId: string, layerId: string, + layerContext: ILayerContext): Rx.Observable { + + return Rx.Observable.create(observer => { + this.database.layerContext.update(layerContext, { + where: { + layerId: layerId, + contextId: contextId + } + }).then((count: [number, LayerContextInstance[]]) => { + if (count[0]) { + observer.next({ + layerId: layerId, + contextId: contextId + }); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public delete(contextId: string, layerId: string): Rx.Observable<{}> { + return Rx.Observable.create(observer => { + this.database.layerContext.destroy({ + where: { + layerId: layerId, + contextId: contextId + } + }).then((count: number) => { + if (count) { + observer.next({}); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public deleteByContextId(contextId: string): Rx.Observable<{}> { + return Rx.Observable.create(observer => { + this.database.layerContext.destroy({ + where: { + contextId: contextId + } + }).then((count: number) => { + if (count) { + observer.next({}); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public getByContextId( + contextId: string): Rx.Observable { + + return Rx.Observable.create(observer => { + this.database.layerContext.findAll({ + where: { + contextId: contextId, + }, + order: ['order'] + }).then((layerContextsContexts: LayerContextInstance[]) => { + const plainLayerContextsContexts = layerContextsContexts.map( + (layerContext) => ObjectUtils.removeNull(layerContext.get()) + ); + observer.next(plainLayerContextsContexts); + observer.complete(); + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public getById(contextId: string, + layerId: string): Rx.Observable { + + return Rx.Observable.create(observer => { + this.database.layerContext.findOne({ + where: { + layerId: layerId, + contextId: contextId + } + }).then((layerContext: LayerContextInstance) => { + if (layerContext) { + observer.next(ObjectUtils.removeNull(layerContext.get())); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public bulkCreate( + contextId: string, + layers: ILayer[], + createLayerIfNotExist = false) { + + const createFct = (layerId, order, next) => { + this.create({ + contextId: contextId, + layerId: layerId, + order: order + }).subscribe( + (rep) => next(), + (error) => next(error) + ); + }; + + return Rx.Observable.create(observer => { + async.forEach(layers, + (layer: ILayer, next) => { + this.layer.getBySource(layer).subscribe( + (layerFound: LayerInstance) => { + createFct(layerFound.id, layer.order, next); + }, + (error: Boom.BoomError) => { + if (createLayerIfNotExist) { + this.layer.create(layer).subscribe( + (layerCreated) => { + createFct(layerCreated.id, layer.order, next); + }, + (createError) => next(createError) + ); + } else { + next(error); + } + } + ); + }, + (error) => { + if (error) { + observer.error(error); + } else { + observer.next(); + observer.complete(); + } + } + ); + }); + } + +} diff --git a/src/layerContext/layerContext.validator.ts b/src/layerContext/layerContext.validator.ts new file mode 100644 index 0000000..6c8f0b9 --- /dev/null +++ b/src/layerContext/layerContext.validator.ts @@ -0,0 +1,24 @@ +import * as Joi from 'joi'; + +export class LayerContextValidator { + + static createModel = Joi.object().keys({ + layerId: Joi.number().required(), + view: Joi.object().keys({ + attribution: Joi.string().allow(''), + minZoom: Joi.number(), + maxZoom: Joi.number() + }), + order: Joi.number() + }); + + static updateModel = Joi.object().keys({ + view: Joi.object().keys({ + attribution: Joi.string().allow(''), + minZoom: Joi.number(), + maxZoom: Joi.number() + }), + order: Joi.number() + }); + +} diff --git a/src/layerContext/routes.ts b/src/layerContext/routes.ts new file mode 100644 index 0000000..72f4c8e --- /dev/null +++ b/src/layerContext/routes.ts @@ -0,0 +1,152 @@ +import * as Hapi from 'hapi'; +import * as Joi from 'joi'; + +import { LayerContextController } from './layerContext.controller'; +import { LayerContextValidator } from './layerContext.validator'; +import { ContextPermissionValidator } from '../contextPermission'; + + +export default function (server: Hapi.Server) { + + const layerContextController = new LayerContextController(); + server.bind(layerContextController); + + server.route({ + method: 'GET', + path: '/contexts/{contextId}/layers', + config: { + handler: layerContextController.getByContextId, + tags: ['api', 'layerContext', 'layers', 'contexts'], + description: 'Get layers by context id.', + validate: { + params: { + contextId: Joi.string().required() + }, + headers: ContextPermissionValidator.readPermission + }, + plugins: { + 'hapi-swagger': { + responses: { + '200': { + 'description': 'Layers founded.' + }, + '404': { + 'description': 'Context does not exists.' + } + } + } + } + } + }); + + server.route({ + method: 'GET', + path: '/contexts/{contextId}/layers/{layerId}', + config: { + handler: layerContextController.getById, + tags: ['api', 'layerContext'], + description: 'Get layerContext by id.', + validate: { + params: { + layerId: Joi.string().required(), + contextId: Joi.string().required() + }, + headers: ContextPermissionValidator.readPermission + }, + plugins: { + 'hapi-swagger': { + responses: { + '200': { + 'description': 'LayerContext founded.' + }, + '404': { + 'description': 'LayerContext does not exists.' + } + } + } + } + } + }); + + server.route({ + method: 'DELETE', + path: '/contexts/{contextId}/layers/{layerId}', + config: { + handler: layerContextController.delete, + tags: ['api', 'layerContext'], + description: 'Delete layerContext by id.', + validate: { + params: { + layerId: Joi.string().required(), + contextId: Joi.string().required() + }, + headers: ContextPermissionValidator.writePermission + }, + plugins: { + 'hapi-swagger': { + responses: { + '204': { + 'description': 'Deleted LayerContext.', + }, + '404': { + 'description': 'LayerContext does not exists.' + } + } + } + } + } + }); + + server.route({ + method: 'PATCH', + path: '/contexts/{contextId}/layers/{layerId}', + config: { + handler: layerContextController.update, + tags: ['api', 'layerContext'], + description: 'Update layerContext by id.', + validate: { + params: { + layerId: Joi.string().required(), + contextId: Joi.string().required() + }, + payload: LayerContextValidator.updateModel, + headers: ContextPermissionValidator.writePermission + }, + plugins: { + 'hapi-swagger': { + responses: { + '200': { + 'description': 'Deleted LayerContext.', + }, + '404': { + 'description': 'LayerContext does not exists.' + } + } + } + } + } + }); + + server.route({ + method: 'POST', + path: '/contexts/{contextId}/layers', + config: { + handler: layerContextController.create, + tags: ['api', 'layerContext'], + description: 'Create a layerContext.', + validate: { + payload: LayerContextValidator.createModel, + headers: ContextPermissionValidator.writePermission + }, + plugins: { + 'hapi-swagger': { + responses: { + '201': { + 'description': 'Created LayerContext.' + } + } + } + } + } + }); +} diff --git a/src/layers/index.ts b/src/layers/index.ts deleted file mode 100644 index 0cc53d9..0000000 --- a/src/layers/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as Hapi from 'hapi'; -import Routes from './routes'; -import { IDatabase } from '../database'; -import { IServerConfigurations } from '../configurations'; - -export function init(server: Hapi.Server, - configs: IServerConfigurations, - database: IDatabase) { - Routes(server, configs, database); -} diff --git a/src/layers/layer.controller.ts b/src/layers/layer.controller.ts deleted file mode 100644 index 5d9f700..0000000 --- a/src/layers/layer.controller.ts +++ /dev/null @@ -1,87 +0,0 @@ -import * as Hapi from 'hapi'; -import * as Boom from 'boom'; -import { ILayer, LayerInstance } from './layer.model'; -import { IDatabase } from '../database'; -import { IServerConfigurations } from '../configurations'; - -export default class LayerController { - - private database: IDatabase; - private configs: IServerConfigurations; - - constructor(configs: IServerConfigurations, database: IDatabase) { - this.configs = configs; - this.database = database; - } - - public createLayer(request: Hapi.Request, reply: Hapi.IReply) { - const newLayer: ILayer = request.payload; - this.database.layer.create(newLayer).then((layer) => { - reply(layer).code(201); - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - - public updateLayer(request: Hapi.Request, reply: Hapi.IReply) { - const id = request.params['id']; - const layer: ILayer = request.payload; - - this.database.layer.update(layer, { - where: { - id: id - } - }).then((count: [number, LayerInstance[]]) => { - if (count[0]) { - reply({}); - } else { - reply(Boom.notFound()); - } - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - - public deleteLayer(request: Hapi.Request, reply: Hapi.IReply) { - const id = request.params['id']; - this.database.layer.destroy({ - where: { - id: id - } - }).then((count: number) => { - if (count) { - reply({}); - } else { - reply(Boom.notFound()); - } - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - - public getLayerById(request: Hapi.Request, reply: Hapi.IReply) { - const id = request.params['id']; - this.database.layer.findOne({ - where: { - id: id - } - }).then((layer: LayerInstance) => { - if (layer) { - reply(layer); - } else { - reply(Boom.notFound()); - } - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - - public getLayers(request: Hapi.Request, reply: Hapi.IReply) { - this.database.layer.findAll() - .then((layers: Array) => { - reply(layers); - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } -} diff --git a/src/layers/layer.model.ts b/src/layers/layer.model.ts deleted file mode 100644 index d2552ee..0000000 --- a/src/layers/layer.model.ts +++ /dev/null @@ -1,91 +0,0 @@ -import * as Sequelize from 'sequelize'; - -interface ViewLayer { - attribution: string; - minZoom: number; - maxZoom: number; -}; - -interface SourceLayer { - url: string; -}; - -export interface ILayer { - title: string; - type: string; - url: string; - protected: boolean; - view: ViewLayer; - source: SourceLayer; -}; - -export interface LayerInstance extends Sequelize.Instance { - id: string; - createdAt: Date; - updatedAt: Date; - - title: string; - type: string; - url: string; - protected: boolean; - view: ViewLayer; - source: SourceLayer; -} - -export interface LayerModel - extends Sequelize.Model { } - - -export default function define(sequelize: Sequelize.Sequelize, DataTypes) { - const layer = sequelize.define('layer', { - 'id': { - 'type': DataTypes.INTEGER, - 'allowNull': false, - 'primaryKey': true, - 'autoIncrement': true - }, - 'title': { - 'type': DataTypes.STRING(64), - 'allowNull': false - }, - 'type': { - 'type': DataTypes.STRING(32), - 'allowNull': false - }, - 'url': { - 'type': DataTypes.STRING(255), - 'validate': { - 'isUrl': true - } - }, - 'protected': { - 'type': DataTypes.BOOLEAN - }, - 'view': { - 'type': DataTypes.TEXT, - 'get': function() { - return JSON.parse(this.getDataValue('view')); - }, - 'set': function(val) { - this.setDataValue('view', JSON.stringify({})); - } - }, - 'source': { - 'type': DataTypes.TEXT, - 'get': function() { - return JSON.parse(this.getDataValue('source')); - }, - 'set': function(val) { - this.setDataValue('source', JSON.stringify({})); - } - } - }, - { - 'tableName': 'layer', - 'timestamps': true - }); - - layer.sync(); - - return layer; -} diff --git a/src/layers/layer.test.ts b/src/layers/layer.test.ts deleted file mode 100644 index ccf75ed..0000000 --- a/src/layers/layer.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as test from 'tape'; -// import LayerCont from './layer.controller'; -import * as Server from '../server'; -import * as Configs from '../configurations'; - -const serverConfigs = Configs.getServerConfigs(); - -Server.init(serverConfigs).then((server) => { - - test('Basic HTTP Tests - GET /layers', function(t) { - const options = { - method: 'GET', - url: '/layers' - }; - server.inject(options, function(response) { - t.equal(response.statusCode, 200); - t.equal(response.result.length, 0); - server.stop(t.end); - }); - }); - - - test('Basic HTTP Tests - GET /layers/{id}', function(t) { - const options = { - method: 'GET', - url: '/layers/2' - }; - server.inject(options, function(response) { - t.equal(response.statusCode, 404); - server.stop(t.end); - }); - }); - - - test('Basic HTTP Tests - POST /layers', function(t) { - const options = { - method: 'POST', - url: '/layers', - payload: { - title: 'dummy', - type: 'osm', - protected: false, - view: {}, - source: {} - } - }; - server.inject(options, function(response) { - t.equal(response.statusCode, 201); - server.stop(t.end); - }); - }); - - -}); diff --git a/src/layers/layer.validator.ts b/src/layers/layer.validator.ts deleted file mode 100644 index 946c38e..0000000 --- a/src/layers/layer.validator.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as Joi from 'joi'; - -export const createLayerModel = Joi.object().keys({ - title: Joi.string().required().max(64), - type: Joi.string().required().max(32), - url: Joi.string(), - protected: Joi.boolean(), - view: Joi.object().required().keys({ - attribution: Joi.string(), - minZoom: Joi.number(), - maxZoom: Joi.number() - }), - source: Joi.object().required().keys({ - url: Joi.string() - }) -}); - -export const updateLayerModel = Joi.object().keys({ - title: Joi.string().max(64), - type: Joi.string().max(32), - url: Joi.string(), - protected: Joi.boolean(), - view: Joi.object().required().keys({ - attribution: Joi.string(), - minZoom: Joi.number(), - maxZoom: Joi.number() - }), - source: Joi.object().required().keys({ - url: Joi.string() - }) -}); diff --git a/src/layersContexts/index.ts b/src/layersContexts/index.ts deleted file mode 100644 index 0cc53d9..0000000 --- a/src/layersContexts/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as Hapi from 'hapi'; -import Routes from './routes'; -import { IDatabase } from '../database'; -import { IServerConfigurations } from '../configurations'; - -export function init(server: Hapi.Server, - configs: IServerConfigurations, - database: IDatabase) { - Routes(server, configs, database); -} diff --git a/src/layersContexts/layerContext.controller.ts b/src/layersContexts/layerContext.controller.ts deleted file mode 100644 index 79af138..0000000 --- a/src/layersContexts/layerContext.controller.ts +++ /dev/null @@ -1,105 +0,0 @@ -import * as Hapi from 'hapi'; -import * as Boom from 'boom'; -import { ILayerContext, LayerContextInstance } from './layerContext.model'; -import { IDatabase } from '../database'; -import { IServerConfigurations } from '../configurations'; - -export default class LayerContextController { - - private database: IDatabase; - private configs: IServerConfigurations; - - constructor(configs: IServerConfigurations, database: IDatabase) { - this.configs = configs; - this.database = database; - } - - public createLayerContext(request: Hapi.Request, reply: Hapi.IReply) { - const newLayerContext: ILayerContext = request.payload; - this.database.layerContext.create(newLayerContext) - .then((layerContext) => { - reply(layerContext).code(201); - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - - public updateLayerContext(request: Hapi.Request, reply: Hapi.IReply) { - const id = request.params['id']; - const layerContext: ILayerContext = request.payload; - - this.database.layerContext.update(layerContext, { - where: { - id: id - } - }).then((count: [number, LayerContextInstance[]]) => { - if (count[0]) { - reply({}); - } else { - reply(Boom.notFound()); - } - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - - public deleteLayerContext(request: Hapi.Request, reply: Hapi.IReply) { - const id = request.params['id']; - this.database.layerContext.destroy({ - where: { - id: id - } - }).then((count: number) => { - if (count) { - reply({}); - } else { - reply(Boom.notFound()); - } - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - - public getLayerContextById(request: Hapi.Request, reply: Hapi.IReply) { - const id = request.params['id']; - this.database.layerContext.findOne({ - where: { - id: id - } - }).then((layerContext: LayerContextInstance) => { - if (layerContext) { - reply(layerContext); - } else { - reply(Boom.notFound()); - } - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - - public getlayersContexts(request: Hapi.Request, reply: Hapi.IReply) { - this.database.layerContext.findAll() - .then((layersContexts: Array) => { - reply(layersContexts); - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - - - public getLayersByContextId(request: Hapi.Request, reply: Hapi.IReply) { - const id = request.params['id']; - - this.database.context.findAll({ - include: [ this.database.layer ], - where: { - id: id - } - }).then((layersContexts: Array) => { - reply(layersContexts); - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - -} diff --git a/src/layersContexts/layerContext.test.ts b/src/layersContexts/layerContext.test.ts deleted file mode 100644 index c49a941..0000000 --- a/src/layersContexts/layerContext.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import * as test from 'tape'; -// import LayerContextCont from './layerContext.controller'; -import * as Server from '../server'; -import * as Configs from '../configurations'; - -const serverConfigs = Configs.getServerConfigs(); - -Server.init(serverConfigs).then((server) => { - - test('Basic HTTP Tests - GET /layersContexts', function(t) { - const options = { - method: 'GET', - url: '/layersContexts' - }; - server.inject(options, function(response) { - t.equal(response.statusCode, 200); - t.equal(response.result.length, 0); - server.stop(t.end); - }); - }); - - - test('Basic HTTP Tests - GET /layersContexts/{id}', function(t) { - const options = { - method: 'GET', - url: '/layersContexts/2' - }; - server.inject(options, function(response) { - t.equal(response.statusCode, 404); - server.stop(t.end); - }); - }); - - - test('Basic HTTP Tests - POST /layersContexts', function(t) { - const options = { - method: 'POST', - url: '/layersContexts', - payload: { - context_id: 1, - layer_id: 1, - view: {}, - source: {} - } - }; - server.inject(options, function(response) { - t.equal(response.statusCode, 201); - server.stop(t.end); - }); - }); - - -}); diff --git a/src/layersContexts/layerContext.validator.ts b/src/layersContexts/layerContext.validator.ts deleted file mode 100644 index 8add485..0000000 --- a/src/layersContexts/layerContext.validator.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as Joi from 'joi'; - -export const createLayerContextModel = Joi.object().keys({ - context_id: Joi.number().required(), - layer_id: Joi.number().required(), - view: Joi.object().required().keys({ - attribution: Joi.string(), - minZoom: Joi.number(), - maxZoom: Joi.number() - }), - source: Joi.object().required().keys({ - url: Joi.string() - }) -}); - -export const updateLayerContextModel = Joi.object().keys({ - context_id: Joi.number(), - layer_id: Joi.number(), - view: Joi.object().keys({ - attribution: Joi.string(), - minZoom: Joi.number(), - maxZoom: Joi.number() - }), - source: Joi.object().keys({ - url: Joi.string() - }) -}); diff --git a/src/layersContexts/routes.ts b/src/layersContexts/routes.ts deleted file mode 100644 index 4cf64dc..0000000 --- a/src/layersContexts/routes.ts +++ /dev/null @@ -1,180 +0,0 @@ -import * as Hapi from 'hapi'; -import * as Joi from 'joi'; -import LayerContextController from './layerContext.controller'; -import * as LayerContextValidator from './layerContext.validator'; -// import { jwtValidator } from '../users/user-validator'; -import { IDatabase } from '../database'; -import { IServerConfigurations } from '../configurations'; - -export default function (server: Hapi.Server, - configs: IServerConfigurations, - database: IDatabase) { - - const layerContextController = new LayerContextController(configs, database); - server.bind(layerContextController); - - server.route({ - method: 'GET', - path: '/contexts/{id}/layers', - config: { - handler: layerContextController.getLayersByContextId, - // auth: 'jwt', - auth: false, - tags: ['api', 'layersContexts', 'layers', 'contexts'], - description: 'Get layers by context id.', - validate: { - params: { - id: Joi.string().required() - } - // headers: jwtValidator - }, - plugins: { - 'hapi-swagger': { - responses: { - '200': { - 'description': 'Layers founded.' - }, - '404': { - 'description': 'Context does not exists.' - } - } - } - } - } - }); - - server.route({ - method: 'GET', - path: '/layersContexts/{id}', - config: { - handler: layerContextController.getLayerContextById, - // auth: 'jwt', - auth: false, - tags: ['api', 'layersContexts'], - description: 'Get layersContexts by id.', - validate: { - params: { - id: Joi.string().required() - } - // headers: jwtValidator - }, - plugins: { - 'hapi-swagger': { - responses: { - '200': { - 'description': 'LayerContext founded.' - }, - '404': { - 'description': 'LayerContext does not exists.' - } - } - } - } - } - }); - - server.route({ - method: 'GET', - path: '/layersContexts', - config: { - handler: layerContextController.getlayersContexts, - // auth: 'jwt', - auth: false, - tags: ['api', 'layersContexts'], - description: 'Get all layersContexts.', - validate: { - query: { - // top: Joi.number().default(5), - // skip: Joi.number().default(0) - } - // headers: jwtValidator - } - } - }); - - server.route({ - method: 'DELETE', - path: '/layersContexts/{id}', - config: { - handler: layerContextController.deleteLayerContext, - // auth: 'jwt', - auth: false, - tags: ['api', 'layersContexts'], - description: 'Delete layerContext by id.', - validate: { - params: { - id: Joi.string().required() - } - // headers: jwtValidator - }, - plugins: { - 'hapi-swagger': { - responses: { - '200': { - 'description': 'Deleted LayerContext.', - }, - '404': { - 'description': 'LayerContext does not exists.' - } - } - } - } - } - }); - - server.route({ - method: 'PUT', - path: '/layersContexts/{id}', - config: { - handler: layerContextController.updateLayerContext, - // auth: 'jwt', - auth: false, - tags: ['api', 'layersContexts'], - description: 'Update layerContext by id.', - validate: { - params: { - id: Joi.string().required() - }, - payload: LayerContextValidator.updateLayerContextModel - // headers: jwtValidator - }, - plugins: { - 'hapi-swagger': { - responses: { - '200': { - 'description': 'Deleted LayerContext.', - }, - '404': { - 'description': 'LayerContext does not exists.' - } - } - } - } - } - }); - - server.route({ - method: 'POST', - path: '/layersContexts', - config: { - handler: layerContextController.createLayerContext, - // auth: 'jwt', - auth: false, - tags: ['api', 'layersContexts'], - description: 'Create a layerContext.', - validate: { - payload: LayerContextValidator.createLayerContextModel - // headers: jwtValidator - }, - plugins: { - 'hapi-swagger': { - responses: { - '201': { - 'description': 'Created LayerContext.' - } - } - } - } - } - }); -} diff --git a/src/plugins/interfaces.ts b/src/plugins/interfaces.ts index ccd7021..fcf6943 100644 --- a/src/plugins/interfaces.ts +++ b/src/plugins/interfaces.ts @@ -1,11 +1,11 @@ import * as Hapi from 'hapi'; import { IDatabase } from '../database'; -import { IServerConfigurations } from '../configurations'; +import { IServerConfiguration } from '../configurations'; export interface IPluginOptions { database?: IDatabase; - serverConfigs: IServerConfigurations; + serverConfigs: IServerConfiguration; } export interface IPlugin { diff --git a/src/plugins/jwt-auth/index.ts b/src/plugins/jwt-auth/index.ts deleted file mode 100644 index b070ea5..0000000 --- a/src/plugins/jwt-auth/index.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { IPlugin, IPluginOptions } from '../interfaces'; -import * as Hapi from 'hapi'; -// import { IUser, UserModel } from '../../users/user'; - -export default (): IPlugin => { - return { - register: (server: Hapi.Server, options: IPluginOptions): Promise => { - return new Promise(resolve => { - // const database = options.database; - const serverConfig = options.serverConfigs; - - const validateUser = (decoded, request, cb) => { - /*database.userModel.findById(decoded.id).lean(true) - .then((user: IUser) => { - if (!user) { - return cb(null, false); - } - - return cb(null, true); - });*/ - return cb(null, true); - }; - - server.register(require('hapi-auth-jwt2'), (error) => { - if (error) { - console.log('error', error); - } else { - server.auth.strategy('jwt', 'jwt', true, { - key: serverConfig.jwtSecret, - validateFunc: validateUser, - verifyOptions: { algorithms: ['HS256'] } - }); - } - - resolve(); - }); - }); - }, - info: () => { - return { - name: 'JWT Authentication', - version: '1.0.0' - }; - } - }; -}; diff --git a/src/poi/index.ts b/src/poi/index.ts new file mode 100644 index 0000000..47bc3be --- /dev/null +++ b/src/poi/index.ts @@ -0,0 +1,9 @@ +import * as Hapi from 'hapi'; +import Routes from './routes'; + +export function init(server: Hapi.Server) { + Routes(server); +} + +export * from './poi.model' +export * from './poi' diff --git a/src/poi/poi.controller.ts b/src/poi/poi.controller.ts new file mode 100644 index 0000000..78a6149 --- /dev/null +++ b/src/poi/poi.controller.ts @@ -0,0 +1,64 @@ +import * as Hapi from 'hapi'; +import * as Boom from 'boom'; + +import { POI } from './poi'; +import { IPOI, POIInstance } from './poi.model'; + +export class POIController { + + private poi: POI; + + constructor() { + this.poi = new POI(); + } + + public create(request: Hapi.Request, reply: Hapi.IReply) { + const poiToCreate: IPOI = request.payload; + poiToCreate.userId = request.headers['x-consumer-custom-id']; + + this.poi.create(poiToCreate).subscribe( + (poi: POIInstance) => reply(poi).code(201), + (error: Boom.BoomError) => reply(error) + ); + } + + public update(request: Hapi.Request, reply: Hapi.IReply) { + const id = request.params['id']; + const userId = request.headers['x-consumer-custom-id']; + const poiToUpdate: IPOI = request.payload; + + this.poi.update(id, userId, poiToUpdate).subscribe( + (poi: POIInstance) => reply(poi), + (error: Boom.BoomError) => reply(error) + ); + } + + public delete(request: Hapi.Request, reply: Hapi.IReply) { + const id = request.params['id']; + const userId = request.headers['x-consumer-custom-id']; + + this.poi.delete(id, userId).subscribe( + (poi: POIInstance) => reply(poi).code(204), + (error: Boom.BoomError) => reply(error) + ); + } + + public getById(request: Hapi.Request, reply: Hapi.IReply) { + const id = request.params['id']; + const userId = request.headers['x-consumer-custom-id']; + + this.poi.getById(id, userId).subscribe( + (poi: POIInstance) => reply(poi), + (error: Boom.BoomError) => reply(error) + ); + } + + public get(request: Hapi.Request, reply: Hapi.IReply) { + const userId = request.headers['x-consumer-custom-id']; + + this.poi.get(userId).subscribe( + (pois: POIInstance[]) => reply(pois), + (error: Boom.BoomError) => reply(error) + ); + } +} diff --git a/src/poi/poi.model.ts b/src/poi/poi.model.ts new file mode 100644 index 0000000..331d248 --- /dev/null +++ b/src/poi/poi.model.ts @@ -0,0 +1,72 @@ +import * as Sequelize from 'sequelize'; + +export interface IPOI { + id?: string; + userId?: string; + title: string; + x: number; + y: number; + zoom: number; +}; + +export interface POIInstance extends Sequelize.Instance { + id: string; + createdAt: Date; + updatedAt: Date; + + userId: string; + title: string; + x: number; + y: number; + zoom: number; +} + +export interface POIModel + extends Sequelize.Model { } + + +export default function define(sequelize: Sequelize.Sequelize, DataTypes) { + const poi = sequelize.define('poi', { + 'id': { + 'type': DataTypes.INTEGER, + 'allowNull': false, + 'primaryKey': true, + 'autoIncrement': true + }, + 'title': { + 'type': DataTypes.STRING(64), + 'allowNull': false + }, + 'x': { + 'type': DataTypes.DECIMAL, + 'allowNull': false + }, + 'y': { + 'type': DataTypes.DECIMAL, + 'allowNull': false + }, + 'zoom': { + 'type': DataTypes.INTEGER(2), + 'allowNull': false + } + }, { + 'indexes': [{ + 'fields': ['userId'] + }], + 'tableName': 'poi', + 'timestamps': true + }); + + const user = sequelize.models['user']; + + user.hasMany(poi, { + foreignKey: { + name: 'userId', + allowNull: false + } + }); + + poi.sync(); + + return poi; +} diff --git a/src/poi/poi.test.ts b/src/poi/poi.test.ts new file mode 100644 index 0000000..86a9713 --- /dev/null +++ b/src/poi/poi.test.ts @@ -0,0 +1,394 @@ +import * as test from 'tape'; +import * as Server from '../server'; +import * as Configs from '../configurations'; + +const serverConfigs = Configs.getServerConfig(); +const testConfigs = Configs.getTestConfig(); +const anonyme = testConfigs.anonyme; +const user1 = testConfigs.user1; + +Server.init(serverConfigs).then((server) => { + + test('POST /users/login', function(t) { + const options = { + method: 'POST', + url: '/users/login', + payload: { + username: 'test', + password: 'testIgo2Password', + typeConnection: 'test' + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('POST /pois - headers missing', function(t) { + const options = { + method: 'POST', + url: '/pois', + payload: { + title: 'poi1', + zoom: 6, + x: -73.22, + y: 46.44 + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must be authenticated'); + t.equal(response.statusCode, 401); + server.stop(t.end); + }); + }); + + test('POST /pois - zoom missing', function(t) { + const options = { + method: 'POST', + url: '/pois', + headers: { + 'x-consumer-username': 'test', + 'x-consumer-id': user1.xConsumerId, + 'x-consumer-custom-id': '1' + }, + payload: { + title: 'poi1', + x: -73.22, + y: 46.44 + } + }; + server.inject(options, function(response) { + const result: any = response.result; + const message = 'child "zoom" fails because ["zoom" is required]'; + t.equal(result.message, message); + t.equal(response.statusCode, 400); + server.stop(t.end); + }); + }); + + test('POST /pois - anonyme', function(t) { + const options = { + method: 'POST', + url: '/pois', + headers: { + 'x-consumer-username': anonyme.xConsumerUsername, + 'x-consumer-id': anonyme.xConsumerId, + 'x-anonymous-consumer': 'true' + }, + payload: { + title: 'poi1', + zoom: 6, + x: -73.22, + y: 46.44 + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must be authenticated'); + t.equal(response.statusCode, 401); + server.stop(t.end); + }); + }); + + test('POST /pois - user1', function(t) { + const options = { + method: 'POST', + url: '/pois', + headers: { + 'x-consumer-username': 'test', + 'x-consumer-id': user1.xConsumerId, + 'x-consumer-custom-id': '1' + }, + payload: { + title: 'poi1', + zoom: 6, + x: -73.22, + y: 46.44 + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.title, 'poi1'); + t.equal(result.zoom, 6); + t.equal(result.x, -73.22); + t.equal(result.y, 46.44); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /pois - user1 bis', function(t) { + const options = { + method: 'POST', + url: '/pois', + headers: { + 'x-consumer-username': 'test', + 'x-consumer-id': user1.xConsumerId, + 'x-consumer-custom-id': '1' + }, + payload: { + title: 'poi2', + zoom: 2, + x: -53.22, + y: 26.44 + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.title, 'poi2'); + t.equal(result.zoom, 2); + t.equal(result.x, -53.22); + t.equal(result.y, 26.44); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + // ---------------------------------------------------------------- + + test('GET /pois - anonyme', function(t) { + const options = { + method: 'GET', + url: '/pois', + headers: { + 'x-consumer-username': anonyme.xConsumerUsername, + 'x-consumer-id': anonyme.xConsumerId, + 'x-anonymous-consumer': 'true' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must be authenticated'); + t.equal(response.statusCode, 401); + server.stop(t.end); + }); + }); + + test('GET /pois - user1', function(t) { + const options = { + method: 'GET', + url: '/pois', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId, + 'x-consumer-custom-id': '1' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.length, 2); + t.equal(result[0].title, 'poi1'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /pois - user2', function(t) { + const options = { + method: 'GET', + url: '/pois', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId, + 'x-consumer-custom-id': '10' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.length, 0); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + +// ---------------------------------------------------------------- + + test('PATCH /pois/{id} - anonyme', function(t) { + const options = { + method: 'PATCH', + url: '/pois/2', + headers: { + 'x-consumer-username': anonyme.xConsumerUsername, + 'x-consumer-id': anonyme.xConsumerId, + 'x-anonymous-consumer': 'true' + }, + payload: { + title: 'dummy99' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must be authenticated'); + t.equal(response.statusCode, 401); + server.stop(t.end); + }); + }); + + test('PATCH /pois/{id} - user1', function(t) { + const options = { + method: 'PATCH', + url: '/pois/2', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId, + 'x-consumer-custom-id': '1' + }, + payload: { + title: 'dummy99' + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 200); + const result: any = response.result; + t.equal(result.id, '2'); + server.stop(t.end); + }); + }); + + test('PATCH /pois/{id} - another user', function(t) { + const options = { + method: 'PATCH', + url: '/pois/2', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId, + 'x-consumer-custom-id': '9' + }, + payload: { + title: 'dummy99' + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 404); + server.stop(t.end); + }); + }); + + // ---------------------------------------------------------------- + + test('GET /pois/{id} - anonyme', function(t) { + const options = { + method: 'GET', + url: '/pois/2', + headers: { + 'x-consumer-username': anonyme.xConsumerUsername, + 'x-consumer-id': anonyme.xConsumerId, + 'x-anonymous-consumer': 'true' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must be authenticated'); + t.equal(response.statusCode, 401); + server.stop(t.end); + }); + }); + + test('GET /pois/{id} - user1', function(t) { + const options = { + method: 'GET', + url: '/pois/2', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId, + 'x-consumer-custom-id': '1' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.title, 'dummy99'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /pois/{id} - another user', function(t) { + const options = { + method: 'GET', + url: '/pois/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId, + 'x-consumer-custom-id': '9' + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 404); + server.stop(t.end); + }); + }); + + // ---------------------------------------------------------------- + + test('DELETE /pois/{id} - anonyme', function(t) { + const options = { + method: 'DELETE', + url: '/pois/2', + headers: { + 'x-consumer-username': anonyme.xConsumerUsername, + 'x-consumer-id': anonyme.xConsumerId, + 'x-anonymous-consumer': 'true' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must be authenticated'); + t.equal(response.statusCode, 401); + server.stop(t.end); + }); + }); + + test('DELETE /pois/{id} - another user', function(t) { + const options = { + method: 'DELETE', + url: '/pois/2', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId, + 'x-consumer-custom-id': '9' + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 404); + server.stop(t.end); + }); + }); + + test('DELETE /pois/{id} - user1', function(t) { + const options = { + method: 'DELETE', + url: '/pois/2', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId, + 'x-consumer-custom-id': '1' + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 204); + server.stop(t.end); + }); + }); + + + test('GET /pois - after delete', function(t) { + const options = { + method: 'GET', + url: '/pois', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId, + 'x-consumer-custom-id': '1' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.length, 1); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + +}); diff --git a/src/poi/poi.ts b/src/poi/poi.ts new file mode 100644 index 0000000..087935d --- /dev/null +++ b/src/poi/poi.ts @@ -0,0 +1,105 @@ +import * as Rx from 'rxjs'; +import * as Boom from 'boom'; + +import { IDatabase, database } from '../database'; +import { ObjectUtils } from '../utils'; + +import { IPOI, POIInstance } from './poi.model'; + +export class POI { + + private database: IDatabase = database; + + constructor() {} + + public create(poi: IPOI): Rx.Observable { + return Rx.Observable.create(observer => { + this.database.poi.create(poi).then((createdPOI) => { + observer.next(createdPOI); + observer.complete(); + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public update(id: string, userId: string, + poi: IPOI): Rx.Observable { + + return Rx.Observable.create(observer => { + this.database.poi.update(poi, { + where: { + id: id, + userId: userId + } + }).then((count: [number, POIInstance[]]) => { + if (count[0]) { + observer.next({id: id}); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public delete(id: string, userId: string): Rx.Observable<{}> { + return Rx.Observable.create(observer => { + this.database.poi.destroy({ + where: { + id: id, + userId: userId + } + }).then((count: number) => { + if (count) { + observer.next({}); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public get(userId: string): Rx.Observable { + return Rx.Observable.create(observer => { + this.database.poi.findAll({ + where: { + userId: userId + } + }).then((pois: POIInstance[]) => { + const plainPOIs = pois.map( + (poi) => ObjectUtils.removeNull(poi.get()) + ); + observer.next(plainPOIs); + observer.complete(); + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public getById(id: string, userId: string): Rx.Observable { + return Rx.Observable.create(observer => { + this.database.poi.findOne({ + where: { + id: id, + userId: userId + } + }).then((poi: POIInstance) => { + if (poi) { + observer.next(ObjectUtils.removeNull(poi.get())); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } +} diff --git a/src/poi/poi.validator.ts b/src/poi/poi.validator.ts new file mode 100644 index 0000000..8ed4550 --- /dev/null +++ b/src/poi/poi.validator.ts @@ -0,0 +1,20 @@ +import * as Joi from 'joi'; + + +export class POIValidator { + + static createModel = Joi.object().keys({ + title: Joi.string().required(), + x: Joi.number().required(), + y: Joi.number().required(), + zoom: Joi.number().required(), + }); + + static updateModel = Joi.object().keys({ + title: Joi.string(), + x: Joi.number(), + y: Joi.number(), + zoom: Joi.number(), + }); + +} diff --git a/src/poi/routes.ts b/src/poi/routes.ts new file mode 100644 index 0000000..c63d0ba --- /dev/null +++ b/src/poi/routes.ts @@ -0,0 +1,133 @@ +import * as Hapi from 'hapi'; +import * as Joi from 'joi'; + +import { POIController } from './poi.controller'; +import { POIValidator } from './poi.validator'; +import { UserValidator } from '../user/user.validator'; + +export default function (server: Hapi.Server) { + + const poiController = new POIController(); + server.bind(poiController); + + server.route({ + method: 'GET', + path: '/pois/{id}', + config: { + handler: poiController.getById, + tags: ['api', 'pois'], + description: 'Get pois by id.', + validate: { + params: { + id: Joi.string().required() + }, + headers: UserValidator.authenticateValidator + }, + plugins: { + 'hapi-swagger': { + responses: { + '200': { + 'description': 'POI founded.' + }, + '404': { + 'description': 'POI does not exists.' + } + } + } + } + } + }); + + server.route({ + method: 'GET', + path: '/pois', + config: { + handler: poiController.get, + tags: ['api', 'pois'], + description: 'Get all pois.', + validate: { + headers: UserValidator.authenticateValidator + } + } + }); + + server.route({ + method: 'DELETE', + path: '/pois/{id}', + config: { + handler: poiController.delete, + tags: ['api', 'pois'], + description: 'Delete poi by id.', + validate: { + params: { + id: Joi.string().required() + }, + headers: UserValidator.authenticateValidator + }, + plugins: { + 'hapi-swagger': { + responses: { + '204': { + 'description': 'Deleted POI.', + }, + '404': { + 'description': 'POI does not exists.' + } + } + } + } + } + }); + + server.route({ + method: 'PATCH', + path: '/pois/{id}', + config: { + handler: poiController.update, + tags: ['api', 'pois'], + description: 'Update poi by id.', + validate: { + params: { + id: Joi.string().required() + }, + payload: POIValidator.updateModel, + headers: UserValidator.authenticateValidator + }, + plugins: { + 'hapi-swagger': { + responses: { + '200': { + 'description': 'Deleted POI.', + }, + '404': { + 'description': 'POI does not exists.' + } + } + } + } + } + }); + + server.route({ + method: 'POST', + path: '/pois', + config: { + handler: poiController.create, + tags: ['api', 'pois'], + description: 'Create a poi.', + validate: { + payload: POIValidator.createModel, + headers: UserValidator.authenticateValidator + }, + plugins: { + 'hapi-swagger': { + responses: { + '201': { + 'description': 'Created POI.' + } + } + } + } + } + }); +} diff --git a/src/server.ts b/src/server.ts index de2b0a2..9c9d56a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,23 +1,23 @@ import * as Hapi from 'hapi'; import { IPlugin } from './plugins/interfaces'; -import { IServerConfigurations } from './configurations'; +import { IServerConfiguration } from './configurations'; import database from './database'; -import * as Contexts from './contexts'; -import * as Layers from './layers'; -import * as Tools from './tools'; -import * as LayersContexts from './layersContexts'; -import * as ToolsContexts from './toolsContexts'; - -export function init(configs: IServerConfigurations): Promise { +import * as Users from './user'; +import * as POIs from './poi'; +import * as Contexts from './context'; +import * as Layers from './layer'; +import * as Tools from './tool'; +import * as LayersContexts from './layerContext'; +import * as ToolsContexts from './toolContext'; +import * as ContextsPermissions from './contextPermission'; + +export function init(configs: IServerConfiguration): Promise { return new Promise(resolve => { const port = process.env.port || configs.port; const server = new Hapi.Server(); server.connection({ - port: port, - routes: { - cors: true - } + port: port }); // Setup Hapi Plugins @@ -41,11 +41,14 @@ export function init(configs: IServerConfigurations): Promise { // Init Features console.log('Routes loading'); - Contexts.init(server, configs, database); - Layers.init(server, configs, database); - Tools.init(server, configs, database); - ToolsContexts.init(server, configs, database); - LayersContexts.init(server, configs, database); + Users.init(server); + POIs.init(server); + Contexts.init(server); + Layers.init(server); + Tools.init(server); + ToolsContexts.init(server); + LayersContexts.init(server); + ContextsPermissions.init(server); console.log('Routes loaded'); resolve(server); diff --git a/src/tool/index.ts b/src/tool/index.ts new file mode 100644 index 0000000..b3e92d5 --- /dev/null +++ b/src/tool/index.ts @@ -0,0 +1,8 @@ +import * as Hapi from 'hapi'; +import Routes from './routes'; + +export function init(server: Hapi.Server) { + Routes(server); +} + +export * from './tool' diff --git a/src/tools/routes.ts b/src/tool/routes.ts similarity index 64% rename from src/tools/routes.ts rename to src/tool/routes.ts index 53b4f0f..ca65174 100644 --- a/src/tools/routes.ts +++ b/src/tool/routes.ts @@ -1,32 +1,27 @@ import * as Hapi from 'hapi'; import * as Joi from 'joi'; -import ToolController from './tool.controller'; -import * as ToolValidator from './tool.validator'; -// import { jwtValidator } from '../users/user-validator'; -import { IDatabase } from '../database'; -import { IServerConfigurations } from '../configurations'; -export default function (server: Hapi.Server, - configs: IServerConfigurations, - database: IDatabase) { +import { ToolController } from './tool.controller'; +import { ToolValidator } from './tool.validator'; +import { UserValidator } from '../user/user.validator'; - const toolController = new ToolController(configs, database); +export default function (server: Hapi.Server) { + + const toolController = new ToolController(); server.bind(toolController); server.route({ method: 'GET', path: '/tools/{id}', config: { - handler: toolController.getToolById, - // auth: 'jwt', - auth: false, + handler: toolController.getById, tags: ['api', 'tools'], description: 'Get tools by id.', validate: { params: { id: Joi.string().required() - } - // headers: jwtValidator + }, + headers: UserValidator.authenticateValidator }, plugins: { 'hapi-swagger': { @@ -47,17 +42,11 @@ export default function (server: Hapi.Server, method: 'GET', path: '/tools', config: { - handler: toolController.getTools, - // auth: 'jwt', - auth: false, + handler: toolController.get, tags: ['api', 'tools'], description: 'Get all tools.', validate: { - query: { - // top: Joi.number().default(5), - // skip: Joi.number().default(0) - } - // headers: jwtValidator + headers: UserValidator.authenticateValidator } } }); @@ -66,21 +55,19 @@ export default function (server: Hapi.Server, method: 'DELETE', path: '/tools/{id}', config: { - handler: toolController.deleteTool, - // auth: 'jwt', - auth: false, + handler: toolController.delete, tags: ['api', 'tools'], description: 'Delete tool by id.', validate: { params: { id: Joi.string().required() - } - // headers: jwtValidator + }, + headers: UserValidator.adminValidator }, plugins: { 'hapi-swagger': { responses: { - '200': { + '204': { 'description': 'Deleted Tool.', }, '404': { @@ -93,20 +80,18 @@ export default function (server: Hapi.Server, }); server.route({ - method: 'PUT', + method: 'PATCH', path: '/tools/{id}', config: { - handler: toolController.updateTool, - // auth: 'jwt', - auth: false, + handler: toolController.update, tags: ['api', 'tools'], description: 'Update tool by id.', validate: { params: { id: Joi.string().required() }, - payload: ToolValidator.updateToolModel - // headers: jwtValidator + payload: ToolValidator.updateModel, + headers: UserValidator.adminValidator }, plugins: { 'hapi-swagger': { @@ -127,14 +112,12 @@ export default function (server: Hapi.Server, method: 'POST', path: '/tools', config: { - handler: toolController.createTool, - // auth: 'jwt', - auth: false, + handler: toolController.create, tags: ['api', 'tools'], description: 'Create a tool.', validate: { - payload: ToolValidator.createToolModel - // headers: jwtValidator + payload: ToolValidator.createModel, + headers: UserValidator.adminValidator }, plugins: { 'hapi-swagger': { diff --git a/src/tool/tool.controller.ts b/src/tool/tool.controller.ts new file mode 100644 index 0000000..a39d101 --- /dev/null +++ b/src/tool/tool.controller.ts @@ -0,0 +1,58 @@ +import * as Hapi from 'hapi'; +import * as Boom from 'boom'; + +import { Tool } from './tool'; +import { ITool, ToolInstance } from './tool.model'; + +export class ToolController { + + private tool: Tool; + + constructor() { + this.tool = new Tool(); + } + + public create(request: Hapi.Request, reply: Hapi.IReply) { + const toolToCreate: ITool = request.payload; + + this.tool.create(toolToCreate).subscribe( + (tool: ToolInstance) => reply(tool).code(201), + (error: Boom.BoomError) => reply(error) + ); + } + + public update(request: Hapi.Request, reply: Hapi.IReply) { + const id = request.params['id']; + const toolToUpdate: ITool = request.payload; + + this.tool.update(id, toolToUpdate).subscribe( + (tool: ToolInstance) => reply(tool), + (error: Boom.BoomError) => reply(error) + ); + } + + public delete(request: Hapi.Request, reply: Hapi.IReply) { + const id = request.params['id']; + + this.tool.delete(id).subscribe( + (tool: ToolInstance) => reply(tool).code(204), + (error: Boom.BoomError) => reply(error) + ); + } + + public getById(request: Hapi.Request, reply: Hapi.IReply) { + const id = request.params['id']; + + this.tool.getById(id).subscribe( + (tool: ToolInstance) => reply(tool), + (error: Boom.BoomError) => reply(error) + ); + } + + public get(request: Hapi.Request, reply: Hapi.IReply) { + this.tool.get().subscribe( + (tools: ToolInstance[]) => reply(tools), + (error: Boom.BoomError) => reply(error) + ); + } +} diff --git a/src/tool/tool.model.ts b/src/tool/tool.model.ts new file mode 100644 index 0000000..d4d47ff --- /dev/null +++ b/src/tool/tool.model.ts @@ -0,0 +1,73 @@ +import * as Sequelize from 'sequelize'; + +export interface ITool { + id?: string; + name: string; + title?: string; + tooltip?: string; + icon?: string; + inToolbar?: boolean; + options?: { [key: string]: any }; +}; + +export interface ToolInstance extends Sequelize.Instance { + id: string; + createdAt: Date; + updatedAt: Date; + + name: string; + title?: string; + tooltip?: string; + icon?: string; + inToolbar?: boolean; + options?: { [key: string]: any }; +} + +export interface ToolModel + extends Sequelize.Model { } + + +export default function define(sequelize: Sequelize.Sequelize, DataTypes) { + const tool = sequelize.define('tool', { + 'id': { + 'type': DataTypes.INTEGER, + 'allowNull': false, + 'primaryKey': true, + 'autoIncrement': true + }, + 'name': { + 'type': DataTypes.STRING(64), + 'allowNull': false + }, + 'title': { + 'type': DataTypes.STRING(64) + }, + 'tooltip': { + 'type': DataTypes.STRING(128) + }, + 'icon': { + 'type': DataTypes.STRING(128) + }, + 'inToolbar': { + 'type': DataTypes.BOOLEAN + }, + 'options': { + 'type': DataTypes.TEXT, + 'get': function() { + const options = this.getDataValue('options'); + return options ? JSON.parse(options) : {}; + }, + 'set': function(val) { + this.setDataValue('options', JSON.stringify(val)); + } + } + }, + { + 'tableName': 'tool', + 'timestamps': true + }); + + tool.sync(); + + return tool; +} diff --git a/src/tool/tool.test.ts b/src/tool/tool.test.ts new file mode 100644 index 0000000..5341038 --- /dev/null +++ b/src/tool/tool.test.ts @@ -0,0 +1,461 @@ +import * as test from 'tape'; +// import ToolCont from './tool.controller'; +import * as Server from '../server'; +import * as Configs from '../configurations'; + +const serverConfigs = Configs.getServerConfig(); +const testConfigs = Configs.getTestConfig(); +const admin = testConfigs.admin; +const anonyme = testConfigs.anonyme; +const user1 = testConfigs.user1; + +Server.init(serverConfigs).then((server) => { + + test('POST /tools - admin', function(t) { + const options = { + method: 'POST', + url: '/tools', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + name: 'dummyName', + title: 'dummyTitle', + inToolbar: true, + options: {} + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.name, 'dummyName'); + t.equal(result.title, 'dummyTitle'); + t.equal(result.inToolbar, true); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /tools - admin 2', function(t) { + const options = { + method: 'POST', + url: '/tools', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + name: 'dummyName2', + title: 'dummyTitle2', + inToolbar: false + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.name, 'dummyName2'); + t.equal(result.title, 'dummyTitle2'); + t.equal(result.inToolbar, false); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /tools - admin - name missing', function(t) { + const options = { + method: 'POST', + url: '/tools', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + title: 'dummyTitle3', + inToolbar: false, + options: {} + } + }; + server.inject(options, function(response) { + const result: any = response.result; + const message = 'child "name" fails because ["name" is required]'; + t.equal(result.message, message); + t.equal(response.statusCode, 400); + server.stop(t.end); + }); + }); + + test('POST /tools - admin - another param', function(t) { + const options = { + method: 'POST', + url: '/tools', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + name: 'dummyTitle4', + title: 'dummyTitle4', + inToolbar: false, + anotherParam: false + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, '"anotherParam" is not allowed'); + t.equal(response.statusCode, 400); + server.stop(t.end); + }); + }); + + test('POST /tools - anonyme', function(t) { + const options = { + method: 'POST', + url: '/tools', + headers: { + 'x-consumer-username': anonyme.xConsumerUsername, + 'x-consumer-id': anonyme.xConsumerId + }, + payload: { + name: 'dummy', + title: 'dummy', + inToolbar: true, + options: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 403); + const result: any = response.result; + t.equal(result.message, 'Must be administrator'); + server.stop(t.end); + }); + }); + + test('POST /tools - user1', function(t) { + const options = { + method: 'POST', + url: '/tools', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + name: 'dummy', + title: 'dummy', + inToolbar: true, + options: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 403); + const result: any = response.result; + t.equal(result.message, 'Must be administrator'); + server.stop(t.end); + }); + }); + + test('POST /tools - Tool 3', function(t) { + const options = { + method: 'POST', + url: '/tools', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + name: 'Tool3', + title: 'TitleTool3', + inToolbar: true, + options: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /tools - Tool 4', function(t) { + const options = { + method: 'POST', + url: '/tools', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + name: 'tool4', + title: 'TitleTool4', + inToolbar: false + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + // ---------------------------------------------------------------- + + test('GET /tools - admin', function(t) { + const options = { + method: 'GET', + url: '/tools', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.length, 4); + t.equal(result[0].name, 'dummyName'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /tools - anonyme', function(t) { + const options = { + method: 'GET', + url: '/tools', + headers: { + 'x-consumer-username': anonyme.xConsumerUsername, + 'x-consumer-id': anonyme.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.length, 4); + t.equal(result[0].name, 'dummyName'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /tools - user1', function(t) { + const options = { + method: 'GET', + url: '/tools', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.length, 4); + t.equal(result[0].name, 'dummyName'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + // ---------------------------------------------------------------- + + test('PATCH /tools/{id} - admin', function(t) { + const options = { + method: 'PATCH', + url: '/tools/2', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + inToolbar: true, + options: { + optionParams: true + } + } + }; + server.inject(options, function(response) { + // const result: any = response.result; + // t.equal(result.name, 'dummyName2'); + // t.equal(result.inToolbar, true); + // t.equal(result.options.optionParams, true); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('PATCH /tools/{id} - anonyme', function(t) { + const options = { + method: 'PATCH', + url: '/tools/2', + headers: { + 'x-consumer-username': anonyme.xConsumerUsername, + 'x-consumer-id': anonyme.xConsumerId + }, + payload: { + title: 'dummy99' + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 403); + const result: any = response.result; + t.equal(result.message, 'Must be administrator'); + server.stop(t.end); + }); + }); + + test('PATCH /tools/{id} - user1', function(t) { + const options = { + method: 'PATCH', + url: '/tools/2', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + name: 'dummy99' + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 403); + const result: any = response.result; + t.equal(result.message, 'Must be administrator'); + server.stop(t.end); + }); + }); + + // ---------------------------------------------------------------- + + test('GET /tools/{id} - 404', function(t) { + const options = { + method: 'GET', + url: '/tools/13', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 404); + server.stop(t.end); + }); + }); + + test('GET /tools/{id} - admin', function(t) { + const options = { + method: 'GET', + url: '/tools/1', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.name, 'dummyName'); + t.equal(result.inToolbar, true); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /tools/{id} - anonyme', function(t) { + const options = { + method: 'GET', + url: '/tools/2', + headers: { + 'x-consumer-username': anonyme.xConsumerUsername, + 'x-consumer-id': anonyme.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.name, 'dummyName2'); + t.equal(result.inToolbar, true); + t.equal(result.options.optionParams, true); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /tools/{id} - user1', function(t) { + const options = { + method: 'GET', + url: '/tools/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.name, 'dummyName'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + // ---------------------------------------------------------------- + + + test('DELETE /tools/{id} - anonyme', function(t) { + const options = { + method: 'DELETE', + url: '/tools/2', + headers: { + 'x-consumer-username': anonyme.xConsumerUsername, + 'x-consumer-id': anonyme.xConsumerId + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 403); + const result: any = response.result; + t.equal(result.message, 'Must be administrator'); + server.stop(t.end); + }); + }); + + test('DELETE /tools/{id} - user1', function(t) { + const options = { + method: 'DELETE', + url: '/tools/2', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 403); + const result: any = response.result; + t.equal(result.message, 'Must be administrator'); + server.stop(t.end); + }); + }); + + test('DELETE /tools/{id} - admin', function(t) { + const options = { + method: 'DELETE', + url: '/tools/3', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 204); + server.stop(t.end); + }); + }); + + + test('GET /tools - after delete', function(t) { + const options = { + method: 'GET', + url: '/tools', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.length, 3); + t.equal(result[0].name, 'dummyName'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + +}); diff --git a/src/tool/tool.ts b/src/tool/tool.ts new file mode 100644 index 0000000..0b8cbb1 --- /dev/null +++ b/src/tool/tool.ts @@ -0,0 +1,97 @@ +import * as Rx from 'rxjs'; +import * as Boom from 'boom'; + +import { IDatabase, database } from '../database'; +import { ObjectUtils } from '../utils'; + +import { ITool, ToolInstance } from './tool.model'; + +export class Tool { + + private database: IDatabase = database; + + constructor() {} + + public create(tool: ITool): Rx.Observable { + return Rx.Observable.create(observer => { + this.database.tool.create(tool).then((createdTool) => { + observer.next(createdTool); + observer.complete(); + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public update(id: string, tool: ITool): Rx.Observable { + return Rx.Observable.create(observer => { + this.database.tool.update(tool, { + where: { + id: id + } + }).then((count: [number, ToolInstance[]]) => { + if (count[0]) { + observer.next({id: id}); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public delete(id: string): Rx.Observable<{}> { + return Rx.Observable.create(observer => { + this.database.tool.destroy({ + where: { + id: id + } + }).then((count: number) => { + if (count) { + observer.next({}); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public get(): Rx.Observable { + return Rx.Observable.create(observer => { + this.database.tool.findAll().then((tools: ToolInstance[]) => { + const plainTools = tools.map( + (tool) => ObjectUtils.removeNull(tool.get()) + ); + observer.next(plainTools); + observer.complete(); + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public getById(id: string): Rx.Observable { + return Rx.Observable.create(observer => { + this.database.tool.findOne({ + where: { + id: id + } + }).then((tool: ToolInstance) => { + if (tool) { + observer.next(ObjectUtils.removeNull(tool.get())); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + +} diff --git a/src/tool/tool.validator.ts b/src/tool/tool.validator.ts new file mode 100644 index 0000000..ede495a --- /dev/null +++ b/src/tool/tool.validator.ts @@ -0,0 +1,21 @@ +import * as Joi from 'joi'; + +export class ToolValidator { + static createModel = Joi.object().keys({ + name: Joi.string().required().max(64), + title: Joi.string().allow('').max(64), + tooltip: Joi.string().allow('').max(128), + icon: Joi.string().allow('').max(128), + inToolbar: Joi.boolean(), + options: Joi.object() + }); + + static updateModel = Joi.object().keys({ + name: Joi.string().max(64), + title: Joi.string().allow('').max(64), + tooltip: Joi.string().allow('').max(128), + icon: Joi.string().allow('').max(128), + inToolbar: Joi.boolean(), + options: Joi.object() + }); +} diff --git a/src/toolContext/index.ts b/src/toolContext/index.ts new file mode 100644 index 0000000..094ee74 --- /dev/null +++ b/src/toolContext/index.ts @@ -0,0 +1,9 @@ +import * as Hapi from 'hapi'; +import Routes from './routes'; + +export function init(server: Hapi.Server) { + Routes(server); +} + +export * from './toolContext.model' +export * from './toolContext' diff --git a/src/toolsContexts/routes.ts b/src/toolContext/routes.ts similarity index 50% rename from src/toolsContexts/routes.ts rename to src/toolContext/routes.ts index 798d540..512148f 100644 --- a/src/toolsContexts/routes.ts +++ b/src/toolContext/routes.ts @@ -1,32 +1,27 @@ import * as Hapi from 'hapi'; import * as Joi from 'joi'; -import ToolContextController from './toolContext.controller'; -import * as ToolContextValidator from './toolContext.validator'; -// import { jwtValidator } from '../users/user-validator'; -import { IDatabase } from '../database'; -import { IServerConfigurations } from '../configurations'; -export default function (server: Hapi.Server, - configs: IServerConfigurations, - database: IDatabase) { +import { ToolContextController } from './toolContext.controller'; +import { ToolContextValidator } from './toolContext.validator'; +import { ContextPermissionValidator } from '../contextPermission'; - const toolContextController = new ToolContextController(configs, database); +export default function (server: Hapi.Server) { + + const toolContextController = new ToolContextController(); server.bind(toolContextController); server.route({ method: 'GET', - path: '/contexts/{id}/tools', + path: '/contexts/{contextId}/tools', config: { - handler: toolContextController.getToolsByContextId, - // auth: 'jwt', - auth: false, - tags: ['api', 'toolsContexts', 'tools', 'contexts'], + handler: toolContextController.getByContextId, + tags: ['api', 'toolContext', 'tools', 'contexts'], description: 'Get tools by context id.', validate: { params: { - id: Joi.string().required() - } - // headers: jwtValidator + contextId: Joi.string().required() + }, + headers: ContextPermissionValidator.readPermission }, plugins: { 'hapi-swagger': { @@ -45,18 +40,17 @@ export default function (server: Hapi.Server, server.route({ method: 'GET', - path: '/toolsContexts/{id}', + path: '/contexts/{contextId}/tools/{toolId}', config: { - handler: toolContextController.getToolContextById, - // auth: 'jwt', - auth: false, - tags: ['api', 'toolsContexts'], - description: 'Get toolsContexts by id.', + handler: toolContextController.getById, + tags: ['api', 'toolContext'], + description: 'Get toolContext by id.', validate: { params: { - id: Joi.string().required() - } - // headers: jwtValidator + toolId: Joi.string().required(), + contextId: Joi.string().required() + }, + headers: ContextPermissionValidator.readPermission }, plugins: { 'hapi-swagger': { @@ -73,44 +67,24 @@ export default function (server: Hapi.Server, } }); - server.route({ - method: 'GET', - path: '/toolsContexts', - config: { - handler: toolContextController.gettoolsContexts, - // auth: 'jwt', - auth: false, - tags: ['api', 'toolsContexts'], - description: 'Get all toolsContexts.', - validate: { - query: { - // top: Joi.number().default(5), - // skip: Joi.number().default(0) - } - // headers: jwtValidator - } - } - }); - server.route({ method: 'DELETE', - path: '/toolsContexts/{id}', + path: '/contexts/{contextId}/tools/{toolId}', config: { - handler: toolContextController.deleteToolContext, - // auth: 'jwt', - auth: false, - tags: ['api', 'toolsContexts'], + handler: toolContextController.delete, + tags: ['api', 'toolContext'], description: 'Delete toolContext by id.', validate: { params: { - id: Joi.string().required() - } - // headers: jwtValidator + toolId: Joi.string().required(), + contextId: Joi.string().required() + }, + headers: ContextPermissionValidator.writePermission }, plugins: { 'hapi-swagger': { responses: { - '200': { + '204': { 'description': 'Deleted ToolContext.', }, '404': { @@ -123,20 +97,19 @@ export default function (server: Hapi.Server, }); server.route({ - method: 'PUT', - path: '/toolsContexts/{id}', + method: 'PATCH', + path: '/contexts/{contextId}/tools/{toolId}', config: { - handler: toolContextController.updateToolContext, - // auth: 'jwt', - auth: false, - tags: ['api', 'toolsContexts'], + handler: toolContextController.update, + tags: ['api', 'toolContext'], description: 'Update toolContext by id.', validate: { params: { - id: Joi.string().required() + toolId: Joi.string().required(), + contextId: Joi.string().required() }, - payload: ToolContextValidator.updateToolContextModel - // headers: jwtValidator + payload: ToolContextValidator.updateModel, + headers: ContextPermissionValidator.writePermission }, plugins: { 'hapi-swagger': { @@ -155,16 +128,17 @@ export default function (server: Hapi.Server, server.route({ method: 'POST', - path: '/toolsContexts', + path: '/contexts/{contextId}/tools', config: { - handler: toolContextController.createToolContext, - // auth: 'jwt', - auth: false, - tags: ['api', 'toolsContexts'], + handler: toolContextController.create, + tags: ['api', 'toolContext'], description: 'Create a toolContext.', validate: { - payload: ToolContextValidator.createToolContextModel - // headers: jwtValidator + params: { + contextId: Joi.string().required() + }, + payload: ToolContextValidator.createModel, + headers: ContextPermissionValidator.writePermission }, plugins: { 'hapi-swagger': { diff --git a/src/toolContext/toolContext.controller.ts b/src/toolContext/toolContext.controller.ts new file mode 100644 index 0000000..22e8276 --- /dev/null +++ b/src/toolContext/toolContext.controller.ts @@ -0,0 +1,64 @@ +import * as Hapi from 'hapi'; +import * as Boom from 'boom'; + +import { ToolContext, IToolContext, ToolContextInstance } from './index'; + +export class ToolContextController { + + private toolContext: ToolContext; + + constructor() { + this.toolContext = new ToolContext(); + } + + public create(request: Hapi.Request, reply: Hapi.IReply) { + const newToolContext: IToolContext = request.payload; + newToolContext['contextId'] = request.params['contextId']; + + this.toolContext.create(newToolContext).subscribe( + (tc: ToolContextInstance) => reply(tc).code(201), + (error: Boom.BoomError) => reply(error) + ); + } + + public update(request: Hapi.Request, reply: Hapi.IReply) { + const toolId = request.params['toolId']; + const contextId = request.params['contextId']; + const toolContext: IToolContext = request.payload; + + this.toolContext.update(contextId, toolId, toolContext).subscribe( + (cp: ToolContextInstance) => reply(cp), + (error: Boom.BoomError) => reply(error) + ); + } + + public delete(request: Hapi.Request, reply: Hapi.IReply) { + const toolId = request.params['toolId']; + const contextId = request.params['contextId']; + + this.toolContext.delete(contextId, toolId).subscribe( + (cp: ToolContextInstance) => reply(cp).code(204), + (error: Boom.BoomError) => reply(error) + ); + } + + public getById(request: Hapi.Request, reply: Hapi.IReply) { + const toolId = request.params['toolId']; + const contextId = request.params['contextId']; + + this.toolContext.getById(contextId, toolId).subscribe( + (cp: ToolContextInstance) => reply(cp), + (error: Boom.BoomError) => reply(error) + ); + } + + public getByContextId(request: Hapi.Request, reply: Hapi.IReply) { + const contextId = request.params['contextId']; + + this.toolContext.getByContextId(contextId).subscribe( + (cp: ToolContextInstance[]) => reply(cp), + (error: Boom.BoomError) => reply(error) + ); + } + +} diff --git a/src/toolsContexts/toolContext.model.ts b/src/toolContext/toolContext.model.ts similarity index 73% rename from src/toolsContexts/toolContext.model.ts rename to src/toolContext/toolContext.model.ts index dc70074..0eebfcb 100644 --- a/src/toolsContexts/toolContext.model.ts +++ b/src/toolContext/toolContext.model.ts @@ -1,6 +1,9 @@ import * as Sequelize from 'sequelize'; export interface IToolContext { + id?: string; + toolId?: string; + contextId?: string; options?: {[key: string]: any}; }; @@ -10,6 +13,8 @@ export interface ToolContextInstance createdAt: Date; updatedAt: Date; + toolId: string; + contextId: string; options?: {[key: string]: any}; } @@ -28,20 +33,29 @@ export default function define(sequelize: Sequelize.Sequelize, DataTypes) { 'options': { 'type': DataTypes.TEXT, 'get': function() { - return JSON.parse(this.getDataValue('options')); + const options = this.getDataValue('options'); + return options ? JSON.parse(options) : {}; }, 'set': function(val) { - this.setDataValue('options', JSON.stringify({})); + this.setDataValue('options', JSON.stringify(val)); } }, - context_id: { + contextId: { type: DataTypes.INTEGER }, - tool_id: { + toolId: { type: DataTypes.INTEGER } }, { + 'indexes': [{ + 'unique': true, + 'fields': ['contextId', 'toolId'] + }, { + 'fields': ['contextId'] + }, { + 'fields': ['toolId'] + }], 'tableName': 'toolContext', 'timestamps': true } @@ -56,7 +70,7 @@ export default function define(sequelize: Sequelize.Sequelize, DataTypes) { unique: false }, foreignKey: { - name: 'tool_id', + name: 'toolId', allowNull: false } }); @@ -67,7 +81,7 @@ export default function define(sequelize: Sequelize.Sequelize, DataTypes) { unique: false }, foreignKey: { - name: 'context_id', + name: 'contextId', allowNull: false } }); diff --git a/src/toolContext/toolContext.test.ts b/src/toolContext/toolContext.test.ts new file mode 100644 index 0000000..0264b72 --- /dev/null +++ b/src/toolContext/toolContext.test.ts @@ -0,0 +1,860 @@ +import * as test from 'tape'; +import * as Server from '../server'; +import * as Configs from '../configurations'; + +const serverConfigs = Configs.getServerConfig(); +const testConfigs = Configs.getTestConfig(); +const admin = testConfigs.admin; +const user1 = testConfigs.user1; +const user2 = testConfigs.user2; + +Server.init(serverConfigs).then((server) => { + + test('POST /contexts - before toolContext - ', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + uri: 'user1Private', + title: 'user1Private', + scope: 'private', + map: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts - before toolContext - context 2', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + uri: 'user2Private', + title: 'user2Private', + scope: 'private', + map: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts - before toolContext - context 3', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + uri: 'user2Public', + title: 'user2Public', + scope: 'public', + map: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts - before toolContext - context 4', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + uri: 'user2PublicWrite', + title: 'user2PublicWrite', + scope: 'public', + map: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts - before toolContext - context 5', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + uri: 'user2Protected', + title: 'user2Protected', + scope: 'protected', + map: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts - before toolContext - context 6', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + uri: 'user2ProtectedWrite', + title: 'user2ProtectedWrite', + scope: 'protected', + map: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts/4/permissions - before toolContext', function(t) { + const options = { + method: 'POST', + url: '/contexts/4/permissions', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + typePermission: 'write', + profil: user1.xConsumerUsername + } + }; + server.inject(options, function(response) { + server.stop(t.end); + }); + }); + + test('POST /contexts/6/permissions - before toolContext', function(t) { + const options = { + method: 'POST', + url: '/contexts/6/permissions', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + typePermission: 'write', + profil: user1.xConsumerUsername + } + }; + server.inject(options, function(response) { + server.stop(t.end); + }); + }); + + test('POST /tools - before toolContext', function(t) { + const options = { + method: 'POST', + url: '/tools', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + name: 'dummyName', + title: 'dummyTitle', + inToolbar: true, + options: {} + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /tools - before toolContext', function(t) { + const options = { + method: 'POST', + url: '/tools', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + }, + payload: { + name: 'dummyName2', + title: 'dummyTitle2', + inToolbar: false + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + // =========================================================== + + test('POST /contexts/1/tools - user1', function(t) { + const options = { + method: 'POST', + url: '/contexts/1/tools', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + toolId: 1, + options: { + minZoom: 5 + } + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.toolId, 1); + t.equal(Number(result.contextId), 1); + t.equal(result.options.minZoom, 5); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts/2/tools - user1', function(t) { + const options = { + method: 'POST', + url: '/contexts/2/tools', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + toolId: 1 + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('POST /contexts/3/tools - user1', function(t) { + const options = { + method: 'POST', + url: '/contexts/3/tools', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + toolId: 1 + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('POST /contexts/4/tools - user1', function(t) { + const options = { + method: 'POST', + url: '/contexts/4/tools', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + toolId: 1 + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.toolId, 1); + t.equal(Number(result.contextId), 4); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts/5/tools - user1', function(t) { + const options = { + method: 'POST', + url: '/contexts/5/tools', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + toolId: 1 + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('POST /contexts/6/tools - user1', function(t) { + const options = { + method: 'POST', + url: '/contexts/6/tools', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + toolId: 1 + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.toolId, 1); + t.equal(Number(result.contextId), 6); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts/10/tools - user1', function(t) { + const options = { + method: 'POST', + url: '/contexts/1/tools', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + toolId: 10, + options: { + minZoom: 5 + } + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Tool can not be found.'); + t.equal(response.statusCode, 400); + server.stop(t.end); + }); + }); + + test('POST /contexts/6/tools - user2', function(t) { + const options = { + method: 'POST', + url: '/contexts/6/tools', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + toolId: 2 + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.toolId, 2); + t.equal(Number(result.contextId), 6); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('POST /contexts/6/tools - user2', function(t) { + const options = { + method: 'POST', + url: '/contexts/6/tools', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + toolId: 2 + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'The pair contextId and toolId must be unique.'); + t.equal(response.statusCode, 409); + server.stop(t.end); + }); + }); + + test('POST /contexts/3/tools - user2', function(t) { + const options = { + method: 'POST', + url: '/contexts/3/tools', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + }, + payload: { + toolId: 2 + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.toolId, 2); + t.equal(Number(result.contextId), 3); + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + // =============================================== + + test('PATCH /contexts/1/tools/1 - user1 = toolId not allowed', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/1/tools/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + toolId: 1, + options: { + minZoom: 5 + } + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, '"toolId" is not allowed'); + t.equal(response.statusCode, 400); + server.stop(t.end); + }); + }); + + test('PATCH /contexts/1/tools/1 - user1', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/1/tools/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + options: { + minZoom: 3 + } + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(Number(result.toolId), 1); + t.equal(Number(result.contextId), 1); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('PATCH /contexts/2/tools/1 - user1', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/2/tools/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + options: { + minZoom: 4 + } + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('PATCH /contexts/3/tools/1 - user1', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/3/tools/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + options: { + minZoom: 8 + } + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('PATCH /contexts/4/tools/1 - user1', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/4/tools/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + options: { + minZoom: 6 + } + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(Number(result.toolId), 1); + t.equal(Number(result.contextId), 4); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('PATCH /contexts/5/tools/1 - user1', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/5/tools/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + options: { + minZoom: 9 + } + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have write permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('PATCH /contexts/6/tools/1 - user1', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/6/tools/1', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + options: { + minZoom: 11 + } + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(Number(result.toolId), 1); + t.equal(Number(result.contextId), 6); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('PATCH /contexts/1/tools/10 - user1', function(t) { + const options = { + method: 'PATCH', + url: '/contexts/1/tools/10', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + options: { + minZoom: 5 + } + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 404); + server.stop(t.end); + }); + }); + +// =================================== + + test('GET /contexts/1/tools - user1', function(t) { + const options = { + method: 'GET', + url: '/contexts/1/tools', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.length, 1); + t.equal(result[0].options.minZoom, 3); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /contexts/2/tools - user1', function(t) { + const options = { + method: 'GET', + url: '/contexts/2/tools', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have read permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('GET /contexts/3/tools - user1', function(t) { + const options = { + method: 'GET', + url: '/contexts/3/tools', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.length, 1); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /contexts/4/tools - user1', function(t) { + const options = { + method: 'GET', + url: '/contexts/4/tools', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.length, 1); + t.equal(result[0].options.minZoom, 6); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /contexts/5/tools - user1', function(t) { + const options = { + method: 'GET', + url: '/contexts/5/tools', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Must have read permission for this context'); + t.equal(response.statusCode, 403); + server.stop(t.end); + }); + }); + + test('GET /contexts/6/tools - user1', function(t) { + const options = { + method: 'GET', + url: '/contexts/6/tools', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.length, 2); + t.equal(result[0].options.minZoom, 11); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + // ============================================ + + test('GET /contexts/6/tools/2 - user1', function(t) { + const options = { + method: 'GET', + url: '/contexts/6/tools/2', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.toolId, 2); + t.equal(result.contextId, 6); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('DELETE /contexts/6/tools/2 - user1', function(t) { + const options = { + method: 'DELETE', + url: '/contexts/6/tools/2', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 204); + server.stop(t.end); + }); + }); + + test('GET /contexts/6/tools/2 - user1', function(t) { + const options = { + method: 'GET', + url: '/contexts/6/tools/2', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 404); + server.stop(t.end); + }); + }); + + // ============================================== + + test('GET /contexts/1/details - toolContext 1 ', function(t) { + const options = { + method: 'GET', + url: '/contexts/1/details', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.tools.length, 1); + t.equal(result.tools[0].title, 'dummyTitle'); + t.equal(result.toolbar.length, 1); + t.equal(result.toolbar[0], 'dummyName'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /contexts/2/details - toolContext 2 ', function(t) { + const options = { + method: 'GET', + url: '/contexts/2/details', + headers: { + 'x-consumer-username': user2.xConsumerUsername, + 'x-consumer-id': user2.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.tools.length, 0); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + let idContextWithTool; + test('POST /contexts - toolContext 1 ', function(t) { + const options = { + method: 'POST', + url: '/contexts', + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + }, + payload: { + uri: 'withTool', + title: 'withTool', + scope: 'public', + map: {}, + tools: [{ + id: '1' + }, { + id: '90' + }, { + id: '2' + }, { + name: 'extraTool' + }] + } + }; + server.inject(options, function(response) { + const result: any = response.result; + idContextWithTool = result.id; + t.equal(response.statusCode, 201); + server.stop(t.end); + }); + }); + + test('GET /contexts/{id}/details - context with tool ', function(t) { + const options = { + method: 'GET', + url: `/contexts/${idContextWithTool}/details`, + headers: { + 'x-consumer-username': user1.xConsumerUsername, + 'x-consumer-id': user1.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.uri, 'withTool'); + t.equal(result.tools.length, 2); + t.equal(result.tools[0].id, 1); + t.equal(result.tools[1].id, 2); + t.equal(result.toolbar.length, 2); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + +}); diff --git a/src/toolContext/toolContext.ts b/src/toolContext/toolContext.ts new file mode 100644 index 0000000..3242c2d --- /dev/null +++ b/src/toolContext/toolContext.ts @@ -0,0 +1,173 @@ +import * as Rx from 'rxjs'; +import * as Boom from 'boom'; +import * as async from 'async'; + +import { IDatabase, database } from '../database'; +import { ObjectUtils } from '../utils'; + +import { IToolContext, ToolContextInstance } from './toolContext.model'; + +export class ToolContext { + + private database: IDatabase = database; + + constructor() {} + + public create(toolContext: IToolContext): Rx.Observable { + return Rx.Observable.create(observer => { + this.database.toolContext.create(toolContext) + .then((createdToolContext) => { + observer.next(createdToolContext); + observer.complete(); + }).catch((error) => { + const uniqueFields = ['contextId', 'toolId']; + if (error.name === 'SequelizeUniqueConstraintError' && + error.fields.toString() === uniqueFields.toString()) { + const message = 'The pair contextId and toolId must be unique.'; + observer.error(Boom.conflict(message)); + } else if (error.name === 'SequelizeForeignKeyConstraintError') { + const message = 'Tool can not be found.'; + observer.error(Boom.badRequest(message)); + } else { + observer.error(Boom.badImplementation(error)); + } + }); + }); + } + + public update(contextId: string, toolId: string, + toolContext: IToolContext): Rx.Observable { + + return Rx.Observable.create(observer => { + this.database.toolContext.update(toolContext, { + where: { + toolId: toolId, + contextId: contextId + } + }).then((count: [number, ToolContextInstance[]]) => { + if (count[0]) { + observer.next({ + toolId: toolId, + contextId: contextId + }); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public delete(contextId: string, toolId: string): Rx.Observable<{}> { + return Rx.Observable.create(observer => { + this.database.toolContext.destroy({ + where: { + toolId: toolId, + contextId: contextId + } + }).then((count: number) => { + if (count) { + observer.next({}); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public deleteByContextId(contextId: string): Rx.Observable<{}> { + return Rx.Observable.create(observer => { + this.database.toolContext.destroy({ + where: { + contextId: contextId + } + }).then((count: number) => { + if (count) { + observer.next({}); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public getByContextId( + contextId: string): Rx.Observable { + + return Rx.Observable.create(observer => { + this.database.toolContext.findAll({ + where: { + contextId: contextId + } + }).then((toolContextsContexts: ToolContextInstance[]) => { + const plainToolContextsContexts = toolContextsContexts.map( + (toolContext) => ObjectUtils.removeNull(toolContext.get()) + ); + observer.next(plainToolContextsContexts); + observer.complete(); + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public getById(contextId: string, + toolId: string): Rx.Observable { + + return Rx.Observable.create(observer => { + this.database.toolContext.findOne({ + where: { + toolId: toolId, + contextId: contextId + } + }).then((toolContext: ToolContextInstance) => { + if (toolContext) { + observer.next(ObjectUtils.removeNull(toolContext.get())); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public bulkCreate(contextId: string, tools: IToolContext[]) { + return Rx.Observable.create(observer => { + let count = 0; + async.forEach(tools, + (tool: IToolContext, next) => { + if (tool.id) { + this.create({ + contextId: contextId, + toolId: tool.id + }).subscribe( + (rep) => { count++; next(); }, + (error) => next(error) + ); + } else { + next(); + } + }, + (error) => { + if (error) { + observer.error(error); + } else { + observer.next(count); + observer.complete(); + } + } + ); + }); + } + +} diff --git a/src/toolContext/toolContext.validator.ts b/src/toolContext/toolContext.validator.ts new file mode 100644 index 0000000..06a62f6 --- /dev/null +++ b/src/toolContext/toolContext.validator.ts @@ -0,0 +1,14 @@ +import * as Joi from 'joi'; + +export class ToolContextValidator { + + static createModel = Joi.object().keys({ + toolId: Joi.number().required(), + options: Joi.object() + }); + + static updateModel = Joi.object().keys({ + options: Joi.object() + }); + +} diff --git a/src/tools/index.ts b/src/tools/index.ts deleted file mode 100644 index 0cc53d9..0000000 --- a/src/tools/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as Hapi from 'hapi'; -import Routes from './routes'; -import { IDatabase } from '../database'; -import { IServerConfigurations } from '../configurations'; - -export function init(server: Hapi.Server, - configs: IServerConfigurations, - database: IDatabase) { - Routes(server, configs, database); -} diff --git a/src/tools/tool.controller.ts b/src/tools/tool.controller.ts deleted file mode 100644 index 85e3983..0000000 --- a/src/tools/tool.controller.ts +++ /dev/null @@ -1,87 +0,0 @@ -import * as Hapi from 'hapi'; -import * as Boom from 'boom'; -import { ITool, ToolInstance } from './tool.model'; -import { IDatabase } from '../database'; -import { IServerConfigurations } from '../configurations'; - -export default class ToolController { - - private database: IDatabase; - private configs: IServerConfigurations; - - constructor(configs: IServerConfigurations, database: IDatabase) { - this.configs = configs; - this.database = database; - } - - public createTool(request: Hapi.Request, reply: Hapi.IReply) { - const newTool: ITool = request.payload; - this.database.tool.create(newTool).then((tool) => { - reply(tool).code(201); - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - - public updateTool(request: Hapi.Request, reply: Hapi.IReply) { - const id = request.params['id']; - const tool: ITool = request.payload; - - this.database.tool.update(tool, { - where: { - id: id - } - }).then((count: [number, ToolInstance[]]) => { - if (count[0]) { - reply({}); - } else { - reply(Boom.notFound()); - } - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - - public deleteTool(request: Hapi.Request, reply: Hapi.IReply) { - const id = request.params['id']; - this.database.tool.destroy({ - where: { - id: id - } - }).then((count: number) => { - if (count) { - reply({}); - } else { - reply(Boom.notFound()); - } - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - - public getToolById(request: Hapi.Request, reply: Hapi.IReply) { - const id = request.params['id']; - this.database.tool.findOne({ - where: { - id: id - } - }).then((tool: ToolInstance) => { - if (tool) { - reply(tool); - } else { - reply(Boom.notFound()); - } - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - - public getTools(request: Hapi.Request, reply: Hapi.IReply) { - this.database.tool.findAll() - .then((tools: Array) => { - reply(tools); - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } -} diff --git a/src/tools/tool.model.ts b/src/tools/tool.model.ts deleted file mode 100644 index dcd37ab..0000000 --- a/src/tools/tool.model.ts +++ /dev/null @@ -1,80 +0,0 @@ -import * as Sequelize from 'sequelize'; - -export interface ITool { - name: string; - title: string; - icon?: string; - url?: string; - protected: boolean; - inToolbar?: boolean; - options?: {[key: string]: any}; -}; - -export interface ToolInstance extends Sequelize.Instance { - id: string; - createdAt: Date; - updatedAt: Date; - - name: string; - title: string; - icon?: string; - url?: string; - protected: boolean; - inToolbar?: boolean; - options?: {[key: string]: any}; -} - -export interface ToolModel - extends Sequelize.Model { } - - -export default function define(sequelize: Sequelize.Sequelize, DataTypes) { - const tool = sequelize.define('tool', { - 'id': { - 'type': DataTypes.INTEGER, - 'allowNull': false, - 'primaryKey': true, - 'autoIncrement': true - }, - 'name': { - 'type': DataTypes.STRING(64), - 'allowNull': false - }, - 'title': { - 'type': DataTypes.STRING(64), - 'allowNull': false - }, - 'icon': { - 'type': DataTypes.STRING(128) - }, - 'url': { - 'type': DataTypes.STRING(255), - 'validate': { - 'isUrl': true - } - }, - 'protected': { - 'type': DataTypes.BOOLEAN - }, - 'inToolbar': { - 'type': DataTypes.BOOLEAN - }, - 'options': { - 'type': DataTypes.TEXT, - 'get': function() { - return JSON.parse(this.getDataValue('options')); - }, - 'set': function(val) { - this.setDataValue('options', JSON.stringify({})); - } - } - }, - { - 'tableName': 'tool', - 'timestamps': true - }); - - tool.sync(); - - return tool; -} diff --git a/src/tools/tool.test.ts b/src/tools/tool.test.ts deleted file mode 100644 index 47f3e5c..0000000 --- a/src/tools/tool.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as test from 'tape'; -// import ToolCont from './tool.controller'; -import * as Server from '../server'; -import * as Configs from '../configurations'; - -const serverConfigs = Configs.getServerConfigs(); - -Server.init(serverConfigs).then((server) => { - - test('Basic HTTP Tests - GET /tools', function(t) { - const options = { - method: 'GET', - url: '/tools' - }; - server.inject(options, function(response) { - t.equal(response.statusCode, 200); - t.equal(response.result.length, 0); - server.stop(t.end); - }); - }); - - - test('Basic HTTP Tests - GET /tools/{id}', function(t) { - const options = { - method: 'GET', - url: '/tools/2' - }; - server.inject(options, function(response) { - t.equal(response.statusCode, 404); - server.stop(t.end); - }); - }); - - - test('Basic HTTP Tests - POST /tools', function(t) { - const options = { - method: 'POST', - url: '/tools', - payload: { - name: 'dummy', - title: 'dummy', - protected: false, - inToolbar: true, - options: {} - } - }; - server.inject(options, function(response) { - t.equal(response.statusCode, 201); - server.stop(t.end); - }); - }); - - -}); diff --git a/src/tools/tool.validator.ts b/src/tools/tool.validator.ts deleted file mode 100644 index f7dae6a..0000000 --- a/src/tools/tool.validator.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as Joi from 'joi'; - -export const createToolModel = Joi.object().keys({ - name: Joi.string().required().max(64), - title: Joi.string().required().max(64), - icon: Joi.string().max(128), - url: Joi.string(), - protected: Joi.boolean(), - inToolbar: Joi.boolean(), - options: Joi.object().required() -}); - -export const updateToolModel = Joi.object().keys({ - name: Joi.string().max(64), - title: Joi.string().max(64), - icon: Joi.string().max(128), - url: Joi.string(), - protected: Joi.boolean(), - inToolbar: Joi.boolean(), - options: Joi.object().required() -}); diff --git a/src/toolsContexts/index.ts b/src/toolsContexts/index.ts deleted file mode 100644 index 0cc53d9..0000000 --- a/src/toolsContexts/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as Hapi from 'hapi'; -import Routes from './routes'; -import { IDatabase } from '../database'; -import { IServerConfigurations } from '../configurations'; - -export function init(server: Hapi.Server, - configs: IServerConfigurations, - database: IDatabase) { - Routes(server, configs, database); -} diff --git a/src/toolsContexts/toolContext.controller.ts b/src/toolsContexts/toolContext.controller.ts deleted file mode 100644 index fb9366e..0000000 --- a/src/toolsContexts/toolContext.controller.ts +++ /dev/null @@ -1,107 +0,0 @@ -import * as Hapi from 'hapi'; -import * as Boom from 'boom'; -import { IToolContext, ToolContextInstance } from './toolContext.model'; -import { IDatabase } from '../database'; -import { IServerConfigurations } from '../configurations'; - -export default class ToolContextController { - - private database: IDatabase; - private configs: IServerConfigurations; - - constructor(configs: IServerConfigurations, database: IDatabase) { - this.configs = configs; - this.database = database; - } - - public createToolContext(request: Hapi.Request, reply: Hapi.IReply) { - const newToolContext: IToolContext = request.payload; - this.database.toolContext.create(newToolContext) - .then((toolContext) => { - reply(toolContext).code(201); - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - - public updateToolContext(request: Hapi.Request, reply: Hapi.IReply) { - const id = request.params['id']; - const toolContext: IToolContext = request.payload; - - this.database.toolContext.update(toolContext, { - where: { - id: id - } - }).then((count: [number, ToolContextInstance[]]) => { - if (count[0]) { - reply({}); - } else { - reply(Boom.notFound()); - } - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - - public deleteToolContext(request: Hapi.Request, reply: Hapi.IReply) { - const id = request.params['id']; - this.database.toolContext.destroy({ - where: { - id: id - } - }).then((count: number) => { - if (count) { - reply({}); - } else { - reply(Boom.notFound()); - } - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - - public getToolContextById(request: Hapi.Request, reply: Hapi.IReply) { - const id = request.params['id']; - this.database.toolContext.findOne({ - where: { - id: id - } - }).then((toolContext: ToolContextInstance) => { - if (toolContext) { - reply(toolContext); - } else { - reply(Boom.notFound()); - } - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - - public gettoolsContexts(request: Hapi.Request, reply: Hapi.IReply) { - this.database.toolContext.findAll() - .then((toolsContexts: Array) => { - reply(toolsContexts); - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - - - public getToolsByContextId(request: Hapi.Request, reply: Hapi.IReply) { - const id = request.params['id']; - - - - this.database.context.findAll({ - include: [ this.database.tool ], - where: { - id: id - } - }).then((toolsContexts: Array) => { - reply(toolsContexts); - }).catch((error) => { - reply(Boom.badImplementation(error)); - }); - } - -} diff --git a/src/toolsContexts/toolContext.test.ts b/src/toolsContexts/toolContext.test.ts deleted file mode 100644 index 722790b..0000000 --- a/src/toolsContexts/toolContext.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import * as test from 'tape'; -// import ToolContextCont from './toolContext.controller'; -import * as Server from '../server'; -import * as Configs from '../configurations'; - -const serverConfigs = Configs.getServerConfigs(); - -Server.init(serverConfigs).then((server) => { - - test('Basic HTTP Tests - GET /toolsContexts', function(t) { - const options = { - method: 'GET', - url: '/toolsContexts' - }; - server.inject(options, function(response) { - t.equal(response.statusCode, 200); - t.equal(response.result.length, 0); - server.stop(t.end); - }); - }); - - - test('Basic HTTP Tests - GET /toolsContexts/{id}', function(t) { - const options = { - method: 'GET', - url: '/toolsContexts/2' - }; - server.inject(options, function(response) { - t.equal(response.statusCode, 404); - server.stop(t.end); - }); - }); - - - test('Basic HTTP Tests - POST /toolsContexts', function(t) { - const options = { - method: 'POST', - url: '/toolsContexts', - payload: { - context_id: 1, - tool_id: 1, - options: {} - } - }; - server.inject(options, function(response) { - t.equal(response.statusCode, 201); - server.stop(t.end); - }); - }); - - -}); diff --git a/src/toolsContexts/toolContext.validator.ts b/src/toolsContexts/toolContext.validator.ts deleted file mode 100644 index 36c29fb..0000000 --- a/src/toolsContexts/toolContext.validator.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as Joi from 'joi'; - -export const createToolContextModel = Joi.object().keys({ - context_id: Joi.number().required(), - tool_id: Joi.number().required(), - options: Joi.object().required().keys({ - attribution: Joi.string(), - minZoom: Joi.number(), - maxZoom: Joi.number() - }) -}); - -export const updateToolContextModel = Joi.object().keys({ - context_id: Joi.number(), - tool_id: Joi.number(), - options: Joi.object().required().keys({ - attribution: Joi.string(), - minZoom: Joi.number(), - maxZoom: Joi.number() - }) -}); diff --git a/src/user/index.ts b/src/user/index.ts new file mode 100644 index 0000000..335a0b7 --- /dev/null +++ b/src/user/index.ts @@ -0,0 +1,9 @@ +import * as Hapi from 'hapi'; +import Routes from './routes'; + +export function init(server: Hapi.Server) { + Routes(server); +} + +export * from './user' +export * from './user.validator' diff --git a/src/user/routes.ts b/src/user/routes.ts new file mode 100644 index 0000000..9e949d6 --- /dev/null +++ b/src/user/routes.ts @@ -0,0 +1,132 @@ +import * as Hapi from 'hapi'; +import { UserController } from './user.controller'; +import { UserValidator } from './user.validator'; + +export default function(server: Hapi.Server) { + + const userController = new UserController(); + server.bind(userController); + + server.route({ + method: 'GET', + path: '/users/info', + config: { + handler: userController.info, + tags: ['api', 'users'], + description: 'Get user info.', + validate: { + headers: UserValidator.authenticateValidator, + }, + plugins: { + 'hapi-swagger': { + responses: { + '200': { + 'description': 'User founded.' + }, + '401': { + 'description': 'Please login.' + } + } + } + } + } + }); + + server.route({ + method: 'GET', + path: '/users/profils', + config: { + handler: userController.getProfils, + tags: ['api', 'users', 'profils'], + description: 'Get profils from user.', + validate: { + headers: UserValidator.authenticateValidator, + }, + plugins: { + 'hapi-swagger': { + responses: { + '200': { + 'description': 'Profils founded.' + }, + '401': { + 'description': 'Please login.' + } + } + } + } + } + }); + + server.route({ + method: 'DELETE', + path: '/users', + config: { + handler: userController.delete, + tags: ['api', 'users'], + description: 'Delete current user.', + validate: { + headers: UserValidator.authenticateValidator, + }, + plugins: { + 'hapi-swagger': { + responses: { + '204': { + 'description': 'User deleted.', + }, + '401': { + 'description': 'User does not have authorization.' + } + } + } + } + } + }); + + server.route({ + method: 'PATCH', + path: '/users', + config: { + handler: userController.update, + tags: ['api', 'users'], + description: 'Update current user info.', + validate: { + payload: UserValidator.updateModel, + headers: UserValidator.authenticateValidator, + }, + plugins: { + 'hapi-swagger': { + responses: { + '200': { + 'description': 'Updated info.', + }, + '401': { + 'description': 'User does not have authorization.' + } + } + } + } + } + }); + + server.route({ + method: 'POST', + path: '/users/login', + config: { + handler: userController.login, + tags: ['api', 'users'], + description: 'Login a user.', + validate: { + payload: UserValidator.loginModel + }, + plugins: { + 'hapi-swagger': { + responses: { + '200': { + 'description': 'User logged in.' + } + } + } + } + } + }); +} diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts new file mode 100644 index 0000000..8e23f81 --- /dev/null +++ b/src/user/user.controller.ts @@ -0,0 +1,95 @@ +import * as Hapi from 'hapi'; +import * as Boom from 'boom'; + +import { IUser, UserInstance } from './user.model'; +import { User } from './user'; + + +export class UserController { + + private user: User; + + constructor() { + this.user = new User(); + } + + public login(request: Hapi.Request, reply: Hapi.IReply) { + const typeConnection = request.payload.typeConnection || 'msp'; + if (typeConnection === 'msp') { + this.loginMspUser(request, reply); + } else if (typeConnection === 'test') { + this.loginTestUser(request, reply); + } else { + this.loginSocialUser(request, reply); + } + } + + public update(request: Hapi.Request, reply: Hapi.IReply) { + const id = request.headers['x-consumer-custom-id']; + const user: IUser = request.payload; + + this.user.update(id, user).subscribe( + (rep: UserInstance) => reply(rep), + (error: Boom.BoomError) => reply(error) + ); + } + + public delete(request: Hapi.Request, reply: Hapi.IReply) { + const id = request.headers['x-consumer-custom-id']; + const kongId = request.headers['x-consumer-id']; + + this.user.delete(id, kongId).subscribe( + (rep: UserInstance) => reply(rep).code(204), + (error: Boom.BoomError) => reply(error) + ); + } + + public info(request: Hapi.Request, reply: Hapi.IReply) { + const customId = request.headers['x-consumer-custom-id']; + + this.user.info(customId).subscribe( + (tool: UserInstance) => reply(tool), + (error: Boom.BoomError) => reply(error) + ); + } + + public getProfils(request: Hapi.Request, reply: Hapi.IReply) { + const kongId = request.headers['x-consumer-id']; + + User.getProfils(kongId).subscribe( + (profils) => reply({profils: profils}), + (error: Boom.BoomError) => reply(error) + ); + } + + private loginMspUser(request: Hapi.Request, reply: Hapi.IReply) { + const username = request.payload.username; + const password = request.payload.password; + + this.user.loginMspUser(username, password).subscribe( + (token) => reply(token), + (error: Boom.BoomError) => reply(error) + ); + } + + private loginTestUser(request: Hapi.Request, reply: Hapi.IReply) { + const username = request.payload.username; + const password = request.payload.password; + + this.user.loginTestUser(username, password).subscribe( + (token) => reply(token), + (error: Boom.BoomError) => reply(error) + ); + } + + private loginSocialUser(request: Hapi.Request, reply: Hapi.IReply) { + const socialToken = request.payload.token; + const typeConnection = request.payload.typeConnection; + + this.user.loginSocialUser(socialToken, typeConnection).subscribe( + (token) => reply(token), + (error: Boom.BoomError) => reply(error) + ); + } + +} diff --git a/src/user/user.model.ts b/src/user/user.model.ts new file mode 100644 index 0000000..a14fde6 --- /dev/null +++ b/src/user/user.model.ts @@ -0,0 +1,54 @@ +import * as Sequelize from 'sequelize'; +// import * as Bcrypt from "bcryptjs"; + +export interface IUser { + id?: string; + source: string; + sourceId: string; + email?: string; +}; + +export interface UserInstance extends Sequelize.Instance { + id: string; + source: string; + sourceId: string; + email?: string; + createdAt: Date; + updatedAt: Date; +} + +export interface UserModel + extends Sequelize.Model { } + + +export default function define(sequelize: Sequelize.Sequelize, DataTypes) { + const user = sequelize.define('user', { + 'id': { + 'type': DataTypes.INTEGER, + 'allowNull': false, + 'primaryKey': true, + 'autoIncrement': true + }, + 'source': { + 'type': DataTypes.STRING(64), + 'allowNull': false + }, + 'sourceId': { + 'type': DataTypes.STRING(64), + 'allowNull': false + }, + 'email': { + 'type': DataTypes.STRING(128), + 'allowNull': true, + 'unique': true + } + }, + { + 'tableName': 'user', + 'timestamps': true + }); + + user.sync(); + + return user; +} diff --git a/src/user/user.test.ts b/src/user/user.test.ts new file mode 100644 index 0000000..7aa88c5 --- /dev/null +++ b/src/user/user.test.ts @@ -0,0 +1,207 @@ +import * as test from 'tape'; +import * as Server from '../server'; +import * as Configs from '../configurations'; + +const serverConfigs = Configs.getServerConfig(); +const testConfigs = Configs.getTestConfig(); +const admin = testConfigs.admin; +const anonyme = testConfigs.anonyme; +const user1 = testConfigs.user1; + +Server.init(serverConfigs).then((server) => { + + test('POST /users/login - wrong password', function(t) { + const options = { + method: 'POST', + url: '/users/login', + payload: { + username: 'test', + password: 'test', + typeConnection: 'test' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.message, 'Incorrect username and/or password'); + t.equal(response.statusCode, 401); + server.stop(t.end); + }); + }); + + test('POST /users/login', function(t) { + const options = { + method: 'POST', + url: '/users/login', + payload: { + username: 'test', + password: 'testIgo2Password', + typeConnection: 'test' + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + // ======================================================= + + test('GET /users/info - user1', function(t) { + const options = { + method: 'GET', + url: '/users/info', + headers: { + 'x-consumer-username': 'test', + 'x-consumer-id': user1.xConsumerId, + 'x-consumer-custom-id': '1' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.source, 'test'); + t.equal(result.sourceId, 'test'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /users/info - inexistant', function(t) { + const options = { + method: 'GET', + url: '/users/info', + headers: { + 'x-consumer-username': 'test', + 'x-consumer-id': 'test', + 'x-consumer-custom-id': '2' + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 404); + server.stop(t.end); + }); + }); + + test('GET /users/info - inexistant', function(t) { + const options = { + method: 'GET', + url: '/users/info' + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 401); + server.stop(t.end); + }); + }); + + // ======================================================= + + test('GET /users/profils - admin', function(t) { + const options = { + method: 'GET', + url: '/users/profils', + headers: { + 'x-consumer-username': admin.xConsumerUsername, + 'x-consumer-id': admin.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.profils.length, 1); + t.equal(result.profils[0], 'admin'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /users/profils - anonyme', function(t) { + const options = { + method: 'GET', + url: '/users/profils', + headers: { + 'x-consumer-username': anonyme.xConsumerUsername, + 'x-consumer-id': anonyme.xConsumerId + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.profils.length, 0); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + // ======================================================= + + test('PATCH /users - inexistant', function(t) { + const options = { + method: 'PATCH', + url: '/users', + headers: { + 'x-consumer-username': 'test', + 'x-consumer-id': 'test', + 'x-consumer-custom-id': '1' + }, + payload: { + email: 'test@igo.com' + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + + test('GET /users/info - after patch', function(t) { + const options = { + method: 'GET', + url: '/users/info', + headers: { + 'x-consumer-username': 'test', + 'x-consumer-id': 'test', + 'x-consumer-custom-id': '1' + } + }; + server.inject(options, function(response) { + const result: any = response.result; + t.equal(result.source, 'test'); + t.equal(result.sourceId, 'test'); + t.equal(result.email, 'test@igo.com'); + t.equal(response.statusCode, 200); + server.stop(t.end); + }); + }); + +// ======================================================= + + test('DELETE /users - inexistant', function(t) { + const options = { + method: 'DELETE', + url: '/users', + headers: { + 'x-consumer-username': 'test', + 'x-consumer-id': 'test', + 'x-consumer-custom-id': '1' + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 204); + server.stop(t.end); + }); + }); + + test('GET /users/info - after delete', function(t) { + const options = { + method: 'GET', + url: '/users/info', + headers: { + 'x-consumer-username': 'test', + 'x-consumer-id': 'test', + 'x-consumer-custom-id': '1' + } + }; + server.inject(options, function(response) { + t.equal(response.statusCode, 404); + server.stop(t.end); + }); + }); + +}); diff --git a/src/user/user.ts b/src/user/user.ts new file mode 100644 index 0000000..acd2f74 --- /dev/null +++ b/src/user/user.ts @@ -0,0 +1,656 @@ +import * as Rx from 'rxjs'; +import * as Boom from 'boom'; +import * as Jwt from 'jsonwebtoken'; +import * as ldap from 'ldapjs'; +import * as https from 'https'; +import * as http from 'http'; +import * as querystring from 'querystring'; + +import { Base64, ObjectUtils } from '../utils'; +import * as Configs from '../configurations'; +import { IDatabase, database } from '../database'; + +import { IUser, UserInstance } from './user.model'; + +const ServerConfigs = Configs.getServerConfig(); + +export class User { + + private database: IDatabase = database; + + static getProfils(id: string) { + return Rx.Observable.create(observer => { + if (!id) { + observer.next([]); + observer.complete(); + return; + } + + const options = { + host: ServerConfigs.userApi.host, + port: ServerConfigs.userApi.port, + path: `/consumers/${id}/acls`, + method: 'GET' + }; + + const callback = (res) => { + res.setEncoding('utf8') ; + res.on('data', (d) => { + const data = JSON.parse(d); + if (!data.data) { + const message = `User '${id}' can not be found.`; + observer.error(Boom.badRequest(message)); + return; + } + + const profils = []; + + for (const p of data.data) { + profils.push(p.group); + } + + observer.next(profils); + observer.complete(); + }); + }; + + const req = http.request(options, callback); + req.end(); + }); + } + + public loginTestUser(username: string, + password: string): Rx.Observable { + + return Rx.Observable.create(observer => { + if (username !== 'test' || password !== 'testIgo2Password') { + const message = 'Incorrect username and/or password'; + observer.error(Boom.unauthorized(message)); + return; + } + this.getOrCreateUser(username, 'test').subscribe( + user => { + user.email = 'test@test.com'; + this.generateToken(user).subscribe( + token => { + observer.next({ + token: token + }); + observer.complete(); + } + ); + } + ); + }); + } + + public loginMspUser(username: string, + password: string): Rx.Observable { + + return Rx.Observable.create(observer => { + this.validatePassword(username, password).subscribe( + userInfo => { + this.getOrCreateUser(username, 'msp').subscribe( + user => { + user.email = userInfo.mail; + const groups = []; + for (const g of userInfo.groupMembership) { + if (g.search('ou=APP,ou=SSO,o=MSP') !== -1) { + const iStart = g.indexOf('cn=') + 3; + const iEnd = g.indexOf(','); + const gName = g.substring(iStart, iEnd); + groups.push(gName); + } + } + this.generateToken(user, groups).subscribe( + token => { + observer.next({ + token: token + }); + observer.complete(); + } + ); + } + ); + }, + error => { + const message = 'Incorrect username and/or password'; + observer.error(Boom.unauthorized(message)); + } + ); + }); + } + + public loginSocialUser(socialToken: string, + typeConnection: string): Rx.Observable { + + return Rx.Observable.create(observer => { + this.validateSocialToken(socialToken, typeConnection).subscribe( + userInfo => { + this.getOrCreateUser(userInfo.email, typeConnection).subscribe( + user => { + this.generateToken(user).subscribe( + token => { + observer.next({ + token: token + }); + observer.complete(); + } + ); + } + ); + }, + error => { + const message = `Incorrect token from ${typeConnection}`; + observer.error(Boom.unauthorized(message)); + } + ); + }); + } + + public update(id: string, user: IUser): Rx.Observable { + return Rx.Observable.create(observer => { + this.database.user.update(user, { + where: { + id: id + } + }).then((count: [number, UserInstance[]]) => { + if (count[0]) { + observer.next({id: id}); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public delete(id: string, kongId: string): Rx.Observable<{}> { + return Rx.Observable.create(observer => { + this.database.user.destroy({ + where: { + id: id + } + }).then((count: number) => { + if (count) { + this.deleteUserKong(kongId).subscribe(() => { + observer.next({}); + observer.complete(); + }); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + public info(id: string): Rx.Observable { + return Rx.Observable.create(observer => { + this.database.user.findOne({ + where: { + id: id + } + }).then((user: UserInstance) => { + if (user) { + observer.next(ObjectUtils.removeNull(user.get())); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + private getUserBySource(sourceId: string, sources: string | string[]) { + if (sources === 'facebook' || sources === 'google') { + sources = ['facebook', 'google']; + } + return Rx.Observable.create(observer => { + this.database.user.findOne({ + where: { + sourceId: sourceId, + source: sources + } + }).then((user: UserInstance) => { + if (user) { + observer.next(ObjectUtils.removeNull(user.get())); + observer.complete(); + } else { + observer.error(Boom.notFound()); + } + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + private createUserBySource(user: IUser) { + return Rx.Observable.create(observer => { + this.database.user.create(user).then((userCreated: UserInstance) => { + observer.next(ObjectUtils.removeNull(userCreated.get())); + observer.complete(); + }).catch((error) => { + observer.error(Boom.badImplementation(error)); + }); + }); + } + + private getOrCreateUser(sourceId: string, source: string) { + return Rx.Observable.create(observer => { + this.getUserBySource(sourceId, source).subscribe( + (user: UserInstance) => { + observer.next(user); + observer.complete(); + }, + (error) => { + if (error.output.statusCode === 404) { + this.createUserBySource({ + sourceId: sourceId, + source: source + }).subscribe((userCreated: UserInstance) => { + observer.next(userCreated); + observer.complete(); + }); + } + } + ); + }); + } + + private createUserKong(user: UserInstance) { + return Rx.Observable.create(observer => { + const userKongToCreate = querystring.stringify({ + username: user.sourceId, + custom_id: user.id + }); + const options = { + host: ServerConfigs.userApi.host, + port: ServerConfigs.userApi.port, + path: `/consumers`, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': Buffer.byteLength(userKongToCreate) + } + }; + + const callback = (res) => { + res.setEncoding('utf8'); + res.on('data', function(d) { + const data = JSON.parse(d); + observer.next(data); + observer.complete(); + }); + }; + + const req = http.request(options, callback); + req.write(userKongToCreate); + req.end(); + }); + } + + private deleteUserKong(id: string) { + return Rx.Observable.create(observer => { + const options = { + host: ServerConfigs.userApi.host, + port: ServerConfigs.userApi.port, + path: `/consumers/${id}`, + method: 'DELETE' + }; + + const callback = (res) => { + res.on('readable', () => {}); + res.on('end', () => { + observer.next(); + observer.complete(); + }); + }; + + const req = http.request(options, callback); + req.end(); + + }); + } + + private getUserKong(user: UserInstance) { + return Rx.Observable.create(observer => { + const options = { + host: ServerConfigs.userApi.host, + port: ServerConfigs.userApi.port, + path: `/consumers/${user.sourceId}`, + method: 'GET' + }; + + const callback = (res) => { + res.setEncoding('utf8'); + res.on('data', function(d) { + const data = JSON.parse(d); + observer.next(data); + observer.complete(); + }); + }; + + const req = http.request(options, callback); + req.end(); + }); + } + + private getOrCreateUserKong(user: UserInstance) { + return Rx.Observable.create(observer => { + this.getUserKong(user).subscribe((userKong) => { + if (!userKong.username) { + this.createUserKong(user).subscribe((userKongCreated) => { + observer.next(userKongCreated); + observer.complete(); + }); + } else { + observer.next(userKong); + observer.complete(); + } + }); + }); + } + + private createAccessTokenUserKong(userKong) { + return Rx.Observable.create(observer => { + const options = { + host: ServerConfigs.userApi.host, + port: ServerConfigs.userApi.port, + path: `/consumers/${userKong.id}/jwt`, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }; + + const callback = (res) => { + res.setEncoding('utf8'); + res.on('data', function(d) { + const data = JSON.parse(d); + observer.next(data); + observer.complete(); + }); + }; + + const req = http.request(options, callback); + req.end(); + }); + } + + private getAccessTokenUserKong(userKong) { + return Rx.Observable.create(observer => { + const options = { + host: ServerConfigs.userApi.host, + port: ServerConfigs.userApi.port, + path: `/consumers/${userKong.id}/jwt`, + method: 'GET' + }; + + const callback = (res) => { + res.setEncoding('utf8'); + res.on('data', (d) => { + const data = JSON.parse(d); + if (data.total) { + observer.next(data); + observer.complete(); + } else { + this.createAccessTokenUserKong(userKong).subscribe((tokenKong) => { + observer.next({ data: [tokenKong] }); + observer.complete(); + }); + } + }); + }; + + const req = http.request(options, callback); + req.end(); + }); + } + + private deleteGroupUserKong(userKong, groupKong) { + const options = { + host: ServerConfigs.userApi.host, + port: ServerConfigs.userApi.port, + path: `/consumers/${userKong.id}/acls/${groupKong.id}`, + method: 'DELETE' + }; + + const callback = (res) => { + res.setEncoding('utf8'); + res.on('data', (d) => { + + }); + }; + + const req = http.request(options, callback); + req.end(); + } + + private createGroupUserKong(userKong, groupKong) { + const aclToCreate = querystring.stringify({ + group: groupKong + }); + + const options = { + host: ServerConfigs.userApi.host, + port: ServerConfigs.userApi.port, + path: `/consumers/${userKong.id}/acls`, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': Buffer.byteLength(aclToCreate) + } + }; + + const callback = (res) => { + res.setEncoding('utf8'); + res.on('data', (d) => { + + }); + }; + + const req = http.request(options, callback); + req.write(aclToCreate); + req.end(); + } + + private updateGroupsUserKong(user: UserInstance, groups: string[], userKong) { + return Rx.Observable.create(observer => { + if (user.source !== 'msp') { + observer.next(); + observer.complete(); + return; + } + const options = { + host: ServerConfigs.userApi.host, + port: ServerConfigs.userApi.port, + path: `/consumers/${userKong.id}/acls`, + method: 'GET' + }; + + const callback = (res) => { + res.setEncoding('utf8'); + res.on('data', (d) => { + const data = JSON.parse(d); + for (const acl of data.data) { + if (acl.group.substring(0, 5) !== 'GRAPP') { + continue; + } + const gFound = groups.find((g) => g === acl.group); + if (!gFound) { + this.deleteGroupUserKong(userKong, acl); + } + } + + for (const g of groups) { + const aclFound = data.data.find((acl) => g === acl.group); + if (!aclFound) { + this.createGroupUserKong(userKong, g); + } + } + + + observer.next(); + observer.complete(); + }); + }; + + const req = http.request(options, callback); + req.end(); + }); + } + + private generateToken(user: UserInstance, groups?: string[]) { + return Rx.Observable.create(observer => { + const jwtExpiration = ServerConfigs.jwtExpiration; + + this.getOrCreateUserKong(user).subscribe((userKong) => { + this.updateGroupsUserKong(user, groups, userKong).subscribe(() => { + this.getAccessTokenUserKong(userKong).subscribe((tokenKong) => { + const jwtSecret = tokenKong.data[0].secret; + const iss = tokenKong.data[0].key; + const jwt = Jwt.sign( + { + user: user, + iss: iss + }, + jwtSecret, + { expiresIn: jwtExpiration } + ); + observer.next(jwt); + observer.complete(); + }); + }); + }); + }); + } + + private validatePassword(username: string, password: string) { + return Rx.Observable.create(observer => { + if (!username) { + observer.error(Boom.unauthorized('Username empty')); + } + const client = ldap.createClient({ + url: 'ldap://ldap.sso.msp.gouv.qc.ca' + }); + + const opts = { + filter: `(&(objectclass=person)(cn=${username}))`, + scope: 'sub', + attributes: ['dn', 'mail', 'sn', 'cn', + 'givenName', 'fullName', 'Language', + 'passwordExpirationTime', 'groupMembership'], + sizeLimit: 1 + }; + + let ldapres; + const baseSearch = 'ou=DTIA,ou=DGSG,ou=SSO,o=msp'; + client.search(baseSearch, opts, function(errSearch, search) { + search.on('searchEntry', function(entry) { + ldapres = entry.object; + }); + + search.on('end', function(result) { + if (ldapres) { + client.bind(ldapres.dn, Base64.decode(password), function(errBind) { + if (errBind) { + // TODO : gérer les mdp expirés. + observer.error(Boom.unauthorized('Wrong password')); + } else { + observer.next(ldapres); + observer.complete(); + } + }); + } else { + observer.error(Boom.unauthorized('Invalid username')); + } + }); + }); + }); + } + + + private validateSocialToken(token: string, typeConnection: string) { + if (typeConnection === 'facebook') { + return this.validateFacebookToken(token); + } else if (typeConnection === 'google') { + return this.validateGoogleToken(token); + } else { + return Rx.Observable.create(observer => { + observer.error(Boom.badRequest('Invalid connection type')); + }); + } + } + + private validateFacebookToken(token: string) { + return Rx.Observable.create(observer => { + + const fields = 'name,email,gender,first_name,last_name,picture,locale'; + const options = { + host: 'graph.facebook.com', + path: `/me?fields=${fields}&access_token=${token}`, + method: 'GET' + }; + + const callback = (res) => { + res.setEncoding('utf8'); + res.on('data', function(d) { + const data = JSON.parse(d); + if (data && !data.error) { + observer.next(data); + } else { + observer.error(Boom.unauthorized('Invalid token')); + } + observer.complete(); + }); + }; + + const req = https.request(options, callback); + req.end(); + }); + } + + private validateGoogleToken(token: string) { + return Rx.Observable.create(observer => { + + const key = ServerConfigs.googleKey; + const fields = 'person.names,person.locales,person.emailAddresses'; + + const options = { + host: 'content-people.googleapis.com', + path: `/v1/people/me?requestMask.includeField=${fields}&key=${key}`, + method: 'GET', + headers: { + 'Authorization': 'Bearer ' + token + } + }; + + const callback = (res) => { + res.setEncoding('utf8'); + res.on('data', function(d) { + const data = JSON.parse(d); + if (data && !data.error) { + const userInfo = { + email: data.emailAddresses[0].value, + id: data.names[0].metadata.source.id + }; + observer.next(userInfo); + } else { + observer.error(Boom.unauthorized('Invalid token')); + } + observer.complete(); + }); + }; + + const req = https.request(options, callback); + req.end(); + }); + } + +} diff --git a/src/user/user.validator.ts b/src/user/user.validator.ts new file mode 100644 index 0000000..24dd5aa --- /dev/null +++ b/src/user/user.validator.ts @@ -0,0 +1,61 @@ +import * as Joi from 'joi'; +import * as Boom from 'boom'; + +import * as Configs from '../configurations'; + +import { User } from './user'; + +export class UserValidator { + + static updateModel = Joi.object().keys({ + email: Joi.string().email() + }); + + static loginModel = Joi.object().keys({ + username: Joi.string(), + password: Joi.string().trim(), + token: Joi.string(), + typeConnection: Joi.string().valid('msp', 'facebook', 'google', 'test'), + }).xor('username', 'token'); + + static userValidator = Joi.object({ + 'x-consumer-id': Joi.string().required(), + 'x-consumer-username': Joi.string().required() + }).unknown(); + + static notAnonymousValidator = UserValidator.userValidator.concat( + Joi.object({ + 'x-anonymous-consumer': Joi.boolean().forbidden() + }).unknown() + ); + + static authenticateValidator = (value, options, next) => { + + const valid = Joi.validate(value, UserValidator.notAnonymousValidator); + + if (valid.error) { + next(Boom.unauthorized('Must be authenticated')); + } else { + next(null, value, options, next); + } + } + + static adminValidator = (value, options, next) => { + + const valid = Joi.validate(value, UserValidator.notAnonymousValidator); + + if (valid.error) { + next(Boom.unauthorized('Must be authenticated')); + } else { + const configs = Configs.getServerConfig(); + User.getProfils(value['x-consumer-id']).subscribe((profils) => { + if (profils.includes(configs.adminProfil)) { + next(null, value); + } else { + next(Boom.forbidden('Must be administrator')); + } + }); + } + } + +} diff --git a/src/utils/base64.ts b/src/utils/base64.ts new file mode 100644 index 0000000..802cd00 --- /dev/null +++ b/src/utils/base64.ts @@ -0,0 +1,85 @@ +/* tslint:disable */ +export class Base64 { + private static PADCHAR: string = '='; + private static ALPHA: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + + private static getByte(s: string, i: number): number { + const x = s.charCodeAt(i); + return x; + } + + private static getByte64(s: string, i: number): number { + const idx = this.ALPHA.indexOf(s.charAt(i)); + return idx; + } + + public static decode (s: string): string { + let pads = 0, + i, b10, imax = s.length, + x = []; + + s = String(s); + + if (imax === 0) { + return s; + } + + if (s.charAt(imax - 1) === this.PADCHAR) { + pads = 1; + if (s.charAt(imax - 2) === this.PADCHAR) { + pads = 2; + } + imax -= 4; + } + + for (i = 0; i < imax; i += 4) { + b10 = (this.getByte64(s, i) << 18) | (this.getByte64(s, i + 1) << 12) | (this.getByte64(s, i + 2) << 6) | this.getByte64(s, i + 3); + x.push(String.fromCharCode(b10 >> 16, (b10 >> 8) & 255, b10 & 255)); + } + + switch (pads) { + case 1: + b10 = (this.getByte64(s, i) << 18) | (this.getByte64(s, i + 1) << 12) | (this.getByte64(s, i + 2) << 6); + x.push(String.fromCharCode(b10 >> 16, (b10 >> 8) & 255)); + break; + case 2: + b10 = (this.getByte64(s, i) << 18) | (this.getByte64(s, i + 1) << 12); + x.push(String.fromCharCode(b10 >> 16)); + break; + } + + return x.join(''); + } + + public static encode(s: string): string { + s = String(s); + + let i, b10, x = [], + imax = s.length - s.length % 3; + + if (s.length === 0) { + return s; + } + + for (i = 0; i < imax; i += 3) { + b10 = (this.getByte(s, i) << 16) | (this.getByte(s, i + 1) << 8) | this.getByte(s, i + 2); + x.push(this.ALPHA.charAt(b10 >> 18)); + x.push(this.ALPHA.charAt((b10 >> 12) & 63)); + x.push(this.ALPHA.charAt((b10 >> 6) & 63)); + x.push(this.ALPHA.charAt(b10 & 63)); + } + + switch (s.length - imax) { + case 1: + b10 = this.getByte(s, i) << 16; + x.push(this.ALPHA.charAt(b10 >> 18) + this.ALPHA.charAt((b10 >> 12) & 63) + this.PADCHAR + this.PADCHAR); + break; + case 2: + b10 = (this.getByte(s, i) << 16) | (this.getByte(s, i + 1) << 8); + x.push(this.ALPHA.charAt(b10 >> 18) + this.ALPHA.charAt((b10 >> 12) & 63) + this.ALPHA.charAt((b10 >> 6) & 63) + this.PADCHAR); + break; + } + + return x.join(''); + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..cce8c08 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,3 @@ +export * from './base64'; +export * from './uuid'; +export * from './object-utils'; diff --git a/src/utils/object-utils.ts b/src/utils/object-utils.ts new file mode 100644 index 0000000..a6b07ab --- /dev/null +++ b/src/utils/object-utils.ts @@ -0,0 +1,93 @@ +export class ObjectUtils { + static resolve(obj: Object, key: string): any { + const keysArray = key.replace(/\[/g, '.').replace(/\]/g, '').split('.'); + let current = obj; + while (keysArray.length) { + if (typeof current !== 'object') { + return undefined; + } + current = current[keysArray.shift()]; + } + + return current; + } + + static isObject(item: Object) { + return (item && typeof item === 'object' && + !Array.isArray(item) && item !== null && + !(item instanceof Date)); + } + + static mergeDeep(target: Object, source: Object, + ignoreUndefined = false): any { + + const output = Object.assign({}, target); + if (ObjectUtils.isObject(target) && ObjectUtils.isObject(source)) { + Object.keys(source) + .filter((key) => !ignoreUndefined || source[key] !== undefined) + .forEach(key => { + if (ObjectUtils.isObject(source[key])) { + if (!(key in target)) { + Object.assign(output, { [key]: source[key] }); + } else { + output[key] = ObjectUtils.mergeDeep(target[key], + source[key], ignoreUndefined); + } + } else { + Object.assign(output, { [key]: source[key] }); + } + }); + } + return output; + } + + static removeUndefined(obj: Object): any { + const output = {}; + if (ObjectUtils.isObject(obj)) { + Object.keys(obj) + .filter((key) => obj[key] !== undefined) + .forEach(key => { + if (ObjectUtils.isObject(obj[key]) || Array.isArray(obj[key])) { + output[key] = ObjectUtils.removeUndefined(obj[key]); + } else { + output[key] = obj[key]; + } + }); + + return output; + } + + if (Array.isArray(obj)) { + return obj.map( + (o) => ObjectUtils.removeUndefined(o) + ); + } + + return obj; + } + + static removeNull(obj: Object): any { + const output = {}; + if (ObjectUtils.isObject(obj)) { + Object.keys(obj) + .filter((key) => obj[key] !== null) + .forEach(key => { + if (ObjectUtils.isObject(obj[key]) || Array.isArray(obj[key])) { + output[key] = ObjectUtils.removeNull(obj[key]); + } else { + output[key] = obj[key]; + } + }); + + return output; + } + + if (Array.isArray(obj)) { + return obj.map( + (o) => ObjectUtils.removeNull(o) + ); + } + + return obj; + } +} diff --git a/src/utils/uuid.ts b/src/utils/uuid.ts new file mode 100644 index 0000000..a3c5ff4 --- /dev/null +++ b/src/utils/uuid.ts @@ -0,0 +1,9 @@ +export function S4() { + // tslint:disable-next-line: no-bitwise + return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); +} + +export function uuid() { + return `${S4()}${S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}` + .toLowerCase(); +}