[API Node] Show user state when logged in via API key (#3838)

Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Chenlei Hu <hcl@comfy.org>
This commit is contained in:
Christian Byrne
2025-05-09 11:45:32 -07:00
committed by GitHub
parent fdad2475ce
commit 6408623b71
14 changed files with 192 additions and 101 deletions

View File

@@ -5,10 +5,10 @@
<Divider class="mb-3" /> <Divider class="mb-3" />
<!-- Normal User Panel --> <!-- Normal User Panel -->
<div v-if="user" class="flex flex-col gap-2"> <div v-if="isLoggedIn" class="flex flex-col gap-2">
<UserAvatar <UserAvatar
v-if="user.photoURL" v-if="userPhotoUrl"
:photo-url="user.photoURL" :photo-url="userPhotoUrl"
shape="circle" shape="circle"
size="large" size="large"
/> />
@@ -18,7 +18,7 @@
{{ $t('userSettings.name') }} {{ $t('userSettings.name') }}
</h3> </h3>
<div class="text-muted"> <div class="text-muted">
{{ user.displayName || $t('userSettings.notSet') }} {{ userDisplayName || $t('userSettings.notSet') }}
</div> </div>
</div> </div>
@@ -26,8 +26,8 @@
<h3 class="font-medium"> <h3 class="font-medium">
{{ $t('userSettings.email') }} {{ $t('userSettings.email') }}
</h3> </h3>
<a :href="'mailto:' + user.email" class="hover:underline"> <a :href="'mailto:' + userEmail" class="hover:underline">
{{ user.email }} {{ userEmail }}
</a> </a>
</div> </div>
@@ -67,27 +67,6 @@
/> />
</div> </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 --> <!-- Login Section -->
<div v-else class="flex flex-col gap-4"> <div v-else class="flex flex-col gap-4">
<p class="text-gray-600"> <p class="text-gray-600">
@@ -112,59 +91,22 @@ import Button from 'primevue/button'
import Divider from 'primevue/divider' import Divider from 'primevue/divider'
import ProgressSpinner from 'primevue/progressspinner' import ProgressSpinner from 'primevue/progressspinner'
import TabPanel from 'primevue/tabpanel' import TabPanel from 'primevue/tabpanel'
import { computed } from 'vue'
import UserAvatar from '@/components/common/UserAvatar.vue' import UserAvatar from '@/components/common/UserAvatar.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useDialogService } from '@/services/dialogService' import { useDialogService } from '@/services/dialogService'
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
import { useCommandStore } from '@/stores/commandStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
const dialogService = useDialogService() const dialogService = useDialogService()
const authStore = useFirebaseAuthStore() const {
const commandStore = useCommandStore() loading,
const apiKeyStore = useApiKeyAuthStore() isLoggedIn,
isEmailProvider,
const user = computed(() => authStore.currentUser) userDisplayName,
const loading = computed(() => authStore.loading) userEmail,
const hasApiKey = computed(() => apiKeyStore.hasApiKey) userPhotoUrl,
providerName,
const providerName = computed(() => { providerIcon,
const providerId = user.value?.providerData[0]?.providerId handleSignOut,
if (providerId?.includes('google')) { handleSignIn
return 'Google' } = useCurrentUser()
}
if (providerId?.includes('github')) {
return 'GitHub'
}
return providerId
})
const providerIcon = computed(() => {
const providerId = user.value?.providerData[0]?.providerId
if (providerId?.includes('google')) {
return 'pi pi-google'
}
if (providerId?.includes('github')) {
return 'pi pi-github'
}
return 'pi pi-user'
})
const isEmailProvider = computed(() => {
const providerId = user.value?.providerData[0]?.providerId
return providerId === 'password'
})
const handleSignOut = async () => {
await commandStore.execute('Comfy.User.SignOut')
}
const handleApiKeySignOut = async () => {
await apiKeyStore.clearStoredApiKey()
}
const handleSignIn = async () => {
await commandStore.execute('Comfy.User.OpenSignInDialog')
}
</script> </script>

View File

@@ -2,7 +2,7 @@
<template> <template>
<div> <div>
<Button <Button
v-if="isAuthenticated" v-if="isLoggedIn"
class="user-profile-button p-1" class="user-profile-button p-1"
severity="secondary" severity="secondary"
text text
@@ -30,15 +30,14 @@ import Popover from 'primevue/popover'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import UserAvatar from '@/components/common/UserAvatar.vue' import UserAvatar from '@/components/common/UserAvatar.vue'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import CurrentUserPopover from './CurrentUserPopover.vue' import CurrentUserPopover from './CurrentUserPopover.vue'
const authStore = useFirebaseAuthStore() const { isLoggedIn, userPhotoUrl } = useCurrentUser()
const popover = ref<InstanceType<typeof Popover> | null>(null) const popover = ref<InstanceType<typeof Popover> | null>(null)
const isAuthenticated = computed(() => authStore.isAuthenticated)
const photoURL = computed<string | undefined>( const photoURL = computed<string | undefined>(
() => authStore.currentUser?.photoURL ?? undefined () => userPhotoUrl.value ?? undefined
) )
</script> </script>

View File

@@ -6,19 +6,19 @@
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<UserAvatar <UserAvatar
class="mb-3" class="mb-3"
:photo-url="user?.photoURL" :photo-url="userPhotoUrl"
:pt:icon:class="{ :pt:icon:class="{
'!text-2xl': !user?.photoURL '!text-2xl': !userPhotoUrl
}" }"
size="large" size="large"
/> />
<!-- User Details --> <!-- User Details -->
<h3 class="text-lg font-semibold truncate my-0 mb-1"> <h3 class="text-lg font-semibold truncate my-0 mb-1">
{{ user?.displayName || $t('g.user') }} {{ userDisplayName || $t('g.user') }}
</h3> </h3>
<p v-if="user?.email" class="text-sm text-muted truncate my-0"> <p v-if="userEmail" class="text-sm text-muted truncate my-0">
{{ user.email }} {{ userEmail }}
</p> </p>
</div> </div>
</div> </div>
@@ -64,20 +64,18 @@
<script setup lang="ts"> <script setup lang="ts">
import Button from 'primevue/button' import Button from 'primevue/button'
import Divider from 'primevue/divider' import Divider from 'primevue/divider'
import { computed, onMounted } from 'vue' import { onMounted } from 'vue'
import UserAvatar from '@/components/common/UserAvatar.vue' import UserAvatar from '@/components/common/UserAvatar.vue'
import UserCredit from '@/components/common/UserCredit.vue' import UserCredit from '@/components/common/UserCredit.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useDialogService } from '@/services/dialogService' import { useDialogService } from '@/services/dialogService'
import { useFirebaseAuthService } from '@/services/firebaseAuthService' import { useFirebaseAuthService } from '@/services/firebaseAuthService'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
const authStore = useFirebaseAuthStore() const { userDisplayName, userEmail, userPhotoUrl } = useCurrentUser()
const authService = useFirebaseAuthService() const authService = useFirebaseAuthService()
const dialogService = useDialogService() const dialogService = useDialogService()
const user = computed(() => authStore.currentUser)
const handleOpenUserSettings = () => { const handleOpenUserSettings = () => {
dialogService.showSettingsDialog('user') dialogService.showSettingsDialog('user')
} }

View File

@@ -0,0 +1,101 @@
import { computed } from 'vue'
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
import { useCommandStore } from '@/stores/commandStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
export const useCurrentUser = () => {
const authStore = useFirebaseAuthStore()
const commandStore = useCommandStore()
const apiKeyStore = useApiKeyAuthStore()
const firebaseUser = computed(() => authStore.currentUser)
const isApiKeyLogin = computed(() => apiKeyStore.isAuthenticated)
const isLoggedIn = computed(
() => !!isApiKeyLogin.value || firebaseUser.value !== null
)
const userDisplayName = computed(() => {
if (isApiKeyLogin.value) {
return apiKeyStore.currentUser?.name
}
return firebaseUser.value?.displayName
})
const userEmail = computed(() => {
if (isApiKeyLogin.value) {
return apiKeyStore.currentUser?.email
}
return firebaseUser.value?.email
})
const providerName = computed(() => {
if (isApiKeyLogin.value) {
return 'Comfy API Key'
}
const providerId = firebaseUser.value?.providerData[0]?.providerId
if (providerId?.includes('google')) {
return 'Google'
}
if (providerId?.includes('github')) {
return 'GitHub'
}
return providerId
})
const providerIcon = computed(() => {
if (isApiKeyLogin.value) {
return 'pi pi-key'
}
const providerId = firebaseUser.value?.providerData[0]?.providerId
if (providerId?.includes('google')) {
return 'pi pi-google'
}
if (providerId?.includes('github')) {
return 'pi pi-github'
}
return 'pi pi-user'
})
const isEmailProvider = computed(() => {
if (isApiKeyLogin.value) {
return false
}
const providerId = firebaseUser.value?.providerData[0]?.providerId
return providerId === 'password'
})
const userPhotoUrl = computed(() => {
if (isApiKeyLogin.value) return null
return firebaseUser.value?.photoURL
})
const handleSignOut = async () => {
if (isApiKeyLogin.value) {
await apiKeyStore.clearStoredApiKey()
} else {
await commandStore.execute('Comfy.User.SignOut')
}
}
const handleSignIn = async () => {
await commandStore.execute('Comfy.User.OpenSignInDialog')
}
return {
loading: authStore.loading,
isLoggedIn,
isApiKeyLogin,
isEmailProvider,
userDisplayName,
userEmail,
userPhotoUrl,
providerName,
providerIcon,
handleSignOut,
handleSignIn
}
}

View File

@@ -7,13 +7,14 @@ import {
} from 'vue' } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { SettingTreeNode, useSettingStore } from '@/stores/settingStore' import { SettingTreeNode, useSettingStore } from '@/stores/settingStore'
import type { SettingParams } from '@/types/settingTypes' import type { SettingParams } from '@/types/settingTypes'
import { isElectron } from '@/utils/envUtil' import { isElectron } from '@/utils/envUtil'
import { normalizeI18nKey } from '@/utils/formatUtil' import { normalizeI18nKey } from '@/utils/formatUtil'
import { buildTree } from '@/utils/treeUtil' import { buildTree } from '@/utils/treeUtil'
import { useCurrentUser } from '../auth/useCurrentUser'
interface SettingPanelItem { interface SettingPanelItem {
node: SettingTreeNode node: SettingTreeNode
component: Component component: Component
@@ -29,7 +30,7 @@ export function useSettingUI(
| 'credits' | 'credits'
) { ) {
const { t } = useI18n() const { t } = useI18n()
const firebaseAuthStore = useFirebaseAuthStore() const { isLoggedIn } = useCurrentUser()
const settingStore = useSettingStore() const settingStore = useSettingStore()
const activeCategory = ref<SettingTreeNode | null>(null) const activeCategory = ref<SettingTreeNode | null>(null)
@@ -165,7 +166,7 @@ export function useSettingUI(
label: 'Account', label: 'Account',
children: [ children: [
userPanel.node, userPanel.node,
...(firebaseAuthStore.isAuthenticated ? [creditsPanel.node] : []) ...(isLoggedIn.value ? [creditsPanel.node] : [])
].map(translateCategory) ].map(translateCategory)
}, },
// Normal settings stored in the settingStore // Normal settings stored in the settingStore

View File

@@ -1308,7 +1308,8 @@
"success": "Login successful", "success": "Login successful",
"failed": "Login failed", "failed": "Login failed",
"insecureContextWarning": "This connection is insecure (HTTP) - your credentials may be intercepted by attackers if you proceed to login.", "insecureContextWarning": "This connection is insecure (HTTP) - your credentials may be intercepted by attackers if you proceed to login.",
"questionsContactPrefix": "Questions? Contact us at" "questionsContactPrefix": "Questions? Contact us at",
"noAssociatedUser": "There is no Comfy user associated with the provided API key"
}, },
"signup": { "signup": {
"title": "Create an account", "title": "Create an account",

View File

@@ -61,6 +61,7 @@
"loginWithGithub": "Iniciar sesión con Github", "loginWithGithub": "Iniciar sesión con Github",
"loginWithGoogle": "Iniciar sesión con Google", "loginWithGoogle": "Iniciar sesión con Google",
"newUser": "¿Eres nuevo aquí?", "newUser": "¿Eres nuevo aquí?",
"noAssociatedUser": "No hay ningún usuario de Comfy asociado con la clave API proporcionada",
"orContinueWith": "O continuar con", "orContinueWith": "O continuar con",
"passwordLabel": "Contraseña", "passwordLabel": "Contraseña",
"passwordPlaceholder": "Ingresa tu contraseña", "passwordPlaceholder": "Ingresa tu contraseña",

View File

@@ -61,6 +61,7 @@
"loginWithGithub": "Se connecter avec Github", "loginWithGithub": "Se connecter avec Github",
"loginWithGoogle": "Se connecter avec Google", "loginWithGoogle": "Se connecter avec Google",
"newUser": "Nouveau ici?", "newUser": "Nouveau ici?",
"noAssociatedUser": "Aucun utilisateur Comfy n'est associé à la clé API fournie",
"orContinueWith": "Ou continuer avec", "orContinueWith": "Ou continuer avec",
"passwordLabel": "Mot de passe", "passwordLabel": "Mot de passe",
"passwordPlaceholder": "Entrez votre mot de passe", "passwordPlaceholder": "Entrez votre mot de passe",

View File

@@ -61,6 +61,7 @@
"loginWithGithub": "Githubでログイン", "loginWithGithub": "Githubでログイン",
"loginWithGoogle": "Googleでログイン", "loginWithGoogle": "Googleでログイン",
"newUser": "新規ユーザーですか?", "newUser": "新規ユーザーですか?",
"noAssociatedUser": "指定されたAPIキーに関連付けられたComfyユーザーが存在しません",
"orContinueWith": "または以下で続ける", "orContinueWith": "または以下で続ける",
"passwordLabel": "パスワード", "passwordLabel": "パスワード",
"passwordPlaceholder": "パスワードを入力してください", "passwordPlaceholder": "パスワードを入力してください",

View File

@@ -61,6 +61,7 @@
"loginWithGithub": "Github로 로그인", "loginWithGithub": "Github로 로그인",
"loginWithGoogle": "구글로 로그인", "loginWithGoogle": "구글로 로그인",
"newUser": "처음이신가요?", "newUser": "처음이신가요?",
"noAssociatedUser": "제공된 API 키와 연결된 Comfy 사용자가 없습니다",
"orContinueWith": "또는 다음으로 계속", "orContinueWith": "또는 다음으로 계속",
"passwordLabel": "비밀번호", "passwordLabel": "비밀번호",
"passwordPlaceholder": "비밀번호를 입력하세요", "passwordPlaceholder": "비밀번호를 입력하세요",

View File

@@ -61,6 +61,7 @@
"loginWithGithub": "Войти через Github", "loginWithGithub": "Войти через Github",
"loginWithGoogle": "Войти через Google", "loginWithGoogle": "Войти через Google",
"newUser": "Вы здесь впервые?", "newUser": "Вы здесь впервые?",
"noAssociatedUser": "С предоставленным API-ключом не связан ни один пользователь Comfy",
"orContinueWith": "Или продолжить с", "orContinueWith": "Или продолжить с",
"passwordLabel": "Пароль", "passwordLabel": "Пароль",
"passwordPlaceholder": "Введите ваш пароль", "passwordPlaceholder": "Введите ваш пароль",

View File

@@ -61,6 +61,7 @@
"loginWithGithub": "使用Github登录", "loginWithGithub": "使用Github登录",
"loginWithGoogle": "使用Google登录", "loginWithGoogle": "使用Google登录",
"newUser": "新来的?", "newUser": "新来的?",
"noAssociatedUser": "所提供的 API 密钥未关联任何 Comfy 用户",
"orContinueWith": "或者继续使用", "orContinueWith": "或者继续使用",
"passwordLabel": "密码", "passwordLabel": "密码",
"passwordPlaceholder": "输入您的密码", "passwordPlaceholder": "输入您的密码",

View File

@@ -1,18 +1,51 @@
import { useLocalStorage } from '@vueuse/core' import { useLocalStorage } from '@vueuse/core'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { computed } from 'vue' import { computed, ref, watch } from 'vue'
import { useErrorHandling } from '@/composables/useErrorHandling' import { useErrorHandling } from '@/composables/useErrorHandling'
import { t } from '@/i18n' import { t } from '@/i18n'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { useToastStore } from '@/stores/toastStore' import { useToastStore } from '@/stores/toastStore'
import { ApiKeyAuthHeader } from '@/types/authTypes'
import { operations } from '@/types/comfyRegistryTypes'
type ComfyApiUser =
operations['createCustomer']['responses']['201']['content']['application/json']
const STORAGE_KEY = 'comfy_api_key' const STORAGE_KEY = 'comfy_api_key'
export const useApiKeyAuthStore = defineStore('apiKeyAuth', () => { export const useApiKeyAuthStore = defineStore('apiKeyAuth', () => {
const firebaseAuthStore = useFirebaseAuthStore()
const apiKey = useLocalStorage<string | null>(STORAGE_KEY, null) const apiKey = useLocalStorage<string | null>(STORAGE_KEY, null)
const toastStore = useToastStore() const toastStore = useToastStore()
const { wrapWithErrorHandlingAsync, toastErrorHandler } = useErrorHandling() const { wrapWithErrorHandlingAsync, toastErrorHandler } = useErrorHandling()
const currentUser = ref<ComfyApiUser | null>(null)
const isAuthenticated = computed(() => !!currentUser.value)
const initializeUserFromApiKey = async () => {
const createCustomerResponse = await firebaseAuthStore.createCustomer()
if (!createCustomerResponse) {
apiKey.value = null
throw new Error(t('auth.login.noAssociatedUser'))
}
currentUser.value = createCustomerResponse
}
watch(
apiKey,
() => {
if (apiKey.value) {
// IF API key is set, initialize user
void initializeUserFromApiKey()
} else {
// IF API key is cleared, clear user
currentUser.value = null
}
},
{ immediate: true }
)
const reportError = (error: unknown) => { const reportError = (error: unknown) => {
if (error instanceof Error && error.message === 'STORAGE_FAILED') { if (error instanceof Error && error.message === 'STORAGE_FAILED') {
toastStore.add({ toastStore.add({
@@ -49,7 +82,11 @@ export const useApiKeyAuthStore = defineStore('apiKeyAuth', () => {
const getApiKey = () => apiKey.value const getApiKey = () => apiKey.value
const getAuthHeader = () => { /**
* Retrieves the appropriate authentication header for API requests if an
* API key is available, otherwise returns null.
*/
const getAuthHeader = (): ApiKeyAuthHeader | null => {
const comfyOrgApiKey = getApiKey() const comfyOrgApiKey = getApiKey()
if (comfyOrgApiKey) { if (comfyOrgApiKey) {
return { return {
@@ -60,7 +97,11 @@ export const useApiKeyAuthStore = defineStore('apiKeyAuth', () => {
} }
return { return {
hasApiKey: computed(() => !!apiKey.value), // State
currentUser,
isAuthenticated,
// Actions
storeApiKey, storeApiKey,
clearStoredApiKey, clearStoredApiKey,
getAuthHeader, getAuthHeader,

View File

@@ -107,6 +107,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
* - null if neither authentication method is available * - null if neither authentication method is available
*/ */
const getAuthHeader = async (): Promise<AuthHeader | null> => { const getAuthHeader = async (): Promise<AuthHeader | null> => {
// If available, set header with JWT used to identify the user to Firebase service
const token = await getIdToken() const token = await getIdToken()
if (token) { if (token) {
return { return {
@@ -114,8 +115,8 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
} }
} }
const apiKeyStore = useApiKeyAuthStore() // If not authenticated with Firebase, try falling back to API key if available
return apiKeyStore.getAuthHeader() return useApiKeyAuthStore().getAuthHeader()
} }
const fetchBalance = async (): Promise<GetCustomerBalanceResponse | null> => { const fetchBalance = async (): Promise<GetCustomerBalanceResponse | null> => {
@@ -356,6 +357,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
login, login,
register, register,
logout, logout,
createCustomer,
getIdToken, getIdToken,
loginWithGoogle, loginWithGoogle,
loginWithGithub, loginWithGithub,