mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-01 22:09:55 +00:00
[API Node] Allow authentification via Comfy API key (#3815)
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -1,95 +1,132 @@
|
||||
<template>
|
||||
<div class="w-96 p-2">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col gap-4 mb-8">
|
||||
<h1 class="text-2xl font-medium leading-normal my-0">
|
||||
{{ isSignIn ? t('auth.login.title') : t('auth.signup.title') }}
|
||||
</h1>
|
||||
<p class="text-base my-0">
|
||||
<span class="text-muted">{{
|
||||
isSignIn
|
||||
? t('auth.login.newUser')
|
||||
: t('auth.signup.alreadyHaveAccount')
|
||||
}}</span>
|
||||
<span class="ml-1 cursor-pointer text-blue-500" @click="toggleState">{{
|
||||
isSignIn ? t('auth.login.signUp') : t('auth.signup.signIn')
|
||||
}}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Message v-if="!isSecureContext" severity="warn" class="mb-4">
|
||||
{{ t('auth.login.insecureContextWarning') }}
|
||||
</Message>
|
||||
|
||||
<!-- Form -->
|
||||
<SignInForm v-if="isSignIn" @submit="signInWithEmail" />
|
||||
<div class="w-96 p-2 overflow-x-hidden">
|
||||
<ApiKeyForm
|
||||
v-if="showApiKeyForm"
|
||||
@back="showApiKeyForm = false"
|
||||
@success="onSuccess"
|
||||
/>
|
||||
<template v-else>
|
||||
<Message v-if="userIsInChina" severity="warn" class="mb-4">
|
||||
{{ t('auth.signup.regionRestrictionChina') }}
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col gap-4 mb-8">
|
||||
<h1 class="text-2xl font-medium leading-normal my-0">
|
||||
{{ isSignIn ? t('auth.login.title') : t('auth.signup.title') }}
|
||||
</h1>
|
||||
<p class="text-base my-0">
|
||||
<span class="text-muted">{{
|
||||
isSignIn
|
||||
? t('auth.login.newUser')
|
||||
: t('auth.signup.alreadyHaveAccount')
|
||||
}}</span>
|
||||
<span
|
||||
class="ml-1 cursor-pointer text-blue-500"
|
||||
@click="toggleState"
|
||||
>{{
|
||||
isSignIn ? t('auth.login.signUp') : t('auth.signup.signIn')
|
||||
}}</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Message v-if="!isSecureContext" severity="warn" class="mb-4">
|
||||
{{ t('auth.login.insecureContextWarning') }}
|
||||
</Message>
|
||||
<SignUpForm v-else @submit="signUpWithEmail" />
|
||||
|
||||
<!-- Form -->
|
||||
<SignInForm v-if="isSignIn" @submit="signInWithEmail" />
|
||||
<template v-else>
|
||||
<Message v-if="userIsInChina" severity="warn" class="mb-4">
|
||||
{{ t('auth.signup.regionRestrictionChina') }}
|
||||
</Message>
|
||||
<SignUpForm v-else @submit="signUpWithEmail" />
|
||||
</template>
|
||||
|
||||
<!-- Divider -->
|
||||
<Divider align="center" layout="horizontal" class="my-8">
|
||||
<span class="text-muted">{{ t('auth.login.orContinueWith') }}</span>
|
||||
</Divider>
|
||||
|
||||
<!-- Social Login Buttons -->
|
||||
<div class="flex flex-col gap-6">
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="signInWithGoogle"
|
||||
>
|
||||
<i class="pi pi-google mr-2"></i>
|
||||
{{
|
||||
isSignIn
|
||||
? t('auth.login.loginWithGoogle')
|
||||
: t('auth.signup.signUpWithGoogle')
|
||||
}}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="signInWithGithub"
|
||||
>
|
||||
<i class="pi pi-github mr-2"></i>
|
||||
{{
|
||||
isSignIn
|
||||
? t('auth.login.loginWithGithub')
|
||||
: t('auth.signup.signUpWithGithub')
|
||||
}}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="showApiKeyForm = true"
|
||||
>
|
||||
<img
|
||||
src="/assets/images/comfy-logo-mono.svg"
|
||||
class="w-5 h-5 mr-2"
|
||||
alt="Comfy"
|
||||
/>
|
||||
{{ t('auth.login.useApiKey') }}
|
||||
</Button>
|
||||
<small class="text-muted text-center">
|
||||
{{ t('auth.apiKey.helpText') }}
|
||||
<a
|
||||
href="https://platform.comfy.org/login"
|
||||
target="_blank"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
>
|
||||
{{ t('auth.apiKey.generateKey') }}
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Terms & Contact -->
|
||||
<p class="text-xs text-muted mt-8">
|
||||
{{ t('auth.login.termsText') }}
|
||||
<a
|
||||
href="https://www.comfy.org/terms-of-service"
|
||||
target="_blank"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
>
|
||||
{{ t('auth.login.termsLink') }}
|
||||
</a>
|
||||
{{ t('auth.login.andText') }}
|
||||
<a
|
||||
href="https://www.comfy.org/privacy"
|
||||
target="_blank"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
>
|
||||
{{ t('auth.login.privacyLink') }} </a
|
||||
>.
|
||||
{{ t('auth.login.questionsContactPrefix') }}
|
||||
<a href="mailto:hello@comfy.org" class="text-blue-500 cursor-pointer">
|
||||
hello@comfy.org</a
|
||||
>.
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<!-- Divider -->
|
||||
<Divider align="center" layout="horizontal" class="my-8">
|
||||
<span class="text-muted">{{ t('auth.login.orContinueWith') }}</span>
|
||||
</Divider>
|
||||
|
||||
<!-- Social Login Buttons -->
|
||||
<div class="flex flex-col gap-6">
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="signInWithGoogle"
|
||||
>
|
||||
<i class="pi pi-google mr-2"></i>
|
||||
{{
|
||||
isSignIn
|
||||
? t('auth.login.loginWithGoogle')
|
||||
: t('auth.signup.signUpWithGoogle')
|
||||
}}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="signInWithGithub"
|
||||
>
|
||||
<i class="pi pi-github mr-2"></i>
|
||||
{{
|
||||
isSignIn
|
||||
? t('auth.login.loginWithGithub')
|
||||
: t('auth.signup.signUpWithGithub')
|
||||
}}
|
||||
</Button>
|
||||
</div>
|
||||
<!-- Terms & Contact -->
|
||||
<p class="text-xs text-muted mt-8">
|
||||
{{ t('auth.login.termsText') }}
|
||||
<a
|
||||
href="https://www.comfy.org/terms-of-service"
|
||||
target="_blank"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
>
|
||||
{{ t('auth.login.termsLink') }}
|
||||
</a>
|
||||
{{ t('auth.login.andText') }}
|
||||
<a
|
||||
href="https://www.comfy.org/privacy"
|
||||
target="_blank"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
>
|
||||
{{ t('auth.login.privacyLink') }} </a
|
||||
>.
|
||||
{{ t('auth.login.questionsContactPrefix') }}
|
||||
<a href="mailto:hello@comfy.org" class="text-blue-500 cursor-pointer">
|
||||
hello@comfy.org</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -104,6 +141,7 @@ import { SignInData, SignUpData } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
|
||||
import { isInChina } from '@/utils/networkUtil'
|
||||
|
||||
import ApiKeyForm from './signin/ApiKeyForm.vue'
|
||||
import SignInForm from './signin/SignInForm.vue'
|
||||
import SignUpForm from './signin/SignUpForm.vue'
|
||||
|
||||
@@ -115,8 +153,11 @@ const { t } = useI18n()
|
||||
const authService = useFirebaseAuthService()
|
||||
const isSecureContext = window.isSecureContext
|
||||
const isSignIn = ref(true)
|
||||
const showApiKeyForm = ref(false)
|
||||
|
||||
const toggleState = () => {
|
||||
isSignIn.value = !isSignIn.value
|
||||
showApiKeyForm.value = false
|
||||
}
|
||||
|
||||
const signInWithGoogle = async () => {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<h2 class="text-2xl font-bold mb-2">{{ $t('userSettings.title') }}</h2>
|
||||
<Divider class="mb-3" />
|
||||
|
||||
<!-- Normal User Panel -->
|
||||
<div v-if="user" class="flex flex-col gap-2">
|
||||
<UserAvatar
|
||||
v-if="user.photoURL"
|
||||
@@ -66,6 +67,27 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- API Key Panel -->
|
||||
<div v-else-if="hasApiKey" class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<h3 class="font-medium">
|
||||
{{ $t('auth.apiKey.title') }}
|
||||
</h3>
|
||||
<div class="text-muted flex items-center gap-1">
|
||||
<i class="pi pi-key" />
|
||||
{{ $t('auth.apiKey.label') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
class="mt-4 w-32"
|
||||
severity="secondary"
|
||||
:label="$t('auth.signOut.signOut')"
|
||||
icon="pi pi-sign-out"
|
||||
@click="handleApiKeySignOut"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Login Section -->
|
||||
<div v-else class="flex flex-col gap-4">
|
||||
<p class="text-gray-600">
|
||||
@@ -94,14 +116,18 @@ import { computed } from 'vue'
|
||||
|
||||
import UserAvatar from '@/components/common/UserAvatar.vue'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
const dialogService = useDialogService()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const commandStore = useCommandStore()
|
||||
const apiKeyStore = useApiKeyAuthStore()
|
||||
|
||||
const user = computed(() => authStore.currentUser)
|
||||
const loading = computed(() => authStore.loading)
|
||||
const hasApiKey = computed(() => apiKeyStore.hasApiKey)
|
||||
|
||||
const providerName = computed(() => {
|
||||
const providerId = user.value?.providerData[0]?.providerId
|
||||
@@ -134,6 +160,10 @@ const handleSignOut = async () => {
|
||||
await commandStore.execute('Comfy.User.SignOut')
|
||||
}
|
||||
|
||||
const handleApiKeySignOut = async () => {
|
||||
await apiKeyStore.clearStoredApiKey()
|
||||
}
|
||||
|
||||
const handleSignIn = async () => {
|
||||
await commandStore.execute('Comfy.User.OpenSignInDialog')
|
||||
}
|
||||
|
||||
114
src/components/dialog/content/signin/ApiKeyForm.test.ts
Normal file
114
src/components/dialog/content/signin/ApiKeyForm.test.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { Form } from '@primevue/forms'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import Button from 'primevue/button'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Message from 'primevue/message'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createApp } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import ApiKeyForm from './ApiKeyForm.vue'
|
||||
|
||||
const mockStoreApiKey = vi.fn()
|
||||
const mockLoading = vi.fn(() => false)
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
loading: mockLoading()
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/apiKeyAuthStore', () => ({
|
||||
useApiKeyAuthStore: vi.fn(() => ({
|
||||
storeApiKey: mockStoreApiKey
|
||||
}))
|
||||
}))
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
auth: {
|
||||
apiKey: {
|
||||
title: 'API Key',
|
||||
label: 'API Key',
|
||||
placeholder: 'Enter your API Key',
|
||||
error: 'Invalid API Key',
|
||||
helpText: 'Need an API key?',
|
||||
generateKey: 'Get one here',
|
||||
whitelistInfo: 'About non-whitelisted sites'
|
||||
}
|
||||
},
|
||||
g: {
|
||||
back: 'Back',
|
||||
save: 'Save',
|
||||
learnMore: 'Learn more'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
describe('ApiKeyForm', () => {
|
||||
beforeEach(() => {
|
||||
const app = createApp({})
|
||||
app.use(PrimeVue)
|
||||
vi.clearAllMocks()
|
||||
mockStoreApiKey.mockReset()
|
||||
mockLoading.mockReset()
|
||||
})
|
||||
|
||||
const mountComponent = (props: any = {}) => {
|
||||
return mount(ApiKeyForm, {
|
||||
global: {
|
||||
plugins: [PrimeVue, createPinia(), i18n],
|
||||
components: { Button, Form, InputText, Message }
|
||||
},
|
||||
props
|
||||
})
|
||||
}
|
||||
|
||||
it('renders correctly with all required elements', () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
expect(wrapper.find('h1').text()).toBe('API Key')
|
||||
expect(wrapper.find('label').text()).toBe('API Key')
|
||||
expect(wrapper.findComponent(InputText).exists()).toBe(true)
|
||||
expect(wrapper.findComponent(Button).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('emits back event when back button is clicked', async () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
await wrapper.findComponent(Button).trigger('click')
|
||||
expect(wrapper.emitted('back')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows loading state when submitting', async () => {
|
||||
mockLoading.mockReturnValue(true)
|
||||
const wrapper = mountComponent()
|
||||
const input = wrapper.findComponent(InputText)
|
||||
|
||||
await input.setValue(
|
||||
'comfyui-123456789012345678901234567890123456789012345678901234567890123456789012'
|
||||
)
|
||||
await wrapper.find('form').trigger('submit')
|
||||
|
||||
const submitButton = wrapper
|
||||
.findAllComponents(Button)
|
||||
.find((btn) => btn.text() === 'Save')
|
||||
expect(submitButton?.props('loading')).toBe(true)
|
||||
})
|
||||
|
||||
it('displays help text and links correctly', () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
const helpText = wrapper.find('small')
|
||||
expect(helpText.text()).toContain('Need an API key?')
|
||||
expect(helpText.find('a').attributes('href')).toBe(
|
||||
'https://platform.comfy.org/login'
|
||||
)
|
||||
})
|
||||
})
|
||||
111
src/components/dialog/content/signin/ApiKeyForm.vue
Normal file
111
src/components/dialog/content/signin/ApiKeyForm.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-4 mb-8">
|
||||
<h1 class="text-2xl font-medium leading-normal my-0">
|
||||
{{ t('auth.apiKey.title') }}
|
||||
</h1>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-base my-0 text-muted">
|
||||
{{ t('auth.apiKey.description') }}
|
||||
</p>
|
||||
<a
|
||||
href="https://docs.comfy.org/interface/user#logging-in-with-an-api-key"
|
||||
target="_blank"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
>
|
||||
{{ t('g.learnMore') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
v-slot="$form"
|
||||
class="flex flex-col gap-6"
|
||||
:resolver="zodResolver(apiKeySchema)"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<Message v-if="$form.apiKey?.invalid" severity="error" class="mb-4">
|
||||
{{ $form.apiKey.error.message }}
|
||||
</Message>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label
|
||||
class="opacity-80 text-base font-medium mb-2"
|
||||
for="comfy-org-api-key"
|
||||
>
|
||||
{{ t('auth.apiKey.label') }}
|
||||
</label>
|
||||
<div class="flex flex-col gap-2">
|
||||
<InputText
|
||||
pt:root:id="comfy-org-api-key"
|
||||
pt:root:autocomplete="off"
|
||||
class="h-10"
|
||||
name="apiKey"
|
||||
type="password"
|
||||
:placeholder="t('auth.apiKey.placeholder')"
|
||||
:invalid="$form.apiKey?.invalid"
|
||||
/>
|
||||
<small class="text-muted">
|
||||
{{ t('auth.apiKey.helpText') }}
|
||||
<a
|
||||
href="https://platform.comfy.org/login"
|
||||
target="_blank"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
>
|
||||
{{ t('auth.apiKey.generateKey') }}
|
||||
</a>
|
||||
<span class="mx-1">•</span>
|
||||
<a
|
||||
href="https://docs.comfy.org/tutorials/api-nodes/overview#log-in-with-api-key-on-non-whitelisted-websites"
|
||||
target="_blank"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
>
|
||||
{{ t('auth.apiKey.whitelistInfo') }}
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<Button type="button" link @click="$emit('back')">
|
||||
{{ t('g.back') }}
|
||||
</Button>
|
||||
<Button type="submit" :loading="loading" :disabled="loading">
|
||||
{{ t('g.save') }}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Form, FormSubmitEvent } from '@primevue/forms'
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Message from 'primevue/message'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { apiKeySchema } from '@/schemas/signInSchema'
|
||||
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const apiKeyStore = useApiKeyAuthStore()
|
||||
const loading = computed(() => authStore.loading)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'back'): void
|
||||
(e: 'success'): void
|
||||
}>()
|
||||
|
||||
const onSubmit = async (event: FormSubmitEvent) => {
|
||||
if (event.valid) {
|
||||
await apiKeyStore.storeApiKey(event.values.apiKey)
|
||||
emit('success')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1232,8 +1232,27 @@
|
||||
"unauthorizedDomain": "Your domain {domain} is not authorized to use this service. Please contact {email} to add your domain to the whitelist."
|
||||
},
|
||||
"auth": {
|
||||
"apiKey": {
|
||||
"title": "API Key",
|
||||
"label": "API Key",
|
||||
"description": "Use your Comfy API key to enable API Nodes",
|
||||
"placeholder": "Enter your API Key",
|
||||
"error": "Invalid API Key",
|
||||
"storageFailed": "Failed to store API Key",
|
||||
"storageFailedDetail": "Please try again.",
|
||||
"stored": "API Key stored",
|
||||
"storedDetail": "Your API Key has been stored successfully",
|
||||
"cleared": "API Key cleared",
|
||||
"clearedDetail": "Your API Key has been cleared successfully",
|
||||
"invalid": "Invalid API Key",
|
||||
"invalidDetail": "Please enter a valid API Key",
|
||||
"helpText": "Need an API key?",
|
||||
"generateKey": "Get one here",
|
||||
"whitelistInfo": "About non-whitelisted sites"
|
||||
},
|
||||
"login": {
|
||||
"title": "Log in to your account",
|
||||
"useApiKey": "Comfy API Key",
|
||||
"signInOrSignUp": "Sign In / Sign Up",
|
||||
"forgotPasswordError": "Failed to send password reset email",
|
||||
"passwordResetSent": "Password reset email sent",
|
||||
@@ -1290,6 +1309,8 @@
|
||||
"required": "Required",
|
||||
"minLength": "Must be at least {length} characters",
|
||||
"maxLength": "Must be no more than {length} characters",
|
||||
"prefix": "Must start with {prefix}",
|
||||
"length": "Must be {length} characters",
|
||||
"password": {
|
||||
"requirements": "Password requirements",
|
||||
"minLength": "Must be between 8 and 32 characters",
|
||||
|
||||
@@ -29,6 +29,24 @@
|
||||
"title": "Se requiere iniciar sesión para usar los nodos de API"
|
||||
},
|
||||
"auth": {
|
||||
"apiKey": {
|
||||
"cleared": "Clave API eliminada",
|
||||
"clearedDetail": "Tu clave API se ha eliminado correctamente",
|
||||
"description": "Usa tu clave API de Comfy para habilitar los nodos de API",
|
||||
"error": "Clave API no válida",
|
||||
"generateKey": "Consíguela aquí",
|
||||
"helpText": "¿Necesitas una clave API?",
|
||||
"invalid": "Clave API no válida",
|
||||
"invalidDetail": "Por favor, introduce una clave API válida",
|
||||
"label": "Clave API",
|
||||
"placeholder": "Introduce tu clave API",
|
||||
"storageFailed": "No se pudo guardar la clave API",
|
||||
"storageFailedDetail": "Por favor, inténtalo de nuevo.",
|
||||
"stored": "Clave API guardada",
|
||||
"storedDetail": "Tu clave API se ha guardado correctamente",
|
||||
"title": "Clave API",
|
||||
"whitelistInfo": "Acerca de los sitios no incluidos en la lista blanca"
|
||||
},
|
||||
"login": {
|
||||
"andText": "y",
|
||||
"confirmPasswordLabel": "Confirmar contraseña",
|
||||
@@ -56,6 +74,7 @@
|
||||
"termsLink": "Términos de uso",
|
||||
"termsText": "Al hacer clic en \"Siguiente\" o \"Registrarse\", aceptas nuestros",
|
||||
"title": "Inicia sesión en tu cuenta",
|
||||
"useApiKey": "Clave API de Comfy",
|
||||
"userAvatar": "Avatar de usuario"
|
||||
},
|
||||
"passwordUpdate": {
|
||||
@@ -1330,6 +1349,7 @@
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "Dirección de correo electrónico inválida",
|
||||
"length": "Debe tener {length} caracteres",
|
||||
"maxLength": "No debe tener más de {length} caracteres",
|
||||
"minLength": "Debe tener al menos {length} caracteres",
|
||||
"password": {
|
||||
@@ -1342,6 +1362,7 @@
|
||||
"uppercase": "Debe contener al menos una letra mayúscula"
|
||||
},
|
||||
"personalDataConsentRequired": "Debes aceptar el procesamiento de tus datos personales.",
|
||||
"prefix": "Debe comenzar con {prefix}",
|
||||
"required": "Requerido"
|
||||
},
|
||||
"welcome": {
|
||||
|
||||
@@ -29,6 +29,24 @@
|
||||
"title": "Connexion requise pour utiliser les nœuds API"
|
||||
},
|
||||
"auth": {
|
||||
"apiKey": {
|
||||
"cleared": "Clé API supprimée",
|
||||
"clearedDetail": "Votre clé API a été supprimée avec succès",
|
||||
"description": "Utilisez votre clé API Comfy pour activer les nœuds API",
|
||||
"error": "Clé API invalide",
|
||||
"generateKey": "Obtenez-en une ici",
|
||||
"helpText": "Besoin d'une clé API ?",
|
||||
"invalid": "Clé API invalide",
|
||||
"invalidDetail": "Veuillez entrer une clé API valide",
|
||||
"label": "Clé API",
|
||||
"placeholder": "Entrez votre clé API",
|
||||
"storageFailed": "Échec de l’enregistrement de la clé API",
|
||||
"storageFailedDetail": "Veuillez réessayer.",
|
||||
"stored": "Clé API enregistrée",
|
||||
"storedDetail": "Votre clé API a été enregistrée avec succès",
|
||||
"title": "Clé API",
|
||||
"whitelistInfo": "À propos des sites non autorisés"
|
||||
},
|
||||
"login": {
|
||||
"andText": "et",
|
||||
"confirmPasswordLabel": "Confirmer le mot de passe",
|
||||
@@ -56,6 +74,7 @@
|
||||
"termsLink": "Conditions d'utilisation",
|
||||
"termsText": "En cliquant sur \"Suivant\" ou \"S'inscrire\", vous acceptez nos",
|
||||
"title": "Connectez-vous à votre compte",
|
||||
"useApiKey": "Clé API Comfy",
|
||||
"userAvatar": "Avatar utilisateur"
|
||||
},
|
||||
"passwordUpdate": {
|
||||
@@ -1330,6 +1349,7 @@
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "Adresse e-mail invalide",
|
||||
"length": "Doit comporter {length} caractères",
|
||||
"maxLength": "Ne doit pas dépasser {length} caractères",
|
||||
"minLength": "Doit contenir au moins {length} caractères",
|
||||
"password": {
|
||||
@@ -1342,6 +1362,7 @@
|
||||
"uppercase": "Doit contenir au moins une lettre majuscule"
|
||||
},
|
||||
"personalDataConsentRequired": "Vous devez accepter le traitement de vos données personnelles.",
|
||||
"prefix": "Doit commencer par {prefix}",
|
||||
"required": "Requis"
|
||||
},
|
||||
"welcome": {
|
||||
|
||||
@@ -29,6 +29,24 @@
|
||||
"title": "APIノードを使用するためにはサインインが必要です"
|
||||
},
|
||||
"auth": {
|
||||
"apiKey": {
|
||||
"cleared": "APIキーが削除されました",
|
||||
"clearedDetail": "APIキーが正常に削除されました",
|
||||
"description": "Comfy APIキーを使用してAPIノードを有効にします",
|
||||
"error": "無効なAPIキーです",
|
||||
"generateKey": "こちらから取得",
|
||||
"helpText": "APIキーが必要ですか?",
|
||||
"invalid": "無効なAPIキーです",
|
||||
"invalidDetail": "有効なAPIキーを入力してください",
|
||||
"label": "APIキー",
|
||||
"placeholder": "APIキーを入力してください",
|
||||
"storageFailed": "APIキーの保存に失敗しました",
|
||||
"storageFailedDetail": "もう一度お試しください。",
|
||||
"stored": "APIキーが保存されました",
|
||||
"storedDetail": "APIキーが正常に保存されました",
|
||||
"title": "APIキー",
|
||||
"whitelistInfo": "ホワイトリストに登録されていないサイトについて"
|
||||
},
|
||||
"login": {
|
||||
"andText": "および",
|
||||
"confirmPasswordLabel": "パスワードの確認",
|
||||
@@ -56,6 +74,7 @@
|
||||
"termsLink": "利用規約",
|
||||
"termsText": "「次へ」または「サインアップ」をクリックすると、私たちの",
|
||||
"title": "アカウントにログインする",
|
||||
"useApiKey": "Comfy APIキー",
|
||||
"userAvatar": "ユーザーアバター"
|
||||
},
|
||||
"passwordUpdate": {
|
||||
@@ -1330,6 +1349,7 @@
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "無効なメールアドレス",
|
||||
"length": "{length}文字でなければなりません",
|
||||
"maxLength": "{length}文字以下でなければなりません",
|
||||
"minLength": "{length}文字以上でなければなりません",
|
||||
"password": {
|
||||
@@ -1342,6 +1362,7 @@
|
||||
"uppercase": "少なくとも1つの大文字を含む必要があります"
|
||||
},
|
||||
"personalDataConsentRequired": "個人データの処理に同意する必要があります。",
|
||||
"prefix": "{prefix}で始める必要があります",
|
||||
"required": "必須"
|
||||
},
|
||||
"welcome": {
|
||||
|
||||
@@ -29,6 +29,24 @@
|
||||
"title": "API 노드 사용에 필요한 로그인"
|
||||
},
|
||||
"auth": {
|
||||
"apiKey": {
|
||||
"cleared": "API 키 삭제됨",
|
||||
"clearedDetail": "API 키가 성공적으로 삭제되었습니다",
|
||||
"description": "Comfy API 키를 사용하여 API 노드를 활성화하세요",
|
||||
"error": "유효하지 않은 API 키",
|
||||
"generateKey": "여기에서 받기",
|
||||
"helpText": "API 키가 필요하신가요?",
|
||||
"invalid": "유효하지 않은 API 키",
|
||||
"invalidDetail": "유효한 API 키를 입력해 주세요",
|
||||
"label": "API 키",
|
||||
"placeholder": "API 키를 입력하세요",
|
||||
"storageFailed": "API 키 저장 실패",
|
||||
"storageFailedDetail": "다시 시도해 주세요.",
|
||||
"stored": "API 키 저장됨",
|
||||
"storedDetail": "API 키가 성공적으로 저장되었습니다",
|
||||
"title": "API 키",
|
||||
"whitelistInfo": "비허용 사이트에 대하여"
|
||||
},
|
||||
"login": {
|
||||
"andText": "및",
|
||||
"confirmPasswordLabel": "비밀번호 확인",
|
||||
@@ -56,6 +74,7 @@
|
||||
"termsLink": "이용 약관",
|
||||
"termsText": "\"다음\" 또는 \"가입하기\"를 클릭하면 우리의",
|
||||
"title": "계정에 로그인",
|
||||
"useApiKey": "Comfy API 키",
|
||||
"userAvatar": "사용자 아바타"
|
||||
},
|
||||
"passwordUpdate": {
|
||||
@@ -1330,6 +1349,7 @@
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "유효하지 않은 이메일 주소",
|
||||
"length": "{length}자여야 합니다",
|
||||
"maxLength": "{length}자를 초과할 수 없습니다",
|
||||
"minLength": "{length}자 이상이어야 합니다",
|
||||
"password": {
|
||||
@@ -1342,6 +1362,7 @@
|
||||
"uppercase": "적어도 하나의 대문자를 포함해야 합니다"
|
||||
},
|
||||
"personalDataConsentRequired": "개인 데이터 처리에 동의해야 합니다.",
|
||||
"prefix": "{prefix}(으)로 시작해야 합니다",
|
||||
"required": "필수"
|
||||
},
|
||||
"welcome": {
|
||||
|
||||
@@ -29,6 +29,24 @@
|
||||
"title": "Требуется вход для использования API Nodes"
|
||||
},
|
||||
"auth": {
|
||||
"apiKey": {
|
||||
"cleared": "API-ключ удалён",
|
||||
"clearedDetail": "Ваш API-ключ был успешно удалён",
|
||||
"description": "Используйте ваш Comfy API-ключ для активации API-узлов",
|
||||
"error": "Недействительный API-ключ",
|
||||
"generateKey": "Получить здесь",
|
||||
"helpText": "Нужен API-ключ?",
|
||||
"invalid": "Недействительный API-ключ",
|
||||
"invalidDetail": "Пожалуйста, введите действительный API-ключ",
|
||||
"label": "API-ключ",
|
||||
"placeholder": "Введите ваш API-ключ",
|
||||
"storageFailed": "Не удалось сохранить API-ключ",
|
||||
"storageFailedDetail": "Пожалуйста, попробуйте еще раз.",
|
||||
"stored": "API-ключ сохранён",
|
||||
"storedDetail": "Ваш API-ключ был успешно сохранён",
|
||||
"title": "API-ключ",
|
||||
"whitelistInfo": "О не включённых в белый список сайтах"
|
||||
},
|
||||
"login": {
|
||||
"andText": "и",
|
||||
"confirmPasswordLabel": "Подтвердите пароль",
|
||||
@@ -56,6 +74,7 @@
|
||||
"termsLink": "Условиями использования",
|
||||
"termsText": "Нажимая \"Далее\" или \"Зарегистрироваться\", вы соглашаетесь с нашими",
|
||||
"title": "Войдите в свой аккаунт",
|
||||
"useApiKey": "Comfy API-ключ",
|
||||
"userAvatar": "Аватар пользователя"
|
||||
},
|
||||
"passwordUpdate": {
|
||||
@@ -1330,6 +1349,7 @@
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "Недействительный адрес электронной почты",
|
||||
"length": "Должно быть {length} символов",
|
||||
"maxLength": "Должно быть не более {length} символов",
|
||||
"minLength": "Должно быть не менее {length} символов",
|
||||
"password": {
|
||||
@@ -1342,6 +1362,7 @@
|
||||
"uppercase": "Должен содержать хотя бы одну заглавную букву"
|
||||
},
|
||||
"personalDataConsentRequired": "Вы должны согласиться на обработку ваших персональных данных.",
|
||||
"prefix": "Должно начинаться с {prefix}",
|
||||
"required": "Обязательно"
|
||||
},
|
||||
"welcome": {
|
||||
|
||||
@@ -29,6 +29,24 @@
|
||||
"title": "使用API节点需要登录"
|
||||
},
|
||||
"auth": {
|
||||
"apiKey": {
|
||||
"cleared": "API 密钥已清除",
|
||||
"clearedDetail": "您的 API 密钥已成功清除",
|
||||
"description": "使用您的 Comfy API 密钥以启用 API 节点",
|
||||
"error": "无效的 API 密钥",
|
||||
"generateKey": "在这里获取",
|
||||
"helpText": "需要 API 密钥?",
|
||||
"invalid": "无效的 API 密钥",
|
||||
"invalidDetail": "请输入有效的 API 密钥",
|
||||
"label": "API 密钥",
|
||||
"placeholder": "请输入您的 API 密钥",
|
||||
"storageFailed": "API 密钥存储失败",
|
||||
"storageFailedDetail": "请重试。",
|
||||
"stored": "API 密钥已存储",
|
||||
"storedDetail": "您的 API 密钥已成功存储",
|
||||
"title": "API 密钥",
|
||||
"whitelistInfo": "关于非白名单网站"
|
||||
},
|
||||
"login": {
|
||||
"andText": "和",
|
||||
"confirmPasswordLabel": "确认密码",
|
||||
@@ -56,6 +74,7 @@
|
||||
"termsLink": "使用条款",
|
||||
"termsText": "点击“下一步”或“注册”即表示您同意我们的",
|
||||
"title": "登录您的账户",
|
||||
"useApiKey": "Comfy API 密钥",
|
||||
"userAvatar": "用户头像"
|
||||
},
|
||||
"passwordUpdate": {
|
||||
@@ -1330,6 +1349,7 @@
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "无效的电子邮件地址",
|
||||
"length": "必须为{length}个字符",
|
||||
"maxLength": "不能超过{length}个字符",
|
||||
"minLength": "必须至少有{length}个字符",
|
||||
"password": {
|
||||
@@ -1342,6 +1362,7 @@
|
||||
"uppercase": "必须包含至少一个大写字母"
|
||||
},
|
||||
"personalDataConsentRequired": "您必须同意处理您的个人数据。",
|
||||
"prefix": "必须以 {prefix} 开头",
|
||||
"required": "必填"
|
||||
},
|
||||
"welcome": {
|
||||
|
||||
@@ -2,6 +2,16 @@ import { z } from 'zod'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
|
||||
export const apiKeySchema = z.object({
|
||||
apiKey: z
|
||||
.string()
|
||||
.trim()
|
||||
.startsWith('comfyui-', t('validation.prefix', { prefix: 'comfyui-' }))
|
||||
.length(72, t('validation.length', { length: 72 }))
|
||||
})
|
||||
|
||||
export type ApiKeyData = z.infer<typeof apiKeySchema>
|
||||
|
||||
export const signInSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
|
||||
@@ -58,6 +58,21 @@ interface QueuePromptRequestBody {
|
||||
* ```
|
||||
*/
|
||||
auth_token_comfy_org?: string
|
||||
/**
|
||||
* The auth token for the comfy org account if the user is logged in.
|
||||
*
|
||||
* Backend node can access this token by specifying following input:
|
||||
* ```python
|
||||
* def INPUT_TYPES(s):
|
||||
* return {
|
||||
* "hidden": { "api_key": "API_KEY_COMFY_ORG" }
|
||||
* }
|
||||
*
|
||||
* def execute(self, api_key: str):
|
||||
* print(f"API Key: {api_key}")
|
||||
* ```
|
||||
*/
|
||||
api_key_comfy_org?: string
|
||||
}
|
||||
front?: boolean
|
||||
number?: number
|
||||
@@ -228,6 +243,10 @@ export class ComfyApi extends EventTarget {
|
||||
* custom nodes are patched.
|
||||
*/
|
||||
authToken?: string
|
||||
/**
|
||||
* The API key for the comfy org account if the user logged in via API key.
|
||||
*/
|
||||
apiKey?: string
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
@@ -545,6 +564,7 @@ export class ComfyApi extends EventTarget {
|
||||
prompt,
|
||||
extra_data: {
|
||||
auth_token_comfy_org: this.authToken,
|
||||
api_key_comfy_org: this.apiKey,
|
||||
extra_pnginfo: { workflow }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import { useDialogService } from '@/services/dialogService'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
@@ -1186,6 +1187,7 @@ export class ComfyApp {
|
||||
|
||||
let comfyOrgAuthToken =
|
||||
(await useFirebaseAuthStore().getIdToken()) ?? undefined
|
||||
let comfyOrgApiKey = useApiKeyAuthStore().getApiKey()
|
||||
|
||||
try {
|
||||
while (this.#queueItems.length) {
|
||||
@@ -1199,8 +1201,10 @@ export class ComfyApp {
|
||||
const p = await this.graphToPrompt(this.graph, { queueNodeIds })
|
||||
try {
|
||||
api.authToken = comfyOrgAuthToken
|
||||
api.apiKey = comfyOrgApiKey ?? undefined
|
||||
const res = await api.queuePrompt(number, p)
|
||||
delete api.authToken
|
||||
delete api.apiKey
|
||||
executionStore.lastNodeErrors = res.node_errors ?? null
|
||||
if (executionStore.lastNodeErrors?.length) {
|
||||
this.canvas.draw(true, true)
|
||||
|
||||
69
src/stores/apiKeyAuthStore.ts
Normal file
69
src/stores/apiKeyAuthStore.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { t } from '@/i18n'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
const STORAGE_KEY = 'comfy_api_key'
|
||||
|
||||
export const useApiKeyAuthStore = defineStore('apiKeyAuth', () => {
|
||||
const apiKey = useLocalStorage<string | null>(STORAGE_KEY, null)
|
||||
const toastStore = useToastStore()
|
||||
const { wrapWithErrorHandlingAsync, toastErrorHandler } = useErrorHandling()
|
||||
|
||||
const reportError = (error: unknown) => {
|
||||
if (error instanceof Error && error.message === 'STORAGE_FAILED') {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('auth.apiKey.storageFailed'),
|
||||
detail: t('auth.apiKey.storageFailedDetail')
|
||||
})
|
||||
} else {
|
||||
toastErrorHandler(error)
|
||||
}
|
||||
}
|
||||
|
||||
const storeApiKey = wrapWithErrorHandlingAsync(async (newApiKey: string) => {
|
||||
apiKey.value = newApiKey
|
||||
toastStore.add({
|
||||
severity: 'success',
|
||||
summary: t('auth.apiKey.stored'),
|
||||
detail: t('auth.apiKey.storedDetail'),
|
||||
life: 5000
|
||||
})
|
||||
return true
|
||||
}, reportError)
|
||||
|
||||
const clearStoredApiKey = wrapWithErrorHandlingAsync(async () => {
|
||||
apiKey.value = null
|
||||
toastStore.add({
|
||||
severity: 'success',
|
||||
summary: t('auth.apiKey.cleared'),
|
||||
detail: t('auth.apiKey.clearedDetail'),
|
||||
life: 5000
|
||||
})
|
||||
return true
|
||||
}, reportError)
|
||||
|
||||
const getApiKey = () => apiKey.value
|
||||
|
||||
const getAuthHeader = () => {
|
||||
const comfyOrgApiKey = getApiKey()
|
||||
if (comfyOrgApiKey) {
|
||||
return {
|
||||
'X-API-KEY': comfyOrgApiKey
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
hasApiKey: computed(() => !!apiKey.value),
|
||||
storeApiKey,
|
||||
clearStoredApiKey,
|
||||
getAuthHeader,
|
||||
getApiKey
|
||||
}
|
||||
})
|
||||
@@ -20,6 +20,8 @@ import { useFirebaseAuth } from 'vuefire'
|
||||
|
||||
import { COMFY_API_BASE_URL } from '@/config/comfyApi'
|
||||
import { t } from '@/i18n'
|
||||
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
|
||||
import { type AuthHeader } from '@/types/authTypes'
|
||||
import { operations } from '@/types/comfyRegistryTypes'
|
||||
|
||||
type CreditPurchaseResponse =
|
||||
@@ -93,12 +95,34 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the appropriate authentication header for API requests.
|
||||
* Checks for authentication in the following order:
|
||||
* 1. Firebase authentication token (if user is logged in)
|
||||
* 2. API key (if stored in the browser's credential manager)
|
||||
*
|
||||
* @returns {Promise<AuthHeader | null>}
|
||||
* - A LoggedInAuthHeader with Bearer token if Firebase authenticated
|
||||
* - An ApiKeyAuthHeader with X-API-KEY if API key exists
|
||||
* - null if neither authentication method is available
|
||||
*/
|
||||
const getAuthHeader = async (): Promise<AuthHeader | null> => {
|
||||
const token = await getIdToken()
|
||||
if (token) {
|
||||
return {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
const apiKeyStore = useApiKeyAuthStore()
|
||||
return apiKeyStore.getAuthHeader()
|
||||
}
|
||||
|
||||
const fetchBalance = async (): Promise<GetCustomerBalanceResponse | null> => {
|
||||
isFetchingBalance.value = true
|
||||
try {
|
||||
const token = await getIdToken()
|
||||
if (!token) {
|
||||
isFetchingBalance.value = false
|
||||
const authHeader = await getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.userNotAuthenticated')
|
||||
)
|
||||
@@ -106,7 +130,8 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
|
||||
const response = await fetch(`${COMFY_API_BASE_URL}/customers/balance`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
...authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -133,14 +158,17 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const createCustomer = async (
|
||||
token: string
|
||||
): Promise<CreateCustomerResponse> => {
|
||||
const createCustomer = async (): Promise<CreateCustomerResponse> => {
|
||||
const authHeader = await getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
const createCustomerRes = await fetch(`${COMFY_API_BASE_URL}/customers`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
...authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
if (!createCustomerRes.ok) {
|
||||
@@ -181,7 +209,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
if (!token) {
|
||||
throw new Error('Cannot create customer: User not authenticated')
|
||||
}
|
||||
await createCustomer(token)
|
||||
await createCustomer()
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -242,22 +270,22 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
const addCredits = async (
|
||||
requestBodyContent: CreditPurchasePayload
|
||||
): Promise<CreditPurchaseResponse> => {
|
||||
const token = await getIdToken()
|
||||
if (!token) {
|
||||
const authHeader = await getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
// Ensure customer was created during login/registration
|
||||
if (!customerCreated.value) {
|
||||
await createCustomer(token)
|
||||
await createCustomer()
|
||||
customerCreated.value = true
|
||||
}
|
||||
|
||||
const response = await fetch(`${COMFY_API_BASE_URL}/customers/credit`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
...authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestBodyContent)
|
||||
})
|
||||
@@ -282,16 +310,16 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
const accessBillingPortal = async (
|
||||
requestBody?: AccessBillingPortalReqBody
|
||||
): Promise<AccessBillingPortalResponse> => {
|
||||
const token = await getIdToken()
|
||||
if (!token) {
|
||||
const authHeader = await getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
const response = await fetch(`${COMFY_API_BASE_URL}/customers/billing`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
...authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
...(requestBody && {
|
||||
body: JSON.stringify(requestBody)
|
||||
@@ -335,6 +363,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
fetchBalance,
|
||||
accessBillingPortal,
|
||||
sendPasswordReset,
|
||||
updatePassword: _updatePassword
|
||||
updatePassword: _updatePassword,
|
||||
getAuthHeader
|
||||
}
|
||||
})
|
||||
|
||||
9
src/types/authTypes.ts
Normal file
9
src/types/authTypes.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
type LoggedInAuthHeader = {
|
||||
Authorization: `Bearer ${string}`
|
||||
}
|
||||
|
||||
export type ApiKeyAuthHeader = {
|
||||
'X-API-KEY': string
|
||||
}
|
||||
|
||||
export type AuthHeader = LoggedInAuthHeader | ApiKeyAuthHeader
|
||||
Reference in New Issue
Block a user