[Auth] Allow change password in user panel (#3699)

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Chenlei Hu
2025-04-30 16:12:57 -04:00
committed by GitHub
parent a43d1e1ee8
commit 834d5820d2
15 changed files with 305 additions and 169 deletions

View File

@@ -0,0 +1,47 @@
<template>
<Form
class="flex flex-col gap-6 w-96"
:resolver="zodResolver(updatePasswordSchema)"
@submit="onSubmit"
>
<PasswordFields />
<!-- Submit Button -->
<Button
type="submit"
:label="$t('userSettings.updatePassword')"
class="h-10 font-medium mt-4"
:loading="loading"
/>
</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 { ref } from 'vue'
import PasswordFields from '@/components/dialog/content/signin/PasswordFields.vue'
import { updatePasswordSchema } from '@/schemas/signInSchema'
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
const authService = useFirebaseAuthService()
const loading = ref(false)
const { onSuccess } = defineProps<{
onSuccess: () => void
}>()
const onSubmit = async (event: FormSubmitEvent) => {
if (event.valid) {
loading.value = true
try {
await authService.updatePassword(event.values.password)
onSuccess()
} finally {
loading.value = false
}
}
}
</script>

View File

@@ -38,6 +38,17 @@
<div class="text-muted flex items-center gap-1">
<i :class="providerIcon" />
{{ providerName }}
<Button
v-if="isEmailProvider"
v-tooltip="{
value: $t('userSettings.updatePassword'),
showDelay: 300
}"
icon="pi pi-pen-to-square"
severity="secondary"
text
@click="dialogService.showUpdatePasswordDialog()"
/>
</div>
</div>
@@ -83,9 +94,11 @@ import ProgressSpinner from 'primevue/progressspinner'
import TabPanel from 'primevue/tabpanel'
import { computed } from 'vue'
import { useDialogService } from '@/services/dialogService'
import { useCommandStore } from '@/stores/commandStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
const dialogService = useDialogService()
const authStore = useFirebaseAuthStore()
const commandStore = useCommandStore()
const user = computed(() => authStore.currentUser)
@@ -113,6 +126,11 @@ const providerIcon = computed(() => {
return 'pi pi-user'
})
const isEmailProvider = computed(() => {
const providerId = user.value?.providerData[0]?.providerId
return providerId === 'password'
})
const handleSignOut = async () => {
await commandStore.execute('Comfy.User.SignOut')
}

View File

@@ -0,0 +1,111 @@
<template>
<!-- Password Field -->
<FormField v-slot="$field" name="password" 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': $field.invalid }"
fluid
class="h-10"
/>
<div class="flex flex-col gap-1">
<small v-if="$field.dirty || $field.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>
</FormField>
<!-- Confirm Password Field -->
<FormField v-slot="$field" name="confirmPassword" 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': $field.invalid }"
fluid
class="h-10"
/>
<small v-if="$field.error" class="text-red-500">{{
$field.error.message
}}</small>
</FormField>
</template>
<script setup lang="ts">
import { FormField } from '@primevue/forms'
import Password from 'primevue/password'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
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)
}))
</script>

View File

@@ -1,12 +1,11 @@
<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">
<FormField v-slot="$field" name="email" class="flex flex-col gap-2">
<label
class="opacity-80 text-base font-medium mb-2"
for="comfy-org-sign-up-email"
@@ -17,116 +16,27 @@
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"
:invalid="$field.invalid"
/>
<small v-if="$form.email?.invalid" class="text-red-500">{{
$form.email.error.message
<small v-if="$field.error" class="text-red-500">{{
$field.error.message
}}</small>
</div>
</FormField>
<!-- 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>
<PasswordFields />
<!-- Personal Data Consent Checkbox -->
<div class="flex items-center gap-2">
<FormField
v-slot="$field"
name="personalDataConsent"
class="flex items-center gap-2"
>
<Checkbox
input-id="comfy-org-sign-up-personal-data-consent"
name="personalDataConsent"
:binary="true"
:invalid="$form.personalDataConsent?.invalid"
:invalid="$field.invalid"
/>
<label
for="comfy-org-sign-up-personal-data-consent"
@@ -134,10 +44,10 @@
>
{{ t('auth.signup.personalDataConsentLabel') }}
</label>
</div>
<small v-if="$form.personalDataConsent?.error" class="text-red-500 -mt-4">{{
$form.personalDataConsent.error.message
}}</small>
<small v-if="$field.error" class="text-red-500 -mt-4">{{
$field.error.message
}}</small>
</FormField>
<!-- Submit Button -->
<Button
@@ -149,29 +59,18 @@
</template>
<script setup lang="ts">
import { Form, FormSubmitEvent } from '@primevue/forms'
import { Form, FormField, FormSubmitEvent } from '@primevue/forms'
import { zodResolver } from '@primevue/forms/resolvers/zod'
import Button from 'primevue/button'
import Checkbox from 'primevue/checkbox'
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('')
import PasswordFields from './PasswordFields.vue'
// 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 { t } = useI18n()
const emit = defineEmits<{
submit: [values: SignUpData]