From c1e1da6ffcf820b8e0bbd02818899efbeea20dd4 Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Tue, 27 Aug 2024 18:05:41 +0800 Subject: [PATCH] feat(webui): api key support --- komga-webui/src/components/ApiKeyTable.vue | 177 ++++++++++++++++++ .../AuthenticationActivityTable.vue | 3 +- komga-webui/src/components/UsersList.vue | 2 +- .../components/dialogs/ApiKeyAddDialog.vue | 167 +++++++++++++++++ .../src/components/dialogs/UserAddDialog.vue | 33 ++-- .../src/components/dialogs/UserEditDialog.vue | 29 ++- komga-webui/src/locales/en.json | 41 +++- komga-webui/src/main.ts | 2 + .../src/plugins/komga-syncpoints.plugin.ts | 17 ++ .../src/services/komga-syncpoints.service.ts | 29 +++ .../src/services/komga-users.service.ts | 48 ++++- komga-webui/src/types/enum-users.ts | 3 +- komga-webui/src/types/komga-settings.ts | 2 + komga-webui/src/types/komga-users.ts | 15 ++ komga-webui/src/views/AccountSettings.vue | 20 +- komga-webui/src/views/ServerSettings.vue | 13 ++ 16 files changed, 547 insertions(+), 54 deletions(-) create mode 100644 komga-webui/src/components/ApiKeyTable.vue create mode 100644 komga-webui/src/components/dialogs/ApiKeyAddDialog.vue create mode 100644 komga-webui/src/plugins/komga-syncpoints.plugin.ts create mode 100644 komga-webui/src/services/komga-syncpoints.service.ts diff --git a/komga-webui/src/components/ApiKeyTable.vue b/komga-webui/src/components/ApiKeyTable.vue new file mode 100644 index 00000000..dbd93758 --- /dev/null +++ b/komga-webui/src/components/ApiKeyTable.vue @@ -0,0 +1,177 @@ + + + diff --git a/komga-webui/src/components/AuthenticationActivityTable.vue b/komga-webui/src/components/AuthenticationActivityTable.vue index 2c30cbd5..e744d7b3 100644 --- a/komga-webui/src/components/AuthenticationActivityTable.vue +++ b/komga-webui/src/components/AuthenticationActivityTable.vue @@ -33,7 +33,7 @@ + + diff --git a/komga-webui/src/components/dialogs/UserAddDialog.vue b/komga-webui/src/components/dialogs/UserAddDialog.vue index cd7a72c2..9951748b 100644 --- a/komga-webui/src/components/dialogs/UserAddDialog.vue +++ b/komga-webui/src/components/dialogs/UserAddDialog.vue @@ -50,24 +50,12 @@ - {{ $t('dialog.add_user.label_roles') }} - - - {{ $t('common.roles') }} + @@ -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[] diff --git a/komga-webui/src/components/dialogs/UserEditDialog.vue b/komga-webui/src/components/dialogs/UserEditDialog.vue index 8917acfd..ad6b3d1e 100644 --- a/komga-webui/src/components/dialogs/UserEditDialog.vue +++ b/komga-webui/src/components/dialogs/UserEditDialog.vue @@ -15,23 +15,11 @@ - - - @@ -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 }, diff --git a/komga-webui/src/locales/en.json b/komga-webui/src/locales/en.json index b02603a0..8ae4bc12 100644 --- a/komga-webui/src/locales/en.json +++ b/komga-webui/src/locales/en.json @@ -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" }, diff --git a/komga-webui/src/main.ts b/komga-webui/src/main.ts index 193229d4..38ac2b19 100644 --- a/komga-webui/src/main.ts +++ b/komga-webui/src/main.ts @@ -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}) diff --git a/komga-webui/src/plugins/komga-syncpoints.plugin.ts b/komga-webui/src/plugins/komga-syncpoints.plugin.ts new file mode 100644 index 00000000..b0ae251c --- /dev/null +++ b/komga-webui/src/plugins/komga-syncpoints.plugin.ts @@ -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; + } +} diff --git a/komga-webui/src/services/komga-syncpoints.service.ts b/komga-webui/src/services/komga-syncpoints.service.ts new file mode 100644 index 00000000..347865bc --- /dev/null +++ b/komga-webui/src/services/komga-syncpoints.service.ts @@ -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) + } + } +} diff --git a/komga-webui/src/services/komga-users.service.ts b/komga-webui/src/services/komga-users.service.ts index e94d6342..dab552ca 100644 --- a/komga-webui/src/services/komga-users.service.ts +++ b/komga-webui/src/services/komga-users.service.ts @@ -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 { + async getLatestAuthenticationActivityForUser(userId: string, apiKeyId?: string): Promise { 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 { + 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 { + 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}` } diff --git a/komga-webui/src/types/enum-users.ts b/komga-webui/src/types/enum-users.ts index 2ec4b645..b385da99 100644 --- a/komga-webui/src/types/enum-users.ts +++ b/komga-webui/src/types/enum-users.ts @@ -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 { diff --git a/komga-webui/src/types/komga-settings.ts b/komga-webui/src/types/komga-settings.ts index 0ef340cc..5879f374 100644 --- a/komga-webui/src/types/komga-settings.ts +++ b/komga-webui/src/types/komga-settings.ts @@ -6,6 +6,7 @@ export interface SettingsDto { taskPoolSize: number, serverPort: SettingMultiSource, serverContextPath: SettingMultiSource, + koboProxy: boolean, } export interface SettingMultiSource { @@ -23,6 +24,7 @@ export interface SettingsUpdateDto { taskPoolSize?: number, serverPort?: number, serverContextPath?: string, + koboProxy?: boolean, } export enum ThumbnailSizeDto { diff --git a/komga-webui/src/types/komga-users.ts b/komga-webui/src/types/komga-users.ts index acc528ba..6b4e38d7 100644 --- a/komga-webui/src/types/komga-users.ts +++ b/komga-webui/src/types/komga-users.ts @@ -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, +} diff --git a/komga-webui/src/views/AccountSettings.vue b/komga-webui/src/views/AccountSettings.vue index 01e13224..fd5a659c 100644 --- a/komga-webui/src/views/AccountSettings.vue +++ b/komga-webui/src/views/AccountSettings.vue @@ -29,7 +29,18 @@ {{ $t('account_settings.change_password') }} + >{{ $t('account_settings.change_password') }} + + + + + + {{ $t('users.api_keys') }} + + + + + @@ -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 }, }, diff --git a/komga-webui/src/views/ServerSettings.vue b/komga-webui/src/views/ServerSettings.vue index 181ab19f..0ab924e2 100644 --- a/komga-webui/src/views/ServerSettings.vue +++ b/komga-webui/src/views/ServerSettings.vue @@ -106,6 +106,13 @@ + + @@ -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()