diff --git a/src/api/me.ts b/src/api/me.ts new file mode 100644 index 0000000000..3ba7ae3ea5 --- /dev/null +++ b/src/api/me.ts @@ -0,0 +1,28 @@ +// Mock API for user onboarding status +// TODO: Replace with actual API calls when backend is ready + +export interface UserOnboardingStatus { + surveyTaken: boolean + whitelisted: boolean + email?: string + userId?: string +} + +// Mock data storage (in production, this would come from backend) +let mockUserData: UserOnboardingStatus = { + surveyTaken: false, + whitelisted: false +} + +export async function getMe(): Promise { + // Simulate API delay + await new Promise(resolve => setTimeout(resolve, 300)) + + // Return mock data + return { ...mockUserData } +} + +// Helper function to update mock data (for testing) +export function setMockUserData(data: Partial) { + mockUserData = { ...mockUserData, ...data } +} \ No newline at end of file diff --git a/src/api/survey.ts b/src/api/survey.ts new file mode 100644 index 0000000000..705d78ac5a --- /dev/null +++ b/src/api/survey.ts @@ -0,0 +1,38 @@ +// Mock API for survey submission +// TODO: Replace with actual API calls when backend is ready + +import { setMockUserData } from './me' + +export interface SurveyPayload { + useCase?: string + experience?: string + teamSize?: string + [key: string]: any +} + +export interface SurveyResponse { + whitelisted: boolean + message?: string +} + +export async function submitSurvey(_payload: SurveyPayload): Promise { + // Simulate API delay + await new Promise(resolve => setTimeout(resolve, 500)) + + // Mock logic: whitelist some users based on payload + // In production, this would be determined by backend + const isWhitelisted = Math.random() > 0.5 // 50% chance for demo + + // Update mock user data + setMockUserData({ + surveyTaken: true, + whitelisted: isWhitelisted + }) + + return { + whitelisted: isWhitelisted, + message: isWhitelisted + ? 'Welcome! You have been granted access.' + : 'Thank you! You have been added to the waitlist.' + } +} \ No newline at end of file diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 0a41fa9fef..89a5c68619 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -1565,6 +1565,11 @@ "confirmPasswordLabel": "Confirm Password", "confirmPasswordPlaceholder": "Enter the same password again", "forgotPassword": "Forgot password?", + "passwordResetInstructions": "Enter your email address and we'll send you a link to reset your password.", + "sendResetLink": "Send reset link", + "backToLogin": "Back to login", + "didntReceiveEmail": "Didn't receive an email? Contact us at", + "passwordResetError": "Failed to send password reset email. Please try again.", "loginButton": "Log in", "orContinueWith": "Or continue with", "loginWithGoogle": "Log in with Google", @@ -1721,5 +1726,27 @@ "showGroups": "Show Frames/Groups", "renderBypassState": "Render Bypass State", "renderErrorState": "Render Error State" + }, + "cloudOnboarding": { + "survey": { + "title": "Cloud Survey", + "placeholder": "Survey questions placeholder" + }, + "waitlist": { + "title": "Cloud Waitlist", + "message": "You have been added to the waitlist. We will notify you when access is available." + }, + "forgotPassword": { + "title": "Forgot Password", + "instructions": "Enter your email address and we'll send you a link to reset your password.", + "emailLabel": "Email", + "emailPlaceholder": "Enter your email", + "sendResetLink": "Send reset link", + "backToLogin": "Back to login", + "didntReceiveEmail": "Didn't receive an email? Contact us at", + "passwordResetSent": "Password reset email sent", + "passwordResetError": "Failed to send password reset email. Please try again.", + "emailRequired": "Email is required" + } } -} \ No newline at end of file +} diff --git a/src/platform/onboarding/cloud/CloudForgotPasswordView.vue b/src/platform/onboarding/cloud/CloudForgotPasswordView.vue new file mode 100644 index 0000000000..366f970cd5 --- /dev/null +++ b/src/platform/onboarding/cloud/CloudForgotPasswordView.vue @@ -0,0 +1,128 @@ + + + diff --git a/src/platform/onboarding/cloud/CloudLoginView.vue b/src/platform/onboarding/cloud/CloudLoginView.vue new file mode 100644 index 0000000000..a58f8573b5 --- /dev/null +++ b/src/platform/onboarding/cloud/CloudLoginView.vue @@ -0,0 +1,170 @@ + + + diff --git a/src/platform/onboarding/cloud/CloudSignupView.vue b/src/platform/onboarding/cloud/CloudSignupView.vue new file mode 100644 index 0000000000..5c40086e42 --- /dev/null +++ b/src/platform/onboarding/cloud/CloudSignupView.vue @@ -0,0 +1,175 @@ + + + \ No newline at end of file diff --git a/src/platform/onboarding/cloud/CloudSurveyView.vue b/src/platform/onboarding/cloud/CloudSurveyView.vue new file mode 100644 index 0000000000..123fa06ff2 --- /dev/null +++ b/src/platform/onboarding/cloud/CloudSurveyView.vue @@ -0,0 +1,146 @@ + + + \ No newline at end of file diff --git a/src/platform/onboarding/cloud/CloudWaitlistView.vue b/src/platform/onboarding/cloud/CloudWaitlistView.vue new file mode 100644 index 0000000000..2f2bf551db --- /dev/null +++ b/src/platform/onboarding/cloud/CloudWaitlistView.vue @@ -0,0 +1,96 @@ + + + \ No newline at end of file diff --git a/src/platform/onboarding/cloud/components/CloudSignInForm.vue b/src/platform/onboarding/cloud/components/CloudSignInForm.vue new file mode 100644 index 0000000000..31222bb8f7 --- /dev/null +++ b/src/platform/onboarding/cloud/components/CloudSignInForm.vue @@ -0,0 +1,109 @@ + + + diff --git a/src/router.ts b/src/router.ts index e04f10ebdb..7da8466f0d 100644 --- a/src/router.ts +++ b/src/router.ts @@ -6,14 +6,65 @@ import { createWebHistory } from 'vue-router' +import { getMe } from '@/api/me' +import { cloudOnboardingRoutes } from '@/router/onboarding.cloud' 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' +const PUBLIC_ROUTE_NAMES = new Set([ + 'cloud-login', + 'cloud-signup', + 'cloud-forgot-password', + 'verify-email' +]) + +const isPublicRoute = (to: RouteLocationNormalized) => { + const name = String(to.name) + if (PUBLIC_ROUTE_NAMES.has(name)) return true + const path = to.path + // 로그인 전에도 접근 가능해야 자연스러운 경로들 + if (path === '/login' || path === '/signup' || path === '/forgot-password') + return true + if (path.startsWith('/code')) return true // /code/:inviteCode + if (path.startsWith('/verify-email')) return true // 이메일 인증 콜백 + return false +} + const isFileProtocol = window.location.protocol === 'file:' -const basePath = isElectron() ? '/' : window.location.pathname + +// Determine base path for the router +// - Electron always uses root +// - Web uses root unless serving from a real subdirectory (e.g., /ComfyBackendDirect) +function getBasePath(): string { + if (isElectron()) { + return '/' + } + + const pathname = window.location.pathname + + // These are app routes, not deployment subdirectories + const appRoutes = [ + '/login', + '/signup', + '/forgot-password', + '/survey', + '/waitlist' + ] + const isAppRoute = appRoutes.some((route) => pathname.startsWith(route)) + + // Use root if we're on an app route or at root + if (pathname === '/' || isAppRoute) { + return '/' + } + + // Otherwise, this might be a subdirectory deployment (e.g., /ComfyBackendDirect) + return pathname +} + +const basePath = getBasePath() const guardElectronAccess = ( _to: RouteLocationNormalized, @@ -35,6 +86,8 @@ const router = createRouter({ // we need this base path or assets will incorrectly resolve from 'http://localhost:7801/' createWebHistory(basePath), routes: [ + // Cloud onboarding routes + ...cloudOnboardingRoutes, { path: '/', component: LayoutDefault, @@ -132,7 +185,7 @@ const router = createRouter({ }) // Global authentication guard -router.beforeEach(async (_to, _from, next) => { +router.beforeEach(async (to, _from, next) => { const authStore = useFirebaseAuthStore() // Wait for Firebase auth to initialize @@ -147,25 +200,54 @@ router.beforeEach(async (_to, _from, next) => { }) } - // Check if user is authenticated (Firebase or API key) + // Check if user is authenticated const authHeader = await authStore.getAuthHeader() + const isLoggedIn = !!authHeader - if (!authHeader) { - // User is not authenticated, show sign-in dialog - const dialogService = useDialogService() - const loginSuccess = await dialogService.showSignInDialog() - - 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) + // Allow public routes without authentication + if (isPublicRoute(to)) { + // If logged in and trying to access login/signup, redirect based on status + if ( + isLoggedIn && + (to.name === 'cloud-login' || to.name === 'cloud-signup') + ) { + try { + const me = await getMe() + if (!me.surveyTaken) { + return next({ name: 'cloud-survey' }) + } + if (!me.whitelisted) { + return next({ name: 'cloud-waitlist' }) + } + return next({ path: '/' }) + } catch (error) { + console.error('Error fetching user status:', error) + return next({ path: '/' }) + } } - } else { - // User is authenticated, proceed - next() + // Allow access to public routes + return next() } + + // Handle protected routes + if (!isLoggedIn) { + // For Electron, use dialog + if (isElectron()) { + const dialogService = useDialogService() + const loginSuccess = await dialogService.showSignInDialog() + return loginSuccess ? next() : next(false) + } + + // For web, redirect to login + const redirectTarget = to.fullPath === '/' ? undefined : to.fullPath + return next({ + name: 'cloud-login', + query: redirectTarget ? { redirect: redirectTarget } : undefined + }) + } + + // User is logged in and accessing protected route + return next() }) export default router diff --git a/src/router/onboarding.cloud.ts b/src/router/onboarding.cloud.ts new file mode 100644 index 0000000000..aa28bc02eb --- /dev/null +++ b/src/router/onboarding.cloud.ts @@ -0,0 +1,33 @@ +import type { RouteRecordRaw } from 'vue-router' + +export const cloudOnboardingRoutes: RouteRecordRaw[] = [ + { + path: '/login', + name: 'cloud-login', + component: () => import('@/platform/onboarding/cloud/CloudLoginView.vue') + }, + { + path: '/signup', + name: 'cloud-signup', + component: () => import('@/platform/onboarding/cloud/CloudSignupView.vue') + }, + { + path: '/forgot-password', + name: 'cloud-forgot-password', + component: () => + import('@/platform/onboarding/cloud/CloudForgotPasswordView.vue') + }, + { + path: '/survey', + name: 'cloud-survey', + component: () => import('@/platform/onboarding/cloud/CloudSurveyView.vue'), + meta: { requiresAuth: true } + }, + { + path: '/waitlist', + name: 'cloud-waitlist', + component: () => + import('@/platform/onboarding/cloud/CloudWaitlistView.vue'), + meta: { requiresAuth: true } + } +]