diff --git a/README-en.md b/README-en.md index 915d3da..540d6bc 100644 --- a/README-en.md +++ b/README-en.md @@ -141,6 +141,20 @@ The project uses admin username/password login. Before using in production: - Production command: `wrangler secret put AUTH_SECRET` - Default admin credentials: `admin / admin123456` — **change your password immediately after first login.** +### Forgot Your Password? + +Reset your password to `admin123456` via the D1 Console in Cloudflare Dashboard: + +1. Go to [dash.cloudflare.com](https://dash.cloudflare.com) → **Storage & Databases** → **D1** → click `edgekey-db` +2. Open the **Console** tab +3. Run the following SQL: + +```sql +UPDATE Admin SET passwordHash = '$2b$10$viMe8RgcpM30gmmF9OpOcuA/QgleSIUk5VRtqjOulfSIbgK5jQCI6' WHERE username = 'admin'; +``` + +4. Log in and change your password immediately. + ## Local Development Bun is recommended (npm/pnpm/yarn also work). diff --git a/README.md b/README.md index 8dcdaab..e62d9d0 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,20 @@ sed -i 's/"database_name": "edgekey-db"/"database_name": "edgekey-db", "database - 生产环境配置AUTH_SECRET命令 `wrangler secret put AUTH_SECRET` - 默认管理员账号为 `admin / admin123456`,首次登录后请立即修改密码 +### 忘记密码? + +在 Cloudflare Dashboard 中通过 D1 Console 将密码重置为 `admin123456`: + +1. 进入 [dash.cloudflare.com](https://dash.cloudflare.com) → **Storage & Databases** → **D1** → 点击 `edgekey-db` +2. 顶部 tab → **Console** +3. 执行以下 SQL: + +```sql +UPDATE Admin SET passwordHash = '$2b$10$viMe8RgcpM30gmmF9OpOcuA/QgleSIUk5VRtqjOulfSIbgK5jQCI6' WHERE username = 'admin'; +``` + +4. 登录后台后立即修改密码 + ## 本地开发 推荐使用 Bun(也可替换为 npm/pnpm/yarn)。 diff --git a/bun.lock b/bun.lock index 340c868..36c77ac 100644 --- a/bun.lock +++ b/bun.lock @@ -17,6 +17,7 @@ "@tiptap/starter-kit": "^3.22.3", "@tiptap/vue-3": "^3.22.3", "@universal-middleware/core": "^0.4.17", + "bcryptjs": "^3.0.3", "hono": "^4.12.8", "pinyin-pro": "^3.28.1", "telefunc": "^0.2.19", @@ -614,6 +615,8 @@ "baseline-browser-mapping": ["baseline-browser-mapping@2.10.13", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw=="], + "bcryptjs": ["bcryptjs@3.0.3", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g=="], + "better-result": ["better-result@2.7.0", "", { "dependencies": { "@clack/prompts": "^0.11.0" }, "bin": { "better-result": "bin/cli.mjs" } }, "sha512-7zrmXjAK8u8Z6SOe4R65XObOR5X+Y2I/VVku3t5cPOGQ8/WsBcfFmfnIPiEl5EBMDOzPHRwbiPbMtQBKYdw7RA=="], "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], diff --git a/modules/auth/crypto.ts b/modules/auth/crypto.ts index 8f78f68..6c22431 100644 --- a/modules/auth/crypto.ts +++ b/modules/auth/crypto.ts @@ -1,5 +1,15 @@ import { createHash } from "node:crypto"; +import bcrypt from "bcryptjs"; -export function hashAdminPassword(password: string) { - return createHash("sha256").update(password).digest("hex"); +export async function hashAdminPassword(password: string) { + return bcrypt.hash(password, 10); } + +export async function verifyAdminPassword(password: string, hash: string) { + // 兼容旧 SHA-256 哈希(不以 $2b$ 开头) + if (!hash.startsWith("$2b$") && !hash.startsWith("$2a$")) { + const sha256 = createHash("sha256").update(password).digest("hex"); + return sha256 === hash; + } + return bcrypt.compare(password, hash); +} \ No newline at end of file diff --git a/modules/auth/service.ts b/modules/auth/service.ts index 0739c9e..e71ebec 100644 --- a/modules/auth/service.ts +++ b/modules/auth/service.ts @@ -1,7 +1,7 @@ import { getContext } from "telefunc"; import type { PrismaClient } from "../../generated/prisma/client"; import { badRequestError, notFoundError, unauthorizedError } from "../../lib/app-error"; -import { hashAdminPassword } from "./crypto"; +import { hashAdminPassword, verifyAdminPassword } from "./crypto"; export function assertAdminAccess() { const context = getContext<{ session?: { user?: { role?: string } } }>(); @@ -80,13 +80,14 @@ export async function updateAdminProfile(input: { throw badRequestError("新密码长度不能少于 8 位", "PASSWORD_TOO_SHORT"); } - const currentHash = hashAdminPassword(currentPassword); - if (currentHash !== admin.passwordHash) { + const currentValid = await verifyAdminPassword(currentPassword, admin.passwordHash); + if (!currentValid) { throw badRequestError("当前密码不正确", "CURRENT_PASSWORD_INVALID"); } - const newHash = hashAdminPassword(newPassword); - if (newHash === admin.passwordHash) { + const newHash = await hashAdminPassword(newPassword); + const newSameAsOld = await verifyAdminPassword(newPassword, admin.passwordHash); + if (newSameAsOld) { throw badRequestError("新密码不能与当前密码相同", "PASSWORD_UNCHANGED"); } diff --git a/package.json b/package.json index 574eb7d..7bc36a2 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "1.1.1", + "version": "1.2.1", "scripts": { "dev": "vike dev", "build": "bun run db:generate && vike build", @@ -31,6 +31,7 @@ "@tiptap/starter-kit": "^3.22.3", "@tiptap/vue-3": "^3.22.3", "@universal-middleware/core": "^0.4.17", + "bcryptjs": "^3.0.3", "hono": "^4.12.8", "pinyin-pro": "^3.28.1", "telefunc": "^0.2.19", diff --git a/pages/admin/login/+Page.vue b/pages/admin/login/+Page.vue index 5228090..3f12f95 100644 --- a/pages/admin/login/+Page.vue +++ b/pages/admin/login/+Page.vue @@ -40,12 +40,15 @@ const csrfToken = ref(""); const loading = ref(true); const errorMsg = ref(""); -const ERROR_MAP: Record = {CredentialsSignin: "用户名或密码错误", +const ERROR_MAP: Record = { + CredentialsSignin: "用户名或密码错误", + password_upgrade_failed: "登录成功但密码升级失败,请重置密码后重试", }; onMounted(async () => { - const error = new URLSearchParams(location.search).get("error"); - if (error) errorMsg.value = ERROR_MAP[error] ?? "登录失败,请重试"; + const params = new URLSearchParams(location.search); + const code = params.get("code") ?? params.get("error"); + if (code) errorMsg.value = ERROR_MAP[code] ?? "登录失败,请重试"; try { const response = await fetch("/api/auth/csrf", { credentials: "same-origin", diff --git a/scripts/seed.sql b/scripts/seed.sql index e7bd3ce..011a0d7 100644 --- a/scripts/seed.sql +++ b/scripts/seed.sql @@ -5,7 +5,7 @@ -- 管理员账号 INSERT INTO "Admin" ("username", "passwordHash", "nickname", "status", "updatedAt") -VALUES ('admin', 'ac0e7d037817094e9e0b4441f9bae3209d67b02fa484917065f71b16109a1a78', '管理员', 'ACTIVE', CURRENT_TIMESTAMP) +VALUES ('admin', '$2b$10$viMe8RgcpM30gmmF9OpOcuA/QgleSIUk5VRtqjOulfSIbgK5jQCI6', '管理员', 'ACTIVE', CURRENT_TIMESTAMP) ON CONFLICT("username") DO NOTHING; -- 站点设置 diff --git a/server/authjs-handler.ts b/server/authjs-handler.ts index 37b5136..0b7dfea 100644 --- a/server/authjs-handler.ts +++ b/server/authjs-handler.ts @@ -1,11 +1,12 @@ import { Auth, type AuthConfig, createActionURL, setEnvDefaults } from "@auth/core"; +import { CredentialsSignin } from "@auth/core/errors"; import CredentialsProvider from "@auth/core/providers/credentials"; import type { Session } from "@auth/core/types"; import { enhance, type UniversalHandler, type UniversalMiddleware } from "@universal-middleware/core"; import { PrismaClient } from "../generated/prisma/client"; import { internalServerError, rateLimitError } from "../lib/app-error"; import { logger } from "../lib/logger"; -import { hashAdminPassword } from "../modules/auth/crypto"; +import { verifyAdminPassword, hashAdminPassword } from "../modules/auth/crypto"; const ADMIN_ROLE = "admin" as const; const loginAttemptStore = new Map(); @@ -75,11 +76,24 @@ async function findAdminByCredentials(prisma: PrismaClient, username: string, pa return null; } - const passwordHash = hashAdminPassword(password); - if (admin.passwordHash !== passwordHash) { + const valid = await verifyAdminPassword(password, admin.passwordHash); + if (!valid) { return null; } + // 旧 SHA-256 哈希自动升级为 bcrypt + if (!admin.passwordHash.startsWith("$2b$") && !admin.passwordHash.startsWith("$2a$")) { + try { + const newHash = await hashAdminPassword(password); + await prisma.admin.update({ where: { username }, data: { passwordHash: newHash } }); + } catch (e) { + logger.error("auth.password_upgrade.failed", { error: e }); + const err = new CredentialsSignin("哈希字符串升级失败,请参考官网文档重置管理员密码"); + err.code = "password_upgrade_failed"; + throw err; + } + } + return { id: String(admin.id), name: admin.nickname || admin.username,