From 0d2b202450db3794bc920dad797ff738939d1541 Mon Sep 17 00:00:00 2001 From: LorenzoJokhan Date: Fri, 12 Jan 2024 16:08:35 +0100 Subject: [PATCH] Rewritten logic for anonimising all users, preventing the removal of certain auth db records if deleting fails on the api side --- src/models/Site.js | 665 ++++++++++++++++++++++++----------------- src/routes/api/site.js | 507 ++++++++++++++++--------------- 2 files changed, 650 insertions(+), 522 deletions(-) diff --git a/src/models/Site.js b/src/models/Site.js index c28681f9..25b11c49 100755 --- a/src/models/Site.js +++ b/src/models/Site.js @@ -6,48 +6,48 @@ const OAuthApi = require('../services/oauth-api'); const userHasRole = require('../lib/sequelize-authorization/lib/hasRole'); module.exports = function (db, sequelize, DataTypes) { + var Site = sequelize.define( + 'site', + { + name: { + type: DataTypes.STRING(255), + allowNull: true, + defaultValue: 'Nieuwe site', + }, - var Site = sequelize.define('site', { - - name: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: 'Nieuwe site', - }, - - title: { - type: DataTypes.STRING(255), - allowNull: true, - defaultValue: 'Nieuwe site', - }, - - domain: { - type: DataTypes.STRING(255), - allowNull: false, - defaultValue: 'demo.openstad.nl', - }, - - config: { - type: DataTypes.JSON, - allowNull: false, - defaultValue: {}, - get: function () { - let value = this.getDataValue('config'); - return this.parseConfig(value); + title: { + type: DataTypes.STRING(255), + allowNull: true, + defaultValue: 'Nieuwe site', }, - set: function (value) { - var currentconfig = this.getDataValue('config'); - value = value || {}; - value = merge.recursive(true, currentconfig, value); - this.setDataValue('config', this.parseConfig(value)); + + domain: { + type: DataTypes.STRING(255), + allowNull: false, + defaultValue: 'demo.openstad.nl', }, - auth: { - viewableBy: 'editor', - updateableBy: 'editor', + + config: { + type: DataTypes.JSON, + allowNull: false, + defaultValue: {}, + get: function () { + let value = this.getDataValue('config'); + return this.parseConfig(value); + }, + set: function (value) { + var currentconfig = this.getDataValue('config'); + value = value || {}; + value = merge.recursive(true, currentconfig, value); + this.setDataValue('config', this.parseConfig(value)); + }, + auth: { + viewableBy: 'editor', + updateableBy: 'editor', + }, }, - }, - /* + /* HostStatus is used for tracking domain status For instance, mostly managed by checkHostStatus service { @@ -55,102 +55,108 @@ module.exports = function (db, sequelize, DataTypes) { "ingress": false // if on k8s cluster will try to make a ingress host file if IP address is set properly, k8s cert manager will then try get a let's encrypt cert } if */ - hostStatus: { - type: DataTypes.JSON, - allowNull: false, - defaultValue: {}, - auth: { - viewableBy: 'admin', + hostStatus: { + type: DataTypes.JSON, + allowNull: false, + defaultValue: {}, + auth: { + viewableBy: 'admin', + }, }, - }, - - areaId: { - type: DataTypes.INTEGER, - allowNull: true, - } - - }, { - - hooks: { - beforeValidate: async function (instance, options) { - - try { - // ik zou verwachten dat je dit met _previousDataValues kunt doen, maar die bevat al de nieuwe waarde - let current = await db.Site.findOne({ where: { id: instance.id } }); - - // on update of projectHasEnded also update isActive of all the parts - if (current && typeof instance.config.project.projectHasEnded != 'undefined' && current.config.project.projectHasEnded !== instance.config.project.projectHasEnded) { - let config = merge.recursive(true, instance.config); - if (instance.config.project.projectHasEnded) { - config.votes.isActive = false; - config.ideas.canAddNewIdeas = false; - config.articles.canAddNewArticles = false; - config.arguments.isClosed = true; - config.polls.canAddPolls = false; - config.users.canCreateNewUsers = false; - } else { - // commented: do not update these params on unsetting - // config.votes.isActive = true; - // config.ideas.canAddNewIdeas = true; - // config.articles.canAddNewArticles = true; - // config.arguments.isClosed = false; - // config.polls.canAddPolls = true; - // config.users.canCreateNewUsers = true; + areaId: { + type: DataTypes.INTEGER, + allowNull: true, + }, + }, + { + hooks: { + beforeValidate: async function (instance, options) { + try { + // ik zou verwachten dat je dit met _previousDataValues kunt doen, maar die bevat al de nieuwe waarde + let current = await db.Site.findOne({ where: { id: instance.id } }); + + // on update of projectHasEnded also update isActive of all the parts + if ( + current && + typeof instance.config.project.projectHasEnded != 'undefined' && + current.config.project.projectHasEnded !== + instance.config.project.projectHasEnded + ) { + let config = merge.recursive(true, instance.config); + if (instance.config.project.projectHasEnded) { + config.votes.isActive = false; + config.ideas.canAddNewIdeas = false; + config.articles.canAddNewArticles = false; + config.arguments.isClosed = true; + config.polls.canAddPolls = false; + config.users.canCreateNewUsers = false; + } else { + // commented: do not update these params on unsetting + // config.votes.isActive = true; + // config.ideas.canAddNewIdeas = true; + // config.articles.canAddNewArticles = true; + // config.arguments.isClosed = false; + // config.polls.canAddPolls = true; + // config.users.canCreateNewUsers = true; + } + instance.set('config', config); } - instance.set('config', config); + } catch (err) { + console.log(err); + throw err; } - - } catch (err) { - console.log(err); - throw err; - } - - - - }, + }, - beforeCreate: function (instance, options) { - return beforeUpdateOrCreate(instance, options); - }, + beforeCreate: function (instance, options) { + return beforeUpdateOrCreate(instance, options); + }, - beforeUpdate: function (instance, options) { - return beforeUpdateOrCreate(instance, options); - }, + beforeUpdate: function (instance, options) { + return beforeUpdateOrCreate(instance, options); + }, - beforeDestroy: async function (instance, options) { - // project has ended - if (!(instance && instance.config && instance.config.project.projectHasEnded)) throw Error('Cannot delete an active site - first set the project-has-ended parameter'); - // are all users anonymized - let found = await db.User - .findAll({ - where: { - siteId: instance.id, - role: 'member', - } - }) + beforeDestroy: async function (instance, options) { + // project has ended + if ( + !( + instance && + instance.config && + instance.config.project.projectHasEnded + ) + ) + throw Error( + 'Cannot delete an active site - first set the project-has-ended parameter' + ); + // are all users anonymized + let found = await db.User.findAll({ + where: { + siteId: instance.id, + role: 'member', + }, + }); - if (found.length > 0) throw Error('Cannot delete an active site - first anonymize all users'); - return + if (found.length > 0) + throw Error( + 'Cannot delete an active site - first anonymize all users' + ); + return; + }, }, - - }, - - }); + } + ); async function beforeUpdateOrCreate(instance, options) { try { - // TODO: dit gebeurd nu in de route maar moet denk ik naar hier -// // canCreateNewUsers must be updated on the clients -// if (instance.config.users && typeof instance.config.users.canCreateNewUsers != 'undefined' ) { -// let config = { users: { canCreateNewUsers: instance.config.users.canCreateNewUsers } } -// // for ( let which of Object.keys(instance.config.oauth) ) { // TODO: moet deze loop naar dde OAuthApi? -// let which = 'anonymous'; -// await OAuthApi.updateClient({ siteConfig: instance.config, which, clientData: { config } }) -//// } -// } - + // // canCreateNewUsers must be updated on the clients + // if (instance.config.users && typeof instance.config.users.canCreateNewUsers != 'undefined' ) { + // let config = { users: { canCreateNewUsers: instance.config.users.canCreateNewUsers } } + // // for ( let which of Object.keys(instance.config.oauth) ) { // TODO: moet deze loop naar dde OAuthApi? + // let which = 'anonymous'; + // await OAuthApi.updateClient({ siteConfig: instance.config, which, clientData: { config } }) + //// } + // } } catch (err) { console.log(err); throw err; @@ -162,30 +168,29 @@ module.exports = function (db, sequelize, DataTypes) { defaultScope: {}, withArea: { - include: [{ - model: db.Area - }] - } + include: [ + { + model: db.Area, + }, + ], + }, }; - } + }; Site.associate = function (models) { this.hasMany(models.User); this.hasMany(models.Idea); this.belongsTo(models.Area); - } + }; Site.configOptions = function () { // definition of possible config values // todo: formaat gelijktrekken met sequelize defs // todo: je zou ook opties kunnen hebben die wel een default hebbe maar niet editable zijn? apiUrl bijv. Of misschien is die afgeleid return { - allowedDomains: { type: 'arrayOfStrings', - default: [ - 'openstad-api.amsterdam.nl' - ] + default: ['openstad-api.amsterdam.nl'], }, project: { @@ -261,7 +266,7 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o type: 'string', default: 'LqKNcKC7', }, - } + }, }, cms: { @@ -283,28 +288,28 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o type: 'string', default: '/oauth/login?jwt=[[jwt]]', }, - "redirectURI": { + redirectURI: { type: 'string', default: undefined, }, - "widgetDisplaySettings": { - "type": "object", - "subset": { - "beta": { - "type": "boolean", - "default": false + widgetDisplaySettings: { + type: 'object', + subset: { + beta: { + type: 'boolean', + default: false, }, - "deprecated": { - "type": "boolean", - "default": false + deprecated: { + type: 'boolean', + default: false, }, - "visibleWidgets": { - "type": "arrayOfStrings", - "default": [] - } - } - } - } + visibleWidgets: { + type: 'arrayOfStrings', + default: [], + }, + }, + }, + }, }, notifications: { @@ -339,38 +344,38 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o type: 'string', // todo: add type email/list of emails default: 'EMAIL@NOT.DEFINED', }, - } - } - } + }, + }, + }, }, - 'oauth': { + oauth: { type: 'objectsInObject', subset: { - "auth-server-url": { + 'auth-server-url': { type: 'string', }, - "auth-client-id": { + 'auth-client-id': { type: 'string', }, - "auth-client-secret": { + 'auth-client-secret': { type: 'string', }, - "auth-server-login-path": { + 'auth-server-login-path': { type: 'string', }, - "auth-server-exchange-code-path": { + 'auth-server-exchange-code-path': { type: 'string', }, - "auth-server-get-user-path": { + 'auth-server-get-user-path': { type: 'string', }, - "auth-server-logout-path": { + 'auth-server-logout-path': { type: 'string', }, - "after-login-redirect-uri": { + 'after-login-redirect-uri': { type: 'string', - } - } + }, + }, }, ideas: { type: 'object', @@ -427,7 +432,7 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o }, inzendingPath: { type: 'string', - default: "/PATH/TO/PLAN/[[ideaId]]", + default: '/PATH/TO/PLAN/[[ideaId]]', }, template: { type: 'string', @@ -445,7 +450,7 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o }, inzendingPath: { type: 'string', - default: "/PATH/TO/PLAN/[[ideaId]]", + default: '/PATH/TO/PLAN/[[ideaId]]', }, template: { type: 'string', @@ -453,23 +458,23 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o }, }, conceptToPublishedEmail: { - from: { - type: 'string', // todo: add type email/list of emails - default: 'EMAIL@NOT.DEFINED', - }, - subject: { - type: 'string', - default: undefined, - }, - inzendingPath: { - type: 'string', - default: "/PATH/TO/PLAN/[[ideaId]]", - }, - template: { - type: 'string', - default: undefined, - }, + from: { + type: 'string', // todo: add type email/list of emails + default: 'EMAIL@NOT.DEFINED', + }, + subject: { + type: 'string', + default: undefined, + }, + inzendingPath: { + type: 'string', + default: '/PATH/TO/PLAN/[[ideaId]]', + }, + template: { + type: 'string', + default: undefined, }, + }, extraDataMustBeDefined: { type: 'boolean', default: false, @@ -517,13 +522,13 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o default: 'white', }, // TODO: deze komen uit cms thema; werk dat verder uit - "flag": {type: 'string', default: ''}, - "mapUploadedFlag": {type: 'string', default: ''}, - "mapFlagWidth": {type: 'string', default: ''}, - "mapFlagHeight": {type: 'string', default: ''}, - "Initialavailablebudget": {type: 'int', default: 0}, - "minimalBudgetSpent": {type: 'int', default: 0}, - } + flag: { type: 'string', default: '' }, + mapUploadedFlag: { type: 'string', default: '' }, + mapFlagWidth: { type: 'string', default: '' }, + mapFlagHeight: { type: 'string', default: '' }, + Initialavailablebudget: { type: 'int', default: 0 }, + minimalBudgetSpent: { type: 'int', default: 0 }, + }, }, automaticallyUpdateStatus: { isActive: { @@ -535,7 +540,7 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o default: 90, }, }, - } + }, }, arguments: { type: 'object', @@ -553,14 +558,14 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o notAllowedMessage: { type: 'string', default: null, - } - } + }, + }, }, showFields: { type: 'arrayOfStrings', // eh... default: ['zipCode', 'displayName'], - } - } + }, + }, }, isClosed: { @@ -570,10 +575,10 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o closedText: { type: 'string', - default: 'De reactiemogelijkheid is gesloten, u kunt niet meer reageren', + default: + 'De reactiemogelijkheid is gesloten, u kunt niet meer reageren', }, - - } + }, }, users: { type: 'object', @@ -598,7 +603,6 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o votes: { type: 'object', subset: { - isViewable: { type: 'boolean', default: false, @@ -637,7 +641,13 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o voteType: { type: 'enum', - values: ['likes', 'count', 'budgeting', 'count-per-theme', 'budgeting-per-theme'], + values: [ + 'likes', + 'count', + 'budgeting', + 'count-per-theme', + 'budgeting-per-theme', + ], default: 'likes', }, @@ -646,11 +656,11 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o default: [ { label: 'voor', - value: 'yes' + value: 'yes', }, { label: 'tegen', - value: 'no' + value: 'no', }, ], }, @@ -686,9 +696,8 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o type: 'int', default: undefined, }, - } + }, }, - }, }, @@ -742,7 +751,7 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o }, inzendingPath: { type: 'string', - default: "/PATH/TO/ARTICLE/[[articleId]]", + default: '/PATH/TO/ARTICLE/[[articleId]]', }, template: { type: 'string', @@ -755,8 +764,8 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o }, extraData: { type: 'object', - } - } + }, + }, }, polls: { @@ -784,7 +793,7 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o type: 'boolean', default: false, }, - "confirmationEmail": { + confirmationEmail: { type: 'object', subset: { from: { @@ -797,7 +806,7 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o }, url: { type: 'string', - default: "/PATH/TO/CONFIRMATION/[[token]]", + default: '/PATH/TO/CONFIRMATION/[[token]]', }, template: { type: 'string', @@ -812,17 +821,14 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o status: null, }, - "ignoreBruteForce": { + ignoreBruteForce: { type: 'arrayOfStrings', - default: [] + default: [], }, - - } - } + }; + }; Site.prototype.parseConfig = function (config) { - - try { if (typeof config == 'string') { config = JSON.parse(config); @@ -833,34 +839,59 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o let options = Site.configOptions(); - - config = checkValues(config, options) + config = checkValues(config, options); return config; function checkValues(value, options) { - let newValue = {}; - Object.keys(options).forEach(key => { - + Object.keys(options).forEach((key) => { // backwards compatibility op oauth settings - if (key == 'oauth' && value[key] && !value[key].default && (value[key]['auth-server-url'] || value[key]['auth-client-id'] || value[key]['auth-client-secret'] || value[key]['auth-server-login-path'] || value[key]['auth-server-exchange-code-path'] || value[key]['auth-server-get-user-path'] || value[key]['auth-server-logout-path'] || value[key]['after-login-redirect-uri'])) { + if ( + key == 'oauth' && + value[key] && + !value[key].default && + (value[key]['auth-server-url'] || + value[key]['auth-client-id'] || + value[key]['auth-client-secret'] || + value[key]['auth-server-login-path'] || + value[key]['auth-server-exchange-code-path'] || + value[key]['auth-server-get-user-path'] || + value[key]['auth-server-logout-path'] || + value[key]['after-login-redirect-uri']) + ) { // dit is een oude - value[key] = {default: value[key]}; + value[key] = { default: value[key] }; } // backwards compatibility op notifications settings if (key == 'notifications' && value[key]) { - if (value[key].from && ( !(value[key].fromAddress) || value[key].fromAddress == options[key].subset.fromAddress.default )) { + if ( + value[key].from && + (!value[key].fromAddress || + value[key].fromAddress == options[key].subset.fromAddress.default) + ) { value[key].fromAddress = value[key].from; value[key].from = undefined; } if (value[key].to) { - if ( !value[key].projectmanagerAddress || value[key].projectmanagerAddress == options[key].subset.projectmanagerAddress.default ) { - value[key].projectmanagerAddress = value[key].to || apiConfig.notifications.admin.emailAddress || options[key].subset.projectmanagerAddress.default; + if ( + !value[key].projectmanagerAddress || + value[key].projectmanagerAddress == + options[key].subset.projectmanagerAddress.default + ) { + value[key].projectmanagerAddress = + value[key].to || + apiConfig.notifications.admin.emailAddress || + options[key].subset.projectmanagerAddress.default; } - if ( !value[key].siteadminAddress || value[key].siteadminAddress == options[key].subset.default ) { - value[key].siteadminAddress = apiConfig.notifications.admin.emailAddress || value[key].projectmanagerAddress; + if ( + !value[key].siteadminAddress || + value[key].siteadminAddress == options[key].subset.default + ) { + value[key].siteadminAddress = + apiConfig.notifications.admin.emailAddress || + value[key].projectmanagerAddress; } value[key].to = undefined; } @@ -869,10 +900,13 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o // backwards compatibility projectHasEnded if (key == 'project') { value[key] = value[key] || {}; - if (typeof value[key].projectHasEnded == 'undefined' && typeof value.projectHasEnded != 'undefined') { + if ( + typeof value[key].projectHasEnded == 'undefined' && + typeof value.projectHasEnded != 'undefined' + ) { // dit is een oude value[key].projectHasEnded = value.projectHasEnded; - delete value.projectHasEnded + delete value.projectHasEnded; } } @@ -881,18 +915,25 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o // objects in objects if (options[key].type == 'object' && options[key].subset) { let temp = checkValues(value[key] || {}, options[key].subset); // recusion - return newValue[key] = Object.keys(temp) ? temp : undefined; + return (newValue[key] = Object.keys(temp) ? temp : undefined); } // objects in objects - if (options[key].type == 'objectsInObject' && options[key].subset && value[key]) { + if ( + options[key].type == 'objectsInObject' && + options[key].subset && + value[key] + ) { newValue[key] = {}; let elementkeys = Object.keys(value[key]); for (let i = 0; i < elementkeys.length; i++) { let elementkey = elementkeys[i]; if (value[key][elementkey] == null) { } else { - let temp = checkValues(value[key][elementkey] || {}, options[key].subset); // recusion + let temp = checkValues( + value[key][elementkey] || {}, + options[key].subset + ); // recusion newValue[key][elementkey] = Object.keys(temp) ? temp : undefined; } } @@ -901,33 +942,74 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o // TODO: in progress if (typeof value[key] != 'undefined' && value[key] != null) { - if (options[key].type && options[key].type === 'int' && parseInt(value[key]) !== value[key]) { + if ( + options[key].type && + options[key].type === 'int' && + parseInt(value[key]) !== value[key] + ) { throw new Error(`site.config: ${key} must be an int`); } - if (options[key].type && options[key].type === 'string' && typeof value[key] !== 'string') { + if ( + options[key].type && + options[key].type === 'string' && + typeof value[key] !== 'string' + ) { throw new Error(`site.config: ${key} must be an string`); } - if (options[key].type && options[key].type === 'boolean' && typeof value[key] !== 'boolean') { - throw new Error(`site.config: ${key} must be an boolean ${value[key]}, ${options}, ${typeof value[key]}`); + if ( + options[key].type && + options[key].type === 'boolean' && + typeof value[key] !== 'boolean' + ) { + throw new Error( + `site.config: ${key} must be an boolean ${ + value[key] + }, ${options}, ${typeof value[key]}` + ); } - if (options[key].type && options[key].type === 'object' && typeof value[key] !== 'object') { + if ( + options[key].type && + options[key].type === 'object' && + typeof value[key] !== 'object' + ) { throw new Error(`site.config: ${key} must be an object`); } - if (options[key].type && options[key].type === 'arrayOfStrings' && !(typeof value[key] === 'object' && Array.isArray(value[key]) && !value[key].find(val => typeof val !== 'string'))) { + if ( + options[key].type && + options[key].type === 'arrayOfStrings' && + !( + typeof value[key] === 'object' && + Array.isArray(value[key]) && + !value[key].find((val) => typeof val !== 'string') + ) + ) { throw new Error(`site.config: ${key} must be an array of strings`); } - if (options[key].type && options[key].type === 'arrayOfObjects' && !(typeof value[key] === 'object' && Array.isArray(value[key]) && !value[key].find(val => typeof val !== 'object'))) { + if ( + options[key].type && + options[key].type === 'arrayOfObjects' && + !( + typeof value[key] === 'object' && + Array.isArray(value[key]) && + !value[key].find((val) => typeof val !== 'object') + ) + ) { throw new Error(`site.config: ${key} must be an array of objects`); } - if (options[key].type && options[key].type === 'enum' && options[key].values && options[key].values.indexOf(value[key]) == -1) { + if ( + options[key].type && + options[key].type === 'enum' && + options[key].values && + options[key].values.indexOf(value[key]) == -1 + ) { throw new Error(`site.config: ${key} has an invalid value`); } - return newValue[key] = value[key]; + return (newValue[key] = value[key]); } // default? if (typeof options[key].default != 'undefined') { - return newValue[key] = options[key].default + return (newValue[key] = options[key].default); } // set to null @@ -941,22 +1023,19 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o } return newValue[key]; - }); // voor nu mag je er in stoppen wat je wilt; uiteindelijk moet dat zo gaan werken dat je alleen bestaande opties mag gebruiken // dit blok kan dan weg - Object.keys(value).forEach(key => { + Object.keys(value).forEach((key) => { if (typeof newValue[key] == 'undefined') { newValue[key] = value[key]; } }); return newValue; - } - - } + }; Site.prototype.willAnonymizeAllUsers = async function () { let self = this; @@ -964,66 +1043,104 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o try { if (!self.id) throw Error('Site not found'); - if (!self.config.project.projectHasEnded) throw Error('Cannot anonymize users on an active site - first set the project-has-ended parameter'); + if (!self.config.project.projectHasEnded) + throw Error( + 'Cannot anonymize users on an active site - first set the project-has-ended parameter' + ); - let users = await db.User.findAll({ where: { siteId: self.id, externalUserId: { [Sequelize.Op.ne]: null } } }); + let users = await db.User.findAll({ + where: { siteId: self.id, externalUserId: { [Sequelize.Op.ne]: null } }, + }); // do not anonymize admins - result.admins = users.filter( user => userHasRole(user, 'admin') ); - result.users = users.filter( user => !userHasRole(user, 'admin') ); + result.admins = users.filter((user) => userHasRole(user, 'admin')); + result.users = users.filter((user) => !userHasRole(user, 'admin')); // extract externalUserIds - result.externalUserIds = result.users.filter( user => user.externalUserId ).map( user => user.externalUserId ); + result.externalUserIds = result.users + .filter((user) => user.externalUserId) + .map((user) => user.externalUserId); } catch (err) { console.log(err); throw err; } return result; - } + }; - Site.prototype.doAnonymizeAllUsers = async function (usersToAnonymize, externalUserIds, useOauth='default') { + Site.prototype.doAnonymizeAllUsers = async function ( + usersToAnonymize, + externalUserIds, + useOauth = 'default' + ) { // anonymize all users for this site let self = this; const amountOfUsersPerSecond = 50; + let externalUsrIds = [...externalUserIds]; + try { // Anonymize users - for (const user of usersToAnonymize) { - await new Promise((resolve, reject) => { - setTimeout(async function() { - user.site = self; - let res = await user.doAnonymize(); - user.site = null; - }, 1000 / amountOfUsersPerSecond) - }) - .then(result => resolve() ) - .catch(function (err) { - throw err; - }); + const anonymizingActions = usersToAnonymize.map( + (user) => + new Promise((resolve, reject) => { + setTimeout(async function () { + try { + user.site = self; + let res = await user.doAnonymize(); + user.site = null; + resolve(user); + } catch (e) { + reject({ error: e, user }); + } + }, 1000 / amountOfUsersPerSecond); + }) + ); + + const anonymizeResult = await Promise.allSettled(anonymizingActions); + const failedAnonimisations = anonymizeResult.filter( + (r) => r.status === 'rejected' + ); + + // Should for any reason, some of the operations fail, prevent the removal of externalUserIds. So in a next succesfull try, the operation after this one will work as usual + for (const failedAnonimisation of failedAnonimisations) { + externalUsrIds = externalUsrIds.filter( + (extUsrId) => + extUsrId === failedAnonimisation.reason.user.externalUserId + ); } - for (let externalUserId of externalUserIds) { + for (let externalUserId of externalUsrIds) { let users = await db.User.findAll({ where: { externalUserId } }); if (users.length == 0) { // no api users left for this oauth user, so remove the oauth user let siteConfig = self && merge({}, self.config, { id: self.id }); - await OAuthApi.deleteUser({ siteConfig, useOauth, userData: { id: externalUserId }}) + await OAuthApi.deleteUser({ + siteConfig, + useOauth, + userData: { id: externalUserId }, + }); } } } catch (err) { console.log(err); throw err; } - } + }; Site.prototype.isVoteActive = function () { let self = this; let voteIsActive = self.config.votes.isActive; - if ( ( voteIsActive == null || typeof voteIsActive == 'undefined' ) && self.config.votes.isActiveFrom && self.config.votes.isActiveTo ) { - voteIsActive = moment().isAfter(self.config.votes.isActiveFrom) && moment().isBefore(self.config.votes.isActiveTo) + if ( + (voteIsActive == null || typeof voteIsActive == 'undefined') && + self.config.votes.isActiveFrom && + self.config.votes.isActiveTo + ) { + voteIsActive = + moment().isAfter(self.config.votes.isActiveFrom) && + moment().isBefore(self.config.votes.isActiveTo); } return voteIsActive; - } + }; Site.auth = Site.prototype.auth = { listableBy: 'moderator', @@ -1031,16 +1148,14 @@ Wil je dit liever niet? Dan hoef je alleen een keer in te loggen op de website o createableBy: 'admin', updateableBy: 'editor', deleteableBy: 'admin', - canAnonymizeAllUsers : function(user, self) { + canAnonymizeAllUsers: function (user, self) { self = self || this; if (!user) user = self.auth && self.auth.user; if (!user || !user.role) user = { role: 'all' }; let isValid = userHasRole(user, 'admin', self.id); return isValid; - } - - } + }, + }; return Site; - }; diff --git a/src/routes/api/site.js b/src/routes/api/site.js index 9761ed1b..eba04bdd 100755 --- a/src/routes/api/site.js +++ b/src/routes/api/site.js @@ -1,305 +1,317 @@ -const express = require('express'); -const config = require('config'); -const fetch = require('node-fetch'); -const merge = require('merge'); -const Sequelize = require('sequelize'); -const db = require('../../db'); -const auth = require('../../middleware/sequelize-authorization-middleware'); -const pagination = require('../../middleware/pagination'); -const searchResults = require('../../middleware/search-results-user'); -const oauthClients = require('../../middleware/oauth-clients'); -const checkHostStatus = require('../../services/checkHostStatus') -const OAuthApi = require('../../services/oauth-api'); +const express = require('express'); +const config = require('config'); +const fetch = require('node-fetch'); +const merge = require('merge'); +const Sequelize = require('sequelize'); +const db = require('../../db'); +const auth = require('../../middleware/sequelize-authorization-middleware'); +const pagination = require('../../middleware/pagination'); +const searchResults = require('../../middleware/search-results-user'); +const oauthClients = require('../../middleware/oauth-clients'); +const checkHostStatus = require('../../services/checkHostStatus'); +const OAuthApi = require('../../services/oauth-api'); const sitesWithIssues = require('../../services/sites-with-issues'); -let router = express.Router({mergeParams: true}); +let router = express.Router({ mergeParams: true }); const refreshSiteConfigMw = function (req, res, next) { - const site = req.results; + const site = req.results; - // assume https, wont work for some dev environments - const cmsUrl = site.config.cms && site.config.cms.url ? site.config.cms.url : 'https://' + site.domain; + // assume https, wont work for some dev environments + const cmsUrl = + site.config.cms && site.config.cms.url + ? site.config.cms.url + : 'https://' + site.domain; - if (!cmsUrl) { - next(); - } + if (!cmsUrl) { + next(); + } - //return fetch(cmsUrl + '/modules/openstad-api/refresh') - /* + //return fetch(cmsUrl + '/modules/openstad-api/refresh') + /* @todo The /modules/openstad-api/refresh is cleaner, doesn't require a restart but needs basichAuth headers in case a site is password protected */ - return fetch(cmsUrl + '/config-reset') - .then(function () { next(); }) - .catch(function (err) { console.log('errrr', err); next(); }); -} - -router.route('/') + return fetch(cmsUrl + '/config-reset') + .then(function () { + next(); + }) + .catch(function (err) { + console.log('errrr', err); + next(); + }); +}; -// list sites -// ---------- - .get(auth.can('Site', 'list')) - .get(pagination.init) - .get(function(req, res, next) { +router + .route('/') - const scope = ['withArea']; + // list sites + // ---------- + .get(auth.can('Site', 'list')) + .get(pagination.init) + .get(function (req, res, next) { + const scope = ['withArea']; - db.Site - .scope(scope) - .findAndCountAll({ offset: req.dbQuery.offset, limit: req.dbQuery.limit }) - .then( result => { + db.Site.scope(scope) + .findAndCountAll({ offset: req.dbQuery.offset, limit: req.dbQuery.limit }) + .then((result) => { req.results = result.rows; req.dbQuery.count = result.count; return next(); - }) - .catch(next); - }) - .get(searchResults) - .get(auth.useReqUser) - .get(pagination.paginateResults) - .get(function(req, res, next) { - let records = req.results.records || req.results - records.forEach((record, i) => { - let site = record.toJSON() - if (!( req.user && req.user.role && req.user.role == 'admin' )) { + }) + .catch(next); + }) + .get(searchResults) + .get(auth.useReqUser) + .get(pagination.paginateResults) + .get(function (req, res, next) { + let records = req.results.records || req.results; + records.forEach((record, i) => { + let site = record.toJSON(); + if (!(req.user && req.user.role && req.user.role == 'admin')) { site.config = undefined; - } + } records[i] = site; }); - res.json(req.results); + res.json(req.results); }) -// create site -// ----------- - .post(auth.can('Site', 'create')) - .post(function(req, res, next) { - db.Site - .create(req.body) - .then((result) => { - req.results = result; - next(); - //return checkHostStatus({id: result.id}); - }) - .catch(next) - }) - .post(auth.useReqUser) - .post(refreshSiteConfigMw) - .post(function(req, res, next) { - return res.json(req.results); + // create site + // ----------- + .post(auth.can('Site', 'create')) + .post(function (req, res, next) { + db.Site.create(req.body) + .then((result) => { + req.results = result; + next(); + //return checkHostStatus({id: result.id}); + }) + .catch(next); }) + .post(auth.useReqUser) + .post(refreshSiteConfigMw) + .post(function (req, res, next) { + return res.json(req.results); + }); // list sites with issues -router.route('/issues') -// ------------------------------- - .get(auth.can('Site', 'list')) - .get(pagination.init) - .get(function(req, res, next) { +router + .route('/issues') + // ------------------------------- + .get(auth.can('Site', 'list')) + .get(pagination.init) + .get(function (req, res, next) { req.results = []; req.dbQuery.count = 0; return next(); }) - .get(function(req, res, next) { - + .get(function (req, res, next) { // sites that should be ended but are not - sitesWithIssues.shouldHaveEndedButAreNot({ offset: req.dbQuery.offset, limit: req.dbQuery.limit }) - .then( result => { - req.results = req.results.concat( result.rows ); + sitesWithIssues + .shouldHaveEndedButAreNot({ + offset: req.dbQuery.offset, + limit: req.dbQuery.limit, + }) + .then((result) => { + req.results = req.results.concat(result.rows); req.dbQuery.count += result.count; return next(); - }) - .catch(next); - - }) - .get(function(req, res, next) { - + }) + .catch(next); + }) + .get(function (req, res, next) { // sites that have ended but are not anonymized - sitesWithIssues.endedButNotAnonymized({ offset: req.dbQuery.offset, limit: req.dbQuery.limit }) - .then( result => { - req.results = req.results.concat( result.rows ); + sitesWithIssues + .endedButNotAnonymized({ + offset: req.dbQuery.offset, + limit: req.dbQuery.limit, + }) + .then((result) => { + req.results = req.results.concat(result.rows); req.dbQuery.count += result.count; return next(); - }) - .catch(next); - - }) - .get(searchResults) - .get(auth.useReqUser) - .get(pagination.paginateResults) - .get(function(req, res, next) { - let records = req.results.records || req.results - records.forEach((record, i) => { - let site = record.toJSON() - if (!( req.user && req.user.role && req.user.role == 'admin' )) { + }) + .catch(next); + }) + .get(searchResults) + .get(auth.useReqUser) + .get(pagination.paginateResults) + .get(function (req, res, next) { + let records = req.results.records || req.results; + records.forEach((record, i) => { + let site = record.toJSON(); + if (!(req.user && req.user.role && req.user.role == 'admin')) { site.config = undefined; - } + } records[i] = site; }); - res.json(req.results); - }) + res.json(req.results); + }); // one site routes: get site // ------------------------- -router.route('/:siteIdOrDomain') //(\\d+) - .all(auth.can('Site', 'view')) - .all(function(req, res, next) { - const siteIdOrDomain = req.params.siteIdOrDomain; - let query; - - if (isNaN(siteIdOrDomain)) { - query = { where: { domain: siteIdOrDomain } } - } else { - query = { where: { id: parseInt(siteIdOrDomain) } } - } - - db.Site - .scope('withArea') - .findOne(query) - .then(found => { - if ( !found ) throw new Error('Site not found'); - req.results = found; - req.site = req.results; // middleware expects this to exist - next(); - }) - .catch(next); - }) +router + .route('/:siteIdOrDomain') //(\\d+) + .all(auth.can('Site', 'view')) + .all(function (req, res, next) { + const siteIdOrDomain = req.params.siteIdOrDomain; + let query; + + if (isNaN(siteIdOrDomain)) { + query = { where: { domain: siteIdOrDomain } }; + } else { + query = { where: { id: parseInt(siteIdOrDomain) } }; + } -// view site -// --------- - .get(auth.can('Site', 'view')) - .get(auth.useReqUser) - .get(function(req, res, next) { - res.json(req.results); - }) + db.Site.scope('withArea') + .findOne(query) + .then((found) => { + if (!found) throw new Error('Site not found'); + req.results = found; + req.site = req.results; // middleware expects this to exist + next(); + }) + .catch(next); + }) -// update site -// ----------- - .put(auth.useReqUser) - .put(oauthClients.withAllForSite) - .put(function(req, res, next) { - const site = req.results; - if (!( site && site.can && site.can('update') )) return next( new Error('You cannot update this site') ); + // view site + // --------- + .get(auth.can('Site', 'view')) + .get(auth.useReqUser) + .get(function (req, res, next) { + res.json(req.results); + }) - req.results - .authorizeData(req.body, 'update') - .update(req.body) - .then(result => { - return checkHostStatus({id: result.id}); - }) - .then(() => { - next(); + // update site + // ----------- + .put(auth.useReqUser) + .put(oauthClients.withAllForSite) + .put(function (req, res, next) { + const site = req.results; + if (!(site && site.can && site.can('update'))) + return next(new Error('You cannot update this site')); + + req.results + .authorizeData(req.body, 'update') + .update(req.body) + .then((result) => { + return checkHostStatus({ id: result.id }); + }) + .then(() => { + next(); return null; - }) - .catch((err) => { - console.log('ERROR',err); - next(); + }) + .catch((err) => { + console.log('ERROR', err); + next(); return null; - }); - }) - -// update certain parts of config to the oauth client -// mainly styling settings are synched so in line with the CMS - .put(function (req, res, next) { + }); + }) + // update certain parts of config to the oauth client + // mainly styling settings are synched so in line with the CMS + .put(function (req, res, next) { // todo: gebruik de oauth-api service - const authServerUrl = config.authorization['auth-server-url']; - const updates = []; + const authServerUrl = config.authorization['auth-server-url']; + const updates = []; - req.siteOAuthClients.forEach((oauthClient, i) => { - const authUpdateUrl = authServerUrl + '/api/admin/client/' + oauthClient.id; - const configKeysToSync = ['users', 'styling', 'ideas']; + req.siteOAuthClients.forEach((oauthClient, i) => { + const authUpdateUrl = + authServerUrl + '/api/admin/client/' + oauthClient.id; + const configKeysToSync = ['users', 'styling', 'ideas']; // todo: gebruik de oauth-api service // todo: specifieker selecteren van sync velden (user.canCreateNewUsers) - // todo: ik denk dat dit in het model moet - - oauthClient.config = oauthClient.config ? oauthClient.config : {}; + // todo: ik denk dat dit in het model moet - configKeysToSync.forEach(field => { - oauthClient.config[field] = req.site.config[field]; - }); - oauthClient.config['users'] = { canCreateNewUsers: req.site.config.users.canCreateNewUsers } + oauthClient.config = oauthClient.config ? oauthClient.config : {}; + configKeysToSync.forEach((field) => { + oauthClient.config[field] = req.site.config[field]; + }); + oauthClient.config['users'] = { + canCreateNewUsers: req.site.config.users.canCreateNewUsers, + }; const apiCredentials = { - client_id: oauthClient.clientId, - client_secret: oauthClient.clientSecret, - } - - const options = { - method: 'post', - headers: { - 'Content-Type': 'application/json', - }, - mode: 'cors', - body: JSON.stringify(Object.assign(apiCredentials, oauthClient)) - } - - updates.push(fetch(authUpdateUrl, options)); - }); + client_id: oauthClient.clientId, + client_secret: oauthClient.clientSecret, + }; + + const options = { + method: 'post', + headers: { + 'Content-Type': 'application/json', + }, + mode: 'cors', + body: JSON.stringify(Object.assign(apiCredentials, oauthClient)), + }; + + updates.push(fetch(authUpdateUrl, options)); + }); - Promise.all(updates) - .then(() => { - next() - }) - .catch((e) => { - console.log('errr oauth', e); - next(e) - }); - }) -// call the site, to let the site know a refresh of the siteConfig is needed - .put(function (req, res, next) { - // when succesfull return site JSON - res.json(req.results); + Promise.all(updates) + .then(() => { + next(); + }) + .catch((e) => { + console.log('errr oauth', e); + next(e); + }); + }) + // call the site, to let the site know a refresh of the siteConfig is needed + .put(function (req, res, next) { + // when succesfull return site JSON + res.json(req.results); next(); - }) - .put(refreshSiteConfigMw) // after response; no need to wait for this - .put(function (req, res, next) { + }) + .put(refreshSiteConfigMw) // after response; no need to wait for this + .put(function (req, res, next) { // the end - }) + }) -// delete site -// --------- - .delete(auth.can('Site', 'delete')) - .delete(function(req, res, next) { - req.results - .destroy() - .then(() => { - res.json({ "site": "deleted" }); - }) - .catch(next); - }) + // delete site + // --------- + .delete(auth.can('Site', 'delete')) + .delete(function (req, res, next) { + req.results + .destroy() + .then(() => { + res.json({ site: 'deleted' }); + }) + .catch(next); + }); // anonymize all users // ------------------- -router.route('/:siteId(\\d+)/:willOrDo(will|do)-anonymize-all-users') - .put(auth.can('Site', 'anonymizeAllUsers')) - .put(function(req, res, next) { +router + .route('/:siteId(\\d+)/:willOrDo(will|do)-anonymize-all-users') + .put(auth.can('Site', 'anonymizeAllUsers')) + .put(function (req, res, next) { // the site - let where = { id: parseInt(req.params.siteId) }; - db.Site - .findOne({ where }) - .then(found => { - if ( !found ) throw new Error('Site not found'); - req.results = found; - req.site = req.results; // middleware expects this to exist - next(); - }) - .catch(next); - }) + let where = { id: parseInt(req.params.siteId) }; + db.Site.findOne({ where }) + .then((found) => { + if (!found) throw new Error('Site not found'); + req.results = found; + req.site = req.results; // middleware expects this to exist + next(); + }) + .catch(next); + }) .put(async function (req, res, next) { try { - const result = await req.site.willAnonymizeAllUsers(); - req.results = result; + const result = await req.site.willAnonymizeAllUsers(); + req.results = result; if (req.params.willOrDo == 'do') { - result.message = 'Ok'; - - req.site.doAnonymizeAllUsers( - [...result.users], - [...result.externalUserIds], - req.query.useOauth - - ); - } + result.message = 'Ok'; + + req.site.doAnonymizeAllUsers( + [...result.users], + [...result.externalUserIds], + req.query.useOauth + ); + } next(); } catch (err) { return next(err); @@ -307,20 +319,21 @@ router.route('/:siteId(\\d+)/:willOrDo(will|do)-anonymize-all-users') }) .put(function (req, res, next) { // customized version of auth.useReqUser - delete req.results.externalUserIds - Object.keys(req.results).forEach(which => { - req.results[which] && req.results[which].forEach && req.results[which].forEach( result => { - if (typeof result == 'object') { - result.auth = result.auth || {}; - result.auth.user = req.user; - } - }); - }); + delete req.results.externalUserIds; + Object.keys(req.results).forEach((which) => { + req.results[which] && + req.results[which].forEach && + req.results[which].forEach((result) => { + if (typeof result == 'object') { + result.auth = result.auth || {}; + result.auth.user = req.user; + } + }); + }); return next(); }) .put(function (req, res, next) { res.json(req.results); - }) - + }); module.exports = router;