From c009cc13927b0438943181cbeb8f47c317bc68f6 Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Fri, 3 Jul 2020 19:36:33 -0400 Subject: [PATCH] feat: new login experience (#2139) * feat: multiple auth instances * fix: auth setup + strategy initialization * feat: admin auth - add strategy * feat: redirect on login - group setting * feat: oauth2 generic - props definitions * feat: new login UI (wip) * feat: new login UI (wip) * feat: admin security login settings * feat: tabset editor indicators + print view improvements * fix: code styling --- client/.modernizrrc.js | 7 + client/components/admin.vue | 2 +- client/components/admin/admin-auth.vue | 531 +++++++++------- .../admin/admin-groups-edit-permissions.vue | 26 +- .../admin/admin-groups-edit-rules.vue | 2 +- client/components/admin/admin-groups-edit.vue | 138 +++- client/components/admin/admin-groups.vue | 4 +- client/components/admin/admin-security.vue | 143 ++++- .../components/admin/admin-users-create.vue | 12 +- client/components/admin/admin-users.vue | 14 +- client/components/common/notify.vue | 8 +- client/components/editor/editor-markdown.vue | 40 ++ client/components/editor/markdown/tabset.js | 16 + client/components/login.vue | 594 +++++++++++------- .../auth/auth-mutation-save-strategies.gql | 12 - .../admin/groups/groups-mutation-delete.gql | 12 - .../admin/groups/groups-mutation-update.gql | 12 - .../admin/groups/groups-query-single.gql | 25 - client/index-app.js | 2 + client/libs/modernizr/modernizr.js | 3 + client/static/img/splash/1.jpg | Bin 0 -> 616188 bytes client/static/img/splash/2.jpg | Bin 0 -> 524213 bytes client/store/site.js | 3 +- client/themes/default/components/page.vue | 23 +- client/themes/default/scss/app.scss | 4 + dev/webpack/webpack.dev.js | 7 +- package.json | 2 + server/app/data.yml | 6 + server/core/auth.js | 7 +- server/db/migrations-sqlite/2.5.1.js | 23 + server/db/migrations-sqlite/2.5.12.js | 8 + server/db/migrations/2.5.1.js | 23 + server/db/migrations/2.5.12.js | 8 + server/graph/resolvers/authentication.js | 66 +- server/graph/resolvers/group.js | 5 + server/graph/resolvers/page.js | 27 +- server/graph/resolvers/site.js | 15 +- server/graph/schemas/authentication.graphql | 26 +- server/graph/schemas/group.graphql | 2 + server/graph/schemas/site.graphql | 10 + server/models/authentication.js | 73 +-- server/models/groups.js | 2 + .../authentication/local/definition.yml | 2 +- .../authentication/oauth2/definition.yml | 53 +- server/setup.js | 14 +- yarn.lock | 63 +- 46 files changed, 1365 insertions(+), 710 deletions(-) create mode 100644 client/.modernizrrc.js create mode 100644 client/components/editor/markdown/tabset.js delete mode 100644 client/graph/admin/auth/auth-mutation-save-strategies.gql delete mode 100644 client/graph/admin/groups/groups-mutation-delete.gql delete mode 100644 client/graph/admin/groups/groups-mutation-update.gql delete mode 100644 client/graph/admin/groups/groups-query-single.gql create mode 100644 client/libs/modernizr/modernizr.js create mode 100644 client/static/img/splash/1.jpg create mode 100644 client/static/img/splash/2.jpg create mode 100644 server/db/migrations-sqlite/2.5.1.js create mode 100644 server/db/migrations-sqlite/2.5.12.js create mode 100644 server/db/migrations/2.5.1.js create mode 100644 server/db/migrations/2.5.12.js diff --git a/client/.modernizrrc.js b/client/.modernizrrc.js new file mode 100644 index 00000000..b3f48f1d --- /dev/null +++ b/client/.modernizrrc.js @@ -0,0 +1,7 @@ +module.exports = { + classPrefix: 'mdz-', + options: ['setClasses'], + 'feature-detects': [ + 'css/backdropfilter' + ] +} diff --git a/client/components/admin.vue b/client/components/admin.vue index 65f93558..97cea971 100644 --- a/client/components/admin.vue +++ b/client/components/admin.vue @@ -129,7 +129,7 @@ v-list-item-avatar(size='24', tile): v-icon mdi-heart-outline v-list-item-title {{ $t('admin:contribute.title') }} - v-content(:class='$vuetify.theme.dark ? "grey darken-5" : "grey lighten-5"') + v-main(:class='$vuetify.theme.dark ? "grey darken-5" : "grey lighten-5"') transition(name='admin-router') router-view diff --git a/client/components/admin/admin-auth.vue b/client/components/admin/admin-auth.vue index 695fc7c2..e4217cd6 100644 --- a/client/components/admin/admin-auth.vue +++ b/client/components/admin/admin-auth.vue @@ -18,195 +18,188 @@ v-flex(lg3, xs12) v-card.animated.fadeInUp - v-toolbar(flat, color='primary', dark, dense) - .subtitle-1 {{$t('admin:auth.strategies')}} + v-toolbar(flat, color='teal', dark, dense) + .subtitle-1 {{$t('admin:auth.activeStrategies')}} v-list(two-line, dense).py-0 - template(v-for='(str, idx) in strategies') - v-list-item(:key='str.key', @click='selectedStrategy = str.key', :disabled='!str.isAvailable') - v-list-item-avatar(size='24') - v-icon(color='grey', v-if='!str.isAvailable') mdi-minus-box-outline - v-icon(color='primary', v-else-if='str.isEnabled && str.key !== `local`', v-ripple, @click='str.isEnabled = false') mdi-checkbox-marked-outline - v-icon(color='primary', v-else-if='str.isEnabled && str.key === `local`') mdi-checkbox-marked-outline - v-icon(color='grey', v-else, v-ripple, @click='str.isEnabled = true') mdi-checkbox-blank-outline - v-list-item-content - v-list-item-title.body-2(:class='!str.isAvailable ? `grey--text` : (selectedStrategy === str.key ? `primary--text` : ``)') {{ str.title }} - v-list-item-subtitle: .caption(:class='!str.isAvailable ? `grey--text text--lighten-1` : (selectedStrategy === str.key ? `blue--text ` : ``)') {{ str.description }} - v-list-item-avatar(v-if='selectedStrategy === str.key', size='24') - v-icon.animated.fadeInLeft(color='primary', large) mdi-chevron-right - v-divider(v-if='idx < strategies.length - 1') - - v-card.mt-3.animated.fadeInUp.wait-p2s - v-toolbar(flat, color='primary', dark, dense) - .subtitle-1 {{$t('admin:auth.globalAdvSettings')}} - v-card-text - v-text-field.md2( - v-model='jwtAudience' - outlined - prepend-icon='mdi-account-group-outline' - :label='$t(`admin:auth.jwtAudience`)' - :hint='$t(`admin:auth.jwtAudienceHint`)' - persistent-hint - ) - v-text-field.mt-3.md2( - v-model='jwtExpiration' - outlined - prepend-icon='mdi-clock-outline' - :label='$t(`admin:auth.tokenExpiration`)' - :hint='$t(`admin:auth.tokenExpirationHint`)' - persistent-hint - ) - v-text-field.mt-3.md2( - v-model='jwtRenewablePeriod' - outlined - prepend-icon='mdi-update' - :label='$t(`admin:auth.tokenRenewalPeriod`)' - :hint='$t(`admin:auth.tokenRenewalPeriodHint`)' - persistent-hint - ) + draggable( + v-model='activeStrategies' + handle='.is-handle' + direction='vertical' + :store='order' + ) + transition-group + v-list-item( + v-for='(str, idx) in activeStrategies' + :key='str.key' + @click='selectedStrategy = str.key' + :class='selectedStrategy === str.key ? ($vuetify.theme.dark ? `grey darken-5` : `teal lighten-5`) : ``' + ) + v-list-item-avatar.is-handle(size='24') + v-icon(:color='selectedStrategy === str.key ? `teal` : `grey`') mdi-drag-horizontal + v-list-item-content + v-list-item-title.body-2(:class='selectedStrategy === str.key ? `teal--text` : ``') {{ str.displayName }} + v-list-item-subtitle: .caption(:class='selectedStrategy === str.key ? `teal--text ` : ``') {{ str.strategy.title }} + v-list-item-avatar(v-if='selectedStrategy === str.key', size='24') + v-icon.animated.fadeInLeft(color='teal', large) mdi-chevron-right + v-card-chin + v-menu(offset-y, bottom, min-width='250px', max-width='550px', max-height='50vh', style='flex: 1 1;', center) + template(v-slot:activator='{ on }') + v-btn(v-on='on', color='primary', depressed, block) + v-icon(left) mdi-plus + span {{$t('admin:auth.addStrategy')}} + v-list(dense) + template(v-for='(str, idx) of strategies') + v-list-item( + :key='str.key' + :disabled='str.isDisabled' + @click='addStrategy(str)' + ) + v-list-item-avatar(height='24', width='48', tile) + v-img(:src='str.logo', width='48px', height='24px', contain, :style='str.isDisabled ? `opacity: .25;` : ``') + v-list-item-content + v-list-item-title {{str.title}} + v-list-item-subtitle: .caption(:style='str.isDisabled ? `opacity: .4;` : ``') {{str.description}} + v-divider(v-if='idx < strategies.length - 1') v-flex(xs12, lg9) v-card.animated.fadeInUp.wait-p2s v-toolbar(color='primary', dense, flat, dark) - .subtitle-1 {{strategy.title}} + .subtitle-1 {{strategy.displayName}} #[em ({{strategy.strategy.title}})] v-spacer - v-switch( - dark - color='blue lighten-5' - label='Active' - v-model='strategy.isEnabled' - hide-details - inset - :disabled='strategy.key === `local`' - ) + v-btn(small, outlined, dark, color='white', :disabled='strategy.key === `local`', @click='deleteStrategy()') + v-icon(left) mdi-close + span {{$t('common:actions.delete')}} + v-card-info(color='blue') + div + span {{strategy.strategy.description}} + .caption: a(:href='strategy.strategy.website') {{strategy.strategy.website}} + v-spacer + .authlogo + img(:src='strategy.strategy.logo', :alt='strategy.strategy.title') v-card-text - v-form - .authlogo - img(:src='strategy.logo', :alt='strategy.title') - .body-2.pt-3 {{strategy.description}} - .body-2.pt-3.pb-5: a(:href='strategy.website') {{strategy.website}} - i18next.body-2(path='admin:auth.strategyState', tag='div', v-if='strategy.isEnabled') - v-chip(color='green', small, dark, label, place='state') {{$t('admin:auth.strategyStateActive')}} - span(v-if='selectedStrategy === `local`', place='locked') {{$t('admin:auth.strategyStateLocked')}} - span(v-else, place='locked', v-text='') - i18next.body-2(path='admin:auth.strategyState', tag='div', v-else) - v-chip(color='red', small, dark, label, place='state') {{$t('admin:auth.strategyStateInactive')}} + .overline.mb-5 {{$t('admin:auth.strategyConfiguration')}} + v-text-field.mb-3( + outlined + label='Display Name' + v-model='strategy.displayName' + prepend-icon='mdi-format-title' + hint='The title shown to the end user for this authentication strategy.' + persistent-hint + ) + template(v-for='cfg in strategy.config') + v-select.mb-3( + v-if='cfg.value.type === "string" && cfg.value.enum' + outlined + :items='cfg.value.enum' + :key='cfg.key' + :label='cfg.value.title' + v-model='cfg.value.value' + prepend-icon='mdi-cog-box' + :hint='cfg.value.hint ? cfg.value.hint : ""' + persistent-hint + :class='cfg.value.hint ? "mb-2" : ""' + :style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``' + ) + v-switch.mb-6( + v-else-if='cfg.value.type === "boolean"' + :key='cfg.key' + :label='cfg.value.title' + v-model='cfg.value.value' + color='primary' + prepend-icon='mdi-cog-box' + :hint='cfg.value.hint ? cfg.value.hint : ""' + persistent-hint + inset + ) + v-textarea.mb-3( + v-else-if='cfg.value.type === "string" && cfg.value.multiline' + outlined + :key='cfg.key' + :label='cfg.value.title' + v-model='cfg.value.value' + prepend-icon='mdi-cog-box' + :hint='cfg.value.hint ? cfg.value.hint : ""' + persistent-hint + :class='cfg.value.hint ? "mb-2" : ""' + ) + v-text-field.mb-3( + v-else + outlined + :key='cfg.key' + :label='cfg.value.title' + v-model='cfg.value.value' + prepend-icon='mdi-cog-box' + :hint='cfg.value.hint ? cfg.value.hint : ""' + persistent-hint + :class='cfg.value.hint ? "mb-2" : ""' + :style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``' + ) + v-divider.mt-3 + .overline.my-5 {{$t('admin:auth.registration')}} + .pr-3 + v-switch.ml-3( + v-model='strategy.selfRegistration' + :label='$t(`admin:auth.selfRegistration`)' + color='primary' + :hint='$t(`admin:auth.selfRegistrationHint`)' + persistent-hint + inset + ) + v-combobox.ml-3.mt-3( + :label='$t(`admin:auth.domainsWhitelist`)' + v-model='strategy.domainWhitelist' + prepend-icon='mdi-email-check-outline' + outlined + :disabled='!strategy.selfRegistration' + :hint='$t(`admin:auth.domainsWhitelistHint`)' + persistent-hint + small-chips + deletable-chips + clearable + multiple + chips + ) + v-autocomplete.mt-3.ml-3( + outlined + :disabled='!strategy.selfRegistration' + :items='groups' + item-text='name' + item-value='id' + :label='$t(`admin:auth.autoEnrollGroups`)' + v-model='strategy.autoEnrollGroups' + prepend-icon='mdi-account-group' + :hint='$t(`admin:auth.autoEnrollGroupsHint`)' + small-chips + persistent-hint + deletable-chips + clearable + multiple + chips + ) + template(v-if='strategy.useForm') v-divider.mt-3 - .overline.my-5 {{$t('admin:auth.strategyConfiguration')}} - .body-2.ml-3(v-if='!strategy.config || strategy.config.length < 1'): em {{$t('admin:auth.strategyNoConfiguration')}} - template(v-else, v-for='cfg in strategy.config') - v-select.mb-3( - v-if='cfg.value.type === "string" && cfg.value.enum' - outlined - :items='cfg.value.enum' - :key='cfg.key' - :label='cfg.value.title' - v-model='cfg.value.value' - prepend-icon='mdi-cog-box' - :hint='cfg.value.hint ? cfg.value.hint : ""' - persistent-hint - :class='cfg.value.hint ? "mb-2" : ""' - :style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``' - ) - v-switch.mb-6( - v-else-if='cfg.value.type === "boolean"' - :key='cfg.key' - :label='cfg.value.title' - v-model='cfg.value.value' - color='primary' - prepend-icon='mdi-cog-box' - :hint='cfg.value.hint ? cfg.value.hint : ""' - persistent-hint - inset - ) - v-textarea.mb-3( - v-else-if='cfg.value.type === "string" && cfg.value.multiline' - outlined - :key='cfg.key' - :label='cfg.value.title' - v-model='cfg.value.value' - prepend-icon='mdi-cog-box' - :hint='cfg.value.hint ? cfg.value.hint : ""' - persistent-hint - :class='cfg.value.hint ? "mb-2" : ""' - ) - v-text-field.mb-3( - v-else - outlined - :key='cfg.key' - :label='cfg.value.title' - v-model='cfg.value.value' - prepend-icon='mdi-cog-box' - :hint='cfg.value.hint ? cfg.value.hint : ""' - persistent-hint - :class='cfg.value.hint ? "mb-2" : ""' - :style='cfg.value.maxWidth > 0 ? `max-width:` + cfg.value.maxWidth + `px;` : ``' - ) - v-divider.mt-3 - .overline.my-5 {{$t('admin:auth.registration')}} - .pr-3 - v-switch.ml-3( - v-model='strategy.selfRegistration' - :label='$t(`admin:auth.selfRegistration`)' - color='primary' - :hint='$t(`admin:auth.selfRegistrationHint`)' - persistent-hint - inset - ) - v-combobox.ml-3.mt-3( - :label='$t(`admin:auth.domainsWhitelist`)' - v-model='strategy.domainWhitelist' - prepend-icon='mdi-email-check-outline' - outlined - :disabled='!strategy.selfRegistration' - :hint='$t(`admin:auth.domainsWhitelistHint`)' - persistent-hint - small-chips - deletable-chips - clearable - multiple - chips - ) - v-autocomplete.mt-3.ml-3( - outlined - :disabled='!strategy.selfRegistration' - :items='groups' - item-text='name' - item-value='id' - :label='$t(`admin:auth.autoEnrollGroups`)' - v-model='strategy.autoEnrollGroups' - prepend-icon='mdi-account-group' - :hint='$t(`admin:auth.autoEnrollGroupsHint`)' - small-chips - persistent-hint - deletable-chips - clearable - multiple - chips - ) - template(v-if='strategy.useForm') - v-divider.mt-3 - .d-flex.my-5.align-center - .overline {{$t('admin:auth.security')}} - v-chip.ml-3.grey--text(outlined, small, label) Coming soon - v-switch.ml-3( - v-if='strategy.key === `local`' - :disabled='!strategy.selfRegistration || true' - v-model='strategy.recaptcha' - label='Use reCAPTCHA by Google' - color='primary' - hint='Protects against spam robots and malicious registrations.' - persistent-hint - inset - ) - v-switch.ml-3( - v-model='strategy.recaptcha' - :disabled='true' - :label='$t(`admin:auth.force2fa`)' - color='primary' - :hint='$t(`admin:auth.force2faHint`)' - persistent-hint - inset - ) + .d-flex.my-5.align-center + .overline {{$t('admin:auth.security')}} + v-chip.ml-3.grey--text(outlined, small, label) Coming soon + v-switch.ml-3( + v-if='strategy.key === `local`' + :disabled='!strategy.selfRegistration || true' + v-model='strategy.recaptcha' + label='Use reCAPTCHA by Google' + color='primary' + hint='Protects against spam robots and malicious registrations.' + persistent-hint + inset + ) + v-switch.ml-3( + v-model='strategy.recaptcha' + :disabled='true' + :label='$t(`admin:auth.force2fa`)' + color='primary' + :hint='$t(`admin:auth.force2faHint`)' + persistent-hint + inset + ) v-card.mt-4.wiki-form.animated.fadeInUp.wait-p4s(v-if='selectedStrategy !== `local`') v-toolbar(color='primary', dense, flat, dark) @@ -236,13 +229,18 @@