在开发一个无代码平台时,用户注册与登录模块是最基础且关键的部分。它不仅是用户进入平台的入口,还为后续的功能模块(如页面设计、协作、模板市场等)提供了身份验证和权限管理的基础。本文将详细介绍如何在 Next.js 框架中搭建用户注册与登录模块,包括数据库设计、后端 API 实现、前端页面集成以及身份验证机制。
1. 数据库设计
首先,我们需要在数据库中设计用户表(User 表),用于存储用户的基本信息和登录凭证。使用 Prisma 作为 ORM 工具,可以方便地定义和管理数据库模型。
在 schema.prisma 文件中定义 User 表:
kotlin
model User {
id Int @id @default(autoincrement())
email String @unique
password String // 存储加密后的密码
name String?
role String @default("USER") // 用户角色,如 USER、ADMIN
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
运行 Prisma Migrate 同步数据库:
css
npx prisma migrate dev --name init_user
2. 实现注册功能
2.1 注册页面
在 app/register/page.tsx 中创建一个注册页面,包含邮箱、密码和用户名输入框。
ini
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { FaUser, FaEnvelope, FaLock, FaEye, FaEyeSlash } from 'react-icons/fa';
export default function Register() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [name, setName] = useState('');
const [error, setError] = useState('');
const [showPassword, setShowPassword] = useState(false); // 控制密码显示
const router = useRouter();
const validateForm = () => {
if (!email || !password || !name) {
setError('All fields are required');
return false;
}
if (!/^[^\s@]+@[^\s@]+.[^\s@]+$/.test(email)) {
setError('Invalid email format');
return false;
}
if (password.length < 6) {
setError('Password must be at least 6 characters');
return false;
}
setError('');
return true;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) return;
const response = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, name }),
});
if (response.ok) {
router.push('/login');
} else {
const errorData = await response.json();
setError(errorData.message || 'Registration failed');
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-r from-blue-500 to-purple-600">
<div className="bg-white p-8 rounded-lg shadow-lg w-96">
<h1 className="text-2xl font-bold mb-6 text-center text-gray-800">Register</h1>
{error && <p className="text-red-500 text-sm mb-4">{error}</p>}
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
Name
</label>
<div className="relative">
<FaUser className="absolute left-3 top-3 text-gray-400" />
<input
type="text"
id="name"
placeholder="Your Name"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full pl-10 pr-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="email">
Email
</label>
<div className="relative">
<FaEnvelope className="absolute left-3 top-3 text-gray-400" />
<input
type="email"
id="email"
placeholder="Your Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full pl-10 pr-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<div className="mb-6">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="password">
Password
</label>
<div className="relative">
<FaLock className="absolute left-3 top-3 text-gray-400" />
<input
type={showPassword ? 'text' : 'password'} // 切换输入框类型
id="password"
placeholder="Your Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full pl-10 pr-10 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)} // 切换显示状态
className="absolute right-3 top-3 text-gray-400 hover:text-gray-600"
>
{showPassword ? <FaEyeSlash /> : <FaEye />} {/* 切换图标 */}
</button>
</div>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition duration-300"
>
Register
</button>
</form>
<p className="text-center text-gray-600 mt-4">
Already have an account?{' '}
<a href="/login" className="text-blue-500 hover:underline">
Login here
</a>
</p>
</div>
</div>
);
}
2.2 注册 API
在 app/api/register/route.ts 中处理注册逻辑:
javascript
import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcryptjs'; // 使用 bcryptjs
import { NextResponse } from 'next/server';
const prisma = new PrismaClient();
export async function POST(req: Request) {
const { email, password, name } = await req.json();
// 检查邮箱是否已注册
const existingUser = await prisma.user.findUnique({ where: { email } });
if (existingUser) {
return NextResponse.json(
{ message: 'Email already registered' },
{ status: 400 }
);
}
// 加密密码
const hashedPassword = bcrypt.hashSync(password, 10);
// 创建用户
const user = await prisma.user.create({
data: {
email,
password: hashedPassword,
name,
},
});
return NextResponse.json(
{ message: 'User registered successfully', user },
{ status: 201 }
);
}
3. 实现登录功能
3.1 登录页面
在 app/login/page.tsx 中创建一个登录页面,包含邮箱和密码输入框。
ini
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { FaEnvelope, FaLock, FaEye, FaEyeSlash } from 'react-icons/fa';
export default function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [showPassword, setShowPassword] = useState(false); // 控制密码显示
const router = useRouter();
const validateForm = () => {
if (!email || !password) {
setError('All fields are required');
return false;
}
if (!/^[^\s@]+@[^\s@]+.[^\s@]+$/.test(email)) {
setError('Invalid email format');
return false;
}
setError('');
return true;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) return;
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (response.ok) {
const { token } = await response.json();
localStorage.setItem('token', token); // 存储 JWT
router.push('/');
} else {
const errorData = await response.json();
setError(errorData.message || 'Login failed');
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-r from-blue-500 to-purple-600">
<div className="bg-white p-8 rounded-lg shadow-lg w-96">
<h1 className="text-2xl font-bold mb-6 text-center text-gray-800">Login</h1>
{error && <p className="text-red-500 text-sm mb-4">{error}</p>}
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="email">
Email
</label>
<div className="relative">
<FaEnvelope className="absolute left-3 top-3 text-gray-400" />
<input
type="email"
id="email"
placeholder="Your Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full pl-10 pr-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<div className="mb-6">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="password">
Password
</label>
<div className="relative">
<FaLock className="absolute left-3 top-3 text-gray-400" />
<input
type={showPassword ? 'text' : 'password'} // 切换输入框类型
id="password"
placeholder="Your Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full pl-10 pr-10 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)} // 切换显示状态
className="absolute right-3 top-3 text-gray-400 hover:text-gray-600"
>
{showPassword ? <FaEyeSlash /> : <FaEye />} {/* 切换图标 */}
</button>
</div>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition duration-300"
>
Login
</button>
</form>
<p className="text-center text-gray-600 mt-4">
Don't have an account?{' '}
<a href="/register" className="text-blue-500 hover:underline">
Register here
</a>
</p>
</div>
</div>
);
}
3.2 登录 API
在 app/api/login/route.ts 中处理登录逻辑:
php
import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcryptjs'; // 使用 bcryptjs
import jwt from 'jsonwebtoken';
import { NextResponse } from 'next/server';
const prisma = new PrismaClient();
export async function POST(req: Request) {
const { email, password } = await req.json();
// 查找用户
const user = await prisma.user.findUnique({ where: { email } });
if (!user) {
return NextResponse.json(
{ message: 'Invalid email or password' },
{ status: 400 }
);
}
// 验证密码
const isValidPassword = bcrypt.compareSync(password, user.password);
if (!isValidPassword) {
return NextResponse.json(
{ message: 'Invalid email or password' },
{ status: 400 }
);
}
// 生成 JWT
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET!, {
expiresIn: '1h',
});
return NextResponse.json(
{ message: 'Login successful', token },
{ status: 200 }
);
}
4. 实现身份验证
为了保护需要登录才能访问的页面,我们可以创建一个高阶组件(HOC)来验证用户身份。
typescript
// utils/withAuth.tsx
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
export const withAuth = (WrappedComponent: React.ComponentType) => {
return (props: any) => {
const router = useRouter();
useEffect(() => {
const token = localStorage.getItem('token');
if (!token) {
router.push('/login');
}
}, []);
return <WrappedComponent {...props} />;
};
};
在需要保护的页面中使用 withAuth:
javascript
// app/dashboard/page.tsx
import { withAuth } from '@/utils/withAuth';
function Dashboard() {
return <div>Welcome to the Dashboard!</div>;
}
export default withAuth(Dashboard);
5. 提醒:配置 JWT_SECRET
在实现用户注册与登录模块时,JWT_SECRET 是一个非常重要的环境变量。它用于生成和验证 JSON Web Token (JWT) ,确保用户身份验证的安全性。如果未正确配置 JWT_SECRET,可能会导致登录功能无法正常工作或存在安全风险。
如何配置 JWT_SECRET?
-
在项目根目录下创建或编辑
.env文件,添加以下内容:iniJWT_SECRET=your-secret-key将
your-secret-key替换为一个随机的、足够复杂的字符串。 -
确保
.env文件没有被提交到版本控制(如 Git)。在.gitignore中添加以下内容:bash.env -
在代码中通过
process.env.JWT_SECRET使用该密钥。
6. 总结
通过以上步骤,我们成功搭建了用户注册与登录模块,包括:
- 数据库设计:使用 Prisma 定义
User表。 - 注册功能:实现注册页面和 API。
- 登录功能:实现登录页面和 API。
- 身份验证:使用 JWT 和 HOC 保护页面。
接下来,可以在此基础上扩展用户管理、权限控制等功能模块。希望本文对你有所帮助!如果有任何问题,欢迎留言讨论。😊