feat(webui): api key support

This commit is contained in:
Gauthier Roebroeck 2024-08-27 18:05:41 +08:00
parent 210c7b1e50
commit c1e1da6ffc
16 changed files with 547 additions and 54 deletions

View 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>

View File

@ -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},
)

View File

@ -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 => {
})

View 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>

View File

@ -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[]

View File

@ -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
},

View File

@ -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"
},

View File

@ -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})

View 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;
}
}

View 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)
}
}
}

View File

@ -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}`
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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,
}

View File

@ -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
},
},

View File

@ -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()