mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-19 22:34:15 +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>
|
||||
Reference in New Issue
Block a user