1
0
mirror of https://github.com/ZeroCatDev/ClassworksKV.git synced 2025-07-01 11:59:22 +00:00

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.

This commit is contained in:
SunWuyuan 2025-05-24 14:57:57 +08:00
parent cf646d619f
commit 0e490e5def
No known key found for this signature in database
GPG Key ID: A6A54CF66F56BB64
14 changed files with 388 additions and 75 deletions

13
.dockerignore Normal file
View File

@ -0,0 +1,13 @@
node_modules
npm-debug.log
.git
.gitignore
.env
*.md
.vscode
.idea
dist
build
coverage
.DS_Store
prisma/migrations

View File

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

137
DEPLOYMENT.md Normal file
View File

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

View File

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

View File

@ -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+" 访问");
}

BIN
data/db.db Normal file

Binary file not shown.

70
docker-compose.yml Normal file
View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
generator client {
provider = "prisma-client-js"
output = "../generated/postgres/client"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

View File

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

View File

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

View File

@ -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
* 删除指定命名空间及其所有键值对

View File

@ -86,7 +86,7 @@
<script>
document.getElementById('generateUUID').addEventListener('click', async function() {
try {
const response = await fetch('/uuid', {
const response = await fetch('/_uuid', {
method: 'GET'
});
const data = await response.json();
@ -110,7 +110,7 @@
}
try {
const response = await fetch(`/${namespace}/batch-import`, {
const response = await fetch(`/${namespace}/_batchimport`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'