feat: implement account settings dialog for managing user passwords and binding Space accounts

This commit is contained in:
Junyan Qin
2025-12-26 23:20:51 +08:00
parent 1d4c5bbdf1
commit 24c15b4479
13 changed files with 901 additions and 206 deletions
+160 -136
View File
@@ -36,10 +36,15 @@ const formSchema = (t: (key: string) => string) =>
password: z.string().min(1, t('common.emptyPassword')),
});
type AccountType = 'local' | 'space';
export default function Login() {
const router = useRouter();
const { t } = useTranslation();
const [spaceLoading, setSpaceLoading] = useState(false);
const [accountType, setAccountType] = useState<AccountType | null>(null);
const [hasPassword, setHasPassword] = useState(false);
const [loading, setLoading] = useState(true);
const form = useForm<z.infer<ReturnType<typeof formSchema>>>({
resolver: zodResolver(formSchema(t)),
@@ -50,19 +55,25 @@ export default function Login() {
});
useEffect(() => {
getIsInitialized();
checkIfAlreadyLoggedIn();
checkAccountInfo();
}, []);
function getIsInitialized() {
httpClient
.checkIfInited()
.then((res) => {
if (!res.initialized) {
router.push('/register');
}
})
.catch(() => {});
async function checkAccountInfo() {
try {
const res = await httpClient.getAccountInfo();
if (!res.initialized) {
router.push('/register');
return;
}
setAccountType(res.account_type || 'local');
setHasPassword(res.has_password || false);
setLoading(false);
// Also check if already logged in
checkIfAlreadyLoggedIn();
} catch {
setLoading(false);
}
}
function checkIfAlreadyLoggedIn() {
@@ -95,19 +106,12 @@ export default function Login() {
});
}
// Space OAuth redirect handler
const handleSpaceLoginClick = async () => {
setSpaceLoading(true);
try {
// Build the redirect URI to the OAuth callback page
const currentOrigin = window.location.origin;
const redirectUri = `${currentOrigin}/auth/space/callback`;
// Get the authorization URL from backend
const response = await httpClient.getSpaceAuthorizeUrl(redirectUri);
// Redirect to Space authorization page
window.location.href = response.authorize_url;
} catch {
toast.error(t('common.spaceLoginFailed'));
@@ -115,6 +119,19 @@ export default function Login() {
}
};
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-neutral-900">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
);
}
// Determine what to show based on account type
const showLocalLogin =
accountType === 'local' || (accountType === 'space' && hasPassword);
const showSpaceLogin = accountType === 'space';
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:dark:bg-neutral-900">
<Card className="w-[375px] shadow-lg dark:shadow-white/10">
@@ -136,128 +153,135 @@ export default function Login() {
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Space Login - Recommended */}
<div className="space-y-3">
<Button
type="button"
className="w-full cursor-pointer"
onClick={handleSpaceLoginClick}
disabled={spaceLoading}
>
{spaceLoading ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<svg
className="mr-2 h-4 w-4"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 2L2 7L12 12L22 7L12 2Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M2 17L12 22L22 17"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M2 12L12 17L22 12"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
{t('common.loginWithSpace')}
</Button>
<p className="text-xs text-center text-muted-foreground">
{t('common.spaceLoginRecommended')}
</p>
</div>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-white dark:bg-card px-2 text-muted-foreground">
{t('common.or')}
</span>
</div>
</div>
{/* Local Account Login */}
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{t('common.email')}</FormLabel>
<FormControl>
<div className="relative">
<Mail className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<Input
placeholder={t('common.enterEmail')}
className="pl-10"
{...field}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<div className="flex justify-between">
<FormLabel>{t('common.password')}</FormLabel>
<Link
href="/reset-password"
className="text-sm text-blue-500"
>
{t('common.forgotPassword')}
</Link>
</div>
<FormControl>
<div className="relative">
<Lock className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<Input
type="password"
placeholder={t('common.enterPassword')}
className="pl-10"
{...field}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Space Login - only show for space accounts */}
{showSpaceLogin && (
<div className="space-y-3">
<Button
type="submit"
variant="outline"
type="button"
className="w-full cursor-pointer"
onClick={handleSpaceLoginClick}
disabled={spaceLoading}
>
{t('common.loginLocal')}
{spaceLoading ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<svg
className="mr-2 h-4 w-4"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 2L2 7L12 12L22 7L12 2Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M2 17L12 22L22 17"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M2 12L12 17L22 12"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
{t('common.loginWithSpace')}
</Button>
</form>
</Form>
</div>
)}
{/* Divider - only show if both login methods are available */}
{showSpaceLogin && showLocalLogin && (
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-white dark:bg-card px-2 text-muted-foreground">
{t('common.or')}
</span>
</div>
</div>
)}
{/* Local Account Login - show for local accounts or space accounts with password */}
{showLocalLogin && (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6"
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{t('common.email')}</FormLabel>
<FormControl>
<div className="relative">
<Mail className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<Input
placeholder={t('common.enterEmail')}
className="pl-10"
{...field}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<div className="flex justify-between">
<FormLabel>{t('common.password')}</FormLabel>
<Link
href="/reset-password"
className="text-sm text-blue-500"
>
{t('common.forgotPassword')}
</Link>
</div>
<FormControl>
<div className="relative">
<Lock className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<Input
type="password"
placeholder={t('common.enterPassword')}
className="pl-10"
{...field}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
variant={showSpaceLogin ? 'outline' : 'default'}
className="w-full cursor-pointer"
>
{t('common.loginWithPassword')}
</Button>
</form>
</Form>
)}
</CardContent>
</Card>
</div>