From 4643336e9df512e1146043092f30dd7d0e7b3526 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Sun, 1 Jul 2018 19:50:42 -0400 Subject: [PATCH] feat: auth + storage config improvements --- .vscode/extensions.json | 1 - client/components/admin/admin-auth.vue | 39 +++++++++++----- client/components/admin/admin-storage.vue | 39 +++++++++++----- client/components/admin/admin-utilities.vue | 18 +++++++- .../auth/auth-mutation-save-strategies.gql | 6 +-- .../storage/storage-mutation-save-targets.gql | 6 +-- package.json | 42 +++++++++--------- server/db/models/authentication.js | 19 +++++++- server/db/models/storage.js | 19 +++++++- server/graph/resolvers/authentication.js | 8 ++-- server/graph/resolvers/storage.js | 12 ++--- server/helpers/common.js | 17 +++++++ server/modules/authentication/auth0.js | 6 ++- server/modules/authentication/azure.js | 13 +++++- server/modules/authentication/discord.js | 5 ++- server/modules/authentication/dropbox.js | 5 ++- server/modules/authentication/facebook.js | 5 ++- server/modules/authentication/github.js | 5 ++- server/modules/authentication/google.js | 5 ++- server/modules/authentication/ldap.js | 25 ++++++++++- server/modules/authentication/local.js | 2 +- server/modules/authentication/microsoft.js | 5 ++- server/modules/authentication/oauth2.js | 7 ++- server/modules/authentication/slack.js | 5 ++- server/modules/authentication/twitch.js | 5 ++- server/modules/storage/azure.js | 6 ++- server/modules/storage/digitalocean.js | 10 ++++- server/modules/storage/disk.js | 4 +- server/modules/storage/dropbox.js | 5 ++- server/modules/storage/gdrive.js | 5 ++- server/modules/storage/git.js | 20 ++++++++- server/modules/storage/onedrive.js | 5 ++- server/modules/storage/s3.js | 7 ++- server/modules/storage/scp.js | 14 +++++- yarn.lock | Bin 462257 -> 470577 bytes 35 files changed, 309 insertions(+), 86 deletions(-) create mode 100644 server/helpers/common.js diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 87e9d93b..adb5f8c6 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -4,7 +4,6 @@ "dbaeumer.vscode-eslint", "christian-kohler.path-intellisense", "mrmlnc.vscode-puglint", - "robinbentley.sass-indented", "octref.vetur" ] } diff --git a/client/components/admin/admin-auth.vue b/client/components/admin/admin-auth.vue index f740dd8b..c269eb1a 100644 --- a/client/components/admin/admin-auth.vue +++ b/client/components/admin/admin-auth.vue @@ -12,7 +12,7 @@ .body-2.grey--text.text--darken-1 Select which authentication strategies to enable: .caption.grey--text.pb-2 Some strategies require additional configuration in their dedicated tab (when selected). v-form - v-checkbox( + v-checkbox.my-1( v-for='strategy in strategies' v-model='strategy.isEnabled' :key='strategy.key' @@ -27,14 +27,30 @@ v-form v-subheader.pl-0 Strategy Configuration .body-1.ml-3(v-if='!strategy.config || strategy.config.length < 1') This strategy has no configuration options you can modify. - v-text-field( - v-else - v-for='cfg in strategy.config' - :key='cfg.key' - :label='cfg.key' - v-model='cfg.value' - prepend-icon='settings_applications' + template(v-else, v-for='cfg in strategy.config') + v-select( + v-if='cfg.value.type === "string" && cfg.value.enum' + :items='cfg.value.enum' + :key='cfg.key' + :label='cfg.key | startCase' + v-model='cfg.value.value' + prepend-icon='settings_applications' ) + v-switch( + v-else-if='cfg.value.type === "boolean"' + :key='cfg.key' + :label='cfg.key | startCase' + v-model='cfg.value.value' + color='primary' + prepend-icon='settings_applications' + ) + v-text-field( + v-else + :key='cfg.key' + :label='cfg.key | startCase' + v-model='cfg.value.value' + prepend-icon='settings_applications' + ) v-divider v-subheader.pl-0 Registration .pr-3 @@ -90,6 +106,9 @@ import strategiesQuery from 'gql/admin/auth/auth-query-strategies.gql' import strategiesSaveMutation from 'gql/admin/auth/auth-mutation-save-strategies.gql' export default { + filters: { + startCase(val) { return _.startCase(val) } + }, data() { return { groups: [], @@ -122,7 +141,7 @@ export default { 'selfRegistration', 'domainWhitelist', 'autoEnrollGroups' - ])) + ])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: cfg.value.value}))})) } }) this.$store.commit('showNotification', { @@ -137,7 +156,7 @@ export default { strategies: { query: strategiesQuery, fetchPolicy: 'network-only', - update: (data) => _.cloneDeep(data.authentication.strategies), + update: (data) => _.cloneDeep(data.authentication.strategies).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.parse(cfg.value)}))})), watchLoading (isLoading) { this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-auth-refresh') } diff --git a/client/components/admin/admin-storage.vue b/client/components/admin/admin-storage.vue index 211eed9e..5d9c51b4 100644 --- a/client/components/admin/admin-storage.vue +++ b/client/components/admin/admin-storage.vue @@ -12,7 +12,7 @@ .body-2.grey--text.text--darken-1 Select which storage targets to enable: .caption.grey--text.pb-2 Some storage targets require additional configuration in their dedicated tab (when selected). v-form - v-checkbox( + v-checkbox.my-1( v-for='tgt in targets' v-model='tgt.isEnabled' :key='tgt.key' @@ -27,14 +27,30 @@ v-form v-subheader.pl-0 Target Configuration .body-1.ml-3(v-if='!tgt.config || tgt.config.length < 1') This storage target has no configuration options you can modify. - v-text-field( - v-else - v-for='cfg in tgt.config' - :key='cfg.key' - :label='cfg.key' - v-model='cfg.value' - prepend-icon='settings_applications' + template(v-else, v-for='cfg in tgt.config') + v-select( + v-if='cfg.value.type === "string" && cfg.value.enum' + :items='cfg.value.enum' + :key='cfg.key' + :label='cfg.key | startCase' + v-model='cfg.value.value' + prepend-icon='settings_applications' ) + v-switch( + v-else-if='cfg.value.type === "boolean"' + :key='cfg.key' + :label='cfg.key | startCase' + v-model='cfg.value.value' + color='primary' + prepend-icon='settings_applications' + ) + v-text-field( + v-else + :key='cfg.key' + :label='cfg.key | startCase' + v-model='cfg.value.value' + prepend-icon='settings_applications' + ) v-divider v-subheader.pl-0 Sync Direction .body-1.ml-3 Choose how content synchronization is handled for this storage target. @@ -80,6 +96,9 @@ import targetsQuery from 'gql/admin/storage/storage-query-targets.gql' import targetsSaveMutation from 'gql/admin/storage/storage-mutation-save-targets.gql' export default { + filters: { + startCase(val) { return _.startCase(val) } + }, data() { return { targets: [] @@ -109,7 +128,7 @@ export default { 'key', 'config', 'mode' - ])) + ])).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: cfg.value.value}))})) } }) this.$store.commit('showNotification', { @@ -124,7 +143,7 @@ export default { targets: { query: targetsQuery, fetchPolicy: 'network-only', - update: (data) => _.cloneDeep(data.storage.targets), + update: (data) => _.cloneDeep(data.storage.targets).map(str => ({...str, config: str.config.map(cfg => ({...cfg, value: JSON.parse(cfg.value)}))})), watchLoading (isLoading) { this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-storage-refresh') } diff --git a/client/components/admin/admin-utilities.vue b/client/components/admin/admin-utilities.vue index dcaf17b0..4d2e5b0e 100644 --- a/client/components/admin/admin-utilities.vue +++ b/client/components/admin/admin-utilities.vue @@ -17,17 +17,31 @@ v-toolbar-title .subheading Authentication v-subheader Flush User Sessions - v-card-text.pt-0 + v-card-text.pt-0.pl-4 .body-1 This will cause all users to be logged out. You will need to log back in after the operation. v-btn(depressed).ml-0 v-icon(left, color='grey') build span Proceed + v-divider.my-0 v-subheader Reset Guest User - v-card-text.pt-0 + v-card-text.pt-0.pl-4 .body-1 This will reset the guest user to its default parameters and permissions. v-btn(depressed).ml-0 v-icon(left, color='grey') build span Proceed + v-card.mt-3 + v-toolbar(:color='$vuetify.dark ? "" : "grey darken-3"', dark, dense, flat) + v-toolbar-title + .subheading Modules + v-subheader Rescan Modules + v-card-text.pt-0.pl-4 + .body-1 Look for new modules on disk. Existing configurations will be merged. + v-btn(depressed).ml-0 + v-icon(left, color='grey') youtube_searched_for + span Authentication + v-btn(depressed).ml-0 + v-icon(left, color='grey') youtube_searched_for + span Storage v-flex(xs12, sm6) v-card v-toolbar(:color='$vuetify.dark ? "" : "grey darken-3"', dark, dense, flat) diff --git a/client/graph/admin/auth/auth-mutation-save-strategies.gql b/client/graph/admin/auth/auth-mutation-save-strategies.gql index b8f95250..b488a535 100644 --- a/client/graph/admin/auth/auth-mutation-save-strategies.gql +++ b/client/graph/admin/auth/auth-mutation-save-strategies.gql @@ -1,6 +1,6 @@ -mutation($targets: [StorageTargetInput]) { - storage { - updateTargets(targets: $targets) { +mutation($strategies: [AuthenticationStrategyInput]) { + authentication { + updateStrategies(strategies: $strategies) { responseResult { succeeded errorCode diff --git a/client/graph/admin/storage/storage-mutation-save-targets.gql b/client/graph/admin/storage/storage-mutation-save-targets.gql index b488a535..b8f95250 100644 --- a/client/graph/admin/storage/storage-mutation-save-targets.gql +++ b/client/graph/admin/storage/storage-mutation-save-targets.gql @@ -1,6 +1,6 @@ -mutation($strategies: [AuthenticationStrategyInput]) { - authentication { - updateStrategies(strategies: $strategies) { +mutation($targets: [StorageTargetInput]) { + storage { + updateTargets(targets: $targets) { responseResult { succeeded errorCode diff --git a/package.json b/package.json index c89c3ed9..2aa9ad13 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "node": ">=8.11" }, "dependencies": { - "apollo-server": "2.0.0-rc.2", + "apollo-server": "2.0.0-rc.5", "apollo-server-express": "2.0.0-rc.2", "auto-load": "3.0.0", "axios": "0.18.0", @@ -69,9 +69,9 @@ "getos": "3.1.0", "graphql": "0.13.2", "graphql-list-fields": "2.0.2", - "graphql-tools": "3.0.2", + "graphql-tools": "3.0.4", "i18next": "11.3.3", - "i18next-express-middleware": "1.1.1", + "i18next-express-middleware": "1.2.0", "i18next-localstorage-cache": "1.1.1", "i18next-node-fs-backend": "1.0.0", "image-size": "0.6.3", @@ -79,7 +79,7 @@ "js-yaml": "3.12.0", "jsonwebtoken": "8.3.0", "klaw": "2.1.1", - "knex": "0.14.6", + "knex": "0.15.0", "lodash": "4.17.10", "markdown-it": "8.4.1", "markdown-it-abbr": "1.0.4", @@ -99,9 +99,9 @@ "mime-types": "2.1.18", "moment": "2.22.2", "moment-timezone": "0.5.21", - "mongodb": "3.1.0-beta4", + "mongodb": "3.1.0", "mssql": "4.1.0", - "multer": "1.3.0", + "multer": "1.3.1", "mysql2": "1.5.3", "node-2fa": "1.1.2", "oauth2orize": "1.11.0", @@ -134,12 +134,12 @@ "scim-query-filter-parser": "1.1.0", "semver": "5.5.0", "serve-favicon": "2.5.0", - "sqlite3": "4.0.0", - "uuid": "3.2.1", + "sqlite3": "4.0.1", + "uuid": "3.3.2", "validator": "10.4.0", "validator-as-promised": "1.0.2", "winston": "3.0.0", - "yargs": "11.0.0" + "yargs": "12.0.1" }, "devDependencies": { "@panter/vue-i18next": "0.11.0", @@ -152,11 +152,11 @@ "apollo-link-error": "1.1.0", "apollo-link-http": "1.5.4", "apollo-link-persisted-queries": "0.2.1", - "autoprefixer": "8.6.3", + "autoprefixer": "8.6.4", "babel-cli": "6.26.0", "babel-core": "6.26.3", "babel-eslint": "8.2.5", - "babel-jest": "23.0.1", + "babel-jest": "23.2.0", "babel-loader": "7.1.4", "babel-plugin-graphql-tag": "1.6.0", "babel-plugin-lodash": "3.3.4", @@ -168,14 +168,14 @@ "cache-loader": "1.2.2", "chart.js": "2.7.2", "clean-webpack-plugin": "0.1.19", - "copy-webpack-plugin": "4.5.1", + "copy-webpack-plugin": "4.5.2", "css-loader": "0.28.11", "cssnano": "4.0.0-rc.2", "duplicate-package-checker-webpack-plugin": "3.0.0", - "eslint": "5.0.0", + "eslint": "5.0.1", "eslint-config-requarks": "1.0.7", "eslint-config-standard": "11.0.0", - "eslint-plugin-import": "2.12.0", + "eslint-plugin-import": "2.13.0", "eslint-plugin-node": "6.0.1", "eslint-plugin-promise": "3.8.0", "eslint-plugin-standard": "3.1.0", @@ -190,20 +190,20 @@ "html-webpack-pug-plugin": "0.3.0", "i18next-xhr-backend": "1.5.1", "ignore-loader": "0.1.2", - "jest": "23.1.0", + "jest": "23.2.0", "jest-junit": "5.1.0", "js-cookie": "2.2.0", "lodash-webpack-plugin": "0.11.5", - "mini-css-extract-plugin": "0.4.0", + "mini-css-extract-plugin": "0.4.1", "node-sass": "4.9.0", "offline-plugin": "5.0.5", - "optimize-css-assets-webpack-plugin": "4.0.2", + "optimize-css-assets-webpack-plugin": "4.0.3", "postcss-cssnext": "3.1.0", "postcss-flexbugs-fixes": "3.3.1", "postcss-flexibility": "2.0.0", "postcss-import": "11.1.0", "postcss-loader": "2.1.5", - "postcss-preset-env": "5.1.0", + "postcss-preset-env": "5.2.1", "postcss-selector-parser": "5.0.0-rc.3", "pug-lint": "2.5.0", "pug-loader": "2.4.0", @@ -220,7 +220,7 @@ "stylus-loader": "3.0.2", "twemoji-awesome": "1.0.6", "url-loader": "1.0.1", - "vee-validate": "2.1.0-beta.2", + "vee-validate": "2.1.0-beta.5", "velocity-animate": "1.5.1", "vue": "2.5.16", "vue-apollo": "3.0.0-beta.19", @@ -234,10 +234,10 @@ "vue-router": "3.0.1", "vue-simple-breakpoints": "1.0.3", "vue-template-compiler": "2.5.16", - "vuetify": "1.0.19", + "vuetify": "1.1.1", "vuex": "3.0.1", "vuex-persistedstate": "2.5.4", - "webpack": "4.12.0", + "webpack": "4.14.0", "webpack-bundle-analyzer": "2.13.1", "webpack-cli": "3.0.8", "webpack-dev-middleware": "3.1.3", diff --git a/server/db/models/authentication.js b/server/db/models/authentication.js index d9c360a8..8befded7 100644 --- a/server/db/models/authentication.js +++ b/server/db/models/authentication.js @@ -2,6 +2,7 @@ const Model = require('objection').Model const autoload = require('auto-load') const path = require('path') const _ = require('lodash') +const commonHelper = require('../../helpers/common') /* global WIKI */ @@ -51,8 +52,22 @@ module.exports = class Authentication extends Model { title: strategy.title, isEnabled: false, useForm: strategy.useForm, - config: _.reduce(strategy.props, (result, value, key) => { - _.set(result, value, '') + config: _.transform(strategy.props, (result, value, key) => { + if (_.isPlainObject(value)) { + let cfgValue = { + type: typeof value.type(), + value: !_.isNil(value.default) ? value.default : commonHelper.getTypeDefaultValue(value) + } + if (_.isArray(value.enum)) { + cfgValue.enum = value.enum + } + _.set(result, key, cfgValue) + } else { + _.set(result, key, { + type: typeof value(), + value: commonHelper.getTypeDefaultValue(value) + }) + } return result }, {}), selfRegistration: false, diff --git a/server/db/models/storage.js b/server/db/models/storage.js index 75ac0248..1a115d5b 100644 --- a/server/db/models/storage.js +++ b/server/db/models/storage.js @@ -2,6 +2,7 @@ const Model = require('objection').Model const autoload = require('auto-load') const path = require('path') const _ = require('lodash') +const commonHelper = require('../../helpers/common') /* global WIKI */ @@ -43,8 +44,22 @@ module.exports = class Storage extends Model { title: target.title, isEnabled: false, mode: 'push', - config: _.reduce(target.props, (result, value, key) => { - _.set(result, value, '') + config: _.transform(target.props, (result, value, key) => { + if (_.isPlainObject(value)) { + let cfgValue = { + type: typeof value.type(), + value: !_.isNil(value.default) ? value.default : commonHelper.getTypeDefaultValue(value) + } + if (_.isArray(value.enum)) { + cfgValue.enum = value.enum + } + _.set(result, key, cfgValue) + } else { + _.set(result, key, { + type: typeof value(), + value: commonHelper.getTypeDefaultValue(value) + }) + } return result }, {}) }) diff --git a/server/graph/resolvers/authentication.js b/server/graph/resolvers/authentication.js index 48ac4cf8..9a0c7d6c 100644 --- a/server/graph/resolvers/authentication.js +++ b/server/graph/resolvers/authentication.js @@ -19,9 +19,9 @@ module.exports = { let strategies = await WIKI.db.authentication.getStrategies() strategies = strategies.map(stg => ({ ...stg, - config: _.transform(stg.config, (res, value, key) => { - res.push({ key, value }) - }, []) + config: _.sortBy(_.transform(stg.config, (res, value, key) => { + res.push({ key, value: JSON.stringify(value) }) + }, []), 'key') })) if (args.filter) { strategies = graphHelper.filter(strategies, args.filter) } if (args.orderBy) { strategies = graphHelper.orderBy(strategies, args.orderBy) } @@ -57,7 +57,7 @@ module.exports = { await WIKI.db.authentication.query().patch({ isEnabled: str.isEnabled, config: _.reduce(str.config, (result, value, key) => { - _.set(result, value.key, value.value) + _.set(result, `${value.key}.value`, value.value) return result }, {}), selfRegistration: str.selfRegistration, diff --git a/server/graph/resolvers/storage.js b/server/graph/resolvers/storage.js index 2910d5ed..b4d89467 100644 --- a/server/graph/resolvers/storage.js +++ b/server/graph/resolvers/storage.js @@ -13,11 +13,11 @@ module.exports = { StorageQuery: { async targets(obj, args, context, info) { let targets = await WIKI.db.storage.getTargets() - targets = targets.map(stg => ({ - ...stg, - config: _.transform(stg.config, (res, value, key) => { - res.push({ key, value }) - }, []) + targets = targets.map(tgt => ({ + ...tgt, + config: _.sortBy(_.transform(tgt.config, (res, value, key) => { + res.push({ key, value: JSON.stringify(value) }) + }, []), 'key') })) if (args.filter) { targets = graphHelper.filter(targets, args.filter) } if (args.orderBy) { targets = graphHelper.orderBy(targets, args.orderBy) } @@ -32,7 +32,7 @@ module.exports = { isEnabled: tgt.isEnabled, mode: tgt.mode, config: _.reduce(tgt.config, (result, value, key) => { - _.set(result, value.key, value.value) + _.set(result, `${value.key}.value`, value.value) return result }, {}) }).where('key', tgt.key) diff --git a/server/helpers/common.js b/server/helpers/common.js new file mode 100644 index 00000000..5dbef225 --- /dev/null +++ b/server/helpers/common.js @@ -0,0 +1,17 @@ +const _ = require('lodash') + +module.exports = { + /** + * Get default value of type + * + * @param {any} Type Primitive Type + * @returns Default value + */ + getTypeDefaultValue (Type) { + if (_.isArray(Type)) { + return _.head(Type) + } else { + return new Type() + } + } +} diff --git a/server/modules/authentication/auth0.js b/server/modules/authentication/auth0.js index 58ec34ff..c1f6bd99 100644 --- a/server/modules/authentication/auth0.js +++ b/server/modules/authentication/auth0.js @@ -10,7 +10,11 @@ module.exports = { key: 'auth0', title: 'Auth0', useForm: false, - props: ['domain', 'clientId', 'clientSecret'], + props: { + domain: String, + clientId: String, + clientSecret: String + }, init (passport, conf) { passport.use('auth0', new Auth0Strategy({ diff --git a/server/modules/authentication/azure.js b/server/modules/authentication/azure.js index 6dae3b62..156d4def 100644 --- a/server/modules/authentication/azure.js +++ b/server/modules/authentication/azure.js @@ -10,7 +10,18 @@ module.exports = { key: 'azure', title: 'Azure Active Directory', useForm: false, - props: ['clientId', 'clientSecret', 'resource', 'tenant'], + props: { + clientId: String, + clientSecret: String, + resource: { + type: String, + default: '00000002-0000-0000-c000-000000000000' + }, + tenant: { + type: String, + default: 'YOUR_TENANT.onmicrosoft.com' + } + }, init (passport, conf) { const jwt = require('jsonwebtoken') passport.use('azure_ad_oauth2', diff --git a/server/modules/authentication/discord.js b/server/modules/authentication/discord.js index 1a5e2f5f..b9c3e51a 100644 --- a/server/modules/authentication/discord.js +++ b/server/modules/authentication/discord.js @@ -10,7 +10,10 @@ module.exports = { key: 'discord', title: 'Discord', useForm: false, - props: ['clientId', 'clientSecret'], + props: { + clientId: String, + clientSecret: String + }, init (passport, conf) { passport.use('discord', new DiscordStrategy({ diff --git a/server/modules/authentication/dropbox.js b/server/modules/authentication/dropbox.js index 750a82e0..07cc43dc 100644 --- a/server/modules/authentication/dropbox.js +++ b/server/modules/authentication/dropbox.js @@ -10,7 +10,10 @@ module.exports = { key: 'dropbox', title: 'Dropbox', useForm: false, - props: ['clientId', 'clientSecret'], + props: { + clientId: String, + clientSecret: String + }, init (passport, conf) { passport.use('dropbox', new DropboxStrategy({ diff --git a/server/modules/authentication/facebook.js b/server/modules/authentication/facebook.js index 54aa7628..f3818fb5 100644 --- a/server/modules/authentication/facebook.js +++ b/server/modules/authentication/facebook.js @@ -10,7 +10,10 @@ module.exports = { key: 'facebook', title: 'Facebook', useForm: false, - props: ['clientId', 'clientSecret'], + props: { + clientId: String, + clientSecret: String + }, init (passport, conf) { passport.use('facebook', new FacebookStrategy({ diff --git a/server/modules/authentication/github.js b/server/modules/authentication/github.js index da618805..9f140953 100644 --- a/server/modules/authentication/github.js +++ b/server/modules/authentication/github.js @@ -10,7 +10,10 @@ module.exports = { key: 'github', title: 'GitHub', useForm: false, - props: ['clientId', 'clientSecret'], + props: { + clientId: String, + clientSecret: String + }, init (passport, conf) { passport.use('github', new GitHubStrategy({ diff --git a/server/modules/authentication/google.js b/server/modules/authentication/google.js index 100bb038..bffc8b0e 100644 --- a/server/modules/authentication/google.js +++ b/server/modules/authentication/google.js @@ -10,7 +10,10 @@ module.exports = { key: 'google', title: 'Google', useForm: false, - props: ['clientId', 'clientSecret'], + props: { + clientId: String, + clientSecret: String + }, init (passport, conf) { passport.use('google', new GoogleStrategy({ diff --git a/server/modules/authentication/ldap.js b/server/modules/authentication/ldap.js index c924baad..d55c8ac8 100644 --- a/server/modules/authentication/ldap.js +++ b/server/modules/authentication/ldap.js @@ -11,7 +11,30 @@ module.exports = { key: 'ldap', title: 'LDAP / Active Directory', useForm: true, - props: ['url', 'bindDn', 'bindCredentials', 'searchBase', 'searchFilter', 'tlsEnabled', 'tlsCertPath'], + props: { + url: { + type: String, + default: 'ldap://serverhost:389' + }, + bindDn: { + type: String, + default: `cn='root'` + }, + bindCredentials: String, + searchBase: { + type: String, + default: 'o=users,o=example.com' + }, + searchFilter: { + type: String, + default: '(uid={{username}})' + }, + tlsEnabled: { + type: Boolean, + default: false + }, + tlsCertPath: String + }, init (passport, conf) { passport.use('ldapauth', new LdapStrategy({ diff --git a/server/modules/authentication/local.js b/server/modules/authentication/local.js index 752c67d4..ec21550c 100644 --- a/server/modules/authentication/local.js +++ b/server/modules/authentication/local.js @@ -10,7 +10,7 @@ module.exports = { key: 'local', title: 'Local', useForm: true, - props: [], + props: {}, init (passport, conf) { passport.use('local', new LocalStrategy({ diff --git a/server/modules/authentication/microsoft.js b/server/modules/authentication/microsoft.js index cfe23760..28e943f6 100644 --- a/server/modules/authentication/microsoft.js +++ b/server/modules/authentication/microsoft.js @@ -10,7 +10,10 @@ module.exports = { key: 'microsoft', title: 'Microsoft Account', useForm: false, - props: ['clientId', 'clientSecret'], + props: { + clientId: String, + clientSecret: String + }, init (passport, conf) { passport.use('microsoft', new WindowsLiveStrategy({ diff --git a/server/modules/authentication/oauth2.js b/server/modules/authentication/oauth2.js index e8ad97ff..cbc03d27 100644 --- a/server/modules/authentication/oauth2.js +++ b/server/modules/authentication/oauth2.js @@ -10,7 +10,12 @@ module.exports = { key: 'oauth2', title: 'OAuth2', useForm: false, - props: ['clientId', 'clientSecret', 'authorizationURL', 'tokenURL'], + props: { + clientId: String, + clientSecret: String, + authorizationURL: String, + tokenURL: String + }, init (passport, conf) { passport.use('oauth2', new OAuth2Strategy({ diff --git a/server/modules/authentication/slack.js b/server/modules/authentication/slack.js index 76291352..bc710c70 100644 --- a/server/modules/authentication/slack.js +++ b/server/modules/authentication/slack.js @@ -10,7 +10,10 @@ module.exports = { key: 'slack', title: 'Slack', useForm: false, - props: ['clientId', 'clientSecret'], + props: { + clientId: String, + clientSecret: String + }, init (passport, conf) { passport.use('slack', new SlackStrategy({ diff --git a/server/modules/authentication/twitch.js b/server/modules/authentication/twitch.js index 952b318b..da28eacc 100644 --- a/server/modules/authentication/twitch.js +++ b/server/modules/authentication/twitch.js @@ -10,7 +10,10 @@ module.exports = { key: 'twitch', title: 'Twitch', useForm: false, - props: ['clientId', 'clientSecret'], + props: { + clientId: String, + clientSecret: String + }, init (passport, conf) { passport.use('twitch', new TwitchStrategy({ diff --git a/server/modules/storage/azure.js b/server/modules/storage/azure.js index d4992a88..8f0612f6 100644 --- a/server/modules/storage/azure.js +++ b/server/modules/storage/azure.js @@ -1,7 +1,11 @@ module.exports = { key: 'azure', title: 'Azure Blob Storage', - props: [], + props: { + accountName: String, + accountKey: String, + container: String + }, activate() { }, diff --git a/server/modules/storage/digitalocean.js b/server/modules/storage/digitalocean.js index 4b013329..d1ce8ef8 100644 --- a/server/modules/storage/digitalocean.js +++ b/server/modules/storage/digitalocean.js @@ -1,7 +1,15 @@ module.exports = { key: 'digitalocean', title: 'DigialOcean Spaces', - props: ['accessKeyId', 'accessSecret', 'region', 'bucket'], + props: { + accessKeyId: String, + accessSecret: String, + region: { + type: String, + default: 'nyc3' + }, + bucket: String + }, activate() { }, diff --git a/server/modules/storage/disk.js b/server/modules/storage/disk.js index 88920d4f..028ff02a 100644 --- a/server/modules/storage/disk.js +++ b/server/modules/storage/disk.js @@ -1,7 +1,9 @@ module.exports = { key: 'disk', title: 'Local FS', - props: ['path'], + props: { + path: String + }, activate() { }, diff --git a/server/modules/storage/dropbox.js b/server/modules/storage/dropbox.js index 3fc5b9fb..38b892d5 100644 --- a/server/modules/storage/dropbox.js +++ b/server/modules/storage/dropbox.js @@ -1,7 +1,10 @@ module.exports = { key: 'dropbox', title: 'Dropbox', - props: [], + props: { + appKey: String, + appSecret: String + }, activate() { }, diff --git a/server/modules/storage/gdrive.js b/server/modules/storage/gdrive.js index 7d03828a..5d63cd3c 100644 --- a/server/modules/storage/gdrive.js +++ b/server/modules/storage/gdrive.js @@ -1,7 +1,10 @@ module.exports = { key: 'gdrive', title: 'Google Drive', - props: [], + props: { + clientId: String, + clientSecret: String + }, activate() { }, diff --git a/server/modules/storage/git.js b/server/modules/storage/git.js index 0735104b..8c4a9454 100644 --- a/server/modules/storage/git.js +++ b/server/modules/storage/git.js @@ -1,7 +1,25 @@ module.exports = { key: 'git', title: 'Git', - props: [], + props: { + authType: { + type: String, + default: 'ssh', + enum: ['basic', 'ssh'] + }, + repoUrl: String, + branch: { + type: String, + default: 'master' + }, + verifySSL: { + type: Boolean, + default: true + }, + sshPrivateKeyPath: String, + basicUsername: String, + basicPassword: String + }, activate() { }, diff --git a/server/modules/storage/onedrive.js b/server/modules/storage/onedrive.js index 38dd9f0c..888bd777 100644 --- a/server/modules/storage/onedrive.js +++ b/server/modules/storage/onedrive.js @@ -1,7 +1,10 @@ module.exports = { key: 'onedrive', title: 'OneDrive', - props: [], + props: { + clientId: String, + clientSecret: String + }, activate() { }, diff --git a/server/modules/storage/s3.js b/server/modules/storage/s3.js index a5c0cd64..16fbcf0d 100644 --- a/server/modules/storage/s3.js +++ b/server/modules/storage/s3.js @@ -1,7 +1,12 @@ module.exports = { key: 's3', title: 'Amazon S3', - props: [], + props: { + accessKeyId: String, + accessSecret: String, + region: String, + bucket: String + }, activate() { }, diff --git a/server/modules/storage/scp.js b/server/modules/storage/scp.js index 476a5f06..be637a62 100644 --- a/server/modules/storage/scp.js +++ b/server/modules/storage/scp.js @@ -1,7 +1,19 @@ module.exports = { key: 'scp', title: 'SCP (SSH)', - props: [], + props: { + host: String, + port: { + type: Number, + default: 22 + }, + username: String, + privateKeyPath: String, + basePath: { + type: String, + default: '~' + } + }, activate() { }, diff --git a/yarn.lock b/yarn.lock index 2b4157623eb2d55705a9893abe4271571e3b5615..9dc4681a8913bd9cdbc01e1c25bf3794962105fb 100644 GIT binary patch delta 9445 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 delta 5777 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