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 React, { useState } from 'react';
import { useNavigate } from '@remix-run/react'; import { useNavigate } from '@remix-run/react';
import { validatePhoneNumber } from '~/utils/validation';
export function Login() { export function Login() {
const [email, setEmail] = useState(''); const [phone, setPhone] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [phoneError, setPhoneError] = useState('');
const navigate = useNavigate(); const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!validatePhoneNumber(phone)) {
setPhoneError('请输入有效的手机号码');
return;
}
try { try {
const response = await fetch('/api/auth/login', { const response = await fetch('/api/auth/login', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }), body: JSON.stringify({ phone, password }),
}); });
if (response.ok) { if (response.ok) {
const data = (await response.json()) as { token: string }; const data = (await response.json()) as { token: string };
@ -27,22 +33,45 @@ export function Login() {
}; };
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit} className="space-y-6">
<input <div>
type="email" <label htmlFor="phone" className="block text-sm font-medium text-bolt-elements-textPrimary">
value={email}
onChange={(e) => setEmail(e.target.value)} </label>
placeholder="邮箱" <input
required type="tel"
/> id="phone"
<input value={phone}
type="password" onChange={(e) => {
value={password} setPhone(e.target.value);
onChange={(e) => setPassword(e.target.value)} setPhoneError('');
placeholder="密码" }}
required 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"
<button type="submit"></button> />
{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> </form>
); );
} }

View File

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

View File

@ -44,13 +44,13 @@ export function Header() {
)} )}
<button <button
onClick={() => setIsLoginOpen(true)} 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>
<button <button
onClick={() => setIsRegisterOpen(true)} 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> </button>

View File

@ -1,15 +1,25 @@
import { json } from '@remix-run/cloudflare'; import { json } from '@remix-run/node';
import { type ActionFunction } from '@remix-run/node'; import type { ActionFunction } from '@remix-run/node';
import { validatePhoneNumber } from '~/utils/validation';
import { verifyLogin, createToken } from '~/utils/auth.server'; import { verifyLogin, createToken } from '~/utils/auth.server';
export const action: ActionFunction = async ({ request }) => { export const action: ActionFunction = async ({ request }) => {
const { email, password } = (await request.json()) as { email: string; password: string }; const { phone, password } = await request.json() as { phone: string, password: string };
const user = await verifyLogin(email, password); if (!validatePhoneNumber(phone)) {
if (!user) { return json({ error: '无效的手机号码' }, { status: 400 });
return json({ success: false, error: 'Invalid credentials' }, { 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 { db } from '~/utils/db.server';
import { hashPassword } from '~/utils/auth.server'; import { hashPassword } from '~/utils/auth.server';
export const action: ActionFunction = async ({ request }) => { 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 { try {
const existingUser = await db('users').where({ phone }).first();
if (existingUser) {
return json({ error: '该手机号已被注册' }, { status: 400 });
}
const hashedPassword = await hashPassword(password); const hashedPassword = await hashPassword(password);
const [userId] = await db('users').insert({ const [userId] = await db('users').insert({
username, phone,
email,
password: hashedPassword password: hashedPassword
}); });
return json({ success: true, userId }); return json({ success: true, userId });
} catch (error) { } catch (error) {
console.error('Registration error:', 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);
}