mirror of
https://github.com/jellyfin/jellyfin-vue.git
synced 2024-12-13 08:45:51 +00:00
Merge pull request #573 from jellyfin/api-keyz
feat(apikeys settings): add settings page for managing API keys
This commit is contained in:
commit
4b3f90250b
@ -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;
|
||||
}
|
||||
|
94
components/System/AddApiKey.vue
Normal file
94
components/System/AddApiKey.vue
Normal 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>
|
@ -79,8 +79,4 @@ export default Vue.extend({
|
||||
.empty-picture {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.text-capitalize-first-letter::first-letter {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
|
@ -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
129
pages/settings/apikeys.vue
Normal 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>
|
@ -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'
|
||||
}
|
||||
],
|
||||
[
|
||||
|
@ -249,9 +249,3 @@ export default Vue.extend({
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.text-capitalize-first-letter::first-letter {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
|
Loading…
Reference in New Issue
Block a user