feat: add per-tab workspace authentication infrastructure (#8073)

## Summary
Add workspace authentication composables and types for per-tab workspace
isolation. This infrastructure enables users to work in different
workspaces in different browser tabs.

## Changes
- **useWorkspaceAuth composable** - workspace token management
- Exchange Firebase token for workspace-scoped JWT via `POST
/api/auth/token`
  - Auto-refresh tokens 5 minutes before expiry
  - Per-tab sessionStorage caching
- **useWorkspaceSwitch composable** - workspace switching with unsaved
changes confirmation
- **WorkspaceWithRole/WorkspaceTokenResponse types** - aligned with
backend API
- **firebaseAuthStore.getAuthHeader()** - prioritizes workspace tokens
over Firebase tokens
- **useSessionCookie** - uses Firebase token directly (getIdToken())
since getAuthHeader() now returns workspace token

## Backend Dependency
- `POST /api/auth/token` - exchange Firebase token for workspace token
- `GET /api/workspaces` - list user's workspaces

## Related
- https://github.com/Comfy-Org/ComfyUI_frontend/pull/6295

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8073-feat-add-per-tab-workspace-authentication-infrastructure-2e96d73d3650816c8cf9dae9c330aebb)
by [Unito](https://www.unito.io)

---------

Co-authored-by: anthropic/claude <noreply@anthropic.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Simula_r <18093452+simula-r@users.noreply.github.com>
This commit is contained in:
Christian Byrne
2026-01-15 17:24:48 -08:00
committed by GitHub
parent 5409bf86a9
commit 6048fab239
12 changed files with 1392 additions and 36 deletions

View File

@@ -23,7 +23,9 @@ import { useFirebaseAuth } from 'vuefire'
import { getComfyApiBaseUrl } from '@/config/comfyApi'
import { t } from '@/i18n'
import { WORKSPACE_STORAGE_KEYS } from '@/platform/auth/workspace/workspaceConstants'
import { isCloud } from '@/platform/distribution/types'
import { remoteConfig } from '@/platform/remoteConfig/remoteConfig'
import { useTelemetry } from '@/platform/telemetry'
import { useDialogService } from '@/services/dialogService'
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
@@ -107,6 +109,15 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
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)
}
}
// Reset balance when auth state changes
@@ -152,16 +163,34 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
/**
* 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)
* 1. Workspace token (if team_workspaces_enabled and user has active workspace context)
* 2. Firebase authentication token (if user is logged in)
* 3. API key (if stored in the browser's credential manager)
*
* @returns {Promise<AuthHeader | null>}
* - A LoggedInAuthHeader with Bearer token if Firebase authenticated
* - A LoggedInAuthHeader with Bearer token (workspace or Firebase)
* - An ApiKeyAuthHeader with X-API-KEY if API key exists
* - null if neither authentication method is available
* - null if no authentication method is available
*/
const getAuthHeader = async (): Promise<AuthHeader | null> => {
// If available, set header with JWT used to identify the user to Firebase service
if (remoteConfig.value.team_workspaces_enabled) {
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 token = await getIdToken()
if (token) {
return {
@@ -169,7 +198,6 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
}
}
// If not authenticated with Firebase, try falling back to API key if available
return useApiKeyAuthStore().getAuthHeader()
}