mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-01 01:59:08 +00:00
Compare commits
8 Commits
v1.17.0
...
export-gen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6a98e3286 | ||
|
|
9ce3cccfd4 | ||
|
|
9935b322f0 | ||
|
|
60dd242b23 | ||
|
|
cec0dcbccd | ||
|
|
907632a250 | ||
|
|
45c450cdb9 | ||
|
|
ca85b2b144 |
@@ -49,4 +49,6 @@ const additionalInstructions = `
|
||||
7. Implement proper error handling
|
||||
8. Follow Vue 3 style guide and naming conventions
|
||||
9. Use Vite for fast development and building
|
||||
10. Use vue-i18n in composition API for any string literals. Place new translation
|
||||
entries in src/locales/en/main.json.
|
||||
`;
|
||||
|
||||
25
.vscode/extensions.json
vendored
Normal file
25
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"austenc.tailwind-docs",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"eamodio.gitlens",
|
||||
"esbenp.prettier-vscode",
|
||||
"figma.figma-vscode-extension",
|
||||
"github.vscode-github-actions",
|
||||
"github.vscode-pull-request-github",
|
||||
"hbenl.vscode-test-explorer",
|
||||
"lokalise.i18n-ally",
|
||||
"ms-playwright.playwright",
|
||||
"vitest.explorer",
|
||||
"vue.volar",
|
||||
"sonarsource.sonarlint-vscode",
|
||||
"deque-systems.vscode-axe-linter",
|
||||
"kisstkondoros.vscode-codemetrics",
|
||||
"donjayamanne.githistory",
|
||||
"wix.vscode-import-cost",
|
||||
"prograhammer.tslint-vue",
|
||||
"antfu.vite"
|
||||
]
|
||||
}
|
||||
1166
package-lock.json
generated
1166
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -73,7 +73,7 @@
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.31",
|
||||
"@comfyorg/litegraph": "^0.13.2",
|
||||
"@comfyorg/litegraph": "^0.13.3",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
@@ -91,6 +91,7 @@
|
||||
"algoliasearch": "^5.21.0",
|
||||
"axios": "^1.8.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"firebase": "^11.6.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"jsondiffpatch": "^0.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -103,6 +104,7 @@
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^9.14.3",
|
||||
"vue-router": "^4.4.3",
|
||||
"vuefire": "^3.2.1",
|
||||
"zod": "^3.23.8",
|
||||
"zod-validation-error": "^3.3.0"
|
||||
}
|
||||
|
||||
124
src/components/dialog/content/SignInContent.vue
Normal file
124
src/components/dialog/content/SignInContent.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div class="w-96 p-2">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col gap-4 mb-8">
|
||||
<h1 class="text-2xl font-medium leading-normal my-0">
|
||||
{{ isSignIn ? t('auth.login.title') : t('auth.signup.title') }}
|
||||
</h1>
|
||||
<p class="text-base my-0">
|
||||
<span class="text-muted">{{
|
||||
isSignIn
|
||||
? t('auth.login.newUser')
|
||||
: t('auth.signup.alreadyHaveAccount')
|
||||
}}</span>
|
||||
<span class="ml-1 cursor-pointer text-blue-500" @click="toggleState">{{
|
||||
isSignIn ? t('auth.login.signUp') : t('auth.signup.signIn')
|
||||
}}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<SignInForm v-if="isSignIn" @submit="signInWithEmail" />
|
||||
<SignUpForm v-else @submit="signInWithEmail" />
|
||||
|
||||
<!-- Divider -->
|
||||
<Divider align="center" layout="horizontal" class="my-8">
|
||||
<span class="text-muted">{{ t('auth.login.orContinueWith') }}</span>
|
||||
</Divider>
|
||||
|
||||
<!-- Social Login Buttons -->
|
||||
<div class="flex flex-col gap-6">
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="signInWithGoogle"
|
||||
>
|
||||
<i class="pi pi-google mr-2"></i>
|
||||
{{
|
||||
isSignIn
|
||||
? t('auth.login.loginWithGoogle')
|
||||
: t('auth.signup.signUpWithGoogle')
|
||||
}}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
class="h-10"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="signInWithGithub"
|
||||
>
|
||||
<i class="pi pi-github mr-2"></i>
|
||||
{{
|
||||
isSignIn
|
||||
? t('auth.login.loginWithGithub')
|
||||
: t('auth.signup.signUpWithGithub')
|
||||
}}
|
||||
</Button>
|
||||
</div>
|
||||
<!-- Terms -->
|
||||
<p class="text-xs text-muted mt-8">
|
||||
{{ t('auth.login.termsText') }}
|
||||
<span class="text-blue-500 cursor-pointer">{{
|
||||
t('auth.login.termsLink')
|
||||
}}</span>
|
||||
{{ t('auth.login.andText') }}
|
||||
<span class="text-blue-500 cursor-pointer">{{
|
||||
t('auth.login.privacyLink')
|
||||
}}</span
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { SignInData, SignUpData } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
import SignInForm from './signin/SignInForm.vue'
|
||||
import SignUpForm from './signin/SignUpForm.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { onSuccess } = defineProps<{
|
||||
onSuccess: () => void
|
||||
}>()
|
||||
|
||||
const firebaseAuthStore = useFirebaseAuthStore()
|
||||
|
||||
const isSignIn = ref(true)
|
||||
const toggleState = () => {
|
||||
isSignIn.value = !isSignIn.value
|
||||
}
|
||||
|
||||
const signInWithGoogle = () => {
|
||||
// Implement Google login
|
||||
console.log(isSignIn.value)
|
||||
console.log('Google login clicked')
|
||||
onSuccess()
|
||||
}
|
||||
|
||||
const signInWithGithub = () => {
|
||||
// Implement Github login
|
||||
console.log(isSignIn.value)
|
||||
console.log('Github login clicked')
|
||||
onSuccess()
|
||||
}
|
||||
|
||||
const signInWithEmail = async (values: SignInData | SignUpData) => {
|
||||
const { email, password } = values
|
||||
if (isSignIn.value) {
|
||||
await firebaseAuthStore.login(email, password)
|
||||
} else {
|
||||
await firebaseAuthStore.register(email, password)
|
||||
}
|
||||
onSuccess()
|
||||
}
|
||||
</script>
|
||||
@@ -101,7 +101,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Form, FormField, type FormSubmitEvent } from '@primevue/forms'
|
||||
// @ts-expect-error https://github.com/primefaces/primevue/issues/6722
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import type { CaptureContext, User } from '@sentry/core'
|
||||
import { captureMessage } from '@sentry/core'
|
||||
|
||||
89
src/components/dialog/content/signin/SignInForm.vue
Normal file
89
src/components/dialog/content/signin/SignInForm.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<Form
|
||||
v-slot="$form"
|
||||
class="flex flex-col gap-6"
|
||||
:resolver="zodResolver(signInSchema)"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<!-- Email Field -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label
|
||||
class="opacity-80 text-base font-medium mb-2"
|
||||
for="comfy-org-sign-in-email"
|
||||
>
|
||||
{{ t('auth.login.emailLabel') }}
|
||||
</label>
|
||||
<InputText
|
||||
pt:root:id="comfy-org-sign-in-email"
|
||||
pt:root:autocomplete="email"
|
||||
class="h-10"
|
||||
name="email"
|
||||
type="text"
|
||||
:placeholder="t('auth.login.emailPlaceholder')"
|
||||
:invalid="$form.email?.invalid"
|
||||
/>
|
||||
<small v-if="$form.email?.invalid" class="text-red-500">{{
|
||||
$form.email.error.message
|
||||
}}</small>
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label
|
||||
class="opacity-80 text-base font-medium"
|
||||
for="comfy-org-sign-in-password"
|
||||
>
|
||||
{{ t('auth.login.passwordLabel') }}
|
||||
</label>
|
||||
<span class="text-muted text-base font-medium cursor-pointer">
|
||||
{{ t('auth.login.forgotPassword') }}
|
||||
</span>
|
||||
</div>
|
||||
<Password
|
||||
input-id="comfy-org-sign-in-password"
|
||||
pt:pc-input-text:root:autocomplete="current-password"
|
||||
name="password"
|
||||
:feedback="false"
|
||||
toggle-mask
|
||||
:placeholder="t('auth.login.passwordPlaceholder')"
|
||||
:class="{ 'p-invalid': $form.password?.invalid }"
|
||||
fluid
|
||||
class="h-10"
|
||||
/>
|
||||
<small v-if="$form.password?.invalid" class="text-red-500">{{
|
||||
$form.password.error.message
|
||||
}}</small>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<Button
|
||||
type="submit"
|
||||
:label="t('auth.login.loginButton')"
|
||||
class="h-10 font-medium mt-4"
|
||||
/>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Form, FormSubmitEvent } from '@primevue/forms'
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Password from 'primevue/password'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { type SignInData, signInSchema } from '@/schemas/signInSchema'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [values: SignInData]
|
||||
}>()
|
||||
|
||||
const onSubmit = (event: FormSubmitEvent) => {
|
||||
if (event.valid) {
|
||||
emit('submit', event.values as SignInData)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
165
src/components/dialog/content/signin/SignUpForm.vue
Normal file
165
src/components/dialog/content/signin/SignUpForm.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<Form
|
||||
v-slot="$form"
|
||||
class="flex flex-col gap-6"
|
||||
:resolver="zodResolver(signUpSchema)"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<!-- Email Field -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label
|
||||
class="opacity-80 text-base font-medium mb-2"
|
||||
for="comfy-org-sign-up-email"
|
||||
>
|
||||
{{ t('auth.signup.emailLabel') }}
|
||||
</label>
|
||||
<InputText
|
||||
pt:root:id="comfy-org-sign-up-email"
|
||||
pt:root:autocomplete="email"
|
||||
class="h-10"
|
||||
name="email"
|
||||
type="text"
|
||||
:placeholder="t('auth.signup.emailPlaceholder')"
|
||||
:invalid="$form.email?.invalid"
|
||||
/>
|
||||
<small v-if="$form.email?.invalid" class="text-red-500">{{
|
||||
$form.email.error.message
|
||||
}}</small>
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label
|
||||
class="opacity-80 text-base font-medium"
|
||||
for="comfy-org-sign-up-password"
|
||||
>
|
||||
{{ t('auth.signup.passwordLabel') }}
|
||||
</label>
|
||||
</div>
|
||||
<Password
|
||||
v-model="password"
|
||||
input-id="comfy-org-sign-up-password"
|
||||
pt:pc-input-text:root:autocomplete="new-password"
|
||||
name="password"
|
||||
:feedback="false"
|
||||
toggle-mask
|
||||
:placeholder="t('auth.signup.passwordPlaceholder')"
|
||||
:class="{ 'p-invalid': $form.password?.invalid }"
|
||||
fluid
|
||||
class="h-10"
|
||||
/>
|
||||
<div class="flex flex-col gap-1">
|
||||
<small
|
||||
v-if="$form.password?.dirty || $form.password?.invalid"
|
||||
class="text-sm"
|
||||
>
|
||||
{{ t('validation.password.requirements') }}:
|
||||
<ul class="mt-1 space-y-1">
|
||||
<li
|
||||
:class="{
|
||||
'text-red-500': !passwordChecks.length
|
||||
}"
|
||||
>
|
||||
{{ t('validation.password.minLength') }}
|
||||
</li>
|
||||
<li
|
||||
:class="{
|
||||
'text-red-500': !passwordChecks.uppercase
|
||||
}"
|
||||
>
|
||||
{{ t('validation.password.uppercase') }}
|
||||
</li>
|
||||
<li
|
||||
:class="{
|
||||
'text-red-500': !passwordChecks.lowercase
|
||||
}"
|
||||
>
|
||||
{{ t('validation.password.lowercase') }}
|
||||
</li>
|
||||
<li
|
||||
:class="{
|
||||
'text-red-500': !passwordChecks.number
|
||||
}"
|
||||
>
|
||||
{{ t('validation.password.number') }}
|
||||
</li>
|
||||
<li
|
||||
:class="{
|
||||
'text-red-500': !passwordChecks.special
|
||||
}"
|
||||
>
|
||||
{{ t('validation.password.special') }}
|
||||
</li>
|
||||
</ul>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password Field -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label
|
||||
class="opacity-80 text-base font-medium mb-2"
|
||||
for="comfy-org-sign-up-confirm-password"
|
||||
>
|
||||
{{ t('auth.login.confirmPasswordLabel') }}
|
||||
</label>
|
||||
<Password
|
||||
name="confirmPassword"
|
||||
input-id="comfy-org-sign-up-confirm-password"
|
||||
pt:pc-input-text:root:autocomplete="new-password"
|
||||
:feedback="false"
|
||||
toggle-mask
|
||||
:placeholder="t('auth.login.confirmPasswordPlaceholder')"
|
||||
:class="{ 'p-invalid': $form.confirmPassword?.invalid }"
|
||||
fluid
|
||||
class="h-10"
|
||||
/>
|
||||
<small v-if="$form.confirmPassword?.error" class="text-red-500">{{
|
||||
$form.confirmPassword.error.message
|
||||
}}</small>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<Button
|
||||
type="submit"
|
||||
:label="t('auth.signup.signUpButton')"
|
||||
class="h-10 font-medium mt-4"
|
||||
/>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Form, FormSubmitEvent } from '@primevue/forms'
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Password from 'primevue/password'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { type SignUpData, signUpSchema } from '@/schemas/signInSchema'
|
||||
|
||||
const { t } = useI18n()
|
||||
const password = ref('')
|
||||
|
||||
// TODO: Use dynamic form to better organize the password checks.
|
||||
// Ref: https://primevue.org/forms/#dynamic
|
||||
const passwordChecks = computed(() => ({
|
||||
length: password.value.length >= 8 && password.value.length <= 32,
|
||||
uppercase: /[A-Z]/.test(password.value),
|
||||
lowercase: /[a-z]/.test(password.value),
|
||||
number: /\d/.test(password.value),
|
||||
special: /[^A-Za-z0-9]/.test(password.value)
|
||||
}))
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [values: SignUpData]
|
||||
}>()
|
||||
|
||||
const onSubmit = (event: FormSubmitEvent) => {
|
||||
if (event.valid) {
|
||||
emit('submit', event.values as SignUpData)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
12
src/config/firebase.ts
Normal file
12
src/config/firebase.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { FirebaseOptions } from 'firebase/app'
|
||||
|
||||
export const FIREBASE_CONFIG: FirebaseOptions = {
|
||||
apiKey: 'AIzaSyC2-fomLqgCjb7ELwta1I9cEarPK8ziTGs',
|
||||
authDomain: 'dreamboothy.firebaseapp.com',
|
||||
databaseURL: 'https://dreamboothy-default-rtdb.firebaseio.com',
|
||||
projectId: 'dreamboothy',
|
||||
storageBucket: 'dreamboothy.appspot.com',
|
||||
messagingSenderId: '357148958219',
|
||||
appId: '1:357148958219:web:f5917f72e5f36a2015310e',
|
||||
measurementId: 'G-3ZBD3MBTG4'
|
||||
}
|
||||
@@ -1051,5 +1051,56 @@
|
||||
"noTemplatesToExport": "No templates to export",
|
||||
"failedToFetchLogs": "Failed to fetch server logs",
|
||||
"migrateToLitegraphReroute": "Reroute nodes will be removed in future versions. Click to migrate to litegraph-native reroute."
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
"title": "Log in to your account",
|
||||
"newUser": "New here?",
|
||||
"signUp": "Sign up",
|
||||
"emailLabel": "Email",
|
||||
"emailPlaceholder": "Enter your email",
|
||||
"passwordLabel": "Password",
|
||||
"passwordPlaceholder": "Enter your password",
|
||||
"confirmPasswordLabel": "Confirm Password",
|
||||
"confirmPasswordPlaceholder": "Enter the same password again",
|
||||
"forgotPassword": "Forgot password?",
|
||||
"loginButton": "Log in",
|
||||
"orContinueWith": "Or continue with",
|
||||
"loginWithGoogle": "Log in with Google",
|
||||
"loginWithGithub": "Log in with Github",
|
||||
"termsText": "By clicking \"Next\" or \"Sign Up\", you agree to our",
|
||||
"termsLink": "Terms of Use",
|
||||
"andText": "and",
|
||||
"privacyLink": "Privacy Policy",
|
||||
"success": "Login successful",
|
||||
"failed": "Login failed"
|
||||
},
|
||||
"signup": {
|
||||
"title": "Create an account",
|
||||
"alreadyHaveAccount": "Already have an account?",
|
||||
"emailLabel": "Email",
|
||||
"emailPlaceholder": "Enter your email",
|
||||
"passwordLabel": "Password",
|
||||
"passwordPlaceholder": "Enter new password",
|
||||
"signUpButton": "Sign up",
|
||||
"signIn": "Sign in",
|
||||
"signUpWithGoogle": "Sign up with Google",
|
||||
"signUpWithGithub": "Sign up with Github"
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "Invalid email address",
|
||||
"required": "Required",
|
||||
"minLength": "Must be at least {length} characters",
|
||||
"maxLength": "Must be no more than {length} characters",
|
||||
"password": {
|
||||
"requirements": "Password requirements",
|
||||
"minLength": "Must be between 8 and 32 characters",
|
||||
"uppercase": "Must contain at least one uppercase letter",
|
||||
"lowercase": "Must contain at least one lowercase letter",
|
||||
"number": "Must contain at least one number",
|
||||
"special": "Must contain at least one special character",
|
||||
"match": "Passwords must match"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,42 @@
|
||||
"message": "Este flujo de trabajo contiene nodos de API, que requieren que inicies sesión en tu cuenta para poder ejecutar.",
|
||||
"title": "Se requiere iniciar sesión para usar los nodos de API"
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
"andText": "y",
|
||||
"confirmPasswordLabel": "Confirmar contraseña",
|
||||
"confirmPasswordPlaceholder": "Ingresa la misma contraseña nuevamente",
|
||||
"emailLabel": "Correo electrónico",
|
||||
"emailPlaceholder": "Ingresa tu correo electrónico",
|
||||
"failed": "Inicio de sesión fallido",
|
||||
"forgotPassword": "¿Olvidaste tu contraseña?",
|
||||
"loginButton": "Iniciar sesión",
|
||||
"loginWithGithub": "Iniciar sesión con Github",
|
||||
"loginWithGoogle": "Iniciar sesión con Google",
|
||||
"newUser": "¿Eres nuevo aquí?",
|
||||
"orContinueWith": "O continuar con",
|
||||
"passwordLabel": "Contraseña",
|
||||
"passwordPlaceholder": "Ingresa tu contraseña",
|
||||
"privacyLink": "Política de privacidad",
|
||||
"signUp": "Regístrate",
|
||||
"success": "Inicio de sesión exitoso",
|
||||
"termsLink": "Términos de uso",
|
||||
"termsText": "Al hacer clic en \"Siguiente\" o \"Registrarse\", aceptas nuestros",
|
||||
"title": "Inicia sesión en tu cuenta"
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "¿Ya tienes una cuenta?",
|
||||
"emailLabel": "Correo electrónico",
|
||||
"emailPlaceholder": "Ingresa tu correo electrónico",
|
||||
"passwordLabel": "Contraseña",
|
||||
"passwordPlaceholder": "Ingresa una nueva contraseña",
|
||||
"signIn": "Iniciar sesión",
|
||||
"signUpButton": "Registrarse",
|
||||
"signUpWithGithub": "Registrarse con Github",
|
||||
"signUpWithGoogle": "Registrarse con Google",
|
||||
"title": "Crea una cuenta"
|
||||
}
|
||||
},
|
||||
"clipboard": {
|
||||
"errorMessage": "Error al copiar al portapapeles",
|
||||
"errorNotSupported": "API del portapapeles no soportada en su navegador",
|
||||
@@ -1043,6 +1079,21 @@
|
||||
"next": "Siguiente",
|
||||
"selectUser": "Selecciona un usuario"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "Dirección de correo electrónico inválida",
|
||||
"maxLength": "No debe tener más de {length} caracteres",
|
||||
"minLength": "Debe tener al menos {length} caracteres",
|
||||
"password": {
|
||||
"lowercase": "Debe contener al menos una letra minúscula",
|
||||
"match": "Las contraseñas deben coincidir",
|
||||
"minLength": "Debe tener entre 8 y 32 caracteres",
|
||||
"number": "Debe contener al menos un número",
|
||||
"requirements": "Requisitos de la contraseña",
|
||||
"special": "Debe contener al menos un carácter especial",
|
||||
"uppercase": "Debe contener al menos una letra mayúscula"
|
||||
},
|
||||
"required": "Requerido"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "Empezar",
|
||||
"title": "Bienvenido a ComfyUI"
|
||||
|
||||
@@ -8,6 +8,42 @@
|
||||
"message": "Ce flux de travail contient des nœuds API, qui nécessitent que vous soyez connecté à votre compte pour pouvoir fonctionner.",
|
||||
"title": "Connexion requise pour utiliser les nœuds API"
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
"andText": "et",
|
||||
"confirmPasswordLabel": "Confirmer le mot de passe",
|
||||
"confirmPasswordPlaceholder": "Entrez à nouveau le même mot de passe",
|
||||
"emailLabel": "Email",
|
||||
"emailPlaceholder": "Entrez votre email",
|
||||
"failed": "Échec de la connexion",
|
||||
"forgotPassword": "Mot de passe oublié?",
|
||||
"loginButton": "Se connecter",
|
||||
"loginWithGithub": "Se connecter avec Github",
|
||||
"loginWithGoogle": "Se connecter avec Google",
|
||||
"newUser": "Nouveau ici?",
|
||||
"orContinueWith": "Ou continuer avec",
|
||||
"passwordLabel": "Mot de passe",
|
||||
"passwordPlaceholder": "Entrez votre mot de passe",
|
||||
"privacyLink": "Politique de confidentialité",
|
||||
"signUp": "S'inscrire",
|
||||
"success": "Connexion réussie",
|
||||
"termsLink": "Conditions d'utilisation",
|
||||
"termsText": "En cliquant sur \"Suivant\" ou \"S'inscrire\", vous acceptez nos",
|
||||
"title": "Connectez-vous à votre compte"
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "Vous avez déjà un compte?",
|
||||
"emailLabel": "Email",
|
||||
"emailPlaceholder": "Entrez votre email",
|
||||
"passwordLabel": "Mot de passe",
|
||||
"passwordPlaceholder": "Entrez un nouveau mot de passe",
|
||||
"signIn": "Se connecter",
|
||||
"signUpButton": "S'inscrire",
|
||||
"signUpWithGithub": "S'inscrire avec Github",
|
||||
"signUpWithGoogle": "S'inscrire avec Google",
|
||||
"title": "Créer un compte"
|
||||
}
|
||||
},
|
||||
"clipboard": {
|
||||
"errorMessage": "Échec de la copie dans le presse-papiers",
|
||||
"errorNotSupported": "L'API du presse-papiers n'est pas prise en charge par votre navigateur",
|
||||
@@ -1043,6 +1079,21 @@
|
||||
"next": "Suivant",
|
||||
"selectUser": "Sélectionnez un utilisateur"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "Adresse e-mail invalide",
|
||||
"maxLength": "Ne doit pas dépasser {length} caractères",
|
||||
"minLength": "Doit contenir au moins {length} caractères",
|
||||
"password": {
|
||||
"lowercase": "Doit contenir au moins une lettre minuscule",
|
||||
"match": "Les mots de passe doivent correspondre",
|
||||
"minLength": "Doit contenir entre 8 et 32 caractères",
|
||||
"number": "Doit contenir au moins un chiffre",
|
||||
"requirements": "Exigences du mot de passe",
|
||||
"special": "Doit contenir au moins un caractère spécial",
|
||||
"uppercase": "Doit contenir au moins une lettre majuscule"
|
||||
},
|
||||
"required": "Requis"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "Commencer",
|
||||
"title": "Bienvenue sur ComfyUI"
|
||||
|
||||
@@ -8,6 +8,42 @@
|
||||
"message": "このワークフローにはAPIノードが含まれており、実行するためにはアカウントにサインインする必要があります。",
|
||||
"title": "APIノードを使用するためにはサインインが必要です"
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
"andText": "および",
|
||||
"confirmPasswordLabel": "パスワードの確認",
|
||||
"confirmPasswordPlaceholder": "もう一度同じパスワードを入力してください",
|
||||
"emailLabel": "メール",
|
||||
"emailPlaceholder": "メールアドレスを入力してください",
|
||||
"failed": "ログイン失敗",
|
||||
"forgotPassword": "パスワードを忘れましたか?",
|
||||
"loginButton": "ログイン",
|
||||
"loginWithGithub": "Githubでログイン",
|
||||
"loginWithGoogle": "Googleでログイン",
|
||||
"newUser": "新規ユーザーですか?",
|
||||
"orContinueWith": "または以下で続ける",
|
||||
"passwordLabel": "パスワード",
|
||||
"passwordPlaceholder": "パスワードを入力してください",
|
||||
"privacyLink": "プライバシーポリシー",
|
||||
"signUp": "サインアップ",
|
||||
"success": "ログイン成功",
|
||||
"termsLink": "利用規約",
|
||||
"termsText": "「次へ」または「サインアップ」をクリックすると、私たちの",
|
||||
"title": "アカウントにログインする"
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "すでにアカウントをお持ちですか?",
|
||||
"emailLabel": "メール",
|
||||
"emailPlaceholder": "メールアドレスを入力してください",
|
||||
"passwordLabel": "パスワード",
|
||||
"passwordPlaceholder": "新しいパスワードを入力してください",
|
||||
"signIn": "サインイン",
|
||||
"signUpButton": "サインアップ",
|
||||
"signUpWithGithub": "Githubでサインアップ",
|
||||
"signUpWithGoogle": "Googleでサインアップ",
|
||||
"title": "アカウントを作成する"
|
||||
}
|
||||
},
|
||||
"clipboard": {
|
||||
"errorMessage": "クリップボードへのコピーに失敗しました",
|
||||
"errorNotSupported": "お使いのブラウザではクリップボードAPIがサポートされていません",
|
||||
@@ -1043,6 +1079,21 @@
|
||||
"next": "次へ",
|
||||
"selectUser": "ユーザーを選択"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "無効なメールアドレス",
|
||||
"maxLength": "{length}文字以下でなければなりません",
|
||||
"minLength": "{length}文字以上でなければなりません",
|
||||
"password": {
|
||||
"lowercase": "少なくとも1つの小文字を含む必要があります",
|
||||
"match": "パスワードが一致する必要があります",
|
||||
"minLength": "8文字から32文字の間でなければなりません",
|
||||
"number": "少なくとも1つの数字を含む必要があります",
|
||||
"requirements": "パスワードの要件",
|
||||
"special": "少なくとも1つの特殊文字を含む必要があります",
|
||||
"uppercase": "少なくとも1つの大文字を含む必要があります"
|
||||
},
|
||||
"required": "必須"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "はじめる",
|
||||
"title": "ComfyUIへようこそ"
|
||||
|
||||
@@ -8,6 +8,42 @@
|
||||
"message": "이 워크플로우에는 API 노드가 포함되어 있으며, 실행하려면 계정에 로그인해야 합니다.",
|
||||
"title": "API 노드 사용에 필요한 로그인"
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
"andText": "및",
|
||||
"confirmPasswordLabel": "비밀번호 확인",
|
||||
"confirmPasswordPlaceholder": "동일한 비밀번호를 다시 입력하세요",
|
||||
"emailLabel": "이메일",
|
||||
"emailPlaceholder": "이메일을 입력하세요",
|
||||
"failed": "로그인 실패",
|
||||
"forgotPassword": "비밀번호를 잊으셨나요?",
|
||||
"loginButton": "로그인",
|
||||
"loginWithGithub": "Github로 로그인",
|
||||
"loginWithGoogle": "구글로 로그인",
|
||||
"newUser": "처음이신가요?",
|
||||
"orContinueWith": "또는 다음으로 계속",
|
||||
"passwordLabel": "비밀번호",
|
||||
"passwordPlaceholder": "비밀번호를 입력하세요",
|
||||
"privacyLink": "개인정보 보호정책",
|
||||
"signUp": "가입하기",
|
||||
"success": "로그인 성공",
|
||||
"termsLink": "이용 약관",
|
||||
"termsText": "\"다음\" 또는 \"가입하기\"를 클릭하면 우리의",
|
||||
"title": "계정에 로그인"
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "이미 계정이 있으신가요?",
|
||||
"emailLabel": "이메일",
|
||||
"emailPlaceholder": "이메일을 입력하세요",
|
||||
"passwordLabel": "비밀번호",
|
||||
"passwordPlaceholder": "새 비밀번호를 입력하세요",
|
||||
"signIn": "로그인",
|
||||
"signUpButton": "가입하기",
|
||||
"signUpWithGithub": "Github로 가입하기",
|
||||
"signUpWithGoogle": "구글로 가입하기",
|
||||
"title": "계정 생성"
|
||||
}
|
||||
},
|
||||
"clipboard": {
|
||||
"errorMessage": "클립보드에 복사하지 못했습니다",
|
||||
"errorNotSupported": "브라우저가 클립보드 API를 지원하지 않습니다.",
|
||||
@@ -1043,6 +1079,21 @@
|
||||
"next": "다음",
|
||||
"selectUser": "사용자 선택"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "유효하지 않은 이메일 주소",
|
||||
"maxLength": "{length}자를 초과할 수 없습니다",
|
||||
"minLength": "{length}자 이상이어야 합니다",
|
||||
"password": {
|
||||
"lowercase": "적어도 하나의 소문자를 포함해야 합니다",
|
||||
"match": "비밀번호가 일치해야 합니다",
|
||||
"minLength": "8자에서 32자 사이여야 합니다",
|
||||
"number": "적어도 하나의 숫자를 포함해야 합니다",
|
||||
"requirements": "비밀번호 요구사항",
|
||||
"special": "적어도 하나의 특수 문자를 포함해야 합니다",
|
||||
"uppercase": "적어도 하나의 대문자를 포함해야 합니다"
|
||||
},
|
||||
"required": "필수"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "시작하기",
|
||||
"title": "ComfyUI에 오신 것을 환영합니다"
|
||||
|
||||
@@ -8,6 +8,42 @@
|
||||
"message": "Этот рабочий процесс содержит API Nodes, которые требуют входа в вашу учетную запись для выполнения.",
|
||||
"title": "Требуется вход для использования API Nodes"
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
"andText": "и",
|
||||
"confirmPasswordLabel": "Подтвердите пароль",
|
||||
"confirmPasswordPlaceholder": "Введите тот же пароль еще раз",
|
||||
"emailLabel": "Электронная почта",
|
||||
"emailPlaceholder": "Введите вашу электронную почту",
|
||||
"failed": "Вход не удался",
|
||||
"forgotPassword": "Забыли пароль?",
|
||||
"loginButton": "Войти",
|
||||
"loginWithGithub": "Войти через Github",
|
||||
"loginWithGoogle": "Войти через Google",
|
||||
"newUser": "Вы здесь впервые?",
|
||||
"orContinueWith": "Или продолжить с",
|
||||
"passwordLabel": "Пароль",
|
||||
"passwordPlaceholder": "Введите ваш пароль",
|
||||
"privacyLink": "Политикой конфиденциальности",
|
||||
"signUp": "Зарегистрироваться",
|
||||
"success": "Вход выполнен успешно",
|
||||
"termsLink": "Условиями использования",
|
||||
"termsText": "Нажимая \"Далее\" или \"Зарегистрироваться\", вы соглашаетесь с нашими",
|
||||
"title": "Войдите в свой аккаунт"
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "Уже есть аккаунт?",
|
||||
"emailLabel": "Электронная почта",
|
||||
"emailPlaceholder": "Введите вашу электронную почту",
|
||||
"passwordLabel": "Пароль",
|
||||
"passwordPlaceholder": "Введите новый пароль",
|
||||
"signIn": "Войти",
|
||||
"signUpButton": "Зарегистрироваться",
|
||||
"signUpWithGithub": "Зарегистрироваться через Github",
|
||||
"signUpWithGoogle": "Зарегистрироваться через Google",
|
||||
"title": "Создать аккаунт"
|
||||
}
|
||||
},
|
||||
"clipboard": {
|
||||
"errorMessage": "Не удалось скопировать в буфер обмена",
|
||||
"errorNotSupported": "API буфера обмена не поддерживается в вашем браузере",
|
||||
@@ -1043,6 +1079,21 @@
|
||||
"next": "Далее",
|
||||
"selectUser": "Выберите пользователя"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "Недействительный адрес электронной почты",
|
||||
"maxLength": "Должно быть не более {length} символов",
|
||||
"minLength": "Должно быть не менее {length} символов",
|
||||
"password": {
|
||||
"lowercase": "Должен содержать хотя бы одну строчную букву",
|
||||
"match": "Пароли должны совпадать",
|
||||
"minLength": "Должно быть от 8 до 32 символов",
|
||||
"number": "Должен содержать хотя бы одну цифру",
|
||||
"requirements": "Требования к паролю",
|
||||
"special": "Должен содержать хотя бы один специальный символ",
|
||||
"uppercase": "Должен содержать хотя бы одну заглавную букву"
|
||||
},
|
||||
"required": "Обязательно"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "Начать",
|
||||
"title": "Добро пожаловать в ComfyUI"
|
||||
|
||||
@@ -8,6 +8,42 @@
|
||||
"message": "此工作流包含API节点,需要您登录账户才能运行。",
|
||||
"title": "使用API节点需要登录"
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
"andText": "和",
|
||||
"confirmPasswordLabel": "确认密码",
|
||||
"confirmPasswordPlaceholder": "再次输入相同的密码",
|
||||
"emailLabel": "电子邮件",
|
||||
"emailPlaceholder": "输入您的电子邮件",
|
||||
"failed": "登录失败",
|
||||
"forgotPassword": "忘记密码?",
|
||||
"loginButton": "登录",
|
||||
"loginWithGithub": "使用Github登录",
|
||||
"loginWithGoogle": "使用Google登录",
|
||||
"newUser": "新来的?",
|
||||
"orContinueWith": "或者继续使用",
|
||||
"passwordLabel": "密码",
|
||||
"passwordPlaceholder": "输入您的密码",
|
||||
"privacyLink": "隐私政策",
|
||||
"signUp": "注册",
|
||||
"success": "登录成功",
|
||||
"termsLink": "使用条款",
|
||||
"termsText": "点击“下一步”或“注册”即表示您同意我们的",
|
||||
"title": "登录您的账户"
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "已经有账户了?",
|
||||
"emailLabel": "电子邮件",
|
||||
"emailPlaceholder": "输入您的电子邮件",
|
||||
"passwordLabel": "密码",
|
||||
"passwordPlaceholder": "输入新密码",
|
||||
"signIn": "登录",
|
||||
"signUpButton": "注册",
|
||||
"signUpWithGithub": "使用Github注册",
|
||||
"signUpWithGoogle": "使用Google注册",
|
||||
"title": "创建一个账户"
|
||||
}
|
||||
},
|
||||
"clipboard": {
|
||||
"errorMessage": "复制到剪贴板失败",
|
||||
"errorNotSupported": "您的浏览器不支持剪贴板API",
|
||||
@@ -1043,6 +1079,21 @@
|
||||
"next": "下一步",
|
||||
"selectUser": "选择用户"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "无效的电子邮件地址",
|
||||
"maxLength": "不能超过{length}个字符",
|
||||
"minLength": "必须至少有{length}个字符",
|
||||
"password": {
|
||||
"lowercase": "必须包含至少一个小写字母",
|
||||
"match": "密码必须匹配",
|
||||
"minLength": "必须在8到32个字符之间",
|
||||
"number": "必须包含至少一个数字",
|
||||
"requirements": "密码要求",
|
||||
"special": "必须包含至少一个特殊字符",
|
||||
"uppercase": "必须包含至少一个大写字母"
|
||||
},
|
||||
"required": "必填"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "开始使用",
|
||||
"title": "欢迎使用 ComfyUI"
|
||||
|
||||
@@ -2,6 +2,7 @@ import '@comfyorg/litegraph/style.css'
|
||||
import { definePreset } from '@primevue/themes'
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import * as Sentry from '@sentry/vue'
|
||||
import { initializeApp } from 'firebase/app'
|
||||
import { createPinia } from 'pinia'
|
||||
import 'primeicons/primeicons.css'
|
||||
import PrimeVue from 'primevue/config'
|
||||
@@ -9,8 +10,10 @@ import ConfirmationService from 'primevue/confirmationservice'
|
||||
import ToastService from 'primevue/toastservice'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { createApp } from 'vue'
|
||||
import { VueFire, VueFireAuth } from 'vuefire'
|
||||
|
||||
import '@/assets/css/style.css'
|
||||
import { FIREBASE_CONFIG } from '@/config/firebase'
|
||||
import router from '@/router'
|
||||
|
||||
import App from './App.vue'
|
||||
@@ -23,6 +26,8 @@ const ComfyUIPreset = definePreset(Aura, {
|
||||
}
|
||||
})
|
||||
|
||||
const firebaseApp = initializeApp(FIREBASE_CONFIG)
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
Sentry.init({
|
||||
@@ -58,4 +63,8 @@ app
|
||||
.use(ToastService)
|
||||
.use(pinia)
|
||||
.use(i18n)
|
||||
.use(VueFire, {
|
||||
firebaseApp,
|
||||
modules: [VueFireAuth()]
|
||||
})
|
||||
.mount('#vue-app')
|
||||
|
||||
36
src/schemas/signInSchema.ts
Normal file
36
src/schemas/signInSchema.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
|
||||
export const signInSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.email(t('validation.invalidEmail'))
|
||||
.min(1, t('validation.required')),
|
||||
password: z.string().min(1, t('validation.required'))
|
||||
})
|
||||
|
||||
export type SignInData = z.infer<typeof signInSchema>
|
||||
|
||||
export const signUpSchema = z
|
||||
.object({
|
||||
email: z
|
||||
.string()
|
||||
.email(t('validation.invalidEmail'))
|
||||
.min(1, t('validation.required')),
|
||||
password: z
|
||||
.string()
|
||||
.min(8, t('validation.minLength', { length: 8 }))
|
||||
.max(32, t('validation.maxLength', { length: 32 }))
|
||||
.regex(/[A-Z]/, t('validation.password.uppercase'))
|
||||
.regex(/[a-z]/, t('validation.password.lowercase'))
|
||||
.regex(/\d/, t('validation.password.number'))
|
||||
.regex(/[^A-Za-z0-9]/, t('validation.password.special')),
|
||||
confirmPassword: z.string().min(1, t('validation.required'))
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: t('validation.password.match'),
|
||||
path: ['confirmPassword']
|
||||
})
|
||||
|
||||
export type SignUpData = z.infer<typeof signUpSchema>
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as formatUtil from '@/utils/formatUtil'
|
||||
import { applyTextReplacements as _applyTextReplacements } from '@/utils/searchAndReplace'
|
||||
|
||||
import { api } from './api'
|
||||
@@ -121,3 +122,7 @@ export function setStorageValue(id: string, value: string) {
|
||||
}
|
||||
localStorage.setItem(id, value)
|
||||
}
|
||||
|
||||
export function generateUUID() {
|
||||
return formatUtil.generateUUID()
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import ManagerProgressDialogContent from '@/components/dialog/content/ManagerPro
|
||||
import MissingModelsWarning from '@/components/dialog/content/MissingModelsWarning.vue'
|
||||
import PromptDialogContent from '@/components/dialog/content/PromptDialogContent.vue'
|
||||
import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue'
|
||||
import SignInContent from '@/components/dialog/content/SignInContent.vue'
|
||||
import ManagerDialogContent from '@/components/dialog/content/manager/ManagerDialogContent.vue'
|
||||
import ManagerHeader from '@/components/dialog/content/manager/ManagerHeader.vue'
|
||||
import ManagerProgressFooter from '@/components/dialog/footer/ManagerProgressFooter.vue'
|
||||
@@ -232,7 +233,7 @@ export const useDialogService = () => {
|
||||
component: ApiNodesSignInContent,
|
||||
props: {
|
||||
apiNodes,
|
||||
onLogin: () => resolve(true),
|
||||
onLogin: () => showSignInDialog().then((result) => resolve(result)),
|
||||
onCancel: () => resolve(false)
|
||||
},
|
||||
headerComponent: ComfyOrgHeader,
|
||||
@@ -247,6 +248,26 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
async function showSignInDialog(): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
dialogStore.showDialog({
|
||||
key: 'global-signin',
|
||||
component: SignInContent,
|
||||
headerComponent: ComfyOrgHeader,
|
||||
props: {
|
||||
onSuccess: () => resolve(true)
|
||||
},
|
||||
dialogComponentProps: {
|
||||
closable: false,
|
||||
onClose: () => resolve(false)
|
||||
}
|
||||
})
|
||||
}).then((result) => {
|
||||
dialogStore.closeDialog({ key: 'global-signin' })
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
async function prompt({
|
||||
title,
|
||||
message,
|
||||
@@ -332,6 +353,7 @@ export const useDialogService = () => {
|
||||
showManagerProgressDialog,
|
||||
showErrorDialog,
|
||||
showApiNodesSignInDialog,
|
||||
showSignInDialog,
|
||||
prompt,
|
||||
confirm
|
||||
}
|
||||
|
||||
99
src/stores/firebaseAuthStore.ts
Normal file
99
src/stores/firebaseAuthStore.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
type Auth,
|
||||
type User,
|
||||
type UserCredential,
|
||||
createUserWithEmailAndPassword,
|
||||
onAuthStateChanged,
|
||||
signInWithEmailAndPassword,
|
||||
signOut
|
||||
} from 'firebase/auth'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useFirebaseAuth } from 'vuefire'
|
||||
|
||||
export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
// State
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const currentUser = ref<User | null>(null)
|
||||
const isInitialized = ref(false)
|
||||
|
||||
// Getters
|
||||
const isAuthenticated = computed(() => !!currentUser.value)
|
||||
const userEmail = computed(() => currentUser.value?.email)
|
||||
const userId = computed(() => currentUser.value?.uid)
|
||||
|
||||
// Get auth from VueFire and listen for auth state changes
|
||||
const auth = useFirebaseAuth()
|
||||
if (auth) {
|
||||
onAuthStateChanged(auth, (user) => {
|
||||
currentUser.value = user
|
||||
isInitialized.value = true
|
||||
})
|
||||
} else {
|
||||
error.value = 'Firebase Auth not available from VueFire'
|
||||
}
|
||||
|
||||
const executeAuthAction = async <T>(
|
||||
action: (auth: Auth) => Promise<T>
|
||||
): Promise<T> => {
|
||||
if (!auth) throw new Error('Firebase Auth not initialized')
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
return await action(auth)
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : 'Unknown error'
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const login = async (
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<UserCredential> =>
|
||||
executeAuthAction((authInstance) =>
|
||||
signInWithEmailAndPassword(authInstance, email, password)
|
||||
)
|
||||
|
||||
const register = async (
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<UserCredential> =>
|
||||
executeAuthAction((authInstance) =>
|
||||
createUserWithEmailAndPassword(authInstance, email, password)
|
||||
)
|
||||
|
||||
const logout = async (): Promise<void> =>
|
||||
executeAuthAction((authInstance) => signOut(authInstance))
|
||||
|
||||
const getIdToken = async (): Promise<string | null> => {
|
||||
if (currentUser.value) {
|
||||
return currentUser.value.getIdToken()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
loading,
|
||||
error,
|
||||
currentUser,
|
||||
isInitialized,
|
||||
|
||||
// Getters
|
||||
isAuthenticated,
|
||||
userEmail,
|
||||
userId,
|
||||
|
||||
// Actions
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
getIdToken
|
||||
}
|
||||
})
|
||||
@@ -21,6 +21,43 @@ export interface paths {
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/customer': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
get?: never
|
||||
put?: never
|
||||
/**
|
||||
* Create a new customer
|
||||
* @description Creates a new customer using the provided token. No request body is needed as user information is extracted from the token.
|
||||
*/
|
||||
post: operations['createCustomer']
|
||||
delete?: never
|
||||
options?: never
|
||||
head?: never
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/customer/credit': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
get?: never
|
||||
put?: never
|
||||
/** Initiates a Credit Purchase. */
|
||||
post: operations['InitiateCreditPurchase']
|
||||
delete?: never
|
||||
options?: never
|
||||
head?: never
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/upload-artifact': {
|
||||
parameters: {
|
||||
query?: never
|
||||
@@ -900,6 +937,98 @@ export interface paths {
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/proxy/ideogram/generate': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
get?: never
|
||||
put?: never
|
||||
/**
|
||||
* Proxy request to Ideogram for image generation
|
||||
* @description Forwards image generation requests to Ideogram's API and returns the results.
|
||||
*/
|
||||
post: operations['ideogramGenerate']
|
||||
delete?: never
|
||||
options?: never
|
||||
head?: never
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/webhook/metronome/zero-balance': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
get?: never
|
||||
put?: never
|
||||
/** receive alert on remaining balance is 0 */
|
||||
post: {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
properties?: {
|
||||
/** @description the metronome customer id */
|
||||
customer_id?: string
|
||||
/** @description the customer remaining balance */
|
||||
remaining_balance?: number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
/** @description Webhook processed succesfully */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['IdeogramGenerateResponse']
|
||||
}
|
||||
}
|
||||
/** @description Bad Request */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Internal Server Error (proxy or upstream issue) */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delete?: never
|
||||
options?: never
|
||||
head?: never
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
}
|
||||
export type webhooks = Record<string, never>
|
||||
export interface components {
|
||||
@@ -1225,6 +1354,81 @@ export interface components {
|
||||
/** @description The pip freeze output */
|
||||
pip_freeze?: string
|
||||
}
|
||||
Customer: {
|
||||
/** @description The firebase UID of the user */
|
||||
id: string
|
||||
/** @description The email address for this user */
|
||||
email?: string
|
||||
/** @description The name for this user */
|
||||
name?: string
|
||||
/**
|
||||
* Format: date-time
|
||||
* @description The date and time the user was created
|
||||
*/
|
||||
createdAt?: string
|
||||
/**
|
||||
* Format: date-time
|
||||
* @description The date and time the user was last updated
|
||||
*/
|
||||
updatedAt?: string
|
||||
}
|
||||
/** @description Parameters for the Ideogram generation proxy request. Based on Ideogram's API. */
|
||||
IdeogramGenerateRequest: {
|
||||
/** @description The image generation request parameters. */
|
||||
image_request: {
|
||||
/** @description Required. The prompt to use to generate the image. */
|
||||
prompt: string
|
||||
/** @description Optional. The aspect ratio (e.g., 'ASPECT_16_9', 'ASPECT_1_1'). Cannot be used with resolution. Defaults to 'ASPECT_1_1' if unspecified. */
|
||||
aspect_ratio?: string
|
||||
/** @description Optional. The model used (e.g., 'V_2', 'V_2A_TURBO'). Defaults to 'V_2' if unspecified. */
|
||||
model?: string
|
||||
/** @description Optional. MagicPrompt usage ('AUTO', 'ON', 'OFF'). */
|
||||
magic_prompt_option?: string
|
||||
/**
|
||||
* Format: int64
|
||||
* @description Optional. A number between 0 and 2147483647.
|
||||
*/
|
||||
seed?: number
|
||||
/** @description Optional. Style type ('AUTO', 'GENERAL', 'REALISTIC', 'DESIGN', 'RENDER_3D', 'ANIME'). Only for models V_2 and above. */
|
||||
style_type?: string
|
||||
/** @description Optional. Description of what to exclude. Only for V_1, V_1_TURBO, V_2, V_2_TURBO. */
|
||||
negative_prompt?: string
|
||||
/**
|
||||
* @description Optional. Number of images to generate (1-8). Defaults to 1.
|
||||
* @default 1
|
||||
*/
|
||||
num_images: number
|
||||
/** @description Optional. Resolution (e.g., 'RESOLUTION_1024_1024'). Only for model V_2. Cannot be used with aspect_ratio. */
|
||||
resolution?: string
|
||||
/** @description Optional. Color palette object. Only for V_2, V_2_TURBO. */
|
||||
color_palette?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
/** @description Response from the Ideogram image generation API. */
|
||||
IdeogramGenerateResponse: {
|
||||
/**
|
||||
* Format: date-time
|
||||
* @description Timestamp when the generation was created.
|
||||
*/
|
||||
created?: string
|
||||
/** @description Array of generated image information. */
|
||||
data?: {
|
||||
/** @description The prompt used to generate this image. */
|
||||
prompt?: string
|
||||
/** @description The resolution of the generated image (e.g., '1024x1024'). */
|
||||
resolution?: string
|
||||
/** @description Indicates whether the image is considered safe. */
|
||||
is_image_safe?: boolean
|
||||
/** @description The seed value used for this generation. */
|
||||
seed?: number
|
||||
/** @description URL to the generated image. */
|
||||
url?: string
|
||||
/** @description The style type used for generation (e.g., 'REALISTIC', 'ANIME'). */
|
||||
style_type?: string
|
||||
}[]
|
||||
}
|
||||
}
|
||||
responses: never
|
||||
parameters: never
|
||||
@@ -1268,6 +1472,111 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
createCustomer: {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
requestBody?: never
|
||||
responses: {
|
||||
/** @description Customer created successfully */
|
||||
201: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['Customer']
|
||||
}
|
||||
}
|
||||
/** @description Bad request, invalid token or user already exists */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Unauthorized or invalid token */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
InitiateCreditPurchase: {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/**
|
||||
* Format: int64
|
||||
* @description the amount of the checkout transaction in micro value
|
||||
*/
|
||||
amount_micros?: number
|
||||
/** @description the currency used in the checkout transaction */
|
||||
currency?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
/** @description Customer Checkout created successfully */
|
||||
201: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': {
|
||||
/** @description the url to redirect the customer */
|
||||
checkout_url?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
/** @description Bad request, invalid token or user already exists */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Unauthorized or invalid token */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
getWorkflowResult: {
|
||||
parameters: {
|
||||
query?: never
|
||||
@@ -3417,4 +3726,80 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
ideogramGenerate: {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['IdeogramGenerateRequest']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
/** @description Successful response from Ideogram proxy */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['IdeogramGenerateResponse']
|
||||
}
|
||||
}
|
||||
/** @description Bad Request (invalid input to proxy) */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Rate limit exceeded (either from proxy or Ideogram) */
|
||||
429: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Internal Server Error (proxy or upstream issue) */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Bad Gateway (error communicating with Ideogram) */
|
||||
502: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Gateway Timeout (Ideogram took too long to respond) */
|
||||
504: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
275
tests-ui/tests/store/firebaseAuthStore.test.ts
Normal file
275
tests-ui/tests/store/firebaseAuthStore.test.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import * as firebaseAuth from 'firebase/auth'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import * as vuefire from 'vuefire'
|
||||
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
vi.mock('vuefire', () => ({
|
||||
useFirebaseAuth: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('firebase/auth', () => ({
|
||||
signInWithEmailAndPassword: vi.fn(),
|
||||
createUserWithEmailAndPassword: vi.fn(),
|
||||
signOut: vi.fn(),
|
||||
onAuthStateChanged: vi.fn()
|
||||
}))
|
||||
|
||||
describe('useFirebaseAuthStore', () => {
|
||||
let store: ReturnType<typeof useFirebaseAuthStore>
|
||||
let authStateCallback: (user: any) => void
|
||||
|
||||
const mockAuth = {
|
||||
/* mock Auth object */
|
||||
}
|
||||
|
||||
const mockUser = {
|
||||
uid: 'test-user-id',
|
||||
email: 'test@example.com',
|
||||
getIdToken: vi.fn().mockResolvedValue('mock-id-token')
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
|
||||
// Mock useFirebaseAuth to return our mock auth object
|
||||
vi.mocked(vuefire.useFirebaseAuth).mockReturnValue(mockAuth as any)
|
||||
|
||||
// Mock onAuthStateChanged to capture the callback and simulate initial auth state
|
||||
vi.mocked(firebaseAuth.onAuthStateChanged).mockImplementation(
|
||||
(_, callback) => {
|
||||
authStateCallback = callback as (user: any) => void
|
||||
// Call the callback with our mock user
|
||||
;(callback as (user: any) => void)(mockUser)
|
||||
// Return an unsubscribe function
|
||||
return vi.fn()
|
||||
}
|
||||
)
|
||||
|
||||
// Initialize Pinia
|
||||
setActivePinia(createPinia())
|
||||
store = useFirebaseAuthStore()
|
||||
})
|
||||
|
||||
it('should initialize with the current user', () => {
|
||||
expect(store.currentUser).toEqual(mockUser)
|
||||
expect(store.isAuthenticated).toBe(true)
|
||||
expect(store.userEmail).toBe('test@example.com')
|
||||
expect(store.userId).toBe('test-user-id')
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should properly clean up error state between operations', async () => {
|
||||
// First, cause an error
|
||||
const mockError = new Error('Invalid password')
|
||||
vi.mocked(firebaseAuth.signInWithEmailAndPassword).mockRejectedValueOnce(
|
||||
mockError
|
||||
)
|
||||
|
||||
try {
|
||||
await store.login('test@example.com', 'wrong-password')
|
||||
} catch (e) {
|
||||
// Error expected
|
||||
}
|
||||
|
||||
expect(store.error).toBe('Invalid password')
|
||||
|
||||
// Now, succeed on next attempt
|
||||
vi.mocked(firebaseAuth.signInWithEmailAndPassword).mockResolvedValueOnce({
|
||||
user: mockUser
|
||||
} as any)
|
||||
|
||||
await store.login('test@example.com', 'correct-password')
|
||||
|
||||
// Error should be cleared
|
||||
expect(store.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should handle auth initialization failure', async () => {
|
||||
// Mock auth as null to simulate initialization failure
|
||||
vi.mocked(vuefire.useFirebaseAuth).mockReturnValue(null)
|
||||
|
||||
// Create a new store instance
|
||||
setActivePinia(createPinia())
|
||||
const uninitializedStore = useFirebaseAuthStore()
|
||||
|
||||
// Check that isInitialized is false
|
||||
expect(uninitializedStore.isInitialized).toBe(false)
|
||||
|
||||
// Verify store actions throw appropriate errors
|
||||
await expect(
|
||||
uninitializedStore.login('test@example.com', 'password')
|
||||
).rejects.toThrow('Firebase Auth not initialized')
|
||||
})
|
||||
|
||||
describe('login', () => {
|
||||
it('should login with valid credentials', async () => {
|
||||
const mockUserCredential = { user: mockUser }
|
||||
vi.mocked(firebaseAuth.signInWithEmailAndPassword).mockResolvedValue(
|
||||
mockUserCredential as any
|
||||
)
|
||||
|
||||
const result = await store.login('test@example.com', 'password')
|
||||
|
||||
expect(firebaseAuth.signInWithEmailAndPassword).toHaveBeenCalledWith(
|
||||
mockAuth,
|
||||
'test@example.com',
|
||||
'password'
|
||||
)
|
||||
expect(result).toEqual(mockUserCredential)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should handle login errors', async () => {
|
||||
const mockError = new Error('Invalid password')
|
||||
vi.mocked(firebaseAuth.signInWithEmailAndPassword).mockRejectedValue(
|
||||
mockError
|
||||
)
|
||||
|
||||
await expect(
|
||||
store.login('test@example.com', 'wrong-password')
|
||||
).rejects.toThrow('Invalid password')
|
||||
|
||||
expect(firebaseAuth.signInWithEmailAndPassword).toHaveBeenCalledWith(
|
||||
mockAuth,
|
||||
'test@example.com',
|
||||
'wrong-password'
|
||||
)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe('Invalid password')
|
||||
})
|
||||
})
|
||||
|
||||
describe('register', () => {
|
||||
it('should register a new user', async () => {
|
||||
const mockUserCredential = { user: mockUser }
|
||||
vi.mocked(firebaseAuth.createUserWithEmailAndPassword).mockResolvedValue(
|
||||
mockUserCredential as any
|
||||
)
|
||||
|
||||
const result = await store.register('new@example.com', 'password')
|
||||
|
||||
expect(firebaseAuth.createUserWithEmailAndPassword).toHaveBeenCalledWith(
|
||||
mockAuth,
|
||||
'new@example.com',
|
||||
'password'
|
||||
)
|
||||
expect(result).toEqual(mockUserCredential)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe(null)
|
||||
})
|
||||
|
||||
it('should handle registration errors', async () => {
|
||||
const mockError = new Error('Email already in use')
|
||||
vi.mocked(firebaseAuth.createUserWithEmailAndPassword).mockRejectedValue(
|
||||
mockError
|
||||
)
|
||||
|
||||
await expect(
|
||||
store.register('existing@example.com', 'password')
|
||||
).rejects.toThrow('Email already in use')
|
||||
|
||||
expect(firebaseAuth.createUserWithEmailAndPassword).toHaveBeenCalledWith(
|
||||
mockAuth,
|
||||
'existing@example.com',
|
||||
'password'
|
||||
)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBe('Email already in use')
|
||||
})
|
||||
|
||||
it('should handle concurrent login attempts correctly', async () => {
|
||||
// Set up multiple login promises
|
||||
const mockUserCredential = { user: mockUser }
|
||||
vi.mocked(firebaseAuth.signInWithEmailAndPassword).mockResolvedValue(
|
||||
mockUserCredential as any
|
||||
)
|
||||
|
||||
const loginPromise1 = store.login('user1@example.com', 'password1')
|
||||
const loginPromise2 = store.login('user2@example.com', 'password2')
|
||||
|
||||
// Resolve both promises
|
||||
await Promise.all([loginPromise1, loginPromise2])
|
||||
|
||||
// Verify the loading state is reset
|
||||
expect(store.loading).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('logout', () => {
|
||||
it('should sign out the user', async () => {
|
||||
vi.mocked(firebaseAuth.signOut).mockResolvedValue(undefined)
|
||||
|
||||
await store.logout()
|
||||
|
||||
expect(firebaseAuth.signOut).toHaveBeenCalledWith(mockAuth)
|
||||
})
|
||||
|
||||
it('should handle logout errors', async () => {
|
||||
const mockError = new Error('Network error')
|
||||
vi.mocked(firebaseAuth.signOut).mockRejectedValue(mockError)
|
||||
|
||||
await expect(store.logout()).rejects.toThrow('Network error')
|
||||
|
||||
expect(firebaseAuth.signOut).toHaveBeenCalledWith(mockAuth)
|
||||
expect(store.error).toBe('Network error')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getIdToken', () => {
|
||||
it('should return the user ID token', async () => {
|
||||
// FIX 2: Reset the mock and set a specific return value
|
||||
mockUser.getIdToken.mockReset()
|
||||
mockUser.getIdToken.mockResolvedValue('mock-id-token')
|
||||
|
||||
const token = await store.getIdToken()
|
||||
|
||||
expect(mockUser.getIdToken).toHaveBeenCalled()
|
||||
expect(token).toBe('mock-id-token')
|
||||
})
|
||||
|
||||
it('should return null when no user is logged in', async () => {
|
||||
// Simulate logged out state
|
||||
authStateCallback(null)
|
||||
|
||||
const token = await store.getIdToken()
|
||||
|
||||
expect(token).toBeNull()
|
||||
})
|
||||
|
||||
it('should return null for token after login and logout sequence', async () => {
|
||||
// Setup mock for login
|
||||
const mockUserCredential = { user: mockUser }
|
||||
vi.mocked(firebaseAuth.signInWithEmailAndPassword).mockResolvedValue(
|
||||
mockUserCredential as any
|
||||
)
|
||||
|
||||
// Login
|
||||
await store.login('test@example.com', 'password')
|
||||
|
||||
// Simulate successful auth state update after login
|
||||
authStateCallback(mockUser)
|
||||
|
||||
// Verify we're logged in and can get a token
|
||||
mockUser.getIdToken.mockReset()
|
||||
mockUser.getIdToken.mockResolvedValue('mock-id-token')
|
||||
expect(await store.getIdToken()).toBe('mock-id-token')
|
||||
|
||||
// Setup mock for logout
|
||||
vi.mocked(firebaseAuth.signOut).mockResolvedValue(undefined)
|
||||
|
||||
// Logout
|
||||
await store.logout()
|
||||
|
||||
// Simulate successful auth state update after logout
|
||||
authStateCallback(null)
|
||||
|
||||
// Verify token is null after logout
|
||||
const tokenAfterLogout = await store.getIdToken()
|
||||
expect(tokenAfterLogout).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -8,7 +8,7 @@
|
||||
"incremental": true,
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "Node",
|
||||
"moduleResolution": "bundler",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"resolveJsonModule": true,
|
||||
|
||||
Reference in New Issue
Block a user