-
Notifications
You must be signed in to change notification settings - Fork 407
DO NOT MERGE: Feat/cloud auth gates #6174
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
0562d87
249e27a
4abdaa3
13b429a
82e9a22
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,8 +34,19 @@ export const useCurrentUser = () => { | |
| return null | ||
| }) | ||
|
|
||
| <<<<<<< HEAD | ||
| const onUserResolved = (callback: (user: AuthUserInfo) => void) => | ||
| whenever(resolvedUserInfo, callback, { immediate: true }) | ||
| ======= | ||
| const onUserResolved = (callback: (user: AuthUserInfo) => void) => { | ||
| if (resolvedUserInfo.value) { | ||
| callback(resolvedUserInfo.value) | ||
| } | ||
|
|
||
| const stop = whenever(resolvedUserInfo, callback) | ||
| return () => stop() | ||
| } | ||
| >>>>>>> 775c856bf (port user ID expose hook from 6786d8e to cloud) | ||
|
|
||
| const userDisplayName = computed(() => { | ||
| if (isApiKeyLogin.value) { | ||
|
|
@@ -132,7 +143,10 @@ export const useCurrentUser = () => { | |
| resolvedUserInfo, | ||
| handleSignOut, | ||
| handleSignIn, | ||
| <<<<<<< HEAD | ||
| handleDeleteAccount, | ||
| ======= | ||
| >>>>>>> 775c856bf (port user ID expose hook from 6786d8e to cloud) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Take deletion |
||
| onUserResolved | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import { FirebaseError } from 'firebase/app' | ||
| import { AuthErrorCodes } from 'firebase/auth' | ||
| import { ref } from 'vue' | ||
| import { useRouter } from 'vue-router' | ||
|
|
||
| import { useErrorHandling } from '@/composables/useErrorHandling' | ||
| import type { ErrorRecoveryStrategy } from '@/composables/useErrorHandling' | ||
|
|
@@ -18,6 +19,7 @@ import { usdToMicros } from '@/utils/formatUtil' | |
| export const useFirebaseAuthActions = () => { | ||
| const authStore = useFirebaseAuthStore() | ||
| const toastStore = useToastStore() | ||
| const router = useRouter() | ||
| const { wrapWithErrorHandlingAsync, toastErrorHandler } = useErrorHandling() | ||
|
|
||
| const accessError = ref(false) | ||
|
|
@@ -54,6 +56,12 @@ export const useFirebaseAuthActions = () => { | |
| detail: t('auth.signOut.successDetail'), | ||
| life: 5000 | ||
| }) | ||
|
|
||
| // Redirect to login page if we're on cloud domain | ||
| const hostname = window.location.hostname | ||
| if (hostname.includes('cloud.comfy.org')) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. short term: if(isCloud)...
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. longer term: |
||
| await router.push({ name: 'cloud-login' }) | ||
| } | ||
| }, reportError) | ||
|
|
||
| const sendPasswordReset = wrapWithErrorHandlingAsync( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,11 +4,12 @@ import { | |
| createWebHistory | ||
| } from 'vue-router' | ||
|
|
||
| import { useDialogService } from '@/services/dialogService' | ||
| import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' | ||
| import { useUserStore } from '@/stores/userStore' | ||
| import { isElectron } from '@/utils/envUtil' | ||
| import LayoutDefault from '@/views/layouts/LayoutDefault.vue' | ||
|
|
||
| import { useUserStore } from './stores/userStore' | ||
| import { isElectron } from './utils/envUtil' | ||
|
|
||
| const isFileProtocol = window.location.protocol === 'file:' | ||
| const basePath = isElectron() ? '/' : window.location.pathname | ||
|
|
||
|
|
@@ -56,4 +57,41 @@ const router = createRouter({ | |
| } | ||
| }) | ||
|
|
||
| // Global authentication guard | ||
| router.beforeEach(async (_to, _from, next) => { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. short term: |
||
| const authStore = useFirebaseAuthStore() | ||
|
|
||
| // Wait for Firebase auth to initialize | ||
| if (!authStore.isInitialized) { | ||
| await new Promise<void>((resolve) => { | ||
| const unwatch = authStore.$subscribe((_, state) => { | ||
| if (state.isInitialized) { | ||
| unwatch() | ||
| resolve() | ||
| } | ||
| }) | ||
| }) | ||
| } | ||
|
|
||
| // Check if user is authenticated (Firebase or API key) | ||
| const authHeader = await authStore.getAuthHeader() | ||
|
|
||
| if (!authHeader) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if (authHeader) {
next()
return
} |
||
| // User is not authenticated, show sign-in dialog | ||
| const dialogService = useDialogService() | ||
| const loginSuccess = await dialogService.showSignInDialog() | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. double check to see if this is part of the app already. |
||
|
|
||
| if (loginSuccess) { | ||
| // After successful login, proceed to the intended route | ||
| next() | ||
| } else { | ||
| // User cancelled login, stay on current page or redirect to home | ||
| next(false) | ||
| } | ||
| } else { | ||
| // User is authenticated, proceed | ||
| next() | ||
| } | ||
| }) | ||
|
|
||
| export default router | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,6 +43,11 @@ import type { | |
| } from '@/schemas/apiSchema' | ||
| import type { ComfyNodeDef } from '@/schemas/nodeDefSchema' | ||
| import type { NodeExecutionId } from '@/types/nodeIdentification' | ||
| <<<<<<< HEAD | ||
| ======= | ||
| import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' | ||
| import { WorkflowTemplates } from '@/types/workflowTemplateTypes' | ||
| >>>>>>> 23e881e22 (Prevent access without login.) | ||
|
|
||
| interface QueuePromptRequestBody { | ||
| client_id: string | ||
|
|
@@ -317,7 +322,27 @@ export class ComfyApi extends EventTarget { | |
| return this.api_base + route | ||
| } | ||
|
|
||
| fetchApi(route: string, options?: RequestInit) { | ||
| /** | ||
| * Waits for Firebase auth to be initialized before proceeding | ||
| */ | ||
| async #waitForAuthInitialization(): Promise<void> { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| const authStore = useFirebaseAuthStore() | ||
|
|
||
| if (authStore.isInitialized) { | ||
| return | ||
| } | ||
|
|
||
| return new Promise<void>((resolve) => { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| const unwatch = authStore.$subscribe((_, state) => { | ||
| if (state.isInitialized) { | ||
| unwatch() | ||
| resolve() | ||
| } | ||
| }) | ||
| }) | ||
| } | ||
|
|
||
| async fetchApi(route: string, options?: RequestInit) { | ||
| if (!options) { | ||
| options = {} | ||
| } | ||
|
|
@@ -328,6 +353,30 @@ export class ComfyApi extends EventTarget { | |
| options.cache = 'no-cache' | ||
| } | ||
|
|
||
| // Wait for Firebase auth to be initialized before making any API request | ||
| await this.#waitForAuthInitialization() | ||
|
|
||
| // Add Firebase JWT token if user is logged in | ||
| try { | ||
| const authHeader = await useFirebaseAuthStore().getAuthHeader() | ||
| if (authHeader) { | ||
| if (Array.isArray(options.headers)) { | ||
| for (const [key, value] of Object.entries(authHeader)) { | ||
| options.headers.push([key, value]) | ||
| } | ||
| } else if (options.headers instanceof Headers) { | ||
| for (const [key, value] of Object.entries(authHeader)) { | ||
| options.headers.set(key, value) | ||
| } | ||
| } else { | ||
| Object.assign(options.headers, authHeader) | ||
| } | ||
| } | ||
| } catch (error) { | ||
| // Silently ignore auth errors to avoid breaking API calls | ||
| console.warn('Failed to get auth header:', error) | ||
| } | ||
|
|
||
| if (Array.isArray(options.headers)) { | ||
| options.headers.push(['Comfy-User', this.user]) | ||
| } else if (options.headers instanceof Headers) { | ||
|
|
@@ -402,19 +451,39 @@ export class ComfyApi extends EventTarget { | |
| * Creates and connects a WebSocket for realtime updates | ||
| * @param {boolean} isReconnect If the socket is connection is a reconnect attempt | ||
| */ | ||
| #createSocket(isReconnect?: boolean) { | ||
| async #createSocket(isReconnect?: boolean) { | ||
| if (this.socket) { | ||
| return | ||
| } | ||
|
|
||
| let opened = false | ||
| let existingSession = window.name | ||
|
|
||
| // Get auth token if available | ||
| let authToken: string | undefined | ||
| try { | ||
| authToken = await useFirebaseAuthStore().getIdToken() | ||
| } catch (error) { | ||
| // Continue without auth token if there's an error | ||
| console.warn('Could not get auth token for WebSocket connection:', error) | ||
| } | ||
|
|
||
| // Build WebSocket URL with query parameters | ||
| let wsUrl = `ws${window.location.protocol === 'https:' ? 's' : ''}://${this.api_host}${this.api_base}/ws` | ||
| const params = new URLSearchParams() | ||
|
|
||
| if (existingSession) { | ||
| existingSession = '?clientId=' + existingSession | ||
| params.set('clientId', existingSession) | ||
| } | ||
| this.socket = new WebSocket( | ||
| `ws${window.location.protocol === 'https:' ? 's' : ''}://${this.api_host}${this.api_base}/ws${existingSession}` | ||
| ) | ||
| if (authToken) { | ||
| params.set('token', authToken) | ||
| } | ||
|
|
||
| if (params.toString()) { | ||
| wsUrl += '?' + params.toString() | ||
| } | ||
|
|
||
| this.socket = new WebSocket(wsUrl) | ||
| this.socket.binaryType = 'arraybuffer' | ||
|
|
||
| this.socket.addEventListener('open', () => { | ||
|
|
@@ -441,9 +510,9 @@ export class ComfyApi extends EventTarget { | |
| }) | ||
|
|
||
| this.socket.addEventListener('close', () => { | ||
| setTimeout(() => { | ||
| setTimeout(async () => { | ||
| this.socket = null | ||
| this.#createSocket(true) | ||
| await this.#createSocket(true) | ||
| }, 300) | ||
| if (opened) { | ||
| this.dispatchCustomEvent('status', null) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -197,9 +197,14 @@ export interface ComfyExtension { | |
| ): Promise<void> | void | ||
|
|
||
| /** | ||
| <<<<<<< HEAD | ||
| * Fired whenever authentication resolves, providing the anonymized user id.. | ||
| * Extensions can register at any time and will receive the latest value immediately. | ||
| * This is an experimental API and may be changed or removed in the future. | ||
| ======= | ||
| * Fired whenever authentication resolves, providing the user id. | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. take new |
||
| * Extensions can register at any time and will receive the latest value immediately. | ||
| >>>>>>> 775c856bf (port user ID expose hook from 6786d8e to cloud) | ||
| */ | ||
| onAuthUserResolved?(user: AuthUserInfo, app: ComfyApp): Promise<void> | void | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do note use this. take HEAD