🛠️ fix: v1 interface feedback regressions
Resolve verified V1 frontend feedback by improving channel workflows, auth behavior, API key interactions, user filtering, layout persistence, subscription quota handling, i18n text, pricing metadata, and stale frontend cache recovery. - Add a global frontend cache version cleanup to prevent old frontend localStorage from causing page errors after upgrades. - Fix channel copy refresh, model mapping input focus loss, create-channel fetch-model title state, upstream model update confirmation, and batch test toast behavior. - Respect password login settings and improve Turnstile, forgot-password, registration, and invite-link flows. - Make user role/status filtering server-side and preserve table page size in URLs. - Improve API key edit validation feedback and prefetch real keys for reliable copy actions. - Fix rankings access fail-open behavior, double scrollbars, subscription received amount conversion/display, token i18n wording, model deletion confirmation grammar, and Claude pricing context inference. - Add clearer Playground model/group loading errors. Validation: - bun run typecheck - bun run i18n:sync - gofmt on modified Go files - go test ./controller ./model -run '^$'
This commit is contained in:
+11
-2
@@ -67,6 +67,7 @@ export function ForgotPasswordForm({
|
||||
resolver: zodResolver(forgotPasswordFormSchema),
|
||||
defaultValues: { email: '' },
|
||||
})
|
||||
const turnstileReady = !isTurnstileEnabled || Boolean(turnstileToken)
|
||||
|
||||
async function onSubmit(data: z.infer<typeof forgotPasswordFormSchema>) {
|
||||
if (!validateTurnstile()) return
|
||||
@@ -78,6 +79,8 @@ export function ForgotPasswordForm({
|
||||
form.reset()
|
||||
startCountdown()
|
||||
toast.success(t('Reset email sent, please check your inbox'))
|
||||
} else {
|
||||
toast.error(res?.message || t('Failed to send reset email'))
|
||||
}
|
||||
} catch (_error) {
|
||||
// Errors are handled by global interceptor
|
||||
@@ -107,8 +110,14 @@ export function ForgotPasswordForm({
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type='submit' className='mt-2' disabled={isLoading || isActive}>
|
||||
{isActive ? `Resend (${secondsLeft}s)` : 'Send reset email'}
|
||||
<Button
|
||||
type='submit'
|
||||
className='mt-2'
|
||||
disabled={isLoading || isActive || !turnstileReady}
|
||||
>
|
||||
{isActive
|
||||
? t('Resend ({{seconds}}s)', { seconds: secondsLeft })
|
||||
: t('Send reset email')}
|
||||
{isLoading ? <Loader2 className='animate-spin' /> : <ArrowRight />}
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -61,6 +61,9 @@ export function useEmailVerification(options?: UseEmailVerificationOptions) {
|
||||
toast.success(i18next.t('Verification email sent'))
|
||||
return true
|
||||
}
|
||||
toast.error(
|
||||
res?.message || i18next.t('Failed to send verification email')
|
||||
)
|
||||
return false
|
||||
} catch (_error) {
|
||||
// Errors are handled by global interceptor
|
||||
|
||||
+115
-86
@@ -81,6 +81,10 @@ export function UserAuthForm({
|
||||
const passkeyLoginEnabled = Boolean(
|
||||
status?.passkey_login ?? status?.data?.passkey_login
|
||||
)
|
||||
const passwordLoginEnabled =
|
||||
(status?.password_login_enabled ??
|
||||
status?.data?.password_login_enabled ??
|
||||
true) !== false
|
||||
const {
|
||||
isTurnstileEnabled,
|
||||
turnstileSiteKey,
|
||||
@@ -98,6 +102,16 @@ export function UserAuthForm({
|
||||
!passkeySupported ||
|
||||
(requiresLegalConsent && !agreedToLegal)
|
||||
const hasWeChatLogin = Boolean(status?.wechat_login)
|
||||
const hasOAuthLogin = Boolean(
|
||||
status?.github_oauth ||
|
||||
status?.discord_oauth ||
|
||||
status?.oidc_enabled ||
|
||||
status?.linuxdo_oauth ||
|
||||
status?.telegram_oauth ||
|
||||
(status?.custom_oauth_providers?.length ?? 0) > 0
|
||||
)
|
||||
const hasAlternativeLogin =
|
||||
passkeyLoginEnabled || hasWeChatLogin || hasOAuthLogin
|
||||
|
||||
useEffect(() => {
|
||||
if (requiresLegalConsent) {
|
||||
@@ -275,6 +289,42 @@ export function UserAuthForm({
|
||||
}
|
||||
}
|
||||
|
||||
const alternativeLoginMethods = (
|
||||
<>
|
||||
{passkeyLoginEnabled && (
|
||||
<div className='mt-2 space-y-1'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
disabled={passkeyButtonDisabled}
|
||||
onClick={handlePasskeyLogin}
|
||||
className='h-11 w-full justify-center gap-2 rounded-lg'
|
||||
>
|
||||
{isPasskeyLoading ? (
|
||||
<Loader2 className='h-4 w-4 animate-spin' />
|
||||
) : (
|
||||
<KeyRound className='h-4 w-4' />
|
||||
)}
|
||||
{t('Sign in with Passkey')}
|
||||
</Button>
|
||||
{!passkeySupported && (
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('Passkey is not supported on this device.')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* OAuth Providers */}
|
||||
<OAuthProviders
|
||||
status={status}
|
||||
disabled={isLoading || (requiresLegalConsent && !agreedToLegal)}
|
||||
onWeChatLogin={hasWeChatLogin ? handleOpenWeChatDialog : undefined}
|
||||
isWeChatLoading={isWeChatSubmitting}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -282,63 +332,72 @@ export function UserAuthForm({
|
||||
className={cn('grid gap-4', className)}
|
||||
{...props}
|
||||
>
|
||||
{/* Username Field */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='username'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('Username or Email')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t('Enter your username or email')}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{hasAlternativeLogin && alternativeLoginMethods}
|
||||
|
||||
{/* Password Field */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='password'
|
||||
render={({ field }) => (
|
||||
<FormItem className='relative'>
|
||||
<FormLabel>{t('Password')}</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput placeholder={t('Enter password')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<Link
|
||||
to='/forgot-password'
|
||||
className='text-muted-foreground absolute end-0 -top-0.5 z-10 text-sm font-medium hover:opacity-75'
|
||||
>
|
||||
{t('Forgot password?')}
|
||||
</Link>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button
|
||||
type='submit'
|
||||
className='mt-2 w-full justify-center gap-2'
|
||||
disabled={isLoading || (requiresLegalConsent && !agreedToLegal)}
|
||||
>
|
||||
{isLoading ? <Loader2 className='animate-spin' /> : <LogIn />}
|
||||
{t('Sign in')}
|
||||
</Button>
|
||||
|
||||
{/* Turnstile */}
|
||||
{isTurnstileEnabled && (
|
||||
<div className='mt-2'>
|
||||
<Turnstile
|
||||
siteKey={turnstileSiteKey}
|
||||
onVerify={setTurnstileToken}
|
||||
{passwordLoginEnabled && (
|
||||
<>
|
||||
{/* Username Field */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='username'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('Username or Email')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t('Enter your username or email')}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Password Field */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='password'
|
||||
render={({ field }) => (
|
||||
<FormItem className='relative'>
|
||||
<FormLabel>{t('Password')}</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput
|
||||
placeholder={t('Enter password')}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<Link
|
||||
to='/forgot-password'
|
||||
className='text-muted-foreground absolute end-0 -top-0.5 z-10 text-sm font-medium hover:opacity-75'
|
||||
>
|
||||
{t('Forgot password?')}
|
||||
</Link>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button
|
||||
type='submit'
|
||||
className='mt-2 w-full justify-center gap-2'
|
||||
disabled={isLoading || (requiresLegalConsent && !agreedToLegal)}
|
||||
>
|
||||
{isLoading ? <Loader2 className='animate-spin' /> : <LogIn />}
|
||||
{t('Sign in')}
|
||||
</Button>
|
||||
|
||||
{/* Turnstile */}
|
||||
{isTurnstileEnabled && (
|
||||
<div className='mt-2'>
|
||||
<Turnstile
|
||||
siteKey={turnstileSiteKey}
|
||||
onVerify={setTurnstileToken}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<LegalConsent
|
||||
@@ -348,37 +407,7 @@ export function UserAuthForm({
|
||||
className='mt-1'
|
||||
/>
|
||||
|
||||
{passkeyLoginEnabled && (
|
||||
<div className='mt-2 space-y-1'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
disabled={passkeyButtonDisabled}
|
||||
onClick={handlePasskeyLogin}
|
||||
className='h-11 w-full justify-center gap-2 rounded-lg'
|
||||
>
|
||||
{isPasskeyLoading ? (
|
||||
<Loader2 className='h-4 w-4 animate-spin' />
|
||||
) : (
|
||||
<KeyRound className='h-4 w-4' />
|
||||
)}
|
||||
{t('Sign in with Passkey')}
|
||||
</Button>
|
||||
{!passkeySupported && (
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('Passkey is not supported on this device.')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* OAuth Providers */}
|
||||
<OAuthProviders
|
||||
status={status}
|
||||
disabled={isLoading || (requiresLegalConsent && !agreedToLegal)}
|
||||
onWeChatLogin={hasWeChatLogin ? handleOpenWeChatDialog : undefined}
|
||||
isWeChatLoading={isWeChatSubmitting}
|
||||
/>
|
||||
{!hasAlternativeLogin && alternativeLoginMethods}
|
||||
</form>
|
||||
|
||||
{hasWeChatLogin && (
|
||||
|
||||
@@ -53,7 +53,10 @@ import { registerFormSchema } from '@/features/auth/constants'
|
||||
import { useAuthRedirect } from '@/features/auth/hooks/use-auth-redirect'
|
||||
import { useEmailVerification } from '@/features/auth/hooks/use-email-verification'
|
||||
import { useTurnstile } from '@/features/auth/hooks/use-turnstile'
|
||||
import { getAffiliateCode } from '@/features/auth/lib/storage'
|
||||
import {
|
||||
getAffiliateCode,
|
||||
saveAffiliateCode,
|
||||
} from '@/features/auth/lib/storage'
|
||||
|
||||
export function SignUpForm({
|
||||
className,
|
||||
@@ -107,6 +110,7 @@ export function SignUpForm({
|
||||
status?.data?.oauth_register_enabled ??
|
||||
true
|
||||
const hasWeChatLogin = Boolean(status?.wechat_login)
|
||||
const turnstileReady = !isTurnstileEnabled || Boolean(turnstileToken)
|
||||
|
||||
const wechatQrCodeUrl = useMemo(() => {
|
||||
return (
|
||||
@@ -130,6 +134,13 @@ export function SignUpForm({
|
||||
}
|
||||
}, [requiresLegalConsent])
|
||||
|
||||
useEffect(() => {
|
||||
const aff = new URLSearchParams(window.location.search).get('aff')?.trim()
|
||||
if (aff) {
|
||||
saveAffiliateCode(aff)
|
||||
}
|
||||
}, [])
|
||||
|
||||
async function onSubmit(data: z.infer<typeof registerFormSchema>) {
|
||||
if (requiresLegalConsent && !agreedToLegal) {
|
||||
toast.error(legalConsentErrorMessage)
|
||||
@@ -164,6 +175,8 @@ export function SignUpForm({
|
||||
if (res?.success) {
|
||||
toast.success(t('Account created! Please sign in'))
|
||||
redirectToLogin()
|
||||
} else {
|
||||
toast.error(res?.message || t('Failed to create account'))
|
||||
}
|
||||
} catch (_error) {
|
||||
// Errors are handled by global interceptor
|
||||
@@ -307,7 +320,13 @@ export function SignUpForm({
|
||||
<Button
|
||||
variant='outline'
|
||||
type='button'
|
||||
disabled={isLoading || isSendingCode || isActive || !emailValue}
|
||||
disabled={
|
||||
isLoading ||
|
||||
isSendingCode ||
|
||||
isActive ||
|
||||
!emailValue ||
|
||||
!turnstileReady
|
||||
}
|
||||
onClick={handleSendVerificationCode}
|
||||
>
|
||||
{isActive ? (
|
||||
@@ -343,7 +362,11 @@ export function SignUpForm({
|
||||
<Button
|
||||
type='submit'
|
||||
className='mt-2 w-full justify-center gap-2'
|
||||
disabled={isLoading || (requiresLegalConsent && !agreedToLegal)}
|
||||
disabled={
|
||||
isLoading ||
|
||||
(requiresLegalConsent && !agreedToLegal) ||
|
||||
!turnstileReady
|
||||
}
|
||||
>
|
||||
{isLoading ? <Loader2 className='h-4 w-4 animate-spin' /> : null}
|
||||
{t('Create account')}
|
||||
|
||||
+2
@@ -126,6 +126,7 @@ export interface SystemStatus {
|
||||
privacy_policy_enabled?: boolean
|
||||
oauth_register_enabled?: boolean
|
||||
register_enabled?: boolean
|
||||
password_login_enabled?: boolean
|
||||
password_register_enabled?: boolean
|
||||
custom_oauth_providers?: CustomOAuthProviderInfo[]
|
||||
[key: string]: unknown
|
||||
@@ -168,6 +169,7 @@ export interface SystemStatus {
|
||||
privacy_policy_enabled?: boolean
|
||||
oauth_register_enabled?: boolean
|
||||
register_enabled?: boolean
|
||||
password_login_enabled?: boolean
|
||||
password_register_enabled?: boolean
|
||||
custom_oauth_providers?: CustomOAuthProviderInfo[]
|
||||
[key: string]: unknown
|
||||
|
||||
Reference in New Issue
Block a user