mirror of
https://github.com/gotson/komga.git
synced 2025-01-08 11:47:47 +08:00
feat(webui): api key support
This commit is contained in:
parent
210c7b1e50
commit
c1e1da6ffc
177
komga-webui/src/components/ApiKeyTable.vue
Normal file
177
komga-webui/src/components/ApiKeyTable.vue
Normal file
@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<div style="position: relative">
|
||||
<div v-if="apiKeys.length > 0">
|
||||
<v-list elevation="3"
|
||||
three-line
|
||||
>
|
||||
<div v-for="(apiKey, index) in apiKeys" :key="apiKey.id">
|
||||
<v-list-item>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ apiKey.comment }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{
|
||||
$t('account_settings.api_key.created_date', {
|
||||
date:
|
||||
new Intl.DateTimeFormat($i18n.locale, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short'
|
||||
}).format(apiKey.createdDate)
|
||||
})
|
||||
}}
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-subtitle v-if="apiKeyLastActivity[apiKey.id] !== undefined">
|
||||
{{
|
||||
$t('settings_user.latest_activity', {
|
||||
date:
|
||||
new Intl.DateTimeFormat($i18n.locale, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short'
|
||||
}).format(apiKeyLastActivity[apiKey.id])
|
||||
})
|
||||
}}
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-subtitle v-else>{{ $t('settings_user.no_recent_activity') }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
|
||||
<v-list-item-action>
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn icon @click="promptSyncPointDelete(apiKey)" v-on="on">
|
||||
<v-icon>mdi-book-refresh</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ $t('account_settings.api_key.force_kobo_sync') }}</span>
|
||||
</v-tooltip>
|
||||
</v-list-item-action>
|
||||
|
||||
<v-list-item-action>
|
||||
<v-btn icon @click="promptDeleteApiKey(apiKey)">
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider v-if="index !== apiKeys.length-1"/>
|
||||
</div>
|
||||
</v-list>
|
||||
|
||||
<v-btn fab absolute bottom color="primary"
|
||||
:right="!$vuetify.rtl"
|
||||
:left="$vuetify.rtl"
|
||||
class="mx-6"
|
||||
small
|
||||
@click="generateApiKey"
|
||||
>
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<v-container fluid class="pa-0">
|
||||
<v-row>
|
||||
<v-col>{{ $t('account_settings.api_key.no_keys') }}</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-btn color="primary" @click="generateApiKey">{{ $t('account_settings.api_key.generate_api_key') }}</v-btn>
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
|
||||
</v-container>
|
||||
</div>
|
||||
|
||||
<confirmation-dialog
|
||||
v-model="modalDeleteSyncPoints"
|
||||
:title="$t('dialog.force_kobo_sync.dialog_title')"
|
||||
:body-html="$t('dialog.force_kobo_sync.warning_html')"
|
||||
:button-confirm="$t('common.i_understand')"
|
||||
button-confirm-color="warning"
|
||||
@confirm="deleteSyncPoint"
|
||||
/>
|
||||
|
||||
<confirmation-dialog
|
||||
v-model="modalDeleteApiKey"
|
||||
:title="$t('dialog.delete_apikey.dialog_title')"
|
||||
:body-html="$t('dialog.delete_apikey.warning_html')"
|
||||
:confirm-text=" $t('dialog.delete_apikey.confirm_delete', {name: apiKeyToDelete.comment})"
|
||||
:button-confirm="$t('dialog.delete_apikey.button_confirm')"
|
||||
button-confirm-color="error"
|
||||
@confirm="deleteApiKey"
|
||||
/>
|
||||
|
||||
<api-key-add-dialog
|
||||
v-model="modalGenerateApiKey"
|
||||
@generate="loadApiKeys"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import {ApiKeyDto} from '@/types/komga-users'
|
||||
import {ERROR} from '@/types/events'
|
||||
import ConfirmationDialog from '@/components/dialogs/ConfirmationDialog.vue'
|
||||
import ApiKeyAddDialog from '@/components/dialogs/ApiKeyAddDialog.vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'ApiKeyTable',
|
||||
components: {ApiKeyAddDialog, ConfirmationDialog},
|
||||
data: () => {
|
||||
return {
|
||||
apiKeys: [] as ApiKeyDto[],
|
||||
apiKeyToDelete: {} as ApiKeyDto,
|
||||
apiKeySyncPointsToDelete: {} as ApiKeyDto,
|
||||
modalDeleteApiKey: false,
|
||||
modalDeleteSyncPoints: false,
|
||||
modalGenerateApiKey: false,
|
||||
apiKeyLastActivity: {} as any,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadApiKeys()
|
||||
},
|
||||
methods: {
|
||||
async loadApiKeys() {
|
||||
try {
|
||||
this.apiKeys = await this.$komgaUsers.getApiKeys()
|
||||
this.apiKeys.forEach((a: ApiKeyDto) => {
|
||||
this.$komgaUsers.getLatestAuthenticationActivityForUser(a.userId, a.id)
|
||||
.then(value => this.$set(this.apiKeyLastActivity, `${a.id}`, value.dateTime))
|
||||
.catch(e => {
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
this.$eventHub.$emit(ERROR, {message: e.message} as ErrorEvent)
|
||||
}
|
||||
},
|
||||
promptDeleteApiKey(apiKey: ApiKeyDto) {
|
||||
this.apiKeyToDelete = apiKey
|
||||
this.modalDeleteApiKey = true
|
||||
},
|
||||
promptSyncPointDelete(apiKey: ApiKeyDto) {
|
||||
this.apiKeySyncPointsToDelete = apiKey
|
||||
this.modalDeleteSyncPoints = true
|
||||
},
|
||||
async deleteSyncPoint() {
|
||||
try {
|
||||
await this.$komgaSyncPoints.deleteMySyncPointsByApiKey(this.apiKeySyncPointsToDelete.id)
|
||||
} catch (e) {
|
||||
this.$eventHub.$emit(ERROR, {message: e.message} as ErrorEvent)
|
||||
}
|
||||
},
|
||||
async deleteApiKey() {
|
||||
try {
|
||||
await this.$komgaUsers.deleteApiKey(this.apiKeyToDelete.id)
|
||||
await this.loadApiKeys()
|
||||
} catch (e) {
|
||||
this.$eventHub.$emit(ERROR, {message: e.message} as ErrorEvent)
|
||||
}
|
||||
},
|
||||
generateApiKey() {
|
||||
this.modalGenerateApiKey = true
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
@ -33,7 +33,7 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import {ERROR} from '@/types/events'
|
||||
import { AuthenticationActivityDto } from '@/types/komga-users'
|
||||
import {AuthenticationActivityDto} from '@/types/komga-users'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'AuthenticationActivityTable',
|
||||
@ -68,6 +68,7 @@ export default Vue.extend({
|
||||
{text: this.$t('authentication_activity.user_agent').toString(), value: 'userAgent'},
|
||||
{text: this.$t('authentication_activity.success').toString(), value: 'success'},
|
||||
{text: this.$t('authentication_activity.source').toString(), value: 'source'},
|
||||
{text: this.$t('authentication_activity.api_key').toString(), value: 'apiKeyComment'},
|
||||
{text: this.$t('authentication_activity.error').toString(), value: 'error'},
|
||||
{text: this.$t('authentication_activity.datetime').toString(), value: 'dateTime', groupable: false},
|
||||
)
|
||||
|
@ -161,7 +161,7 @@ export default Vue.extend({
|
||||
watch: {
|
||||
users(val) {
|
||||
val.forEach((u: UserDto) => {
|
||||
this.$komgaUsers.getLatestAuthenticationActivityForUser(u)
|
||||
this.$komgaUsers.getLatestAuthenticationActivityForUser(u.id)
|
||||
.then(value => this.$set(this.usersLastActivity, `${u.id}`, value.dateTime))
|
||||
.catch(e => {
|
||||
})
|
||||
|
167
komga-webui/src/components/dialogs/ApiKeyAddDialog.vue
Normal file
167
komga-webui/src/components/dialogs/ApiKeyAddDialog.vue
Normal file
@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<v-dialog v-model="modal"
|
||||
max-width="600"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>{{ $t('dialog.add_api_key.dialog_title') }}</v-card-title>
|
||||
<v-btn icon absolute top right @click="dialogClose">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-card-text>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<v-col>{{ $t('dialog.add_api_key.context') }}</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="!apiKey">
|
||||
<v-col>
|
||||
<v-text-field v-model.trim="form.comment"
|
||||
autofocus
|
||||
:label="$t('dialog.add_api_key.field_comment')"
|
||||
:hint="$t('dialog.add_api_key.field_comment_hint')"
|
||||
:error-messages="getErrors('comment')"
|
||||
@blur="$v.form.comment.$touch()"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="apiKey">
|
||||
<v-col>
|
||||
<v-alert type="info" class="body-2">{{ $t('dialog.add_api_key.info_copy') }}</v-alert>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="apiKey">
|
||||
<v-col>
|
||||
<v-icon color="success">mdi-check</v-icon>
|
||||
{{ apiKey.key }}
|
||||
|
||||
<v-tooltip top v-model="copied">
|
||||
<template v-slot:activator="on">
|
||||
<v-btn v-on="on"
|
||||
icon
|
||||
x-small
|
||||
class="align-content-end"
|
||||
@click="copyApiKeyToClipboard"
|
||||
>
|
||||
<v-icon v-if="copied" color="success">mdi-check</v-icon>
|
||||
<v-icon v-else>mdi-content-copy</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ $t('common.copied') }}</span>
|
||||
</v-tooltip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer/>
|
||||
<v-btn text @click="dialogClose">{{ $t('common.close') }}</v-btn>
|
||||
<v-btn color="primary" @click="generateApiKey" :disabled="apiKey">{{ $t('dialog.add_api_key.button_confirm') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {UserRoles} from '@/types/enum-users'
|
||||
import Vue from 'vue'
|
||||
import {required} from 'vuelidate/lib/validators'
|
||||
import {ERROR} from '@/types/events'
|
||||
import {ApiKeyDto, ApiKeyRequestDto} from '@/types/komga-users'
|
||||
|
||||
function validComment(value: string) {
|
||||
return !this.alreadyUsedComment.includes(value)
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'ApiKeyAddDialog',
|
||||
data: function () {
|
||||
return {
|
||||
UserRoles,
|
||||
modal: false,
|
||||
apiKey: undefined as ApiKeyDto,
|
||||
copied: false,
|
||||
alreadyUsedComment: [] as string[],
|
||||
form: {
|
||||
comment: '',
|
||||
},
|
||||
}
|
||||
},
|
||||
props: {
|
||||
value: Boolean,
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
this.modal = val
|
||||
if (val) {
|
||||
this.clear()
|
||||
}
|
||||
},
|
||||
modal(val) {
|
||||
!val && this.dialogClose()
|
||||
},
|
||||
},
|
||||
validations: {
|
||||
form: {
|
||||
comment: {required, validComment},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clear() {
|
||||
this.apiKey = undefined
|
||||
this.alreadyUsedComment = []
|
||||
this.form.comment = ''
|
||||
this.$v.$reset()
|
||||
},
|
||||
dialogClose() {
|
||||
this.$emit('input', false)
|
||||
},
|
||||
getErrors(fieldName: string): string[] {
|
||||
const errors = [] as string[]
|
||||
|
||||
const field = this.$v.form!![fieldName] as any
|
||||
if (field && field.$invalid && field.$dirty) {
|
||||
if (!field.validComment) errors.push(this.$t('error_codes.ERR_1034').toString())
|
||||
if (!field.required) errors.push(this.$t('common.required').toString())
|
||||
}
|
||||
return errors
|
||||
},
|
||||
validateInput(): ApiKeyRequestDto {
|
||||
this.$v.$touch()
|
||||
|
||||
if (!this.$v.$invalid) {
|
||||
return {
|
||||
comment: this.form.comment,
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
async generateApiKey() {
|
||||
const apiKeyRequest = this.validateInput()
|
||||
if (apiKeyRequest) {
|
||||
try {
|
||||
this.apiKey = await this.$komgaUsers.createApiKey(apiKeyRequest)
|
||||
this.$emit('generate')
|
||||
} catch (e) {
|
||||
if (e.message.includes('ERR_1034'))
|
||||
this.alreadyUsedComment.push(this.form.comment)
|
||||
else
|
||||
this.$eventHub.$emit(ERROR, {message: e.message} as ErrorEvent)
|
||||
}
|
||||
}
|
||||
},
|
||||
copyApiKeyToClipboard() {
|
||||
navigator.clipboard.writeText(this.apiKey.key)
|
||||
this.copied = true
|
||||
setTimeout(() => this.copied = false, 3000)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -50,24 +50,12 @@
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<span>{{ $t('dialog.add_user.label_roles') }}</span>
|
||||
<v-checkbox
|
||||
v-model="form.roles"
|
||||
:label="$t('dialog.add_user.field_role_administrator')"
|
||||
:value="UserRoles.ADMIN"
|
||||
hide-details
|
||||
/>
|
||||
<v-checkbox
|
||||
v-model="form.roles"
|
||||
:label="$t('dialog.add_user.field_role_page_streaming')"
|
||||
:value="UserRoles.PAGE_STREAMING"
|
||||
hide-details
|
||||
/>
|
||||
<v-checkbox
|
||||
v-model="form.roles"
|
||||
:label="$t('dialog.add_user.field_role_file_download')"
|
||||
:value="UserRoles.FILE_DOWNLOAD"
|
||||
hide-details
|
||||
<span>{{ $t('common.roles') }}</span>
|
||||
<v-checkbox v-for="role in userRoles" :key="role.value"
|
||||
v-model="form.roles"
|
||||
:label="role.text"
|
||||
:value="role.value"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@ -95,7 +83,6 @@ export default Vue.extend({
|
||||
name: 'UserAddDialog',
|
||||
data: function () {
|
||||
return {
|
||||
UserRoles,
|
||||
modalAddUser: true,
|
||||
showPassword: false,
|
||||
dialogTitle: this.$i18n.t('dialog.add_user.dialog_title').toString(),
|
||||
@ -118,6 +105,14 @@ export default Vue.extend({
|
||||
password: {required},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
userRoles(): any[] {
|
||||
return Object.keys(UserRoles).map(x => ({
|
||||
text: this.$t(`user_roles.${x}`),
|
||||
value: x,
|
||||
}))
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getErrors(fieldName: string): string[] {
|
||||
const errors = [] as string[]
|
||||
|
@ -15,23 +15,11 @@
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-checkbox
|
||||
v-model="roles"
|
||||
:label="$t('dialog.add_user.field_role_administrator')"
|
||||
:value="UserRoles.ADMIN"
|
||||
hide-details
|
||||
/>
|
||||
<v-checkbox
|
||||
v-model="roles"
|
||||
:label="$t('dialog.add_user.field_role_page_streaming')"
|
||||
:value="UserRoles.PAGE_STREAMING"
|
||||
hide-details
|
||||
/>
|
||||
<v-checkbox
|
||||
v-model="roles"
|
||||
:label="$t('dialog.add_user.field_role_file_download')"
|
||||
:value="UserRoles.FILE_DOWNLOAD"
|
||||
hide-details
|
||||
<v-checkbox v-for="role in userRoles" :key="role.value"
|
||||
v-model="roles"
|
||||
:label="role.text"
|
||||
:value="role.value"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@ -62,7 +50,6 @@ export default Vue.extend({
|
||||
name: 'UserEditDialog',
|
||||
data: () => {
|
||||
return {
|
||||
UserRoles,
|
||||
modal: false,
|
||||
roles: [] as string[],
|
||||
}
|
||||
@ -86,6 +73,12 @@ export default Vue.extend({
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
userRoles(): any[] {
|
||||
return Object.keys(UserRoles).map(x => ({
|
||||
text: this.$t(`user_roles.${x}`),
|
||||
value: x,
|
||||
}))
|
||||
},
|
||||
libraries(): LibraryDto[] {
|
||||
return this.$store.state.komgaLibraries.libraries
|
||||
},
|
||||
|
@ -19,6 +19,12 @@
|
||||
},
|
||||
"account_settings": {
|
||||
"account_settings": "Account Settings",
|
||||
"api_key": {
|
||||
"created_date": "Created date: {date}",
|
||||
"force_kobo_sync": "Force Kobo sync",
|
||||
"generate_api_key": "Generate API key",
|
||||
"no_keys": "No API Keys created yet"
|
||||
},
|
||||
"change_password": "change password"
|
||||
},
|
||||
"announcements": {
|
||||
@ -27,6 +33,7 @@
|
||||
"tab_title": "Announcements"
|
||||
},
|
||||
"authentication_activity": {
|
||||
"api_key": "API Key",
|
||||
"datetime": "Date Time",
|
||||
"email": "Email",
|
||||
"error": "Error",
|
||||
@ -203,6 +210,7 @@
|
||||
"choose_image": "Choose an image",
|
||||
"close": "Close",
|
||||
"collections": "Collections",
|
||||
"copied": "Copied!",
|
||||
"create": "Create",
|
||||
"delete": "Delete",
|
||||
"dimension": "w: {width}, h: {height}",
|
||||
@ -220,6 +228,7 @@
|
||||
"go_to_library": "Go to library",
|
||||
"go_to_readlist": "Go to read list",
|
||||
"go_to_series": "Go to series",
|
||||
"i_understand": "I understand",
|
||||
"library": "Library",
|
||||
"locale_name": "English",
|
||||
"locale_rtl": "false",
|
||||
@ -287,6 +296,14 @@
|
||||
"tab_title": "Data Import"
|
||||
},
|
||||
"dialog": {
|
||||
"add_api_key": {
|
||||
"button_confirm": "Generate",
|
||||
"context": "API Keys can be used to authenticate through the Kobo Sync protocol.",
|
||||
"dialog_title": "Generate new API key",
|
||||
"field_comment": "Comment",
|
||||
"field_comment_hint": "What's this API key for?",
|
||||
"info_copy": "Make sure to copy your API key now. You won't be able to see it again!"
|
||||
},
|
||||
"add_to_collection": {
|
||||
"button_create": "Create",
|
||||
"card_collection_subtitle": "No series | 1 series | {count} series",
|
||||
@ -309,11 +326,7 @@
|
||||
"dialog_title": "Add User",
|
||||
"field_email": "Email",
|
||||
"field_email_error": "Must be a valid email address",
|
||||
"field_password": "Password",
|
||||
"field_role_administrator": "Administrator",
|
||||
"field_role_file_download": "File Download",
|
||||
"field_role_page_streaming": "Page Streaming",
|
||||
"label_roles": "Roles"
|
||||
"field_password": "Password"
|
||||
},
|
||||
"analyze_library": {
|
||||
"body": "Analyzes all the media files in the library. The analysis captures information about the media. Depending on your library size, this may take a long time.",
|
||||
@ -324,6 +337,12 @@
|
||||
"filter": "Filter by book number, title, or release date",
|
||||
"title": "Select Book"
|
||||
},
|
||||
"delete_apikey": {
|
||||
"button_confirm": "Delete",
|
||||
"confirm_delete": "I understand, delete the API key \"{name}\"",
|
||||
"dialog_title": "Delete API key",
|
||||
"warning_html": "Any applications or scripts using this API key will no longer be able to access the Komga API. You cannot undo this action."
|
||||
},
|
||||
"delete_book": {
|
||||
"button_confirm": "Delete",
|
||||
"confirm_delete": "Yes, delete book \"{name}\" and its files",
|
||||
@ -542,6 +561,10 @@
|
||||
},
|
||||
"title": "Destination File Name"
|
||||
},
|
||||
"force_kobo_sync": {
|
||||
"dialog_title": "Force Kobo sync",
|
||||
"warning_html": "This will delete all sync history for this API key. Your Kobo will sync everything on the next sync."
|
||||
},
|
||||
"password_change": {
|
||||
"button_cancel": "Cancel",
|
||||
"button_confirm": "Change password",
|
||||
@ -765,7 +788,8 @@
|
||||
"ERR_1030": "ComicRack CBL has no Name element",
|
||||
"ERR_1031": "ComicRack CBL Book is missing series or number",
|
||||
"ERR_1032": "EPUB file has wrong media type",
|
||||
"ERR_1033": "Some entries are missing"
|
||||
"ERR_1033": "Some entries are missing",
|
||||
"ERR_1034": "An API key with that comment already exists"
|
||||
},
|
||||
"filter": {
|
||||
"age_rating": "age rating",
|
||||
@ -911,6 +935,7 @@
|
||||
},
|
||||
"label_delete_empty_collections": "Delete empty collections after scan",
|
||||
"label_delete_empty_readlists": "Delete empty read lists after scan",
|
||||
"label_kobo_proxy": "Proxy Kobo Sync requests to Kobo Store",
|
||||
"label_rememberme_duration": "Remember me duration (in days)",
|
||||
"label_server_context_path": "Base URL",
|
||||
"label_server_port": "Server Port",
|
||||
@ -962,10 +987,12 @@
|
||||
"user_roles": {
|
||||
"ADMIN": "Administrator",
|
||||
"FILE_DOWNLOAD": "File download",
|
||||
"PAGE_STREAMING": "Stream pages",
|
||||
"KOBO_SYNC": "Kobo Sync",
|
||||
"PAGE_STREAMING": "Page streaming",
|
||||
"USER": "User"
|
||||
},
|
||||
"users": {
|
||||
"api_keys": "API Keys",
|
||||
"authentication_activity": "Authentication Activity",
|
||||
"users": "Users"
|
||||
},
|
||||
|
@ -23,6 +23,7 @@ import komgaUsers from './plugins/komga-users.plugin'
|
||||
import komgaTransientBooks from './plugins/komga-transientbooks.plugin'
|
||||
import komgaSse from './plugins/komga-sse.plugin'
|
||||
import komgaTasks from './plugins/komga-tasks.plugin'
|
||||
import komgaSyncPoints from './plugins/komga-syncpoints.plugin'
|
||||
import komgaOauth2 from './plugins/komga-oauth2.plugin'
|
||||
import komgaLogin from './plugins/komga-login.plugin'
|
||||
import komgaPageHashes from './plugins/komga-pagehashes.plugin'
|
||||
@ -69,6 +70,7 @@ Vue.use(komgaLibraries, {store: store, http: Vue.prototype.$http})
|
||||
Vue.use(komgaSse, {eventHub: Vue.prototype.$eventHub, store: store})
|
||||
Vue.use(actuator, {http: Vue.prototype.$http})
|
||||
Vue.use(komgaTasks, {http: Vue.prototype.$http})
|
||||
Vue.use(komgaSyncPoints, {http: Vue.prototype.$http})
|
||||
Vue.use(komgaOauth2, {http: Vue.prototype.$http})
|
||||
Vue.use(komgaLogin, {http: Vue.prototype.$http})
|
||||
Vue.use(komgaPageHashes, {http: Vue.prototype.$http})
|
||||
|
17
komga-webui/src/plugins/komga-syncpoints.plugin.ts
Normal file
17
komga-webui/src/plugins/komga-syncpoints.plugin.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import {AxiosInstance} from 'axios'
|
||||
import _Vue from 'vue'
|
||||
import KomgaSyncPointsService from '@/services/komga-syncpoints.service'
|
||||
|
||||
export default {
|
||||
install(
|
||||
Vue: typeof _Vue,
|
||||
{http}: { http: AxiosInstance }) {
|
||||
Vue.prototype.$komgaSyncPoints = new KomgaSyncPointsService(http)
|
||||
},
|
||||
}
|
||||
|
||||
declare module 'vue/types/vue' {
|
||||
interface Vue {
|
||||
$komgaSyncPoints: KomgaSyncPointsService;
|
||||
}
|
||||
}
|
29
komga-webui/src/services/komga-syncpoints.service.ts
Normal file
29
komga-webui/src/services/komga-syncpoints.service.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import {AxiosInstance} from 'axios'
|
||||
|
||||
const qs = require('qs')
|
||||
|
||||
const API_SYNCPOINTS = '/api/v1/syncpoints'
|
||||
|
||||
export default class KomgaSyncPointsService {
|
||||
private http: AxiosInstance
|
||||
|
||||
constructor(http: AxiosInstance) {
|
||||
this.http = http
|
||||
}
|
||||
|
||||
async deleteMySyncPointsByApiKey(apiKeyId: string) {
|
||||
try {
|
||||
await this.http.delete(`${API_SYNCPOINTS}/me`, {
|
||||
params: {
|
||||
key_id: apiKeyId,
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to delete syncpoints for apikey '${apiKeyId}'`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
import {AxiosInstance} from 'axios'
|
||||
import {
|
||||
ApiKeyDto,
|
||||
ApiKeyRequestDto,
|
||||
AuthenticationActivityDto,
|
||||
PasswordUpdateDto,
|
||||
UserCreationDto,
|
||||
@ -162,11 +164,51 @@ export default class KomgaUsersService {
|
||||
}
|
||||
}
|
||||
|
||||
async getLatestAuthenticationActivityForUser(user: UserDto): Promise<AuthenticationActivityDto> {
|
||||
async getLatestAuthenticationActivityForUser(userId: string, apiKeyId?: string): Promise<AuthenticationActivityDto> {
|
||||
try {
|
||||
return (await this.http.get(`${API_USERS}/${user.id}/authentication-activity/latest`)).data
|
||||
const params = {} as any
|
||||
if (apiKeyId) {
|
||||
params.apikey_id = apiKeyId
|
||||
}
|
||||
return (await this.http.get(`${API_USERS}/${userId}/authentication-activity/latest`, {params: params})).data
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to retrieve latest authentication activity for user ${user.email}`
|
||||
let msg = `An error occurred while trying to retrieve latest authentication activity for user ${userId}`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async getApiKeys(): Promise<ApiKeyDto[]> {
|
||||
try {
|
||||
return (await this.http.get(`${API_USERS}/me/api-keys`)).data
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to retrieve api keys'
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async createApiKey(apiKeyRequest: ApiKeyRequestDto): Promise<ApiKeyDto> {
|
||||
try {
|
||||
return (await this.http.post(`${API_USERS}/me/api-keys`, apiKeyRequest)).data
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to create api key'
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async deleteApiKey(apiKeyId: string) {
|
||||
try {
|
||||
await this.http.delete(`${API_USERS}/me/api-keys/${apiKeyId}`)
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to delete api key ${apiKeyId}`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
export enum UserRoles {
|
||||
ADMIN = 'ADMIN',
|
||||
FILE_DOWNLOAD = 'FILE_DOWNLOAD',
|
||||
PAGE_STREAMING = 'PAGE_STREAMING'
|
||||
PAGE_STREAMING = 'PAGE_STREAMING',
|
||||
KOBO_SYNC = 'KOBO_SYNC'
|
||||
}
|
||||
|
||||
export enum AllowExclude {
|
||||
|
@ -6,6 +6,7 @@ export interface SettingsDto {
|
||||
taskPoolSize: number,
|
||||
serverPort: SettingMultiSource<number>,
|
||||
serverContextPath: SettingMultiSource<string>,
|
||||
koboProxy: boolean,
|
||||
}
|
||||
|
||||
export interface SettingMultiSource<T> {
|
||||
@ -23,6 +24,7 @@ export interface SettingsUpdateDto {
|
||||
taskPoolSize?: number,
|
||||
serverPort?: number,
|
||||
serverContextPath?: string,
|
||||
koboProxy?: boolean,
|
||||
}
|
||||
|
||||
export enum ThumbnailSizeDto {
|
||||
|
@ -40,6 +40,8 @@ export interface UserUpdateDto {
|
||||
export interface AuthenticationActivityDto {
|
||||
userId?: string,
|
||||
email?: string,
|
||||
apiKeyId?: string,
|
||||
apiKeyComment?: string,
|
||||
ip?: string,
|
||||
userAgent?: string,
|
||||
success: Boolean,
|
||||
@ -47,3 +49,16 @@ export interface AuthenticationActivityDto {
|
||||
dateTime: Date,
|
||||
source?: string,
|
||||
}
|
||||
|
||||
export interface ApiKeyDto {
|
||||
id: string,
|
||||
userId: string,
|
||||
key: string,
|
||||
comment: string,
|
||||
createdDate: Date,
|
||||
lastModifiedDate: Date
|
||||
}
|
||||
|
||||
export interface ApiKeyRequestDto {
|
||||
comment: string,
|
||||
}
|
||||
|
@ -29,7 +29,18 @@
|
||||
<v-col>
|
||||
<v-btn color="primary"
|
||||
@click.prevent="modalPasswordChange = true"
|
||||
>{{ $t('account_settings.change_password') }}</v-btn>
|
||||
>{{ $t('account_settings.change_password') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col class="text-h5">{{ $t('users.api_keys') }}</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="10" lg="8" xl="4">
|
||||
<api-key-table/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
@ -54,11 +65,12 @@
|
||||
import PasswordChangeDialog from '@/components/dialogs/PasswordChangeDialog.vue'
|
||||
import Vue from 'vue'
|
||||
import AuthenticationActivityTable from '@/components/AuthenticationActivityTable.vue'
|
||||
import { UserDto } from '@/types/komga-users'
|
||||
import {UserDto} from '@/types/komga-users'
|
||||
import ApiKeyTable from '@/components/ApiKeyTable.vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'AccountSettings',
|
||||
components: {AuthenticationActivityTable, PasswordChangeDialog },
|
||||
components: {ApiKeyTable, AuthenticationActivityTable, PasswordChangeDialog},
|
||||
data: () => {
|
||||
return {
|
||||
modalPasswordChange: false,
|
||||
@ -66,7 +78,7 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
me (): UserDto {
|
||||
me(): UserDto {
|
||||
return this.$store.state.komgaUsers.me
|
||||
},
|
||||
},
|
||||
|
@ -106,6 +106,13 @@
|
||||
</v-tooltip>
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<v-checkbox
|
||||
v-model="form.koboProxy"
|
||||
@change="$v.form.koboProxy.$touch()"
|
||||
:label="$t('server_settings.label_kobo_proxy')"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
@ -158,6 +165,7 @@ export default Vue.extend({
|
||||
taskPoolSize: 1,
|
||||
serverPort: 25600,
|
||||
serverContextPath: '',
|
||||
koboProxy: false,
|
||||
},
|
||||
existingSettings: {} as SettingsDto,
|
||||
dialogRegenerateThumbnails: false,
|
||||
@ -183,6 +191,7 @@ export default Vue.extend({
|
||||
serverContextPath: {
|
||||
contextPath,
|
||||
},
|
||||
koboProxy: {},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
@ -260,6 +269,10 @@ export default Vue.extend({
|
||||
// coerce empty string to null
|
||||
this.$_.merge(newSettings, {serverContextPath: this.form.serverContextPath || null})
|
||||
|
||||
if (this.$v.form?.koboProxy?.$dirty)
|
||||
this.$_.merge(newSettings, {koboProxy: this.form.koboProxy})
|
||||
|
||||
|
||||
await this.$komgaSettings.updateSettings(newSettings)
|
||||
await this.refreshSettings()
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user