mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 22:09:37 +00:00
Merge branch 'main' into version-bump-1.43.14
This commit is contained in:
107
.github/workflows/api-update-registry-api-types.yaml
vendored
107
.github/workflows/api-update-registry-api-types.yaml
vendored
@@ -1,107 +0,0 @@
|
||||
# Description: When upstream comfy-api is updated, click dispatch to update the TypeScript type definitions in this repo
|
||||
name: 'Api: Update Registry API Types'
|
||||
|
||||
on:
|
||||
# Manual trigger
|
||||
workflow_dispatch:
|
||||
|
||||
# Triggered from comfy-api repo
|
||||
repository_dispatch:
|
||||
types: [comfy-api-updated]
|
||||
|
||||
jobs:
|
||||
update-registry-types:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Checkout comfy-api repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: Comfy-Org/comfy-api
|
||||
path: comfy-api
|
||||
token: ${{ secrets.COMFY_API_PAT }}
|
||||
clean: true
|
||||
|
||||
- name: Get API commit information
|
||||
id: api-info
|
||||
run: |
|
||||
cd comfy-api
|
||||
API_COMMIT=$(git rev-parse --short HEAD)
|
||||
echo "commit=${API_COMMIT}" >> $GITHUB_OUTPUT
|
||||
cd ..
|
||||
|
||||
- name: Generate API types
|
||||
run: |
|
||||
echo "Generating TypeScript types from comfy-api@${{ steps.api-info.outputs.commit }}..."
|
||||
mkdir -p ./packages/registry-types/src
|
||||
pnpm dlx openapi-typescript ./comfy-api/openapi.yml --output ./packages/registry-types/src/comfyRegistryTypes.ts
|
||||
|
||||
- name: Validate generated types
|
||||
run: |
|
||||
if [ ! -f ./packages/registry-types/src/comfyRegistryTypes.ts ]; then
|
||||
echo "Error: Types file was not generated."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if file is not empty
|
||||
if [ ! -s ./packages/registry-types/src/comfyRegistryTypes.ts ]; then
|
||||
echo "Error: Generated types file is empty."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Lint generated types
|
||||
run: |
|
||||
echo "Linting generated Comfy Registry API types..."
|
||||
pnpm lint:fix:no-cache -- ./packages/registry-types/src/comfyRegistryTypes.ts
|
||||
|
||||
- name: Check for changes
|
||||
id: check-changes
|
||||
run: |
|
||||
if [[ -z $(git status --porcelain ./packages/registry-types/src/comfyRegistryTypes.ts) ]]; then
|
||||
echo "No changes to Comfy Registry API types detected."
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
else
|
||||
echo "Changes detected in Comfy Registry API types."
|
||||
echo "changed=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.check-changes.outputs.changed == 'true'
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
with:
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
commit-message: '[chore] Update Comfy Registry API types from comfy-api@${{ steps.api-info.outputs.commit }}'
|
||||
title: '[chore] Update Comfy Registry API types from comfy-api@${{ steps.api-info.outputs.commit }}'
|
||||
body: |
|
||||
## Automated API Type Update
|
||||
|
||||
This PR updates the Comfy Registry API types from the latest comfy-api OpenAPI specification.
|
||||
|
||||
- API commit: ${{ steps.api-info.outputs.commit }}
|
||||
- Generated on: ${{ github.event.repository.updated_at }}
|
||||
|
||||
These types are automatically generated using openapi-typescript.
|
||||
branch: update-registry-types-${{ steps.api-info.outputs.commit }}
|
||||
base: main
|
||||
labels: CNR
|
||||
delete-branch: true
|
||||
add-paths: |
|
||||
packages/registry-types/src/comfyRegistryTypes.ts
|
||||
33
.github/workflows/ci-website-build.yaml
vendored
Normal file
33
.github/workflows/ci-website-build.yaml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
# Description: Build and validate the marketing website (apps/website)
|
||||
name: 'CI: Website Build'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master, website/*]
|
||||
paths:
|
||||
- 'apps/website/**'
|
||||
- 'packages/design-system/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
pull_request:
|
||||
branches-ignore: [wip/*, draft/*, temp/*]
|
||||
paths:
|
||||
- 'apps/website/**'
|
||||
- 'packages/design-system/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
|
||||
- name: Build website
|
||||
run: pnpm --filter @comfyorg/website build
|
||||
@@ -1,11 +1,12 @@
|
||||
import { defineConfig } from 'astro/config'
|
||||
import sitemap from '@astrojs/sitemap'
|
||||
import vue from '@astrojs/vue'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
export default defineConfig({
|
||||
site: 'https://comfy.org',
|
||||
output: 'static',
|
||||
integrations: [vue()],
|
||||
integrations: [vue(), sitemap()],
|
||||
vite: {
|
||||
plugins: [tailwindcss()]
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/sitemap": "catalog:",
|
||||
"@comfyorg/design-system": "workspace:*",
|
||||
"@vercel/analytics": "catalog:",
|
||||
"vue": "catalog:"
|
||||
|
||||
4
apps/website/public/robots.txt
Normal file
4
apps/website/public/robots.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://comfy.org/sitemap-index.xml
|
||||
@@ -96,6 +96,10 @@ const socials = [
|
||||
v-for="link in column.links"
|
||||
:key="link.href"
|
||||
:href="link.href"
|
||||
:target="link.href.startsWith('http') ? '_blank' : undefined"
|
||||
:rel="
|
||||
link.href.startsWith('http') ? 'noopener noreferrer' : undefined
|
||||
"
|
||||
class="text-sm text-smoke-700 transition-colors hover:text-white"
|
||||
>
|
||||
{{ link.label }}
|
||||
|
||||
@@ -7,12 +7,14 @@ interface Props {
|
||||
title: string
|
||||
description?: string
|
||||
ogImage?: string
|
||||
noindex?: boolean
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
description = 'Comfy is the AI creation engine for visual professionals who demand control.',
|
||||
ogImage = '/og-default.png',
|
||||
noindex = false,
|
||||
} = Astro.props
|
||||
|
||||
const siteBase = Astro.site ?? 'https://comfy.org'
|
||||
@@ -21,6 +23,29 @@ const ogImageURL = new URL(ogImage, siteBase)
|
||||
const locale = Astro.currentLocale ?? 'en'
|
||||
const gtmId = 'GTM-NP9JM6K7'
|
||||
const gtmEnabled = import.meta.env.PROD
|
||||
|
||||
const organizationJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Organization',
|
||||
name: 'Comfy Org',
|
||||
url: 'https://comfy.org',
|
||||
logo: 'https://comfy.org/favicon.svg',
|
||||
sameAs: [
|
||||
'https://github.com/comfyanonymous/ComfyUI',
|
||||
'https://discord.gg/comfyorg',
|
||||
'https://x.com/comaboratory',
|
||||
'https://reddit.com/r/comfyui',
|
||||
'https://linkedin.com/company/comfyorg',
|
||||
'https://instagram.com/comfyorg',
|
||||
],
|
||||
}
|
||||
|
||||
const websiteJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebSite',
|
||||
name: 'Comfy',
|
||||
url: 'https://comfy.org',
|
||||
}
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
@@ -29,26 +54,36 @@ const gtmEnabled = import.meta.env.PROD
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content={description} />
|
||||
{noindex && <meta name="robots" content="noindex, nofollow" />}
|
||||
<title>{title}</title>
|
||||
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="canonical" href={canonicalURL.href} />
|
||||
<link rel="preconnect" href="https://www.googletagmanager.com" />
|
||||
<link rel="dns-prefetch" href="https://www.googletagmanager.com" />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={ogImageURL.href} />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:url" content={canonicalURL.href} />
|
||||
<meta property="og:locale" content={locale} />
|
||||
<meta property="og:site_name" content="Comfy" />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@comaboratory" />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={ogImageURL.href} />
|
||||
|
||||
<!-- Structured Data -->
|
||||
<script type="application/ld+json" set:html={JSON.stringify(organizationJsonLd)} />
|
||||
<script type="application/ld+json" set:html={JSON.stringify(websiteJsonLd)} />
|
||||
|
||||
<!-- Google Tag Manager -->
|
||||
{gtmEnabled && (
|
||||
<script is:inline define:vars={{ gtmId }}>
|
||||
|
||||
@@ -7,6 +7,7 @@ import SiteFooter from '../components/SiteFooter.vue'
|
||||
<BaseLayout
|
||||
title="Privacy Policy — Comfy"
|
||||
description="Comfy privacy policy. Learn how we collect, use, and protect your personal information."
|
||||
noindex
|
||||
>
|
||||
<SiteNav client:load />
|
||||
<main class="mx-auto max-w-3xl px-6 py-24">
|
||||
|
||||
@@ -7,6 +7,7 @@ import SiteFooter from '../components/SiteFooter.vue'
|
||||
<BaseLayout
|
||||
title="Terms of Service — Comfy"
|
||||
description="Terms of Service for ComfyUI and related Comfy services."
|
||||
noindex
|
||||
>
|
||||
<SiteNav client:load />
|
||||
<main class="mx-auto max-w-3xl px-6 py-24 sm:py-32">
|
||||
|
||||
@@ -7,6 +7,7 @@ import SiteFooter from '../../components/SiteFooter.vue'
|
||||
<BaseLayout
|
||||
title="隐私政策 — Comfy"
|
||||
description="Comfy 隐私政策。了解我们如何收集、使用和保护您的个人信息。"
|
||||
noindex
|
||||
>
|
||||
<SiteNav client:load />
|
||||
<main class="mx-auto max-w-3xl px-6 py-24">
|
||||
|
||||
@@ -7,6 +7,7 @@ import SiteFooter from '../../components/SiteFooter.vue'
|
||||
<BaseLayout
|
||||
title="服务条款 — Comfy"
|
||||
description="ComfyUI 及相关 Comfy 服务的服务条款。"
|
||||
noindex
|
||||
>
|
||||
<SiteNav client:load />
|
||||
<main class="mx-auto max-w-3xl px-6 py-24 sm:py-32">
|
||||
|
||||
26
apps/website/vercel.json
Normal file
26
apps/website/vercel.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||
"buildCommand": "pnpm --filter @comfyorg/website build",
|
||||
"outputDirectory": "apps/website/dist",
|
||||
"installCommand": "pnpm install --frozen-lockfile",
|
||||
"framework": null,
|
||||
"redirects": [
|
||||
{
|
||||
"source": "/pricing",
|
||||
"destination": "/cloud/pricing",
|
||||
"permanent": true
|
||||
},
|
||||
{
|
||||
"source": "/enterprise",
|
||||
"destination": "/cloud/enterprise",
|
||||
"permanent": true
|
||||
},
|
||||
{
|
||||
"source": "/blog",
|
||||
"destination": "https://blog.comfy.org/",
|
||||
"permanent": true
|
||||
},
|
||||
{ "source": "/contact", "destination": "/about", "permanent": true },
|
||||
{ "source": "/press", "destination": "/about", "permanent": true }
|
||||
]
|
||||
}
|
||||
44
pnpm-lock.yaml
generated
44
pnpm-lock.yaml
generated
@@ -9,6 +9,9 @@ catalogs:
|
||||
'@alloc/quick-lru':
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0
|
||||
'@astrojs/sitemap':
|
||||
specifier: ^3.7.1
|
||||
version: 3.7.1
|
||||
'@astrojs/vue':
|
||||
specifier: ^5.0.0
|
||||
version: 5.1.4
|
||||
@@ -910,6 +913,9 @@ importers:
|
||||
|
||||
apps/website:
|
||||
dependencies:
|
||||
'@astrojs/sitemap':
|
||||
specifier: 'catalog:'
|
||||
version: 3.7.1
|
||||
'@comfyorg/design-system':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/design-system
|
||||
@@ -1095,6 +1101,9 @@ packages:
|
||||
resolution: {integrity: sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ==}
|
||||
engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0}
|
||||
|
||||
'@astrojs/sitemap@3.7.1':
|
||||
resolution: {integrity: sha512-IzQqdTeskaMX+QDZCzMuJIp8A8C1vgzMBp/NmHNnadepHYNHcxQdGLQZYfkbd2EbRXUfOS+UDIKx8sKg0oWVdw==}
|
||||
|
||||
'@astrojs/telemetry@3.3.0':
|
||||
resolution: {integrity: sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==}
|
||||
engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0}
|
||||
@@ -4394,6 +4403,9 @@ packages:
|
||||
'@types/react@19.1.9':
|
||||
resolution: {integrity: sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==}
|
||||
|
||||
'@types/sax@1.2.7':
|
||||
resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==}
|
||||
|
||||
'@types/semver@7.7.0':
|
||||
resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==}
|
||||
|
||||
@@ -5091,6 +5103,9 @@ packages:
|
||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
arg@5.0.2:
|
||||
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
|
||||
|
||||
argparse@1.0.10:
|
||||
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
|
||||
|
||||
@@ -8698,6 +8713,11 @@ packages:
|
||||
sisteransi@1.0.5:
|
||||
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||
|
||||
sitemap@9.0.1:
|
||||
resolution: {integrity: sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ==}
|
||||
engines: {node: '>=20.19.5', npm: '>=10.8.2'}
|
||||
hasBin: true
|
||||
|
||||
slash@3.0.0:
|
||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -8779,6 +8799,9 @@ packages:
|
||||
prettier:
|
||||
optional: true
|
||||
|
||||
stream-replace-string@2.0.0:
|
||||
resolution: {integrity: sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==}
|
||||
|
||||
string-argv@0.3.2:
|
||||
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
|
||||
engines: {node: '>=0.6.19'}
|
||||
@@ -10090,6 +10113,12 @@ snapshots:
|
||||
dependencies:
|
||||
prismjs: 1.30.0
|
||||
|
||||
'@astrojs/sitemap@3.7.1':
|
||||
dependencies:
|
||||
sitemap: 9.0.1
|
||||
stream-replace-string: 2.0.0
|
||||
zod: 4.3.6
|
||||
|
||||
'@astrojs/telemetry@3.3.0':
|
||||
dependencies:
|
||||
ci-info: 4.4.0
|
||||
@@ -13442,6 +13471,10 @@ snapshots:
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
|
||||
'@types/sax@1.2.7':
|
||||
dependencies:
|
||||
'@types/node': 25.0.3
|
||||
|
||||
'@types/semver@7.7.0': {}
|
||||
|
||||
'@types/stats.js@0.17.3': {}
|
||||
@@ -14248,6 +14281,8 @@ snapshots:
|
||||
normalize-path: 3.0.0
|
||||
picomatch: 2.3.1
|
||||
|
||||
arg@5.0.2: {}
|
||||
|
||||
argparse@1.0.10:
|
||||
dependencies:
|
||||
sprintf-js: 1.0.3
|
||||
@@ -18814,6 +18849,13 @@ snapshots:
|
||||
|
||||
sisteransi@1.0.5: {}
|
||||
|
||||
sitemap@9.0.1:
|
||||
dependencies:
|
||||
'@types/node': 24.10.4
|
||||
'@types/sax': 1.2.7
|
||||
arg: 5.0.2
|
||||
sax: 1.4.4
|
||||
|
||||
slash@3.0.0: {}
|
||||
|
||||
slice-ansi@4.0.0:
|
||||
@@ -18902,6 +18944,8 @@ snapshots:
|
||||
- react-dom
|
||||
- utf-8-validate
|
||||
|
||||
stream-replace-string@2.0.0: {}
|
||||
|
||||
string-argv@0.3.2: {}
|
||||
|
||||
string-width@4.2.3:
|
||||
|
||||
@@ -4,6 +4,7 @@ packages:
|
||||
|
||||
catalog:
|
||||
'@alloc/quick-lru': ^5.2.0
|
||||
'@astrojs/sitemap': ^3.7.1
|
||||
'@astrojs/vue': ^5.0.0
|
||||
'@comfyorg/comfyui-electron-types': 0.6.2
|
||||
'@eslint/js': ^9.39.1
|
||||
|
||||
@@ -343,6 +343,10 @@ export const useWorkspaceAuthStore = defineStore('workspaceAuth', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function getWorkspaceToken(): string | undefined {
|
||||
return workspaceToken.value ?? undefined
|
||||
}
|
||||
|
||||
function clearWorkspaceContext(): void {
|
||||
// Increment request ID to invalidate any in-flight stale refresh operations
|
||||
refreshRequestId++
|
||||
@@ -370,6 +374,7 @@ export const useWorkspaceAuthStore = defineStore('workspaceAuth', () => {
|
||||
switchWorkspace,
|
||||
refreshToken,
|
||||
getWorkspaceAuthHeader,
|
||||
getWorkspaceToken,
|
||||
clearWorkspaceContext
|
||||
}
|
||||
})
|
||||
|
||||
211
src/stores/__tests__/authTokenPriority.test.ts
Normal file
211
src/stores/__tests__/authTokenPriority.test.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import type { User } from 'firebase/auth'
|
||||
import * as firebaseAuth from 'firebase/auth'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import type { Mock } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import * as vuefire from 'vuefire'
|
||||
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
|
||||
const { mockFeatureFlags } = vi.hoisted(() => ({
|
||||
mockFeatureFlags: {
|
||||
teamWorkspacesEnabled: false
|
||||
}
|
||||
}))
|
||||
|
||||
const { mockDistributionTypes } = vi.hoisted(() => ({
|
||||
mockDistributionTypes: {
|
||||
isCloud: true,
|
||||
isDesktop: true
|
||||
}
|
||||
}))
|
||||
|
||||
const mockWorkspaceAuthHeader = vi.fn().mockReturnValue(null)
|
||||
const mockGetWorkspaceToken = vi.fn().mockReturnValue(undefined)
|
||||
const mockClearWorkspaceContext = vi.fn()
|
||||
|
||||
vi.mock('@/platform/workspace/stores/workspaceAuthStore', () => ({
|
||||
useWorkspaceAuthStore: () => ({
|
||||
getWorkspaceAuthHeader: mockWorkspaceAuthHeader,
|
||||
getWorkspaceToken: mockGetWorkspaceToken,
|
||||
clearWorkspaceContext: mockClearWorkspaceContext
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useFeatureFlags', () => ({
|
||||
useFeatureFlags: () => ({
|
||||
flags: mockFeatureFlags
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('vuefire', () => ({
|
||||
useFirebaseAuth: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({ t: (key: string) => key }),
|
||||
createI18n: () => ({ global: { t: (key: string) => key } })
|
||||
}))
|
||||
|
||||
vi.mock('firebase/auth', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof firebaseAuth>()
|
||||
return {
|
||||
...actual,
|
||||
signInWithEmailAndPassword: vi.fn(),
|
||||
createUserWithEmailAndPassword: vi.fn(),
|
||||
signOut: vi.fn(),
|
||||
onAuthStateChanged: vi.fn(),
|
||||
onIdTokenChanged: vi.fn(),
|
||||
signInWithPopup: vi.fn(),
|
||||
GoogleAuthProvider: class {
|
||||
addScope = vi.fn()
|
||||
setCustomParameters = vi.fn()
|
||||
},
|
||||
GithubAuthProvider: class {
|
||||
addScope = vi.fn()
|
||||
setCustomParameters = vi.fn()
|
||||
},
|
||||
getAdditionalUserInfo: vi.fn(),
|
||||
setPersistence: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/platform/telemetry', () => ({
|
||||
useTelemetry: () => ({ trackAuth: vi.fn() })
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/toastStore', () => ({
|
||||
useToastStore: () => ({ add: vi.fn() })
|
||||
}))
|
||||
|
||||
vi.mock('@/services/dialogService')
|
||||
vi.mock('@/platform/distribution/types', () => mockDistributionTypes)
|
||||
|
||||
const mockApiKeyGetAuthHeader = vi.fn().mockReturnValue(null)
|
||||
vi.mock('@/stores/apiKeyAuthStore', () => ({
|
||||
useApiKeyAuthStore: () => ({
|
||||
getAuthHeader: mockApiKeyGetAuthHeader,
|
||||
getApiKey: vi.fn(),
|
||||
currentUser: null,
|
||||
isAuthenticated: false,
|
||||
storeApiKey: vi.fn(),
|
||||
clearStoredApiKey: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
type MockUser = Omit<User, 'getIdToken'> & { getIdToken: Mock }
|
||||
|
||||
describe('auth token priority chain', () => {
|
||||
let store: ReturnType<typeof useAuthStore>
|
||||
let authStateCallback: (user: User | null) => void
|
||||
|
||||
const mockAuth: Record<string, unknown> = {}
|
||||
|
||||
const mockUser: MockUser = {
|
||||
uid: 'test-user-id',
|
||||
email: 'test@example.com',
|
||||
getIdToken: vi.fn().mockResolvedValue('firebase-token')
|
||||
} as Partial<User> as MockUser
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
|
||||
mockFeatureFlags.teamWorkspacesEnabled = false
|
||||
mockWorkspaceAuthHeader.mockReturnValue(null)
|
||||
mockGetWorkspaceToken.mockReturnValue(undefined)
|
||||
mockApiKeyGetAuthHeader.mockReturnValue(null)
|
||||
mockUser.getIdToken.mockResolvedValue('firebase-token')
|
||||
|
||||
vi.mocked(vuefire.useFirebaseAuth).mockReturnValue(
|
||||
mockAuth as unknown as ReturnType<typeof vuefire.useFirebaseAuth>
|
||||
)
|
||||
|
||||
vi.mocked(firebaseAuth.onAuthStateChanged).mockImplementation(
|
||||
(_, callback) => {
|
||||
authStateCallback = callback as (user: User | null) => void
|
||||
;(callback as (user: User | null) => void)(mockUser)
|
||||
return vi.fn()
|
||||
}
|
||||
)
|
||||
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
store = useAuthStore()
|
||||
})
|
||||
|
||||
describe('getAuthHeader priority', () => {
|
||||
it('returns workspace auth header when workspace is active and feature enabled', async () => {
|
||||
mockFeatureFlags.teamWorkspacesEnabled = true
|
||||
mockWorkspaceAuthHeader.mockReturnValue({
|
||||
Authorization: 'Bearer workspace-token'
|
||||
})
|
||||
|
||||
const header = await store.getAuthHeader()
|
||||
|
||||
expect(header).toEqual({
|
||||
Authorization: 'Bearer workspace-token'
|
||||
})
|
||||
})
|
||||
|
||||
it('returns Firebase token when workspace is not active but user is authenticated', async () => {
|
||||
mockFeatureFlags.teamWorkspacesEnabled = true
|
||||
mockWorkspaceAuthHeader.mockReturnValue(null)
|
||||
|
||||
const header = await store.getAuthHeader()
|
||||
|
||||
expect(header).toEqual({
|
||||
Authorization: 'Bearer firebase-token'
|
||||
})
|
||||
})
|
||||
|
||||
it('returns API key when neither workspace nor Firebase are available', async () => {
|
||||
authStateCallback(null)
|
||||
mockApiKeyGetAuthHeader.mockReturnValue({ 'X-API-KEY': 'test-key' })
|
||||
|
||||
const header = await store.getAuthHeader()
|
||||
|
||||
expect(header).toEqual({ 'X-API-KEY': 'test-key' })
|
||||
})
|
||||
|
||||
it('returns null when no auth method is available', async () => {
|
||||
authStateCallback(null)
|
||||
|
||||
const header = await store.getAuthHeader()
|
||||
|
||||
expect(header).toBeNull()
|
||||
})
|
||||
|
||||
it('skips workspace header when team_workspaces feature is disabled', async () => {
|
||||
mockFeatureFlags.teamWorkspacesEnabled = false
|
||||
mockWorkspaceAuthHeader.mockReturnValue({
|
||||
Authorization: 'Bearer workspace-token'
|
||||
})
|
||||
|
||||
const header = await store.getAuthHeader()
|
||||
|
||||
expect(header).toEqual({
|
||||
Authorization: 'Bearer firebase-token'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAuthToken priority', () => {
|
||||
it('returns workspace token when workspace is active and feature enabled', async () => {
|
||||
mockFeatureFlags.teamWorkspacesEnabled = true
|
||||
mockGetWorkspaceToken.mockReturnValue('workspace-raw-token')
|
||||
|
||||
const token = await store.getAuthToken()
|
||||
|
||||
expect(token).toBe('workspace-raw-token')
|
||||
})
|
||||
|
||||
it('returns Firebase token when workspace token is not available', async () => {
|
||||
mockFeatureFlags.teamWorkspacesEnabled = true
|
||||
mockGetWorkspaceToken.mockReturnValue(undefined)
|
||||
|
||||
const token = await store.getAuthToken()
|
||||
|
||||
expect(token).toBe('firebase-token')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -22,10 +22,10 @@ import { useFirebaseAuth } from 'vuefire'
|
||||
|
||||
import { getComfyApiBaseUrl } from '@/config/comfyApi'
|
||||
import { t } from '@/i18n'
|
||||
import { WORKSPACE_STORAGE_KEYS } from '@/platform/workspace/workspaceConstants'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useWorkspaceAuthStore } from '@/platform/workspace/stores/workspaceAuthStore'
|
||||
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
|
||||
import type { AuthHeader } from '@/types/authTypes'
|
||||
import type { operations } from '@/types/comfyRegistryTypes'
|
||||
@@ -110,15 +110,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
isInitialized.value = true
|
||||
if (user === null) {
|
||||
lastTokenUserId.value = null
|
||||
|
||||
// Clear workspace sessionStorage on logout to prevent stale tokens
|
||||
try {
|
||||
sessionStorage.removeItem(WORKSPACE_STORAGE_KEYS.CURRENT_WORKSPACE)
|
||||
sessionStorage.removeItem(WORKSPACE_STORAGE_KEYS.TOKEN)
|
||||
sessionStorage.removeItem(WORKSPACE_STORAGE_KEYS.EXPIRES_AT)
|
||||
} catch {
|
||||
// Ignore sessionStorage errors (e.g., in private browsing mode)
|
||||
}
|
||||
useWorkspaceAuthStore().clearWorkspaceContext()
|
||||
}
|
||||
|
||||
// Reset balance when auth state changes
|
||||
@@ -175,21 +167,8 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
*/
|
||||
const getAuthHeader = async (): Promise<AuthHeader | null> => {
|
||||
if (flags.teamWorkspacesEnabled) {
|
||||
const workspaceToken = sessionStorage.getItem(
|
||||
WORKSPACE_STORAGE_KEYS.TOKEN
|
||||
)
|
||||
const expiresAt = sessionStorage.getItem(
|
||||
WORKSPACE_STORAGE_KEYS.EXPIRES_AT
|
||||
)
|
||||
|
||||
if (workspaceToken && expiresAt) {
|
||||
const expiryTime = parseInt(expiresAt, 10)
|
||||
if (Date.now() < expiryTime) {
|
||||
return {
|
||||
Authorization: `Bearer ${workspaceToken}`
|
||||
}
|
||||
}
|
||||
}
|
||||
const wsHeader = useWorkspaceAuthStore().getWorkspaceAuthHeader()
|
||||
if (wsHeader) return wsHeader
|
||||
}
|
||||
|
||||
const token = await getIdToken()
|
||||
@@ -218,19 +197,8 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
*/
|
||||
const getAuthToken = async (): Promise<string | undefined> => {
|
||||
if (flags.teamWorkspacesEnabled) {
|
||||
const workspaceToken = sessionStorage.getItem(
|
||||
WORKSPACE_STORAGE_KEYS.TOKEN
|
||||
)
|
||||
const expiresAt = sessionStorage.getItem(
|
||||
WORKSPACE_STORAGE_KEYS.EXPIRES_AT
|
||||
)
|
||||
|
||||
if (workspaceToken && expiresAt) {
|
||||
const expiryTime = parseInt(expiresAt, 10)
|
||||
if (Date.now() < expiryTime) {
|
||||
return workspaceToken
|
||||
}
|
||||
}
|
||||
const wsToken = useWorkspaceAuthStore().getWorkspaceToken()
|
||||
if (wsToken) return wsToken
|
||||
}
|
||||
|
||||
return await getIdToken()
|
||||
|
||||
Reference in New Issue
Block a user