Browse Source
- Updated the users table migration to include a password field. - Enhanced UserModel with methods to find users by username and email. - Implemented user registration and login functionalities in UserService. - Added JWT authentication middleware with support for whitelisting and blacklisting routes. - Created a response time middleware for logging request durations. - Replaced the previous Send and ResponseTime plugins with new middleware implementations. - Introduced bcrypt utility functions for password hashing and comparison. - Added a base singleton utility class for managing single instances of classes. - Created a helper function for formatting API responses. - Set up a Dockerfile for containerizing the application with health checks and entrypoint script. - Added a basic HTML login and registration interface.alpha
35 changed files with 821 additions and 459 deletions
@ -0,0 +1,29 @@ |
|||||
|
# 使用官方 Bun 运行时的轻量级镜像 |
||||
|
FROM oven/bun:alpine as base |
||||
|
|
||||
|
WORKDIR /app |
||||
|
|
||||
|
# 仅复制生产依赖相关文件 |
||||
|
COPY package.json bun.lockb knexfile.mjs .npmrc ./ |
||||
|
|
||||
|
# 安装依赖(生产环境) |
||||
|
RUN bun install --production |
||||
|
|
||||
|
# 复制应用代码和静态资源 |
||||
|
COPY src ./src |
||||
|
COPY public ./public |
||||
|
|
||||
|
COPY entrypoint.sh ./entrypoint.sh |
||||
|
RUN chmod +x ./entrypoint.sh |
||||
|
|
||||
|
# 如需数据库文件(如 SQLite),可挂载到宿主机 |
||||
|
VOLUME /app/database |
||||
|
|
||||
|
# 启动命令(如有端口需求可暴露端口) |
||||
|
EXPOSE 3000 |
||||
|
|
||||
|
# 健康检查:每30秒检查一次服务端口,3次失败则容器为unhealthy |
||||
|
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ |
||||
|
CMD wget --spider -q http://localhost:3000/ || exit 1 |
||||
|
|
||||
|
ENTRYPOINT ["./entrypoint.sh"] |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,18 @@ |
|||||
|
#!/bin/sh |
||||
|
set -e |
||||
|
|
||||
|
# 数据库文件路径(可根据实际环境调整) |
||||
|
DB_FILE=./database/db.sqlite3 |
||||
|
|
||||
|
# 如果数据库文件不存在,先 migrate 再 seed |
||||
|
if [ ! -f "$DB_FILE" ]; then |
||||
|
echo "Database not found, running migration and seed..." |
||||
|
bun run npx knex migrate:latest |
||||
|
bun run npx knex seed:run |
||||
|
else |
||||
|
echo "Database exists, running migration only..." |
||||
|
bun run npx knex migrate:latest |
||||
|
fi |
||||
|
|
||||
|
# 启动主服务 |
||||
|
exec bun src/main.js |
@ -1 +0,0 @@ |
|||||
sada |
|
@ -0,0 +1,101 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="zh-cn"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
<title>登录 / 注册</title> |
||||
|
<style> |
||||
|
body { background: #f5f6fa; font-family: 'Segoe UI', Arial, sans-serif; } |
||||
|
.container { max-width: 350px; margin: 60px auto; background: #fff; border-radius: 8px; box-shadow: 0 2px 12px #0001; padding: 32px 28px; } |
||||
|
h2 { text-align: center; margin-bottom: 24px; color: #333; } |
||||
|
.tabs { display: flex; margin-bottom: 24px; } |
||||
|
.tab { flex: 1; text-align: center; padding: 10px 0; cursor: pointer; border-bottom: 2px solid #eee; color: #888; font-weight: 500; } |
||||
|
.tab.active { color: #1976d2; border-bottom: 2px solid #1976d2; } |
||||
|
.form-group { margin-bottom: 18px; } |
||||
|
label { display: block; margin-bottom: 6px; color: #555; } |
||||
|
input { width: 100%; padding: 8px 10px; border: 1px solid #ccc; border-radius: 4px; font-size: 15px; } |
||||
|
button { width: 100%; padding: 10px; background: #1976d2; color: #fff; border: none; border-radius: 4px; font-size: 16px; cursor: pointer; margin-top: 8px; } |
||||
|
button:active { background: #145ea8; } |
||||
|
.msg { text-align: center; color: #e53935; margin-bottom: 10px; min-height: 22px; } |
||||
|
</style> |
||||
|
</head> |
||||
|
<body> |
||||
|
<div class="container"> |
||||
|
<div class="tabs"> |
||||
|
<div class="tab active" id="loginTab">登录</div> |
||||
|
<div class="tab" id="registerTab">注册</div> |
||||
|
</div> |
||||
|
<div class="msg" id="msg"></div> |
||||
|
<form id="loginForm"> |
||||
|
<div class="form-group"> |
||||
|
<label for="login-username">用户名</label> |
||||
|
<input type="text" id="login-username" required autocomplete="username"> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label for="login-password">密码</label> |
||||
|
<input type="password" id="login-password" required autocomplete="current-password"> |
||||
|
</div> |
||||
|
<button type="submit">登录</button> |
||||
|
</form> |
||||
|
<form id="registerForm" style="display:none;"> |
||||
|
<div class="form-group"> |
||||
|
<label for="register-username">用户名</label> |
||||
|
<input type="text" id="register-username" required autocomplete="username"> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label for="register-email">邮箱</label> |
||||
|
<input type="email" id="register-email" required autocomplete="email"> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label for="register-password">密码</label> |
||||
|
<input type="password" id="register-password" required autocomplete="new-password"> |
||||
|
</div> |
||||
|
<button type="submit">注册</button> |
||||
|
</form> |
||||
|
</div> |
||||
|
<script> |
||||
|
const loginTab = document.getElementById('loginTab'); |
||||
|
const registerTab = document.getElementById('registerTab'); |
||||
|
const loginForm = document.getElementById('loginForm'); |
||||
|
const registerForm = document.getElementById('registerForm'); |
||||
|
const msg = document.getElementById('msg'); |
||||
|
|
||||
|
loginTab.onclick = () => { |
||||
|
loginTab.classList.add('active'); |
||||
|
registerTab.classList.remove('active'); |
||||
|
loginForm.style.display = ''; |
||||
|
registerForm.style.display = 'none'; |
||||
|
msg.textContent = ''; |
||||
|
}; |
||||
|
registerTab.onclick = () => { |
||||
|
registerTab.classList.add('active'); |
||||
|
loginTab.classList.remove('active'); |
||||
|
registerForm.style.display = ''; |
||||
|
loginForm.style.display = 'none'; |
||||
|
msg.textContent = ''; |
||||
|
}; |
||||
|
|
||||
|
loginForm.onsubmit = async e => { |
||||
|
e.preventDefault(); |
||||
|
msg.textContent = ''; |
||||
|
const username = document.getElementById('login-username').value.trim(); |
||||
|
const password = document.getElementById('login-password').value; |
||||
|
if (!username || !password) { msg.textContent = '请填写完整信息'; return; } |
||||
|
// TODO: 替换为实际API |
||||
|
msg.style.color = '#1976d2'; |
||||
|
msg.textContent = '登录成功(示例)'; |
||||
|
}; |
||||
|
registerForm.onsubmit = async e => { |
||||
|
e.preventDefault(); |
||||
|
msg.textContent = ''; |
||||
|
const username = document.getElementById('register-username').value.trim(); |
||||
|
const email = document.getElementById('register-email').value.trim(); |
||||
|
const password = document.getElementById('register-password').value; |
||||
|
if (!username || !email || !password) { msg.textContent = '请填写完整信息'; return; } |
||||
|
// TODO: 替换为实际API |
||||
|
msg.style.color = '#1976d2'; |
||||
|
msg.textContent = '注册成功(示例)'; |
||||
|
}; |
||||
|
</script> |
||||
|
</body> |
||||
|
</html> |
@ -1,41 +1,56 @@ |
|||||
// Job Controller 示例:如何调用 service 层动态控制和查询定时任务
|
// Job Controller 示例:如何调用 service 层动态控制和查询定时任务
|
||||
import JobService from "services/JobService.js" |
import JobService from "services/JobService.js" |
||||
import Router from "utils/router.js" |
import { formatResponse } from "utils/helper.js" |
||||
|
|
||||
class JobController { |
const jobService = new JobService() |
||||
static routes() { |
|
||||
const router = new Router({ prefix: "/api/jobs" }) |
|
||||
router.get("/", JobController.list) |
|
||||
router.post("/start/:id", JobController.start) |
|
||||
router.post("/stop/:id", JobController.stop) |
|
||||
router.post("/update/:id", JobController.updateCron) |
|
||||
return router |
|
||||
} |
|
||||
|
|
||||
static async list(ctx) { |
export const list = async (ctx) => { |
||||
ctx.body = JobService.listJobs() |
try { |
||||
|
const data = jobService.listJobs() |
||||
|
ctx.body = formatResponse(true, data) |
||||
|
} catch (err) { |
||||
|
ctx.body = formatResponse(false, null, err.message || "获取任务列表失败") |
||||
} |
} |
||||
|
} |
||||
|
|
||||
static async start(ctx) { |
export const start = async (ctx) => { |
||||
const { id } = ctx.params |
const { id } = ctx.params |
||||
JobService.startJob(id) |
try { |
||||
ctx.body = { success: true, message: `${id} 任务已启动` } |
jobService.startJob(id) |
||||
|
ctx.body = formatResponse(true, null, null, `${id} 任务已启动`) |
||||
|
} catch (err) { |
||||
|
ctx.body = formatResponse(false, null, err.message || "启动任务失败") |
||||
} |
} |
||||
|
} |
||||
|
|
||||
static async stop(ctx) { |
export const stop = async (ctx) => { |
||||
const { id } = ctx.params |
const { id } = ctx.params |
||||
JobService.stopJob(id) |
try { |
||||
ctx.body = { success: true, message: `${id} 任务已停止` } |
jobService.stopJob(id) |
||||
|
ctx.body = formatResponse(true, null, null, `${id} 任务已停止`) |
||||
|
} catch (err) { |
||||
|
ctx.body = formatResponse(false, null, err.message || "停止任务失败") |
||||
} |
} |
||||
|
} |
||||
|
|
||||
static async updateCron(ctx) { |
export const updateCron = async (ctx) => { |
||||
const { id } = ctx.params |
const { id } = ctx.params |
||||
const { cronTime } = ctx.request.body |
const { cronTime } = ctx.request.body |
||||
JobService.updateJobCron(id, cronTime) |
try { |
||||
ctx.body = { success: true, message: `${id} 任务频率已修改` } |
jobService.updateJobCron(id, cronTime) |
||||
|
ctx.body = formatResponse(true, null, null, `${id} 任务频率已修改`) |
||||
|
} catch (err) { |
||||
|
ctx.body = formatResponse(false, null, err.message || "修改任务频率失败") |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
export default JobController |
// 路由注册示例
|
||||
|
import Router from "utils/router.js" |
||||
// 你可以在路由中引入这些 controller 方法,实现接口调用
|
export function createRoutes() { |
||||
|
const router = new Router({ prefix: "/api/jobs" }) |
||||
|
router.get("/", list) |
||||
|
router.post("/start/:id", start) |
||||
|
router.post("/stop/:id", stop) |
||||
|
router.post("/update/:id", updateCron) |
||||
|
return router |
||||
|
} |
||||
|
@ -1,22 +1,16 @@ |
|||||
import Router from 'utils/router.js'; |
import { formatResponse } from "utils/helper.js" |
||||
|
|
||||
class StatusController { |
export const status = async (ctx) => { |
||||
static routes() { |
ctx.body = "OK" |
||||
const v1 = new Router({ prefix: "/api/v1" }) |
|
||||
|
|
||||
// 组内中间件
|
|
||||
v1.use((ctx, next) => { |
|
||||
ctx.set("X-API-Version", "v1") |
|
||||
return next() |
|
||||
}) |
|
||||
|
|
||||
v1.get("/status", StatusController.status) |
|
||||
return v1 |
|
||||
} |
|
||||
|
|
||||
static async status(ctx) { |
|
||||
ctx.body = "OK" |
|
||||
} |
|
||||
} |
} |
||||
|
|
||||
export default StatusController |
import Router from "utils/router.js" |
||||
|
export function createRoutes() { |
||||
|
const v1 = new Router({ prefix: "/api/v1" }) |
||||
|
v1.use((ctx, next) => { |
||||
|
ctx.set("X-API-Version", "v1") |
||||
|
return next() |
||||
|
}) |
||||
|
v1.get("/status", status) |
||||
|
return v1 |
||||
|
} |
||||
|
@ -1,23 +1,44 @@ |
|||||
import UserService from 'services/UserService.js'; |
import UserService from "services/UserService.js" |
||||
import Router from 'utils/router.js'; |
import { formatResponse } from "utils/helper.js" |
||||
|
|
||||
class UserController { |
const userService = new UserService() |
||||
static routes() { |
|
||||
let router = new Router({ prefix: '/api' }); |
|
||||
router.get('/hello', UserController.hello); |
|
||||
router.get('/user/:id', UserController.getUser); |
|
||||
return router; |
|
||||
} |
|
||||
|
|
||||
static async hello(ctx) { |
export const hello = async (ctx) => { |
||||
ctx.body = 'Hello World'; |
ctx.body = formatResponse(true, "Hello World") |
||||
} |
} |
||||
|
|
||||
|
export const getUser = async (ctx) => { |
||||
|
const user = await userService.getUserById(ctx.params.id) |
||||
|
ctx.body = formatResponse(true, user) |
||||
|
} |
||||
|
|
||||
static async getUser(ctx) { |
export const register = async (ctx) => { |
||||
// 调用 service 层获取用户
|
try { |
||||
const user = await UserService.getUserById(ctx.params.id); |
const { username, email, password } = ctx.request.body |
||||
ctx.body = user; |
const user = await userService.register({ username, email, password }) |
||||
} |
ctx.body = formatResponse(true, user) |
||||
|
} catch (err) { |
||||
|
ctx.body = formatResponse(false, null, err.message) |
||||
|
} |
||||
} |
} |
||||
|
|
||||
export default UserController; |
export const login = async (ctx) => { |
||||
|
try { |
||||
|
const { username, email, password } = ctx.request.body |
||||
|
const result = await userService.login({ username, email, password }) |
||||
|
ctx.body = formatResponse(true, result) |
||||
|
} catch (err) { |
||||
|
ctx.body = formatResponse(false, null, err.message) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 路由注册示例
|
||||
|
import Router from "utils/router.js" |
||||
|
export function createRoutes() { |
||||
|
const router = new Router({ prefix: "/api" }) |
||||
|
router.get("/hello", hello) |
||||
|
router.get("/user/:id", getUser) |
||||
|
router.post("/register", register) |
||||
|
router.post("/login", login) |
||||
|
return router |
||||
|
} |
||||
|
@ -0,0 +1,68 @@ |
|||||
|
// JWT 鉴权中间件,支持白名单和黑名单,白名单/黑名单支持glob语法,白名单可指定是否校验权限(auth: true/false/"try")
|
||||
|
import jwt from "./jwt" |
||||
|
import { minimatch } from "minimatch" |
||||
|
|
||||
|
export const JWT_SECRET = process.env.JWT_SECRET || "jwt-demo-secret" |
||||
|
|
||||
|
function matchList(list, path) { |
||||
|
for (const item of list) { |
||||
|
if (typeof item === "string" && minimatch(path, item)) { |
||||
|
return { matched: true, auth: false } |
||||
|
} |
||||
|
if (typeof item === "object" && minimatch(path, item.pattern)) { |
||||
|
return { matched: true, auth: item.auth } |
||||
|
} |
||||
|
} |
||||
|
return { matched: false } |
||||
|
} |
||||
|
|
||||
|
function verifyToken(ctx) { |
||||
|
const token = ctx.headers["authorization"]?.replace(/^Bearer\s/, "") |
||||
|
if (!token) return { ok: false } |
||||
|
try { |
||||
|
ctx.state.user = jwt.verify(token, JWT_SECRET) |
||||
|
return { ok: true } |
||||
|
} catch { |
||||
|
ctx.state.user = undefined |
||||
|
return { ok: false } |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default function authMiddleware(options = { |
||||
|
whiteList: [], |
||||
|
blackList: [] |
||||
|
}) { |
||||
|
return async (ctx, next) => { |
||||
|
// 黑名单优先生效
|
||||
|
if (matchList(options.blackList, ctx.path).matched) { |
||||
|
ctx.status = 403 |
||||
|
ctx.body = { success: false, error: "禁止访问" } |
||||
|
return |
||||
|
} |
||||
|
// 白名单处理
|
||||
|
const white = matchList(options.whiteList, ctx.path) |
||||
|
if (white.matched) { |
||||
|
if (white.auth === false) { |
||||
|
return await next() |
||||
|
} |
||||
|
if (white.auth === "try") { |
||||
|
verifyToken(ctx) // token可选,校验失败不报错
|
||||
|
return await next() |
||||
|
} |
||||
|
// true 或其他情况,必须有token
|
||||
|
if (!verifyToken(ctx).ok) { |
||||
|
ctx.status = 401 |
||||
|
ctx.body = { success: false, error: "未登录或token缺失或无效" } |
||||
|
return |
||||
|
} |
||||
|
return await next() |
||||
|
} |
||||
|
// 非白名单,必须有token
|
||||
|
if (!verifyToken(ctx).ok) { |
||||
|
ctx.status = 401 |
||||
|
ctx.body = { success: false, error: "未登录或token缺失或无效" } |
||||
|
return |
||||
|
} |
||||
|
await next() |
||||
|
} |
||||
|
} |
@ -0,0 +1,3 @@ |
|||||
|
// 统一导出所有中间件
|
||||
|
import auth from "./auth.js" |
||||
|
export { auth } |
@ -0,0 +1,3 @@ |
|||||
|
// 兼容性导出,便于后续扩展
|
||||
|
import jwt from "jsonwebtoken" |
||||
|
export default jwt |
@ -0,0 +1,33 @@ |
|||||
|
import ResponseTime from "./ResponseTime" |
||||
|
import Send from "./Send" |
||||
|
import { resolve } from "path" |
||||
|
import { fileURLToPath } from "url" |
||||
|
import path from "path" |
||||
|
import errorHandler from "./errorHandler" |
||||
|
import { auth } from "./Auth" |
||||
|
|
||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url)) |
||||
|
const publicPath = resolve(__dirname, "../../public") |
||||
|
|
||||
|
export default app => { |
||||
|
app.use(errorHandler()) |
||||
|
app.use(ResponseTime) |
||||
|
app.use( |
||||
|
auth({ |
||||
|
whiteList: [ |
||||
|
{ pattern: "/", auth: "try" }, |
||||
|
"/api/login", |
||||
|
"/api/register" |
||||
|
], |
||||
|
blackList: [], |
||||
|
}) |
||||
|
) |
||||
|
app.use(async (ctx, next) => { |
||||
|
try { |
||||
|
await Send(ctx, ctx.path, { root: publicPath }) |
||||
|
} catch (err) { |
||||
|
if (err.status !== 404) throw err |
||||
|
} |
||||
|
await next() |
||||
|
}) |
||||
|
} |
@ -1,23 +0,0 @@ |
|||||
import ResponseTime from "./ResponseTime"; |
|
||||
import Send from "./Send"; |
|
||||
import { resolve } from 'path'; |
|
||||
import { fileURLToPath } from 'url'; |
|
||||
import path from "path"; |
|
||||
import errorHandler from './errorHandler'; |
|
||||
|
|
||||
|
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
|
||||
const publicPath = resolve(__dirname, '../../public'); |
|
||||
|
|
||||
export default (app)=>{ |
|
||||
app.use(errorHandler()); |
|
||||
app.use(ResponseTime) |
|
||||
app.use(async (ctx, next) => { |
|
||||
try { |
|
||||
await Send(ctx, ctx.path, { root: publicPath }); |
|
||||
} catch (err) { |
|
||||
if (err.status !== 404) throw err; |
|
||||
} |
|
||||
await next(); |
|
||||
}) |
|
||||
} |
|
@ -1,18 +1,18 @@ |
|||||
import jobs from '../jobs'; |
import jobs from "../jobs" |
||||
|
|
||||
class JobService { |
class JobService { |
||||
static startJob(id) { |
startJob(id) { |
||||
return jobs.start(id); |
return jobs.start(id) |
||||
} |
} |
||||
static stopJob(id) { |
stopJob(id) { |
||||
return jobs.stop(id); |
return jobs.stop(id) |
||||
} |
} |
||||
static updateJobCron(id, cronTime) { |
updateJobCron(id, cronTime) { |
||||
return jobs.updateCronTime(id, cronTime); |
return jobs.updateCronTime(id, cronTime) |
||||
} |
} |
||||
static listJobs() { |
listJobs() { |
||||
return jobs.list(); |
return jobs.list() |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
export default JobService; |
export default JobService |
||||
|
@ -1,39 +1,74 @@ |
|||||
// src/services/userService.js
|
import UserModel from "db/models/UserModel.js" |
||||
// 用户相关业务逻辑
|
import { hashPassword, comparePassword } from "utils/bcrypt.js" |
||||
|
import { JWT_SECRET } from "@/middlewares/Auth/auth.js" |
||||
import UserModel from 'db/models/UserModel.js'; |
import jwt from "@/middlewares/Auth/jwt.js" |
||||
|
|
||||
class UserService { |
class UserService { |
||||
static async getUserById(id) { |
async getUserById(id) { |
||||
// 这里可以调用数据库模型
|
// 这里可以调用数据库模型
|
||||
// 示例返回
|
// 示例返回
|
||||
return { id, name: `User_${id}` }; |
return { id, name: `User_${id}` } |
||||
} |
} |
||||
|
|
||||
// 获取所有用户
|
// 获取所有用户
|
||||
static async getAllUsers() { |
async getAllUsers() { |
||||
return await UserModel.findAll(); |
return await UserModel.findAll() |
||||
} |
} |
||||
|
|
||||
// 创建新用户
|
// 创建新用户
|
||||
static async createUser(data) { |
async createUser(data) { |
||||
if (!data.name) throw new Error('用户名不能为空'); |
if (!data.name) throw new Error("用户名不能为空") |
||||
return await UserModel.create(data); |
return await UserModel.create(data) |
||||
} |
} |
||||
|
|
||||
// 更新用户
|
// 更新用户
|
||||
static async updateUser(id, data) { |
async updateUser(id, data) { |
||||
const user = await UserModel.findById(id); |
const user = await UserModel.findById(id) |
||||
if (!user) throw new Error('用户不存在'); |
if (!user) throw new Error("用户不存在") |
||||
return await UserModel.update(id, data); |
return await UserModel.update(id, data) |
||||
} |
} |
||||
|
|
||||
// 删除用户
|
// 删除用户
|
||||
static async deleteUser(id) { |
async deleteUser(id) { |
||||
const user = await UserModel.findById(id); |
const user = await UserModel.findById(id) |
||||
if (!user) throw new Error('用户不存在'); |
if (!user) throw new Error("用户不存在") |
||||
return await UserModel.delete(id); |
return await UserModel.delete(id) |
||||
} |
} |
||||
|
|
||||
|
// 注册新用户
|
||||
|
async register(data) { |
||||
|
if (!data.username || !data.email || !data.password) throw new Error("用户名、邮箱和密码不能为空") |
||||
|
const existUser = await UserModel.findByUsername(data.username) |
||||
|
if (existUser) throw new Error("用户名已存在") |
||||
|
const existEmail = await UserModel.findByEmail(data.email) |
||||
|
if (existEmail) throw new Error("邮箱已被注册") |
||||
|
// 密码加密
|
||||
|
const hashed = await hashPassword(data.password) |
||||
|
|
||||
|
const user = await UserModel.create({ ...data, password: hashed }) |
||||
|
// 返回脱敏信息
|
||||
|
const { password, ...userInfo } = Array.isArray(user) ? user[0] : user |
||||
|
return userInfo |
||||
|
} |
||||
|
|
||||
|
// 登录
|
||||
|
async login({ username, email, password }) { |
||||
|
let user |
||||
|
if (username) { |
||||
|
user = await UserModel.findByUsername(username) |
||||
|
} else if (email) { |
||||
|
user = await UserModel.findByEmail(email) |
||||
|
} |
||||
|
if (!user) throw new Error("用户不存在") |
||||
|
// 校验密码
|
||||
|
const ok = await comparePassword(password, user.password) |
||||
|
if (!ok) throw new Error("密码错误") |
||||
|
// 生成token
|
||||
|
const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: "2h" }) |
||||
|
// 返回token和用户信息
|
||||
|
const { password: pwd, ...userInfo } = user |
||||
|
return { token, user: userInfo } |
||||
|
} |
||||
} |
} |
||||
|
|
||||
export default UserService; |
export default UserService |
||||
|
@ -0,0 +1,37 @@ |
|||||
|
// 抽象基类,使用泛型来正确推导子类类型
|
||||
|
class BaseSingleton { |
||||
|
static _instance |
||||
|
|
||||
|
constructor() { |
||||
|
if (this.constructor === BaseSingleton) { |
||||
|
throw new Error("禁止直接实例化 BaseOne 抽象类") |
||||
|
} |
||||
|
|
||||
|
if (this.constructor._instance) { |
||||
|
throw new Error("构造函数私有化失败,禁止重复 new") |
||||
|
} |
||||
|
|
||||
|
// this.constructor 是子类,所以这里设为 instance
|
||||
|
this.constructor._instance = this |
||||
|
} |
||||
|
|
||||
|
static getInstance() { |
||||
|
const clazz = this |
||||
|
if (!clazz._instance) { |
||||
|
const self = new this() |
||||
|
const handler = { |
||||
|
get: function (target, prop) { |
||||
|
const value = Reflect.get(target, prop) |
||||
|
if (typeof value === "function") { |
||||
|
return value.bind(target) |
||||
|
} |
||||
|
return Reflect.get(target, prop) |
||||
|
}, |
||||
|
} |
||||
|
clazz._instance = new Proxy(self, handler) |
||||
|
} |
||||
|
return clazz._instance |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export { BaseSingleton } |
@ -0,0 +1,11 @@ |
|||||
|
// 密码加密与校验工具
|
||||
|
import bcrypt from "bcryptjs" |
||||
|
|
||||
|
export async function hashPassword(password) { |
||||
|
const salt = await bcrypt.genSalt(10) |
||||
|
return bcrypt.hash(password, salt) |
||||
|
} |
||||
|
|
||||
|
export async function comparePassword(password, hash) { |
||||
|
return bcrypt.compare(password, hash) |
||||
|
} |
@ -0,0 +1,4 @@ |
|||||
|
|
||||
|
export function formatResponse(success, data = null, error = null) { |
||||
|
return { success, error, data } |
||||
|
} |
Loading…
Reference in new issue