feat: 更新登录和注册表单以支持手机号登录

This commit is contained in:
zyh 2024-10-22 03:36:39 +00:00
parent a88c5f0d62
commit 2da57de786
6 changed files with 157 additions and 62 deletions

View File

@ -1,18 +1,24 @@
import React, { useState } from 'react';
import { useNavigate } from '@remix-run/react';
import { validatePhoneNumber } from '~/utils/validation';
export function Login() {
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [password, setPassword] = useState('');
const [phoneError, setPhoneError] = useState('');
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validatePhoneNumber(phone)) {
setPhoneError('请输入有效的手机号码');
return;
}
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
body: JSON.stringify({ phone, password }),
});
if (response.ok) {
const data = (await response.json()) as { token: string };
@ -27,22 +33,45 @@ export function Login() {
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="邮箱"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="密码"
required
/>
<button type="submit"></button>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="phone" className="block text-sm font-medium text-bolt-elements-textPrimary">
</label>
<input
type="tel"
id="phone"
value={phone}
onChange={(e) => {
setPhone(e.target.value);
setPhoneError('');
}}
required
className="mt-1 block w-full px-3 py-2 bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded-md shadow-sm focus:outline-none focus:ring-bolt-elements-button-primary-background focus:border-bolt-elements-button-primary-background"
/>
{phoneError && <p className="mt-2 text-sm text-red-600">{phoneError}</p>}
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-bolt-elements-textPrimary">
</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="mt-1 block w-full px-3 py-2 bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded-md shadow-sm focus:outline-none focus:ring-bolt-elements-button-primary-background focus:border-bolt-elements-button-primary-background"
/>
</div>
<div>
<button
type="submit"
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-bolt-elements-button-primary-text bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-bolt-elements-button-primary-background"
>
</button>
</div>
</form>
);
}

View File

@ -1,19 +1,29 @@
import React, { useState } from 'react';
import { useNavigate } from '@remix-run/react';
import { validatePhoneNumber } from '~/utils/validation';
export function Register() {
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [phoneError, setPhoneError] = useState('');
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validatePhoneNumber(phone)) {
setPhoneError('请输入有效的手机号码');
return;
}
if (password !== confirmPassword) {
alert('两次输入的密码不一致');
return;
}
try {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, email, password }),
body: JSON.stringify({ phone, password }),
});
if (response.ok) {
navigate('/login');
@ -26,29 +36,58 @@ export function Register() {
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="用户名"
required
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="邮箱"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="密码"
required
/>
<button type="submit"></button>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="phone" className="block text-sm font-medium text-bolt-elements-textPrimary">
</label>
<input
type="tel"
id="phone"
value={phone}
onChange={(e) => {
setPhone(e.target.value);
setPhoneError('');
}}
required
className="mt-1 block w-full px-3 py-2 bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded-md shadow-sm focus:outline-none focus:ring-bolt-elements-button-primary-background focus:border-bolt-elements-button-primary-background"
/>
{phoneError && <p className="mt-2 text-sm text-red-600">{phoneError}</p>}
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-bolt-elements-textPrimary">
</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="mt-1 block w-full px-3 py-2 bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded-md shadow-sm focus:outline-none focus:ring-bolt-elements-button-primary-background focus:border-bolt-elements-button-primary-background"
/>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-bolt-elements-textPrimary">
</label>
<input
type="password"
id="confirmPassword"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
className="mt-1 block w-full px-3 py-2 bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded-md shadow-sm focus:outline-none focus:ring-bolt-elements-button-primary-background focus:border-bolt-elements-button-primary-background"
/>
</div>
<div>
<button
type="submit"
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-bolt-elements-button-primary-text bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-bolt-elements-button-primary-background"
>
</button>
</div>
</form>
);
}

View File

@ -44,13 +44,13 @@ export function Header() {
)}
<button
onClick={() => setIsLoginOpen(true)}
className="text-bolt-elements-textPrimary hover:text-bolt-elements-textSecondary transition-colors"
className="px-4 py-2 text-sm font-medium text-bolt-elements-button-secondary-text bg-bolt-elements-button-secondary-background hover:bg-bolt-elements-button-secondary-backgroundHover rounded-md transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-bolt-elements-button-secondary-background"
>
</button>
<button
onClick={() => setIsRegisterOpen(true)}
className="text-bolt-elements-textPrimary hover:text-bolt-elements-textSecondary transition-colors"
className="px-4 py-2 text-sm font-medium text-bolt-elements-button-primary-text bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover rounded-md transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-bolt-elements-button-primary-background"
>
</button>

View File

@ -1,15 +1,25 @@
import { json } from '@remix-run/cloudflare';
import { type ActionFunction } from '@remix-run/node';
import { json } from '@remix-run/node';
import type { ActionFunction } from '@remix-run/node';
import { validatePhoneNumber } from '~/utils/validation';
import { verifyLogin, createToken } from '~/utils/auth.server';
export const action: ActionFunction = async ({ request }) => {
const { email, password } = (await request.json()) as { email: string; password: string };
const user = await verifyLogin(email, password);
if (!user) {
return json({ success: false, error: 'Invalid credentials' }, { status: 400 });
const { phone, password } = await request.json() as { phone: string, password: string };
if (!validatePhoneNumber(phone)) {
return json({ error: '无效的手机号码' }, { status: 400 });
}
try {
const user = await verifyLogin(phone, password);
if (!user) {
return json({ error: '手机号或密码不正确' }, { status: 401 });
}
const token = createToken(user._id.toString());
return json({ token });
} catch (error) {
console.error('Login error:', error);
return json({ error: '登录失败,请稍后再试' }, { status: 500 });
}
const token = createToken(user._id.toString());
return json({ success: true, token });
};

View File

@ -1,20 +1,31 @@
import { type ActionFunction, json } from '@remix-run/cloudflare';
import { json } from '@remix-run/node';
import type { ActionFunction } from '@remix-run/node';
import { validatePhoneNumber } from '~/utils/validation';
import { db } from '~/utils/db.server';
import { hashPassword } from '~/utils/auth.server';
export const action: ActionFunction = async ({ request }) => {
const { username, email, password } = (await request.json()) as { username: string; email: string; password: string };
const { phone, password } = await request.json() as { phone: string, password: string };
if (!validatePhoneNumber(phone)) {
return json({ error: '无效的手机号码' }, { status: 400 });
}
try {
const existingUser = await db('users').where({ phone }).first();
if (existingUser) {
return json({ error: '该手机号已被注册' }, { status: 400 });
}
const hashedPassword = await hashPassword(password);
const [userId] = await db('users').insert({
username,
email,
phone,
password: hashedPassword
});
return json({ success: true, userId });
} catch (error) {
console.error('Registration error:', error);
return json({ success: false, error: 'Registration failed' }, { status: 400 });
return json({ error: '注册失败,请稍后再试' }, { status: 500 });
}
};

6
app/utils/validation.ts Normal file
View File

@ -0,0 +1,6 @@
export function validatePhoneNumber(phone: string): boolean {
// 这里使用一个简单的中国大陆手机号码验证规则
// 你可能需要根据具体需求调整这个正则表达式
const phoneRegex = /^1[3-9]\d{9}$/;
return phoneRegex.test(phone);
}