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 @@
+
+ div
+ v-dialog(v-model='isShown', max-width='650', persistent)
+ v-card
+ .dialog-header.is-short
+ v-icon.mr-3(color='white') mdi-plus
+ span {{$t('admin:api.newKeyTitle')}}
+ v-card-text.pt-5
+ v-text-field(
+ outlined
+ prepend-icon='mdi-format-title'
+ v-model='name'
+ :label='$t(`admin:api.newKeyName`)'
+ persistent-hint
+ ref='keyNameInput'
+ :hint='$t(`admin:api.newKeyNameHint`)'
+ counter='255'
+ )
+ v-select.mt-3(
+ :items='expirations'
+ outlined
+ prepend-icon='mdi-clock'
+ v-model='expiration'
+ :label='$t(`admin:api.newKeyExpiration`)'
+ :hint='$t(`admin:api.newKeyExpirationHint`)'
+ persistent-hint
+ )
+ v-divider.mt-4
+ v-subheader.pl-2: strong.indigo--text {{$t('admin:api.newKeyPermissionScopes')}}
+ v-list.pl-8(nav)
+ v-list-item-group(v-model='fullAccess')
+ v-list-item(
+ :value='true'
+ active-class='indigo--text'
+ )
+ template(v-slot:default='{ active, toggle }')
+ v-list-item-action
+ v-checkbox(
+ :input-value='active'
+ :true-value='true'
+ color='indigo'
+ @click='toggle'
+ )
+ v-list-item-content
+ v-list-item-title {{$t('admin:api.newKeyFullAccess')}}
+ v-divider.mt-3
+ v-subheader.caption.indigo--text {{$t('admin:api.newKeyGroupPermissions')}}
+ v-list-item
+ v-select(
+ :disabled='fullAccess'
+ :items='groups'
+ item-text='name'
+ item-value='id'
+ outlined
+ color='indigo'
+ v-model='group'
+ :label='$t(`admin:api.newKeyGroup`)'
+ :hint='$t(`admin:api.newKeyGroupHint`)'
+ persistent-hint
+ )
+ v-card-chin
+ v-spacer
+ v-btn(text, @click='isShown = false', :disabled='loading') {{$t('common:actions.cancel')}}
+ v-btn.px-3(depressed, color='primary', @click='generate', :loading='loading')
+ v-icon(left) mdi-chevron-right
+ span {{$t('common:actions.generate')}}
+
+ v-dialog(
+ v-model='isCopyKeyDialogShown'
+ max-width='750'
+ persistent
+ overlay-color='blue darken-5'
+ overlay-opacity='.9'
+ )
+ v-card
+ v-toolbar(dense, flat, color='primary', dark) {{$t('admin:api.newKeyTitle')}}
+ v-card-text.pt-5
+ .body-2.text-center
+ i18next(tag='span', path='admin:api.newKeyCopyWarn')
+ strong(place='bold') {{$t('admin:api.newKeyCopyWarnBold')}}
+ v-textarea.mt-3(
+ ref='keyContentsIpt'
+ filled
+ no-resize
+ readonly
+ v-model='key'
+ :rows='10'
+ hide-details
+ )
+ v-card-chin
+ v-spacer
+ v-btn.px-3(depressed, dark, color='primary', @click='isCopyKeyDialogShown = false') {{$t('common:actions.close')}}
+
+
+
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')}}