diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx
index 55fec8ac..2cb29b5f 100644
--- a/app/admin/layout.tsx
+++ b/app/admin/layout.tsx
@@ -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
{children}
+ }
+
+ // 避免 hydration 错误,等待客户端 mount 并完成鉴权
+ if (!mounted || !authChecked) {
return (
@@ -66,12 +97,19 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
})}
-
+
+
-
返回前台
diff --git a/app/admin/login/page.tsx b/app/admin/login/page.tsx
index 8b9235d0..b92ad3c4 100644
--- a/app/admin/login/page.tsx
+++ b/app/admin/login/page.tsx
@@ -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() {
-
-
- 默认账号: admin /{" "}
- key123456
-
-
{/* Footer */}
diff --git a/app/api/admin/chapters/route.ts b/app/api/admin/chapters/route.ts
index a8ef4a11..90f16f07 100644
--- a/app/api/admin/chapters/route.ts
+++ b/app/api/admin/chapters/route.ts
@@ -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
diff --git a/app/api/admin/content/route.ts b/app/api/admin/content/route.ts
index 84d54cc1..4d156f21 100644
--- a/app/api/admin/content/route.ts
+++ b/app/api/admin/content/route.ts
@@ -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')
diff --git a/app/api/admin/logout/route.ts b/app/api/admin/logout/route.ts
new file mode 100644
index 00000000..9d287345
--- /dev/null
+++ b/app/api/admin/logout/route.ts
@@ -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
+}
diff --git a/app/api/admin/payment/route.ts b/app/api/admin/payment/route.ts
index cde1bea1..3aaba3c5 100644
--- a/app/api/admin/payment/route.ts
+++ b/app/api/admin/payment/route.ts
@@ -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')
diff --git a/app/api/admin/referral/route.ts b/app/api/admin/referral/route.ts
index 832229a4..771c82ad 100644
--- a/app/api/admin/referral/route.ts
+++ b/app/api/admin/referral/route.ts
@@ -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')
diff --git a/app/api/admin/route.ts b/app/api/admin/route.ts
index ae960d77..75ee978f 100644
--- a/app/api/admin/route.ts
+++ b/app/api/admin/route.ts
@@ -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
}
diff --git a/app/api/admin/withdrawals/route.ts b/app/api/admin/withdrawals/route.ts
index 867c5695..29db3002 100644
--- a/app/api/admin/withdrawals/route.ts
+++ b/app/api/admin/withdrawals/route.ts
@@ -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
diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts
new file mode 100644
index 00000000..a578784e
--- /dev/null
+++ b/app/api/auth/login/route.ts
@@ -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 }
+ )
+ }
+}
diff --git a/app/api/auth/reset-password/route.ts b/app/api/auth/reset-password/route.ts
new file mode 100644
index 00000000..3ba84902
--- /dev/null
+++ b/app/api/auth/reset-password/route.ts
@@ -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 }
+ )
+ }
+}
diff --git a/app/api/db/users/route.ts b/app/api/db/users/route.ts
index ff24f1ff..e6eb1421 100644
--- a/app/api/db/users/route.ts
+++ b/app/api/db/users/route.ts
@@ -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 = ?')
diff --git a/app/login/forgot/page.tsx b/app/login/forgot/page.tsx
new file mode 100644
index 00000000..42a5c3f3
--- /dev/null
+++ b/app/login/forgot/page.tsx
@@ -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 (
+
+
密码已重置
+
请使用新密码登录,正在跳转...
+
+ )
+ }
+
+ return (
+
+
+
+
+
+ 请输入注册时使用的手机号和新密码,重置后请使用新密码登录。
+
+
+
+
+
+ 若该手机号未注册,将提示「该手机号未注册」;重置后请使用新密码在登录页登录。
+
+
+
+ )
+}
diff --git a/app/login/page.tsx b/app/login/page.tsx
index 08e33836..820c93c6 100644
--- a/app/login/page.tsx
+++ b/app/login/page.tsx
@@ -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() {
)}
- {/* 验证码/密码 */}
+ {/* 密码 */}
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"
/>
@@ -140,9 +163,16 @@ export default function LoginPage() {
{loading ? "处理中..." : mode === "login" ? "登录" : "注册"}
- {/* 切换模式 */}
-
-
+
+
+ 忘记密码?
+
+
+
{error && {error}
}
{
+ try {
+ const rows = await query(
+ "SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = ?",
+ [colName]
+ ) as any[]
+ if (!rows?.length) {
+ await query(`ALTER TABLE users ADD COLUMN ${colName} ${colDef}`)
+ }
+ } catch (e) { /* 忽略 */ }
+ }
+ await addColumnIfMissing('session_key', 'VARCHAR(100)')
+ await addColumnIfMissing('password', 'VARCHAR(100)')
+ await addColumnIfMissing('referred_by', 'VARCHAR(50)')
+ await addColumnIfMissing('is_admin', 'BOOLEAN DEFAULT FALSE')
+ await addColumnIfMissing('match_count_today', 'INT DEFAULT 0')
+ await addColumnIfMissing('last_match_date', 'DATE')
+ await addColumnIfMissing('withdrawn_earnings', 'DECIMAL(10,2) DEFAULT 0')
console.log('用户表初始化完成')
diff --git a/lib/password.ts b/lib/password.ts
new file mode 100644
index 00000000..1c40ea4e
--- /dev/null
+++ b/lib/password.ts
@@ -0,0 +1,56 @@
+/**
+ * 密码哈希与校验(仅用于 Web 用户注册/登录,与后台管理员密码无关)
+ * 使用 Node crypto.scrypt,存储格式 saltHex:hashHex,兼容旧明文密码
+ */
+
+import { scryptSync, timingSafeEqual, randomFillSync } from 'crypto'
+
+const SALT_LEN = 16
+const KEYLEN = 32
+
+function bufferToHex(buf: Buffer): string {
+ return buf.toString('hex')
+}
+
+function hexToBuffer(hex: string): Buffer {
+ return Buffer.from(hex, 'hex')
+}
+
+/**
+ * 对明文密码做哈希,存入数据库
+ * 格式: saltHex:hashHex(约 97 字符,适配 VARCHAR(100))
+ * 与 verifyPassword 一致:内部先 trim,保证注册/登录/重置用同一套规则
+ */
+export function hashPassword(plain: string): string {
+ const trimmed = String(plain).trim()
+ const salt = Buffer.allocUnsafe(SALT_LEN)
+ randomFillSync(salt)
+ const hash = scryptSync(trimmed, salt, KEYLEN, { N: 16384, r: 8, p: 1 })
+ return bufferToHex(salt) + ':' + bufferToHex(hash)
+}
+
+/**
+ * 校验密码:支持新格式(salt:hash)与旧明文(兼容历史数据)
+ * 与 hashPassword 一致:对输入先 trim 再参与校验
+ */
+export function verifyPassword(plain: string, stored: string | null | undefined): boolean {
+ const trimmed = String(plain).trim()
+ if (stored == null || stored === '') {
+ return trimmed === ''
+ }
+ if (stored.includes(':')) {
+ const [saltHex, hashHex] = stored.split(':')
+ if (!saltHex || !hashHex || saltHex.length !== SALT_LEN * 2 || hashHex.length !== KEYLEN * 2) {
+ return false
+ }
+ try {
+ const salt = hexToBuffer(saltHex)
+ const expected = hexToBuffer(hashHex)
+ const derived = scryptSync(trimmed, salt, KEYLEN, { N: 16384, r: 8, p: 1 })
+ return derived.length === expected.length && timingSafeEqual(derived, expected)
+ } catch {
+ return false
+ }
+ }
+ return trimmed === stored
+}
diff --git a/lib/store.ts b/lib/store.ts
index 53e10685..e036d5cb 100644
--- a/lib/store.ts
+++ b/lib/store.ts
@@ -172,9 +172,9 @@ interface StoreState {
withdrawals: Withdrawal[]
settings: Settings
- login: (phone: string, code: string) => Promise
+ login: (phone: string, password: string) => Promise
logout: () => void
- register: (phone: string, nickname: string, referralCode?: string) => Promise
+ register: (phone: string, nickname: string, password?: string, referralCode?: string) => Promise
purchaseSection: (sectionId: string, sectionTitle?: string, paymentMethod?: string) => Promise
purchaseFullBook: (paymentMethod?: string) => Promise
hasPurchased: (sectionId: string) => boolean
@@ -302,59 +302,63 @@ export const useStore = create()(
withdrawals: [],
settings: initialSettings,
- login: async (phone: string, code: string) => {
- // 真实场景下应该调用后端API验证验证码
- // 这里暂时保留简单验证用于演示
- if (code !== "123456") {
+ login: async (phone: string, password: string) => {
+ try {
+ const res = await fetch("/api/auth/login", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ phone: phone.trim(), password: password.trim() }),
+ })
+ const data = await res.json()
+ if (data.success && data.user) {
+ set({ user: data.user, isLoggedIn: true })
+ return true
+ }
+ return false
+ } catch {
return false
}
- const users = JSON.parse(localStorage.getItem("users") || "[]") as User[]
- const existingUser = users.find((u) => u.phone === phone)
- if (existingUser) {
- set({ user: existingUser, isLoggedIn: true })
- return true
- }
- return false
},
logout: () => {
set({ user: null, isLoggedIn: false })
},
- register: async (phone: string, nickname: string, referralCode?: string) => {
- const users = JSON.parse(localStorage.getItem("users") || "[]") as User[]
- if (users.find((u) => u.phone === phone)) {
+ register: async (phone: string, nickname: string, password?: string, referralCode?: string) => {
+ try {
+ const res = await fetch("/api/db/users", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ phone: phone.trim(),
+ nickname: nickname.trim(),
+ password: password ? String(password).trim() : null,
+ referredBy: referralCode?.trim() || null,
+ }),
+ })
+ const data = await res.json()
+ if (!data.success || !data.user) return false
+ const r = data.user as any
+ const newUser: User = {
+ id: r.id,
+ phone: r.phone || phone,
+ nickname: r.nickname || 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 || referralCode,
+ 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 || new Date().toISOString(),
+ }
+ set({ user: newUser, isLoggedIn: true })
+ return true
+ } catch {
return false
}
-
- const newUser: User = {
- id: `user_${Date.now()}`,
- phone,
- nickname,
- isAdmin: false,
- purchasedSections: [],
- hasFullBook: false,
- referralCode: `REF${Date.now().toString(36).toUpperCase()}`,
- referredBy: referralCode,
- earnings: 0,
- pendingEarnings: 0,
- withdrawnEarnings: 0,
- referralCount: 0,
- createdAt: new Date().toISOString(),
- }
-
- if (referralCode) {
- const referrer = users.find((u) => u.referralCode === referralCode)
- if (referrer) {
- referrer.referralCount = (referrer.referralCount || 0) + 1
- localStorage.setItem("users", JSON.stringify(users))
- }
- }
-
- users.push(newUser)
- localStorage.setItem("users", JSON.stringify(users))
- set({ user: newUser, isLoggedIn: true })
- return true
},
purchaseSection: async (sectionId: string, sectionTitle?: string, paymentMethod?: string) => {
diff --git a/next-env.d.ts b/next-env.d.ts
index c4b7818f..9edff1c7 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/dev/types/routes.d.ts";
+import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/scripts/__pycache__/deploy_baota_pure_api.cpython-311.pyc b/scripts/__pycache__/deploy_baota_pure_api.cpython-311.pyc
index 26bc9e53..01fc0a96 100644
Binary files a/scripts/__pycache__/deploy_baota_pure_api.cpython-311.pyc and b/scripts/__pycache__/deploy_baota_pure_api.cpython-311.pyc differ
diff --git a/scripts/deploy_baota_pure_api.py b/scripts/deploy_baota_pure_api.py
new file mode 100644
index 00000000..08279dc5
--- /dev/null
+++ b/scripts/deploy_baota_pure_api.py
@@ -0,0 +1,155 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+宝塔面板 API 模块 - Node 项目重启 / 计划任务触发
+
+被 devlop.py 内部调用;也可单独使用:
+ python scripts/deploy_baota_pure_api.py # 重启 Node 项目
+ python scripts/deploy_baota_pure_api.py --create-dir # 并创建项目目录
+ python scripts/deploy_baota_pure_api.py --task-id 1 # 触发计划任务 ID=1
+
+环境变量:
+ BAOTA_PANEL_URL # 宝塔面板地址,如 https://42.194.232.22:9988 或带安全入口
+ BAOTA_API_KEY # 宝塔 API 密钥(面板 → 设置 → API 接口)
+ DEPLOY_PM2_APP # PM2 项目名称,默认 soul
+"""
+
+from __future__ import print_function
+
+import os
+import sys
+import time
+import hashlib
+
+try:
+ import requests
+ import urllib3
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+except ImportError:
+ print("请先安装: pip install requests")
+ sys.exit(1)
+
+# 配置:可通过环境变量覆盖
+CFG = {
+ "panel_url": os.environ.get("BAOTA_PANEL_URL", "https://42.194.232.22:9988"),
+ "api_key": os.environ.get("BAOTA_API_KEY", "hsAWqFSi0GOCrunhmYdkxy92tBXfqYjd"),
+ "pm2_name": os.environ.get("DEPLOY_PM2_APP", "soul"),
+ "project_path": os.environ.get("DEPLOY_PROJECT_PATH", "/www/wwwroot/soul"),
+ "site_url": os.environ.get("DEPLOY_SITE_URL", "https://soul.quwanzhi.com"),
+}
+
+
+def _get_sign(api_key):
+ """宝塔鉴权签名:request_token = md5(request_time + md5(api_key))"""
+ now_time = int(time.time())
+ sign_str = str(now_time) + hashlib.md5(api_key.encode("utf-8")).hexdigest()
+ request_token = hashlib.md5(sign_str.encode("utf-8")).hexdigest()
+ return now_time, request_token
+
+
+def _request(base_url, path, data=None, timeout=30):
+ """发起宝塔 API 请求"""
+ url = base_url.rstrip("/") + "/" + path.lstrip("/")
+ api_key = CFG["api_key"]
+ if not api_key:
+ print("请设置 BAOTA_API_KEY(宝塔面板 → 设置 → API 接口)")
+ return None
+ req_time, req_token = _get_sign(api_key)
+ payload = {
+ "request_time": req_time,
+ "request_token": req_token,
+ }
+ if data:
+ payload.update(data)
+ try:
+ r = requests.post(
+ url,
+ data=payload,
+ verify=False,
+ timeout=timeout,
+ )
+ return r.json() if r.text else None
+ except Exception as e:
+ print("请求失败:", e)
+ return None
+
+
+def restart_node_project(panel_url, api_key, pm2_name):
+ """
+ 通过宝塔 API 重启 Node 项目
+ 返回 True 表示成功,False 表示失败
+ """
+ # Node 项目管理为插件接口,路径可能因版本不同
+ paths_to_try = [
+ "/plugin?action=a&name=nodejs&s=restart_project",
+ "/project/nodejs/restart_project",
+ ]
+ payload = {"project_name": pm2_name}
+ req_time, req_token = _get_sign(api_key)
+ payload["request_time"] = req_time
+ payload["request_token"] = req_token
+
+ url_base = panel_url.rstrip("/")
+ for path in paths_to_try:
+ url = url_base + path
+ try:
+ r = requests.post(url, data=payload, verify=False, timeout=30)
+ j = r.json() if r.text else {}
+ if j.get("status") is True or j.get("msg") or r.status_code == 200:
+ print(" 重启成功: %s" % pm2_name)
+ return True
+ # 某些版本返回不同结构
+ if "msg" in j:
+ print(" API 返回:", j.get("msg", j))
+ except Exception as e:
+ print(" 尝试 %s 失败: %s" % (path, e))
+ print(" 重启失败,请检查宝塔 Node 插件是否安装、API 密钥是否正确")
+ return False
+
+
+def create_project_dir():
+ """通过宝塔文件接口创建项目目录"""
+ path = "/files?action=CreateDir"
+ data = {"path": CFG["project_path"]}
+ j = _request(CFG["panel_url"], path, data)
+ if j and j.get("status") is True:
+ print(" 目录已创建: %s" % CFG["project_path"])
+ return True
+ print(" 创建目录失败:", j)
+ return False
+
+
+def trigger_crontab_task(task_id):
+ """触发计划任务"""
+ path = "/crontab?action=StartTask"
+ data = {"id": str(task_id)}
+ j = _request(CFG["panel_url"], path, data)
+ if j and j.get("status") is True:
+ print(" 计划任务 %s 已触发" % task_id)
+ return True
+ print(" 触发失败:", j)
+ return False
+
+
+def main():
+ import argparse
+ parser = argparse.ArgumentParser(description="宝塔 API - 重启 Node / 触发计划任务")
+ parser.add_argument("--create-dir", action="store_true", help="创建项目目录")
+ parser.add_argument("--task-id", type=int, default=0, help="触发计划任务 ID")
+ args = parser.parse_args()
+
+ if args.create_dir:
+ create_project_dir()
+ if args.task_id:
+ ok = trigger_crontab_task(args.task_id)
+ sys.exit(0 if ok else 1)
+ ok = restart_node_project(
+ CFG["panel_url"],
+ CFG["api_key"],
+ CFG["pm2_name"],
+ )
+ sys.exit(0 if ok else 1)
+
+
+if __name__ == "__main__":
+ main()