feat: 用户名密码登录注册系统

- 新增 /api/auth/register 和 /api/auth/login 接口,使用 bcryptjs 哈希密码
- User 模型改为 username + passwordHash,id 自动生成 cuid
- 新增 AuthModal 组件(登录/注册双标签页),替换旧的 ProfileSetupModal
- 重写 /profile 页面:支持修改用户名、密码、头像、绑定邮箱、退出登录
- /api/user PUT 支持密码修改(需验证当前密码)和用户名唯一性校验
- 游客模式保留,右上角显示"登录"按钮;登录后显示头像和用户名
- 全局 nickname -> username 重命名(types、SwipeDeck、RoomManageModal、buildRoomStatus)
- 新增 logout() 清除登录态并重新生成游客 UUID
This commit is contained in:
2026-02-25 00:21:03 +08:00
parent a28f4405e9
commit 04c7b547aa
24 changed files with 1613 additions and 134 deletions
+25
View File
@@ -22,6 +22,7 @@ import {
} from "lucide-react";
import { Restaurant, MatchType, RunnerUp } from "@/types";
import { fireCelebration, playChime } from "@/lib/celebrate";
import { isRegistered } from "@/lib/userId";
interface MatchResultProps {
restaurant: Restaurant;
@@ -30,6 +31,8 @@ interface MatchResultProps {
runnerUps: RunnerUp[];
allRestaurants: Restaurant[];
userCount: number;
roomId: string;
userId: string;
onReset: () => Promise<void>;
onNarrow: (restaurantIds: string[]) => Promise<void>;
resetting: boolean;
@@ -168,6 +171,8 @@ export default function MatchResult({
runnerUps,
allRestaurants,
userCount,
roomId,
userId,
onReset,
onNarrow,
resetting,
@@ -176,6 +181,7 @@ export default function MatchResult({
const [showRunnerUps, setShowRunnerUps] = useState(false);
const [toast, setToast] = useState("");
const celebratedRef = useRef(false);
const historySavedRef = useRef(false);
const isUnanimous = matchType === "unanimous";
const showToast = useCallback((msg: string) => {
@@ -194,6 +200,25 @@ export default function MatchResult({
}
}, [isUnanimous]);
useEffect(() => {
if (historySavedRef.current) return;
if (!isRegistered()) return;
if (matchType === "no_match") return;
historySavedRef.current = true;
fetch("/api/user/history", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
userId,
roomId,
restaurant,
matchType,
participants: userCount,
}),
}).catch(() => {});
}, [userId, roomId, restaurant, matchType, userCount]);
const handleShare = useCallback(async () => {
const lines = [
isUnanimous