From 0e490e5def1bf1db1cde139cd52246b7d5daa21a Mon Sep 17 00:00:00 2001 From: SunWuyuan Date: Sat, 24 May 2025 14:57:57 +0800 Subject: [PATCH] Refactor Dockerfile to include build arguments for database type and set production environment. Update npm commands for dependency installation and Prisma client generation. Modify GitHub Actions workflow to support multiple database types in Docker image builds. Adjust Express server port and enhance logging in `www.js`. Update Prisma schema to use SQLite and clean up model definitions. Revise routes in `kv.js` to improve key-value management functionality and update front-end fetch calls in `index.ejs` for batch import and UUID generation. --- .dockerignore | 13 +++ .github/workflows/docker-image.yml | 12 ++- DEPLOYMENT.md | 137 +++++++++++++++++++++++++ Dockerfile | 25 ++++- bin/www | 8 +- data/db.db | Bin 0 -> 28672 bytes docker-compose.yml | 70 +++++++++++++ package.json | 1 - prisma/database/mysql/schema.prisma | 36 +++++++ prisma/database/postgres/schema.prisma | 9 ++ prisma/database/sqlite/schema.prisma | 37 +++++++ prisma/schema.prisma | 32 +++--- routes/kv.js | 79 +++++++------- views/index.ejs | 4 +- 14 files changed, 388 insertions(+), 75 deletions(-) create mode 100644 .dockerignore create mode 100644 DEPLOYMENT.md create mode 100644 data/db.db create mode 100644 docker-compose.yml create mode 100644 prisma/database/mysql/schema.prisma create mode 100644 prisma/database/postgres/schema.prisma create mode 100644 prisma/database/sqlite/schema.prisma diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d1b142b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +node_modules +npm-debug.log +.git +.gitignore +.env +*.md +.vscode +.idea +dist +build +coverage +.DS_Store +prisma/migrations diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 02873bf..e247d73 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -5,10 +5,14 @@ on: branches: - main workflow_dispatch: + jobs: build_and_push: name: Build and Push runs-on: ubuntu-latest + strategy: + matrix: + database: [mysql, postgres, sqlite] permissions: packages: write contents: read @@ -29,7 +33,6 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata for Docker id: meta uses: docker/metadata-action@v5 @@ -38,14 +41,17 @@ jobs: sunwuyuan/classworks ghcr.io/ZeroCatDev/ClassworksServer tags: | - type=sha + type=sha,suffix=-${{ matrix.database }} + type=raw,value=${{ matrix.database }},enable=${{ github.ref == format('refs/heads/{0}', 'main') }} flavor: | - latest=true + latest=false - name: Build and push Docker images uses: docker/build-push-action@v5 with: context: . + build-args: | + DATABASE_TYPE=${{ matrix.database }} push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..79063c0 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,137 @@ +## 快速开始 + +如果你想快速体验 Classworks,我们推荐使用 SQLite 版本。可以零配置运行。 + +## 部署方案 + +### MySQL 版本 + +```yaml +version: '3.8' + +services: + app: + build: + context: . + args: + DATABASE_TYPE: mysql + environment: + - NODE_ENV=production + - MYSQL_DATABASE_URL=mysql://user:password@mysql:3306/classworks + ports: + - 3000:3000 + depends_on: + mysql: + condition: service_healthy + + mysql: + image: mysql:8 + environment: + - MYSQL_DATABASE=classworks + - MYSQL_USER=user + - MYSQL_PASSWORD=password + - MYSQL_ROOT_PASSWORD=rootpassword + volumes: + - mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + mysql_data: +``` + +默认配置: +- 数据库版本:MySQL 8 +- 默认端口:3306 +- 数据持久化:自动配置 + +### PostgreSQL 版本 + + +```yaml +version: '3.8' + +services: + app: + build: + context: . + args: + DATABASE_TYPE: postgres + environment: + - NODE_ENV=production + - PG_DATABASE_URL=postgresql://user:password@postgres:5432/classworks + ports: + - 3000:3000 + depends_on: + postgres: + condition: service_healthy + + postgres: + image: postgres:15-alpine + environment: + - POSTGRES_DB=classworks + - POSTGRES_USER=user + - POSTGRES_PASSWORD=password + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U user -d classworks"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + postgres_data: +``` + +默认配置: +- 数据库版本:PostgreSQL 15 Alpine +- 默认端口:5432 +- 数据持久化:自动配置 + +### SQLite 版本 + +将以下内容保存为 `docker-compose.yml`: + +```yaml +version: '3.8' + +services: + app: + build: + context: . + args: + DATABASE_TYPE: sqlite + ports: + - 3000:3000 + environment: + - NODE_ENV=production + volumes: + - sqlite_data:/data + +volumes: + sqlite_data: +``` + +## 使用说明 + +1. 选择你需要的版本,将对应的配置复制到 `docker-compose.yml` 文件中 +2. 根据需要修改环境变量(见下方环境变量配置) +3. 运行 `docker compose up -d` 启动服务 + + +## 环境变量配置 +``` +# Axiom.co 遥测配置 可选 +AXIOM_DATASET= +AXIOM_TOKEN= + +# 网站密钥 可选 +SITE_KEY= + +# 服务端口 可选 默认3000 +PORT= +``` \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 3915299..ec3bfa1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,22 @@ FROM node:alpine -LABEL author=wuyuan -RUN apk add --no-cache openssl -COPY . / -RUN npm install + +# Required build argument for database type +ARG DATABASE_TYPE + +# Set production environment +ENV NODE_ENV=production \ + DATABASE_TYPE=${DATABASE_TYPE} + +# Copy all application files +COPY . . + + +# Install dependencies and generate Prisma client +RUN npm install && \ + npx prisma migrate dev --name init && \ + npx prisma generate + +USER node EXPOSE 3000 -CMD ["sh", "-c", "npm run prisma && npm run start"] + +CMD ["npm", "start"] diff --git a/bin/www b/bin/www index 7e40943..d8e4142 100644 --- a/bin/www +++ b/bin/www @@ -5,14 +5,13 @@ */ import app from '../app.js'; -import debug from 'debug'; import { createServer } from 'http'; /** * Get port from environment and store in Express. */ -var port = normalizePort(process.env.PORT || "3030"); +var port = normalizePort(process.env.PORT || "3000"); app.set("port", port); /** @@ -83,8 +82,5 @@ function onError(error) { function onListening() { var addr = server.address(); - var bind = typeof addr === 'string' - ? 'pipe ' + addr - : "port " + addr.port; - debug("Listening on " + bind); + console.log("可以在 http://localhost:"+addr.port+" 访问"); } diff --git a/data/db.db b/data/db.db new file mode 100644 index 0000000000000000000000000000000000000000..8fbb072d5ffdb6536da6b939c2c04ecfbe95f815 GIT binary patch literal 28672 zcmeI)&2QUe90%~nX+1lcs?RvC7G>(i%rWWS$!?sfI zDnhq@&{PC5s1k)cNr5GCGBI^g=qev|h1Pwx^qNiKwB0zVw;u@ilm~l3lucdtQ_Be> zmkQrC$6nAySTUv}<5B_DG{befkL|GdVpm7XJM~_(E9{*14w{X_ov;VZF{o$gb=L0@ zJN9nlL|JWf*lV|yR#)|FJKg%pY1nvT>uVb)ci)W1GjDBi#2o9!XYMCM_MJkd6VIBy zR|6eY4%UgM(ncb)z0EzVOj+-J(D5wC2v^a|uwm(3)>m+Csx6N>Jj!$9@sfL3@rVv5 ztj&9!`>lidpch>%r(uUp=Y0P<++K1z6Bee1a7Vlt;+`P-1KLgQ=A!XTeS4j6h@{Hl zM(nGPxkN^mxv$l!uxdM|JEH2yeCSZmw8rjgN#wHoYQX#>@Tc}G(sZ^jj$}6TXUb=W zcJ59_ON}p`Bw^^l9GmW$p{tZl)r&Nnv|-eIG-_~agn_T>aA-Yr7kgR=4LftJ;gIdH zPqp*Ky)Nz2^{B(npv4Hkt69{1VhOV;q)i;h7<;N`vT5tlksa=i+V;?79l4%iyQ*eQ z{F7Y}8m+E!th9w*tJ64cDMzavO3B^BNIdhQylyV6esHS1TIPS3zvlm4O|gaJZNgqi z5P$##AOHafKmY;|fB*y_009X6Ljq4yT(b8h6)O-D+bs1ZIWOm>oL(vvaz&|7%h8&w zz{}fB*y_009U<00Izz00bcL z{}9MV5@|l2j>TkIrlKtF@5}5?KL7nUL4KD@q;J0wRI{fUJnhUJ*_PvdAyRRlg?|~u z-+Z3+@Bcr~`uG1M{BMN+gJnn%fB*y_009U<00Izz00bZa0SH`Cfz4=|=lqKWk#sbD zdy{P~UKa?S|9?UF7guz6IBW<&00Izz00bZa0SG_<0uX=z1PIA;*V4>kYx;wM;QxV6 t@BF8NJb3>9CE;HJ3jz>;00bZa0SG_<0uX=z1Rwx`*G?e5k@kNJ@D~IAz=Hq) literal 0 HcmV?d00001 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7aadc3b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,70 @@ +version: '3.8' + +services: + app-mysql: + build: + context: . + args: + DATABASE_TYPE: mysql + environment: + - NODE_ENV=production + - DATABASE_URL=mysql://user:password@mysql:3306/classworks + depends_on: + mysql: + condition: service_healthy + + app-postgres: + build: + context: . + args: + DATABASE_TYPE: postgres + environment: + - NODE_ENV=production + - DATABASE_URL=postgresql://user:password@postgres:5432/classworks + depends_on: + postgres: + condition: service_healthy + + app-sqlite: + build: + context: . + args: + DATABASE_TYPE: sqlite + environment: + - NODE_ENV=production + volumes: + - sqlite_data:/data + + mysql: + image: mysql:8 + environment: + - MYSQL_DATABASE=classworks + - MYSQL_USER=user + - MYSQL_PASSWORD=password + - MYSQL_ROOT_PASSWORD=rootpassword + volumes: + - mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + + postgres: + image: postgres:15-alpine + environment: + - POSTGRES_DB=classworks + - POSTGRES_USER=user + - POSTGRES_PASSWORD=password + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U user -d classworks"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + mysql_data: + postgres_data: + sqlite_data: diff --git a/package.json b/package.json index ca009c0..4416e40 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,6 @@ "prisma": "prisma generate", "prisma:pull": "prisma db pull", "dev": "NODE_ENV=development nodemon node .bin/www", - "setup": "node ./scripts/setup.js", "test:rate-limit": "node ./scripts/test-rate-limit.js", "test:stress": "node ./scripts/stress-test.js", "test:distributed": "node ./scripts/distributed-test.js", diff --git a/prisma/database/mysql/schema.prisma b/prisma/database/mysql/schema.prisma new file mode 100644 index 0000000..c0d8d90 --- /dev/null +++ b/prisma/database/mysql/schema.prisma @@ -0,0 +1,36 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "mysql" + url = env("DATABASE_URL") +} + + +enum AccessType { + PUBLIC // No password required for read/write + PROTECTED // No password for read, password for write + PRIVATE // Password required for read/write +} + +model KVStore { + namespace String + key String + value Json + creatorIp String? @default("") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@id([namespace, key]) +} + +model Device { + uuid String @id + password String? + passwordHint String? + name String? + accessType AccessType @default(PUBLIC) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/prisma/database/postgres/schema.prisma b/prisma/database/postgres/schema.prisma new file mode 100644 index 0000000..02ce221 --- /dev/null +++ b/prisma/database/postgres/schema.prisma @@ -0,0 +1,9 @@ +generator client { + provider = "prisma-client-js" + output = "../generated/postgres/client" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} \ No newline at end of file diff --git a/prisma/database/sqlite/schema.prisma b/prisma/database/sqlite/schema.prisma new file mode 100644 index 0000000..954085a --- /dev/null +++ b/prisma/database/sqlite/schema.prisma @@ -0,0 +1,37 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = "file:../data/db.db" +} + + +enum AccessType { + PUBLIC // No password required for read/write + PROTECTED // No password for read, password for write + PRIVATE // Password required for read/write +} + +model KVStore { + namespace String + key String + value Json + creatorIp String? @default("") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@id([namespace, key]) +} + +model Device { + uuid String @id + password String? + passwordHint String? + name String? + accessType AccessType @default(PUBLIC) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + test String? +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bc244f2..b77015f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -3,33 +3,33 @@ generator client { } datasource db { - provider = "mysql" - url = env("DATABASE_URL") + provider = "sqlite" + url = "file:../data/db.db" } enum AccessType { - PUBLIC // No password required for read/write + PUBLIC // No password required for read/write PROTECTED // No password for read, password for write - PRIVATE // Password required for read/write + PRIVATE // Password required for read/write } model KVStore { - namespace String @db.Char(36) - key String - value Json - creatorIp String? @default("") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + namespace String + key String + value Json + creatorIp String? @default("") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@id([namespace, key]) } model Device { - uuid String @id @db.Char(36) - password String? + uuid String @id + password String? passwordHint String? - name String? - accessType AccessType @default(PUBLIC) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + name String? + accessType AccessType @default(PUBLIC) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } diff --git a/routes/kv.js b/routes/kv.js index 6efa4dd..5ab6aef 100644 --- a/routes/kv.js +++ b/routes/kv.js @@ -9,15 +9,9 @@ import { readAuthMiddleware, writeAuthMiddleware, removePasswordMiddleware, - authMiddleware, - deviceInfoMiddleware + deviceInfoMiddleware, } from "../middleware/auth.js"; -import { - DecodeAndhashPassword, - DecodeAndVerifyPassword, - hashPassword, - verifyPassword, -} from "../utils/crypto.js"; +import { hashPassword, verifyPassword } from "../utils/crypto.js"; const prisma = new PrismaClient(); @@ -333,40 +327,6 @@ router.get( return res.json(metadata); }) ); -/** - * POST /:namespace/:key - * 更新指定命名空间下的键值,如果不存在则创建 - */ -router.post( - "/:namespace/:key", - checkRestrictedUUID, - writeAuthMiddleware, - errors.catchAsync(async (req, res, next) => { - const { namespace, key } = req.params; - const value = req.body; - - if (!value || Object.keys(value).length === 0) { - // 创建并传递错误,而不是抛出 - return next(errors.createError(400, "请提供有效的JSON值")); - } - - // 获取客户端IP - const creatorIp = - req.headers["x-forwarded-for"] || - req.connection.remoteAddress || - req.socket.remoteAddress || - req.connection.socket?.remoteAddress || - ""; - - const result = await kvStore.upsert(namespace, key, value, creatorIp); - return res.status(200).json({ - namespace: result.namespace, - key: result.key, - created: result.createdAt.getTime() === result.updatedAt.getTime(), - updatedAt: result.updatedAt, - }); - }) -); /** * POST /:namespace/batch-import @@ -425,6 +385,41 @@ router.post( }); }) ); +/** + * POST /:namespace/:key + * 更新指定命名空间下的键值,如果不存在则创建 + */ +router.post( + "/:namespace/:key", + checkRestrictedUUID, + writeAuthMiddleware, + errors.catchAsync(async (req, res, next) => { + const { namespace, key } = req.params; + const value = req.body; + + if (!value || Object.keys(value).length === 0) { + // 创建并传递错误,而不是抛出 + return next(errors.createError(400, "请提供有效的JSON值")); + } + + // 获取客户端IP + const creatorIp = + req.headers["x-forwarded-for"] || + req.connection.remoteAddress || + req.socket.remoteAddress || + req.connection.socket?.remoteAddress || + ""; + + const result = await kvStore.upsert(namespace, key, value, creatorIp); + return res.status(200).json({ + namespace: result.namespace, + key: result.key, + created: result.createdAt.getTime() === result.updatedAt.getTime(), + updatedAt: result.updatedAt, + }); + }) +); + /** * DELETE /:namespace * 删除指定命名空间及其所有键值对 diff --git a/views/index.ejs b/views/index.ejs index 2b163c0..96faec2 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -86,7 +86,7 @@