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:
parent
cf646d619f
commit
0e490e5def
13
.dockerignore
Normal file
13
.dockerignore
Normal file
@ -0,0 +1,13 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
.git
|
||||
.gitignore
|
||||
.env
|
||||
*.md
|
||||
.vscode
|
||||
.idea
|
||||
dist
|
||||
build
|
||||
coverage
|
||||
.DS_Store
|
||||
prisma/migrations
|
12
.github/workflows/docker-image.yml
vendored
12
.github/workflows/docker-image.yml
vendored
@ -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
137
DEPLOYMENT.md
Normal 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=
|
||||
```
|
25
Dockerfile
25
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"]
|
||||
|
8
bin/www
8
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+" 访问");
|
||||
}
|
||||
|
BIN
data/db.db
Normal file
BIN
data/db.db
Normal file
Binary file not shown.
70
docker-compose.yml
Normal file
70
docker-compose.yml
Normal 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:
|
@ -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",
|
||||
|
36
prisma/database/mysql/schema.prisma
Normal file
36
prisma/database/mysql/schema.prisma
Normal 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
|
||||
}
|
9
prisma/database/postgres/schema.prisma
Normal file
9
prisma/database/postgres/schema.prisma
Normal file
@ -0,0 +1,9 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
output = "../generated/postgres/client"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
37
prisma/database/sqlite/schema.prisma
Normal file
37
prisma/database/sqlite/schema.prisma
Normal 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?
|
||||
}
|
@ -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
|
||||
}
|
||||
|
79
routes/kv.js
79
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
|
||||
* 删除指定命名空间及其所有键值对
|
||||
|
@ -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'
|
||||
|
Loading…
x
Reference in New Issue
Block a user