mirror of
https://github.com/supabase/supabase.git
synced 2026-10-18 15:49:09 +03:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? This PR changes the logic for show/hide password icon in the sign up form. Fix #39399 and #41459 ## What is the current behavior? For Password Field Eye Icon Label <ul> <li>When password is hidden: Button shows "Hide password" label (incorrect). <li>When password is visible: Button shows "Show password" label (incorrect) </ul> For Edge Functions Secrets <ul> <li>All secret values are always visible in plain text <li>No visibility toggle functionality exists <li>No way to hide/show individual secret values </ul> ## What is the new behavior? Sign-up Form Password Field: <ul> <li>When password is hidden: Button correctly shows "Show password" label <li>When password is visible: Button correctly shows "Hide password" label</ul> Labels now accurately describe what clicking the button will do <p></p> Edge Functions Secrets: <ul> <li>Secret values are hidden by default (security-first approach) <li>Each secret has its own independent visibility toggle button <li>Hidden secrets display as bullet points (•••••••••••••••••••••) <li>Eye icon appears when secret is hidden - click to reveal <li>EyeOff icon appears when secret is visible - click to hide <li>Tooltip shows "Hidden" when value is masked, actual value when visible</ul> --------- Co-authored-by: Gildas Garcia <1122076+djhi@users.noreply.github.com> Co-authored-by: Alaister Young <a@alaisteryoung.com>
233 lines
8.0 KiB
TypeScript
233 lines
8.0 KiB
TypeScript
import HCaptcha from '@hcaptcha/react-hcaptcha'
|
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { useSignUpMutation } from 'data/misc/signup-mutation'
|
|
import { motion } from 'framer-motion'
|
|
import { BASE_PATH } from 'lib/constants'
|
|
import { buildPathWithParams } from 'lib/gotrue'
|
|
import { CheckCircle, Eye, EyeOff } from 'lucide-react'
|
|
import { useRouter } from 'next/router'
|
|
import { parseAsString, useQueryStates } from 'nuqs'
|
|
import { useRef, useState } from 'react'
|
|
import { SubmitHandler, useForm } from 'react-hook-form'
|
|
import { toast } from 'sonner'
|
|
import {
|
|
Alert_Shadcn_,
|
|
AlertDescription_Shadcn_,
|
|
AlertTitle_Shadcn_,
|
|
Button,
|
|
cn,
|
|
Form_Shadcn_,
|
|
FormControl_Shadcn_,
|
|
FormField_Shadcn_,
|
|
Input_Shadcn_,
|
|
} from 'ui'
|
|
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
|
import z from 'zod'
|
|
|
|
import PasswordConditionsHelper from './PasswordConditionsHelper'
|
|
|
|
const schema = z.object({
|
|
email: z.string().min(1, 'Email is required').email('Must be a valid email'),
|
|
password: z
|
|
.string()
|
|
.min(1, 'Password is required')
|
|
.max(72, 'Password cannot exceed 72 characters')
|
|
.refine((password) => password.length >= 8, 'Password must be at least 8 characters')
|
|
.refine(
|
|
(password) => /[A-Z]/.test(password),
|
|
'Password must contain at least 1 uppercase character'
|
|
)
|
|
.refine(
|
|
(password) => /[a-z]/.test(password),
|
|
'Password must contain at least 1 lowercase character'
|
|
)
|
|
.refine((password) => /[0-9]/.test(password), 'Password must contain at least 1 number')
|
|
.refine(
|
|
(password) => /[!@#$%^&*()_+\-=\[\]{};`':"\\|,.<>\/?]/.test(password),
|
|
'Password must contain at least 1 symbol'
|
|
),
|
|
})
|
|
|
|
const formId = 'sign-up-form'
|
|
|
|
export const SignUpForm = () => {
|
|
const captchaRef = useRef<HCaptcha>(null)
|
|
const [showConditions, setShowConditions] = useState(false)
|
|
const [isSubmitted, setIsSubmitted] = useState(false)
|
|
const [passwordHidden, setPasswordHidden] = useState(true)
|
|
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
|
|
const router = useRouter()
|
|
const form = useForm<z.infer<typeof schema>>({
|
|
resolver: zodResolver(schema),
|
|
defaultValues: { email: '', password: '' },
|
|
})
|
|
|
|
const [searchParams] = useQueryStates({
|
|
auth_id: parseAsString.withDefault(''),
|
|
token: parseAsString.withDefault(''),
|
|
})
|
|
|
|
const { mutate: signup, isPending: isSigningUp } = useSignUpMutation({
|
|
onSuccess: () => {
|
|
toast.success(`Signed up successfully!`)
|
|
setIsSubmitted(true)
|
|
},
|
|
onError: (error) => {
|
|
setCaptchaToken(null)
|
|
captchaRef.current?.resetCaptcha()
|
|
toast.error(`Failed to sign up: ${error.message}`)
|
|
},
|
|
})
|
|
|
|
const onSubmit: SubmitHandler<z.infer<typeof schema>> = async ({ email, password }) => {
|
|
// [Joshen] Separate submitting state as there's 2 async processes here
|
|
let token = captchaToken
|
|
if (!token) {
|
|
const captchaResponse = await captchaRef.current?.execute({ async: true })
|
|
token = captchaResponse?.response ?? null
|
|
}
|
|
|
|
const isInsideOAuthFlow = !!searchParams.auth_id
|
|
const redirectUrlBase = `${
|
|
process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
|
|
? location.origin
|
|
: process.env.NEXT_PUBLIC_SITE_URL
|
|
}${BASE_PATH}`
|
|
|
|
let redirectTo: string
|
|
|
|
if (isInsideOAuthFlow) {
|
|
redirectTo = `${redirectUrlBase}/authorize?auth_id=${searchParams.auth_id}${searchParams.token && `&token=${searchParams.token}`}`
|
|
} else {
|
|
// Use getRedirectToPath to handle redirect_to parameter and other query params
|
|
const { returnTo } = router.query
|
|
const basePath = returnTo || '/sign-in'
|
|
const fullPath = buildPathWithParams(basePath as string)
|
|
const fullRedirectUrl = `${redirectUrlBase}${fullPath}`
|
|
redirectTo = fullRedirectUrl
|
|
}
|
|
|
|
signup({
|
|
email,
|
|
password,
|
|
hcaptchaToken: token ?? null,
|
|
redirectTo,
|
|
})
|
|
}
|
|
|
|
const password = form.watch('password')
|
|
const isSubmitting = form.formState.isSubmitting || isSigningUp
|
|
|
|
return (
|
|
<div className="relative">
|
|
{isSubmitted && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ duration: 0.5, delay: 0.3 }}
|
|
className="absolute top-0 w-full"
|
|
>
|
|
<Alert_Shadcn_ variant="default">
|
|
<CheckCircle />
|
|
<AlertTitle_Shadcn_>Check your email to confirm</AlertTitle_Shadcn_>
|
|
<AlertDescription_Shadcn_ className="text-xs">
|
|
You've successfully signed up. Please check your email to confirm your account before
|
|
signing in to the Supabase dashboard. The confirmation link expires in 10 minutes.
|
|
</AlertDescription_Shadcn_>
|
|
</Alert_Shadcn_>
|
|
</motion.div>
|
|
)}
|
|
<div
|
|
className={cn(
|
|
'w-full py-1 transition-all duration-500',
|
|
isSubmitted ? 'max-h-[100px] opacity-0 pointer-events-none' : 'max-h-[1000px] opacity-100'
|
|
)}
|
|
>
|
|
<Form_Shadcn_ {...form}>
|
|
<form id={formId} className="flex flex-col gap-4" onSubmit={form.handleSubmit(onSubmit)}>
|
|
<FormField_Shadcn_
|
|
key="email"
|
|
name="email"
|
|
control={form.control}
|
|
render={({ field }) => (
|
|
<FormItemLayout name="email" label="Email">
|
|
<FormControl_Shadcn_>
|
|
<Input_Shadcn_
|
|
id="email"
|
|
autoComplete="email"
|
|
disabled={isSubmitting}
|
|
{...field}
|
|
placeholder="you@example.com"
|
|
/>
|
|
</FormControl_Shadcn_>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
|
|
<FormField_Shadcn_
|
|
key="password"
|
|
name="password"
|
|
control={form.control}
|
|
render={({ field }) => (
|
|
<FormItemLayout name="password" label="Password">
|
|
<FormControl_Shadcn_>
|
|
<div className="relative">
|
|
<Input_Shadcn_
|
|
id="password"
|
|
type={passwordHidden ? 'password' : 'text'}
|
|
autoComplete="new-password"
|
|
placeholder="••••••••"
|
|
{...field}
|
|
onFocus={() => setShowConditions(true)}
|
|
disabled={isSubmitting}
|
|
/>
|
|
<Button
|
|
type="default"
|
|
title={passwordHidden ? `Show password` : `Hide password`}
|
|
aria-label={passwordHidden ? `Show password` : `Hide password`}
|
|
className="absolute right-1 top-1 px-1.5"
|
|
icon={passwordHidden ? <Eye /> : <EyeOff />}
|
|
disabled={isSubmitting}
|
|
onClick={() => setPasswordHidden((prev) => !prev)}
|
|
/>
|
|
</div>
|
|
</FormControl_Shadcn_>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
|
|
<div
|
|
className={`${
|
|
showConditions ? 'max-h-[500px]' : 'max-h-[0px]'
|
|
} transition-all duration-400 overflow-y-hidden`}
|
|
>
|
|
<PasswordConditionsHelper password={password} />
|
|
</div>
|
|
|
|
<div className="self-center">
|
|
<HCaptcha
|
|
ref={captchaRef}
|
|
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
|
|
size="invisible"
|
|
onVerify={(token) => setCaptchaToken(token)}
|
|
onExpire={() => setCaptchaToken(null)}
|
|
/>
|
|
</div>
|
|
|
|
<Button
|
|
block
|
|
form={formId}
|
|
htmlType="submit"
|
|
size="large"
|
|
disabled={password.length === 0 || isSubmitting}
|
|
loading={isSubmitting}
|
|
>
|
|
Sign up
|
|
</Button>
|
|
</form>
|
|
</Form_Shadcn_>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|