更新管理员登录和鉴权逻辑,优化用户体验;重构相关API以支持更安全的身份验证;调整数据库初始化以兼容新字段,确保用户信息安全;修复部分组件样式和功能,提升整体可用性。
This commit is contained in:
@@ -3,17 +3,43 @@
|
||||
import type React from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { usePathname, useRouter } from "next/navigation"
|
||||
import { LayoutDashboard, FileText, Users, CreditCard, Settings, LogOut, Wallet, Globe, BookOpen } from "lucide-react"
|
||||
|
||||
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [authChecked, setAuthChecked] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
// 非登录页时校验 Cookie,未登录则跳转登录页
|
||||
useEffect(() => {
|
||||
if (!mounted || pathname === "/admin/login") return
|
||||
setAuthChecked(false)
|
||||
let cancelled = false
|
||||
fetch("/api/admin", { credentials: "include" })
|
||||
.then((res) => {
|
||||
if (cancelled) return
|
||||
if (res.status === 401) router.replace("/admin/login")
|
||||
else setAuthChecked(true)
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setAuthChecked(true)
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [mounted, pathname, router])
|
||||
|
||||
const handleLogout = async () => {
|
||||
await fetch("/api/admin/logout", { method: "POST", credentials: "include" })
|
||||
router.replace("/admin/login")
|
||||
}
|
||||
|
||||
// 简化菜单:按功能归类,保留核心功能
|
||||
// PDF需求:分账管理、分销管理、订单管理三合一 → 交易中心
|
||||
const menuItems = [
|
||||
@@ -25,8 +51,13 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
|
||||
{ icon: Settings, label: "系统设置", href: "/admin/settings" },
|
||||
]
|
||||
|
||||
// 避免hydration错误,等待客户端mount
|
||||
if (!mounted) {
|
||||
// 登录页:不渲染侧栏,只渲染子页面
|
||||
if (pathname === "/admin/login") {
|
||||
return <div className="min-h-screen bg-[#0a1628]">{children}</div>
|
||||
}
|
||||
|
||||
// 避免 hydration 错误,等待客户端 mount 并完成鉴权
|
||||
if (!mounted || !authChecked) {
|
||||
return (
|
||||
<div className="flex min-h-screen bg-[#0a1628]">
|
||||
<div className="w-64 bg-[#0f2137] border-r border-gray-700/50" />
|
||||
@@ -66,12 +97,19 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-gray-700/50">
|
||||
<div className="p-4 border-t border-gray-700/50 space-y-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 text-gray-400 hover:text-white rounded-lg hover:bg-gray-700/50 transition-colors"
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
<span className="text-sm">退出登录</span>
|
||||
</button>
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-3 px-4 py-3 text-gray-400 hover:text-white rounded-lg hover:bg-gray-700/50 transition-colors"
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
<span className="text-sm">返回前台</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -5,11 +5,9 @@ import { useRouter } from "next/navigation"
|
||||
import { Lock, User, ShieldCheck } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { useStore } from "@/lib/store"
|
||||
|
||||
export default function AdminLoginPage() {
|
||||
const router = useRouter()
|
||||
const { adminLogin } = useStore()
|
||||
const [username, setUsername] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const [error, setError] = useState("")
|
||||
@@ -18,14 +16,22 @@ export default function AdminLoginPage() {
|
||||
const handleLogin = async () => {
|
||||
setError("")
|
||||
setLoading(true)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
const success = adminLogin(username, password)
|
||||
if (success) {
|
||||
router.push("/admin")
|
||||
} else {
|
||||
setError("用户名或密码错误")
|
||||
try {
|
||||
const res = await fetch("/api/admin", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username: username.trim(), password }),
|
||||
credentials: "include",
|
||||
})
|
||||
const data = await res.json()
|
||||
if (res.ok && data.success) {
|
||||
router.push("/admin")
|
||||
return
|
||||
}
|
||||
setError(data.error || "用户名或密码错误")
|
||||
} catch {
|
||||
setError("网络错误,请重试")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
@@ -95,12 +101,6 @@ export default function AdminLoginPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 pt-6 border-t border-gray-700/50">
|
||||
<p className="text-gray-500 text-xs text-center">
|
||||
默认账号: <span className="text-gray-300 font-mono">admin</span> /{" "}
|
||||
<span className="text-gray-300 font-mono">key123456</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { requireAdminResponse } from '@/lib/admin-auth'
|
||||
|
||||
// 获取书籍目录
|
||||
const BOOK_DIR = path.join(process.cwd(), 'book')
|
||||
@@ -14,6 +15,8 @@ const BOOK_DIR = path.join(process.cwd(), 'book')
|
||||
* GET - 获取所有章节列表
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
const authErr = requireAdminResponse(request)
|
||||
if (authErr) return authErr
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const includeContent = searchParams.get('content') === 'true'
|
||||
@@ -274,6 +277,8 @@ export async function GET(request: Request) {
|
||||
* POST - 更新章节设置
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
const authErr = requireAdminResponse(request)
|
||||
if (authErr) return authErr
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { action, chapterId, data } = body
|
||||
|
||||
@@ -5,11 +5,14 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
import { requireAdminResponse } from '@/lib/admin-auth'
|
||||
|
||||
const BOOK_DIR = path.join(process.cwd(), 'book')
|
||||
|
||||
// GET: 获取所有章节列表
|
||||
export async function GET(req: NextRequest) {
|
||||
const authErr = requireAdminResponse(req)
|
||||
if (authErr) return authErr
|
||||
try {
|
||||
const chapters = getAllChapters()
|
||||
|
||||
@@ -28,6 +31,8 @@ export async function GET(req: NextRequest) {
|
||||
|
||||
// POST: 创建新章节
|
||||
export async function POST(req: NextRequest) {
|
||||
const authErr = requireAdminResponse(req)
|
||||
if (authErr) return authErr
|
||||
try {
|
||||
const body = await req.json()
|
||||
const { title, content, category, tags } = body
|
||||
@@ -70,6 +75,8 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
// PUT: 更新章节
|
||||
export async function PUT(req: NextRequest) {
|
||||
const authErr = requireAdminResponse(req)
|
||||
if (authErr) return authErr
|
||||
try {
|
||||
const body = await req.json()
|
||||
const { id, title, content, category, tags } = body
|
||||
@@ -97,6 +104,8 @@ export async function PUT(req: NextRequest) {
|
||||
|
||||
// DELETE: 删除章节
|
||||
export async function DELETE(req: NextRequest) {
|
||||
const authErr = requireAdminResponse(req)
|
||||
if (authErr) return authErr
|
||||
try {
|
||||
const { searchParams } = new URL(req.url)
|
||||
const id = searchParams.get('id')
|
||||
|
||||
9
app/api/admin/logout/route.ts
Normal file
9
app/api/admin/logout/route.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getAdminCookieName, getAdminCookieOptions } from '@/lib/admin-auth'
|
||||
|
||||
export async function POST(_req: NextRequest) {
|
||||
const res = NextResponse.json({ success: true })
|
||||
const opts = getAdminCookieOptions()
|
||||
res.cookies.set(getAdminCookieName(), '', { ...opts, maxAge: 0 })
|
||||
return res
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// 付费模块管理API
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { requireAdminResponse } from '@/lib/admin-auth'
|
||||
|
||||
// 模拟订单数据
|
||||
let orders = [
|
||||
@@ -29,6 +30,8 @@ let orders = [
|
||||
|
||||
// GET: 获取订单列表
|
||||
export async function GET(req: NextRequest) {
|
||||
const authErr = requireAdminResponse(req)
|
||||
if (authErr) return authErr
|
||||
const { searchParams } = new URL(req.url)
|
||||
const status = searchParams.get('status')
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
@@ -71,6 +74,8 @@ export async function GET(req: NextRequest) {
|
||||
|
||||
// POST: 创建订单(手动)
|
||||
export async function POST(req: NextRequest) {
|
||||
const authErr = requireAdminResponse(req)
|
||||
if (authErr) return authErr
|
||||
try {
|
||||
const body = await req.json()
|
||||
const { userId, userName, amount, note } = body
|
||||
@@ -110,6 +115,8 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
// PUT: 更新订单状态
|
||||
export async function PUT(req: NextRequest) {
|
||||
const authErr = requireAdminResponse(req)
|
||||
if (authErr) return authErr
|
||||
try {
|
||||
const body = await req.json()
|
||||
const { orderId, status, note } = body
|
||||
@@ -148,6 +155,8 @@ export async function PUT(req: NextRequest) {
|
||||
|
||||
// DELETE: 删除订单
|
||||
export async function DELETE(req: NextRequest) {
|
||||
const authErr = requireAdminResponse(req)
|
||||
if (authErr) return authErr
|
||||
try {
|
||||
const { searchParams } = new URL(req.url)
|
||||
const orderId = searchParams.get('id')
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 分销模块管理API
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { requireAdminResponse } from '@/lib/admin-auth'
|
||||
|
||||
// 模拟分销数据
|
||||
let referralRecords = [
|
||||
@@ -52,6 +53,8 @@ let commissionRecords = [
|
||||
|
||||
// GET: 获取分销概览或列表
|
||||
export async function GET(req: NextRequest) {
|
||||
const authErr = requireAdminResponse(req)
|
||||
if (authErr) return authErr
|
||||
const { searchParams } = new URL(req.url)
|
||||
const type = searchParams.get('type') || 'list'
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
@@ -95,6 +98,8 @@ export async function GET(req: NextRequest) {
|
||||
|
||||
// POST: 创建分销记录或处理佣金
|
||||
export async function POST(req: NextRequest) {
|
||||
const authErr = requireAdminResponse(req)
|
||||
if (authErr) return authErr
|
||||
try {
|
||||
const body = await req.json()
|
||||
const { action, data } = body
|
||||
@@ -170,6 +175,8 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
// PUT: 更新分销记录
|
||||
export async function PUT(req: NextRequest) {
|
||||
const authErr = requireAdminResponse(req)
|
||||
if (authErr) return authErr
|
||||
try {
|
||||
const body = await req.json()
|
||||
const { referrerId, status, commissionRate, note } = body
|
||||
@@ -205,6 +212,8 @@ export async function PUT(req: NextRequest) {
|
||||
|
||||
// DELETE: 删除分销记录
|
||||
export async function DELETE(req: NextRequest) {
|
||||
const authErr = requireAdminResponse(req)
|
||||
if (authErr) return authErr
|
||||
try {
|
||||
const { searchParams } = new URL(req.url)
|
||||
const referrerId = searchParams.get('id')
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
// app/api/admin/route.ts
|
||||
// 后台管理API入口
|
||||
// 后台管理API入口:登录与鉴权(账号密码从环境变量读取,默认 admin / admin123)
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import {
|
||||
verifyAdminToken,
|
||||
getAdminTokenFromRequest,
|
||||
verifyAdminCredentials,
|
||||
getAdminCredentials,
|
||||
createAdminToken,
|
||||
getAdminCookieName,
|
||||
getAdminCookieOptions,
|
||||
} from '@/lib/admin-auth'
|
||||
|
||||
// 验证管理员权限
|
||||
function verifyAdmin(req: NextRequest) {
|
||||
const token = req.headers.get('Authorization')?.replace('Bearer ', '')
|
||||
|
||||
// TODO: 实现真实的token验证
|
||||
if (!token || token !== 'admin-token-secret') {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
function requireAdmin(req: NextRequest): boolean {
|
||||
const token = getAdminTokenFromRequest(req)
|
||||
return verifyAdminToken(token)
|
||||
}
|
||||
|
||||
// GET: 获取后台概览数据
|
||||
// GET: 获取后台概览数据(需已登录)
|
||||
export async function GET(req: NextRequest) {
|
||||
if (!verifyAdmin(req)) {
|
||||
if (!requireAdmin(req)) {
|
||||
return NextResponse.json(
|
||||
{ error: '未授权访问' },
|
||||
{ error: '未授权访问,请先登录' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
@@ -58,27 +60,31 @@ export async function GET(req: NextRequest) {
|
||||
return NextResponse.json(overview)
|
||||
}
|
||||
|
||||
// POST: 管理员登录
|
||||
// POST: 管理员登录(账号密码从环境变量 ADMIN_USERNAME / ADMIN_PASSWORD 读取,默认 admin / admin123)
|
||||
export async function POST(req: NextRequest) {
|
||||
const body = await req.json()
|
||||
const { username, password } = body
|
||||
|
||||
// TODO: 实现真实的登录验证
|
||||
if (username === 'admin' && password === 'admin123') {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
token: 'admin-token-secret',
|
||||
user: {
|
||||
id: 'admin',
|
||||
username: 'admin',
|
||||
role: 'admin',
|
||||
name: '卡若'
|
||||
}
|
||||
})
|
||||
if (!username || !password) {
|
||||
return NextResponse.json(
|
||||
{ error: '请输入用户名和密码' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: '用户名或密码错误' },
|
||||
{ status: 401 }
|
||||
)
|
||||
if (!verifyAdminCredentials(String(username).trim(), String(password))) {
|
||||
return NextResponse.json(
|
||||
{ error: '用户名或密码错误' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const token = createAdminToken()
|
||||
const res = NextResponse.json({
|
||||
success: true,
|
||||
user: { id: 'admin', username: getAdminCredentials().username, role: 'admin', name: '卡若' },
|
||||
})
|
||||
const opts = getAdminCookieOptions()
|
||||
res.cookies.set(getAdminCookieName(), token, opts)
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -6,9 +6,12 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
import { createTransfer } from '@/lib/wechat-transfer'
|
||||
import { requireAdminResponse } from '@/lib/admin-auth'
|
||||
|
||||
// 获取所有提现记录
|
||||
export async function GET(request: Request) {
|
||||
const authErr = requireAdminResponse(request)
|
||||
if (authErr) return authErr
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const status = searchParams.get('status') // pending, success, failed, all
|
||||
@@ -84,6 +87,8 @@ export async function GET(request: Request) {
|
||||
|
||||
// 处理提现(审批/拒绝)
|
||||
export async function PUT(request: Request) {
|
||||
const authErr = requireAdminResponse(request)
|
||||
if (authErr) return authErr
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { id, action, reason } = body // action: approve, reject
|
||||
|
||||
73
app/api/auth/login/route.ts
Normal file
73
app/api/auth/login/route.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Web 端登录:手机号 + 密码
|
||||
* POST { phone, password } -> 校验后返回用户信息(不含密码)
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
import { verifyPassword } from '@/lib/password'
|
||||
|
||||
function mapRowToUser(r: any) {
|
||||
return {
|
||||
id: r.id,
|
||||
phone: r.phone || '',
|
||||
nickname: r.nickname || '',
|
||||
isAdmin: !!r.is_admin,
|
||||
purchasedSections: Array.isArray(r.purchased_sections)
|
||||
? r.purchased_sections
|
||||
: (r.purchased_sections ? JSON.parse(String(r.purchased_sections)) : []) || [],
|
||||
hasFullBook: !!r.has_full_book,
|
||||
referralCode: r.referral_code || '',
|
||||
referredBy: r.referred_by || undefined,
|
||||
earnings: parseFloat(String(r.earnings || 0)),
|
||||
pendingEarnings: parseFloat(String(r.pending_earnings || 0)),
|
||||
withdrawnEarnings: parseFloat(String(r.withdrawn_earnings || 0)),
|
||||
referralCount: Number(r.referral_count) || 0,
|
||||
createdAt: r.created_at || '',
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { phone, password } = body
|
||||
|
||||
if (!phone || !password) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '请输入手机号和密码' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const rows = await query(
|
||||
'SELECT id, phone, nickname, password, is_admin, has_full_book, referral_code, referred_by, earnings, pending_earnings, withdrawn_earnings, referral_count, purchased_sections, created_at FROM users WHERE phone = ?',
|
||||
[String(phone).trim()]
|
||||
) as any[]
|
||||
|
||||
if (!rows || rows.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '用户不存在或密码错误' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const row = rows[0]
|
||||
const storedPassword = row.password == null ? '' : String(row.password)
|
||||
|
||||
if (!verifyPassword(String(password), storedPassword)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '密码错误' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const user = mapRowToUser(row)
|
||||
return NextResponse.json({ success: true, user })
|
||||
} catch (e) {
|
||||
console.error('[Auth Login] error:', e)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '登录失败' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
54
app/api/auth/reset-password/route.ts
Normal file
54
app/api/auth/reset-password/route.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 忘记密码 / 重置密码(Web 端)
|
||||
* POST { phone, newPassword } -> 按手机号更新密码(无验证码版本,适合内测/内部使用)
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
import { hashPassword } from '@/lib/password'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { phone, newPassword } = body
|
||||
|
||||
if (!phone || !newPassword) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '请输入手机号和新密码' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const trimmedPhone = String(phone).trim()
|
||||
const trimmedPassword = String(newPassword).trim()
|
||||
|
||||
if (trimmedPassword.length < 6) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '密码至少 6 位' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const rows = await query('SELECT id FROM users WHERE phone = ?', [trimmedPhone]) as any[]
|
||||
if (!rows || rows.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '该手机号未注册' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
const hashed = hashPassword(trimmedPassword)
|
||||
await query('UPDATE users SET password = ?, updated_at = NOW() WHERE phone = ?', [
|
||||
hashed,
|
||||
trimmedPhone,
|
||||
])
|
||||
|
||||
return NextResponse.json({ success: true, message: '密码已重置,请使用新密码登录' })
|
||||
} catch (e) {
|
||||
console.error('[Auth ResetPassword] error:', e)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '重置失败' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { query } from '@/lib/db'
|
||||
import { hashPassword } from '@/lib/password'
|
||||
|
||||
// 生成用户ID
|
||||
function generateUserId(): string {
|
||||
@@ -32,29 +33,35 @@ export async function GET(request: NextRequest) {
|
||||
const openId = searchParams.get('openId')
|
||||
|
||||
try {
|
||||
// 获取单个用户
|
||||
const omitPassword = (u: any) => {
|
||||
if (!u) return u
|
||||
const { password: _, ...rest } = u
|
||||
return rest
|
||||
}
|
||||
|
||||
// 获取单个用户(不返回 password)
|
||||
if (id) {
|
||||
const users = await query('SELECT * FROM users WHERE id = ?', [id]) as any[]
|
||||
if (users.length > 0) {
|
||||
return NextResponse.json({ success: true, user: users[0] })
|
||||
return NextResponse.json({ success: true, user: omitPassword(users[0]) })
|
||||
}
|
||||
return NextResponse.json({ success: false, error: '用户不存在' }, { status: 404 })
|
||||
}
|
||||
|
||||
|
||||
// 通过手机号查询
|
||||
if (phone) {
|
||||
const users = await query('SELECT * FROM users WHERE phone = ?', [phone]) as any[]
|
||||
if (users.length > 0) {
|
||||
return NextResponse.json({ success: true, user: users[0] })
|
||||
return NextResponse.json({ success: true, user: omitPassword(users[0]) })
|
||||
}
|
||||
return NextResponse.json({ success: false, error: '用户不存在' }, { status: 404 })
|
||||
}
|
||||
|
||||
|
||||
// 通过openId查询
|
||||
if (openId) {
|
||||
const users = await query('SELECT * FROM users WHERE open_id = ?', [openId]) as any[]
|
||||
if (users.length > 0) {
|
||||
return NextResponse.json({ success: true, user: users[0] })
|
||||
return NextResponse.json({ success: true, user: omitPassword(users[0]) })
|
||||
}
|
||||
return NextResponse.json({ success: false, error: '用户不存在' }, { status: 404 })
|
||||
}
|
||||
@@ -95,13 +102,18 @@ export async function POST(request: NextRequest) {
|
||||
const body = await request.json()
|
||||
const { openId, phone, nickname, password, wechatId, avatar, referredBy, is_admin } = body
|
||||
|
||||
// 密码:确保非空字符串才存储(bcrypt 哈希)
|
||||
const rawPassword = typeof password === 'string' ? password.trim() : ''
|
||||
const passwordToStore = rawPassword.length >= 6 ? hashPassword(rawPassword) : null
|
||||
|
||||
// 检查openId或手机号是否已存在
|
||||
if (openId) {
|
||||
const existing = await query('SELECT id FROM users WHERE open_id = ?', [openId]) as any[]
|
||||
if (existing.length > 0) {
|
||||
// 已存在,返回现有用户
|
||||
const users = await query('SELECT * FROM users WHERE open_id = ?', [openId]) as any[]
|
||||
return NextResponse.json({ success: true, user: users[0], isNew: false })
|
||||
const u = users[0]
|
||||
const { password: _p2, ...userSafe } = u || {}
|
||||
return NextResponse.json({ success: true, user: userSafe, isNew: false })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +127,7 @@ export async function POST(request: NextRequest) {
|
||||
// 生成用户ID和推荐码
|
||||
const userId = generateUserId()
|
||||
const referralCode = generateReferralCode(openId || phone || userId)
|
||||
|
||||
|
||||
// 创建用户
|
||||
await query(`
|
||||
INSERT INTO users (
|
||||
@@ -128,7 +140,7 @@ export async function POST(request: NextRequest) {
|
||||
openId || null,
|
||||
phone || null,
|
||||
nickname || '用户' + userId.slice(-4),
|
||||
password || null,
|
||||
passwordToStore,
|
||||
wechatId || null,
|
||||
avatar || null,
|
||||
referralCode,
|
||||
@@ -136,12 +148,13 @@ export async function POST(request: NextRequest) {
|
||||
is_admin || false
|
||||
])
|
||||
|
||||
// 返回新用户
|
||||
// 返回新用户(不返回 password)
|
||||
const users = await query('SELECT * FROM users WHERE id = ?', [userId]) as any[]
|
||||
|
||||
const u = users[0]
|
||||
const { password: _p, ...userSafe } = u || {}
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
user: users[0],
|
||||
user: userSafe,
|
||||
isNew: true,
|
||||
message: '用户创建成功'
|
||||
})
|
||||
@@ -189,7 +202,7 @@ export async function PUT(request: NextRequest) {
|
||||
}
|
||||
if (password !== undefined) {
|
||||
updates.push('password = ?')
|
||||
values.push(password)
|
||||
values.push(password === '' || password == null ? null : hashPassword(String(password).trim()))
|
||||
}
|
||||
if (has_full_book !== undefined) {
|
||||
updates.push('has_full_book = ?')
|
||||
|
||||
136
app/login/forgot/page.tsx
Normal file
136
app/login/forgot/page.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import Link from "next/link"
|
||||
import { ChevronLeft, Phone, Hash } from "lucide-react"
|
||||
|
||||
export default function ForgotPasswordPage() {
|
||||
const router = useRouter()
|
||||
const [phone, setPhone] = useState("")
|
||||
const [newPassword, setNewPassword] = useState("")
|
||||
const [confirmPassword, setConfirmPassword] = useState("")
|
||||
const [error, setError] = useState("")
|
||||
const [success, setSuccess] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError("")
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
if (!phone.trim()) {
|
||||
setError("请输入手机号")
|
||||
return
|
||||
}
|
||||
if (!newPassword.trim()) {
|
||||
setError("请输入新密码")
|
||||
return
|
||||
}
|
||||
if (newPassword.trim().length < 6) {
|
||||
setError("密码至少 6 位")
|
||||
return
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
setError("两次输入的密码不一致")
|
||||
return
|
||||
}
|
||||
|
||||
const res = await fetch("/api/auth/reset-password", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ phone: phone.trim(), newPassword: newPassword.trim() }),
|
||||
})
|
||||
const data = await res.json()
|
||||
|
||||
if (data.success) {
|
||||
setSuccess(true)
|
||||
setTimeout(() => router.push("/login"), 2000)
|
||||
} else {
|
||||
setError(data.error || "重置失败")
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white flex flex-col items-center justify-center px-6">
|
||||
<p className="text-[#30d158] text-lg mb-4">密码已重置</p>
|
||||
<p className="text-gray-500 text-sm">请使用新密码登录,正在跳转...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white flex flex-col">
|
||||
<header className="flex items-center px-4 py-3">
|
||||
<Link
|
||||
href="/login"
|
||||
className="w-9 h-9 rounded-full bg-[#1c1c1e] flex items-center justify-center"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5 text-gray-400" />
|
||||
</Link>
|
||||
<h1 className="flex-1 text-center text-lg font-semibold">找回密码</h1>
|
||||
<div className="w-9" />
|
||||
</header>
|
||||
|
||||
<main className="flex-1 px-6 pt-8">
|
||||
<p className="text-gray-500 text-sm mb-6">
|
||||
请输入注册时使用的手机号和新密码,重置后请使用新密码登录。
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="relative">
|
||||
<Phone className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
|
||||
<input
|
||||
type="tel"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
placeholder="手机号"
|
||||
className="w-full pl-12 pr-4 py-3.5 bg-[#1c1c1e] rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#30d158]/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Hash className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
placeholder="新密码(至少 6 位)"
|
||||
className="w-full pl-12 pr-4 py-3.5 bg-[#1c1c1e] rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#30d158]/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Hash className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
placeholder="再次输入新密码"
|
||||
className="w-full pl-12 pr-4 py-3.5 bg-[#1c1c1e] rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#30d158]/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-red-500 text-sm">{error}</p>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !phone || !newPassword || !confirmPassword}
|
||||
className="w-full py-3.5 bg-[#30d158] text-white font-medium rounded-xl active:scale-[0.98] transition-transform disabled:opacity-50"
|
||||
>
|
||||
{loading ? "提交中..." : "重置密码"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="text-gray-500 text-xs mt-6 text-center">
|
||||
若该手机号未注册,将提示「该手机号未注册」;重置后请使用新密码在登录页登录。
|
||||
</p>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import Link from "next/link"
|
||||
import { useStore } from "@/lib/store"
|
||||
import { ChevronLeft, Phone, User, Hash } from "lucide-react"
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter()
|
||||
const { login, register, adminLogin } = useStore()
|
||||
const { login, register } = useStore()
|
||||
const [mode, setMode] = useState<"login" | "register">("login")
|
||||
const [phone, setPhone] = useState("")
|
||||
const [code, setCode] = useState("")
|
||||
@@ -21,30 +22,52 @@ export default function LoginPage() {
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
// 管理员登录
|
||||
// 管理员登录(使用 code 作为密码,调用后台 API 并写 Cookie)
|
||||
if (phone.toLowerCase() === "admin") {
|
||||
if (adminLogin(phone, code)) {
|
||||
router.push("/admin")
|
||||
return
|
||||
} else {
|
||||
setError("管理员密码错误")
|
||||
return
|
||||
try {
|
||||
const res = await fetch("/api/admin", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username: phone, password: code }),
|
||||
credentials: "include",
|
||||
})
|
||||
const data = await res.json()
|
||||
if (res.ok && data.success) {
|
||||
router.push("/admin")
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// fallthrough to error
|
||||
}
|
||||
setError("管理员密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === "login") {
|
||||
if (!code.trim()) {
|
||||
setError("请输入密码")
|
||||
return
|
||||
}
|
||||
const success = await login(phone, code)
|
||||
if (success) {
|
||||
router.push("/")
|
||||
} else {
|
||||
setError("验证码错误或用户不存在")
|
||||
setError("密码错误或用户不存在")
|
||||
}
|
||||
} else {
|
||||
if (!nickname.trim()) {
|
||||
setError("请输入昵称")
|
||||
return
|
||||
}
|
||||
const success = await register(phone, nickname, referralCode || undefined)
|
||||
if (!code.trim()) {
|
||||
setError("请设置密码(至少 6 位)")
|
||||
return
|
||||
}
|
||||
if (code.trim().length < 6) {
|
||||
setError("密码至少 6 位")
|
||||
return
|
||||
}
|
||||
const success = await register(phone, nickname, code, referralCode || undefined)
|
||||
if (success) {
|
||||
router.push("/")
|
||||
} else {
|
||||
@@ -102,14 +125,14 @@ export default function LoginPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 验证码/密码 */}
|
||||
{/* 密码 */}
|
||||
<div className="relative">
|
||||
<Hash className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
|
||||
<input
|
||||
type="text"
|
||||
type="password"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
placeholder={mode === "login" ? "验证码(测试:123456)" : "设置密码"}
|
||||
placeholder={mode === "login" ? "密码" : "设置密码(至少 6 位)"}
|
||||
className="w-full pl-12 pr-4 py-3.5 bg-[#1c1c1e] rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#30d158]/50"
|
||||
/>
|
||||
</div>
|
||||
@@ -140,9 +163,16 @@ export default function LoginPage() {
|
||||
{loading ? "处理中..." : mode === "login" ? "登录" : "注册"}
|
||||
</button>
|
||||
|
||||
{/* 切换模式 */}
|
||||
<div className="text-center">
|
||||
<button onClick={() => setMode(mode === "login" ? "register" : "login")} className="text-[#30d158] text-sm">
|
||||
{/* 忘记密码 / 切换模式 */}
|
||||
<div className="text-center space-y-2">
|
||||
{mode === "login" && (
|
||||
<div>
|
||||
<Link href="/login/forgot" className="text-[#30d158] text-sm">
|
||||
忘记密码?
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<button onClick={() => setMode(mode === "login" ? "register" : "login")} className="text-[#30d158] text-sm block mx-auto">
|
||||
{mode === "login" ? "没有账号?去注册" : "已有账号?去登录"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user