Merge pull request #573 from jellyfin/api-keyz

feat(apikeys settings): add settings page for managing API keys
This commit is contained in:
Julien Machiels 2021-01-16 16:28:32 +01:00 committed by GitHub
commit 4b3f90250b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 251 additions and 17 deletions

View File

@ -95,3 +95,7 @@ body {
.theme--dark .v-menu__content .v-list {
background: map-get($material-dark, 'menus') !important;
}
.text-capitalize-first-letter::first-letter {
text-transform: uppercase;
}

View File

@ -0,0 +1,94 @@
<template>
<v-dialog
:value="addingNewKey"
:width="width"
@click:outside="() => (addingNewKey = false)"
>
<v-col class="pa-0 add-key-dialog">
<v-card>
<v-card-title>{{ $t('settings.apiKeys.addApiKey') }}</v-card-title>
<v-card-actions>
<v-form class="add-key-form" @submit.prevent="addApiKey">
<v-text-field
v-model="newKeyAppName"
outlined
:label="$t('settings.apiKeys.appName')"
/>
<v-btn color="primary" @click="addApiKey">
{{ $t('confirm') }}
</v-btn>
<v-btn @click="() => (addingNewKey = false)">
{{ $t('cancel') }}
</v-btn>
</v-form>
</v-card-actions>
</v-card>
</v-col>
</v-dialog>
</template>
<script lang="ts">
import Vue from 'vue';
import { mapActions } from 'vuex';
export default Vue.extend({
data() {
return {
addingNewKey: false,
newKeyAppName: ''
};
},
computed: {
width(): number | string {
switch (this.$vuetify.breakpoint.name) {
case 'xs':
return '100%';
case 'sm':
return 400;
case 'md':
return 500;
case 'lg':
case 'xl':
default:
return 600;
}
}
},
methods: {
...mapActions('snackbar', ['pushSnackbarMessage']),
async addApiKey(): Promise<void> {
try {
await this.$api.apiKey.createKey({
app: this.newKeyAppName
});
this.pushSnackbarMessage({
message: this.$t('settings.apiKeys.createKeySuccess'),
color: 'success'
});
this.newKeyAppName = '';
this.$emit('key-added');
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
this.pushSnackbarMessage({
message: this.$t('settings.apiKeys.createKeyFailure'),
color: 'error'
});
}
this.addingNewKey = false;
},
openDialog(): void {
this.addingNewKey = true;
}
}
});
</script>
<style lang="scss" scoped>
.add-key-form {
width: 100%;
}
</style>

View File

@ -79,8 +79,4 @@ export default Vue.extend({
.empty-picture {
height: 100%;
}
.text-capitalize-first-letter::first-letter {
text-transform: uppercase;
}
</style>

View File

@ -22,6 +22,7 @@
"ok": "Ok"
},
"byArtist": "By",
"confirm": "Confirm",
"cancel": "Cancel",
"castAndCrew": "Cast & Crew",
"changeServer": "Change server",
@ -161,16 +162,31 @@
"serverNotFound": "Server not found",
"serverVersion": "Server version",
"serverVersionTooLow": "Server version needs to be 10.7.0 or higher",
"settings": "Settings",
"settings": {
"settings": "Settings",
"apiKeys": {
"apiKeys": "API keys",
"description": "Add and revoke API keys for external access to your server",
"appName": "App name",
"accessToken": "Access token",
"dateCreated": "Date created",
"revokeSuccess": "Successfully revoked API key",
"revokeFailure": "Error revoking API key",
"revokeAll": "Revoke all API keys",
"revokeAllSuccess": "Successfully revoked all API keys",
"revokeAllFailure": "Error revoking all API keys",
"addNewKey": "Add new API key",
"createKeySuccess": "Successfully created a new API key",
"createKeyFailure": "Error creating a new API key",
"refreshKeysFailure": "Error refreshing API keys",
"addApiKey": "Add an API key"
}
},
"settingsSections": {
"account": {
"description": "Edit your user's information",
"name": "Account"
},
"apiKeys": {
"description": "Add and revoke API keys for external access to your server",
"name": "API keys"
},
"devices": {
"description": "See and manage the devices connected to your server",
"name": "Devices"

129
pages/settings/apikeys.vue Normal file
View File

@ -0,0 +1,129 @@
<template>
<settings-page page-title="settings.apiKeys.apiKeys">
<template #actions>
<v-btn color="primary" @click="() => $refs.addKeyDialog.openDialog()">
{{ $t('settings.apiKeys.addNewKey') }}
</v-btn>
<v-btn v-if="apiKeys.length" color="error" @click="revokeAllApiKeys">
{{ $t('settings.apiKeys.revokeAll') }}
</v-btn>
</template>
<template #content>
<v-col>
<v-data-table :headers="headers" :items="apiKeys" class="elevation-1">
<!-- eslint-disable-next-line vue/valid-v-slot -->
<template #item.DateCreated="{ item }">
<p class="text-capitalize-first-letter mb-0">
{{
$dateFns.formatRelative(
$dateFns.parseJSON(item.DateCreated),
new Date()
)
}}
</p>
</template>
</v-data-table>
</v-col>
<!-- Add API key dialog -->
<add-api-key ref="addKeyDialog" @key-added="refreshApiKeys" />
</template>
</settings-page>
</template>
<script lang="ts">
import Vue from 'vue';
import { mapActions } from 'vuex';
import { AuthenticationInfo } from '@jellyfin/client-axios';
interface TableHeaders {
text: string;
value: string;
}
export default Vue.extend({
async asyncData({ $api }) {
const apiKeys = (await $api.apiKey.getKeys()).data.Items;
return { apiKeys };
},
data() {
return {
apiKeys: [] as AuthenticationInfo[],
addingNewKey: false,
newKeyAppName: ''
};
},
computed: {
headers(): TableHeaders[] {
return [
{ text: this.$t('settings.apiKeys.appName'), value: 'AppName' },
{ text: this.$t('settings.apiKeys.accessToken'), value: 'AccessToken' },
{ text: this.$t('settings.apiKeys.dateCreated'), value: 'DateCreated' }
];
}
},
methods: {
...mapActions('snackbar', ['pushSnackbarMessage']),
async revokeApiKey(token: string): Promise<void> {
try {
await this.$api.apiKey.revokeKey({
key: token
});
this.apiKeys.filter((item) => token !== item.AccessToken);
this.pushSnackbarMessage({
message: this.$t('settings.apiKeys.revokeSuccess'),
color: 'success'
});
this.refreshApiKeys();
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
this.pushSnackbarMessage({
message: this.$t('settings.apiKeys.revokeFailure'),
color: 'error'
});
}
},
async revokeAllApiKeys(): Promise<void> {
try {
for (const key of this.apiKeys) {
await this.$api.apiKey.revokeKey({
key: key.AccessToken || ''
});
}
this.apiKeys = [];
this.pushSnackbarMessage({
message: this.$t('settings.apiKeys.revokeAllSuccess'),
color: 'success'
});
this.refreshApiKeys();
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
this.pushSnackbarMessage({
message: this.$t('settings.apiKeys.revokeAllFailure'),
color: 'error'
});
}
},
async refreshApiKeys(): Promise<void> {
try {
this.apiKeys = (await this.$api.apiKey.getKeys()).data.Items || [];
} catch (error) {
this.pushSnackbarMessage({
message: this.$t('settings.apiKeys.refreshKeysFailure'),
color: 'error'
});
}
}
}
});
</script>

View File

@ -188,8 +188,9 @@ export default Vue.extend({
},
{
icon: 'mdi-key-chain',
name: this.$t('settingsSections.apiKeys.name'),
description: this.$t('settingsSections.apiKeys.description')
name: this.$t('settings.apiKeys.apiKeys'),
description: this.$t('settings.apiKeys.description'),
link: '/settings/apikeys'
}
],
[

View File

@ -249,9 +249,3 @@ export default Vue.extend({
}
});
</script>
<style>
.text-capitalize-first-letter::first-letter {
text-transform: uppercase;
}
</style>