- Edit
- components/HelloWorld.vue to test HMR
-
- Check out - create-vue, the - official Vue + Vite starter -
-- Learn more about IDE Support for Vue in the - Vue Docs Scaling up Guide. -
-Click on the Vite and Vue logos to learn more
- - - diff --git a/apps/dashboard/src/components/LoginDialog.vue b/apps/dashboard/src/components/LoginDialog.vue index 99ae2b75..054dde5e 100644 --- a/apps/dashboard/src/components/LoginDialog.vue +++ b/apps/dashboard/src/components/LoginDialog.vue @@ -60,6 +60,7 @@ import { import { Github, Globe, ChevronRight, Loader2 } from 'lucide-vue-next' import { apiClient } from '@/lib/api' import { toast } from 'vue-sonner' +import { DEFAULT_LOCAL_SERVER } from '@classworks/shared' const props = defineProps({ modelValue: Boolean, @@ -113,7 +114,7 @@ const loadProviders = async () => { // 处理登录 const handleLogin = (provider) => { // 构建OAuth URL - const authUrl = `${import.meta.env.VITE_API_BASE_URL || 'http://localhost:3030'}${provider.authUrl}` + const authUrl = `${import.meta.env.VITE_API_BASE_URL || DEFAULT_LOCAL_SERVER}${provider.authUrl}` // 打开新窗口进行OAuth认证 const width = 600 diff --git a/apps/dashboard/src/pages/auto-auth-test.vue b/apps/dashboard/src/pages/auto-auth-test.vue index 4ed25784..37b5df78 100644 --- a/apps/dashboard/src/pages/auto-auth-test.vue +++ b/apps/dashboard/src/pages/auto-auth-test.vue @@ -157,7 +157,7 @@ const testKVOperation = async () => { case 'get': response = await apiClient.getKVItem(tab3Form.value.token, key) break - case 'set': + case 'set': { let parsedValue try { parsedValue = JSON.parse(value) @@ -166,6 +166,7 @@ const testKVOperation = async () => { } response = await apiClient.setKVItem(tab3Form.value.token, key, parsedValue) break + } case 'delete': response = await apiClient.deleteKVItem(tab3Form.value.token, key) break diff --git a/apps/dashboard/vite.config.js b/apps/dashboard/vite.config.js index f096e221..e8ff6a61 100644 --- a/apps/dashboard/vite.config.js +++ b/apps/dashboard/vite.config.js @@ -1,4 +1,3 @@ -import path from 'path' import { fileURLToPath, URL } from 'node:url' import tailwindcss from '@tailwindcss/vite' import vue from '@vitejs/plugin-vue' diff --git a/apps/server/.env.example b/apps/server/.env.example new file mode 100644 index 00000000..c43817ba --- /dev/null +++ b/apps/server/.env.example @@ -0,0 +1,21 @@ +# Classworks KV 后端环境变量 +# 复制为 .env 后填写。OAuth 提供者详见 .env.oauth.example + +# PostgreSQL +DATABASE_URL=postgresql://user:password@localhost:5432/classworks + +# HTTP +PORT=3000 +BASE_URL=http://localhost:3000 +FRONTEND_URL=http://localhost:5173 + +# JWT(生产环境务必更换) +JWT_SECRET=change-me +REFRESH_TOKEN_SECRET=change-me +ACCESS_TOKEN_EXPIRES_IN=15m +REFRESH_TOKEN_EXPIRES_IN=7d + +# 可选 +NODE_ENV=development +SITE_KEY= +METRICS_TOKEN= diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile index 720af4bb..9a858e2d 100644 --- a/apps/server/Dockerfile +++ b/apps/server/Dockerfile @@ -3,16 +3,7 @@ FROM node:22-alpine AS base RUN corepack enable && corepack prepare pnpm@9.15.0 --activate WORKDIR /app -# --- dependency layer (changes only when package files change) --- -FROM base AS deps - -COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ -COPY apps/server/package.json apps/server/ -COPY packages/shared/package.json packages/shared/ - -RUN pnpm install --frozen-lockfile --prod - -# --- build layer (prisma generate, etc.) --- +# --- build: install workspace deps + generate Prisma client --- FROM base AS build COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ @@ -30,10 +21,10 @@ RUN cd apps/server && npx prisma generate FROM base ENV NODE_ENV=production +ENV SKIP_PRISMA_GENERATE=1 WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules -COPY --from=deps /app/apps/server/node_modules ./apps/server/node_modules +COPY --from=build /app/node_modules ./node_modules COPY --from=build /app/packages/shared ./packages/shared COPY --from=build /app/apps/server ./apps/server COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ diff --git a/apps/server/README.md b/apps/server/README.md index 01c86d2d..cf733a8e 100644 --- a/apps/server/README.md +++ b/apps/server/README.md @@ -24,7 +24,7 @@ See [LICENSE](./LICENSE) for details. 在根目录创建或编辑 `.env`: - 基础地址(用于回调): - - `BASE_URL`: `http://localhost:3030` + - `BASE_URL`: `http://localhost:3000` - `FRONTEND_URL`: `http://localhost:5173` - STCN(Casdoor)OIDC: diff --git a/apps/server/app.js b/apps/server/app.js index a8d39fe1..63d8efb7 100644 --- a/apps/server/app.js +++ b/apps/server/app.js @@ -17,6 +17,12 @@ import accountsRouter from './routes/accounts.js' import autoAuthRouter from './routes/auto-auth.js' import { register } from './utils/metrics.js' import cors from 'cors' +import { + HEADER_APP_TOKEN, + HEADER_SITE_KEY, + HEADER_DEVICE_UUID, + HEADER_DEVICE_PASSWORD, +} from '@classworks/shared' var app = express() @@ -26,7 +32,16 @@ app.use( exposedHeaders: ['ratelimit-policy', 'retry-after', 'ratelimit'], // 告诉浏览器这些响应头可以暴露 maxAge: 86400, // 设置OPTIONS请求的结果缓存24小时(86400秒),减少预检请求 credentials: true, // 允许跨域请求携带凭证 - allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'Accept'], // 允许的请求头 + allowedHeaders: [ + 'Content-Type', + 'Authorization', + 'X-Requested-With', + 'Accept', + HEADER_APP_TOKEN, + HEADER_SITE_KEY, + HEADER_DEVICE_UUID, + HEADER_DEVICE_PASSWORD, + ], methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的HTTP方法 withCredentials: true, // 允许携带cookie等凭证信息 }), diff --git a/apps/server/classworks.js b/apps/server/classworks.js index 355bd285..56db6669 100644 --- a/apps/server/classworks.js +++ b/apps/server/classworks.js @@ -4,79 +4,43 @@ import dotenv from 'dotenv' dotenv.config() -// 🔄 执行数据库迁移函数 -function runDatabaseMigration() { - try { - console.log('🔄 执行数据库迁移...') - execSync('npx prisma migrate deploy', { stdio: 'inherit' }) - console.log('✅ 数据库迁移完成') - } catch (error) { - console.error('❌ 数据库迁移失败:', error.message) - process.exit(1) - } -} - -// 🧱 数据库初始化函数 -function setupDatabase() { +function run(command, failMessage) { try { - // 执行数据库迁移 - runDatabaseMigration() + execSync(command, { stdio: 'inherit' }) } catch (error) { - console.error('❌ 数据库初始化失败:', error.message) + console.error(failMessage, error.message) process.exit(1) } } -// 🔨 本地构建函数 -function buildLocal() { - try { - // 确保数据库迁移已执行 - runDatabaseMigration() - execSync('npm install', { stdio: 'inherit' }) // 安装依赖 - execSync('npx prisma generate', { stdio: 'inherit' }) // 生成 Prisma 客户端 - console.log('✅ 构建完成') - } catch (error) { - console.error('❌ 构建失败:', error.message) - process.exit(1) - } +function runDatabaseMigration() { + console.log('🔄 执行数据库迁移...') + run('npx prisma migrate deploy', '❌ 数据库迁移失败:') + console.log('✅ 数据库迁移完成') } -// 🚀 启动服务函数 function startServer() { - try { - execSync('npm run start', { stdio: 'inherit' }) // 启动项目 - } catch (error) { - console.error('❌ 服务启动失败:', error.message) - process.exit(1) - } -} - -// ▶️ 执行 Prisma CLI 命令函数 -function runPrismaCommand(args) { - try { - const command = `npx prisma ${args.join(' ')}` - execSync(command, { stdio: 'inherit' }) - } catch (error) { - console.error('❌ Prisma 命令执行失败:', error.message) - process.exit(1) - } + console.log('🚀 启动服务...') + run(`${process.execPath} ./bin/www`, '❌ 服务启动失败:') } -// 🧠 主函数,根据命令行参数判断执行哪种流程 async function main() { - const args = process.argv.slice(2) // 获取命令行参数 + const args = process.argv.slice(2) if (args[0] === 'prisma') { - // 如果输入的是 prisma 命令,则执行 prisma 子命令 - runPrismaCommand(args.slice(1)) - } else { - // 否则按默认流程:初始化 → 构建 → 启动服务 - setupDatabase() - buildLocal() - startServer() + run(`npx prisma ${args.slice(1).join(' ')}`, '❌ Prisma 命令执行失败:') + return } + + runDatabaseMigration() + + // 镜像构建阶段已 generate;本地直接跑入口时补一次 + if (!process.env.SKIP_PRISMA_GENERATE) { + run('npx prisma generate', '❌ Prisma 客户端生成失败:') + } + + startServer() } -// 🚨 捕捉主函数异常 main().catch((error) => { console.error('❌ 脚本执行失败:', error) process.exit(1) diff --git a/apps/server/cli/README.md b/apps/server/cli/README.md index fac040fa..8040b550 100644 --- a/apps/server/cli/README.md +++ b/apps/server/cli/README.md @@ -24,7 +24,7 @@ node cli/get-token-callback.js 两种模式都支持以下环境变量: ```bash -# 设置API服务器地址(默认: http://localhost:3030) +# 设置API服务器地址(默认: http://localhost:3000) export API_BASE_URL=https://your-api-server.com # 设置授权页面地址(默认: http://localhost:5173/authorize) @@ -88,7 +88,7 @@ chmod +x cli/get-token.js 设备授权码: 1234-ABCD ============================================================ ℹ 授权码有效期: 15 分钟 -ℹ API服务器: http://localhost:3030 +ℹ API服务器: http://localhost:3000 ℹ 请在浏览器中打开上述地址,或在授权页面手动输入设备代码 ℹ 等待授权中... @@ -106,7 +106,7 @@ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ✓ 令牌已保存到: /home/user/.classworks/token.txt 使用示例: - curl -H "Authorization: Bearer eyJhbGc..." http://localhost:3030/kv + curl -H "Authorization: Bearer eyJhbGc..." http://localhost:3000/kv ``` ### 回调模式输出 @@ -126,7 +126,7 @@ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... 状态参数: abc123 ============================================================ ℹ 回调地址: http://localhost:8080/callback -ℹ API服务器: http://localhost:3030 +ℹ API服务器: http://localhost:3000 ℹ 超时时间: 300 秒 ℹ 正在启动回调服务器... @@ -145,7 +145,7 @@ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ✓ 令牌已保存到: /home/user/.classworks/token-callback.txt 使用示例: - curl -H "Authorization: Bearer eyJhbGc..." http://localhost:3030/kv + curl -H "Authorization: Bearer eyJhbGc..." http://localhost:3000/kv ``` ## 配置选项 @@ -154,7 +154,7 @@ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... 可以通过修改相应文件中的 `CONFIG` 对象或设置环境变量来调整: -- `baseUrl` / `API_BASE_URL` - API 服务器地址(默认: `http://localhost:3030`) +- `baseUrl` / `API_BASE_URL` - API 服务器地址(默认: `http://localhost:3000`) - `authPageUrl` / `AUTH_PAGE_URL` - Classworks 授权页面地址(默认: `http://localhost:5173/authorize`) - `appId` / `APP_ID` - 应用ID(默认: 1) - `siteKey` / `SITE_KEY` - 站点密钥(如果需要) diff --git a/apps/server/cli/get-token-callback.js b/apps/server/cli/get-token-callback.js index 26b7c1c4..8179692b 100644 --- a/apps/server/cli/get-token-callback.js +++ b/apps/server/cli/get-token-callback.js @@ -14,11 +14,12 @@ import http from 'http' import url from 'url' import { randomBytes } from 'crypto' +import { DEFAULT_LOCAL_SERVER, HEADER_SITE_KEY } from '@classworks/shared' // 配置 const CONFIG = { // API服务器地址 - baseUrl: process.env.API_BASE_URL || 'http://localhost:3030', + baseUrl: process.env.API_BASE_URL || DEFAULT_LOCAL_SERVER, // 站点密钥 siteKey: process.env.SITE_KEY || '', // 应用ID @@ -74,7 +75,7 @@ async function request(path, options = {}) { } if (CONFIG.siteKey) { - headers['X-Site-Key'] = CONFIG.siteKey + headers[HEADER_SITE_KEY] = CONFIG.siteKey } try { diff --git a/apps/server/cli/get-token.js b/apps/server/cli/get-token.js index edc7d790..9b964b57 100644 --- a/apps/server/cli/get-token.js +++ b/apps/server/cli/get-token.js @@ -10,10 +10,12 @@ * 或配置为可执行:chmod +x cli/get-token.js && ./cli/get-token.js */ +import { DEFAULT_LOCAL_SERVER, HEADER_SITE_KEY } from '@classworks/shared' + // 配置 const CONFIG = { // API服务器地址 - baseUrl: process.env.API_BASE_URL || 'http://localhost:3030', + baseUrl: process.env.API_BASE_URL || DEFAULT_LOCAL_SERVER, // 站点密钥 siteKey: process.env.SITE_KEY || '', // 应用ID @@ -67,7 +69,7 @@ async function request(path, options = {}) { } if (CONFIG.siteKey) { - headers['X-Site-Key'] = CONFIG.siteKey + headers[HEADER_SITE_KEY] = CONFIG.siteKey } try { diff --git a/apps/server/middleware/device.js b/apps/server/middleware/device.js deleted file mode 100644 index 9de739c8..00000000 --- a/apps/server/middleware/device.js +++ /dev/null @@ -1,158 +0,0 @@ -/** - * 设备管理中间件 - * - * 提供统一的设备UUID处理逻辑: - * 1. deviceMiddleware - 自动获取或创建设备,将设备信息存储到res.locals.device - * 2. deviceInfoMiddleware - 仅获取设备信息,不创建新设备 - * 3. passwordMiddleware - 验证设备密码 - */ -import errors from '../utils/errors.js' -import { verifyDevicePassword } from '../utils/crypto.js' -import { analyzeDevice } from '../utils/deviceDetector.js' -import { prisma } from '../utils/prisma.js' - -/** - * 为新设备创建默认的自动登录配置 - * @param {number} deviceId - 设备ID - */ -async function createDefaultAutoAuth(deviceId) { - try { - // 创建默认的自动授权配置:不需要密码、类型是classroom(一体机) - await prisma.autoAuth.create({ - data: { - deviceId: deviceId, - password: null, // 无密码 - deviceType: 'classroom', // 一体机类型 - isReadOnly: false, // 非只读 - }, - }) - } catch (error) { - console.error('创建默认自动登录配置失败:', error) - // 这里不抛出错误,避免影响设备创建流程 - } -} - -/** - * 设备中间件 - 统一处理设备UUID - * - * 从req.params.deviceUuid或req.body.deviceUuid获取UUID - * 如果设备不存在则自动创建,并智能生成设备名称 - * 将设备信息存储到res.locals.device - * - * 使用方式: - * router.post('/path', deviceMiddleware, handler) - * router.get('/path/:deviceUuid', deviceMiddleware, handler) - */ -export const deviceMiddleware = errors.catchAsync(async (req, res, next) => { - const deviceUuid = req.params.deviceUuid || req.body.deviceUuid - - if (!deviceUuid) { - return next(errors.createError(400, '缺少设备UUID')) - } - - // 查找或创建设备 - let device = await prisma.device.findUnique({ - where: { uuid: deviceUuid }, - }) - - if (!device) { - // 设备不存在,自动创建并生成智能设备名称 - const userAgent = req.headers['user-agent'] - const customDeviceType = req.body.deviceType || req.query.deviceType - const note = req.body.note || req.query.note - - // 生成设备名称,确保不为空 - const deviceName = analyzeDevice(userAgent, req.headers, customDeviceType, note).generatedName - - device = await prisma.device.create({ - data: { - uuid: deviceUuid, - name: deviceName, - password: null, - passwordHint: null, - accountId: null, - }, - }) - - // 为新创建的设备添加默认的自动登录配置 - await createDefaultAutoAuth(device.id) - - // 将设备分析结果添加到响应中 - res.locals.deviceAnalysis = deviceAnalysis - } - - // 将设备信息存储到res.locals - res.locals.device = device - next() -}) - -/** - * 设备信息中间件 - 仅获取设备信息,不创建新设备 - * - * 从req.params.deviceUuid获取UUID - * 如果设备不存在则返回404错误 - * 将设备信息存储到res.locals.device - * - * 使用方式: - * router.get('/path/:deviceUuid', deviceInfoMiddleware, handler) - */ -export const deviceInfoMiddleware = errors.catchAsync(async (req, res, next) => { - const deviceUuid = req.params.deviceUuid - - if (!deviceUuid) { - return next(errors.createError(400, '缺少设备UUID')) - } - - // 查找设备 - const device = await prisma.device.findUnique({ - where: { uuid: deviceUuid }, - }) - - if (!device) { - return next(errors.createError(404, '设备不存在')) - } - - // 将设备信息存储到res.locals - res.locals.device = device - next() -}) - -/** - * 密码验证中间件 - 验证设备密码 - * - * 前置条件:必须先使用deviceMiddleware或deviceInfoMiddleware - * 从req.body.password获取密码 - * 如果设备有密码但未提供或密码错误,则返回401错误 - * - * 特殊规则:如果设备绑定了账户,且req.account存在且匹配,则跳过密码验证 - * - * 使用方式: - * router.post('/path', deviceMiddleware, passwordMiddleware, handler) - */ -export const passwordMiddleware = errors.catchAsync(async (req, res, next) => { - const device = res.locals.device - const { password } = req.body - - if (!device) { - return next(errors.createError(500, '设备信息未加载,请先使用deviceMiddleware')) - } - - // 如果设备绑定了账户,且请求中有账户信息且匹配,则跳过密码验证 - if (device.accountId && req.account && req.account.id === device.accountId) { - return next() - } - - // 如果设备有密码,验证密码 - if (device.password) { - if (!password) { - return next(errors.createError(401, '设备需要密码')) - } - - const isValid = await verifyDevicePassword(password, device.password) - if (!isValid) { - return next(errors.createError(401, '密码错误')) - } - } - - next() -}) diff --git a/apps/server/middleware/uuidAuth.js b/apps/server/middleware/uuidAuth.js index b19c61ff..9f18a7af 100644 --- a/apps/server/middleware/uuidAuth.js +++ b/apps/server/middleware/uuidAuth.js @@ -9,7 +9,7 @@ import errors from '../utils/errors.js' import { verifyToken as verifyAccountJWT } from '../utils/jwt.js' import { verifyDevicePassword } from '../utils/crypto.js' import { prisma } from '../utils/prisma.js' -import { HEADER_DEVICE_UUID } from '@classworks/shared' +import { HEADER_DEVICE_UUID, HEADER_DEVICE_PASSWORD } from '@classworks/shared' /** * UUID+密码/JWT混合认证中间件 @@ -126,7 +126,7 @@ function extractUuid(req) { * 从请求中提取密码 */ function extractPassword(req) { - return req.headers['x-device-password'] || req.query.password || req.query.currentPassword + return req.headers[HEADER_DEVICE_PASSWORD] || req.query.password || req.query.currentPassword } /** diff --git a/apps/web/src/components/InitServiceChooser.vue b/apps/web/src/components/InitServiceChooser.vue index 7f5d746e..c5c6cbf2 100644 --- a/apps/web/src/components/InitServiceChooser.vue +++ b/apps/web/src/components/InitServiceChooser.vue @@ -92,7 +92,7 @@