Browse Source

feat: add user authentication and registration features

- 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
谢亚昕 2 months ago
parent
commit
1d142c3900
  1. 29
      Dockerfile
  2. BIN
      bun.lockb
  3. 0
      database/.gitkeep
  4. BIN
      database/development.sqlite3
  5. BIN
      database/development.sqlite3-shm
  6. BIN
      database/development.sqlite3-wal
  7. 18
      entrypoint.sh
  8. 8
      package.json
  9. 1
      public/aa.txt
  10. 101
      public/index.html
  11. 71
      src/controllers/JobController.js
  12. 32
      src/controllers/StatusController.js
  13. 57
      src/controllers/userController.js
  14. 1
      src/db/migrations/20250616065041_create_users_table.mjs
  15. 7
      src/db/models/UserModel.js
  16. 6
      src/main.js
  17. 68
      src/middlewares/Auth/auth.js
  18. 3
      src/middlewares/Auth/index.js
  19. 3
      src/middlewares/Auth/jwt.js
  20. 0
      src/middlewares/ResponseTime/index.js
  21. 0
      src/middlewares/Send/index.js
  22. 0
      src/middlewares/Send/resolve-path.js
  23. 0
      src/middlewares/errorHandler/index.js
  24. 33
      src/middlewares/install.js
  25. 23
      src/plugins/install.js
  26. 28
      src/services/JobService.js
  27. 105
      src/services/userService.js
  28. 37
      src/utils/BaseSingleton.js
  29. 2
      src/utils/autoRegister.js
  30. 11
      src/utils/bcrypt.js
  31. 4
      src/utils/helper.js

29
Dockerfile

@ -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"]

BIN
bun.lockb

Binary file not shown.

0
database/.gitkeep

BIN
database/development.sqlite3

Binary file not shown.

BIN
database/development.sqlite3-shm

Binary file not shown.

BIN
database/development.sqlite3-wal

Binary file not shown.

18
entrypoint.sh

@ -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

8
package.json

@ -12,12 +12,16 @@
},
"devDependencies": {
"@types/bun": "latest",
"@types/node": "^24.0.1",
"knex": "^3.1.0"
"@types/node": "^24.0.1"
},
"dependencies": {
"bcryptjs": "^3.0.2",
"jsonwebtoken": "^9.0.0",
"knex": "^3.1.0",
"koa": "^3.0.0",
"koa-bodyparser": "^4.4.1",
"log4js": "^6.9.1",
"minimatch": "^9.0.0",
"module-alias": "^2.2.3",
"node-cron": "^4.1.0",
"path-to-regexp": "^8.2.0",

1
public/aa.txt

@ -1 +0,0 @@
sada

101
public/index.html

@ -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>

71
src/controllers/JobController.js

@ -1,41 +1,56 @@
// Job Controller 示例:如何调用 service 层动态控制和查询定时任务
import JobService from "services/JobService.js"
import Router from "utils/router.js"
import { formatResponse } from "utils/helper.js"
class JobController {
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
}
const jobService = new JobService()
static async list(ctx) {
ctx.body = JobService.listJobs()
export const list = async (ctx) => {
try {
const data = jobService.listJobs()
ctx.body = formatResponse(true, data)
} catch (err) {
ctx.body = formatResponse(false, null, err.message || "获取任务列表失败")
}
}
static async start(ctx) {
const { id } = ctx.params
JobService.startJob(id)
ctx.body = { success: true, message: `${id} 任务已启动` }
export const start = async (ctx) => {
const { id } = ctx.params
try {
jobService.startJob(id)
ctx.body = formatResponse(true, null, null, `${id} 任务已启动`)
} catch (err) {
ctx.body = formatResponse(false, null, err.message || "启动任务失败")
}
}
static async stop(ctx) {
const { id } = ctx.params
JobService.stopJob(id)
ctx.body = { success: true, message: `${id} 任务已停止` }
export const stop = async (ctx) => {
const { id } = ctx.params
try {
jobService.stopJob(id)
ctx.body = formatResponse(true, null, null, `${id} 任务已停止`)
} catch (err) {
ctx.body = formatResponse(false, null, err.message || "停止任务失败")
}
}
static async updateCron(ctx) {
const { id } = ctx.params
const { cronTime } = ctx.request.body
JobService.updateJobCron(id, cronTime)
ctx.body = { success: true, message: `${id} 任务频率已修改` }
export const updateCron = async (ctx) => {
const { id } = ctx.params
const { cronTime } = ctx.request.body
try {
jobService.updateJobCron(id, cronTime)
ctx.body = formatResponse(true, null, null, `${id} 任务频率已修改`)
} catch (err) {
ctx.body = formatResponse(false, null, err.message || "修改任务频率失败")
}
}
export default JobController
// 你可以在路由中引入这些 controller 方法,实现接口调用
// 路由注册示例
import Router from "utils/router.js"
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
}

32
src/controllers/StatusController.js

@ -1,22 +1,16 @@
import Router from 'utils/router.js';
import { formatResponse } from "utils/helper.js"
class StatusController {
static routes() {
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 const status = async (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
}

57
src/controllers/userController.js

@ -1,23 +1,44 @@
import UserService from 'services/UserService.js';
import Router from 'utils/router.js';
import UserService from "services/UserService.js"
import { formatResponse } from "utils/helper.js"
class UserController {
static routes() {
let router = new Router({ prefix: '/api' });
router.get('/hello', UserController.hello);
router.get('/user/:id', UserController.getUser);
return router;
}
const userService = new UserService()
static async hello(ctx) {
ctx.body = 'Hello World';
}
export const hello = async (ctx) => {
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) {
// 调用 service 层获取用户
const user = await UserService.getUserById(ctx.params.id);
ctx.body = user;
}
export const register = async (ctx) => {
try {
const { username, email, password } = ctx.request.body
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
}

1
src/db/migrations/20250616065041_create_users_table.mjs

@ -7,6 +7,7 @@ export const up = async knex => {
table.increments("id").primary() // 自增主键
table.string("username", 100).notNullable() // 字符串字段(最大长度100)
table.string("email", 100).unique().notNullable() // 唯一邮箱
table.string("password", 100).unique() // 密码
table.integer("age").unsigned() // 无符号整数
table.timestamp("created_at").defaultTo(knex.fn.now()) // 创建时间
table.timestamp("updated_at").defaultTo(knex.fn.now()) // 更新时间

7
src/db/models/UserModel.js

@ -20,6 +20,13 @@ class UserModel {
static async delete(id) {
return db("users").where("id", id).del()
}
static async findByUsername(username) {
return db("users").where("username", username).first()
}
static async findByEmail(email) {
return db("users").where("email", email).first()
}
}
export default UserModel

6
src/main.js

@ -8,14 +8,16 @@ import os from "os"
import log4js from "log4js"
// 应用插件与自动路由
import LoadPlugins from "./plugins/install.js"
import LoadMiddlewares from "./middlewares/install.js"
import { autoRegisterControllers } from "utils/autoRegister.js"
import bodyParser from "koa-bodyparser"
const logger = log4js.getLogger()
const app = new Koa()
app.use(bodyParser());
// 注册插件
LoadPlugins(app)
LoadMiddlewares(app)
// 自动注册所有 controller
autoRegisterControllers(app)

68
src/middlewares/Auth/auth.js

@ -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()
}
}

3
src/middlewares/Auth/index.js

@ -0,0 +1,3 @@
// 统一导出所有中间件
import auth from "./auth.js"
export { auth }

3
src/middlewares/Auth/jwt.js

@ -0,0 +1,3 @@
// 兼容性导出,便于后续扩展
import jwt from "jsonwebtoken"
export default jwt

0
src/plugins/ResponseTime/index.js → src/middlewares/ResponseTime/index.js

0
src/plugins/Send/index.js → src/middlewares/Send/index.js

0
src/plugins/Send/resolve-path.js → src/middlewares/Send/resolve-path.js

0
src/plugins/errorHandler/index.js → src/middlewares/errorHandler/index.js

33
src/middlewares/install.js

@ -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()
})
}

23
src/plugins/install.js

@ -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();
})
}

28
src/services/JobService.js

@ -1,18 +1,18 @@
import jobs from '../jobs';
import jobs from "../jobs"
class JobService {
static startJob(id) {
return jobs.start(id);
}
static stopJob(id) {
return jobs.stop(id);
}
static updateJobCron(id, cronTime) {
return jobs.updateCronTime(id, cronTime);
}
static listJobs() {
return jobs.list();
}
startJob(id) {
return jobs.start(id)
}
stopJob(id) {
return jobs.stop(id)
}
updateJobCron(id, cronTime) {
return jobs.updateCronTime(id, cronTime)
}
listJobs() {
return jobs.list()
}
}
export default JobService;
export default JobService

105
src/services/userService.js

@ -1,39 +1,74 @@
// src/services/userService.js
// 用户相关业务逻辑
import UserModel from 'db/models/UserModel.js';
import UserModel from "db/models/UserModel.js"
import { hashPassword, comparePassword } from "utils/bcrypt.js"
import { JWT_SECRET } from "@/middlewares/Auth/auth.js"
import jwt from "@/middlewares/Auth/jwt.js"
class UserService {
static async getUserById(id) {
// 这里可以调用数据库模型
// 示例返回
return { id, name: `User_${id}` };
}
// 获取所有用户
static async getAllUsers() {
return await UserModel.findAll();
}
// 创建新用户
static async createUser(data) {
if (!data.name) throw new Error('用户名不能为空');
return await UserModel.create(data);
}
// 更新用户
static async updateUser(id, data) {
const user = await UserModel.findById(id);
if (!user) throw new Error('用户不存在');
return await UserModel.update(id, data);
}
// 删除用户
static async deleteUser(id) {
const user = await UserModel.findById(id);
if (!user) throw new Error('用户不存在');
return await UserModel.delete(id);
}
async getUserById(id) {
// 这里可以调用数据库模型
// 示例返回
return { id, name: `User_${id}` }
}
// 获取所有用户
async getAllUsers() {
return await UserModel.findAll()
}
// 创建新用户
async createUser(data) {
if (!data.name) throw new Error("用户名不能为空")
return await UserModel.create(data)
}
// 更新用户
async updateUser(id, data) {
const user = await UserModel.findById(id)
if (!user) throw new Error("用户不存在")
return await UserModel.update(id, data)
}
// 删除用户
async deleteUser(id) {
const user = await UserModel.findById(id)
if (!user) throw new Error("用户不存在")
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

37
src/utils/BaseSingleton.js

@ -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 }

2
src/utils/autoRegister.js

@ -26,7 +26,7 @@ export function autoRegisterControllers(app, controllersDir = path.resolve(__dir
} catch (e) {
controller = (await import(fullPath)).default
}
const routes = controller.routes || controller.default?.routes || controller.default || controller
const routes = controller.createRoutes || controller.default?.createRoutes || controller.default || controller
// 判断 routes 方法参数个数,支持自动适配
if (typeof routes === "function") {
allRouter.push(routes())

11
src/utils/bcrypt.js

@ -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)
}

4
src/utils/helper.js

@ -0,0 +1,4 @@
export function formatResponse(success, data = null, error = null) {
return { success, error, data }
}
Loading…
Cancel
Save