bknd-password-reset
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePassword Reset Flow
密码重置流程
Implement password reset and change functionality in Bknd applications.
在Bknd应用中实现密码重置和修改功能。
Prerequisites
前提条件
- Bknd project with auth enabled ()
bknd-setup-auth - Password strategy configured
- For email-based reset: email sending capability (external service)
- 已启用身份验证的Bknd项目()
bknd-setup-auth - 已配置密码策略
- 基于邮件的重置:具备邮件发送能力(外部服务)
Important Context
重要说明
Bknd provides:
- Server-side method for admin/system password changes
changePassword() - No built-in forgot-password flow - you must implement token generation, email sending, and validation
Bknd提供:
- **服务器端**方法,用于管理员/系统发起的密码修改
changePassword() - 无内置找回密码流程 - 你需要自行实现令牌生成、邮件发送和验证逻辑
When to Use UI Mode
何时使用UI模式
- Admin-initiated password resets via admin panel
UI steps: Admin Panel > Data > users > Select user > Edit
Note: Direct password editing via UI sets raw value - use server-side method for proper hashing.
- 管理员通过管理面板发起密码重置
UI步骤: 管理面板 > 数据 > 用户 > 选择用户 > 编辑
注意:通过UI直接编辑密码会设置原始值 - 请使用服务器端方法以确保正确哈希。
When to Use Code Mode
何时使用代码模式
- Implementing forgot-password flow with email
- Adding change-password functionality for logged-in users
- Admin tools for password resets
- 实现基于邮件的找回密码流程
- 为已登录用户添加密码修改功能
- 管理员密码重置工具
Server-Side Password Change
服务器端密码修改
Using changePassword() Method
使用changePassword()方法
typescript
import { serve } from "bknd/adapter/node";
import { defineConfig } from "bknd";
export default serve(
defineConfig({ /* config */ }),
{
async seed(ctx) {
// Change password by user ID
await ctx.app.module.auth.changePassword(1, "newSecurePassword123");
// Or find user first
const { data: user } = await ctx.em.repo("users").findOne({
email: "user@example.com",
});
if (user) {
await ctx.app.module.auth.changePassword(user.id, "newPassword456");
}
},
}
);Method signature:
typescript
changePassword(userId: number | string, newPassword: string): Promise<boolean>Constraints:
- User must exist
- User must use password strategy (not OAuth)
- Password is automatically hashed using configured hashing method
typescript
import { serve } from "bknd/adapter/node";
import { defineConfig } from "bknd";
export default serve(
defineConfig({ /* config */ }),
{
async seed(ctx) {
// 通过用户ID修改密码
await ctx.app.module.auth.changePassword(1, "newSecurePassword123");
// 或者先查找用户
const { data: user } = await ctx.em.repo("users").findOne({
email: "user@example.com",
});
if (user) {
await ctx.app.module.auth.changePassword(user.id, "newPassword456");
}
},
}
);方法签名:
typescript
changePassword(userId: number | string, newPassword: string): Promise<boolean>约束条件:
- 用户必须存在
- 用户必须使用密码策略(而非OAuth)
- 密码会通过配置的哈希方法自动哈希
Building Forgot-Password Flow
构建找回密码流程
Since Bknd doesn't have built-in forgot-password, implement a custom flow:
由于Bknd没有内置找回密码功能,需实现自定义流程:
Step 1: Create Reset Token Entity
步骤1:创建重置令牌实体
typescript
import { em, entity, text, date, number } from "bknd";
const schema = em({
password_resets: entity("password_resets", {
email: text().required(),
token: text().required().unique(),
expires_at: date().required(),
used: number().default(0), // 0 = unused, 1 = used
}),
});typescript
import { em, entity, text, date, number } from "bknd";
const schema = em({
password_resets: entity("password_resets", {
email: text().required(),
token: text().required().unique(),
expires_at: date().required(),
used: number().default(0), // 0 = 未使用, 1 = 已使用
}),
});Step 2: Request Reset Endpoint
步骤2:重置请求端点
typescript
import { randomBytes } from "crypto";
async function requestPasswordReset(email: string, ctx: any) {
const api = ctx.api;
// Check if user exists (don't reveal in response)
const { data: user } = await api.data.readOneBy("users", { email });
if (!user) {
return { success: true, message: "If email exists, reset link sent" };
}
// Generate secure token
const token = randomBytes(32).toString("hex");
const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
// Store reset token
await api.data.createOne("password_resets", {
email,
token,
expires_at: expiresAt.toISOString(),
used: 0,
});
// Send email (use your email service: SendGrid, Resend, etc.)
const resetUrl = `https://yourapp.com/reset-password?token=${token}`;
// await emailService.send({ to: email, subject: "Password Reset", ... });
return { success: true, message: "If email exists, reset link sent" };
}typescript
import { randomBytes } from "crypto";
async function requestPasswordReset(email: string, ctx: any) {
const api = ctx.api;
// 检查用户是否存在(响应中不暴露此信息)
const { data: user } = await api.data.readOneBy("users", { email });
if (!user) {
return { success: true, message: "如果邮箱存在,重置链接已发送" };
}
// 生成安全令牌
const token = randomBytes(32).toString("hex");
const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1小时
// 存储重置令牌
await api.data.createOne("password_resets", {
email,
token,
expires_at: expiresAt.toISOString(),
used: 0,
});
// 发送邮件(使用你的邮件服务:SendGrid、Resend等)
const resetUrl = `https://yourapp.com/reset-password?token=${token}`;
// await emailService.send({ to: email, subject: "Password Reset", ... });
return { success: true, message: "如果邮箱存在,重置链接已发送" };
}Step 3: Validate and Reset Password
步骤3:验证并重置密码
typescript
async function resetPassword(token: string, newPassword: string, ctx: any) {
const api = ctx.api;
const auth = ctx.app.module.auth;
// Find valid token
const { data: resetRecord } = await api.data.readOneBy("password_resets", {
token,
used: 0,
});
if (!resetRecord) {
throw new Error("Invalid or expired reset token");
}
// Check expiration
if (new Date(resetRecord.expires_at) < new Date()) {
throw new Error("Reset token has expired");
}
// Find user
const { data: user } = await api.data.readOneBy("users", {
email: resetRecord.email,
});
if (!user) {
throw new Error("User not found");
}
// Change password (properly hashed)
await auth.changePassword(user.id, newPassword);
// Mark token as used
await api.data.updateOne("password_resets", resetRecord.id, { used: 1 });
return { success: true };
}typescript
async function resetPassword(token: string, newPassword: string, ctx: any) {
const api = ctx.api;
const auth = ctx.app.module.auth;
// 查找有效令牌
const { data: resetRecord } = await api.data.readOneBy("password_resets", {
token,
used: 0,
});
if (!resetRecord) {
throw new Error("无效或已过期的重置令牌");
}
// 检查过期时间
if (new Date(resetRecord.expires_at) < new Date()) {
throw new Error("重置令牌已过期");
}
// 查找用户
const { data: user } = await api.data.readOneBy("users", {
email: resetRecord.email,
});
if (!user) {
throw new Error("用户不存在");
}
// 修改密码(自动哈希)
await auth.changePassword(user.id, newPassword);
// 将令牌标记为已使用
await api.data.updateOne("password_resets", resetRecord.id, { used: 1 });
return { success: true };
}Step 4: React Frontend
步骤4:React前端
Request Reset Form:
tsx
function ForgotPasswordForm() {
const [email, setEmail] = useState("");
const [status, setStatus] = useState<"idle" | "loading" | "sent">("idle");
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setStatus("loading");
await fetch("/api/password-reset/request", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
setStatus("sent");
}
if (status === "sent") {
return <p>Check your email for reset instructions.</p>;
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
/>
<button type="submit" disabled={status === "loading"}>
{status === "loading" ? "Sending..." : "Send Reset Link"}
</button>
</form>
);
}Reset Password Form:
tsx
function ResetPasswordForm() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const token = searchParams.get("token");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [error, setError] = useState("");
if (!token) return <p>Invalid reset link.</p>;
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (password !== confirmPassword) {
setError("Passwords do not match");
return;
}
const res = await fetch("/api/password-reset/confirm", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token, password }),
});
if (res.ok) {
navigate("/login");
} else {
const data = await res.json();
setError(data.message || "Reset failed");
}
}
return (
<form onSubmit={handleSubmit}>
{error && <p className="error">{error}</p>}
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="New Password"
minLength={8}
required
/>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Confirm Password"
required
/>
<button type="submit">Reset Password</button>
</form>
);
}找回密码表单:
tsx
function ForgotPasswordForm() {
const [email, setEmail] = useState("");
const [status, setStatus] = useState<"idle" | "loading" | "sent">("idle");
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setStatus("loading");
await fetch("/api/password-reset/request", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
setStatus("sent");
}
if (status === "sent") {
return <p>检查你的邮箱获取重置说明。</p>;
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="邮箱"
required
/>
<button type="submit" disabled={status === "loading"}>
{status === "loading" ? "发送中..." : "发送重置链接"}
</button>
</form>
);
}重置密码表单:
tsx
function ResetPasswordForm() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const token = searchParams.get("token");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [error, setError] = useState("");
if (!token) return <p>无效的重置链接。</p>;
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (password !== confirmPassword) {
setError("密码不匹配");
return;
}
const res = await fetch("/api/password-reset/confirm", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token, password }),
});
if (res.ok) {
navigate("/login");
} else {
const data = await res.json();
setError(data.message || "重置失败");
}
}
return (
<form onSubmit={handleSubmit}>
{error && <p className="error">{error}</p>}
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="新密码"
minLength={8}
required
/>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="确认密码"
required
/>
<button type="submit">重置密码</button>
</form>
);
}Authenticated Password Change
已认证用户的密码修改
For logged-in users changing their own password:
typescript
async function changePassword(currentPassword: string, newPassword: string) {
const api = new Api({ host: "http://localhost:7654", storage: localStorage });
// Verify current password by re-authenticating
const { data: userData } = await api.auth.me();
if (!userData?.user) throw new Error("Not authenticated");
const { ok } = await api.auth.login("password", {
email: userData.user.email,
password: currentPassword,
});
if (!ok) throw new Error("Current password is incorrect");
// Call custom password change endpoint
const res = await fetch("/api/password-change", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ newPassword }),
});
if (!res.ok) throw new Error("Failed to change password");
}针对已登录用户修改自身密码:
typescript
async function changePassword(currentPassword: string, newPassword: string) {
const api = new Api({ host: "http://localhost:7654", storage: localStorage });
// 通过重新验证确认当前密码
const { data: userData } = await api.auth.me();
if (!userData?.user) throw new Error("未认证");
const { ok } = await api.auth.login("password", {
email: userData.user.email,
password: currentPassword,
});
if (!ok) throw new Error("当前密码不正确");
// 调用自定义密码修改端点
const res = await fetch("/api/password-change", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ newPassword }),
});
if (!res.ok) throw new Error("密码修改失败");
}Security Considerations
安全注意事项
Token Security
令牌安全
typescript
// Good: Cryptographically secure token
import { randomBytes } from "crypto";
const token = randomBytes(32).toString("hex"); // 64 chars
// Bad: Predictable token
const token = Math.random().toString(36); // NOT secure!typescript
// 推荐:加密安全的令牌
import { randomBytes } from "crypto";
const token = randomBytes(32).toString("hex"); // 64位字符
// 不推荐:可预测的令牌
const token = Math.random().toString(36); // 不安全!Rate Limiting
速率限制
typescript
const resetAttempts = new Map<string, { count: number; lastAttempt: Date }>();
function checkRateLimit(email: string): boolean {
const record = resetAttempts.get(email);
const now = new Date();
if (!record || record.lastAttempt < new Date(now.getTime() - 3600000)) {
resetAttempts.set(email, { count: 1, lastAttempt: now });
return true;
}
if (record.count >= 3) return false; // Max 3 per hour
record.count++;
return true;
}typescript
const resetAttempts = new Map<string, { count: number; lastAttempt: Date }>();
function checkRateLimit(email: string): boolean {
const record = resetAttempts.get(email);
const now = new Date();
if (!record || record.lastAttempt < new Date(now.getTime() - 3600000)) {
resetAttempts.set(email, { count: 1, lastAttempt: now });
return true;
}
if (record.count >= 3) return false; // 每小时最多3次
record.count++;
return true;
}Common Pitfalls
常见误区
Not Hashing Password
未哈希密码
typescript
// Wrong - bypasses hashing
await api.data.updateOne("users", userId, {
strategy_value: "plainPassword123",
});
// Correct - uses configured hashing
await ctx.app.module.auth.changePassword(userId, "plainPassword123");typescript
// 错误 - 绕过哈希
await api.data.updateOne("users", userId, {
strategy_value: "plainPassword123",
});
// 正确 - 使用配置的哈希方法
await ctx.app.module.auth.changePassword(userId, "plainPassword123");Email Enumeration
邮箱枚举
typescript
// Wrong - reveals email existence
if (!user) return { error: "Email not found" };
// Correct - doesn't reveal
return { success: true, message: "If email exists, reset link sent" };typescript
// 错误 - 暴露邮箱是否存在
if (!user) return { error: "邮箱未找到" };
// 正确 - 不暴露此信息
return { success: true, message: "如果邮箱存在,重置链接已发送" };OAuth User Password Reset
OAuth用户密码重置
typescript
const { data: user } = await api.data.readOneBy("users", { email });
if (user?.strategy !== "password") {
return { error: "This account uses social login" };
}typescript
const { data: user } = await api.data.readOneBy("users", { email });
if (user?.strategy !== "password") {
return { error: "此账户使用社交登录" };
}Verification
验证
bash
undefinedbash
undefined1. Request reset
1. 请求重置
curl -X POST http://localhost:7654/api/password-reset/request
-H "Content-Type: application/json"
-d '{"email": "user@example.com"}'
-H "Content-Type: application/json"
-d '{"email": "user@example.com"}'
curl -X POST http://localhost:7654/api/password-reset/request
-H "Content-Type: application/json"
-d '{"email": "user@example.com"}'
-H "Content-Type: application/json"
-d '{"email": "user@example.com"}'
2. Reset password (use token from email/DB)
2. 重置密码(使用邮箱/数据库中的令牌)
curl -X POST http://localhost:7654/api/password-reset/confirm
-H "Content-Type: application/json"
-d '{"token": "<token>", "password": "newPassword123"}'
-H "Content-Type: application/json"
-d '{"token": "<token>", "password": "newPassword123"}'
curl -X POST http://localhost:7654/api/password-reset/confirm
-H "Content-Type: application/json"
-d '{"token": "<token>", "password": "newPassword123"}'
-H "Content-Type: application/json"
-d '{"token": "<token>", "password": "newPassword123"}'
3. Login with new password
3. 使用新密码登录
curl -X POST http://localhost:7654/api/auth/password/login
-H "Content-Type: application/json"
-d '{"email": "user@example.com", "password": "newPassword123"}'
-H "Content-Type: application/json"
-d '{"email": "user@example.com", "password": "newPassword123"}'
undefinedcurl -X POST http://localhost:7654/api/auth/password/login
-H "Content-Type: application/json"
-d '{"email": "user@example.com", "password": "newPassword123"}'
-H "Content-Type: application/json"
-d '{"email": "user@example.com", "password": "newPassword123"}'
undefinedDOs and DON'Ts
注意事项
DO:
- Use method for proper hashing
changePassword() - Generate cryptographically secure tokens
- Set short expiration (1 hour max)
- Mark tokens as used immediately
- Return consistent responses (don't reveal email existence)
- Rate limit reset requests
DON'T:
- Store/transmit passwords in plain text
- Use predictable tokens (Math.random)
- Allow unlimited reset attempts
- Keep tokens valid indefinitely
- Allow password reset for OAuth users
建议:
- 使用方法确保正确哈希
changePassword() - 生成加密安全的令牌
- 设置短过期时间(最长1小时)
- 立即将令牌标记为已使用
- 返回一致的响应(不暴露邮箱是否存在)
- 限制重置请求速率
禁止:
- 明文存储/传输密码
- 使用可预测的令牌(如Math.random)
- 允许无限次重置尝试
- 令牌长期有效
- 允许OAuth用户重置密码
Related Skills
相关技能
- bknd-setup-auth - Configure authentication system
- bknd-login-flow - Login/logout functionality
- bknd-registration - User registration setup
- bknd-session-handling - Manage user sessions
- bknd-custom-endpoint - Create custom API endpoints
- bknd-setup-auth - 配置身份验证系统
- bknd-login-flow - 登录/登出功能
- bknd-registration - 用户注册设置
- bknd-session-handling - 管理用户会话
- bknd-custom-endpoint - 创建自定义API端点