feat: support disable credential login

This commit is contained in:
javayhu 2025-07-18 22:04:39 +08:00
parent 716eac324f
commit 1be38e3e8d
6 changed files with 213 additions and 176 deletions

View File

@ -1,12 +1,17 @@
import { DeleteAccountCard } from '@/components/settings/security/delete-account-card';
import { PasswordCardWrapper } from '@/components/settings/security/password-card-wrapper';
import { websiteConfig } from '@/config/website';
export default function SecurityPage() {
const credentialLoginEnabled = websiteConfig.auth.enableCredentialLogin;
return (
<div className="flex flex-col gap-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<PasswordCardWrapper />
</div>
{credentialLoginEnabled && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<PasswordCardWrapper />
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<DeleteAccountCard />
</div>

View File

@ -57,6 +57,9 @@ export const LoginForm = ({
const [isPending, setIsPending] = useState(false);
const [showPassword, setShowPassword] = useState(false);
// Check if credential login is enabled
const credentialLoginEnabled = websiteConfig.auth.enableCredentialLogin;
// turnstile captcha schema
const turnstileEnabled = websiteConfig.features.enableTurnstileCaptcha;
const captchaSiteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY;
@ -148,102 +151,111 @@ export const LoginForm = ({
bottomButtonHref={`${Routes.Register}`}
className={cn('', className)}
>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<div className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{t('email')}</FormLabel>
<FormControl>
<Input
{...field}
disabled={isPending}
placeholder="name@example.com"
type="email"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<div className="flex justify-between items-center">
<FormLabel>{t('password')}</FormLabel>
<Button
size="sm"
variant="link"
asChild
className="px-0 font-normal text-muted-foreground"
>
<LocaleLink
href={`${Routes.ForgotPassword}`}
className="text-xs hover:underline hover:underline-offset-4 hover:text-primary"
>
{t('forgotPassword')}
</LocaleLink>
</Button>
</div>
<FormControl>
<div className="relative">
{credentialLoginEnabled && (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<div className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{t('email')}</FormLabel>
<FormControl>
<Input
{...field}
disabled={isPending}
placeholder="******"
type={showPassword ? 'text' : 'password'}
className="pr-10"
placeholder="name@example.com"
type="email"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<div className="flex justify-between items-center">
<FormLabel>{t('password')}</FormLabel>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={togglePasswordVisibility}
disabled={isPending}
variant="link"
asChild
className="px-0 font-normal text-muted-foreground"
>
{showPassword ? (
<EyeOffIcon className="size-4 text-muted-foreground" />
) : (
<EyeIcon className="size-4 text-muted-foreground" />
)}
<span className="sr-only">
{showPassword ? t('hidePassword') : t('showPassword')}
</span>
<LocaleLink
href={`${Routes.ForgotPassword}`}
className="text-xs hover:underline hover:underline-offset-4 hover:text-primary"
>
{t('forgotPassword')}
</LocaleLink>
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
<FormControl>
<div className="relative">
<Input
{...field}
disabled={isPending}
placeholder="******"
type={showPassword ? 'text' : 'password'}
className="pr-10"
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={togglePasswordVisibility}
disabled={isPending}
>
{showPassword ? (
<EyeOffIcon className="size-4 text-muted-foreground" />
) : (
<EyeIcon className="size-4 text-muted-foreground" />
)}
<span className="sr-only">
{showPassword
? t('hidePassword')
: t('showPassword')}
</span>
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormError message={error || urlError || undefined} />
<FormSuccess message={success} />
{captchaConfigured && (
<Captcha
onSuccess={(token) => form.setValue('captchaToken', token)}
validationError={form.formState.errors.captchaToken?.message}
/>
)}
<Button
disabled={isPending || (captchaConfigured && !captchaToken)}
size="lg"
type="submit"
className="w-full flex items-center justify-center gap-2 cursor-pointer"
>
{isPending && (
<Loader2Icon className="mr-2 size-4 animate-spin" />
)}
/>
</div>
<FormError message={error || urlError || undefined} />
<FormSuccess message={success} />
{captchaConfigured && (
<Captcha
onSuccess={(token) => form.setValue('captchaToken', token)}
validationError={form.formState.errors.captchaToken?.message}
/>
)}
<Button
disabled={isPending || (captchaConfigured && !captchaToken)}
size="lg"
type="submit"
className="w-full flex items-center justify-center gap-2 cursor-pointer"
>
{isPending && <Loader2Icon className="mr-2 size-4 animate-spin" />}
<span>{t('signIn')}</span>
</Button>
</form>
</Form>
<span>{t('signIn')}</span>
</Button>
</form>
</Form>
)}
<div className="mt-4">
<SocialLoginButton callbackUrl={callbackUrl} />
<SocialLoginButton
callbackUrl={callbackUrl}
showDivider={credentialLoginEnabled}
/>
</div>
</AuthCard>
);

View File

@ -52,6 +52,9 @@ export const RegisterForm = ({
const [isPending, setIsPending] = useState(false);
const [showPassword, setShowPassword] = useState(false);
// Check if credential login is enabled
const credentialLoginEnabled = websiteConfig.auth.enableCredentialLogin;
// turnstile captcha schema
const turnstileEnabled = websiteConfig.features.enableTurnstileCaptcha;
const captchaSiteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY;
@ -156,100 +159,113 @@ export const RegisterForm = ({
bottomButtonLabel={t('signInHint')}
bottomButtonHref={`${Routes.Login}`}
>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<div className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t('name')}</FormLabel>
<FormControl>
<Input {...field} disabled={isPending} placeholder="name" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{t('email')}</FormLabel>
<FormControl>
<Input
{...field}
disabled={isPending}
placeholder="name@example.com"
type="email"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>{t('password')}</FormLabel>
<FormControl>
<div className="relative">
{credentialLoginEnabled && (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<div className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t('name')}</FormLabel>
<FormControl>
<Input
{...field}
disabled={isPending}
placeholder="******"
type={showPassword ? 'text' : 'password'}
className="pr-10"
placeholder="name"
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={togglePasswordVisibility}
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{t('email')}</FormLabel>
<FormControl>
<Input
{...field}
disabled={isPending}
>
{showPassword ? (
<EyeOffIcon className="size-4 text-muted-foreground" />
) : (
<EyeIcon className="size-4 text-muted-foreground" />
)}
<span className="sr-only">
{showPassword ? t('hidePassword') : t('showPassword')}
</span>
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
placeholder="name@example.com"
type="email"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>{t('password')}</FormLabel>
<FormControl>
<div className="relative">
<Input
{...field}
disabled={isPending}
placeholder="******"
type={showPassword ? 'text' : 'password'}
className="pr-10"
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={togglePasswordVisibility}
disabled={isPending}
>
{showPassword ? (
<EyeOffIcon className="size-4 text-muted-foreground" />
) : (
<EyeIcon className="size-4 text-muted-foreground" />
)}
<span className="sr-only">
{showPassword
? t('hidePassword')
: t('showPassword')}
</span>
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormError message={error} />
<FormSuccess message={success} />
{captchaConfigured && (
<Captcha
onSuccess={(token) => form.setValue('captchaToken', token)}
validationError={form.formState.errors.captchaToken?.message}
/>
)}
<Button
disabled={isPending || (captchaConfigured && !captchaToken)}
size="lg"
type="submit"
className="cursor-pointer w-full flex items-center justify-center gap-2"
>
{isPending && (
<Loader2Icon className="mr-2 size-4 animate-spin" />
)}
/>
</div>
<FormError message={error} />
<FormSuccess message={success} />
{captchaConfigured && (
<Captcha
onSuccess={(token) => form.setValue('captchaToken', token)}
validationError={form.formState.errors.captchaToken?.message}
/>
)}
<Button
disabled={isPending || (captchaConfigured && !captchaToken)}
size="lg"
type="submit"
className="cursor-pointer w-full flex items-center justify-center gap-2"
>
{isPending && <Loader2Icon className="mr-2 size-4 animate-spin" />}
<span>{t('signUp')}</span>
</Button>
</form>
</Form>
<span>{t('signUp')}</span>
</Button>
</form>
</Form>
)}
<div className="mt-4">
<SocialLoginButton callbackUrl={callbackUrl} />
<SocialLoginButton
callbackUrl={callbackUrl}
showDivider={credentialLoginEnabled}
/>
</div>
</AuthCard>
);

View File

@ -15,6 +15,7 @@ import { useState } from 'react';
interface SocialLoginButtonProps {
callbackUrl?: string;
showDivider?: boolean;
}
/**
@ -22,6 +23,7 @@ interface SocialLoginButtonProps {
*/
export const SocialLoginButton = ({
callbackUrl: propCallbackUrl,
showDivider = true,
}: SocialLoginButtonProps) => {
if (
!websiteConfig.auth.enableGoogleLogin &&
@ -93,7 +95,7 @@ export const SocialLoginButton = ({
return (
<div className="w-full flex flex-col gap-4">
<DividerWithText text={t('or')} />
{showDivider && <DividerWithText text={t('or')} />}
{websiteConfig.auth.enableGoogleLogin && (
<Button
size="lg"

View File

@ -50,6 +50,7 @@ export const websiteConfig: WebsiteConfig = {
auth: {
enableGoogleLogin: true,
enableGithubLogin: true,
enableCredentialLogin: true,
},
i18n: {
defaultLocale: 'en',

View File

@ -94,6 +94,7 @@ export interface AnalyticsConfig {
export interface AuthConfig {
enableGoogleLogin?: boolean; // Whether to enable google login
enableGithubLogin?: boolean; // Whether to enable github login
enableCredentialLogin?: boolean; // Whether to enable email/password login
}
/**