Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export const AccountIdentities = () => {
<div className="flex items-center gap-x-1">
{provider === 'email' && (
<Button asChild type="default">
<Link href="/reset-password">Reset password</Link>
<Link href="/reset-password?type=change">Change password</Link>
</Button>
)}
<ButtonTooltip
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ export const ReportBlock = ({
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchIntervalInBackground: false,
retry: (failureCount: number) => {
if (failureCount >= 2) return false
retry: (failureCount: number, error) => {
if (error.code === 404 || failureCount >= 2) return false
return true
},
}
Expand Down Expand Up @@ -151,18 +151,16 @@ export const ReportBlock = ({
? String(executeSqlError)
: undefined
}
isExecuting={executeSqlLoading}
isExecuting={!contentError && executeSqlLoading}
isWriteQuery={isWriteQuery}
actions={
!isLoadingContent && (
<ButtonTooltip
type="text"
icon={<X />}
className="w-7 h-7"
onClick={() => onRemoveChart({ metric: { key: item.attribute } })}
tooltip={{ content: { side: 'bottom', text: 'Remove chart' } }}
/>
)
<ButtonTooltip
type="text"
icon={<X />}
className="w-7 h-7"
onClick={() => onRemoveChart({ metric: { key: item.attribute } })}
tooltip={{ content: { side: 'bottom', text: 'Remove chart' } }}
/>
}
onExecute={(queryType) => {
refetch()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import HCaptcha from '@hcaptcha/react-hcaptcha'
import { zodResolver } from '@hookform/resolvers/zod'
import { useResetPasswordMutation } from 'data/misc/reset-password-mutation'
import { BASE_PATH } from 'lib/constants'
import { auth } from 'lib/gotrue'
import { useRouter } from 'next/router'
import { useRef, useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { toast } from 'sonner'
import * as z from 'zod'

import { useResetPasswordMutation } from 'data/misc/reset-password-mutation'
import { BASE_PATH } from 'lib/constants'
import { auth } from 'lib/gotrue'
import { Button, Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, Input_Shadcn_ } from 'ui'
import { Admonition } from 'ui-patterns'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import * as z from 'zod'

const forgotPasswordSchema = z.object({
email: z.string().min(1, 'Please provide an email address').email('Must be a valid email'),
Expand Down
125 changes: 81 additions & 44 deletions apps/studio/components/interfaces/SignIn/ResetPasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,66 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { useParams } from 'common'
import { captureCriticalError } from 'lib/error-reporting'
import { auth, getReturnToPath } from 'lib/gotrue'
import { Eye, EyeOff } from 'lucide-react'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { Button, cn, Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, Separator } from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { z } from 'zod'

import { captureCriticalError } from 'lib/error-reporting'
import { auth, getReturnToPath } from 'lib/gotrue'
import {
Button,
Form_Shadcn_,
FormControl_Shadcn_,
FormField_Shadcn_,
FormItem_Shadcn_,
FormMessage_Shadcn_,
Input,
} from 'ui'

import PasswordConditionsHelper from './PasswordConditionsHelper'

// Convert the existing yup passwordSchema to Zod
const passwordValidation = z
.string()
.min(1, 'Password is required')
.max(72, 'Password cannot exceed 72 characters')
.refine((password) => {
const hasUppercase = /[A-Z]/.test(password)
const hasLowercase = /[a-z]/.test(password)
const hasNumber = /[0-9]/.test(password)
const hasSpecialChar = /[!@#$%^&*()_+\-=\[\]{};`':"\\|,.<>\/?]/.test(password)
const isLongEnough = password.length >= 8

return hasUppercase && hasLowercase && hasNumber && hasSpecialChar && isLongEnough
}, 'Password must contain at least 8 characters, including uppercase, lowercase, number, and special character')

const passwordSchema = z.object({
password: z
.string()
.min(1, 'Password is required')
.max(72, 'Password cannot exceed 72 characters')
.refine((password) => {
// Basic password validation - you can enhance this based on your requirements
const hasUppercase = /[A-Z]/.test(password)
const hasLowercase = /[a-z]/.test(password)
const hasNumber = /[0-9]/.test(password)
const hasSpecialChar = /[!@#$%^&*()_+\-=\[\]{};`':"\\|,.<>\/?]/.test(password)
const isLongEnough = password.length >= 8

return hasUppercase && hasLowercase && hasNumber && hasSpecialChar && isLongEnough
}, 'Password must contain at least 8 characters, including uppercase, lowercase, number, and special character'),
currentPassword: z.string().min(1, 'Current password is required'),
password: passwordValidation,
})

const recoveryPasswordSchema = z.object({
currentPassword: z.string().optional(),
password: passwordValidation,
})

type FormData = z.infer<typeof passwordSchema>

const ResetPasswordForm = () => {
export const ResetPasswordForm = () => {
const router = useRouter()
const { type } = useParams()
const requireCurrentPassword = type === 'change'

const [showConditions, setShowConditions] = useState(false)
const [passwordHidden, setPasswordHidden] = useState(true)
const [currentPasswordHidden, setCurrentPasswordHidden] = useState(true)

const form = useForm<FormData>({
resolver: zodResolver(passwordSchema),
defaultValues: {
password: '',
},
resolver: zodResolver(requireCurrentPassword ? passwordSchema : recoveryPasswordSchema),
defaultValues: { password: '', currentPassword: '' },
mode: 'onChange',
})

const onResetPassword = async (data: FormData) => {
const toastId = toast.loading('Saving password...')
const { error } = await auth.updateUser({ password: data.password })
const { error } = await auth.updateUser({
password: data.password,
...(requireCurrentPassword ? { current_password: data.currentPassword } : {}),
})

if (!error) {
toast.success('Password saved successfully!', { id: toastId })
Expand All @@ -72,16 +77,46 @@ const ResetPasswordForm = () => {
return (
<Form_Shadcn_ {...form}>
<form onSubmit={form.handleSubmit(onResetPassword)} className="space-y-4 pt-4">
{requireCurrentPassword && (
<FormField_Shadcn_
control={form.control}
name="currentPassword"
render={({ field }) => (
<FormItemLayout label="Current password">
<FormControl_Shadcn_>
<Input
id="currentPassword"
type={currentPasswordHidden ? 'password' : 'text'}
placeholder="&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;"
disabled={form.formState.isSubmitting}
actions={
<Button
icon={currentPasswordHidden ? <Eye /> : <EyeOff />}
type="default"
className="w-7"
onClick={() => setCurrentPasswordHidden((prev) => !prev)}
/>
}
{...field}
onBlur={() => {
field.onBlur()
setCurrentPasswordHidden(true)
}}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
)}
<FormField_Shadcn_
control={form.control}
name="password"
render={({ field }) => (
<FormItem_Shadcn_>
<FormItemLayout label="Password">
<FormControl_Shadcn_>
<Input
id="password"
type={passwordHidden ? 'password' : 'text'}
label="Password"
placeholder="&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;"
disabled={form.formState.isSubmitting}
onFocus={() => setShowConditions(true)}
Expand All @@ -90,27 +125,31 @@ const ResetPasswordForm = () => {
<Button
icon={passwordHidden ? <Eye /> : <EyeOff />}
type="default"
className="!mr-1"
className="w-7"
onClick={() => setPasswordHidden((prev) => !prev)}
/>
}
{...field}
onBlur={() => {
field.onBlur()
setPasswordHidden(true)
}}
/>
</FormControl_Shadcn_>
<FormMessage_Shadcn_ />
</FormItem_Shadcn_>
</FormItemLayout>
)}
/>

<div
className={`${
showConditions ? 'max-h-[500px]' : 'max-h-[0px]'
} transition-all duration-400 overflow-y-hidden`}
className={cn(
showConditions ? 'max-h-[500px]' : 'max-h-[0px]',
'transition-all duration-400 overflow-y-hidden'
)}
>
<PasswordConditionsHelper password={form.watch('password')} />
</div>

<div className="border-overlay-border border-t" />
<Separator className="bg-border" />

<Button
block
Expand All @@ -125,5 +164,3 @@ const ResetPasswordForm = () => {
</Form_Shadcn_>
)
}

export default ResetPasswordForm
16 changes: 8 additions & 8 deletions apps/studio/components/interfaces/SignIn/SignInMfaForm.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { SupportCategories } from '@supabase/shared-types/out/constants'
import type { Factor } from '@supabase/supabase-js'
import { useQueryClient } from '@tanstack/react-query'
import { Lock } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import z from 'zod'

import { SupportCategories } from '@supabase/shared-types/out/constants'
import { useAuthError } from 'common'
import AlertError from 'components/ui/AlertError'
import { useMfaChallengeAndVerifyMutation } from 'data/profile/mfa-challenge-and-verify-mutation'
import { useMfaListFactorsQuery } from 'data/profile/mfa-list-factors-query'
import { useSignOut } from 'lib/auth'
import { getReturnToPath } from 'lib/gotrue'
import { Lock } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { Button, Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, Input_Shadcn_ } from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import z from 'zod'

import { SupportLink } from '../Support/SupportLink'

const schema = z.object({
Expand Down
Loading
Loading