From f72cf664eb6b9aac21d1d2c08a35849587c9258e Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Sat, 22 Feb 2020 17:38:06 -0500 Subject: [PATCH] feat: manage / create API keys (#1516) * fix: admin api UI update * feat: admin api - create dialog UI * feat: admin api - create + list keys * feat: admin api localization (wip) * feat: admin api localization * feat: admin api - toggle state * feat: process API keys + format gql request errors to json --- client/components/admin.vue | 4 +- client/components/admin/admin-api-create.vue | 236 ++++++++++++++ client/components/admin/admin-api.vue | 318 ++++++++++++------- package.json | 1 + server/app/data.yml | 2 + server/core/auth.js | 42 ++- server/db/migrations-sqlite/2.2.3.js | 14 + server/db/migrations/2.2.3.js | 20 ++ server/graph/resolvers/authentication.js | 64 ++++ server/graph/schemas/authentication.graphql | 35 ++ server/master.js | 22 +- server/models/apiKeys.js | 71 +++++ server/models/users.js | 1 - yarn.lock | 2 +- 14 files changed, 712 insertions(+), 120 deletions(-) create mode 100644 client/components/admin/admin-api-create.vue create mode 100644 server/db/migrations-sqlite/2.2.3.js create mode 100644 server/db/migrations/2.2.3.js create mode 100644 server/models/apiKeys.js diff --git a/client/components/admin.vue b/client/components/admin.vue index fbb09d90..4a8fee1a 100644 --- a/client/components/admin.vue +++ b/client/components/admin.vue @@ -83,8 +83,8 @@ template(v-if='hasPermission([`manage:system`, `manage:api`])') v-divider.my-2 v-subheader.pl-4 {{ $t('admin:nav.system') }} - v-list-item(to='/api', v-if='hasPermission([`manage:system`, `manage:api`])', disabled) - v-list-item-avatar(size='24', tile): v-icon(color='grey lighten-2') mdi-call-split + v-list-item(to='/api', v-if='hasPermission([`manage:system`, `manage:api`])') + v-list-item-avatar(size='24', tile): v-icon mdi-call-split v-list-item-title {{ $t('admin:api.title') }} v-list-item(to='/mail', color='primary', v-if='hasPermission(`manage:system`)') v-list-item-avatar(size='24', tile): v-icon mdi-email-multiple-outline diff --git a/client/components/admin/admin-api-create.vue b/client/components/admin/admin-api-create.vue new file mode 100644 index 00000000..c7218dff --- /dev/null +++ b/client/components/admin/admin-api-create.vue @@ -0,0 +1,236 @@ + + + diff --git a/client/components/admin/admin-api.vue b/client/components/admin/admin-api.vue index a48b05d8..552ab66f 100644 --- a/client/components/admin/admin-api.vue +++ b/client/components/admin/admin-api.vue @@ -3,128 +3,232 @@ v-layout(row, wrap) v-flex(xs12) .admin-header - img(src='/svg/icon-rest-api.svg', alt='API', style='width: 80px;') + img.animated.fadeInUp(src='/svg/icon-rest-api.svg', alt='API', style='width: 80px;') .admin-header-title - .headline.blue--text.text--darken-2 API Access - .subtitle-1.grey--text Manage keys to access the API #[v-chip(label, color='primary', small).white--text coming soon] + .headline.primary--text.animated.fadeInLeft {{$t('admin:api.title')}} + .subtitle-1.grey--text.animated.fadeInLeft {{$t('admin:api.subtitle')}} v-spacer - v-btn(outline, color='grey', large, @click='refresh', disabled) - v-icon refresh - v-btn(color='green', disabled, depressed, large, @click='globalSwitch') - v-icon(left) power_settings_new - | Enable API - v-btn(color='primary', depressed, large, @click='newKey', disabled) - v-icon(left) add - | New API Key - v-card.mt-3 - v-data-table( - v-model='selected' - :items='items', - :headers='headers', - :search='search', - :pagination.sync='pagination', - :rows-per-page-items='[15]' - select-all, - hide-actions, - disable-initial-sort - ) - template(slot='headers', slot-scope='props') - tr - th(width='50') - th.text-xs-right( - width='80' - :class='[`column sortable`, pagination.descending ? `desc` : `asc`, pagination.sortBy === `id` ? `active` : ``]' - @click='changeSort(`id`)' - ) - v-icon(small) arrow_upward - | ID - th.text-xs-left( - v-for='header in props.headers' - :key='header.text' - :width='header.width' - :class='[`column sortable`, pagination.descending ? `desc` : `asc`, header.value === pagination.sortBy ? `active` : ``]' - @click='changeSort(header.value)' - ) - | {{ header.text }} - v-icon(small) arrow_upward - template(slot='items', slot-scope='props') - tr(:active='props.selected') - td - v-checkbox(hide-details, :input-value='props.selected', color='blue darken-2', @click='props.selected = !props.selected') - td.text-xs-right {{ props.item.id }} - td {{ props.item.name }} - td {{ props.item.key }} - td {{ props.item.createdOn }} - td {{ props.item.updatedOn }} - td: v-btn(icon): v-icon.grey--text.text--darken-1 more_horiz - template(slot='no-data') - v-alert.mt-3(icon='info', :value='true', outline, color='info') No API keys have been generated yet. - .text-xs-center.py-2 - v-pagination(v-model='pagination.page', :length='pages') + template(v-if='enabled') + status-indicator.mr-3(positive, pulse) + .caption.green--text.animated.fadeInLeft {{$t('admin:api.enabled')}} + template(v-else) + status-indicator.mr-3(negative, pulse) + .caption.red--text.animated.fadeInLeft {{$t('admin:api.disabled')}} + v-spacer + v-btn.mr-3.animated.fadeInDown.wait-p2s(outlined, color='grey', large, @click='refresh') + v-icon mdi-refresh + v-btn.mr-3.animated.fadeInDown.wait-p1s(:color='enabled ? `red` : `green`', depressed, large, @click='globalSwitch', dark, :loading='isToggleLoading') + v-icon(left) mdi-power + span(v-if='!enabled') {{$t('admin:api.enableButton')}} + span(v-else) {{$t('admin:api.disableButton')}} + v-btn.animated.fadeInDown(color='primary', depressed, large, @click='newKey', dark) + v-icon(left) mdi-plus + span {{$t('admin:api.newKeyButton')}} + v-card.mt-3.animated.fadeInUp + v-simple-table(v-if='keys && keys.length > 0') + template(v-slot:default) + thead + tr.grey(:class='$vuetify.theme.dark ? `darken-4-d5` : `lighten-5`') + th {{$t('admin:api.headerName')}} + th {{$t('admin:api.headerKeyEnding')}} + th {{$t('admin:api.headerExpiration')}} + th {{$t('admin:api.headerCreated')}} + th {{$t('admin:api.headerLastUpdated')}} + th(width='100') {{$t('admin:api.headerRevoke')}} + tbody + tr(v-for='key of keys', :key='`key-` + key.id') + td + strong(:class='key.isRevoked ? `red--text` : ``') {{ key.name }} + em.caption.ml-1.red--text(v-if='key.isRevoked') (revoked) + td.caption {{ key.keyShort }} + td(:style='key.isRevoked ? `text-decoration: line-through;` : ``') {{ key.expiration | moment('LL') }} + td {{ key.createdAt | moment('calendar') }} + td {{ key.updatedAt | moment('calendar') }} + td: v-btn(icon, @click='revoke(key)', :disabled='key.isRevoked'): v-icon(color='error') mdi-cancel + v-card-text(v-else) + v-alert.mb-0(icon='mdi-information', :value='true', outlined, color='info') {{$t('admin:api.noKeyInfo')}} + + create-api-key(v-model='isCreateDialogShown', @refresh='refresh(false)') + + v-dialog(v-model='isRevokeConfirmDialogShown', max-width='500', persistent) + v-card + .dialog-header.is-red {{$t('admin:api.revokeConfirm')}} + v-card-text.pa-4 + i18next(tag='span', path='admin:api.revokeConfirmText') + strong(place='name') {{ current.name }} + v-card-actions + v-spacer + v-btn(text, @click='isRevokeConfirmDialogShown = false', :disabled='revokeLoading') {{$t('common:actions.cancel')}} + v-btn(color='red', dark, @click='revokeConfirm', :loading='revokeLoading') {{$t('admin:api.revoke')}}