Secure storage API keys (#181)
* Migrate api key to private.key * Optimize API Key setting
This commit is contained in:
69
src/components/SettingApiKey.vue
Normal file
69
src/components/SettingApiKey.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<InputText
|
||||
class="w-full"
|
||||
v-model="content"
|
||||
placeholder="Set New API Key"
|
||||
autocomplete="off"
|
||||
></InputText>
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div>
|
||||
<span v-show="showError" class="text-red-400">
|
||||
API Key Not Allow Empty
|
||||
</span>
|
||||
</div>
|
||||
<Button label="Save" autofocus @click="saveKeybinding"></Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDialog } from 'hooks/dialog'
|
||||
import { request } from 'hooks/request'
|
||||
import { useToast } from 'hooks/toast'
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { ref, toValue } from 'vue'
|
||||
|
||||
interface Props {
|
||||
keyField: string
|
||||
setter: (val: string) => void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { close } = useDialog()
|
||||
const { toast } = useToast()
|
||||
|
||||
const content = ref<string>()
|
||||
const showError = ref<boolean>(false)
|
||||
|
||||
const saveKeybinding = async () => {
|
||||
const value = toValue(content)
|
||||
if (!value) {
|
||||
showError.value = true
|
||||
return
|
||||
}
|
||||
|
||||
showError.value = false
|
||||
const key = toValue(props.keyField)
|
||||
|
||||
try {
|
||||
const encodeValue = value ? btoa(value) : null
|
||||
await request('/download/setting', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ key, value: encodeValue }),
|
||||
})
|
||||
const desString = value ? value.slice(0, 4) + '****' + value.slice(-4) : ''
|
||||
props.setter(desString)
|
||||
close()
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: error.message,
|
||||
life: 3000,
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,6 +1,9 @@
|
||||
import SettingApiKey from 'components/SettingApiKey.vue'
|
||||
import SettingCardSize from 'components/SettingCardSize.vue'
|
||||
import { request } from 'hooks/request'
|
||||
import { defineStore } from 'hooks/store'
|
||||
import { app } from 'scripts/comfyAPI'
|
||||
import { useToast } from 'hooks/toast'
|
||||
import { $el, app } from 'scripts/comfyAPI'
|
||||
import { computed, onMounted, onUnmounted, readonly, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -65,6 +68,7 @@ export const useConfig = defineStore('config', (store) => {
|
||||
},
|
||||
},
|
||||
flat: flatLayout,
|
||||
apiKeyInfo: ref<Record<string, string>>({}),
|
||||
}
|
||||
|
||||
watch(cardSizeFlag, (val) => {
|
||||
@@ -97,6 +101,84 @@ export const configSetting = {
|
||||
|
||||
function useAddConfigSettings(store: import('hooks/store').StoreProvider) {
|
||||
const { t } = useI18n()
|
||||
const { confirm } = useToast()
|
||||
|
||||
const iconButton = (opt: {
|
||||
icon: string
|
||||
onClick: () => void | Promise<void>
|
||||
}) => {
|
||||
return $el(
|
||||
'span.h-4.cursor-pointer',
|
||||
{ onclick: opt.onClick },
|
||||
$el(`i.${opt.icon.replace(/\s/g, '.')}`),
|
||||
)
|
||||
}
|
||||
|
||||
const setApiKey = async (key: string, setter: (val: string) => void) => {
|
||||
store.dialog.open({
|
||||
key: `setting.api_key.${key}`,
|
||||
title: t(`setting.api_key.${key}`),
|
||||
content: SettingApiKey,
|
||||
modal: true,
|
||||
defaultSize: {
|
||||
width: 500,
|
||||
height: 200,
|
||||
},
|
||||
contentProps: {
|
||||
keyField: key,
|
||||
setter: setter,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const removeApiKey = async (key: string) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
confirm.require({
|
||||
message: t('deleteAsk'),
|
||||
header: 'Danger',
|
||||
icon: 'pi pi-info-circle',
|
||||
accept: () => resolve(true),
|
||||
reject: reject,
|
||||
})
|
||||
})
|
||||
await request('/download/setting', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ key, value: null }),
|
||||
})
|
||||
}
|
||||
|
||||
const renderApiKey = (key: string) => {
|
||||
return () => {
|
||||
const apiKey = store.config.apiKeyInfo.value[key] || 'None'
|
||||
const apiKeyDisplayEl = $el('div.text-sm.text-gray-500.flex-1', {
|
||||
textContent: apiKey,
|
||||
})
|
||||
|
||||
const setter = (val: string) => {
|
||||
store.config.apiKeyInfo.value[key] = val
|
||||
apiKeyDisplayEl.textContent = val || 'None'
|
||||
}
|
||||
return $el('div.flex.gap-4', [
|
||||
apiKeyDisplayEl,
|
||||
iconButton({
|
||||
icon: 'pi pi-pencil text-blue-400',
|
||||
onClick: () => {
|
||||
setApiKey(key, setter)
|
||||
},
|
||||
}),
|
||||
iconButton({
|
||||
icon: 'pi pi-trash text-red-400',
|
||||
onClick: async () => {
|
||||
const value = store.config.apiKeyInfo.value[key]
|
||||
if (value) {
|
||||
await removeApiKey(key)
|
||||
setter('')
|
||||
}
|
||||
},
|
||||
}),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// API keys
|
||||
@@ -104,16 +186,16 @@ function useAddConfigSettings(store: import('hooks/store').StoreProvider) {
|
||||
id: 'ModelManager.APIKey.HuggingFace',
|
||||
category: [t('modelManager'), t('setting.apiKey'), 'HuggingFace'],
|
||||
name: 'HuggingFace API Key',
|
||||
type: 'text',
|
||||
defaultValue: undefined,
|
||||
type: renderApiKey('huggingface'),
|
||||
})
|
||||
|
||||
app.ui?.settings.addSetting({
|
||||
id: 'ModelManager.APIKey.Civitai',
|
||||
category: [t('modelManager'), t('setting.apiKey'), 'Civitai'],
|
||||
name: 'Civitai API Key',
|
||||
type: 'text',
|
||||
defaultValue: undefined,
|
||||
type: renderApiKey('civitai'),
|
||||
})
|
||||
|
||||
const defaultCardSize = store.config.defaultCardSizeMap
|
||||
|
||||
@@ -84,7 +84,16 @@ export const useDownload = defineStore('download', (store) => {
|
||||
})
|
||||
})
|
||||
|
||||
// Initial download settings
|
||||
// Migrate API keys from user settings to private key
|
||||
const init = async () => {
|
||||
const res = await request('/download/init', { method: 'POST' })
|
||||
store.config.apiKeyInfo.value = res
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
init()
|
||||
|
||||
api.addEventListener('reconnected', () => {
|
||||
refresh()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user