diff --git a/.github/PROJECT_STRUCTURE.md b/.github/PROJECT_STRUCTURE.md deleted file mode 100644 index 0d3f22b..0000000 --- a/.github/PROJECT_STRUCTURE.md +++ /dev/null @@ -1,79 +0,0 @@ -# Project Structure - -## 📁 Directory Layout - -``` -ocdp-go/ -├── backend/ # Go 后端 -│ ├── cmd/api/ # 主程序入口 -│ ├── internal/ -│ │ ├── handlers/ # HTTP handlers -│ │ ├── graphql/ # GraphQL schema & resolvers -│ │ ├── models/ # 数据模型 -│ │ ├── storage/ # 存储层(JSON) -│ │ └── auth/ # 认证 -│ └── data/ # 数据存储(JSON文件) -│ -└── frontend/ # React 前端 - └── src/ - ├── app/ # 应用层 - │ ├── providers/ # 全局 Provider - │ ├── routes/ # 路由配置 - │ └── constants/ # 导航配置 - │ - ├── features/ # 功能模块(按类别) - │ ├── configuration/ # 🔧 配置类 - │ │ ├── clusters/ - │ │ └── registries/ - │ ├── monitoring/ # 📊 监控类 - │ │ └── clusters/ - │ ├── artifact/ # 📦 制品类 - │ │ ├── registries/ - │ │ └── instances/ - │ ├── auth/ - │ └── home/ - │ - ├── core/ # 核心层 - │ ├── api/ # API 调用 - │ ├── graphql/ # GraphQL 客户端 - │ ├── types/ # 类型定义 - │ └── config/ # 配置 - │ - └── shared/ # 共享层 - ├── components/ # 通用组件 - ├── hooks/ # 通用 hooks - ├── utils/ # 工具函数 - └── services/ # 服务 - -``` - -## 🎯 Features 模块分类 - -### Configuration(配置类) -- `clusters/` - 配置 K8s 集群连接 -- `registries/` - 配置 OCI 仓库连接 - -### Monitoring(监控类) -- `clusters/` - 监控集群状态 - -### Artifact(制品类) -- `registries/` - 浏览制品仓库内容 -- `instances/` - 管理已部署的服务实例 - -## 🔄 API 架构 - -- **GraphQL** - 用于 CRUD 操作(集群、仓库、实例) -- **RESTful** - 用于代理和实时查询(OCI、K8s 状态) -- **Unified API** - 自动根据配置切换 GraphQL/RESTful - -## 🚀 实例管理术语 - -- **Launch** - 启动/创建服务实例 -- **Modify** - 修改实例配置 -- **Terminate** - 终止实例运行 - -## 📝 说明 - -- 所有功能模块按**业务类别**分类(配置、监控、制品) -- Features 模块名使用**单数**形式(configuration, artifact) -- URL 路径保持 RESTful 风格(`/artifact/registries`) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f920c68 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,46 @@ +# Project Overview + + +# 🤖 Claude Code Agentic Workflow (Strictly Follow) + +作为本项目的资深 AI 研发工程师,你在执行任何指令时,必须严格遵守以下核心原则与工作流。 + +## Ⅰ. 核心原则 (Core Principles) +1. **No Laziness (拒绝偷懒):** 必须找到问题的根本原因 (Root Causes)。禁止使用临时补丁 (Hack/Temporary fixes)。保持高级工程师的标准。 +2. **Demand Elegance (苛求优雅):** 对于非琐碎的修改,停下来问自己:“有更优雅的实现方式吗?”如果你发现之前的代码很 Hacky,在掌握全局上下文后,用优雅的方式重构它(但不要过度设计)。 + +## Ⅱ. 任务管理闭环 (Task Management Protocol) +你必须通过读写 `tasks/` 目录下的文件来管理你的工作状态: +1. **Plan First:** 在开始实现前,将计划写入 `tasks/todo.md`,必须是可勾选的 Checkbox 列表。 +2. **Verify Plan:** 在动手写代码前,先和我(User)确认这个计划是否合理。 +3. **Track Progress:** 边做边在 `todo.md` 中打勾标记完成状态。 +4. **Explain Changes:** 在每执行完一个步骤时,给出高层次的代码修改总结。 +5. **Document Results:** 任务完成后,在 `todo.md` 中补充 Review 总结。 +6. **Capture Lessons:** 如果被我纠正了错误,立刻更新 `tasks/lessons.md`。 + +## Ⅲ. 工作流编排 (Workflow Orchestration) + +### 1. 强制规划模式 (Plan Node Default) +- 对于任何非琐碎任务(涉及 3 个以上步骤或架构决策),必须进入规划模式。 +- 提前写好详细的 Spec 以减少歧义。 +- **一旦情况不对劲(报错连连),立即停止盲目推进**,重新评估并制定新计划。 + +### 2. 经验自我迭代 (Self-Improvement Loop) +- 在每次会话开始时,主动读取 `tasks/lessons.md`,复习该项目的历史教训。 +- 针对犯过的错误,为自己制定防止再次踩坑的规则。 +- 无情地迭代这些经验,直到你的错误率显著下降。 + +### 3. 自主修复 Bug (Autonomous Bug Fixing) +- 当我给你一个 Bug 报告时:**直接去修。不要等我手把手教你。** +- 主动利用 CLI 权限去查看日志、定位错误代码、运行失败的测试用例,然后解决它。 +- 要求对用户“零上下文切换”——你去修复 CI 测试,不需要我告诉你具体该怎么做。 + +### 4. 交付前绝对验证 (Verification Before Done) +- **永远不要在没有证明代码能跑的情况下,把任务标记为“完成”。** +- 问自己:“Staff Engineer(主任工程师)会批准这段代码吗?” +- 必须主动运行测试(例如 `go test`, `npm run build`),检查日志,并向我证明正确性。 +- 对比修改前后的 Diff,确保行为符合预期。 + +### 5. 复杂问题拆解 (Agentic Strategy) +- 遇到极其复杂的问题时,不要试图在一个终端窗口内硬扛。 +- 拆解子任务,主动进行探索性研究,针对焦点问题逐一击破。 diff --git a/COMMANDS_CHEATSHEET.md b/COMMANDS_CHEATSHEET.md deleted file mode 100644 index e685256..0000000 --- a/COMMANDS_CHEATSHEET.md +++ /dev/null @@ -1,543 +0,0 @@ -# OCDP 命令速查表 - -快速查找常用的 Docker 和 Make 命令。 - ---- - -## 🚀 快速启动 - -```bash -# 开发环境(推荐,Mock 模式) -make docker-dev - -# 生产环境(真实数据库) -make docker-prod - -# 测试后端(独立) -make docker-test-backend - -# 测试前端(独立) -make docker-test-frontend -``` - ---- - -## 📋 完整命令列表 - -### Docker 服务管理 - -| 命令 | 说明 | 模式 | -|------|------|------| -| `make docker-dev` | 启动开发环境(前台) | Dev | -| `make docker-dev-bg` | 启动开发环境(后台) | Dev | -| `make docker-prod` | 启动生产环境 | Production | -| `make docker-up` | 同 docker-prod | Production | -| `make docker-down` | 停止所有服务 | - | -| `make docker-down-v` | 停止并删除数据卷 | - | - -### 服务测试 - -| 命令 | 说明 | 端口 | -|------|------|------| -| `make docker-test-backend` | 测试后端(前台) | 8080 | -| `make docker-test-backend-bg` | 测试后端(后台) | 8080 | -| `make docker-test-frontend` | 测试前端(前台) | 3000 | -| `make docker-test-frontend-bg` | 测试前端(后台) | 3000 | - -### 日志查看 - -| 命令 | 说明 | -|------|------| -| `make docker-logs` | 查看所有服务日志 | -| `make docker-logs-backend` | 只看后端日志 | -| `make docker-logs-frontend` | 只看前端日志 | -| `make docker-logs-db` | 只看数据库日志 | - -### 服务状态 - -| 命令 | 说明 | -|------|------| -| `make docker-ps` | 查看服务状态 | -| `make docker-status` | 查看详细状态(含健康检查) | - -### 镜像管理 - -| 命令 | 说明 | -|------|------| -| `make docker-build` | 构建所有镜像 | -| `make docker-build-no-cache` | 无缓存构建 | -| `make docker-restart` | 重启所有服务 | -| `make docker-restart-backend` | 只重启后端 | -| `make docker-restart-frontend` | 只重启前端 | - -### 工具服务 - -| 命令 | 说明 | 端口 | -|------|------|------| -| `make docker-tools` | 启动 pgAdmin + Swagger UI | 5050, 8081 | - ---- - -## 🔧 原始 Docker Compose 命令 - -### 开发模式 - -```bash -# 启动(前台) -docker compose -f docker-compose.yml -f docker-compose.dev.yml up - -# 启动(后台) -docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d - -# 停止 -docker compose -f docker-compose.yml -f docker-compose.dev.yml down - -# 查看日志 -docker compose -f docker-compose.yml -f docker-compose.dev.yml logs -f - -# 重启服务 -docker compose -f docker-compose.yml -f docker-compose.dev.yml restart backend -``` - -### 生产模式 - -```bash -# 启动(前台) -docker compose up - -# 启动(后台) -docker compose up -d - -# 停止 -docker compose down - -# 查看日志 -docker compose logs -f - -# 重启服务 -docker compose restart backend -``` - -### Mock 模式 - -```bash -# 测试后端 -docker compose -f docker-compose.mock.yml up backend - -# 测试前端 -docker compose -f docker-compose.mock.yml up frontend - -# 后台运行 -docker compose -f docker-compose.mock.yml up -d backend - -# 停止 -docker compose -f docker-compose.mock.yml down -``` - ---- - -## 🛠️ 本地开发命令(不使用 Docker) - -### 安装依赖 - -```bash -make install # 安装所有依赖 -make install-tools # 安装 OpenAPI 工具 -make install-deps # 安装项目依赖 -``` - -### 启动服务 - -```bash -make dev-local # 启动完整环境 -make dev-backend-mock # 只启动后端(Mock) -make dev-backend-prod # 只启动后端(生产) -make dev-frontend # 只启动前端 -``` - -### OpenAPI 工作流 - -```bash -make openapi-validate # 验证 OpenAPI 规范 -make openapi-gen # 生成前后端代码 -make openapi-gen-backend # 只生成后端代码 -make openapi-gen-frontend # 只生成前端代码 -make openapi-docs # 生成 HTML 文档 -make openapi-view # 启动文档服务器 -``` - -### 测试 - -```bash -make test # 运行所有测试 -make test-backend # 运行后端测试 -make test-frontend # 运行前端测试 -``` - -### 清理 - -```bash -make clean # 清理构建产物 -make clean-generated # 清理生成的代码 -``` - ---- - -## 📊 常用 Docker 命令 - -### 容器操作 - -```bash -# 进入容器 -docker compose exec backend sh -docker compose exec frontend sh -docker compose exec postgres psql -U postgres -d ocdp - -# 查看容器列表 -docker compose ps - -# 查看容器详情 -docker compose ps -a - -# 删除容器 -docker compose rm backend -docker compose rm -f backend # 强制删除 - -# 停止特定容器 -docker compose stop backend -docker compose stop frontend -``` - -### 日志操作 - -```bash -# 实时日志 -docker compose logs -f - -# 只看特定服务 -docker compose logs -f backend - -# 显示最后 N 行 -docker compose logs --tail=100 backend - -# 显示时间戳 -docker compose logs -f -t backend - -# 不跟随(只显示现有) -docker compose logs backend -``` - -### 镜像操作 - -```bash -# 构建镜像 -docker compose build - -# 无缓存构建 -docker compose build --no-cache - -# 只构建特定服务 -docker compose build backend -docker compose build frontend - -# 查看镜像 -docker images | grep ocdp - -# 删除镜像 -docker rmi ocdp-backend -docker rmi ocdp-frontend -``` - -### 数据卷操作 - -```bash -# 查看数据卷 -docker volume ls - -# 删除特定数据卷 -docker volume rm ocdp_postgres_data -docker volume rm ocdp_redis_data - -# 删除所有未使用的数据卷 -docker volume prune - -# 备份数据卷 -docker run --rm -v ocdp_postgres_data:/data -v $(pwd):/backup alpine tar czf /backup/postgres_backup.tar.gz -C /data . - -# 恢复数据卷 -docker run --rm -v ocdp_postgres_data:/data -v $(pwd):/backup alpine sh -c "cd /data && tar xzf /backup/postgres_backup.tar.gz" -``` - -### 网络操作 - -```bash -# 查看网络 -docker network ls - -# 查看网络详情 -docker network inspect ocdp-network - -# 测试网络连通性 -docker compose exec backend ping frontend -docker compose exec frontend ping backend -``` - ---- - -## 🔍 调试命令 - -### 健康检查 - -```bash -# 后端健康检查 -curl http://localhost:8080/health - -# 前端健康检查 -curl http://localhost:5173/ # Dev 模式 -curl http://localhost:3000/ # Production 模式 - -# 数据库健康检查 -docker compose exec postgres pg_isready -U postgres -``` - -### API 测试 - -```bash -# 登录 -curl -X POST http://localhost:8080/api/v1/auth/login \ - -H "Content-Type: application/json" \ - -d '{"username":"admin","password":"admin123"}' - -# 获取 Registries -curl http://localhost:8080/api/v1/registries - -# 获取 Clusters -curl http://localhost:8080/api/v1/clusters - -# 获取 Repositories -curl http://localhost:8080/api/v1/registries/harbor-prod/repositories - -# 获取 Artifacts(带过滤) -curl "http://localhost:8080/api/v1/registries/harbor-prod/repositories/charts%2Fvllm-serve/artifacts?media_type=chart" -``` - -### 数据库操作 - -```bash -# 连接数据库 -docker compose exec postgres psql -U postgres -d ocdp - -# 查看表 -docker compose exec postgres psql -U postgres -d ocdp -c "\dt" - -# 查询数据 -docker compose exec postgres psql -U postgres -d ocdp -c "SELECT * FROM registries;" - -# 执行 SQL 文件 -docker compose exec -T postgres psql -U postgres -d ocdp < schema.sql -``` - -### 性能监控 - -```bash -# 查看资源使用 -docker stats - -# 只看特定容器 -docker stats ocdp-backend ocdp-frontend - -# 查看容器进程 -docker compose top backend -docker compose top frontend -``` - ---- - -## 🎯 常用组合命令 - -### 完全重置 - -```bash -# 停止并删除所有内容 -docker compose down -v -docker compose -f docker-compose.dev.yml down -v -docker compose -f docker-compose.mock.yml down -v - -# 删除镜像 -docker rmi $(docker images | grep ocdp | awk '{print $3}') - -# 重新构建 -make docker-build - -# 重新启动 -make docker-dev -``` - -### 更新代码后重新部署 - -```bash -# 停止服务 -make docker-down - -# 拉取最新代码 -git pull - -# 重新构建 -make docker-build - -# 启动服务 -make docker-dev -``` - -### 查看完整系统状态 - -```bash -# 查看所有容器 -docker compose ps - -# 查看所有日志 -make docker-logs - -# 查看健康状态 -make docker-status - -# 测试所有服务 -curl http://localhost:8080/health -curl http://localhost:5173/ -curl http://localhost:5432 # 数据库端口 -``` - ---- - -## 📝 环境变量 - -### 后端环境变量 - -```bash -# Mock 模式 -export ADAPTER_MODE=mock -export JWT_SECRET=dev-secret -export ENCRYPTION_KEY=dev-encryption-key-32-bytes-long - -# Production 模式 -export ADAPTER_MODE=production -export DATABASE_URL=postgresql://postgres:postgres@postgres:5432/ocdp?sslmode=disable -export JWT_SECRET=your-production-secret -export ENCRYPTION_KEY=your-production-encryption-key-32-bytes -``` - -### 前端环境变量 - -```bash -export VITE_API_BASE_URL=http://localhost:8080/api/v1 -export VITE_USE_MOCK=false -``` - ---- - -## 🆘 故障排查命令 - -### 问题诊断 - -```bash -# 查看完整日志 -docker compose logs --tail=1000 > debug.log - -# 查看特定时间段日志 -docker compose logs --since 10m backend - -# 查看容器详细信息 -docker compose exec backend env - -# 检查配置 -docker compose config - -# 验证 docker-compose.yml -docker compose -f docker-compose.yml config -docker compose -f docker-compose.dev.yml config -``` - -### 端口冲突检查 - -```bash -# 查看端口占用 -sudo lsof -i :8080 -sudo lsof -i :5173 -sudo lsof -i :3000 -sudo lsof -i :5432 - -# 杀死进程 -sudo kill -9 -``` - -### 磁盘空间清理 - -```bash -# 删除未使用的容器 -docker container prune - -# 删除未使用的镜像 -docker image prune - -# 删除未使用的数据卷 -docker volume prune - -# 删除所有未使用的资源 -docker system prune -a --volumes -``` - ---- - -## 💡 实用技巧 - -### 1. 后台运行并查看日志 - -```bash -make docker-dev-bg && make docker-logs -``` - -### 2. 重启并查看日志 - -```bash -docker compose restart backend && docker compose logs -f backend -``` - -### 3. 构建并启动 - -```bash -make docker-build && make docker-dev -``` - -### 4. 清理并重新开始 - -```bash -make docker-down && docker system prune -f && make docker-dev -``` - -### 5. 快速测试 API - -```bash -# 保存 token -TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \ - -H "Content-Type: application/json" \ - -d '{"username":"admin","password":"admin123"}' | jq -r '.token') - -# 使用 token -curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/v1/registries -``` - ---- - -## 📚 更多资源 - -- [快速开始](./QUICK_START.md) -- [Docker 服务架构](./DOCKER_SERVICES.md) -- [项目重构总结](./PROJECT_RESTRUCTURE_SUMMARY.md) -- [README](./README.md) - ---- - -
- 命令速查表 - 最后更新:2025-11-09 -
- diff --git a/Makefile b/Makefile index e181899..78f2ff8 100644 --- a/Makefile +++ b/Makefile @@ -1,56 +1,192 @@ # ============================================================ -# OCDP stack orchestration Makefile -# run-2: 构建前端静态资源 + 启动 nginx(统一入口)和 backend 栈 -# clean-2: 清理 run-2 产生的容器 / 卷 / 网络 +# OCDP - Open Cloud Development Platform +# Makefile for Docker Compose deployment # ============================================================ SHELL := /bin/bash -COMPOSE_BIN ?= docker compose +# ============================================================ +# Configuration - Modify these for your environment +# ============================================================ -ROOT_COMPOSE := docker-compose.yml -BACKEND_COMPOSE := backend/docker-compose.yml -BACKEND_PROFILE := backend +# Server IP for external access (客户端访问IP) +SERVER_IP ?= 10.6.80.114 -COMPOSE_STACK := $(COMPOSE_BIN) -f $(ROOT_COMPOSE) -f $(BACKEND_COMPOSE) --profile $(BACKEND_PROFILE) -COMPOSE_STACK_ALL := $(COMPOSE_BIN) -f $(ROOT_COMPOSE) -f $(BACKEND_COMPOSE) -STACK_ENV := ADAPTER_MODE=production BACKEND_BUILD_CONTEXT=$(abspath backend) BACKEND_BUILD_DOCKERFILE=$(abspath backend/Dockerfile) BACKEND_MOCK_BUILD_DOCKERFILE=$(abspath backend/Dockerfile.mock) INIT_DB_SQL_PATH=$(abspath backend/scripts/init-db.sql) +# Backend configuration +BACKEND_PORT ?= 8080 +JWT_SECRET ?= change-me-in-production +ENCRYPTION_KEY ?= change-me-32-bytes-long-key-here +DATABASE_URL ?= postgresql://postgres:postgres@postgres:5432/ocdp?sslmode=disable +ADAPTER_MODE ?= production -STACK_SERVICES := postgres backend nginx +# Allowed CORS origins (for external access) +# 格式: http://IP:端口,多个用逗号分隔 +ALLOWED_ORIGINS ?= http://$(SERVER_IP),http://$(SERVER_IP):3000 -.PHONY: run-2 clean-2 build-backend +# Compose files +COMPOSE_FILES := -f docker-compose.yml -f backend/docker-compose.yml -run-2: - @echo "═══════════════════════════════════════════════" - @echo "🚀 run-2: rebuild static assets + start web gateway stack" - @echo "═══════════════════════════════════════════════" +# Database init SQL path (relative to project root) +INIT_DB_SQL_PATH ?= ./backend/scripts/init-db.sql + +# ============================================================ +# Production Commands (Docker Compose) +# ============================================================ + +.PHONY: up down restart clean logs logs-backend logs-frontend status help + +# Start all services +up: + @echo "============================================" + @echo "Starting OCDP services..." + @echo "Server IP: $(SERVER_IP)" + @echo "============================================" + @ALLOWED_DEV_ORIGINS="$(ALLOWED_ORIGINS)" \ + DATABASE_URL="$(DATABASE_URL)" \ + JWT_SECRET="$(JWT_SECRET)" \ + ENCRYPTION_KEY="$(ENCRYPTION_KEY)" \ + ADAPTER_MODE="$(ADAPTER_MODE)" \ + BACKEND_PORT=$(BACKEND_PORT) \ + INIT_DB_SQL_PATH="$(INIT_DB_SQL_PATH)" \ + docker compose $(COMPOSE_FILES) --profile backend up -d @echo "" - @export COMPOSE_PROJECT_NAME=ocdp && \ - export ADAPTER_MODE=production && \ - export BACKEND_BUILD_CONTEXT=$(abspath backend) && \ - export BACKEND_BUILD_DOCKERFILE=$(abspath backend/Dockerfile) && \ - export BACKEND_MOCK_BUILD_DOCKERFILE=$(abspath backend/Dockerfile.mock) && \ - export INIT_DB_SQL_PATH=$(abspath backend/scripts/init-db.sql) && \ - echo "→ Rebuilding frontend static assets" && \ - $(COMPOSE_STACK) run --rm frontend-build && \ - echo "" && \ - echo "→ Rebuilding backend image" && \ - $(COMPOSE_STACK) build backend && \ - echo "" && \ - echo "→ Bringing up backend + nginx services" && \ - $(COMPOSE_STACK) up -d $(STACK_SERVICES) + @echo "✅ Services started:" + @echo " Frontend: http://$(SERVER_IP)" + @echo " Backend: http://$(SERVER_IP):$(BACKEND_PORT)/api/v1" + @echo " Swagger: http://$(SERVER_IP):$(BACKEND_PORT)/api/docs" + @echo " PostgreSQL: localhost:5432" @echo "" - @echo "✅ Services online:" - @echo "═══════════════════════════════════════════════" + @echo " Default login: admin / admin123" + @echo "============================================" -clean-2: - @echo "═══════════════════════════════════════════════" - @echo "🧹 clean-2: tearing down run-2 stack" - @echo "═══════════════════════════════════════════════" - @$(COMPOSE_STACK_ALL) down --remove-orphans || true - @$(COMPOSE_STACK_ALL) down -v --remove-orphans || true - @$(COMPOSE_BIN) -f $(BACKEND_COMPOSE) down -v --remove-orphans || true - @echo "✅ Environment cleaned" - @echo "═══════════════════════════════════════════════" +# Stop all services (保留数据) +down: + @echo "Stopping OCDP services..." + @docker compose $(COMPOSE_FILES) down +# Restart all services +restart: down up +# Full cleanup (删除所有数据卷) +clean: + @echo "============================================" + @echo "⚠️ Full cleanup - 警告:此操作将删除所有数据!" + @echo "============================================" + @docker compose $(COMPOSE_FILES) down -v + @echo "✅ All services stopped and data removed" + +# ============================================================ +# Build Commands +# ============================================================ + +.PHONY: build build-frontend build-backend rebuild + +# Build all images +build: + @docker compose $(COMPOSE_FILES) build + +# Rebuild and start (强制重建前端) +rebuild: + @docker compose $(COMPOSE_FILES) up -d --build --force-recreate + +# Rebuild frontend only +build-frontend: + @docker compose -f docker-compose.yml build frontend + +# Rebuild backend only +build-backend: + @docker compose -f backend/docker-compose.yml build backend + +# ============================================================ +# Log Commands +# ============================================================ + +# View all logs +logs: + @docker compose $(COMPOSE_FILES) logs -f + +# View backend logs +logs-backend: + @docker logs -f ocdp-backend + +# View frontend logs +logs-frontend: + @docker logs -f ocdp-frontend + +# View nginx logs +logs-nginx: + @docker logs -f ocdp-nginx + +# ============================================================ +# Status Commands +# ============================================================ + +# Show service status +status: + @docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" + +# ============================================================ +# Database Commands +# ============================================================ + +.PHONY: db-reset db-init db-shell + +# Reset database (删除并重建) +db-reset: + @echo "Resetting database..." + @docker compose $(COMPOSE_FILES) exec -T postgres psql -U postgres -c "DROP DATABASE IF EXISTS ocdp;" || true + @docker compose $(COMPOSE_FILES) exec -T postgres psql -U postgres -c "CREATE DATABASE ocdp;" || true + @docker compose $(COMPOSE_FILES) exec -T postgres psql -U postgres -d ocdp -c "$$(cat backend/scripts/init-db.sql)" + +# Initialize database +db-init: + @docker compose $(COMPOSE_FILES) exec -T postgres psql -U postgres -d ocdp -c "$$(cat backend/scripts/init-db.sql)" + +# Open database shell +db-shell: + @docker compose $(COMPOSE_FILES) exec postgres psql -U postgres -d ocdp + +# ============================================================ +# Help +# ============================================================ + +help: + @echo "OCDP - Open Cloud Deployment Platform" + @echo "" + @echo "Usage: make [target]" + @echo "" + @echo "Main Commands:" + @echo " make up - 启动所有服务" + @echo " make down - 停止所有服务(保留数据)" + @echo " make restart - 重启所有服务" + @echo " make clean - 完全清理(删除所有数据)" + @echo " make rebuild - 强制重建并启动" + @echo "" + @echo "Build Commands:" + @echo " make build - 构建所有镜像" + @echo " make build-frontend - 只构建前端" + @echo " make build-backend - 只构建后端" + @echo "" + @echo "Log Commands:" + @echo " make logs - 查看所有日志" + @echo " make logs-backend - 只看后端日志" + @echo " make logs-frontend - 只看前端日志" + @echo " make logs-nginx - 只看nginx日志" + @echo "" + @echo "Database Commands:" + @echo " make db-reset - 重置数据库" + @echo " make db-init - 初始化数据库" + @echo " make db-shell - 进入数据库终端" + @echo "" + @echo "Utility Commands:" + @echo " make status - 查看服务状态" + @echo "" + @echo "Environment Variables:" + @echo " SERVER_IP=$(SERVER_IP) - 服务器IP(默认: 10.6.80.114)" + @echo " BACKEND_PORT=$(BACKEND_PORT) - 后端端口(默认: 8080)" + @echo " ALLOWED_ORIGINS=$(ALLOWED_ORIGINS) - 允许的跨域来源" + @echo "" + @echo "Examples:" + @echo " make up SERVER_IP=192.168.1.100 # 自定义IP启动" + @echo " make clean # 完全清理并重新开始" + @echo "============================================" \ No newline at end of file diff --git a/QUICK_START.md b/QUICK_START.md deleted file mode 100644 index 4f5a102..0000000 --- a/QUICK_START.md +++ /dev/null @@ -1,413 +0,0 @@ -# OCDP 快速开始指南 - -## 🚀 5分钟快速体验 - -### 前置要求 - -- Docker 20.10+ -- Docker Compose 2.0+ -- (可选) Make 工具 - -### 第一步:克隆项目 - -```bash -git clone -cd ocdp-go -``` - -### 第二步:选择运行模式 - -#### 方式 1: 开发模式(推荐用于日常开发) - -**特点**: -- ✅ 后端 Mock 模式(无需数据库) -- ✅ 热重载(代码修改自动生效) -- ✅ 快速启动 -- ✅ 适合快速迭代开发 - -```bash -# 使用 Make -make docker-dev - -# 或直接使用 Docker Compose -docker compose --profile dev up -``` - -**访问服务**: -- 前端:http://localhost:5173 -- 后端:http://localhost:8080 -- API 文档:http://localhost:8080/api/v1 - -**默认账号**: -- 用户名:`admin` -- 密码:`admin123` - -#### 方式 2: 生产模式(用于完整功能测试) - -**特点**: -- ✅ 真实数据库 -- ✅ 完整功能 -- ✅ 生产环境配置 - -```bash -# 使用 Make -make docker-prod - -# 或直接使用 Docker Compose -docker compose up -d -``` - -**访问服务**: -- 前端:http://localhost:3000 -- 后端:http://localhost:8080 -- 数据库:localhost:5432 - -#### 方式 3: 独立测试单个服务 - -**测试后端**: -```bash -make docker-test-backend -# 访问:http://localhost:8080 -``` - -**测试前端**: -```bash -make docker-test-frontend -# 访问:http://localhost:3000 -``` - -### 第三步:验证服务 - -```bash -# 检查后端健康状态 -curl http://localhost:8080/health - -# 登录获取 token -curl -X POST http://localhost:8080/api/v1/auth/login \ - -H "Content-Type: application/json" \ - -d '{"username":"admin","password":"admin123"}' - -# 获取 registries 列表 -curl http://localhost:8080/api/v1/registries -``` - -### 第四步:开始开发 - -#### 修改后端代码 - -```bash -# 编辑任意 Go 文件 -vim backend/cmd/api/main.go - -# Air 会自动检测变化并重新编译(开发模式) -# 查看日志确认重载 -docker compose -f docker-compose.yml -f docker-compose.dev.yml logs -f backend -``` - -#### 修改前端代码 - -```bash -# 编辑任意 React 组件 -vim frontend/src/App.tsx - -# Vite HMR 会自动更新浏览器(开发模式) -# 无需手动刷新页面 -``` - -### 停止服务 - -```bash -# 停止并删除容器 -make docker-down - -# 或 -docker compose down - -# 完全清理(包括数据卷) -docker compose down -v -``` - ---- - -## 📚 常用命令速查 - -### Docker 命令 - -| 命令 | 说明 | -|------|------| -| `make docker-dev` | 启动开发环境 | -| `make docker-prod` | 启动生产环境 | -| `make docker-test-backend` | 测试后端 | -| `make docker-test-frontend` | 测试前端 | -| `make docker-logs` | 查看所有日志 | -| `make docker-down` | 停止所有服务 | -| `make docker-build` | 重新构建镜像 | - -### 查看日志 - -```bash -# 所有服务 -docker compose logs -f - -# 只看后端 -docker compose logs -f backend - -# 只看前端 -docker compose logs -f frontend - -# 只看最后 100 行 -docker compose logs --tail=100 -``` - -### 进入容器 - -```bash -# 进入后端容器 -docker compose exec backend sh - -# 进入前端容器 -docker compose exec frontend sh - -# 进入数据库容器 -docker compose exec postgres psql -U postgres -d ocdp -``` - -### 重启服务 - -```bash -# 重启所有 -docker compose restart - -# 重启后端 -docker compose restart backend - -# 重启前端 -docker compose restart frontend -``` - ---- - -## 🎯 使用场景指南 - -### 场景 1: 我要开发新功能 - -```bash -# 1. 启动开发环境 -make docker-dev - -# 2. 修改代码(自动热重载) - -# 3. 查看日志 -make docker-logs - -# 4. 测试功能 -# 访问 http://localhost:5173 - -# 5. 停止 -make docker-down -``` - -### 场景 2: 我要测试完整功能 - -```bash -# 1. 启动生产环境(包含数据库) -make docker-prod - -# 2. 访问前端 -# http://localhost:3000 - -# 3. 停止 -make docker-down -``` - -### 场景 3: 我只想测试后端 API - -```bash -# 1. 启动后端 Mock -make docker-test-backend-bg - -# 2. 测试 API -curl http://localhost:8080/health -curl http://localhost:8080/api/v1/registries - -# 3. 停止 -docker compose -f docker-compose.mock.yml down -``` - -### 场景 4: 我只想测试前端界面 - -```bash -# 1. 启动前端 Mock -make docker-test-frontend-bg - -# 2. 访问前端 -# http://localhost:3000 - -# 3. 停止 -docker compose -f docker-compose.mock.yml down -``` - -### 场景 5: 代码修改不生效 - -```bash -# 1. 停止所有服务 -make docker-down - -# 2. 重新构建镜像 -make docker-build --no-cache - -# 3. 重新启动 -make docker-dev -``` - ---- - -## 🔍 常见问题 - -### Q1: 端口被占用怎么办? - -**问题**:启动时报错 "port is already allocated" - -**解决方案**: -```bash -# 查看占用端口的进程 -sudo lsof -i :8080 -sudo lsof -i :5173 -sudo lsof -i :3000 - -# 杀掉进程或修改 docker-compose.yml 中的端口映射 -``` - -### Q2: 容器启动失败 - -**查看详细错误**: -```bash -docker compose logs backend -docker compose logs frontend -``` - -**常见原因**: -- 端口冲突 -- 依赖服务未就绪 -- 配置错误 - -**解决方案**: -```bash -# 清理并重新启动 -docker compose down -v -docker compose build --no-cache -docker compose up -``` - -### Q3: 热重载不工作 - -**后端**: -```bash -# 确认 Air 是否运行 -docker compose logs backend | grep "air" - -# 检查文件挂载 -docker compose exec backend ls -la /app -``` - -**前端**: -```bash -# 确认 Vite 是否运行 -docker compose logs frontend | grep "VITE" - -# 重启前端 -docker compose restart frontend -``` - -### Q4: 数据库连接失败 - -**检查**: -```bash -# 数据库是否运行 -docker compose ps postgres - -# 健康检查 -docker compose exec postgres pg_isready -``` - -**解决方案**: -- 确保使用生产模式(`docker compose up`) -- 等待数据库健康检查通过(约 10-20 秒) -- 检查 `DATABASE_URL` 环境变量 - -### Q5: 前端无法连接后端 - -**检查**: -```bash -# 后端是否运行 -curl http://localhost:8080/health - -# 网络连接 -docker network inspect ocdp-network -``` - -**解决方案**: -- 确认后端服务运行正常 -- 检查 `VITE_API_BASE_URL` 环境变量 -- 检查浏览器控制台错误 - ---- - -## 📖 进一步学习 - -### 详细文档 - -- [Docker 服务架构](./DOCKER_SERVICES.md) - 完整的服务说明 -- [开发指南](./docs/development/specification.md) - 开发规范 -- [API 文档](./backend/docs/openapi.yaml) - OpenAPI 规范 -- [部署指南](./docs/deployment/docker-guide.md) - 生产部署 - -### 项目结构 - -``` -ocdp-go/ -├── backend/ # Go 后端服务 -├── frontend/ # React 前端应用 -├── api/ # OpenAPI 规范 -├── docs/ # 项目文档 -├── docker-compose.yml # 生产模式 -├── docker-compose.dev.yml # 开发模式 -├── docker-compose.mock.yml # Mock 模式 -└── Makefile # 便捷命令 -``` - -### 架构说明 - -``` -┌─────────────┐ ┌─────────────┐ -│ Frontend │────▶│ Backend │ -│ (React) │◀────│ (Go API) │ -└─────────────┘ └─────────────┘ - │ - ▼ - ┌─────────────┐ - │ PostgreSQL │ - │ Database │ - └─────────────┘ -``` - ---- - -## 🎉 开始使用 - -现在你已经准备好开始使用 OCDP 了! - -**推荐的开发流程**: -1. ✅ 使用 `make docker-dev` 启动开发环境 -2. ✅ 修改代码(自动热重载) -3. ✅ 使用浏览器测试功能 -4. ✅ 使用 `make docker-test-backend` 测试后端 API -5. ✅ 使用 `make docker-prod` 进行完整测试 - -**需要帮助?** -- 查看 [DOCKER_SERVICES.md](./DOCKER_SERVICES.md) 了解详细配置 -- 查看 [GitHub Issues](https://github.com/your-repo/issues) 报告问题 -- 查看项目文档获取更多信息 - -Happy Coding! 🚀 - diff --git a/README.md b/README.md index 20b9f9c..f26f857 100644 --- a/README.md +++ b/README.md @@ -1,336 +1,201 @@ -# OCDP - Open Cloud Development Platform +# OCDP - Open Cloud Deployment Platform -[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -[![Go Version](https://img.shields.io/badge/go-1.24+-00ADD8?logo=go)](https://go.dev/) -[![Node Version](https://img.shields.io/badge/node-20+-339933?logo=node.js)](https://nodejs.org/) -[![Docker](https://img.shields.io/badge/docker-20.10+-2496ED?logo=docker)](https://www.docker.com/) +开源云原生部署平台,支持从 Harbor(或其他 OCI Registry)拉取 Helm Charts 并一键部署到多个 Kubernetes 集群。 -开源云原生开发平台,用于管理 Kubernetes 集群、OCI Registry 和 Helm Charts 部署。 +## 功能特性 ---- +- **多集群管理** - 支持多个 kubeconfig,管理多个 K8S 集群 +- **Registry 管理** - 支持 Harbor、Docker Registry、OCI 标准仓库 +- **多租户支持** - Workspace 隔离,管理员和普通用户角色 +- **存储后端** - NFS/PV/hostPath 存储配置管理 +- **Chart 引用** - 管理可用的 Helm Charts +- **Values 模板** - 版本控制、支持回滚的配置模板 +- **一键部署** - 从 Harbor 拉取 Charts 部署到指定集群 +- **实例管理** - 支持升级、回滚、卸载 Helm Release +- **状态监控** - 实时同步 Helm Release 状态 -## ✨ 特性 +## 技术栈 -- 🎯 **Registry 管理** - 支持 Harbor、Docker Registry、OCI 标准仓库 -- 📦 **Artifact 浏览** - 浏览和管理 Helm Charts、容器镜像 -- 🚀 **一键部署** - 可视化部署 Helm Charts 到 Kubernetes 集群 -- 🔍 **智能过滤** - 按 MediaType 过滤 artifacts(chart、image、other) -- 🎨 **现代 UI** - 响应式设计,基于 React + TypeScript -- 🔐 **安全认证** - JWT 认证,加密存储敏感信息 -- 🐳 **容器化** - 完整的 Docker 支持,多种运行模式 -- 🔄 **热重载** - 开发模式支持代码热重载 +| 层级 | 技术 | +|------|------| +| 后端 | Go 1.21+, Hexagonal Architecture | +| 前端 | React 18, TypeScript, Next.js, TailwindCSS | +| 数据库 | PostgreSQL | +| 网关 | Nginx | ---- +## 快速开始 -## 🚀 快速开始 - -### 前置要求 - -- Docker 20.10+ -- Docker Compose 2.0+ -- (可选) Make 工具 - -### 5分钟快速体验 +### Docker Compose 启动(推荐) ```bash -# 1. 克隆项目 -git clone -cd ocdp-go +# 1. 完全停止并清理现有容器 +docker compose -f docker-compose.yml -f backend/docker-compose.yml down -v -# 2. 启动开发环境(Mock 模式,无需数据库) -make docker-dev +# 2. 启动所有服务(PostgreSQL + Backend + Frontend + Nginx) +ALLOWED_DEV_ORIGINS="http://10.6.80.114:3000" \ +NEXT_PUBLIC_API_URL="http://10.6.80.114:8080/api/v1" \ +BACKEND_PORT=8080 \ +docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend up -d -# 3. 访问应用 -# - 前端:http://localhost:5173 -# - 后端:http://localhost:8080 -# - 默认账号:admin / admin123 +# 3. 查看服务状态 +docker ps + +# 4. 访问 +# 前端: http://10.6.80.114 +# 后端: http://10.6.80.114:8080/api/v1 +# 默认账号: admin / admin123 + +# 停止服务 +docker compose -f docker-compose.yml -f backend/docker-compose.yml down ``` -**详细指南**:查看 [快速开始指南](./QUICK_START.md) +### 开发环境 ---- +```bash +# 1. 确保 PostgreSQL 运行在 localhost:5432 +# 启动 PostgreSQL 容器: +# docker run -d --name ocdp-postgres -e POSTGRES_DB=ocdp -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 postgres:15 -## 📚 文档导航 +# 2. 初始化数据库(首次运行) +# 方法一:手动执行 SQL +cat backend/scripts/init-db.sql | docker exec -i ocdp-postgres psql -U postgres -d ocdp -### 📖 核心文档(必读) -- 🚀 [快速开始](./QUICK_START.md) - 5分钟快速上手 -- 📋 [使用指南](./USAGE_GUIDE.md) - 详细使用说明(推荐) -- 💡 [命令速查表](./COMMANDS_CHEATSHEET.md) - 常用命令快速参考 -- 📚 [文档中心](./docs/README.md) - 完整文档索引 +# 方法二:使用 Make(需确保 PostgreSQL 容器名为 ocdp-postgres) +make db-init -### 🔧 专业文档 -- 📐 [开发规范](./docs/development/specification.md) - 代码规范和架构 -- 🚢 [部署指南](./docs/deployment/docker-guide.md) - 生产环境部署 -- 🔒 [安全实践](./docs/security/security-implementation.md) - 安全配置 -- 🎨 [功能文档](./docs/features/) - 详细功能说明 +# 3. 启动后端(需要设置环境变量) +cd backend && \ +DATABASE_URL="postgres://postgres:postgres@localhost:5432/ocdp?sslmode=disable" \ +JWT_SECRET="test-jwt-secret-key" \ +ENCRYPTION_KEY="test-encryption-key-32-bytes-long" \ +PORT=8081 \ +ALLOWED_DEV_ORIGINS="10.6.80.114,localhost,127.0.0.1" \ +go run cmd/api/main.go -### 🔗 其他资源 -- 📋 [OpenAPI 规范](./backend/docs/openapi.yaml) - RESTful API 定义 -- 📦 [历史文档](./docs/archive/) - 项目演进历史 +# 4. 启动前端(需要 Node.js 20) +source ~/.nvm/nvm.sh && nvm use 20 +cd frontend && NEXT_PUBLIC_API_URL=http://10.6.80.114:8081/api/v1 npm run dev ---- +# 5. 从外部访问 +# 本机访问: http://localhost:3000 +# 外部访问: http://10.6.80.114:3000 +# 默认账号: admin / admin123 -## 🏗️ 技术架构 +# ===== 一键启动脚本 ===== +# 启动所有服务(后台运行) +./start.sh -### 技术栈 - -**后端**: -- Go 1.24+ (Hexagonal Architecture) -- PostgreSQL 16 -- Redis 7 - -**前端**: -- React 18 -- TypeScript 5 -- Vite 6 -- TailwindCSS 3 - -**容器化**: -- Docker -- Docker Compose -- Multi-stage builds - -### 架构图 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Frontend │ -│ React + TypeScript + Vite │ -└──────────────────────────┬──────────────────────────────────┘ - │ HTTP/REST -┌──────────────────────────┼──────────────────────────────────┐ -│ │ Backend API │ -│ ▼ │ -│ ┌─────────────────────┐ │ -│ │ Input Adapters │ │ -│ │ (REST/GraphQL) │ │ -│ └──────────┬──────────┘ │ -│ │ │ -│ ┌──────────▼──────────┐ │ -│ │ Domain Services │ │ -│ │ (Business Logic) │ │ -│ └──────────┬──────────┘ │ -│ │ │ -│ ┌──────────▼──────────┐ │ -│ │ Output Adapters │ │ -│ │ (Repos/Clients) │ │ -│ └──────────┬──────────┘ │ -└───────────────────────┼─┴────────────────────────────────┘ - │ - ┌───────────────┼───────────────┐ - │ │ │ - ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ - │ PG DB │ │ Redis │ │ OCI │ - │ │ │ │ │ Registry│ - └─────────┘ └─────────┘ └─────────┘ +# 停止所有服务 +./stop.sh ``` -### 运行模式 +### 生产环境(Docker Compose) -| 模式 | 特点 | 适用场景 | 命令 | -|------|------|----------|------| -| **开发模式** | Mock 数据,热重载 | 日常开发 | `make docker-dev` | -| **生产模式** | 真实数据库,完整功能 | 生产部署 | `make docker-prod` | -| **Mock 模式** | 独立测试单个服务 | 单元测试 | `make docker-test-backend` | +```bash +# 构建并启动所有服务 +make run-2 ---- +# 停止服务 +make clean-2 +``` -## 🛠️ 开发指南 +## 配置 -### 项目结构 +### 环境变量 + +| 变量 | 默认值 | 说明 | +|------|--------|------| +| DATABASE_URL | postgres://postgres:postgres@localhost:5432/ocdp?sslmode=disable | 数据库连接串 | +| PORT | 8080 | 后端端口 | +| BACKEND_PORT | 8080 | Docker 映射端口 | +| JWT_SECRET | change-me-in-production | JWT 密钥 | +| ENCRYPTION_KEY | change-me-32-bytes-long-key-here | 加密密钥 | +| ALLOWED_DEV_ORIGINS | http://10.6.80.114:3000 | 允许的跨域来源(外部访问时需要配置)| +| NEXT_PUBLIC_API_URL | http://10.6.80.114:8080/api/v1 | 前端调用的API地址 | + +### 配置文件 + +项目根目录 `.env` 文件包含默认配置: +- Kubernetes 集群配置 +- Harbor 仓库信息 +- NFS 存储配置 + +## 访问 + +| 服务 | 地址 | +|------|------| +| 前端 (Nginx) | http://10.6.80.114 | +| 后端 API | http://10.6.80.114:8080/api/v1 | +| API 文档 | http://10.6.80.114:8080/api/docs | + +**默认账号**: `admin` / `admin123` + +## 项目结构 ``` ocdp-go/ -├── backend/ # Go 后端服务 -│ ├── cmd/api/ # 应用入口 -│ ├── internal/ # 内部代码 -│ │ ├── adapter/ # 适配器层 -│ │ ├── domain/ # 领域层 -│ │ └── bootstrap/ # 启动配置 -│ ├── Dockerfile # 生产环境 -│ ├── Dockerfile.dev # 开发环境 -│ └── Dockerfile.mock # Mock 测试 -│ -├── frontend/ # React 前端应用 +├── backend/ # Go 后端 (Hexagonal Architecture) +│ ├── cmd/api/ # 入口点 +│ ├── internal/ +│ │ ├── adapter/ # 适配器层 (HTTP, Persistence) +│ │ ├── domain/ # 领域层 (Entity, Service, Repository) +│ │ └── bootstrap/ # 初始化和种子数据 +│ └── scripts/ # 脚本 (init-db.sql) +├── frontend/ # Next.js 前端 │ ├── src/ -│ │ ├── core/ # 核心功能 -│ │ ├── features/ # 功能模块 -│ │ └── shared/ # 共享组件 -│ ├── Dockerfile # 生产环境 -│ ├── Dockerfile.dev # 开发环境 -│ └── Dockerfile.mock # Mock 测试 -│ -├── api/ # API 规范 -│ └── openapi.yaml # OpenAPI 定义 -│ -├── docs/ # 项目文档 -│ ├── features/ # 功能文档 -│ ├── deployment/ # 部署文档 -│ └── development/ # 开发文档 -│ -├── docker-compose.yml # 统一配置(使用 profiles) -└── Makefile # 便捷命令 +│ │ ├── app/ # 页面路由 +│ │ ├── components/ # 组件 +│ │ └── lib/ # 工具库 (API, types, auth) +│ └── .env.local # 前端环境配置 +├── infra/nginx/ # Nginx 配置 +├── docker-compose.yml # 主配置 +├── backend/docker-compose.yml # 后端配置 +└── Makefile # 构建命令 ``` -### 常用命令 +## 核心 API + +| 模块 | 端点 | 说明 | +|------|------|------| +| 认证 | POST /api/v1/auth/login | 用户登录 | +| 用户 | GET/POST /api/v1/users | 用户管理 | +| Workspaces | GET/POST /api/v1/workspaces | 工作空间管理 | +| Clusters | GET/POST /api/v1/clusters | 集群管理 | +| Registries | GET/POST /api/v1/registries | 镜像仓库管理 | +| Storage | GET/POST /api/v1/storage-backends | 存储后端管理 | +| Chart References | GET/POST /api/v1/chart-references | Chart 引用管理 | +| Values Templates | GET/POST /api/v1/values-templates | 配置模板管理 | +| Instances | GET/POST/DELETE /api/v1/instances | 实例部署管理 | + +## 权限模型 + +- **Admin** - 管理员,可管理所有资源和用户 +- **User** - 普通用户,仅可访问所属 Workspace 的资源 + +## 开发命令 ```bash -# Docker 服务(推荐) -make docker-dev # 启动开发环境 -make docker-prod # 启动生产环境 -make docker-test-backend # 测试后端 -make docker-test-frontend # 测试前端 -make docker-logs # 查看日志 -make docker-down # 停止服务 +# 启动开发服务器 +make dev # 同时启动前后端 +make dev-backend # 仅后端 +make dev-frontend # 仅前端 -# OpenAPI 工作流 -make openapi-validate # 验证 API 规范 -make openapi-gen # 生成代码 -make openapi-docs # 生成文档 +# 数据库操作 +make db-init # 初始化数据库 +make db-reset # 重置数据库 +make db-shell # 打开数据库 shell -# 本地开发(不使用 Docker) -make install # 安装依赖 -make dev-local # 启动本地开发 -make test # 运行测试 +# Docker 构建 +make build # 构建所有镜像 +make build-backend # 构建后端镜像 +make build-frontend # 构建前端镜像 + +# 日志和调试 +make logs # 查看所有日志 +make logs-backend # 后端日志 +make stop # 停止开发服务器 ``` -### 开发工作流 +## License -1. **启动开发环境**: - ```bash - make docker-dev - ``` - -2. **修改代码**(自动热重载): - - 后端:编辑 `backend/` 下的 Go 文件 - - 前端:编辑 `frontend/src/` 下的 React 组件 - -3. **查看日志**: - ```bash - make docker-logs - ``` - -4. **测试功能**: - - 前端:http://localhost:5173 - - 后端:http://localhost:8080 - -5. **提交代码**: - ```bash - git add . - git commit -m "feat: add new feature" - git push - ``` - ---- - -## 🧪 测试 - -### 后端测试 - -```bash -# 启动后端 Mock -make docker-test-backend-bg - -# 测试健康检查 -curl http://localhost:8080/health - -# 测试登录 -curl -X POST http://localhost:8080/api/v1/auth/login \ - -H "Content-Type: application/json" \ - -d '{"username":"admin","password":"admin123"}' - -# 测试 API -curl http://localhost:8080/api/v1/registries -curl http://localhost:8080/api/v1/clusters -``` - -### 前端测试 - -```bash -# 启动前端 Mock -make docker-test-frontend-bg - -# 访问前端 -open http://localhost:3000 -``` - -### 集成测试 - -```bash -# 启动完整环境 -make docker-prod - -# 运行测试套件 -make test -``` - ---- - -## 📦 部署 - -### Docker Compose 部署(推荐) - -```bash -# 1. 配置环境变量 -export JWT_SECRET="your-production-secret" -export ENCRYPTION_KEY="your-32-byte-encryption-key" - -# 2. 启动服务 -docker compose up -d - -# 3. 查看状态 -docker compose ps -``` - -### Kubernetes 部署 - -查看 [Kubernetes 部署指南](./docs/deployment/kubernetes-guide.md) - ---- - -## 🤝 贡献 - -欢迎贡献代码!请遵循以下步骤: - -1. Fork 项目 -2. 创建功能分支 (`git checkout -b feature/amazing-feature`) -3. 提交更改 (`git commit -m 'feat: add amazing feature'`) -4. 推送分支 (`git push origin feature/amazing-feature`) -5. 创建 Pull Request - -### 开发规范 - -- **代码风格**:Go (gofmt),TypeScript (ESLint + Prettier) -- **提交规范**:遵循 [Conventional Commits](https://www.conventionalcommits.org/) -- **测试覆盖**:新功能必须包含测试 - ---- - -## 📄 许可证 - -本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情 - ---- - -## 🙏 致谢 - -- [Go](https://go.dev/) - 后端开发语言 -- [React](https://react.dev/) - 前端框架 -- [Vite](https://vitejs.dev/) - 构建工具 -- [Docker](https://www.docker.com/) - 容器化平台 -- [Kubernetes](https://kubernetes.io/) - 容器编排 -- [Harbor](https://goharbor.io/) - OCI Registry - ---- - -## 📞 联系方式 - -- **项目主页**:https://github.com/your-org/ocdp-go -- **问题反馈**:https://github.com/your-org/ocdp-go/issues -- **文档网站**:https://docs.ocdp.example.com - ---- - -
- Built with ❤️ by the OCDP Team -
+MIT \ No newline at end of file diff --git a/START_BACKEND.md b/START_BACKEND.md deleted file mode 100644 index a81bb4c..0000000 --- a/START_BACKEND.md +++ /dev/null @@ -1,127 +0,0 @@ -# 启动和更新后端服务指南 - -## 方式一:使用 Makefile(推荐) - -### 启动完整服务(前端 + 后端 + Nginx) -```bash -make run-2 -``` - -这会: -1. 重新构建前端静态资源 -2. 重新构建后端镜像 -3. 启动 PostgreSQL、Backend 和 Nginx 服务 - -### 停止服务 -```bash -make clean-2 -``` - ---- - -## 方式二:只更新后端服务 - -### 1. 停止服务 -```bash -docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend down -``` - -### 2. 重新构建并启动后端 -```bash -# 设置环境变量(必须!) -export COMPOSE_PROJECT_NAME=ocdp -export ADAPTER_MODE=production -export BACKEND_BUILD_CONTEXT=$(pwd)/backend -export BACKEND_BUILD_DOCKERFILE=$(pwd)/backend/Dockerfile -export BACKEND_MOCK_BUILD_DOCKERFILE=$(pwd)/backend/Dockerfile.mock -export INIT_DB_SQL_PATH=$(pwd)/backend/scripts/init-db.sql - -# 重新构建后端镜像 -docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend build backend - -# 启动服务 -docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend up -d postgres backend nginx -``` - -### 3. 或者使用一行命令(强制重建) -```bash -docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend up -d --build --force-recreate backend -``` - ---- - -## 方式三:只重启后端容器(不重新构建) - -```bash -# 重启后端容器 -docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend restart backend - -# 或者先停止再启动 -docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend stop backend -docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend start backend -``` - ---- - -## 方式四:查看日志 - -```bash -# 查看所有服务日志 -docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend logs -f - -# 只查看后端日志 -docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend logs -f backend -``` - ---- - -## 方式五:开发模式(只启动数据库,本地运行后端) - -```bash -# 只启动 PostgreSQL -docker compose -f backend/docker-compose.yml up -d postgres - -# 然后在本地运行后端 -cd backend -go run cmd/api/main.go -``` - ---- - -## 常用命令速查 - -```bash -# 查看运行中的服务 -docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend ps - -# 查看后端容器状态 -docker ps | grep ocdp-backend - -# 进入后端容器 -docker exec -it ocdp-backend sh - -# 查看后端健康状态 -curl http://localhost:8080/health - -# 停止所有服务 -docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend down - -# 停止并删除数据卷(清理数据) -docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend down -v -``` - ---- - -## 快速更新后端(推荐流程) - -```bash -# 1. 停止服务 -docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend down - -# 2. 重新构建并启动(使用 Makefile) -make run-2 - -# 或者手动执行 -docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend up -d --build -``` - diff --git a/USAGE_GUIDE.md b/USAGE_GUIDE.md deleted file mode 100644 index 4578320..0000000 --- a/USAGE_GUIDE.md +++ /dev/null @@ -1,355 +0,0 @@ -# OCDP 使用指南 - -## 🎯 统一的 docker-compose.yml - -现在所有配置都整合在一个文件中,使用 **profiles** 区分不同运行模式。 - ---- - -## 🚀 快速开始 - -### 方式 1: 使用 Make(推荐) - -```bash -# 开发模式 -make docker-dev - -# 生产模式 -make docker-prod - -# 测试后端 -make docker-test-backend - -# 测试前端 -make docker-test-frontend -``` - -### 方式 2: 使用 Docker Compose - -```bash -# 开发模式 -docker compose --profile dev up - -# 生产模式 -docker compose --profile production up - -# Mock 测试模式(后端) -docker compose --profile mock up backend-mock - -# Mock 测试模式(前端) -docker compose --profile mock up frontend-mock -``` - ---- - -## 📋 三种运行模式 - -### 1. 开发模式(Dev Profile) - -**特点**: -- ✅ 后端 Mock 模式(无需数据库) -- ✅ 热重载(Air + Vite HMR) -- ✅ 快速启动 - -**启动命令**: -```bash -# 前台运行 -docker compose --profile dev up - -# 后台运行 -docker compose --profile dev up -d - -# 或使用 Make -make docker-dev -make docker-dev-bg -``` - -**访问地址**: -- 前端:http://localhost:5173 -- 后端:http://localhost:8080 - -**服务列表**: -- `backend-dev` - 后端开发服务 -- `frontend-dev` - 前端开发服务 - -### 2. 生产模式(Production Profile) - -**特点**: -- ✅ 真实数据库(PostgreSQL + Redis) -- ✅ 完整功能 -- ✅ 生产环境配置 - -**启动命令**: -```bash -# 后台运行 -docker compose --profile production up -d - -# 或使用 Make -make docker-prod -make docker-up -``` - -**访问地址**: -- 前端:http://localhost:3000 -- 后端:http://localhost:8080 -- 数据库:localhost:5432 - -**服务列表**: -- `postgres` - PostgreSQL 数据库 -- `redis` - Redis 缓存 -- `backend-prod` - 后端生产服务 -- `frontend-prod` - 前端生产服务 - -### 3. Mock 测试模式(Mock Profile) - -**特点**: -- ✅ 独立测试单个服务 -- ✅ 无外部依赖 -- ✅ 快速启动 - -**测试后端**: -```bash -# 前台运行 -docker compose --profile mock up backend-mock - -# 后台运行 -docker compose --profile mock up -d backend-mock - -# 或使用 Make -make docker-test-backend -make docker-test-backend-bg -``` - -**测试前端**: -```bash -# 前台运行 -docker compose --profile mock up frontend-mock - -# 后台运行 -docker compose --profile mock up -d frontend-mock - -# 或使用 Make -make docker-test-frontend -make docker-test-frontend-bg -``` - ---- - -## 🛠️ 常用操作 - -### 查看日志 - -```bash -# 查看所有日志 -docker compose logs -f - -# 查看特定服务日志 -docker compose logs -f backend-dev -docker compose logs -f frontend-dev -docker compose logs -f backend-prod -docker compose logs -f backend-mock - -# 或使用 Make -make docker-logs -make docker-logs-backend -make docker-logs-frontend -``` - -### 停止服务 - -```bash -# 停止所有服务 -docker compose down - -# 停止特定 profile 的服务 -docker compose --profile dev down -docker compose --profile production down -docker compose --profile mock down - -# 或使用 Make -make docker-down -``` - -### 重启服务 - -```bash -# 重启所有运行的服务 -docker compose restart - -# 重启特定服务 -docker compose restart backend-dev -docker compose restart frontend-dev - -# 或使用 Make -make docker-restart -make docker-restart-backend -make docker-restart-frontend -``` - -### 构建镜像 - -```bash -# 构建所有镜像 -docker compose build - -# 无缓存构建 -docker compose build --no-cache - -# 或使用 Make -make docker-build -make docker-build-no-cache -``` - -### 查看状态 - -```bash -# 查看运行的服务 -docker compose ps - -# 查看详细状态 -make docker-status -``` - ---- - -## 🔧 开发工具 - -### 启动 pgAdmin 和 Swagger UI - -```bash -# 需要先启动生产模式 -docker compose --profile production --profile tools up -d - -# 或使用 Make -make docker-tools -``` - -**访问地址**: -- pgAdmin:http://localhost:5050 -- Swagger UI:http://localhost:8081 - ---- - -## 📊 服务对比表 - -| 特性 | 开发模式 | 生产模式 | Mock 模式 | -|------|---------|---------|----------| -| **Profile** | `dev` | `production` | `mock` | -| **后端服务** | `backend-dev` | `backend-prod` | `backend-mock` | -| **前端服务** | `frontend-dev` | `frontend-prod` | `frontend-mock` | -| **数据库** | ❌ Mock | ✅ PostgreSQL | ❌ Mock | -| **前端端口** | 5173 | 3000 | 3000 | -| **热重载** | ✅ | ❌ | ❌ | -| **启动时间** | ~15秒 | ~30秒 | ~5秒 | - ---- - -## 💡 使用场景示例 - -### 场景 1: 日常开发 - -```bash -# 1. 启动开发环境 -make docker-dev - -# 2. 修改代码(自动热重载) - -# 3. 查看日志 -make docker-logs - -# 4. 停止 -make docker-down -``` - -### 场景 2: 测试后端 API - -```bash -# 1. 启动后端 Mock -make docker-test-backend-bg - -# 2. 测试 API -curl http://localhost:8080/health -curl http://localhost:8080/api/v1/registries - -# 3. 停止 -docker compose --profile mock down -``` - -### 场景 3: 生产环境部署 - -```bash -# 1. 配置环境变量 -export JWT_SECRET="your-secret" -export ENCRYPTION_KEY="your-32-byte-key" - -# 2. 启动生产环境 -make docker-prod - -# 3. 检查状态 -make docker-status - -# 4. 启动管理工具 -make docker-tools -``` - -### 场景 4: 完全重置 - -```bash -# 1. 停止并删除所有容器和数据 -docker compose down -v - -# 2. 删除镜像 -docker rmi $(docker images | grep ocdp | awk '{print $3}') - -# 3. 重新构建 -make docker-build - -# 4. 重新启动 -make docker-dev -``` - ---- - -## 🎓 环境变量 - -### 开发模式环境变量 - -后端自动使用: -- `ADAPTER_MODE=mock` -- `JWT_SECRET=dev-secret-key` - -### 生产模式环境变量 - -可通过 `.env` 文件或导出环境变量设置: -```bash -export JWT_SECRET="your-production-secret" -export ENCRYPTION_KEY="your-production-encryption-key-32-bytes" -``` - ---- - -## 📚 相关文档 - -- [README.md](./README.md) - 项目概述 -- [DOCKER_SERVICES.md](./DOCKER_SERVICES.md) - 详细架构说明 -- [COMMANDS_CHEATSHEET.md](./COMMANDS_CHEATSHEET.md) - 命令速查表 -- [QUICK_START.md](./QUICK_START.md) - 快速开始指南 - ---- - -## ✨ 优势 - -通过整合到单个 `docker-compose.yml` 文件: - -- ✅ **更简洁**:只需维护一个配置文件 -- ✅ **更清晰**:所有服务定义在同一处 -- ✅ **更灵活**:通过 profiles 轻松切换模式 -- ✅ **更易维护**:减少配置重复 -- ✅ **向后兼容**:Make 命令保持不变 - ---- - -
- 简化配置,提升效率!🚀 -
- diff --git a/backend/BOOTSTRAP-DATA.md b/backend/BOOTSTRAP-DATA.md deleted file mode 100644 index c88a7b1..0000000 --- a/backend/BOOTSTRAP-DATA.md +++ /dev/null @@ -1,452 +0,0 @@ -# Bootstrap 预注入数据说明 - -## 📋 概述 - -Bootstrap 功能在应用启动时自动预注入初始数据,帮助快速搭建开发/测试环境。 - -**配置文件**: `config/bootstrap.json` - ---- - -## 🔧 预注入数据内容 - -### 1️⃣ 用户 (Users) - -预注入 **1 个管理员账户**: - -| 字段 | 值 | 说明 | -|------|-----|------| -| **username** | `admin` | 管理员用户名 | -| **password** | `admin123` | 初始密码(⚠️ 生产环境请修改) | -| **email** | `admin@example.com` | 邮箱地址 | - -**用途**: -- 登录后台管理系统 -- 测试用户认证功能 -- 管理集群和 Registry - -**密码加密**: 使用 bcrypt 加密存储 - ---- - -### 2️⃣ Registry (OCI 镜像仓库) - -预注入 **1 个 Harbor Registry**: - -| 字段 | 值 | 说明 | -|------|-----|------| -| **name** | `Harbor Production` | Registry 名称 | -| **url** | `https://harbor.example.com` | Registry 地址 | -| **description** | `Production Harbor Registry` | 描述 | -| **username** | `admin` | Registry 用户名 | -| **password** | `Harbor12345` | Registry 密码(加密存储) | -| **insecure** | `false` | 是否跳过 SSL 验证 | - -**用途**: -- 浏览 Helm Chart 制品 -- 拉取 OCI Artifacts -- 测试 Registry 连接 - -**密码加密**: 使用 AES 加密存储(基于 `ENCRYPTION_KEY` 环境变量) - ---- - -### 3️⃣ Kubernetes 集群 (Clusters) - -预注入 **1 个测试集群**: - -| 字段 | 值 | 说明 | -|------|-----|------| -| **name** | `Test Cluster` | 集群名称 | -| **host** | `https://kubernetes.example.com:6443` | Kubernetes API Server 地址 | -| **description** | `Test Kubernetes Cluster` | 描述 | -| **caData** | `LS0tLS1CRUdJTi1D...` | CA 证书(Base64 编码) | -| **certData** | `LS0tLS1CRUdJTi1D...` | 客户端证书(Base64 编码) | -| **keyData** | `LS0tLS1CRUdJTi1S...` | 客户端密钥(Base64 编码) | - -**用途**: -- 部署 Helm Chart 应用 -- 查看集群状态和资源 -- 测试 Kubernetes 集成 - -**证书加密**: CA/Cert/Key 使用 AES 加密存储 - ---- - -## 🎯 Bootstrap 模式 - -### Mock 模式 (run-0) -- ✅ Bootstrap 启用 -- ✅ 数据存储在内存中 -- ✅ 重启后数据重置 - -### 真实模式 (run-1, run-2) -- ✅ Bootstrap 启用 -- ✅ 数据存储在 PostgreSQL -- ✅ 重启后数据持久化 -- ⚠️ **避免重复**: 如果数据已存在会跳过创建 - ---- - -## 📝 配置文件 - -### 完整配置示例 (`config/bootstrap.json`) - -```json -{ - "enabled": true, - "users": [ - { - "username": "admin", - "password": "admin123", - "email": "admin@example.com" - } - ], - "registries": [ - { - "name": "Harbor Production", - "url": "https://harbor.example.com", - "description": "Production Harbor Registry", - "username": "admin", - "password": "Harbor12345", - "insecure": false - } - ], - "clusters": [ - { - "name": "Test Cluster", - "host": "https://kubernetes.example.com:6443", - "description": "Test Kubernetes Cluster", - "caData": "LS0tLS1CRUdJTi1DRVJUSUZJQ0FURS0tLS0t...", - "certData": "LS0tLS1CRUdJTi1DRVJUSUZJQ0FURS0tLS0t...", - "keyData": "LS0tLS1CRUdJTi1SU0EgUFJJVkFURSBLRVktLS0tLQ==" - } - ] -} -``` - ---- - -## ⚙️ 自定义配置 - -### 方式 1: 修改配置文件 - -```bash -# 编辑配置 -vim config/bootstrap.json - -# 重启应用 -make run-1 -``` - -### 方式 2: 通过环境变量 - -```bash -export BOOTSTRAP_CONFIG_JSON='{ - "enabled": true, - "users": [ - {"username": "myuser", "password": "mypass", "email": "user@example.com"} - ], - "registries": [], - "clusters": [] -}' - -make run-1 -``` - -### 方式 3: 指定配置文件路径 - -```bash -export BOOTSTRAP_CONFIG_FILE=/path/to/custom-bootstrap.json -make run-1 -``` - ---- - -## 🔒 安全建议 - -### ⚠️ 生产环境注意事项 - -1. **修改默认密码** - ```json - { - "users": [ - { - "username": "admin", - "password": "YourStrongPasswordHere" // ⚠️ 修改 - } - ] - } - ``` - -2. **设置强加密密钥** - ```bash - # 生成 32 字节随机密钥 - export ENCRYPTION_KEY=$(openssl rand -base64 32) - ``` - -3. **使用真实证书** - - 替换 `caData`, `certData`, `keyData` 为真实集群证书 - - 确保证书有效期和权限正确 - -4. **禁用 Bootstrap(可选)** - ```json - { - "enabled": false - } - ``` - -5. **删除配置文件** - ```bash - # 首次启动后删除(数据已导入) - rm config/bootstrap.json - ``` - ---- - -## 🧪 测试验证 - -### 验证用户 - -```bash -# 登录测试 -curl -X POST http://localhost:8080/api/v1/auth/login \ - -H "Content-Type: application/json" \ - -d '{ - "username": "admin", - "password": "admin123" - }' - -# 预期返回: {"token": "eyJhbGc..."} -``` - -### 验证 Registry - -```bash -# 查看 Registry 列表 -curl http://localhost:8080/api/v1/registries - -# 预期返回: -# [ -# { -# "id": "...", -# "name": "Harbor Production", -# "url": "https://harbor.example.com" -# } -# ] -``` - -### 验证 Cluster - -```bash -# 查看集群列表 -curl http://localhost:8080/api/v1/clusters - -# 预期返回: -# [ -# { -# "id": "...", -# "name": "Test Cluster", -# "host": "https://kubernetes.example.com:6443" -# } -# ] -``` - ---- - -## 📊 启动日志示例 - -### 成功的 Bootstrap 日志 - -``` -🌱 Starting bootstrap seeding... - ↳ Seeding 1 user(s)... - ✓ User 'admin' created - ↳ Seeding 1 registry(ies)... - ✓ Registry 'Harbor Production' created (credentials encrypted) - ↳ Seeding 1 cluster(s)... - ✓ Cluster 'Test Cluster' created (credentials encrypted) -✅ Bootstrap seeding completed -``` - -### 数据已存在的日志 - -``` -🌱 Starting bootstrap seeding... - ↳ Seeding 1 user(s)... - ⊙ User 'admin' already exists, skipping - ↳ Seeding 1 registry(ies)... - ⊙ Registry 'Harbor Production' already exists, skipping - ↳ Seeding 1 cluster(s)... - ⊙ Cluster 'Test Cluster' already exists, skipping -✅ Bootstrap seeding completed -``` - ---- - -## 🔄 重置数据 - -### 重置 Mock 模式数据 - -```bash -# Mock 数据存储在内存,重启即重置 -make run-0 -# Ctrl+C -make run-0 -``` - -### 重置真实数据库数据 - -```bash -# 清理并重新创建 -make clean-1 -make run-1 -``` - ---- - -## 📖 更多示例 - -### 添加多个用户 - -```json -{ - "enabled": true, - "users": [ - { - "username": "admin", - "password": "admin123", - "email": "admin@example.com" - }, - { - "username": "developer", - "password": "dev123", - "email": "dev@example.com" - }, - { - "username": "operator", - "password": "ops123", - "email": "ops@example.com" - } - ] -} -``` - -### 添加多个 Registry - -```json -{ - "registries": [ - { - "name": "Harbor Production", - "url": "https://harbor.example.com", - "username": "admin", - "password": "password1" - }, - { - "name": "Docker Hub", - "url": "https://registry-1.docker.io", - "username": "myuser", - "password": "password2" - }, - { - "name": "GitHub Container Registry", - "url": "https://ghcr.io", - "username": "github-user", - "password": "ghp_token" - } - ] -} -``` - -### 添加多个集群 - -```json -{ - "clusters": [ - { - "name": "Dev Cluster", - "host": "https://dev-k8s.example.com:6443", - "description": "Development Environment", - "caData": "...", - "certData": "...", - "keyData": "..." - }, - { - "name": "Staging Cluster", - "host": "https://staging-k8s.example.com:6443", - "description": "Staging Environment", - "caData": "...", - "certData": "...", - "keyData": "..." - }, - { - "name": "Production Cluster", - "host": "https://prod-k8s.example.com:6443", - "description": "Production Environment", - "caData": "...", - "certData": "...", - "keyData": "..." - } - ] -} -``` - ---- - -## 🛠️ 故障排查 - -### 问题 1: Bootstrap 不生效 - -**症状**: 启动后没有预注入数据 - -**检查**: -```bash -# 1. 检查配置文件是否存在 -ls -la config/bootstrap.json - -# 2. 检查 enabled 是否为 true -cat config/bootstrap.json | jq .enabled - -# 3. 查看启动日志 -# 应该看到 "Starting bootstrap seeding..." -``` - -### 问题 2: 密码不正确 - -**症状**: 无法使用预注入的用户登录 - -**原因**: 密码在配置文件中可能已修改 - -**解决**: -```bash -# 查看配置文件中的密码 -cat config/bootstrap.json | jq '.users[0].password' - -# 使用正确的密码测试 -curl -X POST http://localhost:8080/api/v1/auth/login \ - -d '{"username":"admin","password":"配置文件中的密码"}' -``` - -### 问题 3: 重复创建报错 - -**症状**: 日志显示 "duplicate key" 错误 - -**原因**: 数据库中已存在同名记录 - -**解决**: -- 正常现象,Bootstrap 会自动跳过 -- 如需重新创建,使用 `make clean-1` 清理数据库 - ---- - -## 📚 相关文档 - -- **配置示例**: `config/bootstrap.example.json` -- **代码实现**: `internal/bootstrap/seeder.go` -- **架构文档**: `docs/architecture.md` - Bootstrap 预注入章节 - ---- - -**最后更新**: 2025-11-10 - diff --git a/backend/CODE_FIRST_GUIDE.md b/backend/CODE_FIRST_GUIDE.md deleted file mode 100644 index 0052348..0000000 --- a/backend/CODE_FIRST_GUIDE.md +++ /dev/null @@ -1,324 +0,0 @@ -# Code First API 开发指南 - -## 🎯 概述 - -本项目现已支持 **Code First** 方式开发 API: -1. **后端**:使用 Swagger 注释从代码生成 OpenAPI 文档 -2. **前端**:使用 Orval 从 OpenAPI 文档生成 TypeScript 客户端 - ---- - -## 🛠️ 工具链 - -### 后端 -- **swaggo/swag**: 从 Go 代码注释生成 OpenAPI/Swagger 文档 -- 文档:https://github.com/swaggo/swag - -### 前端 -- **Orval**: 从 OpenAPI 规范生成 TypeScript API 客户端 -- 文档:https://orval.dev/ - ---- - -## 📝 开发流程 - -### 1. 后端:添加 Swagger 注释 - -#### 主程序注释 (cmd/api/main.go) -```go -// @title OCDP Backend API -// @version 1.0 -// @description OCDP (Open Cloud Development Platform) Backend API -// -// @host localhost:8080 -// @BasePath /api/v1 -// -// @securityDefinitions.apikey BearerAuth -// @in header -// @name Authorization -package main -``` - -#### Handler 方法注释示例 -```go -// ListArtifacts 列出 repository 中的所有 artifacts -// @Summary 列出 Repository 中的所有 Artifacts -// @Description 列出指定 Repository 中的所有 Artifact,支持按类型过滤 -// @Tags Artifacts -// @Accept json -// @Produce json -// @Param registry_id path string true "Registry ID" -// @Param repository_name path string true "Repository Name (URL encoded)" -// @Param media_type query string false "过滤类型 (all, chart, image, other)" default(all) -// @Success 200 {array} dto.TagResponse -// @Failure 500 {object} dto.ErrorResponse -// @Router /registries/{registry_id}/repositories/{repository_name}/artifacts [get] -func (h *ArtifactHandler) ListArtifacts(w http.ResponseWriter, r *http.Request) { - // ... -} -``` - -### 2. 生成 OpenAPI 文档 - -```bash -# 在 backend 目录下运行 -cd backend -swag init -g cmd/api/main.go -o docs/swagger --parseDependency --parseInternal - -# 生成的文件在 docs/swagger/ 目录: -# - swagger.json -# - swagger.yaml -# - docs.go -``` - -### 3. 前端:重新生成 API 客户端 - -```bash -# 在 frontend 目录下运行 -cd frontend -npm run openapi-gen - -# 或者 -orval -``` - -生成的文件在 `src/api/generated-orval/` 目录。 - ---- - -## 🔄 完整工作流 - -### 添加新 API 的步骤 - -#### 1. 后端开发 -```bash -# 1.1 创建 DTO -# internal/adapter/input/http/dto/new_feature_dto.go - -# 1.2 创建 Handler 并添加 Swagger 注释 -# internal/adapter/input/http/rest/new_feature_handler.go - -# 1.3 注册路由 -# cmd/api/main.go - -# 1.4 生成 OpenAPI 文档 -cd backend -swag init -g cmd/api/main.go -o docs/swagger --parseDependency --parseInternal - -# 1.5 复制到主文档 (可选) -cp docs/swagger/swagger.yaml docs/openapi.yaml -``` - -#### 2. 前端开发 -```bash -# 2.1 重新生成 API 客户端 -cd frontend -npm run openapi-gen - -# 2.2 使用生成的客户端 -import { listArtifacts } from '@/api/generated-orval/api'; - -const data = await listArtifacts({ - registryId: 'xxx', - repositoryName: 'charts/nginx' -}); -``` - ---- - -## 📋 Swagger 注释速查 - -### 常用标签 - -| 标签 | 说明 | 示例 | -|------|------|------| -| `@Summary` | 简短描述 | `@Summary 列出所有用户` | -| `@Description` | 详细描述 | `@Description 返回系统中的所有用户列表` | -| `@Tags` | API 分组 | `@Tags Users` | -| `@Accept` | 接受的格式 | `@Accept json` | -| `@Produce` | 返回的格式 | `@Produce json` | -| `@Param` | 参数 | `@Param id path string true "User ID"` | -| `@Success` | 成功响应 | `@Success 200 {object} dto.UserResponse` | -| `@Failure` | 失败响应 | `@Failure 404 {object} dto.ErrorResponse` | -| `@Router` | 路由定义 | `@Router /users/{id} [get]` | -| `@Security` | 安全认证 | `@Security BearerAuth` | - -### 参数类型 - -```go -// 路径参数 (path) -@Param id path string true "User ID" - -// 查询参数 (query) -@Param page query int false "Page number" default(1) -@Param size query int false "Page size" default(20) - -// 请求体 (body) -@Param request body dto.CreateUserRequest true "User data" - -// Header 参数 (header) -@Param X-Request-ID header string false "Request ID" -``` - -### 响应定义 - -```go -// 单个对象 -@Success 200 {object} dto.UserResponse - -// 数组 -@Success 200 {array} dto.UserResponse - -// 自定义响应 -@Success 200 {object} map[string]interface{} - -// 多个可能的响应 -@Success 200 {object} dto.UserResponse "Success" -@Success 201 {object} dto.UserResponse "Created" -@Failure 400 {object} dto.ErrorResponse "Bad Request" -@Failure 404 {object} dto.ErrorResponse "Not Found" -@Failure 500 {object} dto.ErrorResponse "Internal Error" -``` - ---- - -## 🎯 最佳实践 - -### 1. 命名规范 - -| 层级 | 风格 | 示例 | -|------|------|------| -| **路径变量** | snake_case | `{registry_id}`, `{repository_name}` | -| **查询参数** | snake_case | `?media_type=chart` | -| **JSON 字段** | camelCase | `repositoryName`, `mediaType` | -| **Go 结构体** | PascalCase | `RepositoryName`, `MediaType` | - -### 2. DTO 定义示例 - -```go -// ArtifactResponse Artifact 响应 -type ArtifactResponse struct { - RepositoryName string `json:"repositoryName"` // Go: PascalCase, JSON: camelCase - Tag string `json:"tag"` - Type string `json:"type"` - Size int64 `json:"size"` - CreatedAt string `json:"createdAt"` -} -``` - -### 3. 完整的 Handler 示例 - -```go -// GetArtifact 获取 artifact 详情 -// @Summary 获取 Artifact 详情 -// @Description 获取指定 Artifact 的详细信息 -// @Tags Artifacts -// @Accept json -// @Produce json -// @Param registry_id path string true "Registry ID" -// @Param repository_name path string true "Repository Name (URL encoded)" -// @Param reference path string true "Artifact Reference (tag or digest)" -// @Success 200 {object} dto.ArtifactResponse -// @Failure 404 {object} dto.ErrorResponse "Artifact not found" -// @Failure 500 {object} dto.ErrorResponse "Internal server error" -// @Router /registries/{registry_id}/repositories/{repository_name}/artifacts/{reference} [get] -func (h *ArtifactHandler) GetArtifact(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - registryID := vars["registry_id"] - repositoryName := vars["repository_name"] - reference := vars["reference"] - - artifact, err := h.artifactService.GetArtifact(r.Context(), registryID, repositoryName, reference) - if err != nil { - respondError(w, http.StatusNotFound, "Artifact not found", err.Error()) - return - } - - response := &dto.ArtifactResponse{ - RepositoryName: artifact.Repository, - Tag: artifact.Tag, - Digest: artifact.Digest, - Type: string(artifact.Type), - Size: artifact.Size, - CreatedAt: artifact.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), - } - - respondJSON(w, http.StatusOK, response) -} -``` - ---- - -## 🔧 故障排除 - -### 问题 1: swag 命令未找到 -```bash -# 解决方案:安装 swag -go install github.com/swaggo/swag/cmd/swag@latest - -# 确保 $GOPATH/bin 在 PATH 中 -export PATH=$PATH:$(go env GOPATH)/bin -``` - -### 问题 2: 生成的文档不完整 -```bash -# 确保使用了正确的参数 -swag init -g cmd/api/main.go -o docs/swagger --parseDependency --parseInternal - -# 检查注释格式是否正确 -# 注释必须紧邻函数定义,中间不能有空行 -``` - -### 问题 3: 前端客户端生成失败 -```bash -# 检查 OpenAPI 文档是否有效 -cd frontend -npx @apidevtools/swagger-cli validate ../backend/docs/openapi.yaml - -# 清理并重新生成 -rm -rf src/api/generated-orval -npm run openapi-gen -``` - ---- - -## 📚 参考资源 - -- **Swaggo 文档**: https://github.com/swaggo/swag -- **Swaggo 声明式注释**: https://github.com/swaggo/swag#declarative-comments-format -- **Orval 文档**: https://orval.dev/ -- **OpenAPI 规范**: https://swagger.io/specification/ - ---- - -## ✅ 当前状态 - -### 已完成 -- [x] 安装 swag 工具 -- [x] 添加主程序 Swagger 注释 -- [x] 为 Artifact Handler 添加完整注释 -- [x] 生成 OpenAPI 文档 -- [x] 前端配置 Orval -- [x] 重新生成前端客户端 - -### 待完成 (可选) -- [ ] 为其他 Handler 添加 Swagger 注释 - - [ ] Auth Handler - - [ ] Cluster Handler - - [ ] Registry Handler - - [ ] Instance Handler - - [ ] Monitoring Handler -- [ ] 添加更详细的错误响应定义 -- [ ] 添加请求/响应示例 -- [ ] 配置 CI/CD 自动生成文档 - ---- - -## 🎉 总结 - -现在项目支持 Code First 开发流程: -1. **后端开发者**:写代码 + 注释 → 生成 OpenAPI -2. **前端开发者**:使用 OpenAPI → 生成 TypeScript 客户端 -3. **文档自动同步**:代码即文档,保证一致性 - -这确保了 API 文档始终与代码保持同步!✨ diff --git a/backend/DEVELOPMENT.md b/backend/DEVELOPMENT.md deleted file mode 100644 index 6311c8f..0000000 --- a/backend/DEVELOPMENT.md +++ /dev/null @@ -1,223 +0,0 @@ -# 开发环境快速指南 - -## 📋 前置要求 - -- Go 1.21+ -- Docker & Docker Compose -- Air(热加载工具): `go install github.com/cosmtrek/air@latest` - -## 🎯 设计理念 - -本项目使用 **Docker Compose Profile** 机制,通过单一的 `docker-compose.yml` 文件支持三种运行模式,避免配置文件重复。 - -## 🚀 三种运行模式 - -### 模式 0: Mock 模式(最快) - -纯本地运行,无需任何外部依赖,所有服务都是 Mock。 - -```bash -make run-0 -``` - -**特点:** -- ✅ 启动最快(秒启动) -- ✅ 无需 Docker -- ✅ 支持热加载 -- ✅ 适合快速功能开发 - -**停止:** `Ctrl+C` - ---- - -### 模式 1: 开发模式(推荐) - -Docker 提供真实的 PostgreSQL,后端在本地运行并支持热加载。 - -```bash -make run-1 -``` - -**特点:** -- ✅ 真实的数据库 -- ✅ 支持热加载 -- ✅ 代码修改实时生效 -- ✅ 适合日常开发 - -**停止:** `Ctrl+C` 停止后端(数据库容器继续运行) - -**清理:** `make clean-1` 清理数据库和临时文件 - ---- - -### 模式 2: 生产模式 - -所有服务完全容器化,模拟生产环境。 - -```bash -make run-2 -``` - -**特点:** -- ✅ 完全容器化 -- ✅ 接近生产环境 -- ✅ 后台运行 -- ❌ 无热加载 - -**查看日志:** -```bash -docker compose --profile backend logs -f -``` - -**停止:** -```bash -docker compose --profile backend down -``` - -**清理:** `make clean-2` 清理所有容器和构建产物 - ---- - -## 🧹 清理命令 - -```bash -# 清理模式 1 的产物(依赖容器 + 临时文件) -make clean-1 - -# 清理模式 2 的产物(所有容器 + 构建产物) -make clean-2 -``` - ---- - -## 📊 常用工作流 - -### 日常开发 - -```bash -# 启动开发环境 -make run-1 - -# ... 编码、测试 ... -# 代码修改会自动重新编译 - -# 下班停止(Ctrl+C) -# 第二天继续(依赖容器还在运行,直接启动) -make run-1 -``` - -### 重置数据库 - -```bash -# 清理并重新开始 -make clean-1 -make run-1 -``` - -### 测试生产部署 - -```bash -# 启动生产模式 -make run-2 - -# 查看日志 -docker compose -f docker-compose.prod.yml logs -f - -# 测试完成后清理 -make clean-2 -``` - ---- - -## 🔧 环境变量配置 - -复制并修改环境变量配置: - -```bash -cp env.example .env -``` - -主要配置项: - -- `ADAPTER_MODE`: 设置为 `mock` 启用 Mock 模式(模式 0) -- `POSTGRES_*`: 数据库配置(模式 1、2) -- `JWT_SECRET`: JWT 密钥(生产环境必须修改) -- `ENCRYPTION_KEY`: 加密密钥(必须 32 字节) - ---- - -## 📂 文件说明 - -| 文件 | 用途 | -|------|------| -| `Makefile` | 开发命令入口(5个核心命令) | -| `.air.toml` | 热加载配置 | -| `docker-compose.yml` | 统一配置文件(使用 profile 区分模式) | -| `Dockerfile` | 生产镜像构建配置 | -| `DEVELOPMENT.md` | 本文档 | - -### Docker Compose Profile 说明 - -- **无 profile**: 只启动 postgres(用于模式 1) -- **--profile backend**: 启动 postgres + backend(用于模式 2) -- **--profile mock**: 启动 backend-mock(独立的 mock 模式容器) - ---- - -## ❓ 常见问题 - -### Q: 模式 1 启动失败,提示端口被占用? - -A: 检查是否有其他 PostgreSQL 实例在运行: -```bash -docker ps | grep postgres -# 或者修改 .env 中的 POSTGRES_PORT -``` - -### Q: 如何查看依赖服务状态? - -A: -```bash -docker compose ps # 模式 1(只有 postgres) -docker compose --profile backend ps # 模式 2(postgres + backend) -``` - -### Q: 模式 1 和模式 2 的数据库数据会互相影响吗? - -A: 会共享同一个数据库容器和数据卷 `ocdp-postgres-data`。 -- 模式 1: 使用本地 air 连接到 postgres 容器 -- 模式 2: 使用 backend 容器连接到 postgres 容器 -- 数据是持久化的,切换模式不会丢失数据 - -### Q: 如何完全清理所有 Docker 资源? - -A: -```bash -make clean-1 -make clean-2 -docker system prune -a --volumes -``` - ---- - -## 💡 提示 - -- **首次使用**: 推荐从 `make run-0` 开始,快速验证环境 -- **日常开发**: 使用 `make run-1`,享受热加载和真实数据库 -- **部署前测试**: 使用 `make run-2`,确保容器化部署没问题 -- **数据持久化**: 模式 1 和 2 的数据库数据都会持久化,`Ctrl+C` 不会丢失数据 -- **完全重置**: 使用 `clean-*` 命令会删除数据卷,数据会丢失 - ---- - -## 🎯 快速参考 - -```bash -make # 显示帮助 -make run-0 # Mock 模式 -make run-1 # 开发模式 -make run-2 # 生产模式 -make clean-1 # 清理开发环境 -make clean-2 # 清理生产环境 -``` - diff --git a/backend/QUICK-REFERENCE.md b/backend/QUICK-REFERENCE.md deleted file mode 100644 index 1e64064..0000000 --- a/backend/QUICK-REFERENCE.md +++ /dev/null @@ -1,133 +0,0 @@ -# 快速参考卡片 - -## 🚀 五个核心命令 - -```bash -make run-0 # Mock 模式(秒启动,无需 Docker) -make run-1 # 开发模式(Docker 依赖 + 本地热加载) -make run-2 # 生产模式(全部容器化) -make clean-1 # 清理开发环境 -make clean-2 # 清理生产环境 -``` - ---- - -## 📋 模式对比 - -| 特性 | run-0 | run-1 | run-2 | -|------|-------|-------|-------| -| **后端位置** | 本地 | 本地 | 容器 | -| **数据库** | Mock | 容器 | 容器 | -| **热加载** | ✅ | ✅ | ❌ | -| **启动速度** | ⚡ 秒启 | 🔥 5秒 | 🐳 30秒 | -| **适用场景** | 快速开发 | 日常开发 | 部署测试 | -| **依赖 Docker** | ❌ | ✅ | ✅ | - ---- - -## 🎯 使用场景 - -### 场景 1: 快速功能开发 -```bash -make run-0 -# 修改代码,自动重新编译 -# Ctrl+C 停止 -``` - -### 场景 2: 日常开发(推荐) -```bash -make run-1 -# 修改代码,自动重新编译 -# Ctrl+C 停止后端(数据库继续运行) - -# 第二天继续 -make run-1 # 数据库还在,直接启动 - -# 重置数据库 -make clean-1 -make run-1 -``` - -### 场景 3: 部署前测试 -```bash -make run-2 -# 查看日志 -docker compose --profile backend logs -f - -# 测试完成 -make clean-2 -``` - ---- - -## 🔧 Docker Compose 直接使用 - -### 启动依赖(run-1 等价) -```bash -docker compose up -d postgres -go run cmd/api/main.go -``` - -### 启动完整环境(run-2 等价) -```bash -docker compose --profile backend up -d -``` - -### 查看状态 -```bash -docker compose ps # run-1 -docker compose --profile backend ps # run-2 -``` - -### 停止 -```bash -docker compose down # run-1 -docker compose --profile backend down # run-2 -``` - ---- - -## 📂 关键文件 - -| 文件 | 说明 | -|------|------| -| `Makefile` | 5个核心命令 | -| `docker-compose.yml` | 使用 profile 的统一配置 | -| `.air.toml` | 热加载配置 | -| `env.example` | 环境变量模板 | - ---- - -## 🐳 Docker Compose Profile 原理 - -```yaml -# docker-compose.yml -services: - postgres: - # 默认启动(无需 profile) - - backend: - profiles: [backend] # 需要 --profile backend -``` - -**使用方式**: -```bash -docker compose up -d # 只启动 postgres -docker compose --profile backend up -d # 启动 postgres + backend -``` - ---- - -## 💡 快速提示 - -- 首次使用运行: `go install github.com/cosmtrek/air@latest` -- 查看帮助: `make` 或 `make help` -- 完整文档: 查看 `DEVELOPMENT.md` -- 审查报告: 查看 `REVIEW.md` - ---- - -## 🎯 一句话总结 - -**三种模式,五个命令,一个 Docker Compose 文件。** - diff --git a/backend/README.md b/backend/README.md index e0d47b2..c04ce9d 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,343 +1,89 @@ # OCDP Backend -基于 Go 的 Kubernetes Helm Chart 管理服务后端,提供完整的制品浏览和应用部署能力。 +Go 后端服务,提供 Kubernetes Helm Chart 部署能力。 -## ✨ 特性 - -- 🎪 **Helm Chart 管理** - 完整的 Helm 生命周期支持 -- 📦 **多 Registry 支持** - Harbor、Docker Hub、GHCR 等 -- 🔍 **Artifact 浏览** - 自动识别类型、大小、metadata -- 🚀 **OCI 标准兼容** - 使用 ORAS Go SDK v2 -- 🖥️ **多集群支持** - 管理多个 Kubernetes 集群 -- 🔐 **认证支持** - JWT + 密码加密 -- 📊 **实时状态** - Helm Release 状态监控 -- 🏗️ **六边形架构** - 清晰的分层设计,易于测试和扩展 -- 🔄 **双模式支持** - Mock 模式(开发调试)+ 默认模式(真实 PostgreSQL) - -## 🚀 快速开始 - -### 方式 1: Mock 模式(最快) - -适合快速功能开发和 API 测试,无需数据库。 - -```bash -# 安装 Air(首次) -go install github.com/air-verse/air@latest - -# 启动 Mock 模式 -make dev-mock -``` - -### 方式 2: 本地 Backend + Docker 数据库(推荐日常开发) - -支持数据持久化和热重载。 - -```bash -# 启动数据库 -docker compose up -d postgres - -# 启动 Backend -make dev -``` - -### 方式 3: 完全容器化(生产部署) - -适合生产环境和 CI/CD。 - -```bash -# 启动完整服务 -make prod - -# 或 -docker compose --profile backend up -d -``` - -### 本地运行 - -```bash -# Mock 模式(快速测试) -make run-mock - -# 或 -export ADAPTER_MODE=mock -go run cmd/api/main.go - -# Production 模式(需要数据库) -make run-prod -``` - -### 验证服务 - -```bash -# 健康检查 -curl http://localhost:8080/health - -# 查看 API -curl http://localhost:8080/api/v1/registries | jq - -# 访问 Swagger UI (交互式 API 文档) -open http://localhost:8080/api/docs -``` - -## 📚 文档 - -| 文档 | 说明 | -|------|------| -| [快速开始](QUICK-START.md) | **快速开始指南** - 3分钟上手 ⭐ | -| [命令速查表](COMMANDS.md) | **Make 命令参考** - 所有命令详解 ⭐ | -| [部署指南](DEPLOYMENT-GUIDE.md) | **完整部署指南** - Mock 和默认模式 | -| [架构文档](docs/architecture.md) | 六边形架构、目录结构、开发指南 | -| [OpenAPI 规范](docs/openapi.yaml) | **OpenAPI 3.0 规范** - 标准 API 定义 | -| [Swagger UI](http://localhost:8080/api/docs) | **交互式 API 文档** - 在线测试 API 🚀 | -| [API 与测试](docs/api-and-test.md) | REST API 参考文档 + 测试指南 | - -### 🎯 API 文档使用指南 - -**方式 1: Swagger UI (推荐)** ⭐ - -启动服务后访问:[http://localhost:8080/api/docs](http://localhost:8080/api/docs) - -特性: -- 📖 交互式文档 - 所有 API 可视化展示 -- 🔧 在线测试 - 直接在浏览器中测试 API -- 🔐 认证支持 - 支持 JWT Token 认证 -- 📦 Schema 查看 - 查看所有请求/响应模型 - -**方式 2: OpenAPI 规范文件** - -```bash -# 查看规范文件 -cat docs/openapi.yaml - -# 使用 OpenAPI 工具生成客户端 -openapi-generator-cli generate -i docs/openapi.yaml -g go -o ./client - -# 在线验证 -curl http://localhost:8080/api/docs/openapi.yaml -``` - -**方式 3: Markdown 文档** - -查看 [docs/api-and-test.md](docs/api-and-test.md) - 完整的 API 参考文档 - -## 🏗️ 架构概览 - -采用**六边形架构**(Hexagonal Architecture),实现清晰的分层和依赖倒置: - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Input Adapters │ -│ (HTTP REST API) │ -└──────────────────────┬──────────────────────────────────────┘ - │ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ Domain Layer │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ Entities │ │ Services │ │ Interfaces │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -└──────────────────────┬──────────────────────────────────────┘ - │ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ Output Adapters │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ Database │ │ OCI Client │ │ Helm Client │ │ -│ │ (Mock/Prod) │ │ (Mock/ORAS) │ │ (Mock/Real) │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -详见 [架构文档](docs/architecture.md) - -## 🎯 核心 API - -| 分类 | 端点 | 说明 | -|------|------|------| -| **认证** | `POST /api/v1/auth/login` | 用户登录 | -| **集群** | `GET /api/v1/clusters` | 列出集群 | -| | `POST /api/v1/clusters` | 创建集群 | -| **Registry** | `GET /api/v1/registries` | 列出 Registry | -| | `POST /api/v1/registries` | 创建 Registry | -| **Artifact** | `GET /api/v1/registries/{id}/repositories` | 列出仓库 | -| | `GET /api/v1/registries/{id}/repositories/{repo}/artifacts` | 列出制品 | -| **实例** | `POST /api/v1/clusters/{id}/instances` | 安装应用 | -| | `GET /api/v1/clusters/{id}/instances` | 列出实例 | -| | `PUT /api/v1/clusters/{id}/instances/{instanceId}` | 升级应用 | -| | `DELETE /api/v1/clusters/{id}/instances/{instanceId}` | 卸载应用 | -| | `GET /api/v1/clusters/{id}/instances/{instanceId}/entries` | 查看实例入口 | -| **监控** | `GET /api/v1/monitoring/summary` | 监控摘要 | - -完整 API 文档: [docs/api.md](docs/api.md) - -## 🔧 开发 - -### 环境要求 +## 技术栈 - Go 1.21+ -- PostgreSQL 15+ (生产模式) -- Docker & Docker Compose (可选) +- gorilla/mux (HTTP 路由) +- ORAS Go SDK v2 (OCI 操作) +- Helm SDK (Helm 操作) +- Kubernetes client-go +- PostgreSQL -### 常用命令 +## 启动 ```bash -# 查看所有命令 -make help +# Mock 模式(无需数据库,无需外部服务) +ADAPTER_MODE=mock go run cmd/api/main.go -# 开发 -make dev # 开发模式(热重载) -make build # 构建 -make run-mock # Mock 模式运行 -make run-prod # Production 模式运行 +# 生产模式(需要 PostgreSQL + K8s/Harbor 连接验证) +# 启动 PostgreSQL +docker compose up -d postgres -# Docker Compose -make mock # Mock 模式 -make prod # 生产模式 -make logs # 查看日志 -make status # 查看状态 -make stop # 停止服务 +# 启动后端(生产模式) +cd backend +export DATABASE_URL="postgres://postgres:postgres@localhost:5432/ocdp?sslmode=disable" +export JWT_SECRET="your-jwt-secret" +export ENCRYPTION_KEY="your-32-byte-encryption-key" +export PORT=8081 +export ADAPTER_MODE=production +export KUBECONFIG=/home/ivanwu/.kube/config # 或你的 kubeconfig 路径 -# 数据库 -make db-up # 启动数据库 -make db-psql # 连接数据库 -make db-backup # 备份数据库 -make pgadmin # 启动 pgAdmin +# Harbor 凭证(可选,用于验证 Registry 连接) +export HARBOR_URL="https://harbor.bwgdi.com" +export HARBOR_USERNAME="your-harbor-user" +export HARBOR_PASSWORD="your-harbor-password" + +# NFS 配置(可选) +export NFS_SERVER="10.6.80.11" +export NFS_SHARE="/volume1/NFS" + +go run cmd/api/main.go ``` -### 项目结构 +## 环境变量说明 + +| 变量 | 必需 | 说明 | +|------|------|------| +| DATABASE_URL | 是 | PostgreSQL 连接字符串 | +| JWT_SECRET | 是 | JWT 签名密钥 | +| ENCRYPTION_KEY | 是 | 32位加密密钥 | +| PORT | 否 | 服务端口,默认 8080 | +| ADAPTER_MODE | 否 | `mock` 或 `production`,默认 production | +| KUBECONFIG | 生产模式 | Kubernetes kubeconfig 文件路径 | +| HARBOR_URL | 否 | Harbor URL,用于 Registry 验证 | +| HARBOR_USERNAME | 否 | Harbor 用户名 | +| HARBOR_PASSWORD | 否 | Harbor 密码 | + +## 连接验证 + +在生产模式下,创建 Cluster 或 Registry 时会自动验证连接: + +- **Cluster**: 尝试使用提供的凭证连接 K8s API Server +- **Registry**: 尝试使用提供的凭证登录 Harbor + +如果验证失败,创建会返回错误信息。 + +## API 访问 + +- API: http://localhost:8081/api/v1 +- Health: http://localhost:8081/health +- Swagger: http://localhost:8081/api/docs + +## 项目结构 ``` backend/ -├── cmd/api/ # 程序入口 +├── cmd/api/ # 入口 ├── internal/ -│ ├── domain/ # 🎯 领域层(核心) -│ │ ├── entity/ # 实体 -│ │ ├── service/ # 业务逻辑 -│ │ └── repository/ # 接口定义 +│ ├── domain/ # 领域层 +│ │ ├── entity/ # 实体 +│ │ ├── service/ # 业务逻辑 +│ │ └── repository/ # 接口 │ ├── adapter/ -│ │ ├── input/http/ # 📥 REST API -│ │ └── output/ # 📤 数据库、OCI、Helm -│ ├── bootstrap/ # Bootstrap 预注入 -│ └── pkg/ # 🔧 工具包 -├── docs/ # 📚 文档 -├── config/ # ⚙️ 配置 -└── scripts/ # 🛠️ 脚本 -``` - -详见 [架构文档](docs/architecture.md) - -## 🔐 安全配置 - -### 环境变量 - -```bash -# 必需配置 -ADAPTER_MODE=production -JWT_SECRET=your-jwt-secret -ENCRYPTION_KEY=your-32-character-encryption-key -DATABASE_URL=postgresql://user:pass@host:5432/ocdp - -# 生成安全密钥 -openssl rand -base64 32 -``` - -### Bootstrap 预注入 - -在 `config/bootstrap.json` 中配置初始数据: - -```json -{ - "enabled": true, - "users": [ - {"username": "admin", "password": "admin123", "email": "admin@example.com"} - ], - "registries": [ - {"name": "harbor", "url": "https://harbor.example.com", "username": "admin", "password": "secret"} - ], - "clusters": [ - {"name": "prod", "host": "https://k8s.example.com:6443", "caData": "...", "certData": "...", "keyData": "..."} - ] -} -``` - -详见 [架构文档 - Bootstrap 预注入](docs/architecture.md#bootstrap-预注入) - -## 🌐 服务访问 - -| 服务 | 地址 | 说明 | -|------|------|------| -| Backend API | http://localhost:8080/api/v1 | REST API | -| Health Check | http://localhost:8080/health | 健康检查 | -| PostgreSQL | localhost:5432 | 数据库 | -| pgAdmin | http://localhost:5050 | 数据库管理 | - -## 🐛 故障排查 - -### 常见问题 - -**端口被占用**: -```bash -# 修改 .env 中的 BACKEND_PORT -BACKEND_PORT=8081 -``` - -**数据库连接失败**: -```bash -# 检查数据库状态 -docker compose ps postgres -docker compose logs postgres -``` - -**完全重置**: -```bash -# 停止并删除所有数据 -docker compose down -v -docker compose --profile production up -d -``` - -更多问题参见 [部署文档 - 故障排查](docs/deployment.md#故障排查) - -## 📊 技术栈 - -| 组件 | 技术 | -|------|------| -| **语言** | Go 1.21+ | -| **Web 框架** | gorilla/mux | -| **OCI 客户端** | ORAS Go SDK v2 | -| **Helm 集成** | Helm SDK | -| **Kubernetes** | client-go | -| **数据库** | PostgreSQL 15+ | -| **容器化** | Docker, Docker Compose | -| **热重载** | Air | - -## 🔗 相关资源 - -### 规范和文档 -- [OCI Distribution Specification](https://github.com/opencontainers/distribution-spec) -- [OCI Image Specification](https://github.com/opencontainers/image-spec) -- [Helm Documentation](https://helm.sh/docs/) - -### 使用的库 -- [Gorilla Mux](https://github.com/gorilla/mux) - HTTP 路由 -- [ORAS Go SDK](https://oras.land/docs/category/go-library) - OCI Registry 操作 -- [Helm SDK](https://helm.sh/docs/topics/advanced/) - Helm 操作 -- [Kubernetes Client-Go](https://github.com/kubernetes/client-go) - K8s API 客户端 - -## 📝 待办事项 - -- [ ] 添加单元测试和集成测试 -- [ ] 实现 Rate Limiting -- [ ] 添加审计日志 -- [ ] 实现 Webhook 通知 -- [ ] 支持更多 OCI Registry 类型 -- [ ] 添加 Metrics 和 Tracing - -## 📄 License - -MIT License - ---- - -**Version**: 2.2.0 -**Last Updated**: 2025-11-09 -**Port**: 8080 (default) +│ │ ├── input/http/ # REST API +│ │ └── output/ # 数据库、OCI、Helm +│ └── bootstrap/ # 启动配置 +└── docs/ # OpenAPI 规范 +``` \ No newline at end of file diff --git a/backend/REVIEW.md b/backend/REVIEW.md deleted file mode 100644 index 161d416..0000000 --- a/backend/REVIEW.md +++ /dev/null @@ -1,283 +0,0 @@ -# 项目实现审查报告 - -## ✅ 要求对照检查 - -### 1. 文档结构 ✅ - -#### 1.1 README.md ✅ -- 位置: `/backend/README.md` -- 状态: ✅ 已存在 -- 内容: 项目概述、快速开始、API文档、架构概览 - -#### 1.2 docs/ ✅ -所有文档齐全: - -##### 2.1 architecture.md ✅ -- 位置: `/backend/docs/architecture.md` -- 状态: ✅ 已存在 -- 内容完整包含: - - ✅ 2.1.1 **需求描述** (Requirement Description) - - 项目背景 - - 核心需求 - - 功能需求 - - 非功能需求 - - ✅ 2.1.2 **业务建模** (Business Modeling) - - 业务领域 - - 核心实体 - - 业务流程 - - 用例场景 - - ✅ 2.1.3 **技术建模** (Technical Modeling) - - ✅ 2.1.3.1 **六边形架构** (Hexagonal Architecture) - - 架构图 - - 层次说明 - - 依赖倒置原则 - - ✅ 2.1.3.2 **技术选型** (Technology Selection) - - Go 语言 - - Web 框架 - - OCI/Helm 客户端 - - 数据库选型 - -##### 2.2 api-and-test.md ✅ -- 位置: `/backend/docs/api-and-test.md` -- 状态: ✅ 已存在 -- 内容: REST API 文档、测试指南 - -##### 2.3 deployment.md ✅ -- 位置: `/backend/docs/deployment.md` -- 状态: ✅ 已存在 -- 内容: 部署指南、环境配置 - ---- - -### 2. 开发模式 ✅ - -#### 3.1 Mode 0: Hot Reload + Mock ✅ -```bash -make run-0 -``` -- ✅ 热加载支持 (Air) -- ✅ Mock 所有依赖 (内存实现) -- ✅ 无需任何容器 -- ✅ 使用环境变量 `ADAPTER_MODE=mock` - -**实现方式**: -```makefile -run-0: - ADAPTER_MODE=mock air -c .air.toml -``` - -#### 3.2 Mode 1: Hot Reload + Real Deps in Container ✅ -```bash -make run-1 -``` -- ✅ 热加载支持 (Air) -- ✅ 真实依赖 (PostgreSQL) 运行在容器中 -- ✅ 后端代码在本地运行 -- ✅ 使用 Docker Compose **无 profile** 启动 postgres - -**实现方式**: -```makefile -run-1: - @docker compose up -d postgres - @sleep 5 - ADAPTER_MODE= \ - DATABASE_URL=postgresql://postgres:postgres@localhost:5432/ocdp?sslmode=disable \ - air -c .air.toml -``` - -#### 3.3 Mode 2: All in Container ✅ -```bash -make run-2 -``` -- ✅ 所有服务容器化 -- ✅ 无热加载(生产模式) -- ✅ 使用 Docker Compose **--profile backend** - -**实现方式**: -```makefile -run-2: - @docker compose --profile backend up --build -d -``` - ---- - -### 3. Makefile 设计 ✅ - -#### 核心要求: 使用 Profile 共享单一 Docker Compose ✅ - -**✅ 已实现**: 使用 `docker-compose.yml` + Profile 机制 - -| 命令 | 功能 | Docker Compose 用法 | -|------|------|---------------------| -| `make run-0` | Mock 模式 | 不使用 Docker | -| `make run-1` | 开发模式 | `docker compose up -d postgres` (无 profile) | -| `make run-2` | 生产模式 | `docker compose --profile backend up -d` | -| `make clean-1` | 清理 run-1 | `docker compose down -v` | -| `make clean-2` | 清理 run-2 | `docker compose --profile backend down -v` | - -#### Docker Compose Profile 配置 ✅ - -文件: `/backend/docker-compose.yml` - -```yaml -services: - postgres: - # 默认启动(无需 profile) - # 用于 run-1 - - backend: - profiles: - - backend # 需要 --profile backend 才启动 - # 用于 run-2 - - backend-mock: - profiles: - - mock # 可选的 mock 容器模式 -``` - -**优点**: -- ✅ 单一配置文件,避免重复 -- ✅ Profile 清晰区分不同模式 -- ✅ 符合 Docker Compose 最佳实践 - ---- - -## 📊 完整流程对照 - -### Process 1: Product Description ✅ - -#### 1.1 Requirement ✅ -- 文档: `docs/architecture.md` - 2.1.1 需求描述 -- 内容: 项目背景、核心需求、功能需求、非功能需求 - -#### 1.2 What API I Need ✅ -- 文档: `docs/api-and-test.md` -- 内容: REST API 端点定义、请求/响应示例 - -#### 1.3 Generator Docs ✅ -所有文档已完整生成: -- ✅ `docs/architecture.md` - 架构文档 - - ✅ Business Modeling - 业务建模 - - ✅ Technical Modeling - 技术建模 - - ✅ Hexagonal Architecture - 六边形架构 - - ✅ Technology Selection - 技术选型 -- ✅ `docs/api-and-test.md` - API 文档 -- ✅ `docs/deployment.md` - 部署文档 - ---- - -### Process 2: Development ✅ - -#### 2.1 Develop Mode 1, 2, 3 ✅ -- ✅ Mode 0 (Mock): `make run-0` -- ✅ Mode 1 (Dev): `make run-1` -- ✅ Mode 2 (Prod): `make run-2` - ---- - -### Process 3: Provide Makefile ✅ - -#### 3.1 Five Commands ✅ -```bash -make run-0 # ✅ Hot reload + Mock (memory) -make run-1 # ✅ Hot reload + Real deps (container) -make run-2 # ✅ All in container -make clean-1 # ✅ Clean run-1 artifacts -make clean-2 # ✅ Clean run-2 artifacts -``` - -#### 3.2 Using Profile to Share Docker Compose ✅ -- ✅ 使用单一的 `docker-compose.yml` -- ✅ 通过 `--profile backend` 区分 mode 1 和 mode 2 -- ✅ Mode 1: `docker compose up -d postgres` (无 profile) -- ✅ Mode 2: `docker compose --profile backend up -d` - ---- - -## 🎯 核心设计亮点 - -### 1. Docker Compose Profile 机制 ⭐⭐⭐⭐⭐ - -**传统做法** (❌ 不推荐): -``` -docker-compose.dev.yml # 重复配置 postgres -docker-compose.prod.yml # 重复配置 postgres -``` - -**本项目做法** (✅ 推荐): -```yaml -# docker-compose.yml (单一文件) -services: - postgres: # 默认启动,run-1 和 run-2 共享 - backend: - profiles: [backend] # 只在 run-2 时启动 -``` - -**优势**: -- ✅ 配置不重复 (DRY 原则) -- ✅ 维护简单 (只需修改一个文件) -- ✅ 数据共享 (run-1 和 run-2 使用同一个数据库) - -### 2. 三种模式的清晰定位 - -| 模式 | 场景 | 启动速度 | 真实依赖 | 热加载 | -|------|------|---------|---------|--------| -| Mode 0 | 快速开发、单元测试 | ⚡ 秒启动 | ❌ Mock | ✅ | -| Mode 1 | 日常开发、集成测试 | 🔥 5秒 | ✅ | ✅ | -| Mode 2 | 部署前测试、生产环境 | 🐳 30秒 | ✅ | ❌ | - -### 3. 命令语义清晰 - -```bash -run-0 → 0 依赖(Mock all) -run-1 → 1 部分容器化(Deps in container) -run-2 → 2 完全容器化(All in container) - -clean-1 → 清理 run-1 的产物 -clean-2 → 清理 run-2 的产物 -``` - ---- - -## ✅ 最终验证 - -### 文档完整性 -- [x] README.md -- [x] docs/architecture.md - - [x] 2.1.1 需求描述 - - [x] 2.1.2 业务建模 - - [x] 2.1.3 技术建模 - - [x] 2.1.3.1 六边形架构 - - [x] 2.1.3.2 技术选型 -- [x] docs/api-and-test.md -- [x] docs/deployment.md - -### 开发模式 -- [x] Mode 0: Hot reload + Mock (memory) -- [x] Mode 1: Hot reload + Real deps (container) -- [x] Mode 2: All in container - -### Makefile -- [x] 5 个核心命令 -- [x] 使用 Profile 共享 Docker Compose -- [x] run-1 和 run-2 使用同一个 docker-compose.yml - ---- - -## 🎉 结论 - -**所有要求均已满足!✅** - -项目完全符合你的设计要求: -1. ✅ 完整的文档结构 (README + docs/) -2. ✅ 三种开发模式 (0/1/2) -3. ✅ 五个核心命令 (run-0/1/2, clean-1/2) -4. ✅ 使用 Profile 共享 Docker Compose(关键设计) - -**特别亮点**: -- 使用 Docker Compose Profile 避免配置重复 -- 命名清晰,语义明确 (run-0/1/2) -- 产物隔离 (clean-1 vs clean-2) - -**可以直接使用!** 🚀 - diff --git a/backend/TEST-REPORT.md b/backend/TEST-REPORT.md deleted file mode 100644 index 17893e4..0000000 --- a/backend/TEST-REPORT.md +++ /dev/null @@ -1,400 +0,0 @@ -# 测试报告 - -**测试日期**: 2025-11-10 -**测试内容**: Makefile 五个核心命令的功能测试 - ---- - -## ✅ 测试结果总览 - -| 命令 | 状态 | 说明 | -|------|------|------| -| `make run-0` | ✅ 通过 | Mock 模式正常工作 | -| `make run-1` | ✅ 通过 | Docker 依赖 + 热加载正常 | -| `make run-2` | ✅ 通过 | 完全容器化部署成功 | -| `make clean-1` | ✅ 通过 | 清理 run-1 产物完整 | -| `make clean-2` | ✅ 通过 | 清理 run-2 产物完整 | - ---- - -## 📋 详细测试结果 - -### 1️⃣ make run-0 (Mock 模式) - -**测试命令**: -```bash -make run-0 -``` - -**预期行为**: -- ✅ 使用 Mock 适配器(内存存储) -- ✅ 无需任何 Docker 容器 -- ✅ 支持热加载(Air) -- ✅ 环境变量 `ADAPTER_MODE=mock` - -**实际结果**: -``` -✅ 服务启动成功 -✅ Health API 正常: {"status":"healthy"} -✅ Registries API 返回 Mock 数据: 1 条记录 -✅ Clusters API 返回 Mock 数据: 1 条记录 -✅ Bootstrap 数据预注入成功 -``` - -**日志输出**: -``` -📝 Configuration: mode=mock, port=8080 -✅ Output Adapters initialized (mode: mock) - 🌱 Starting bootstrap seeding... - ✓ User 'admin' created - ✓ Registry 'Harbor Production' created - ✓ Cluster 'Test Cluster' created - ✅ Bootstrap seeding completed -🌐 Server starting on :8080 -``` - -**结论**: ✅ **完全符合预期** - ---- - -### 2️⃣ make run-1 (Docker 依赖 + 热加载) - -**测试命令**: -```bash -make run-1 -``` - -**预期行为**: -- ✅ 启动 PostgreSQL 容器(使用 `docker compose up -d postgres`) -- ✅ 后端代码在本地运行 -- ✅ 支持热加载(Air) -- ✅ 连接真实数据库 -- ✅ 环境变量 `ADAPTER_MODE=` (空,表示真实模式) - -**实际结果**: -``` -✅ PostgreSQL 容器启动: ocdp-postgres (healthy) -✅ 后端连接数据库成功 -✅ 数据库 Schema 初始化成功 -✅ Health API 正常 -✅ Registries API 连接真实 PostgreSQL -✅ Clusters API 连接真实 PostgreSQL -``` - -**Docker 容器状态**: -``` -NAME IMAGE STATUS -ocdp-postgres postgres:17-alpine Up (healthy) -``` - -**配置**: -```makefile -run-1: - @docker compose up -d postgres - ADAPTER_MODE= \ - DATABASE_URL=postgresql://postgres:postgres@localhost:5432/ocdp?sslmode=disable \ - air -c .air.toml -``` - -**结论**: ✅ **完全符合预期** - ---- - -### 3️⃣ make run-2 (完全容器化) - -**测试命令**: -```bash -make run-2 -``` - -**预期行为**: -- ✅ 构建后端 Docker 镜像 -- ✅ 启动 PostgreSQL + Backend 容器 -- ✅ 使用 `--profile backend` 区分模式 -- ✅ 后台运行 -- ✅ 无热加载 - -**实际结果**: -``` -✅ Docker 镜像构建成功: backend-backend -✅ 两个容器启动: ocdp-postgres + ocdp-backend -✅ 容器健康检查通过 -✅ Health API 正常 -✅ Registries API 返回真实数据: 1 条记录 -✅ Clusters API 返回真实数据: 1 条记录 -✅ 后端日志正常输出 -``` - -**Docker 容器状态**: -``` -NAME IMAGE STATUS -ocdp-backend backend-backend Up (healthy) -ocdp-postgres postgres:17-alpine Up (healthy) -``` - -**配置**: -```makefile -run-2: - @docker compose --profile backend up --build -d -``` - -**Docker Compose Profile 验证**: -- ✅ 使用单一的 `docker-compose.yml` -- ✅ PostgreSQL 默认启动(无 profile) -- ✅ Backend 需要 `--profile backend` 才启动 -- ✅ 符合设计要求 - -**结论**: ✅ **完全符合预期** - ---- - -### 4️⃣ make clean-1 (清理 run-1 产物) - -**测试命令**: -```bash -make clean-1 -``` - -**预期行为**: -- ✅ 停止并删除 PostgreSQL 容器 -- ✅ 删除 Docker 数据卷 -- ✅ 清空 `tmp/` 目录 -- ✅ 删除 Docker 网络 - -**清理前状态**: -``` -Docker 容器: ocdp-postgres (running) -tmp/ 目录: 包含 main, build-errors.log, test.txt -Docker 卷: ocdp-postgres-data -Docker 网络: ocdp-network -``` - -**清理后状态**: -``` -✅ Docker 容器: 0 个 -✅ tmp/ 目录: 已清空 -✅ Docker 卷: ocdp-postgres-data 已删除 -✅ Docker 网络: ocdp-network 已删除 -``` - -**执行输出**: -``` -🧹 Cleaning run-1 artifacts... - Container ocdp-postgres Stopping - Container ocdp-postgres Stopped - Container ocdp-postgres Removing - Container ocdp-postgres Removed - Volume ocdp-postgres-data Removing - Volume ocdp-postgres-data Removed - Network ocdp-network Removed -✅ run-1 cleaned -``` - -**配置**: -```makefile -clean-1: - @docker compose down -v - @rm -rf tmp/ -``` - -**结论**: ✅ **清理完整,无残留** - ---- - -### 5️⃣ make clean-2 (清理 run-2 产物) - -**测试命令**: -```bash -make clean-2 -``` - -**预期行为**: -- ✅ 停止并删除 Backend + PostgreSQL 容器 -- ✅ 删除 Docker 数据卷 -- ✅ 清空 `bin/` 和 `dist/` 目录 -- ✅ 删除 Docker 网络 - -**清理前状态**: -``` -Docker 容器: ocdp-backend (running), ocdp-postgres (running) -bin/ 目录: 存在 -dist/ 目录: 包含 test.tar.gz -Docker 卷: ocdp-postgres-data -Docker 网络: ocdp-network -``` - -**清理后状态**: -``` -✅ Docker 容器: 0 个 -✅ bin/ 目录: 0 个文件 -✅ dist/ 目录: 0 个文件 -✅ Docker 卷: ocdp-postgres-data 已删除 -✅ Docker 网络: ocdp-network 已删除 -``` - -**执行输出**: -``` -🧹 Cleaning run-2 artifacts... - Container ocdp-backend Stopping - Container ocdp-backend Stopped - Container ocdp-backend Removing - Container ocdp-backend Removed - Container ocdp-postgres Stopping - Container ocdp-postgres Stopped - Container ocdp-postgres Removing - Container ocdp-postgres Removed - Volume ocdp-postgres-data Removed - Network ocdp-network Removed -✅ run-2 cleaned -``` - -**配置**: -```makefile -clean-2: - @docker compose --profile backend down -v - @rm -rf bin/ dist/ -``` - -**结论**: ✅ **清理完整,无残留** - ---- - -## 🎯 核心设计验证 - -### Docker Compose Profile 机制 ✅ - -**验证内容**: 使用单一 `docker-compose.yml` + Profile 实现三种模式 - -**验证结果**: - -1. **run-1**: `docker compose up -d postgres` - - ✅ 只启动 postgres 服务(无 profile) - - ✅ backend 服务不启动(需要 profile) - -2. **run-2**: `docker compose --profile backend up -d` - - ✅ 启动 postgres + backend 两个服务 - - ✅ 使用 `--profile backend` 激活 backend - -3. **共享配置**: - - ✅ 两种模式使用同一个 `docker-compose.yml` - - ✅ postgres 配置不重复 - - ✅ 符合 DRY 原则 - -**配置文件**: -```yaml -services: - postgres: - # 默认启动(无需 profile) - - backend: - profiles: - - backend # 需要 --profile backend - depends_on: - - postgres -``` - -**结论**: ✅ **设计正确,实现完美** - ---- - -## 📊 API 功能测试 - -### Health Check API -```bash -curl http://localhost:8080/health -``` - -**结果**: ✅ 三种模式均正常 -```json -{"status":"healthy"} -``` - ---- - -### Registries API -```bash -curl http://localhost:8080/api/v1/registries -``` - -**run-0 (Mock)**: ✅ 返回 1 条 Mock 数据 -**run-1 (PostgreSQL)**: ✅ 返回真实数据 -**run-2 (容器)**: ✅ 返回真实数据 - ---- - -### Clusters API -```bash -curl http://localhost:8080/api/v1/clusters -``` - -**run-0 (Mock)**: ✅ 返回 1 条 Mock 数据 -**run-1 (PostgreSQL)**: ✅ 返回真实数据 -**run-2 (容器)**: ✅ 返回真实数据 - ---- - -## 🎉 总结 - -### ✅ 所有测试通过 - -- ✅ **run-0**: Mock 模式工作正常 -- ✅ **run-1**: Docker 依赖 + 热加载正常 -- ✅ **run-2**: 完全容器化部署成功 -- ✅ **clean-1**: 清理 run-1 产物完整 -- ✅ **clean-2**: 清理 run-2 产物完整 - -### ✅ 核心设计验证 - -- ✅ **Docker Compose Profile** 机制正确 -- ✅ **单一配置文件** 实现多模式 -- ✅ **产物隔离** 清晰 -- ✅ **命名语义** 明确 - -### 🎯 符合所有设计要求 - -1. ✅ 三种运行模式(0/1/2) -2. ✅ 五个核心命令 -3. ✅ 使用 Profile 共享 Docker Compose -4. ✅ 清理命令分离且完整 - ---- - -## 💡 使用建议 - -### 日常开发流程 - -```bash -# 快速开发 -make run-0 - -# 需要真实数据库 -make run-1 - -# 测试部署 -make run-2 - -# 清理环境 -make clean-1 # 或 make clean-2 -``` - -### 注意事项 - -1. **run-0** 和 **run-1** 是前台运行,`Ctrl+C` 停止 -2. **run-2** 是后台运行,使用 `docker compose --profile backend down` 停止 -3. **clean-1** 和 **clean-2** 会删除数据卷,数据会丢失 -4. run-1 和 run-2 共享同一个 PostgreSQL 容器配置和数据 - ---- - -## 📝 测试环境 - -- **操作系统**: Linux 5.15.0-160-generic -- **Docker**: 已安装 -- **Docker Compose**: 已安装 -- **Go**: 1.24+ -- **Air**: 已安装 - ---- - -**测试结论**: 🎉 **所有功能正常,可以投入使用!** - diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go index d32c592..0e31fce 100644 --- a/backend/cmd/api/main.go +++ b/backend/cmd/api/main.go @@ -27,6 +27,7 @@ import ( "log" "net/http" "os" + "strings" "time" "github.com/gorilla/mux" @@ -104,6 +105,20 @@ func main() { repos.MetricsClient, ) + // Workspace Service + workspaceService := service.NewWorkspaceService( + repos.WorkspaceRepo, + repos.QuotaRepo, + repos.UserRepo, + ) + + // User Management Service + userManagementService := service.NewUserManagementService( + repos.UserRepo, + repos.WorkspaceRepo, + passwordHasher, + ) + log.Println("✅ Domain Services initialized") // ===== 6. 加载并执行 Bootstrap 预注入 ===== @@ -128,6 +143,27 @@ func main() { monitoringHandler := rest.NewMonitoringHandler(monitoringService) swaggerHandler := rest.NewSwaggerHandler() + // Workspace Handler + workspaceHandler := rest.NewWorkspaceHandler(workspaceService, authService) + + // User Management Handler (Admin only) + userManagementHandler := rest.NewUserManagementHandler(userManagementService, authService, workspaceService) + + // User Handler + userHandler := rest.NewUserHandler(authService, workspaceService) + + // Storage Handler + storageService := service.NewStorageService(repos.StorageRepo) + storageHandler := rest.NewStorageHandler(storageService) + + // Chart Reference Handler + chartRefService := service.NewChartReferenceService(repos.ChartRefRepo, repos.RegistryRepo) + chartRefHandler := rest.NewChartReferenceHandler(chartRefService) + + // Values Template Handler + valuesTemplateService := service.NewValuesTemplateService(repos.ValuesTemplateRepo, repos.ChartRefRepo) + valuesTemplateHandler := rest.NewValuesTemplateHandler(valuesTemplateService) + log.Println("✅ Input Adapters (REST handlers) initialized") // ===== 8. 设置路由 ===== @@ -139,6 +175,14 @@ func main() { instanceHandler, monitoringHandler, swaggerHandler, + workspaceHandler, + userManagementHandler, + userHandler, + storageHandler, + chartRefHandler, + valuesTemplateHandler, + tokenGenerator, + config.AllowedOrigins, ) // ===== 9. 启动服务器 ===== @@ -161,21 +205,28 @@ func main() { // Config 应用配置 type Config struct { - AdapterMode string - Port string - JWTSecret string - EncryptionKey string - DatabaseURL string + AdapterMode string + Port string + JWTSecret string + EncryptionKey string + DatabaseURL string + AllowedOrigins []string } // loadConfig 加载配置 func loadConfig() *Config { + allowedOrigins := getEnv("ALLOWED_DEV_ORIGINS", "") + var origins []string + if allowedOrigins != "" { + origins = strings.Split(allowedOrigins, ",") + } return &Config{ - AdapterMode: getEnv("ADAPTER_MODE", ""), // 默认为空字符串(真实模式) - Port: getEnv("PORT", "8080"), - JWTSecret: getEnv("JWT_SECRET", "your-secret-key-change-in-production"), - EncryptionKey: getEnv("ENCRYPTION_KEY", "default-encryption-key-change-in-production"), - DatabaseURL: getEnv("DATABASE_URL", ""), + AdapterMode: getEnv("ADAPTER_MODE", ""), // 默认为空字符串(真实模式) + Port: getEnv("PORT", "8080"), + JWTSecret: getEnv("JWT_SECRET", "your-secret-key-change-in-production"), + EncryptionKey: getEnv("ENCRYPTION_KEY", "default-encryption-key-change-in-production"), + DatabaseURL: getEnv("DATABASE_URL", ""), + AllowedOrigins: origins, } } @@ -197,12 +248,66 @@ func setupRouter( instanceHandler *rest.InstanceHandler, monitoringHandler *rest.MonitoringHandler, swaggerHandler *rest.SwaggerHandler, + workspaceHandler *rest.WorkspaceHandler, + userManagementHandler *rest.UserManagementHandler, + userHandler *rest.UserHandler, + storageHandler *rest.StorageHandler, + chartRefHandler *rest.ChartReferenceHandler, + valuesTemplateHandler *rest.ValuesTemplateHandler, + tokenGenerator *jwt.JWTManager, + allowedOrigins []string, ) *mux.Router { router := mux.NewRouter().StrictSlash(true) // 全局中间件 router.Use(loggingMiddleware) - router.Use(corsMiddleware) + router.Use(corsMiddleware(allowedOrigins)) + + // 预检请求处理 - 必须放在路由注册之前 + router.HandleFunc("/{path:.*}", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodOptions { + origin := r.Header.Get("Origin") + if origin == "" { + origin = "*" + } + w.Header().Set("Access-Control-Allow-Origin", origin) + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With") + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Access-Control-Max-Age", "86400") + w.WriteHeader(http.StatusNoContent) + return + } + // 非 OPTIONS 请求返回 404 + http.NotFound(w, r) + }).Methods(http.MethodOptions) + + // JWT 解析中间件 - 为所有需要认证的请求设置用户信息 header + jwtMiddleware := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // 跳过认证路由 + if r.URL.Path == "/api/v1/auth/login" || + r.URL.Path == "/api/v1/auth/register" || + r.URL.Path == "/api/v1/auth/refresh" { + next.ServeHTTP(w, r) + return + } + + authHeader := r.Header.Get("Authorization") + if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") { + token := strings.TrimPrefix(authHeader, "Bearer ") + userID, username, role, workspaceID, err := tokenGenerator.Verify(token) + if err == nil && userID != "" { + // 设置 header 供 handlers 使用 + r.Header.Set("X-User-ID", userID) + r.Header.Set("X-Username", username) + r.Header.Set("X-User-Role", role) + r.Header.Set("X-Workspace-ID", workspaceID) + } + } + next.ServeHTTP(w, r) + }) + } // 健康检查 router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { @@ -220,12 +325,39 @@ func setupRouter( // API v1 api := router.PathPrefix("/api/v1").Subrouter() + // 应用 CORS 和 JWT 中间件到所有 API 路由 + api.Use(corsMiddleware(allowedOrigins)) + api.Use(jwtMiddleware) // ===== 认证路由 ===== api.HandleFunc("/auth/register", authHandler.Register) api.HandleFunc("/auth/login", authHandler.Login) api.HandleFunc("/auth/refresh", authHandler.RefreshToken) + // ===== 用户账户路由 ===== + api.HandleFunc("/users/me", userHandler.GetCurrentUser).Methods(http.MethodGet) + api.HandleFunc("/users/me/password", userHandler.ChangePassword).Methods(http.MethodPut) + api.HandleFunc("/users/me/workspace", userHandler.GetCurrentUserWorkspace).Methods(http.MethodGet) + + // ===== 用户管理路由(Admin) ===== + api.HandleFunc("/admin/users", userManagementHandler.CreateUser).Methods(http.MethodPost) + api.HandleFunc("/admin/users", userManagementHandler.ListUsers).Methods(http.MethodGet) + api.HandleFunc("/admin/users/{user_id}", userManagementHandler.GetUser).Methods(http.MethodGet) + api.HandleFunc("/admin/users/{user_id}", userManagementHandler.UpdateUser).Methods(http.MethodPut) + api.HandleFunc("/admin/users/{user_id}/active", userManagementHandler.SetUserActive).Methods(http.MethodPut) + api.HandleFunc("/admin/users/{user_id}/workspace", userManagementHandler.ChangeUserWorkspace).Methods(http.MethodPut) + api.HandleFunc("/admin/users/{user_id}/password", userManagementHandler.ResetPassword).Methods(http.MethodPut) + api.HandleFunc("/admin/users/{user_id}", userManagementHandler.DeleteUser).Methods(http.MethodDelete) + + // ===== Workspace 路由 ===== + api.HandleFunc("/workspaces", workspaceHandler.CreateWorkspace).Methods(http.MethodPost) + api.HandleFunc("/workspaces", workspaceHandler.ListWorkspaces).Methods(http.MethodGet) + api.HandleFunc("/workspaces/{workspace_id}", workspaceHandler.GetWorkspace).Methods(http.MethodGet) + api.HandleFunc("/workspaces/{workspace_id}", workspaceHandler.UpdateWorkspace).Methods(http.MethodPut) + api.HandleFunc("/workspaces/{workspace_id}", workspaceHandler.DeleteWorkspace).Methods(http.MethodDelete) + api.HandleFunc("/workspaces/{workspace_id}/quotas", workspaceHandler.GetWorkspaceQuotas).Methods(http.MethodGet) + api.HandleFunc("/workspaces/{workspace_id}/quotas", workspaceHandler.SetWorkspaceQuotas).Methods(http.MethodPut) + // ===== 集群路由 ===== api.HandleFunc("/clusters", clusterHandler.CreateCluster).Methods(http.MethodPost) api.HandleFunc("/clusters", clusterHandler.GetAllClusters).Methods(http.MethodGet) @@ -242,11 +374,36 @@ func setupRouter( api.HandleFunc("/registries/{registry_id}", registryHandler.DeleteRegistry).Methods(http.MethodDelete) api.HandleFunc("/registries/{registry_id}/health", registryHandler.GetRegistryHealth).Methods(http.MethodGet) + // ===== Storage Backend 路由 ===== + api.HandleFunc("/storage-backends", storageHandler.CreateStorage).Methods(http.MethodPost) + api.HandleFunc("/storage-backends", storageHandler.GetAllStorage).Methods(http.MethodGet) + api.HandleFunc("/storage-backends/{storage_id}", storageHandler.GetStorage).Methods(http.MethodGet) + api.HandleFunc("/storage-backends/{storage_id}", storageHandler.UpdateStorage).Methods(http.MethodPut) + api.HandleFunc("/storage-backends/{storage_id}", storageHandler.DeleteStorage).Methods(http.MethodDelete) + + // ===== Chart Reference 路由 ===== + api.HandleFunc("/chart-references", chartRefHandler.CreateChartReference).Methods(http.MethodPost) + api.HandleFunc("/chart-references", chartRefHandler.GetAllChartReferences).Methods(http.MethodGet) + api.HandleFunc("/chart-references/{chart_reference_id}", chartRefHandler.GetChartReference).Methods(http.MethodGet) + api.HandleFunc("/chart-references/{chart_reference_id}", chartRefHandler.UpdateChartReference).Methods(http.MethodPut) + api.HandleFunc("/chart-references/{chart_reference_id}", chartRefHandler.DeleteChartReference).Methods(http.MethodDelete) + + // ===== Values Template 路由 ===== + api.HandleFunc("/values-templates", valuesTemplateHandler.CreateValuesTemplate).Methods(http.MethodPost) + api.HandleFunc("/values-templates", valuesTemplateHandler.GetAllValuesTemplates).Methods(http.MethodGet) + api.HandleFunc("/values-templates/{template_id}", valuesTemplateHandler.GetValuesTemplate).Methods(http.MethodGet) + api.HandleFunc("/values-templates/{template_id}", valuesTemplateHandler.UpdateValuesTemplate).Methods(http.MethodPut) + api.HandleFunc("/values-templates/{template_id}", valuesTemplateHandler.DeleteValuesTemplate).Methods(http.MethodDelete) + api.HandleFunc("/chart-references/{chart_reference_id}/values-templates", valuesTemplateHandler.GetValuesTemplatesByChartReference).Methods(http.MethodGet) + api.HandleFunc("/chart-references/{chart_reference_id}/values-templates/history", valuesTemplateHandler.GetValuesTemplateHistory).Methods(http.MethodGet) + api.HandleFunc("/chart-references/{chart_reference_id}/values-templates/rollback", valuesTemplateHandler.RollbackValuesTemplate).Methods(http.MethodPost) + // ===== Artifact 路由 ===== api.HandleFunc("/registries/{registry_id}/repositories", artifactHandler.ListRepositories).Methods(http.MethodGet) api.HandleFunc("/registries/{registry_id}/repositories/{repository_name:.+}/artifacts", artifactHandler.ListArtifacts).Methods(http.MethodGet) api.HandleFunc("/registries/{registry_id}/repositories/{repository_name:.+}/artifacts/{reference}", artifactHandler.GetArtifact).Methods(http.MethodGet) api.HandleFunc("/registries/{registry_id}/repositories/{repository_name:.+}/artifacts/{reference}/values-schema", artifactHandler.GetArtifactValuesSchema).Methods(http.MethodGet) + api.HandleFunc("/registries/{registry_id}/repositories/{repository_name:.+}/artifacts/{reference}/values", artifactHandler.GetArtifactValues).Methods(http.MethodGet) // ===== Instance 路由 ===== api.HandleFunc("/clusters/{cluster_id}/instances", instanceHandler.CreateInstance).Methods(http.MethodPost) @@ -285,25 +442,54 @@ func loggingMiddleware(next http.Handler) http.Handler { } // corsMiddleware CORS 中间件 -func corsMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // 设置 CORS 头 - origin := r.Header.Get("Origin") - if origin == "" { - origin = "*" - } - w.Header().Set("Access-Control-Allow-Origin", origin) - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With") - w.Header().Set("Access-Control-Allow-Credentials", "true") - w.Header().Set("Access-Control-Max-Age", "86400") +func corsMiddleware(allowedOrigins []string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + origin := r.Header.Get("Origin") - // 处理 OPTIONS 预检请求 - if r.Method == http.MethodOptions { - w.WriteHeader(http.StatusNoContent) - return - } + // 验证 origin 是否在允许列表中 + if origin != "" && len(allowedOrigins) > 0 { + allowed := false + for _, ao := range allowedOrigins { + if ao == origin || ao == "*" { + allowed = true + break + } + } + if !allowed { + // Origin 不在允许列表中,拒绝请求 + w.Header().Set("Access-Control-Allow-Origin", "") + w.WriteHeader(http.StatusForbidden) + return + } + } - next.ServeHTTP(w, r) - }) + // 如果没有配置 allowedOrigins,默认允许所有 + if len(allowedOrigins) == 0 { + if origin == "" { + origin = "*" + } + } + + // 优先处理 OPTIONS 预检请求 + if r.Method == http.MethodOptions { + w.Header().Set("Access-Control-Allow-Origin", origin) + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With") + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Access-Control-Max-Age", "86400") + w.WriteHeader(http.StatusNoContent) + return + } + + // 设置 CORS 头 + w.Header().Set("Access-Control-Allow-Origin", origin) + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With") + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Access-Control-Max-Age", "86400") + + next.ServeHTTP(w, r) + }) + } } diff --git a/backend/cmd/gen_hash/main.go b/backend/cmd/gen_hash/main.go new file mode 100644 index 0000000..c25be19 --- /dev/null +++ b/backend/cmd/gen_hash/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "fmt" + "golang.org/x/crypto/bcrypt" +) + +func main() { + hash, _ := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost) + fmt.Println(string(hash)) +} \ No newline at end of file diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 4dab3fd..fc08ee1 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -67,6 +67,13 @@ services: JWT_SECRET: ${JWT_SECRET:-change-me-in-production} ENCRYPTION_KEY: ${ENCRYPTION_KEY:-change-me-32-bytes-long-key-here} DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-ocdp}?sslmode=disable + KUBECONFIG: ${KUBECONFIG:-.kube/config} + HARBOR_URL: ${HARBOR_URL:-} + HARBOR_USERNAME: ${HARBOR_USERNAME:-} + HARBOR_PASSWORD: ${HARBOR_PASSWORD:-} + NFS_SERVER: ${NFS_SERVER:-} + NFS_SHARE: ${NFS_SHARE:-} + ALLOWED_DEV_ORIGINS: ${ALLOWED_DEV_ORIGINS:-} ports: - "${BACKEND_PORT:-8080}:8080" volumes: diff --git a/backend/docs/api-and-test.md b/backend/docs/api-and-test.md deleted file mode 100644 index 4eec6a0..0000000 --- a/backend/docs/api-and-test.md +++ /dev/null @@ -1,1802 +0,0 @@ -# 📚 API 与测试文档 - -> **💡 推荐使用 OpenAPI 规范** -> -> 本项目现在提供标准的 **OpenAPI 3.0 规范**! -> -> - 📖 **交互式 API 文档**: [http://localhost:8080/api/docs](http://localhost:8080/api/docs) (Swagger UI) -> - 📄 **OpenAPI 规范文件**: [openapi.yaml](./openapi.yaml) -> - 🔧 **在线测试**: 在 Swagger UI 中直接测试所有 API -> - 🚀 **客户端生成**: 使用 OpenAPI 规范自动生成客户端代码 -> -> **本文档 (Markdown 版本)** 作为参考文档保留,内容与 OpenAPI 规范保持一致。 -> 如需最新的 API 定义,请参考 [openapi.yaml](./openapi.yaml)。 - ---- - -## 目录 - -### Part 1: API 文档 -- [基础信息](#基础信息) -- [认证 API](#认证-api) -- [集群管理 API](#集群管理-api) -- [Registry 管理 API](#registry-管理-api) -- [Artifact 浏览 API](#artifact-浏览-api) -- [实例管理 API](#实例管理-api) -- [监控 API](#监控-api) -- [响应格式](#响应格式) -- [错误处理](#错误处理) - -### Part 2: 测试文档 -- [测试策略](#测试策略) -- [单元测试](#单元测试) -- [集成测试](#集成测试) -- [API 测试](#api-测试) -- [E2E 测试](#e2e-测试) -- [Mock 测试](#mock-测试) -- [测试工具](#测试工具) -- [测试最佳实践](#测试最佳实践) - ---- - -# Part 1: API 文档 - -## 基础信息 - -### Base URL - -``` -http://localhost:8080/api/v1 -``` - -### 健康检查 - -```bash -GET /health - -# 响应 -{ - "status": "healthy" -} -``` - -### 通用请求头 - -``` -Content-Type: application/json -Authorization: Bearer # 部分接口需要 -``` - ---- - -## 认证 API - -### 用户注册 - -```bash -POST /api/v1/auth/register -Content-Type: application/json - -{ - "username": "john", - "password": "secret123", - "email": "john@example.com" -} - -# 响应 201 -{ - "id": "user-123", - "username": "john", - "email": "john@example.com", - "createdAt": "2025-11-09T10:00:00Z" -} -``` - -### 用户登录 - -```bash -POST /api/v1/auth/login -Content-Type: application/json - -{ - "username": "john", - "password": "secret123" -} - -# 响应 200 -{ - "accessToken": "eyJhbGciOiJIUzI1NiIs...", - "refreshToken": "eyJhbGciOiJIUzI1NiIs...", - "userId": "user-123", - "username": "john" -} -``` - -### 刷新 Token - -```bash -POST /api/v1/auth/refresh -Content-Type: application/json - -{ - "refreshToken": "eyJhbGciOiJIUzI1NiIs..." -} - -# 响应 200 -{ - "accessToken": "eyJhbGciOiJIUzI1NiIs...", - "refreshToken": "eyJhbGciOiJIUzI1NiIs...", - "userId": "user-123", - "username": "john" -} -``` - ---- - -## 集群管理 API - -### 创建集群 - -```bash -POST /api/v1/clusters -Content-Type: application/json - -{ - "name": "Production Cluster", - "host": "https://k8s.example.com:6443", - "description": "生产环境集群", - "caData": "LS0tLS1CRUdJTi0...", # Base64 编码的 CA 证书 - "certData": "LS0tLS1CRUdJTi0...", # Base64 编码的客户端证书 - "keyData": "LS0tLS1CRUdJTi0..." # Base64 编码的客户端密钥 -} - -# 响应 201 -{ - "id": "cluster-abc123", - "name": "Production Cluster", - "host": "https://k8s.example.com:6443", - "description": "生产环境集群", - "status": "healthy", - "createdAt": "2025-11-09T10:00:00Z", - "updatedAt": "2025-11-09T10:00:00Z" -} -``` - -### 列出所有集群 - -```bash -GET /api/v1/clusters - -# 响应 200 -[ - { - "id": "cluster-abc123", - "name": "Production Cluster", - "host": "https://k8s.example.com:6443", - "description": "生产环境集群", - "status": "healthy", - "createdAt": "2025-11-09T10:00:00Z" - } -] -``` - -### 获取集群详情 - -```bash -GET /api/v1/clusters/{clusterId} - -# 响应 200 -{ - "id": "cluster-abc123", - "name": "Production Cluster", - "host": "https://k8s.example.com:6443", - "description": "生产环境集群", - "status": "healthy", - "version": "v1.28.0", - "nodeCount": 5, - "createdAt": "2025-11-09T10:00:00Z", - "updatedAt": "2025-11-09T10:00:00Z" -} -``` - -### 更新集群 - -```bash -PUT /api/v1/clusters/{clusterId} -Content-Type: application/json - -{ - "name": "Production Cluster (Updated)", - "description": "更新后的描述" -} - -# 响应 200 -{ - "id": "cluster-abc123", - "name": "Production Cluster (Updated)", - "description": "更新后的描述", - "updatedAt": "2025-11-09T11:00:00Z" -} -``` - -### 删除集群 - -```bash -DELETE /api/v1/clusters/{clusterId} - -# 响应 204 No Content -``` - -### 集群健康检查 - -```bash -GET /api/v1/clusters/{clusterId}/health - -# 响应 200 -{ - "clusterId": "cluster-abc123", - "status": "healthy", - "version": "v1.28.0", - "nodeCount": 5, - "readyNodes": 5, - "cpuCapacity": "40 cores", - "memoryCapacity": "160Gi", - "checkedAt": "2025-11-09T12:00:00Z" -} -``` - ---- - -## Registry 管理 API - -> **OCI 标准**: 所有 Registry 都遵循 OCI (Open Container Initiative) 标准,支持 Harbor, Docker Hub, GHCR, Nexus, 以及任何兼容 OCI Distribution Spec 的 Registry。 - -### 创建 Registry - -```bash -POST /api/v1/registries -Content-Type: application/json - -{ - "name": "Harbor Production", - "url": "https://harbor.example.com", - "description": "生产环境 Harbor 仓库", - "username": "admin", - "password": "secret", - "insecure": false -} - -# 响应 201 -{ - "id": "registry-123", - "name": "Harbor Production", - "url": "https://harbor.example.com", - "description": "生产环境 Harbor 仓库", - "username": "admin", - "insecure": false, - "createdAt": "2025-11-09T10:00:00Z", - "updatedAt": "2025-11-09T10:00:00Z" -} -``` - -**字段说明**: -- `url`: Registry URL,所有 Registry 都使用 OCI Distribution API -- `username/password`: 可选,用于私有 Registry 认证 -- `insecure`: 是否跳过 TLS 验证(开发环境可用) - -> **注意**: 密码不会在响应中返回,存储时会自动加密。 - -### 列出所有 Registries - -```bash -GET /api/v1/registries - -# 响应 200 -[ - { - "id": "registry-123", - "name": "Harbor Production", - "url": "https://harbor.example.com", - "description": "生产环境 Harbor 仓库", - "username": "admin", - "insecure": false, - "createdAt": "2025-11-09T10:00:00Z" - } -] -``` - -### 获取 Registry 详情 - -```bash -GET /api/v1/registries/{registryId} - -# 响应 200 -{ - "id": "registry-123", - "name": "Harbor Production", - "url": "https://harbor.example.com", - "description": "生产环境 Harbor 仓库", - "username": "admin", - "insecure": false, - "createdAt": "2025-11-09T10:00:00Z", - "updatedAt": "2025-11-09T10:00:00Z" -} -``` - -### 更新 Registry - -```bash -PUT /api/v1/registries/{registryId} -Content-Type: application/json - -{ - "name": "Harbor Production (Updated)", - "url": "https://new-harbor.example.com", - "password": "new-secret" # 可选,只在需要更新密码时提供 -} - -# 响应 200 -{ - "id": "registry-123", - "name": "Harbor Production (Updated)", - "url": "https://new-harbor.example.com", - "updatedAt": "2025-11-09T11:00:00Z" -} -``` - -### 删除 Registry - -```bash -DELETE /api/v1/registries/{registryId} - -# 响应 204 No Content -``` - -### Registry 健康检查 - -```bash -GET /api/v1/registries/{registryId}/health - -# 响应 200 -{ - "registryId": "registry-123", - "status": "healthy", - "url": "https://harbor.example.com", - "reachable": true, - "authenticated": true, - "responseTime": 125, # 毫秒 - "checkedAt": "2025-11-09T12:00:00Z" -} - -# 响应 503 (不健康) -{ - "registryId": "registry-123", - "status": "unhealthy", - "url": "https://harbor.example.com", - "reachable": false, - "error": "connection timeout", - "checkedAt": "2025-11-09T12:00:00Z" -} -``` - ---- - -## Artifact 浏览 API - -### 列出 Repositories - -```bash -GET /api/v1/registries/{registryId}/repositories - -# 响应 200 -{ - "registryId": "registry-123", - "registryUrl": "https://harbor.example.com", - "repositories": [ - "charts/nginx", - "charts/redis", - "charts/vllm-serve", - "library/alpine" - ], - "total": 4, - "catalogSupported": true, - "source": "catalog" -} -``` - -> **注意**: 需要 Registry 支持 `_catalog` API(OCI Distribution Spec)。 - -### 列出 Artifacts - -```bash -GET /api/v1/registries/{registryId}/repositories/{repository_name}/artifacts - -# 示例(需要 URL 编码) -GET /api/v1/registries/registry-123/repositories/charts%2Fnginx/artifacts - -# 响应 200 -{ - "repositoryName": "charts/nginx", - "tags": [ - { - "name": "1.0.0", - "digest": "sha256:abc123def456...", - "type": "chart", - "size": 12345678, - "createdAt": "2025-11-01T10:00:00Z" - } - ], - "total": 1 -} -``` - -**Artifact 类型识别**: -- `chart`: Helm Chart -- `image`: Docker Image / OCI Image -- `other`: 其他类型 - -### 获取 Artifact 详情 - -```bash -GET /api/v1/registries/{registryId}/repositories/{repository_name}/artifacts/{reference} - -# reference 可以是 tag 或 digest -GET /api/v1/registries/registry-123/repositories/charts%2Fnginx/artifacts/1.0.0 - -# 响应 200 -{ - "repositoryName": "charts/nginx", - "tag": "1.0.0", - "digest": "sha256:abc123def456...", - "type": "chart", - "size": 12345678, - "createdAt": "2025-11-01T10:00:00Z" -} -``` - -**字段说明**: -- `repositoryName`: 仓库名称 -- `tag`: 标签名称(如果使用 tag 引用) -- `digest`: SHA256 摘要 -- `type`: 制品类型,从 mediaType 自动识别: - - `chart`: Helm Chart(mediaType 包含 `helm.config` 或 `helm.chart`) - - `image`: Docker/OCI Image(mediaType 包含 `docker.container.image` 或 `oci.image`) - - `other`: 其他类型 -- `size`: 总大小(字节) -- `createdAt`: 创建时间(ISO 8601 格式) - -### 获取 Helm Chart Values Schema - -```bash -GET /api/v1/registries/{registryId}/repositories/{repository_name}/artifacts/{reference}/values-schema - -# 仅支持 Helm Chart 类型 -GET /api/v1/registries/registry-123/repositories/charts%2Fnginx/artifacts/1.0.0/values-schema - -# 响应 200 -{ - "repositoryName": "charts/nginx", - "tag": "1.0.0", - "valuesSchema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "replicaCount": { - "type": "integer", - "default": 1, - "description": "Number of replicas" - }, - "image": { - "type": "object", - "properties": { - "repository": { - "type": "string", - "default": "nginx" - }, - "tag": { - "type": "string", - "default": "latest" - } - } - } - } - } -} -``` - -> **URL 编码提示**: Repository 名称包含斜杠时(如 `charts/nginx`),需要编码为 `charts%2Fnginx`。 - ---- - -## 实例管理 API - -### 安装应用 - -```bash -POST /api/v1/clusters/{clusterId}/instances -Content-Type: application/json - -# 方式 1: 使用 JSON values -{ - "name": "my-nginx", - "namespace": "default", - "registryId": "registry-123", - "repository": "charts/nginx", - "chart": "nginx", - "version": "1.0.0", - "description": "My NGINX deployment", - "values": { - "replicaCount": 2, - "image": { - "tag": "1.21.0" - } - } -} - -# 响应 201 -{ - "id": "instance-xyz789", - "name": "my-nginx", - "namespace": "default", - "clusterId": "cluster-abc123", - "registryId": "registry-123", - "chart": "nginx", - "version": "1.0.0", - "status": "deployed", - "revision": 1, - "description": "My NGINX deployment", - "createdAt": "2025-11-09T10:00:00Z", - "updatedAt": "2025-11-09T10:00:00Z" -} -``` - -### 列出应用实例 - -```bash -GET /api/v1/clusters/{clusterId}/instances - -# 响应 200 -[ - { - "id": "instance-xyz789", - "name": "my-nginx", - "namespace": "default", - "chart": "nginx", - "version": "1.0.0", - "status": "deployed", - "revision": 1, - "createdAt": "2025-11-09T10:00:00Z" - } -] -``` - -### 升级应用 - -```bash -PUT /api/v1/clusters/{clusterId}/instances/{instanceId} -Content-Type: application/json - -{ - "version": "1.1.0", - "values": { - "replicaCount": 3 - }, - "description": "Upgrade to v1.1.0" -} - -# 响应 200 -{ - "id": "instance-xyz789", - "name": "my-nginx", - "version": "1.1.0", - "status": "deployed", - "revision": 4, - "updatedAt": "2025-11-09T12:00:00Z" -} -``` - -### 卸载应用 - -```bash -DELETE /api/v1/clusters/{clusterId}/instances/{instanceId} - -# 响应 204 No Content -``` - -### 获取实例访问入口 - -```bash -GET /api/v1/clusters/{clusterId}/instances/{instanceId}/entries - -# 响应 200 -[ - { - "kind": "Service", - "name": "test-nginx", - "namespace": "default", - "type": "ClusterIP", - "clusterIP": "10.43.120.15", - "ports": [ - { - "name": "http", - "protocol": "TCP", - "port": 80, - "targetPort": "http" - } - ] - }, - { - "kind": "Ingress", - "name": "test-nginx", - "namespace": "default", - "type": "nginx", - "hosts": [ - { - "host": "nginx.example.com", - "paths": [ - { - "path": "/", - "serviceName": "test-nginx", - "servicePort": "http" - } - ] - } - ], - "loadBalancerIngress": [ - "34.120.0.12" - ] - } -] -``` - ---- - -## 监控 API - -### 列出集群监控信息 - -```bash -GET /api/v1/monitoring/clusters - -# 响应 200 -[ - { - "clusterId": "cluster-abc123", - "clusterName": "Production Cluster", - "status": "healthy", - "cpuUsage": 45.2, - "memoryUsage": 62.8, - "nodeCount": 5, - "readyNodes": 5, - "podCount": 120, - "timestamp": "2025-11-09T12:00:00Z" - } -] -``` - -### 获取监控摘要 - -```bash -GET /api/v1/monitoring/summary - -# 响应 200 -{ - "totalClusters": 3, - "healthyClusters": 2, - "unhealthyClusters": 1, - "totalNodes": 15, - "totalInstances": 45, - "averageCpuUsage": 42.3, - "averageMemoryUsage": 58.6, - "timestamp": "2025-11-09T12:00:00Z" -} -``` - ---- - -## 响应格式 - -### 成功响应 - -**单个资源**: - -```json -{ - "id": "resource-123", - "name": "Resource Name", - "status": "active", - "createdAt": "2025-11-09T10:00:00Z" -} -``` - -**资源列表**: - -```json -[ - { - "id": "resource-123", - "name": "Resource 1" - }, - { - "id": "resource-456", - "name": "Resource 2" - } -] -``` - -### HTTP 状态码 - -| 状态码 | 说明 | 使用场景 | -|--------|------|----------| -| 200 | OK | 请求成功 | -| 201 | Created | 资源创建成功 | -| 204 | No Content | 删除成功(无返回内容) | -| 400 | Bad Request | 请求参数错误 | -| 401 | Unauthorized | 未认证或 Token 无效 | -| 403 | Forbidden | 无权限访问 | -| 404 | Not Found | 资源不存在 | -| 409 | Conflict | 资源冲突(如重复创建) | -| 500 | Internal Server Error | 服务器内部错误 | -| 503 | Service Unavailable | 服务不可用 | - ---- - -## 错误处理 - -### 错误响应格式 - -```json -{ - "error": "Error Title", - "message": "Detailed error message", - "code": "ERROR_CODE", - "details": {...} -} -``` - -### 常见错误示例 - -#### 400 Bad Request - -```json -{ - "error": "Invalid Request", - "message": "Field 'name' is required" -} -``` - -#### 401 Unauthorized - -```json -{ - "error": "Unauthorized", - "message": "Invalid or expired token" -} -``` - -#### 404 Not Found - -```json -{ - "error": "Not Found", - "message": "Cluster with ID 'cluster-123' not found" -} -``` - -#### 500 Internal Server Error - -```json -{ - "error": "Internal Server Error", - "message": "Failed to connect to database" -} -``` - ---- - -# Part 2: 测试文档 - -## 测试策略 - -OCDP Backend 采用多层测试策略,确保代码质量和系统稳定性。 - -### 测试金字塔 - -``` - ┌────────────┐ - ╱ E2E ╱│ 少量(慢速、昂贵) - ╱ 集成测试 ╱ │ 中等(中速、适度) - ╱ 单元测试 ╱ │ 大量(快速、便宜) - └──────────┘ │ - │ │ - └────────────┘ -``` - -### 测试类型 - -| 测试类型 | 范围 | 速度 | 依赖 | 执行频率 | -|---------|------|------|------|----------| -| **单元测试** | Domain Layer | 快 | 无 | 每次提交 | -| **集成测试** | 跨层交互 | 中 | Mock | 每次提交 | -| **API 测试** | HTTP 接口 | 中 | Mock | 每次 PR | -| **E2E 测试** | 完整流程 | 慢 | 真实环境 | 发布前 | - ---- - -## 单元测试 - -### 测试 Domain Service - -单元测试专注于业务逻辑,使用 Mock Repository。 - -#### 示例:测试 ClusterService - -```go -// internal/domain/service/cluster_service_test.go -package service_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "ocdp-backend/internal/adapter/output/persistence/mock" - "ocdp-backend/internal/domain/service" -) - -func TestClusterService_CreateCluster(t *testing.T) { - // Arrange - repo := mock.NewClusterRepositoryMock() - svc := service.NewClusterService(repo) - - ctx := context.Background() - name := "Test Cluster" - host := "https://k8s.test:6443" - - // Act - cluster, err := svc.CreateCluster(ctx, name, host, "desc", "ca", "cert", "key") - - // Assert - require.NoError(t, err) - assert.NotEmpty(t, cluster.ID) - assert.Equal(t, name, cluster.Name) - assert.Equal(t, host, cluster.Host) -} - -func TestClusterService_GetCluster(t *testing.T) { - // Arrange - repo := mock.NewClusterRepositoryMock() - svc := service.NewClusterService(repo) - ctx := context.Background() - - // 先创建集群 - created, _ := svc.CreateCluster(ctx, "Test", "https://k8s.test:6443", "", "", "", "") - - // Act - cluster, err := svc.GetCluster(ctx, created.ID) - - // Assert - require.NoError(t, err) - assert.Equal(t, created.ID, cluster.ID) - assert.Equal(t, created.Name, cluster.Name) -} - -func TestClusterService_GetCluster_NotFound(t *testing.T) { - // Arrange - repo := mock.NewClusterRepositoryMock() - svc := service.NewClusterService(repo) - ctx := context.Background() - - // Act - cluster, err := svc.GetCluster(ctx, "non-existent-id") - - // Assert - assert.Error(t, err) - assert.Nil(t, cluster) -} - -func TestClusterService_ListClusters(t *testing.T) { - // Arrange - repo := mock.NewClusterRepositoryMock() - svc := service.NewClusterService(repo) - ctx := context.Background() - - // 创建多个集群 - svc.CreateCluster(ctx, "Cluster 1", "https://k8s1.test:6443", "", "", "", "") - svc.CreateCluster(ctx, "Cluster 2", "https://k8s2.test:6443", "", "", "", "") - - // Act - clusters, err := svc.ListClusters(ctx) - - // Assert - require.NoError(t, err) - assert.Len(t, clusters, 2) -} - -func TestClusterService_DeleteCluster(t *testing.T) { - // Arrange - repo := mock.NewClusterRepositoryMock() - svc := service.NewClusterService(repo) - ctx := context.Background() - - cluster, _ := svc.CreateCluster(ctx, "Test", "https://k8s.test:6443", "", "", "", "") - - // Act - err := svc.DeleteCluster(ctx, cluster.ID) - - // Assert - require.NoError(t, err) - - // 验证已删除 - _, err = svc.GetCluster(ctx, cluster.ID) - assert.Error(t, err) -} -``` - -#### 示例:测试 AuthService - -```go -// internal/domain/service/auth_service_test.go -package service_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "ocdp-backend/internal/adapter/output/persistence/mock" - "ocdp-backend/internal/domain/service" - "ocdp-backend/internal/pkg/password" - "ocdp-backend/internal/pkg/jwt" -) - -func TestAuthService_Register(t *testing.T) { - // Arrange - userRepo := mock.NewUserRepositoryMock() - hasher := password.NewBcryptHasher() - jwtGen := jwt.NewJWTGenerator("test-secret") - svc := service.NewAuthService(userRepo, hasher, jwtGen) - - ctx := context.Background() - - // Act - user, err := svc.Register(ctx, "testuser", "password123", "test@example.com") - - // Assert - require.NoError(t, err) - assert.NotEmpty(t, user.ID) - assert.Equal(t, "testuser", user.Username) - assert.NotEqual(t, "password123", user.PasswordHash) // 密码已哈希 -} - -func TestAuthService_Login(t *testing.T) { - // Arrange - userRepo := mock.NewUserRepositoryMock() - hasher := password.NewBcryptHasher() - jwtGen := jwt.NewJWTGenerator("test-secret") - svc := service.NewAuthService(userRepo, hasher, jwtGen) - - ctx := context.Background() - - // 先注册用户 - svc.Register(ctx, "testuser", "password123", "test@example.com") - - // Act - response, err := svc.Login(ctx, "testuser", "password123") - - // Assert - require.NoError(t, err) - assert.NotEmpty(t, response.AccessToken) - assert.NotEmpty(t, response.RefreshToken) - assert.Equal(t, "testuser", response.User.Username) -} - -func TestAuthService_Login_WrongPassword(t *testing.T) { - // Arrange - userRepo := mock.NewUserRepositoryMock() - hasher := password.NewBcryptHasher() - jwtGen := jwt.NewJWTGenerator("test-secret") - svc := service.NewAuthService(userRepo, hasher, jwtGen) - - ctx := context.Background() - svc.Register(ctx, "testuser", "password123", "test@example.com") - - // Act - response, err := svc.Login(ctx, "testuser", "wrongpassword") - - // Assert - assert.Error(t, err) - assert.Nil(t, response) -} -``` - -### 运行单元测试 - -```bash -# 运行所有单元测试 -go test ./internal/domain/... - -# 运行特定包 -go test ./internal/domain/service - -# 带覆盖率 -go test -cover ./internal/domain/... - -# 详细输出 -go test -v ./internal/domain/... - -# 生成覆盖率报告 -go test -coverprofile=coverage.out ./internal/domain/... -go tool cover -html=coverage.out -``` - ---- - -## 集成测试 - -集成测试验证跨层交互,使用 Mock 适配器。 - -### 示例:测试 REST Handler + Service - -```go -// internal/adapter/input/http/rest/cluster_handler_test.go -package rest_test - -import ( - "bytes" - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gorilla/mux" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "ocdp-backend/internal/adapter/input/http/dto" - "ocdp-backend/internal/adapter/input/http/rest" - "ocdp-backend/internal/adapter/output/persistence/mock" - "ocdp-backend/internal/domain/service" -) - -func setupClusterHandler() (*rest.ClusterHandler, *service.ClusterService) { - repo := mock.NewClusterRepositoryMock() - svc := service.NewClusterService(repo) - handler := rest.NewClusterHandler(svc) - return handler, svc -} - -func TestClusterHandler_CreateCluster(t *testing.T) { - // Arrange - handler, _ := setupClusterHandler() - - reqBody := dto.CreateClusterRequest{ - Name: "Test Cluster", - Host: "https://k8s.test:6443", - Description: "Test cluster", - CAData: "ca-data", - CertData: "cert-data", - KeyData: "key-data", - } - - body, _ := json.Marshal(reqBody) - req := httptest.NewRequest(http.MethodPost, "/api/v1/clusters", bytes.NewBuffer(body)) - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() - - // Act - handler.CreateCluster(rec, req) - - // Assert - assert.Equal(t, http.StatusCreated, rec.Code) - - var response map[string]interface{} - json.Unmarshal(rec.Body.Bytes(), &response) - - assert.NotEmpty(t, response["id"]) - assert.Equal(t, "Test Cluster", response["name"]) -} - -func TestClusterHandler_GetAllClusters(t *testing.T) { - // Arrange - handler, svc := setupClusterHandler() - - // 创建测试数据 - ctx := context.Background() - svc.CreateCluster(ctx, "Cluster 1", "https://k8s1.test:6443", "", "", "", "") - svc.CreateCluster(ctx, "Cluster 2", "https://k8s2.test:6443", "", "", "", "") - - req := httptest.NewRequest(http.MethodGet, "/api/v1/clusters", nil) - rec := httptest.NewRecorder() - - // Act - handler.GetAllClusters(rec, req) - - // Assert - assert.Equal(t, http.StatusOK, rec.Code) - - var clusters []map[string]interface{} - json.Unmarshal(rec.Body.Bytes(), &clusters) - - assert.Len(t, clusters, 2) -} - -func TestClusterHandler_GetCluster(t *testing.T) { - // Arrange - handler, svc := setupClusterHandler() - ctx := context.Background() - - cluster, _ := svc.CreateCluster(ctx, "Test", "https://k8s.test:6443", "", "", "", "") - - req := httptest.NewRequest(http.MethodGet, "/api/v1/clusters/"+cluster.ID, nil) - req = mux.SetURLVars(req, map[string]string{"clusterId": cluster.ID}) - rec := httptest.NewRecorder() - - // Act - handler.GetCluster(rec, req) - - // Assert - assert.Equal(t, http.StatusOK, rec.Code) - - var response map[string]interface{} - json.Unmarshal(rec.Body.Bytes(), &response) - - assert.Equal(t, cluster.ID, response["id"]) -} -``` - -### 运行集成测试 - -```bash -# 使用 Mock 模式运行所有测试 -ADAPTER_MODE=mock go test ./... - -# 测试特定模块 -go test ./internal/adapter/input/http/rest/... - -# 并行测试 -go test -parallel 4 ./... -``` - ---- - -## API 测试 - -使用 HTTP 客户端测试完整的 API 流程。 - -### 使用 cURL 测试 - -```bash -#!/bin/bash -# scripts/test-api.sh - -BASE_URL="http://localhost:8080/api/v1" - -# 健康检查 -echo "=== Health Check ===" -curl -X GET http://localhost:8080/health - -# 注册用户 -echo "\n=== Register User ===" -curl -X POST $BASE_URL/auth/register \ - -H "Content-Type: application/json" \ - -d '{ - "username": "testuser", - "password": "test123", - "email": "test@example.com" - }' - -# 登录 -echo "\n=== Login ===" -LOGIN_RESPONSE=$(curl -X POST $BASE_URL/auth/login \ - -H "Content-Type: application/json" \ - -d '{ - "username": "testuser", - "password": "test123" - }') - -TOKEN=$(echo $LOGIN_RESPONSE | jq -r '.accessToken') - -# 创建集群 -echo "\n=== Create Cluster ===" -curl -X POST $BASE_URL/clusters \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{ - "name": "Test Cluster", - "host": "https://k8s.test:6443", - "description": "Test cluster", - "caData": "LS0tLS...", - "certData": "LS0tLS...", - "keyData": "LS0tLS..." - }' - -# 列出集群 -echo "\n=== List Clusters ===" -curl -X GET $BASE_URL/clusters \ - -H "Authorization: Bearer $TOKEN" - -# 创建 Registry -echo "\n=== Create Registry ===" -curl -X POST $BASE_URL/registries \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{ - "name": "Test Harbor", - "url": "https://harbor.test.com", - "username": "admin", - "password": "secret" - }' - -# 列出 Registries -echo "\n=== List Registries ===" -curl -X GET $BASE_URL/registries \ - -H "Authorization: Bearer $TOKEN" -``` - -### 使用 Go 测试 HTTP 接口 - -```go -// test/api/api_test.go -package api_test - -import ( - "bytes" - "encoding/json" - "net/http" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const baseURL = "http://localhost:8080/api/v1" - -func TestAPI_HealthCheck(t *testing.T) { - resp, err := http.Get("http://localhost:8080/health") - require.NoError(t, err) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestAPI_AuthFlow(t *testing.T) { - // 1. 注册 - registerBody := map[string]string{ - "username": "apitest", - "password": "test123", - "email": "apitest@example.com", - } - - body, _ := json.Marshal(registerBody) - resp, err := http.Post(baseURL+"/auth/register", "application/json", bytes.NewBuffer(body)) - require.NoError(t, err) - defer resp.Body.Close() - - assert.Equal(t, http.StatusCreated, resp.StatusCode) - - // 2. 登录 - loginBody := map[string]string{ - "username": "apitest", - "password": "test123", - } - - body, _ = json.Marshal(loginBody) - resp, err = http.Post(baseURL+"/auth/login", "application/json", bytes.NewBuffer(body)) - require.NoError(t, err) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - - var loginResponse map[string]interface{} - json.NewDecoder(resp.Body).Decode(&loginResponse) - - assert.NotEmpty(t, loginResponse["accessToken"]) - assert.NotEmpty(t, loginResponse["refreshToken"]) -} - -func TestAPI_ClusterCRUD(t *testing.T) { - // 先获取 token - token := getAuthToken(t) - - // 1. 创建集群 - clusterBody := map[string]string{ - "name": "API Test Cluster", - "host": "https://k8s.test:6443", - "description": "Created by API test", - "caData": "test-ca", - "certData": "test-cert", - "keyData": "test-key", - } - - body, _ := json.Marshal(clusterBody) - req, _ := http.NewRequest(http.MethodPost, baseURL+"/clusters", bytes.NewBuffer(body)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - - resp, err := http.DefaultClient.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - - assert.Equal(t, http.StatusCreated, resp.StatusCode) - - var cluster map[string]interface{} - json.NewDecoder(resp.Body).Decode(&cluster) - clusterID := cluster["id"].(string) - - // 2. 获取集群 - req, _ = http.NewRequest(http.MethodGet, baseURL+"/clusters/"+clusterID, nil) - req.Header.Set("Authorization", "Bearer "+token) - - resp, err = http.DefaultClient.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - - // 3. 删除集群 - req, _ = http.NewRequest(http.MethodDelete, baseURL+"/clusters/"+clusterID, nil) - req.Header.Set("Authorization", "Bearer "+token) - - resp, err = http.DefaultClient.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - - assert.Equal(t, http.StatusNoContent, resp.StatusCode) -} - -func getAuthToken(t *testing.T) string { - // 登录并返回 token - loginBody := map[string]string{ - "username": "admin", - "password": "admin123", - } - - body, _ := json.Marshal(loginBody) - resp, _ := http.Post(baseURL+"/auth/login", "application/json", bytes.NewBuffer(body)) - defer resp.Body.Close() - - var response map[string]interface{} - json.NewDecoder(resp.Body).Decode(&response) - - return response["accessToken"].(string) -} -``` - ---- - -## E2E 测试 - -端到端测试验证完整的用户场景。 - -### E2E 测试示例 - -```go -// test/e2e/deployment_test.go -package e2e_test - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestE2E_CompleteDeploymentFlow(t *testing.T) { - if testing.Short() { - t.Skip("Skipping E2E test in short mode") - } - - ctx := context.Background() - - // 1. 注册用户 - t.Log("Step 1: Register user") - user := registerUser(t, "e2euser", "password123") - assert.NotEmpty(t, user.ID) - - // 2. 登录获取 token - t.Log("Step 2: Login") - token := login(t, "e2euser", "password123") - assert.NotEmpty(t, token) - - // 3. 创建 Registry - t.Log("Step 3: Create registry") - registry := createRegistry(t, token, "Test Harbor", "https://harbor.test.com") - assert.NotEmpty(t, registry.ID) - - // 4. 创建 Cluster - t.Log("Step 4: Create cluster") - cluster := createCluster(t, token, "Test K8s", "https://k8s.test:6443") - assert.NotEmpty(t, cluster.ID) - - // 5. 部署应用 - t.Log("Step 5: Deploy application") - instance := deployApp(t, token, cluster.ID, registry.ID, "nginx", "1.0.0") - assert.Equal(t, "deployed", instance.Status) - - // 6. 等待应用就绪 - t.Log("Step 6: Wait for application ready") - time.Sleep(10 * time.Second) - - // 7. 检查应用状态 - t.Log("Step 7: Check application status") - status := getInstanceStatus(t, token, cluster.ID, instance.ID) - assert.Equal(t, "deployed", status) - - // 8. 升级应用 - t.Log("Step 8: Upgrade application") - upgraded := upgradeApp(t, token, cluster.ID, instance.ID, "1.1.0") - assert.Equal(t, 2, upgraded.Revision) - - // 9. 卸载应用 - t.Log("Step 9: Uninstall application") - err := uninstallApp(t, token, cluster.ID, instance.ID) - assert.NoError(t, err) - - // 10. 清理 - t.Log("Step 10: Cleanup") - deleteCluster(t, token, cluster.ID) - deleteRegistry(t, token, registry.ID) -} -``` - -### 运行 E2E 测试 - -```bash -# 使用 Production 模式运行 E2E 测试 -ADAPTER_MODE=production DATABASE_URL="..." go test ./test/e2e/... -timeout 30m - -# 跳过 E2E 测试 -go test -short ./... -``` - ---- - -## Mock 测试 - -### Mock Repository 示例 - -所有 Repository 都有 Mock 实现,位于 `internal/adapter/output/persistence/mock/`。 - -```go -// internal/adapter/output/persistence/mock/cluster_repository_mock.go -package mock - -import ( - "context" - "fmt" - "sync" - - "ocdp-backend/internal/domain/entity" - "ocdp-backend/internal/domain/repository" -) - -type ClusterRepositoryMock struct { - clusters map[string]*entity.Cluster - mu sync.RWMutex -} - -func NewClusterRepositoryMock() repository.ClusterRepository { - return &ClusterRepositoryMock{ - clusters: make(map[string]*entity.Cluster), - } -} - -func (r *ClusterRepositoryMock) Create(ctx context.Context, cluster *entity.Cluster) error { - r.mu.Lock() - defer r.mu.Unlock() - - if _, exists := r.clusters[cluster.ID]; exists { - return fmt.Errorf("cluster already exists") - } - - r.clusters[cluster.ID] = cluster - return nil -} - -func (r *ClusterRepositoryMock) GetByID(ctx context.Context, id string) (*entity.Cluster, error) { - r.mu.RLock() - defer r.mu.RUnlock() - - cluster, exists := r.clusters[id] - if !exists { - return nil, fmt.Errorf("cluster not found") - } - - return cluster, nil -} - -func (r *ClusterRepositoryMock) List(ctx context.Context) ([]*entity.Cluster, error) { - r.mu.RLock() - defer r.mu.RUnlock() - - clusters := make([]*entity.Cluster, 0, len(r.clusters)) - for _, cluster := range r.clusters { - clusters = append(clusters, cluster) - } - - return clusters, nil -} - -func (r *ClusterRepositoryMock) Delete(ctx context.Context, id string) error { - r.mu.Lock() - defer r.mu.Unlock() - - if _, exists := r.clusters[id]; !exists { - return fmt.Errorf("cluster not found") - } - - delete(r.clusters, id) - return nil -} -``` - ---- - -## 测试工具 - -### 推荐的测试库 - -```go -import ( - "testing" - - "github.com/stretchr/testify/assert" // 断言 - "github.com/stretchr/testify/require" // 必需条件 - "github.com/stretchr/testify/mock" // Mock 对象 - "github.com/stretchr/testify/suite" // 测试套件 -) -``` - -### 安装测试依赖 - -```bash -go get github.com/stretchr/testify -``` - -### 测试覆盖率 - -```bash -# 生成覆盖率报告 -go test -coverprofile=coverage.out ./... - -# 查看覆盖率 -go tool cover -func=coverage.out - -# HTML 报告 -go tool cover -html=coverage.out -o coverage.html -``` - -### 性能测试 - -```go -func BenchmarkClusterService_CreateCluster(b *testing.B) { - repo := mock.NewClusterRepositoryMock() - svc := service.NewClusterService(repo) - ctx := context.Background() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - svc.CreateCluster(ctx, "Test", "https://k8s.test:6443", "", "", "", "") - } -} - -// 运行性能测试 -// go test -bench=. -benchmem ./internal/domain/service/ -``` - ---- - -## 测试最佳实践 - -### 1. 测试命名规范 - -```go -// ✅ 好的命名 -func TestClusterService_CreateCluster(t *testing.T) -func TestClusterService_CreateCluster_DuplicateName(t *testing.T) -func TestClusterHandler_GetCluster_NotFound(t *testing.T) - -// ❌ 不好的命名 -func TestCreate(t *testing.T) -func Test1(t *testing.T) -``` - -### 2. AAA 模式 (Arrange-Act-Assert) - -```go -func TestExample(t *testing.T) { - // Arrange - 准备测试数据 - repo := mock.NewClusterRepositoryMock() - svc := service.NewClusterService(repo) - - // Act - 执行操作 - cluster, err := svc.CreateCluster(ctx, "Test", "https://k8s.test:6443", "", "", "", "") - - // Assert - 验证结果 - require.NoError(t, err) - assert.NotEmpty(t, cluster.ID) -} -``` - -### 3. 表驱动测试 - -```go -func TestClusterValidation(t *testing.T) { - tests := []struct { - name string - input string - want bool - wantErr bool - }{ - {"valid URL", "https://k8s.example.com:6443", true, false}, - {"invalid URL", "not-a-url", false, true}, - {"empty URL", "", false, true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := validateClusterURL(tt.input) - - if tt.wantErr { - assert.Error(t, err) - } else { - require.NoError(t, err) - assert.Equal(t, tt.want, got) - } - }) - } -} -``` - -### 4. 使用测试辅助函数 - -```go -// test/helpers/helpers.go -package helpers - -func CreateTestCluster(t *testing.T, svc *service.ClusterService) *entity.Cluster { - t.Helper() - - cluster, err := svc.CreateCluster( - context.Background(), - "Test Cluster", - "https://k8s.test:6443", - "", "", "", "", - ) - - require.NoError(t, err) - return cluster -} - -// 在测试中使用 -func TestSomething(t *testing.T) { - cluster := helpers.CreateTestCluster(t, svc) - // ... -} -``` - -### 5. 清理测试数据 - -```go -func TestWithCleanup(t *testing.T) { - repo := mock.NewClusterRepositoryMock() - svc := service.NewClusterService(repo) - ctx := context.Background() - - cluster, _ := svc.CreateCluster(ctx, "Test", "https://k8s.test:6443", "", "", "", "") - - // 确保清理 - t.Cleanup(func() { - svc.DeleteCluster(ctx, cluster.ID) - }) - - // 测试逻辑 - // ... -} -``` - -### 6. 并行测试 - -```go -func TestParallel(t *testing.T) { - t.Parallel() // 标记为可并行 - - // 测试逻辑 -} -``` - -### 7. 跳过测试 - -```go -func TestSomething(t *testing.T) { - if testing.Short() { - t.Skip("Skipping test in short mode") - } - - // 长时间运行的测试 -} - -// 运行: go test -short -``` - ---- - -## CI/CD 集成 - -### GitHub Actions 示例 - -```yaml -# .github/workflows/test.yml -name: Tests - -on: [push, pull_request] - -jobs: - test: - runs-on: ubuntu-latest - - services: - postgres: - image: postgres:15 - env: - POSTGRES_PASSWORD: postgres - POSTGRES_DB: ocdp_test - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 - - steps: - - uses: actions/checkout@v3 - - - name: Setup Go - uses: actions/setup-go@v4 - with: - go-version: '1.21' - - - name: Install dependencies - run: go mod download - - - name: Run unit tests - run: go test -short -cover ./internal/domain/... - - - name: Run integration tests (Mock) - env: - ADAPTER_MODE: mock - run: go test -short ./... - - - name: Run integration tests (Production) - env: - ADAPTER_MODE: production - DATABASE_URL: postgresql://postgres:postgres@localhost:5432/ocdp_test?sslmode=disable - run: go test ./... - - - name: Generate coverage report - run: | - go test -coverprofile=coverage.out ./... - go tool cover -func=coverage.out -``` - ---- - -## 相关文档 - -- [架构文档](architecture.md) -- [部署文档](deployment.md) -- [主 README](../README.md) - ---- - -**Last Updated**: 2025-11-09 -**API Version**: v1 diff --git a/backend/docs/architecture.md b/backend/docs/architecture.md deleted file mode 100644 index 601aa4c..0000000 --- a/backend/docs/architecture.md +++ /dev/null @@ -1,1305 +0,0 @@ -# 🏗️ 架构文档 - -## 目录 - -- [2.1.1 需求描述](#211-需求描述) - - [项目背景](#项目背景) - - [核心需求](#核心需求) - - [功能需求](#功能需求) - - [非功能需求](#非功能需求) -- [2.1.2 业务建模](#212-业务建模) - - [业务领域](#业务领域) - - [核心实体](#核心实体) - - [业务流程](#业务流程) - - [用例场景](#用例场景) -- [2.1.3 技术建模](#213-技术建模) - - [2.1.3.1 六边形架构](#2131-六边形架构) - - [2.1.3.2 技术选型](#2132-技术选型) - ---- - -# 2.1.1 需求描述 - -## 项目背景 - -### 问题陈述 - -在云原生时代,Kubernetes 已成为容器编排的事实标准,Helm 是 Kubernetes 的包管理工具。然而,当前企业在使用 Helm 进行应用部署时面临以下挑战: - -1. **多集群管理复杂** - 企业通常有多个 Kubernetes 集群(开发、测试、生产),需要统一的管理界面 -2. **制品仓库分散** - Helm Chart 可能分布在多个 OCI Registry(Harbor、Docker Hub、GHCR)中,难以统一浏览 -3. **部署流程繁琐** - 需要手动编写 `helm install` 命令,配置复杂的 values.yaml -4. **缺乏可视化** - 命令行操作对非技术人员不友好,缺少直观的 UI -5. **版本管理困难** - 应用升级需要记住历史版本,容易出错 -6. **监控信息分散** - 需要单独访问 Kubernetes Dashboard 查看应用状态 - -### 解决方案 - -OCDP Backend 提供统一的后端 API 服务,实现: - -- ✅ **统一管理** - 集中管理多个 Kubernetes 集群和 OCI Registry -- ✅ **可视化部署** - 通过 API 简化 Helm Chart 的浏览和部署流程 -- ✅ **版本控制** - 完整的应用生命周期管理(安装、升级、卸载) -- ✅ **实时监控** - 集成 Kubernetes API,实时获取应用状态和资源使用情况 -- ✅ **安全认证** - 支持用户认证和敏感数据加密存储 - ---- - -## 核心需求 - -### 业务需求 - -| 需求 ID | 需求描述 | 优先级 | -|---------|---------|--------| -| BR-001 | 支持管理多个 Kubernetes 集群 | P0 | -| BR-002 | 支持管理多个 OCI Registry | P0 | -| BR-003 | 浏览和搜索 Helm Chart 制品 | P0 | -| BR-004 | 部署 Helm Chart 到 Kubernetes 集群 | P0 | -| BR-005 | 升级已部署的应用 | P0 | -| BR-006 | 查看应用实时状态和资源使用 | P1 | -| BR-007 | 用户认证和权限管理 | P1 | -| BR-008 | 审计日志记录 | P2 | - -### 用户角色 - -1. **平台管理员** - 管理集群、Registry、用户 -2. **开发者** - 部署和管理自己的应用 -3. **运维人员** - 监控和维护应用状态 -4. **访客** - 只读查看应用列表和状态 - ---- - -## 功能需求 - -### F1. 集群管理 - -**描述**: 管理多个 Kubernetes 集群的连接配置 - -**功能点**: -- 添加集群(配置 API Server 地址、证书) -- 查看集群列表和详情 -- 测试集群连接健康状态 -- 更新和删除集群配置 -- 查看集群资源使用情况(CPU、内存、节点数) - -**验收标准**: -- ✅ 支持证书认证和 Token 认证 -- ✅ 敏感信息(证书、密钥)加密存储 -- ✅ 连接失败时给出清晰的错误提示 -- ✅ 支持测试连接功能 - ---- - -### F2. Registry 管理 - -**描述**: 管理多个 OCI Registry 的连接配置 - -**功能点**: -- 添加 Registry(Harbor、Docker Hub、GHCR 等) -- 配置认证信息(用户名/密码) -- 查看 Registry 列表和详情 -- 测试 Registry 连接健康状态 -- 更新和删除 Registry 配置 - -**验收标准**: -- ✅ 支持 Basic Auth 和 Bearer Token -- ✅ 密码加密存储 -- ✅ 支持 HTTP/HTTPS 和自签名证书 -- ✅ 连接测试返回响应时间 - ---- - -### F3. Artifact 浏览 - -**描述**: 浏览和搜索 OCI Registry 中的 Helm Chart - -**功能点**: -- 列出 Registry 中的所有仓库 -- 列出仓库中的所有制品(tags) -- 查看制品详情(大小、创建时间、annotations) -- 自动识别制品类型(Helm Chart、Docker Image) -- 获取 Helm Chart 的 values schema - -**验收标准**: -- ✅ 符合 OCI Distribution Specification -- ✅ 支持 URL 编码的仓库名称(如 `charts/app`) -- ✅ 正确解析 manifest 和 config -- ✅ 计算制品总大小(包含所有 layers) - ---- - -### F4. 应用部署 - -**描述**: 部署 Helm Chart 到 Kubernetes 集群 - -**功能点**: -- 选择集群、Registry、Chart 和版本 -- 配置 values(JSON 或 YAML) -- 安装应用到指定 namespace -- 查看安装进度和状态 -- 获取应用访问端点 - -**验收标准**: -- ✅ 支持自定义 Release 名称 -- ✅ 支持 JSON 和 YAML 格式的 values -- ✅ 记录部署历史 - ---- - -### F5. 应用生命周期管理 - -**描述**: 管理已部署应用的完整生命周期 - -**功能点**: -- 查看应用列表和详情 -- 升级应用到新版本 -- 查看部署历史 -- 卸载应用 - -**验收标准**: -- ✅ 升级时保留配置 -- ✅ 卸载时可选保留历史 -- ✅ 显示每次部署的描述信息 - ---- - -### F6. 监控和状态 - -**描述**: 实时监控应用和集群状态 - -**功能点**: -- 查看应用实时状态(Running、Failed 等) -- 查看应用资源使用(CPU、内存) -- 查看集群整体监控 -- 查看节点资源使用 -- 监控摘要统计 - -**验收标准**: -- ✅ 实时获取 Kubernetes 资源状态 -- ✅ 支持 Prometheus 指标集成 -- ✅ 显示 Pod 状态和事件 -- ✅ 资源使用百分比显示 - ---- - -### F7. 认证和授权 - -**描述**: 用户身份认证和访问控制 - -**功能点**: -- 用户注册和登录 -- JWT Token 认证 -- Token 刷新机制 -- 密码加密存储 - -**验收标准**: -- ✅ 使用 bcrypt 哈希密码 -- ✅ JWT Token 有效期配置 -- ✅ Refresh Token 支持 -- ✅ 密码强度验证 - ---- - -## 非功能需求 - -### NFR1. 性能要求 - -| 指标 | 要求 | -|------|------| -| API 响应时间 | P95 < 500ms | -| 并发用户数 | 支持 100+ 并发 | -| 数据库连接池 | 25 个连接 | -| Helm 操作超时 | 5 分钟 | - -### NFR2. 可用性 - -- **系统可用性**: 99.5% -- **故障恢复时间**: < 5 分钟 -- **数据备份**: 每日备份 -- **健康检查**: 提供 `/health` 端点 - -### NFR3. 安全性 - -- **数据加密**: AES-256 加密敏感数据 -- **传输安全**: 支持 HTTPS -- **认证方式**: JWT Token -- **密码策略**: 最小长度 8 位 -- **审计日志**: 记录所有操作(未来) - -### NFR4. 可扩展性 - -- **水平扩展**: 支持多实例部署 -- **数据库**: PostgreSQL 支持主从复制 -- **无状态设计**: API 服务无状态 -- **缓存策略**: 支持 Redis(未来) - -### NFR5. 可维护性 - -- **代码质量**: 遵循 Go 最佳实践 -- **测试覆盖**: > 70% -- **文档完整**: API 文档、架构文档、部署文档 -- **日志记录**: 结构化日志 -- **监控指标**: Prometheus 指标(未来) - -### NFR6. 兼容性 - -- **Kubernetes 版本**: 1.24+ -- **Helm 版本**: 3.x -- **OCI Registry**: 符合 OCI Distribution Spec -- **数据库**: PostgreSQL 15+ -- **Go 版本**: 1.21+ - ---- - -# 2.1.2 业务建模 - -## 业务领域 - -OCDP Backend 属于**云原生应用管理**领域,主要涉及以下子域: - -1. **制品管理域** - OCI Registry、Helm Chart、Docker Image -2. **集群管理域** - Kubernetes Cluster、Node、Namespace -3. **应用部署域** - Helm Release、Application Instance -4. **监控运维域** - Metrics、Logs、Events - ---- - -## 核心实体 - -### 领域模型图 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Core Domain Entities │ -└─────────────────────────────────────────────────────────────┘ - -┌──────────┐ ┌──────────┐ ┌──────────┐ -│ User │─────────│ Cluster │─────────│ Instance │ -└──────────┘ └──────────┘ └──────────┘ - │ │ │ - │ │ │ - ▼ ▼ ▼ -┌──────────┐ ┌──────────┐ ┌──────────┐ -│ Auth │ │ Health │ │ Status │ -│ Token │ │ Check │ │ Resource │ -└──────────┘ └──────────┘ └──────────┘ - -┌──────────┐ ┌──────────┐ ┌──────────┐ -│ Registry │─────────│Artifact │─────────│ Chart │ -└──────────┘ └──────────┘ └──────────┘ - │ │ │ - │ │ │ - ▼ ▼ ▼ -┌──────────┐ ┌──────────┐ ┌──────────┐ -│ Health │ │Repository│ │ Values │ -│ Check │ │ │ │ Schema │ -└──────────┘ └──────────┘ └──────────┘ -``` - -### 实体详解 - -#### 1. User(用户) - -**职责**: 表示系统用户,负责身份认证 - -**属性**: -- `ID`: 唯一标识符 -- `Username`: 用户名(唯一) -- `Email`: 邮箱 -- `PasswordHash`: 密码哈希(bcrypt) -- `CreatedAt`: 创建时间 -- `UpdatedAt`: 更新时间 - -**业务规则**: -- 用户名唯一 -- 邮箱格式验证 -- 密码最小长度 8 位 - ---- - -#### 2. Cluster(集群) - -**职责**: 表示 Kubernetes 集群连接配置 - -**属性**: -- `ID`: 唯一标识符 -- `Name`: 集群名称 -- `Host`: API Server 地址 -- `Description`: 描述 -- `CAData`: CA 证书(加密存储) -- `CertData`: 客户端证书(加密存储) -- `KeyData`: 客户端密钥(加密存储) -- `Token`: Bearer Token(可选) -- `CreatedAt`: 创建时间 -- `UpdatedAt`: 更新时间 - -**业务规则**: -- 集群名称唯一 -- 必须提供证书或 Token -- Host 必须是有效的 HTTPS URL - ---- - -#### 3. Registry(镜像仓库) - -**职责**: 表示 OCI Registry 连接配置 - -**属性**: -- `ID`: 唯一标识符 -- `Name`: Registry 名称 -- `URL`: Registry URL -- `Description`: 描述 -- `Username`: 用户名 -- `Password`: 密码(加密存储) -- `Insecure`: 是否跳过 SSL 验证 -- `CreatedAt`: 创建时间 -- `UpdatedAt`: 更新时间 - -**业务规则**: -- Registry 名称唯一 -- URL 必须是有效的 HTTP/HTTPS URL -- 密码加密存储 - ---- - -#### 4. Instance(应用实例) - -**职责**: 表示部署在 Kubernetes 中的 Helm Release - -**属性**: -- `ID`: 唯一标识符 -- `Name`: Release 名称 -- `Namespace`: Kubernetes namespace -- `ClusterID`: 所属集群 -- `RegistryID`: Chart 来源 Registry -- `Repository`: Chart 仓库名 -- `Chart`: Chart 名称 -- `Version`: Chart 版本 -- `Status`: 部署状态 -- `Revision`: 当前版本号 -- `Values`: 配置值(JSON) -- `Description`: 描述 -- `CreatedAt`: 创建时间 -- `UpdatedAt`: 更新时间 - -**业务规则**: -- 同一集群和 namespace 下 Release 名称唯一 -- 必须关联有效的 Cluster 和 Registry -- Values 必须是有效的 JSON 或 YAML - ---- - -#### 5. Artifact(制品) - -**职责**: 表示 OCI Registry 中的制品(Helm Chart、Docker Image 等) - -**属性**: -- `RepositoryName`: 仓库名称 -- `Tag`: 标签 -- `Digest`: SHA256 摘要 -- `Type`: 制品类型(helm、docker、oci) -- `Size`: 总大小 -- `MediaType`: 媒体类型 -- `Annotations`: 元数据 -- `CreatedAt`: 创建时间 - -**业务规则**: -- Tag 或 Digest 至少提供一个 -- 自动识别制品类型 -- 计算所有 layers 的总大小 - ---- - -## 业务流程 - -### 流程 1: 用户注册和登录 - -``` -┌──────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ -│ User │ │ Handler │ │ Service │ │ Repo │ -└──┬───┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ - │ │ │ │ - │ Register │ │ │ - ├────────────>│ │ │ - │ │ Create User │ │ - │ ├──────────────>│ │ - │ │ │ Hash Password │ - │ │ ├──────┐ │ - │ │ │<─────┘ │ - │ │ │ Save User │ - │ │ ├──────────────>│ - │ │ │<──────────────┤ - │ │<──────────────┤ │ - │<────────────┤ │ │ - │ │ │ │ - │ Login │ │ │ - ├────────────>│ │ │ - │ │ Authenticate │ │ - │ ├──────────────>│ │ - │ │ │ Verify Pwd │ - │ │ ├──────┐ │ - │ │ │<─────┘ │ - │ │ │ Gen JWT │ - │ │ ├──────┐ │ - │ │ │<─────┘ │ - │ │<──────────────┤ │ - │<────────────┤ Return Token │ │ - │ │ │ │ -``` - ---- - -### 流程 2: 部署应用 - -``` -┌──────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ -│ User │ │ Handler │ │ Service │ │HelmClient│ │Kubernetes│ -└──┬───┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ - │ │ │ │ │ - │ Deploy App │ │ │ │ - ├────────────>│ │ │ │ - │ │ Create Inst │ │ │ - │ ├──────────────>│ │ │ - │ │ │ Validate │ │ - │ │ ├──────┐ │ │ - │ │ │<─────┘ │ │ - │ │ │ Pull Chart │ │ - │ │ ├──────────────>│ │ - │ │ │<──────────────┤ │ - │ │ │ Install Chart │ │ - │ │ ├──────────────>│ │ - │ │ │ │ Apply K8s │ - │ │ │ ├──────────────>│ - │ │ │ │<──────────────┤ - │ │ │<──────────────┤ │ - │ │ │ Save Instance │ │ - │ │ ├──────┐ │ │ - │ │ │<─────┘ │ │ - │ │<──────────────┤ │ │ - │<────────────┤ Return Status │ │ │ - │ │ │ │ │ -``` - ---- - -### 流程 3: 浏览制品 - -``` -┌──────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ -│ User │ │ Handler │ │ Service │ │OCIClient │ │ Registry │ -└──┬───┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ - │ │ │ │ │ - │List Repos │ │ │ │ - ├────────────>│ │ │ │ - │ │ Get Repos │ │ │ - │ ├──────────────>│ │ │ - │ │ │ List Catalog │ │ - │ │ ├──────────────>│ │ - │ │ │ │ GET _catalog │ - │ │ │ ├──────────────>│ - │ │ │ │<──────────────┤ - │ │ │<──────────────┤ │ - │ │<──────────────┤ │ │ - │<────────────┤ │ │ │ - │ │ │ │ │ - │List Tags │ │ │ │ - ├────────────>│ │ │ │ - │ │ Get Artifacts │ │ │ - │ ├──────────────>│ │ │ - │ │ │ List Tags │ │ - │ │ ├──────────────>│ │ - │ │ │ │ GET tags/list │ - │ │ │ ├──────────────>│ - │ │ │ │<──────────────┤ - │ │ │ Get Manifest │ │ - │ │ ├──────────────>│ │ - │ │ │ │ GET manifest │ - │ │ │ ├──────────────>│ - │ │ │ │<──────────────┤ - │ │ │<──────────────┤ │ - │ │<──────────────┤ │ │ - │<────────────┤ │ │ │ - │ │ │ │ │ -``` - ---- - -## 用例场景 - -### UC1: 开发者部署测试应用 - -**主角**: 开发者 Alice - -**前置条件**: -- Alice 已登录系统 -- 系统中已配置开发环境集群 -- 系统中已配置 Harbor Registry - -**基本流程**: -1. Alice 选择"开发集群" -2. Alice 浏览 Harbor 中的"charts/nginx"仓库 -3. Alice 选择 nginx 1.0.0 版本 -4. Alice 配置 values: `{"replicaCount": 2}` -5. Alice 点击"部署" -6. 系统显示部署进度 -7. 部署成功,显示应用访问地址 - -**后置条件**: -- nginx 应用成功部署到开发集群 -- 应用状态为 "deployed" -- Alice 可以访问应用 - -**异常流程**: -- 3a. Chart 版本不存在 → 显示错误提示 -- 5a. 部署失败 → 显示错误日志 - ---- - -### UC2: 运维人员升级生产应用 - -**主角**: 运维 Bob - -**前置条件**: -- Bob 已登录系统 -- 生产环境有运行中的 nginx 应用(版本 1.0.0) - -**基本流程**: -1. Bob 进入"生产集群"应用列表 -2. Bob 选择 nginx 应用 -3. Bob 查看当前版本和配置 -4. Bob 点击"升级" -5. Bob 选择新版本 1.1.0 -6. Bob 更新配置: `{"replicaCount": 3}` -7. Bob 添加升级说明 -8. Bob 确认升级 -9. 系统执行滚动升级 -10. 升级成功,Revision 增加到 2 - -**后置条件**: -- nginx 应用升级到 1.1.0 -- 副本数增加到 3 -- 历史记录中保留了 Revision 1 - -**异常流程**: -- 9a. 升级失败 → 显示错误信息,保持原状态 -- 9b. 超时 → 取消升级,保持原状态 - ---- - -### UC3: 管理员添加新集群 - -**主角**: 管理员 Charlie - -**前置条件**: -- Charlie 已登录系统 -- Charlie 有集群的 kubeconfig 文件 - -**基本流程**: -1. Charlie 进入"集群管理"页面 -2. Charlie 点击"添加集群" -3. Charlie 填写集群信息: - - 名称: "Production Cluster" - - API Server: "https://k8s.prod.com:6443" - - 描述: "生产环境集群" -4. Charlie 从 kubeconfig 提取证书数据 -5. Charlie 粘贴 CA、Cert、Key 数据 -6. Charlie 点击"测试连接" -7. 系统显示"连接成功" -8. Charlie 保存配置 - -**后置条件**: -- 新集群添加到系统 -- 集群证书加密存储 -- 其他用户可以使用此集群 - -**异常流程**: -- 6a. 连接失败 → 显示具体错误信息 -- 6b. 证书格式错误 → 提示正确的格式 - ---- - -# 2.1.3 技术建模 - -## 2.1.3.1 六边形架构 - -### 架构概述 - -OCDP Backend 采用**六边形架构**(Hexagonal Architecture,也称为端口和适配器架构),这是一种分层架构模式,强调业务逻辑与外部依赖的分离。 - -### 核心原则 - -1. **依赖倒置** - 所有层依赖 Domain,Domain 无外部依赖 -2. **端口和适配器** - 通过接口(Port)定义交互协议,通过适配器(Adapter)实现具体技术 -3. **可测试性** - 业务逻辑可独立测试,无需外部依赖 -4. **可替换性** - 适配器可轻松替换(Mock ↔ Production) - -### 架构分层图 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Input Adapters │ -│ (HTTP REST API) │ -│ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ Auth Handler │ │Cluster Handl │ │Instance Handl│ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -│ │ -└──────────────────────┬──────────────────────────────────────┘ - │ DTO / Request - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ Domain Layer │ -│ (Business Logic) │ -│ │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ Entities │ │ -│ │ User | Cluster | Registry | Instance | Artifact │ │ -│ └──────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ Services │ │ -│ │ AuthService | ClusterService | InstanceService │ │ -│ └──────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ Repository Interfaces (Ports) │ │ -│ │ UserRepo | ClusterRepo | OCIClient | HelmClient │ │ -│ └──────────────────────────────────────────────────────┘ │ -│ │ -└──────────────────────┬──────────────────────────────────────┘ - │ Interface Contract - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ Output Adapters │ -│ (Infrastructure Implementations) │ -│ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ Database │ │ OCI Client │ │ Helm Client │ │ -│ │ │ │ │ │ │ │ -│ ├──────────────┤ ├──────────────┤ ├──────────────┤ │ -│ │ Mock: Memory │ │ Mock: Static │ │ Mock: Fake │ │ -│ │ Prod:Postgres│ │ Prod: ORAS │ │ Prod: Helm │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 目录结构 - -``` -internal/ -├── domain/ # 🎯 领域层(核心) -│ ├── entity/ # 领域实体 -│ │ ├── user.go -│ │ ├── cluster.go -│ │ ├── registry.go -│ │ ├── instance.go -│ │ └── artifact.go -│ │ -│ ├── service/ # 业务逻辑服务 -│ │ ├── auth_service.go -│ │ ├── cluster_service.go -│ │ ├── registry_service.go -│ │ ├── artifact_service.go -│ │ ├── instance_service.go -│ │ └── monitoring_service.go -│ │ -│ └── repository/ # 接口定义(Output Ports) -│ ├── user_repository.go -│ ├── cluster_repository.go -│ ├── registry_repository.go -│ ├── instance_repository.go -│ ├── oci_client.go -│ ├── helm_client.go -│ └── metrics_client.go -│ -├── adapter/ -│ ├── input/ # 📥 输入适配器 -│ │ └── http/ -│ │ ├── rest/ # REST API Handlers -│ │ │ ├── auth_handler.go -│ │ │ ├── cluster_handler.go -│ │ │ ├── registry_handler.go -│ │ │ ├── artifact_handler.go -│ │ │ ├── instance_handler.go -│ │ │ ├── monitoring_handler.go -│ │ │ └── utils.go -│ │ │ -│ │ └── dto/ # 数据传输对象 -│ │ ├── auth_dto.go -│ │ ├── cluster_dto.go -│ │ ├── registry_dto.go -│ │ └── instance_dto.go -│ │ -│ └── output/ # 📤 输出适配器 -│ ├── persistence/ -│ │ ├── mock/ # ✅ Mock 实现 -│ │ │ ├── user_repository_mock.go -│ │ │ ├── cluster_repository_mock.go -│ │ │ ├── registry_repository_mock.go -│ │ │ └── instance_repository_mock.go -│ │ │ -│ │ └── postgres/ # 🐘 PostgreSQL 实现 -│ │ ├── user_repository.go -│ │ ├── cluster_repository.go -│ │ ├── registry_repository.go -│ │ └── instance_repository.go -│ │ -│ ├── oci/ -│ │ ├── mock/ # ✅ Mock OCI Client -│ │ │ └── oci_client_mock.go -│ │ └── oras_client.go # ORAS SDK 实现 -│ │ -│ ├── helm/ -│ │ ├── mock/ # ✅ Mock Helm Client -│ │ │ └── helm_client_mock.go -│ │ └── helm_client.go # Helm SDK 实现 -│ │ -│ ├── metrics/ -│ │ ├── mock/ # ✅ Mock Metrics Client -│ │ │ └── metrics_client_mock.go -│ │ └── prometheus_client.go -│ │ -│ └── factory.go # 🏭 适配器工厂 -│ -├── bootstrap/ # Bootstrap 预注入模块 -│ ├── config.go -│ └── seeder.go -│ -└── pkg/ # 🔧 工具包 - ├── jwt/ # JWT 工具 - ├── password/ # 密码哈希 - └── encryption/ # AES 加密 -``` - -### 数据流 - -``` -1. HTTP Request - ↓ -2. [REST Handler] (Input Adapter) - - 验证请求参数 - - 转换为 Domain 对象(Entity) - ↓ -3. [Domain Service] (Business Logic) - - 执行业务逻辑 - - 调用 Repository 接口(Port) - ↓ -4. [Repository Implementation] (Output Adapter) - - Mock: 操作内存数据 - - PostgreSQL: 操作数据库 - - ORAS: 与 OCI Registry 交互 - - Helm: 与 Kubernetes 交互 - ↓ -5. Response - - 返回结果到 Service - - Service 返回到 Handler - - Handler 转换为 HTTP 响应 -``` - -### 依赖注入 - -在 `cmd/api/main.go` 中组装所有组件: - -```go -func main() { - // 1. 加载配置 - config := loadConfig() - - // 2. 创建适配器工厂 - factory := output.NewAdapterFactory( - config.AdapterMode, - config.DatabaseURL, - ) - - // 3. 创建 Output Adapters - repos, _ := factory.CreateAllRepositories() - ociClient, _ := factory.CreateOCIClient() - helmClient, _ := factory.CreateHelmClient() - - // 4. 创建工具类 - hasher := password.NewBcryptHasher() - jwtGen := jwt.NewJWTGenerator(config.JWTSecret) - - // 5. 创建 Domain Services - authService := service.NewAuthService(repos.UserRepo, hasher, jwtGen) - clusterService := service.NewClusterService(repos.ClusterRepo) - registryService := service.NewRegistryService(repos.RegistryRepo) - instanceService := service.NewInstanceService( - repos.InstanceRepo, - repos.ClusterRepo, - repos.RegistryRepo, - helmClient, - ) - - // 6. 创建 Input Adapters (REST Handlers) - authHandler := rest.NewAuthHandler(authService) - clusterHandler := rest.NewClusterHandler(clusterService) - instanceHandler := rest.NewInstanceHandler(instanceService) - - // 7. 设置路由 - router := setupRouter( - authHandler, - clusterHandler, - instanceHandler, - ) - - // 8. 启动服务器 - http.ListenAndServe(":8080", router) -} -``` - -### 适配器模式 - -#### 适配器工厂 - -```go -// internal/adapter/output/factory.go -type AdapterMode string - -const ( - ModeMock AdapterMode = "mock" - ModeProduction AdapterMode = "production" -) - -type AdapterFactory struct { - mode AdapterMode - dbConnString string -} - -func (f *AdapterFactory) CreateUserRepository() (repository.UserRepository, error) { - switch f.mode { - case ModeMock: - return mock.NewUserRepositoryMock(), nil - case ModeProduction: - return postgres.NewUserRepository(f.dbConnString) - default: - return nil, fmt.Errorf("unknown adapter mode: %s", f.mode) - } -} -``` - -#### Mock vs Production - -| 接口 | Mock 模式 | Production 模式 | -|------|----------|----------------| -| UserRepository | ✅ 内存 map | 🐘 PostgreSQL | -| ClusterRepository | ✅ 内存 map | 🐘 PostgreSQL | -| RegistryRepository | ✅ 内存 map | 🐘 PostgreSQL | -| InstanceRepository | ✅ 内存 map | 🐘 PostgreSQL | -| OCIClient | ✅ 静态数据 | 🌐 ORAS SDK v2 | -| HelmClient | ✅ 模拟部署 | ☸️ Helm SDK | -| MetricsClient | ✅ 假数据 | 📊 Prometheus | - ---- - -## 2.1.3.2 技术选型 - -### 编程语言 - -**Go 1.21+** - -**选型理由**: -- ✅ 高性能,原生并发支持 -- ✅ 静态类型,编译时检查 -- ✅ 丰富的云原生生态(Kubernetes client-go、Helm SDK、ORAS) -- ✅ 简单易维护,部署方便(单一二进制文件) -- ✅ 广泛应用于云原生领域 - -**替代方案**: Java/Spring Boot, Python/FastAPI - ---- - -### Web 框架 - -**gorilla/mux** - -**选型理由**: -- ✅ 轻量级,性能好 -- ✅ 灵活的路由匹配(支持正则表达式) -- ✅ 与标准库 `net/http` 完美兼容 -- ✅ 活跃的社区支持 - -**使用示例**: -```go -router := mux.NewRouter() -api := router.PathPrefix("/api/v1").Subrouter() -api.HandleFunc("/clusters", handler.CreateCluster).Methods(http.MethodPost) -api.HandleFunc("/clusters/{clusterId}", handler.GetCluster).Methods(http.MethodGet) -``` - -**替代方案**: Gin, Echo, Fiber - ---- - -### 数据库 - -**PostgreSQL 15+** - -**选型理由**: -- ✅ 开源、成熟、可靠 -- ✅ 支持复杂查询和事务 -- ✅ JSONB 类型适合存储动态配置 -- ✅ 主从复制、高可用支持 -- ✅ 与 Go 生态集成良好 - -**数据库驱动**: `lib/pq` 或 `pgx` - -**替代方案**: MySQL, MongoDB - ---- - -### OCI Registry 客户端 - -**ORAS Go SDK v2** - -**选型理由**: -- ✅ 符合 OCI Distribution Specification -- ✅ 官方维护,质量可靠 -- ✅ 支持所有 OCI 兼容的 Registry -- ✅ 完整的 manifest、blob 操作 - -**使用示例**: -```go -repo, err := remote.NewRepository("harbor.example.com/charts/nginx") -repo.Client = &auth.Client{ - Credential: auth.StaticCredential("username", "password"), -} - -tags, err := repo.Tags(ctx) -manifest, err := repo.FetchReference(ctx, "1.0.0") -``` - -**官网**: https://oras.land/ - -**替代方案**: containerd, go-containerregistry - ---- - -### Kubernetes 客户端 - -**client-go** - -**选型理由**: -- ✅ Kubernetes 官方 Go 客户端 -- ✅ 完整的 API 支持 -- ✅ 动态客户端支持 -- ✅ 与 Helm SDK 集成 - -**使用示例**: -```go -config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) -clientset, err := kubernetes.NewForConfig(config) - -pods, err := clientset.CoreV1().Pods("default").List(ctx, metav1.ListOptions{}) -``` - -**替代方案**: 无(标准库) - ---- - -### Helm 客户端 - -**Helm SDK (helm.sh/helm/v3)** - -**选型理由**: -- ✅ Helm 官方 SDK -- ✅ 完整的 Helm 操作支持 -- ✅ Chart 解析和渲染 -- ✅ Release 生命周期管理 - -**使用示例**: -```go -actionConfig := new(action.Configuration) -actionConfig.Init(settings.RESTClientGetter(), namespace, os.Getenv("HELM_DRIVER"), log.Printf) - -install := action.NewInstall(actionConfig) -install.ReleaseName = "my-release" -install.Namespace = "default" - -chart, err := loader.Load(chartPath) -release, err := install.Run(chart, values) -``` - -**替代方案**: 命令行调用 helm(不推荐) - ---- - -### 认证和授权 - -**JWT (golang-jwt/jwt)** - -**选型理由**: -- ✅ 无状态,易扩展 -- ✅ 标准化(RFC 7519) -- ✅ 支持过期时间和刷新 -- ✅ 广泛应用 - -**使用示例**: -```go -token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "user_id": user.ID, - "exp": time.Now().Add(time.Hour * 24).Unix(), -}) - -tokenString, err := token.SignedString([]byte(jwtSecret)) -``` - -**替代方案**: OAuth 2.0, Session - ---- - -### 密码哈希 - -**bcrypt (golang.org/x/crypto/bcrypt)** - -**选型理由**: -- ✅ 安全、抗暴力破解 -- ✅ 自动加盐 -- ✅ 计算成本可调 -- ✅ Go 标准扩展库 - -**使用示例**: -```go -hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) -err = bcrypt.CompareHashAndPassword(hash, []byte(password)) -``` - -**替代方案**: argon2, scrypt - ---- - -### 数据加密 - -**AES-256 (crypto/aes + crypto/cipher)** - -**选型理由**: -- ✅ 对称加密,性能好 -- ✅ 256 位密钥,安全性高 -- ✅ Go 标准库支持 -- ✅ 适合敏感数据加密(证书、密码) - -**使用模式**: GCM(Galois/Counter Mode) - -**使用示例**: -```go -block, err := aes.NewCipher(key) // 32 bytes key -gcm, err := cipher.NewGCM(block) -nonce := make([]byte, gcm.NonceSize()) -ciphertext := gcm.Seal(nonce, nonce, plaintext, nil) -``` - -**替代方案**: RSA (非对称加密,性能较差) - ---- - -### 容器化 - -**Docker + Docker Compose** - -**选型理由**: -- ✅ 标准化容器技术 -- ✅ 简化部署和环境一致性 -- ✅ 丰富的镜像生态 -- ✅ Docker Compose 简化多容器编排 - -**Dockerfile 示例**: -```dockerfile -FROM golang:1.21-alpine AS builder -WORKDIR /app -COPY . . -RUN go build -o ocdp-backend cmd/api/main.go - -FROM alpine:latest -RUN apk --no-cache add ca-certificates -WORKDIR /root/ -COPY --from=builder /app/ocdp-backend . -EXPOSE 8080 -CMD ["./ocdp-backend"] -``` - -**替代方案**: Podman, containerd - ---- - -### 日志记录 - -**标准库 log + 结构化日志(未来:zerolog/zap)** - -**当前实现**: -```go -log.Printf("✅ User created: %s", user.Username) -log.Printf("⚠️ Warning: %v", err) -log.Printf("❌ Error: %v", err) -``` - -**未来优化**: -```go -logger.Info(). - Str("user_id", user.ID). - Str("username", user.Username). - Msg("User created") -``` - ---- - -### 测试框架 - -**testify** - -**选型理由**: -- ✅ 丰富的断言函数 -- ✅ Mock 支持 -- ✅ 测试套件支持 -- ✅ 活跃的社区 - -**使用示例**: -```go -import ( - "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestCreateUser(t *testing.T) { - user, err := service.CreateUser(ctx, "test", "password") - require.NoError(t, err) - assert.NotEmpty(t, user.ID) - assert.Equal(t, "test", user.Username) -} -``` - ---- - -### 热重载(开发) - -**Air** - -**选型理由**: -- ✅ Go 项目热重载 -- ✅ 监听文件变化自动重启 -- ✅ 配置简单 - -**配置文件**: `.air.toml` - -```toml -[build] - cmd = "go build -o ./tmp/main cmd/api/main.go" - bin = "./tmp/main" - include_ext = ["go", "yaml", "json"] - exclude_dir = ["tmp", "vendor"] -``` - -**启动**: `air -c .air.toml` - ---- - -### 技术栈总结表 - -| 组件 | 技术 | 版本 | 用途 | -|------|------|------|------| -| **语言** | Go | 1.21+ | 主要编程语言 | -| **Web 框架** | gorilla/mux | latest | HTTP 路由 | -| **数据库** | PostgreSQL | 15+ | 持久化存储 | -| **数据库驱动** | lib/pq 或 pgx | latest | Go 数据库驱动 | -| **OCI 客户端** | ORAS Go SDK | v2 | OCI Registry 操作 | -| **Kubernetes** | client-go | latest | K8s API 交互 | -| **Helm** | Helm SDK | v3 | Helm 操作 | -| **JWT** | golang-jwt/jwt | v5 | 认证 Token | -| **密码哈希** | bcrypt | latest | 密码加密 | -| **数据加密** | AES-256 | stdlib | 敏感数据加密 | -| **容器化** | Docker | latest | 应用容器化 | -| **编排** | Docker Compose | latest | 多容器编排 | -| **测试** | testify | latest | 单元测试 | -| **热重载** | Air | latest | 开发环境 | - ---- - -### Bootstrap 预注入 - -**概述**: 在应用启动时自动初始化用户、Registry、Cluster 等数据。 - -**配置文件**: `config/bootstrap.json` - -```json -{ - "enabled": true, - "users": [ - { - "username": "admin", - "password": "admin123", - "email": "admin@example.com" - } - ], - "registries": [ - { - "name": "Harbor Production", - "url": "https://harbor.example.com", - "username": "admin", - "password": "secret" - } - ], - "clusters": [ - { - "name": "Production Cluster", - "host": "https://k8s.example.com:6443", - "caData": "LS0tLS...", - "certData": "LS0tLS...", - "keyData": "LS0tLS..." - } - ] -} -``` - -**特性**: -- ✅ 自动加密敏感数据 -- ✅ 幂等性(重复启动不会重复创建) -- ✅ 可通过环境变量配置 -- ✅ 支持禁用 - -**从 kubeconfig 提取证书**: -```bash -kubectl config view --raw -o jsonpath='{.clusters[0].cluster.certificate-authority-data}' -kubectl config view --raw -o jsonpath='{.users[0].user.client-certificate-data}' -kubectl config view --raw -o jsonpath='{.users[0].user.client-key-data}' -``` - ---- - -### 开发指南 - -#### 添加新功能的步骤 - -1. **定义 Entity** (`internal/domain/entity/`) -2. **定义 Repository 接口** (`internal/domain/repository/`) -3. **实现 Domain Service** (`internal/domain/service/`) -4. **实现 Mock Adapter** (`internal/adapter/output/persistence/mock/`) -5. **实现 Production Adapter** (`internal/adapter/output/persistence/postgres/`) -6. **添加到 Factory** (`internal/adapter/output/factory.go`) -7. **创建 REST Handler** (`internal/adapter/input/http/rest/`) -8. **注册路由** (`cmd/api/main.go`) - -#### 依赖方向规则 - -- ✅ Input Adapters → Domain Layer -- ✅ Domain Layer → Repository Interfaces -- ✅ Output Adapters → Domain Layer (实现接口) -- ❌ Domain Layer → Output Adapters(禁止) - -#### 测试策略 - -- **单元测试**: 测试 Domain Service,使用 Mock Repository -- **集成测试**: 测试 Handler + Service,使用 Mock Adapters -- **E2E 测试**: 完整流程测试,使用真实环境 - ---- - -## 相关文档 - -- [API 与测试](api-and-test.md) -- [部署文档](deployment.md) -- [主 README](../README.md) - ---- - -**Last Updated**: 2025-11-09 diff --git a/backend/docs/deployment.md b/backend/docs/deployment.md deleted file mode 100644 index 7bd2ea1..0000000 --- a/backend/docs/deployment.md +++ /dev/null @@ -1,1546 +0,0 @@ -# 🚀 部署文档 - -## 目录 - -- [快速开始](#快速开始) -- [2.4.1 Mock 模式](#241-mock-模式) -- [2.4.2 Dev 模式](#242-dev-模式) -- [2.4.3 Up 模式](#243-up-模式) -- [环境变量配置](#环境变量配置) -- [故障排查](#故障排查) - ---- - -## 快速开始 - -### 三种部署模式 - -| 模式 | 命令 | Backend | 数据库 | 热重载 | 适用场景 | -|------|------|---------|--------|--------|----------| -| **Mock** | `make mock` | 本地 | 内存 | ✅ | 快速开发、API 测试 | -| **Dev** | `make db-up`
`make dev` | 本地 | Docker | ✅ | 日常开发 ⭐ 推荐 | -| **Up** | `make up` | Docker | Docker | ❌ | 集成测试、生产预演 | - -### 使用 Makefile - -```bash -# 查看所有命令 -make help - -# Mock 模式(零依赖,最快启动) -make mock - -# Dev 模式(推荐日常开发) -make db-up # 启动数据库 -make dev # 启动 Backend(热重载) - -# Up 模式(完全容器化) -make up - -# 管理服务 -make logs # 查看日志 -make stop # 停止服务 -``` - -### 验证服务 - -```bash -# 健康检查 -curl http://localhost:8080/health - -# 测试 API -curl http://localhost:8080/api/v1/registries | jq - -# 访问 Swagger UI -open http://localhost:8080/api/docs -``` - ---- - -# 2.4.1 Mock 模式 - -## 概述 - -Mock 模式使用内存存储,无需任何外部依赖,适合快速开发和测试。 - -**特点**: -- ✅ **零依赖** - 无需数据库、Docker 等 -- ✅ **快速启动** - 3 秒内启动 -- ✅ **热重载** - 支持 Air 自动重载 -- ✅ **自带测试数据** - Bootstrap 预注入 -- ✅ **无需容器** - 本地直接运行 -- ❌ **数据不持久** - 重启后数据丢失 - -**适用场景**: -- 🚀 快速功能开发 -- 🧪 API 接口测试 -- 🎨 前端开发联调 -- 📝 编写单元测试 - -**架构**: - -``` -┌─────────────────────────────────────────┐ -│ Mock 模式(本地直接运行) │ -│ │ -│ ┌──────────────────────────────────┐ │ -│ │ OCDP Backend (Go Process) │ │ -│ │ Port: 8080 │ │ -│ │ │ │ -│ │ ┌─────────────────────────┐ │ │ -│ │ │ Air (Hot Reload) │ │ │ -│ │ │ 监听文件变化自动重载 │ │ │ -│ │ └─────────────────────────┘ │ │ -│ │ │ │ -│ │ Adapter: Mock │ │ -│ │ Storage: Memory │ │ -│ └──────────────────────────────────┘ │ -│ │ -│ 无外部依赖 ✨ │ -└─────────────────────────────────────────┘ -``` - ---- - -## 环境准备 - -```bash -# 1. 确保已安装 Go -go version # 需要 Go 1.21+ - -# 2. 安装 Air(热重载工具) -go install github.com/air-verse/air@latest - -# 3. 验证安装 -air -v -``` - ---- - -## 运行方式 - -### 方式 1: 使用 Makefile(推荐) - -```bash -make mock -``` - -### 方式 2: 使用 Air - -```bash -# 1. 设置环境变量 -export ADAPTER_MODE=mock -export PORT=8080 -export JWT_SECRET=dev-secret-key - -# 2. 启动 Air -air -c .air.toml - -# 输出示例: -# __ _ ___ -# / /\ | | | |_) -# /_/--\ |_| |_| \_ v1.49.0 -# -# watching . -# building... -# running... -# 🚀 Starting OCDP Backend (mode=mock) -# 🌐 Server starting on :8080 -``` - -### 方式 3: 直接运行 Go - -```bash -# 设置环境变量 -export ADAPTER_MODE=mock -export PORT=8080 -export JWT_SECRET=dev-secret-key - -# 运行 -go run cmd/api/main.go -``` - ---- - -## 热重载演示 - -修改任何 `.go` 文件后,Air 会自动检测并重启: - -``` -main.go has changed -building... -running... -🚀 Starting OCDP Backend (mode=mock) -🌐 Server starting on :8080 -``` - -**支持的文件类型**: -- `.go` - Go 源文件 -- `.yaml`, `.json` - 配置文件 -- `.toml` - Air 配置文件 - ---- - -## 配置文件 - -Air 配置文件 `.air.toml` 已包含在项目中: - -```toml -root = "." -testdata_dir = "testdata" -tmp_dir = "tmp" - -[build] - cmd = "go build -o ./tmp/main cmd/api/main.go" - bin = "./tmp/main" - include_ext = ["go", "tpl", "tmpl", "html", "yaml", "json"] - exclude_dir = ["assets", "tmp", "vendor", "testdata"] - exclude_regex = ["_test.go"] - delay = 1000 -``` - ---- - -## 测试数据 - -Mock 模式自动加载 `config/bootstrap.json` 中的测试数据: - -```json -{ - "enabled": true, - "users": [ - { - "username": "admin", - "password": "admin123", - "email": "admin@example.com" - } - ], - "registries": [ - { - "name": "Harbor Production", - "url": "https://harbor.example.com", - "username": "admin", - "password": "secret" - } - ], - "clusters": [ - { - "name": "Production Cluster", - "host": "https://k8s.example.com:6443", - "description": "生产环境集群" - } - ] -} -``` - ---- - -## 测试 API - -```bash -# 登录 -curl -X POST http://localhost:8080/api/v1/auth/login \ - -H "Content-Type: application/json" \ - -d '{"username":"admin","password":"admin123"}' | jq - -# 响应: -# { -# "accessToken": "eyJhbGciOiJIUzI1NiIs...", -# "userId": "user-123", -# "username": "admin" -# } - -# 查看 Registries -curl http://localhost:8080/api/v1/registries | jq - -# 查看 Clusters -curl http://localhost:8080/api/v1/clusters | jq -``` - ---- - -## 环境变量 - -| 变量 | 说明 | 默认值 | 必需 | -|------|------|--------|------| -| `ADAPTER_MODE` | 适配器模式 | `mock` | ✅ | -| `PORT` | 服务端口 | `8080` | ❌ | -| `JWT_SECRET` | JWT 密钥 | - | ✅ | - ---- - -## 优势与限制 - -### ✅ 优势 - -1. **启动极快** - 3 秒内启动,无需等待数据库 -2. **零配置** - 无需配置数据库连接、证书等 -3. **自带数据** - Bootstrap 自动注入测试数据 -4. **热重载** - 代码修改立即生效 -5. **易于调试** - 本地运行,支持 IDE 断点调试 -6. **完美测试** - 适合 API 测试和前端联调 - -### ❌ 限制 - -1. **数据不持久** - 重启后数据丢失 -2. **功能简化** - 某些功能(如真实 K8s 操作)被模拟 -3. **不适合生产** - 仅用于开发调试 - ---- - -# 2.4.2 Dev 模式 - -## 概述 - -Dev 模式使用 Docker 运行 PostgreSQL 数据库,Backend 在本地热重载运行,适合日常开发。 - -**特点**: -- ✅ **隔离的依赖服务** - 数据库在容器中,避免污染本地环境 -- ✅ **本地热重载** - Backend 支持 Air 自动重载 -- ✅ **完整的 IDE 支持** - 断点调试、代码补全、性能分析 -- ✅ **快速迭代** - 代码修改立即生效,无需重新构建镜像 -- ✅ **数据持久化** - PostgreSQL 数据持久保存 -- ✅ **最佳开发体验** - 兼顾隔离性和开发效率 - -**适用场景**: -- 📝 **日常功能开发** ⭐ 最推荐 -- 🐛 **代码调试** - 支持 IDE 断点 -- 🔄 **快速迭代** - 热重载提升效率 -- 🧪 **数据持久化测试** - 验证数据库操作 - -**架构**: - -``` -┌──────────────────────────────────────────────────────────┐ -│ Dev 模式(部分容器化,推荐日常开发) │ -│ │ -│ ┌────────────────────────┐ ┌────────────────────┐ │ -│ │ OCDP Backend │ │ PostgreSQL │ │ -│ │ (本地进程) │───▶│ (Docker 容器) │ │ -│ │ Port: 8080 │ │ Port: 5432 │ │ -│ │ │ │ │ │ -│ │ ┌─────────────────┐ │ │ Volume: │ │ -│ │ │ Air (Hot Reload) │ │ │ postgres_data │ │ -│ │ └─────────────────┘ │ └────────────────────┘ │ -│ └────────────────────────┘ │ -│ │ -│ Backend: 本地运行(热重载) │ -│ Database: Docker 容器(隔离环境) │ -└──────────────────────────────────────────────────────────┘ -``` - ---- - -## 环境准备 - -```bash -# 1. 确保已安装 Docker 和 Docker Compose -docker --version -docker compose version - -# 2. 安装 Air(热重载工具) -go install github.com/air-verse/air@latest - -# 3. 验证安装 -air -v -``` - ---- - -## 运行步骤 - -### 步骤 1: 启动数据库 - -```bash -# 使用 Makefile(推荐) -make db-up - -# 或手动启动 -docker compose up -d postgres - -# 等待数据库启动(查看健康状态) -docker compose ps - -# 预期输出: -# NAME IMAGE STATUS PORTS -# ocdp-postgres postgres:17-alpine Up 10s (healthy) 0.0.0.0:5432->5432/tcp -``` - -**验证数据库连接**: - -```bash -# 方式 1: 使用 psql -psql -h localhost -U postgres -d ocdp - -# 方式 2: 使用 Docker -docker exec -it ocdp-postgres psql -U postgres -d ocdp - -# 测试查询 -SELECT current_database(); -\q -``` - ---- - -### 步骤 2: 启动 Backend(热重载) - -**选项 A: 使用 Makefile(推荐)** - -```bash -# 设置环境变量(首次) -export DATABASE_URL="postgresql://postgres:postgres@localhost:5432/ocdp?sslmode=disable" -export ENCRYPTION_KEY="12345678901234567890123456789012" - -# 启动 Backend -make dev - -# 输出: -# 🚀 启动 Dev 模式 - Backend(本地热重载)... -# 🔌 Connecting to database... -# ✅ Database connected -# 🌱 Bootstrap seeding... -# 🌐 Server starting on :8080 -``` - -**选项 B: 使用 Air** - -```bash -# 设置环境变量 -export ADAPTER_MODE=production -export PORT=8080 -export JWT_SECRET=dev-secret-key -export ENCRYPTION_KEY=12345678901234567890123456789012 -export DATABASE_URL="postgresql://postgres:postgres@localhost:5432/ocdp?sslmode=disable" - -# 启动 Air -air -c .air.toml -``` - -**选项 C: 直接运行 Go** - -```bash -export ADAPTER_MODE=production -export DATABASE_URL="postgresql://postgres:postgres@localhost:5432/ocdp?sslmode=disable" -export JWT_SECRET=dev-secret-key -export ENCRYPTION_KEY=12345678901234567890123456789012 - -go run cmd/api/main.go -``` - ---- - -## 验证服务 - -```bash -# 健康检查 -curl http://localhost:8080/health -# 输出: {"status":"healthy"} - -# 注册用户 -curl -X POST http://localhost:8080/api/v1/auth/register \ - -H "Content-Type: application/json" \ - -d '{ - "username": "testuser", - "password": "test123", - "email": "test@example.com" - }' - -# 登录 -curl -X POST http://localhost:8080/api/v1/auth/login \ - -H "Content-Type: application/json" \ - -d '{ - "username": "testuser", - "password": "test123" - }' | jq -``` - ---- - -## 热重载演示 - -修改代码后,Air 会自动检测并重启: - -```bash -# 终端 1: Air 运行中 -# 修改文件: internal/domain/service/cluster_service.go - -# Air 输出: -# cluster_service.go has changed -# building... -# running... -# 🚀 Starting OCDP Backend (mode=production) -# 🔌 Connecting to database... -# ✅ Database connected -# 🌐 Server starting on :8080 - -# 终端 2: 测试 -curl http://localhost:8080/api/v1/clusters | jq -``` - ---- - -## 开发工作流 - -```bash -# 1. 启动数据库(首次或停止后) -make db-up - -# 2. 启动 Backend(热重载) -make dev - -# 3. 修改代码 -vim internal/domain/service/cluster_service.go -# Air 自动检测并重启 - -# 4. 测试 API -curl http://localhost:8080/api/v1/clusters | jq - -# 5. 查看数据库 -make db -SELECT * FROM users; -\q - -# 6. 停止服务 -# - Backend: Ctrl+C -# - Database: make stop -``` - ---- - -## 管理服务 - -**查看服务状态**: - -```bash -# 查看所有容器 -docker compose ps - -# 查看 PostgreSQL 日志 -docker compose logs -f postgres - -# 查看 Backend 日志(在 Air 终端中) -``` - -**停止服务**: - -```bash -# 停止 Backend: 在 Air 终端按 Ctrl+C - -# 停止数据库 -docker compose stop postgres - -# 停止并删除容器(数据保留) -docker compose down - -# 停止并删除容器和数据卷(⚠️ 数据丢失) -docker compose down -v -``` - -**重启服务**: - -```bash -# 重启数据库 -docker compose restart postgres - -# 重启 Backend: 在 Air 终端按 Ctrl+C 后重新运行 make dev -``` - ---- - -## 数据库管理 - -**连接数据库**: - -```bash -# 使用 Makefile -make db - -# 使用 psql -psql -h localhost -U postgres -d ocdp - -# 或使用 Docker -docker exec -it ocdp-postgres psql -U postgres -d ocdp -``` - -**常用查询**: - -```sql --- 查看所有表 -\dt - --- 查看用户 -SELECT id, username, email, created_at FROM users; - --- 查看 Registry -SELECT id, name, url, created_at FROM registries; - --- 查看集群 -SELECT id, name, host, created_at FROM clusters; - --- 清空测试数据 -TRUNCATE users, registries, clusters, instances CASCADE; - --- 退出 -\q -``` - -**数据库备份与恢复**: - -```bash -# 备份 -docker exec ocdp-postgres pg_dump -U postgres ocdp > backup-$(date +%Y%m%d).sql - -# 恢复 -docker exec -i ocdp-postgres psql -U postgres ocdp < backup.sql - -# 压缩备份 -docker exec ocdp-postgres pg_dump -U postgres ocdp | gzip > backup.sql.gz - -# 从压缩文件恢复 -gunzip -c backup.sql.gz | docker exec -i ocdp-postgres psql -U postgres ocdp -``` - ---- - -## IDE 调试配置 - -### VS Code (launch.json) - -```json -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Launch OCDP Backend (Dev Mode)", - "type": "go", - "request": "launch", - "mode": "auto", - "program": "${workspaceFolder}/cmd/api/main.go", - "env": { - "ADAPTER_MODE": "production", - "PORT": "8080", - "JWT_SECRET": "dev-secret-key", - "ENCRYPTION_KEY": "12345678901234567890123456789012", - "DATABASE_URL": "postgresql://postgres:postgres@localhost:5432/ocdp?sslmode=disable" - }, - "args": [] - } - ] -} -``` - -### GoLand / IntelliJ IDEA - -1. Run → Edit Configurations -2. Add New → Go Build -3. 配置: - - **Files**: `cmd/api/main.go` - - **Working directory**: 项目根目录 - - **Environment**: - ``` - ADAPTER_MODE=production - PORT=8080 - JWT_SECRET=dev-secret-key - ENCRYPTION_KEY=12345678901234567890123456789012 - DATABASE_URL=postgresql://postgres:postgres@localhost:5432/ocdp?sslmode=disable - ``` - ---- - -## 环境变量 - -| 变量 | 说明 | 示例 | 必需 | -|------|------|------|------| -| `ADAPTER_MODE` | 适配器模式 | `production` | ✅ | -| `PORT` | 服务端口 | `8080` | ❌ | -| `JWT_SECRET` | JWT 密钥 | `dev-secret` | ✅ | -| `ENCRYPTION_KEY` | 加密密钥(32字节) | `12345678901234567890123456789012` | ✅ | -| `DATABASE_URL` | 数据库连接 | `postgresql://postgres:postgres@localhost:5432/ocdp?sslmode=disable` | ✅ | - -**生成密钥**: - -```bash -# JWT Secret -openssl rand -base64 32 - -# Encryption Key(32 字节) -openssl rand -base64 32 -``` - ---- - -## 故障排查 - -**问题 1: 数据库连接失败** - -```bash -# 错误: connection refused - -# 检查数据库是否启动 -docker compose ps postgres - -# 检查端口 -netstat -tuln | grep 5432 -# 或 -lsof -i :5432 - -# 测试连接 -psql -h localhost -U postgres -d ocdp -c "SELECT 1;" -``` - -**问题 2: 数据库未初始化** - -```bash -# 重新初始化数据库 -docker compose down postgres -docker compose up -d postgres - -# 等待健康检查通过 -docker compose ps postgres -``` - -**问题 3: 端口冲突** - -```bash -# 修改 docker-compose.yml 中的端口映射 -# ports: -# - "5433:5432" # 使用 5433 而不是 5432 - -# 更新 DATABASE_URL -export DATABASE_URL="postgresql://postgres:postgres@localhost:5433/ocdp?sslmode=disable" -``` - ---- - -# 2.4.3 Up 模式 - -## 概述 - -Up 模式使用 Docker Compose 完全容器化部署,PostgreSQL 和 Backend 都在容器中运行。 - -**特点**: -- ✅ **完全容器化** - 一致的运行环境 -- ✅ **一键部署** - 简单快速 -- ✅ **接近生产** - 与生产环境高度一致 -- ✅ **易于分享** - 团队成员快速启动相同环境 -- ❌ **无热重载** - 代码修改需重新构建镜像 -- ❌ **调试受限** - 需要通过日志调试 - -**适用场景**: -- 🧪 **集成测试** - 测试完整系统 -- 🎯 **生产预演** - 验证部署流程 -- 👥 **团队协作** - 快速搭建统一环境 -- 📦 **交付演示** - 向客户展示系统 - -**架构**: - -``` -┌──────────────────────────────────────────────────────────┐ -│ Up 模式(完全容器化) │ -│ │ -│ ┌────────────────────────────────────────────────────┐ │ -│ │ Docker Compose Network │ │ -│ │ │ │ -│ │ ┌──────────────────┐ ┌──────────────────┐ │ │ -│ │ │ OCDP Backend │ │ PostgreSQL │ │ │ -│ │ │ (Docker 容器) │───▶│ (Docker 容器) │ │ │ -│ │ │ Port: 8080 │ │ Port: 5432 │ │ │ -│ │ └──────────────────┘ └──────────────────┘ │ │ -│ │ │ │ -│ └────────────────────────────────────────────────────┘ │ -│ │ -│ ┌────────────────────────────────────────────────────┐ │ -│ │ Persistent Volumes │ │ -│ │ postgres_data: /var/lib/postgresql/data │ │ -│ └────────────────────────────────────────────────────┘ │ -└──────────────────────────────────────────────────────────┘ -``` - ---- - -## 环境准备 - -```bash -# 确保已安装 Docker 和 Docker Compose -docker --version -docker compose version -``` - ---- - -## 目录结构 - -``` -backend/ -├── docker-compose.yml # Docker Compose 配置 -├── Dockerfile # Backend 镜像 -├── .env # 环境变量 -├── env.example # 环境变量示例 -└── config/ - └── bootstrap.json # 初始数据(可选) -``` - ---- - -## 部署步骤 - -### 步骤 1: 准备环境变量 - -```bash -# 复制示例文件 -cp env.example .env - -# 编辑环境变量 -nano .env -``` - -**.env 文件内容**: - -```bash -# 适配器模式 -ADAPTER_MODE=production - -# 端口配置 -BACKEND_PORT=8080 -POSTGRES_PORT=5432 - -# 安全密钥(⚠️ 生产环境必须修改) -JWT_SECRET=your-jwt-secret-change-in-production -ENCRYPTION_KEY=your-32-character-encryption-key-here - -# 数据库配置 -POSTGRES_DB=ocdp -POSTGRES_USER=postgres -POSTGRES_PASSWORD=your-postgres-password-change-it - -# 数据库连接 URL(容器内部使用) -DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable -``` - -**生成安全密钥**: - -```bash -# 生成随机 JWT Secret -openssl rand -base64 32 - -# 生成随机 Encryption Key(32 字节) -openssl rand -base64 32 - -# 生成随机数据库密码 -openssl rand -base64 16 -``` - ---- - -### 步骤 2: 启动服务 - -**使用 Makefile(推荐)**: - -```bash -make up - -# 查看服务状态 -docker compose ps - -# 预期输出: -# NAME IMAGE STATUS PORTS -# ocdp-backend backend:latest Up 30s (healthy) 0.0.0.0:8080->8080/tcp -# ocdp-postgres postgres:17-alpine Up 40s (healthy) 0.0.0.0:5432->5432/tcp -``` - -**手动启动**: - -```bash -# 使用 profile 启动完整服务 -docker compose --profile backend up -d - -# 查看日志 -docker compose logs -f backend postgres -``` - ---- - -### 步骤 3: 验证部署 - -```bash -# 健康检查 -curl http://localhost:8080/health -# 输出: {"status":"healthy"} - -# 测试 API -curl http://localhost:8080/api/v1/registries | jq - -# 访问 Swagger UI -open http://localhost:8080/api/docs - -# 查看容器状态 -docker compose ps - -# 进入 Backend 容器 -docker compose exec backend sh - -# 连接数据库 -make db -``` - ---- - -## 管理服务 - -**停止服务**: - -```bash -# 停止所有服务 -make stop - -# 或 -docker compose down - -# 停止并删除数据卷(⚠️ 会删除数据) -docker compose down -v - -# 仅停止 Backend -docker compose stop backend -``` - -**重启服务**: - -```bash -# 重启所有服务 -make restart - -# 或 -docker compose restart - -# 重启 Backend -docker compose restart backend - -# 重启数据库 -docker compose restart postgres -``` - -**查看日志**: - -```bash -# 查看所有日志 -make logs - -# 或 -docker compose logs -f - -# 查看 Backend 日志 -docker compose logs -f backend - -# 查看最近 100 行 -docker compose logs --tail=100 backend -``` - ---- - -## 重新构建镜像 - -**代码修改后需要重新构建**: - -```bash -# 方式 1: 使用 Makefile -make up-build - -# 方式 2: 手动构建 -docker compose build backend -docker compose --profile backend up -d - -# 方式 3: 强制重新构建(不使用缓存) -make up-rebuild - -# 或 -docker compose build --no-cache backend -docker compose --profile backend up -d -``` - ---- - -## 数据备份和恢复 - -```bash -# 备份数据库 -docker compose exec postgres pg_dump -U postgres ocdp > backup-$(date +%Y%m%d).sql - -# 恢复数据库 -docker compose exec -T postgres psql -U postgres ocdp < backup.sql - -# 导出为压缩文件 -docker compose exec postgres pg_dump -U postgres ocdp | gzip > backup-$(date +%Y%m%d).sql.gz - -# 从压缩文件恢复 -gunzip -c backup.sql.gz | docker compose exec -T postgres psql -U postgres ocdp -``` - ---- - -## docker-compose.yml 配置 - -```yaml -version: '3.8' - -services: - # PostgreSQL 数据库 - postgres: - image: postgres:17-alpine - container_name: ocdp-postgres - environment: - - POSTGRES_DB=${POSTGRES_DB:-ocdp} - - POSTGRES_USER=${POSTGRES_USER:-postgres} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - ports: - - "${POSTGRES_PORT:-5432}:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - - ${INIT_DB_SQL_PATH:-./scripts/init-db.sql}:/docker-entrypoint-initdb.d/01-init.sql:ro - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-ocdp}"] - interval: 10s - timeout: 5s - retries: 5 - restart: unless-stopped - networks: - - ocdp-network - - # Backend 服务(需要 --profile backend 才启动) - backend: - profiles: ["backend"] - build: - context: . - dockerfile: Dockerfile - container_name: ocdp-backend - ports: - - "${BACKEND_PORT:-8080}:8080" - environment: - - ADAPTER_MODE=${ADAPTER_MODE:-production} - - PORT=8080 - - JWT_SECRET=${JWT_SECRET} - - ENCRYPTION_KEY=${ENCRYPTION_KEY} - - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable - depends_on: - postgres: - condition: service_healthy - restart: unless-stopped - networks: - - ocdp-network - healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"] - interval: 30s - timeout: 3s - retries: 3 - start_period: 40s - -networks: - ocdp-network: - driver: bridge - -volumes: - postgres_data: - driver: local -``` - -> 💡 **提示** -> 如果与仓库根目录下的 `docker-compose.yml` 联合使用(例如通过 `docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend ...`),请确保设置环境变量 `INIT_DB_SQL_PATH=./backend/scripts/init-db.sql`(或绝对路径),以便 PostgreSQL 容器挂载到正确的初始化脚本。 - ---- - -## Dockerfile 配置 - -```dockerfile -# 多阶段构建,优化镜像大小 - -# 构建阶段 -FROM golang:1.21-alpine AS builder - -WORKDIR /app - -# 复制依赖文件 -COPY go.mod go.sum ./ -RUN go mod download - -# 复制源代码 -COPY . . - -# 编译(静态链接) -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-w -s' -o ocdp-backend cmd/api/main.go - -# 运行阶段 -FROM alpine:latest - -# 安装 CA 证书 -RUN apk --no-cache add ca-certificates tzdata wget - -# 设置时区 -ENV TZ=Asia/Shanghai - -WORKDIR /root/ - -# 复制二进制文件 -COPY --from=builder /app/ocdp-backend . - -# 复制配置文件(如果有) -COPY config/ config/ - -# 暴露端口 -EXPOSE 8080 - -# 健康检查 -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 - -# 运行 -CMD ["./ocdp-backend"] -``` - ---- - -## 环境变量 - -| 变量 | 说明 | 示例 | 必需 | -|------|------|------|------| -| `ADAPTER_MODE` | 适配器模式 | `production` | ✅ | -| `BACKEND_PORT` | Backend 端口 | `8080` | ❌ | -| `POSTGRES_PORT` | PostgreSQL 端口 | `5432` | ❌ | -| `JWT_SECRET` | JWT 密钥 | `随机字符串` | ✅ | -| `ENCRYPTION_KEY` | 加密密钥(32字节) | `随机字符串` | ✅ | -| `POSTGRES_DB` | 数据库名 | `ocdp` | ✅ | -| `POSTGRES_USER` | 数据库用户 | `postgres` | ✅ | -| `POSTGRES_PASSWORD` | 数据库密码 | `随机字符串` | ✅ | -| `DATABASE_URL` | 数据库连接 | `postgresql://...` | ✅ | - ---- - -## 故障排查 - -**问题 1: Backend 容器无法启动** - -```bash -# 查看容器日志 -docker compose logs backend - -# 查看容器状态 -docker compose ps - -# 重新构建镜像 -docker compose build --no-cache backend -docker compose --profile backend up -d -``` - -**问题 2: 数据库连接失败** - -```bash -# 检查数据库健康状态 -docker compose exec postgres pg_isready -U postgres - -# 查看数据库日志 -docker compose logs postgres - -# 检查网络连接 -docker compose exec backend ping postgres - -# 等待数据库完全启动后重启 Backend -sleep 10 -docker compose restart backend -``` - -**问题 3: 端口冲突** - -```bash -# 修改 .env 文件中的端口 -BACKEND_PORT=8081 -POSTGRES_PORT=5433 - -# 重新启动 -docker compose down -docker compose --profile backend up -d -``` - -**问题 4: 完全重置** - -```bash -# 停止所有服务 -docker compose down - -# 删除数据卷(⚠️ 会删除数据) -docker compose down -v - -# 清理 Docker 系统 -docker system prune -a --volumes - -# 重新部署 -make up -``` - ---- - -# 环境变量配置 - -## 环境变量对比 - -| 变量 | Mock | Dev | Up | 说明 | -|------|------|-----|-----|------| -| `ADAPTER_MODE` | `mock` | `production` | `production` | 适配器模式 | -| `PORT` | `8080` | `8080` | `8080` | 服务端口 | -| `JWT_SECRET` | 简单值 | 开发值 | 强随机值 | JWT 密钥 | -| `ENCRYPTION_KEY` | - | 开发值 | 强随机值 | 加密密钥 | -| `DATABASE_URL` | - | 本地 PostgreSQL | 容器内 PostgreSQL | 数据库连接 | - ---- - -## 模式对比 - -| 特性 | Mock | Dev | Up | -|------|------|-----|-----| -| **Backend 运行位置** | 本地进程 | 本地进程 | Docker 容器 | -| **依赖服务** | 无 | Docker (PostgreSQL) | Docker (PostgreSQL) | -| **热重载** | ✅ Air | ✅ Air | ❌ 需重新构建 | -| **数据库** | ❌ 内存 | ✅ PostgreSQL | ✅ PostgreSQL | -| **数据持久化** | ❌ | ✅ | ✅ | -| **启动时间** | 3 秒 | 5-8 秒 | 15-30 秒 | -| **调试能力** | ✅ IDE 断点 | ✅ IDE 断点 | ⚠️ 日志调试 | -| **适用场景** | 快速开发、API 测试 | 日常开发 ⭐ 推荐 | 集成测试、生产预演 | -| **环境隔离** | ✅ 完全隔离 | ⚠️ 部分隔离 | ✅ 完全隔离 | -| **生产一致性** | ❌ 低 | ⚠️ 中 | ✅ 高 | - ---- - -## 推荐使用场景 - -### 🚀 Mock 模式 -- ✅ 快速功能开发 -- ✅ API 接口测试 -- ✅ 前端联调 -- ✅ 单元测试 - -### ⭐ Dev 模式(推荐日常开发) -- ✅ 日常功能开发 -- ✅ 代码调试 -- ✅ 数据持久化测试 -- ✅ 快速迭代 - -### 🎯 Up 模式 -- ✅ 集成测试 -- ✅ 生产环境预演 -- ✅ 团队协作环境搭建 -- ✅ 交付演示 - ---- - -# 故障排查 - -## 通用问题 - -### 1. 端口被占用 - -```bash -# 查找占用端口的进程 -lsof -i :8080 -# 或 -netstat -tuln | grep 8080 - -# 关闭进程 -kill -9 - -# 或使用其他端口 -export PORT=8081 -``` - -### 2. Go 环境问题 - -```bash -# 检查 Go 版本 -go version # 需要 Go 1.21+ - -# 更新 Go 模块 -go mod download -go mod tidy - -# 清理 Go 缓存 -go clean -cache -modcache -``` - -### 3. Air 找不到命令 - -```bash -# 确保 GOPATH/bin 在 PATH 中 -export PATH=$PATH:$(go env GOPATH)/bin - -# 重新安装 Air -go install github.com/air-verse/air@latest - -# 验证安装 -air -v -``` - ---- - -## Mock 模式问题 - -### 1. 服务无法启动 - -```bash -# 检查环境变量 -echo $ADAPTER_MODE # 应该是 mock -echo $PORT # 应该是 8080 -echo $JWT_SECRET # 不应为空 - -# 手动运行查看详细错误 -export ADAPTER_MODE=mock -export JWT_SECRET=test-secret -go run cmd/api/main.go -``` - -### 2. Bootstrap 数据未加载 - -```bash -# 检查 bootstrap.json 文件 -cat config/bootstrap.json - -# 确保 enabled 为 true -# { -# "enabled": true, -# ... -# } -``` - ---- - -## Dev 模式问题 - -### 1. 数据库连接失败 - -```bash -# 检查 PostgreSQL 是否运行 -docker compose ps postgres - -# 检查端口 -lsof -i :5432 - -# 测试连接 -psql -h localhost -U postgres -d ocdp -c "SELECT 1;" - -# 重启数据库 -docker compose restart postgres -``` - -### 2. 数据库未初始化 - -```bash -# 重新初始化数据库 -docker compose down postgres -docker compose up -d postgres - -# 等待健康检查通过 -docker compose ps postgres -``` - -### 3. Air 热重载失败 - -```bash -# 清理临时文件 -rm -rf tmp/ - -# 重新启动 Air -air -c .air.toml - -# 检查 .air.toml 配置 -cat .air.toml -``` - ---- - -## Up 模式问题 - -### 1. Backend 容器无法启动 - -```bash -# 查看容器日志 -docker compose logs backend - -# 查看容器状态 -docker compose ps - -# 重新构建镜像 -make up-rebuild -``` - -### 2. 镜像构建失败 - -```bash -# 清理 Docker 缓存 -docker builder prune -a - -# 重新构建 -docker compose build --no-cache backend - -# 检查 Dockerfile -cat Dockerfile -``` - -### 3. 容器健康检查失败 - -```bash -# 查看容器日志 -docker compose logs backend - -# 手动执行健康检查 -docker compose exec backend wget --no-verbose --tries=1 --spider http://localhost:8080/health - -# 检查 healthcheck 配置 -docker compose config | grep -A 5 healthcheck -``` - ---- - -## 数据库问题 - -### 1. 数据库连接超时 - -```bash -# 检查数据库是否启动完成 -docker compose ps postgres - -# 查看数据库日志 -docker compose logs postgres - -# 手动测试连接 -docker compose exec postgres pg_isready -U postgres - -# 等待更长时间 -sleep 10 -docker compose restart backend -``` - -### 2. 数据丢失 - -```bash -# 检查数据卷 -docker volume ls | grep postgres - -# 备份当前数据 -docker compose exec postgres pg_dump -U postgres ocdp > backup.sql - -# 恢复数据 -docker compose exec -T postgres psql -U postgres ocdp < backup.sql -``` - -### 3. 权限问题 - -```bash -# 检查数据卷权限 -docker volume inspect ocdp_postgres_data - -# 重新创建数据卷 -docker compose down -v -docker compose up -d postgres -``` - ---- - -## 健康检查 - -```bash -# 服务健康检查 -curl http://localhost:8080/health -# 预期: {"status":"healthy"} - -# 数据库健康检查 -# Docker 方式 -docker compose exec postgres pg_isready -U postgres - -# 本地方式 -pg_isready -h localhost -p 5432 -U postgres - -# 检查所有服务状态 -docker compose ps -``` - ---- - -## 资源监控 - -```bash -# 查看容器资源使用 -docker stats - -# 查看磁盘使用 -docker system df - -# 查看日志大小 -du -sh $(docker inspect --format='{{.LogPath}}' ocdp-backend) -du -sh $(docker inspect --format='{{.LogPath}}' ocdp-postgres) - -# 清理 Docker 资源 -docker system prune -a --volumes -``` - ---- - -## 完全重置 - -### Mock 模式 - -```bash -# 清理临时文件 -rm -rf tmp/ - -# 重新启动 -make mock -``` - -### Dev 模式 - -```bash -# 停止服务 -docker compose down - -# 删除数据卷(⚠️ 数据丢失) -docker compose down -v - -# 重新启动 -make db-up -make dev -``` - -### Up 模式 - -```bash -# 停止所有服务 -docker compose down - -# 删除数据卷(⚠️ 数据丢失) -docker compose down -v - -# 清理 Docker 系统 -docker system prune -a --volumes - -# 重新部署 -make up -``` - ---- - -## 服务访问地址 - -| 服务 | Mock | Dev | Up | 说明 | -|------|------|-----|-----|------| -| **Backend API** | http://localhost:8080/api/v1 | http://localhost:8080/api/v1 | http://localhost:8080/api/v1 | REST API | -| **Health Check** | http://localhost:8080/health | http://localhost:8080/health | http://localhost:8080/health | 健康检查 | -| **Swagger UI** | http://localhost:8080/api/docs | http://localhost:8080/api/docs | http://localhost:8080/api/docs | API 文档 | -| **PostgreSQL** | - | localhost:5432 | localhost:5432 | 数据库 | - ---- - -## 相关文档 - -- [架构文档](architecture.md) - 六边形架构、技术选型 -- [API 与测试](api-and-test.md) - API 文档、测试指南 -- [主 README](../README.md) - 项目概览 - ---- - -**Last Updated**: 2025-11-09 -**Version**: v3.0.0 diff --git a/backend/docs/docs.go b/backend/docs/docs.go new file mode 100644 index 0000000..1e555a3 --- /dev/null +++ b/backend/docs/docs.go @@ -0,0 +1,3047 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": { + "name": "API Support", + "email": "support@ocdp.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/admin/users": { + "get": { + "description": "获取所有用户列表(Admin 专用),可按 workspace_id 筛选", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "列出用户", + "parameters": [ + { + "type": "string", + "description": "工作空间 ID", + "name": "workspace_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserListResponse" + } + } + } + }, + "post": { + "description": "创建新用户(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "创建用户", + "parameters": [ + { + "description": "创建用户请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserResponse" + } + } + } + } + }, + "/admin/users/{user_id}": { + "get": { + "description": "获取指定用户信息(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "获取用户", + "parameters": [ + { + "type": "string", + "description": "用户 ID", + "name": "user_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserResponse" + } + } + } + }, + "put": { + "description": "更新用户信息(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "更新用户", + "parameters": [ + { + "type": "string", + "description": "用户 ID", + "name": "user_id", + "in": "path", + "required": true + }, + { + "description": "更新用户请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserResponse" + } + } + } + }, + "delete": { + "description": "删除指定用户(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "删除用户", + "parameters": [ + { + "type": "string", + "description": "用户 ID", + "name": "user_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/admin/users/{user_id}/active": { + "put": { + "description": "设置用户是否启用(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "启用/禁用用户", + "parameters": [ + { + "type": "string", + "description": "用户 ID", + "name": "user_id", + "in": "path", + "required": true + }, + { + "description": "启用状态", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.SetUserActiveRequest" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/admin/users/{user_id}/password": { + "put": { + "description": "重置指定用户的密码(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "重置用户密码", + "parameters": [ + { + "type": "string", + "description": "用户 ID", + "name": "user_id", + "in": "path", + "required": true + }, + { + "description": "重置密码请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ResetPasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/admin/users/{user_id}/workspace": { + "put": { + "description": "将用户分配到指定工作空间(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "分配用户到工作空间", + "parameters": [ + { + "type": "string", + "description": "用户 ID", + "name": "user_id", + "in": "path", + "required": true + }, + { + "description": "工作空间分配请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ChangeUserWorkspaceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/auth/login": { + "post": { + "description": "使用用户名和密码获取访问令牌", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "用户登录", + "parameters": [ + { + "description": "登录信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.AuthResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/auth/refresh": { + "post": { + "description": "使用刷新令牌获取新的访问令牌", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "刷新访问令牌", + "parameters": [ + { + "description": "刷新令牌", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RefreshTokenRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.AuthResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/auth/register": { + "post": { + "description": "创建一个新的后台用户", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "用户注册", + "parameters": [ + { + "description": "注册信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegisterRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/clusters": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Clusters" + ], + "summary": "列出所有集群", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建一个新的 Kubernetes 集群配置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Clusters" + ], + "summary": "创建集群", + "parameters": [ + { + "description": "集群信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateClusterRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/clusters/{cluster_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Clusters" + ], + "summary": "获取集群详情", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Clusters" + ], + "summary": "更新集群", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + }, + { + "description": "更新内容", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateClusterRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Clusters" + ], + "summary": "删除集群", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/clusters/{cluster_id}/health": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Clusters" + ], + "summary": "获取集群健康状态", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterHealthResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/clusters/{cluster_id}/instances": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instances" + ], + "summary": "列出实例", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceListResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "在指定集群上部署一个 artifact", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instances" + ], + "summary": "创建实例", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + }, + { + "description": "实例配置", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateInstanceRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/clusters/{cluster_id}/instances/{instance_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instances" + ], + "summary": "获取实例详情", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "实例 ID", + "name": "instance_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instances" + ], + "summary": "更新实例", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "实例 ID", + "name": "instance_id", + "in": "path", + "required": true + }, + { + "description": "更新内容", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateInstanceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instances" + ], + "summary": "删除实例", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "实例 ID", + "name": "instance_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/clusters/{cluster_id}/instances/{instance_id}/entries": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instances" + ], + "summary": "获取实例 Service/Ingress 入口", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "实例 ID", + "name": "instance_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryResponse" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/monitoring/clusters": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Monitoring" + ], + "summary": "列出集群监控", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterMetricsResponse" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/monitoring/clusters/{cluster_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Monitoring" + ], + "summary": "获取集群监控", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterMetricsResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/monitoring/clusters/{cluster_id}/nodes": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Monitoring" + ], + "summary": "获取节点指标", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.NodeMetricsResponse" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/monitoring/summary": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Monitoring" + ], + "summary": "获取监控汇总", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.MonitoringSummaryResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/registries": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Registries" + ], + "summary": "列出所有 Registries", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "新增 OCI Registry 配置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Registries" + ], + "summary": "创建 Registry", + "parameters": [ + { + "description": "Registry 信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateRegistryRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/registries/{registry_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Registries" + ], + "summary": "获取 Registry", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Registries" + ], + "summary": "更新 Registry", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + }, + { + "description": "更新内容", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateRegistryRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Registries" + ], + "summary": "删除 Registry", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/registries/{registry_id}/health": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Registries" + ], + "summary": "检查 Registry 健康", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryHealthResponse" + } + } + } + } + }, + "/registries/{registry_id}/repositories": { + "get": { + "description": "列出指定 Registry 中的所有 Repository", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Artifacts" + ], + "summary": "列出 Registry 中的所有 Repositories", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RepositoryListResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/registries/{registry_id}/repositories/{repository_name}/artifacts": { + "get": { + "description": "列出指定 Repository 中的所有 Artifact,支持按类型过滤", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Artifacts" + ], + "summary": "列出 Repository 中的所有 Artifacts", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Repository Name (URL encoded, e.g. charts%2Fnginx)", + "name": "repository_name", + "in": "path", + "required": true + }, + { + "type": "string", + "default": "all", + "description": "过滤 Artifact 类型 (all, chart, image, other)", + "name": "media_type", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.TagResponse" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/registries/{registry_id}/repositories/{repository_name}/artifacts/{reference}": { + "get": { + "description": "获取指定 Artifact 的详细信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Artifacts" + ], + "summary": "获取 Artifact 详情", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Repository Name (URL encoded)", + "name": "repository_name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Artifact Reference (tag or digest)", + "name": "reference", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ArtifactResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/registries/{registry_id}/repositories/{repository_name}/artifacts/{reference}/values": { + "get": { + "description": "获取 Helm Chart 的 values.yaml 文件内容 (仅支持 Chart 类型)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Artifacts" + ], + "summary": "获取 Helm Chart Values", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Repository Name (URL encoded)", + "name": "repository_name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Artifact Reference (tag or digest)", + "name": "reference", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ValuesResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/registries/{registry_id}/repositories/{repository_name}/artifacts/{reference}/values-schema": { + "get": { + "description": "获取 Helm Chart 的 values.schema.json (仅支持 Chart 类型)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Artifacts" + ], + "summary": "获取 Helm Chart Values Schema", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Repository Name (URL encoded)", + "name": "repository_name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Artifact Reference (tag or digest)", + "name": "reference", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ValuesSchemaResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/users/me": { + "get": { + "description": "获取当前登录用户的基本信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "获取当前用户信息", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserResponse" + } + } + } + } + }, + "/users/me/password": { + "put": { + "description": "修改当前登录用户的密码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "修改当前用户密码", + "parameters": [ + { + "description": "修改密码请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ChangePasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/users/me/workspace": { + "get": { + "description": "获取当前用户所属工作空间的详细信息和配额", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "获取当前用户所属工作空间", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceResponse" + } + } + } + } + }, + "/workspaces": { + "get": { + "description": "获取所有工作空间列表(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "summary": "列出所有工作空间", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceListResponse" + } + } + } + }, + "post": { + "description": "创建新的工作空间(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "summary": "创建工作空间", + "parameters": [ + { + "description": "创建工作空间请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateWorkspaceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceDTO" + } + } + } + } + }, + "/workspaces/{workspace_id}": { + "get": { + "description": "获取指定工作空间的详细信息和配额", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "summary": "获取工作空间", + "parameters": [ + { + "type": "string", + "description": "工作空间 ID", + "name": "workspace_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceResponse" + } + } + } + }, + "put": { + "description": "更新工作空间信息(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "summary": "更新工作空间", + "parameters": [ + { + "type": "string", + "description": "工作空间 ID", + "name": "workspace_id", + "in": "path", + "required": true + }, + { + "description": "更新工作空间请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateWorkspaceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceDTO" + } + } + } + }, + "delete": { + "description": "删除指定工作空间(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "summary": "删除工作空间", + "parameters": [ + { + "type": "string", + "description": "工作空间 ID", + "name": "workspace_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/workspaces/{workspace_id}/quotas": { + "get": { + "description": "获取指定工作空间的资源配额", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "summary": "获取工作空间配额", + "parameters": [ + { + "type": "string", + "description": "工作空间 ID", + "name": "workspace_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaDTO" + } + } + } + } + }, + "put": { + "description": "设置指定工作空间的 CPU/GPU/GPU Memory 配额(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "summary": "设置工作空间配额", + "parameters": [ + { + "type": "string", + "description": "工作空间 ID", + "name": "workspace_id", + "in": "path", + "required": true + }, + { + "description": "配额设置请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.SetQuotasRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaDTO" + } + } + } + } + } + } + }, + "definitions": { + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ArtifactResponse": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "digest": { + "type": "string" + }, + "repositoryName": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "tag": { + "type": "string" + }, + "type": { + "description": "chart | image | other", + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.AuthResponse": { + "type": "object", + "properties": { + "accessToken": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ChangePasswordRequest": { + "type": "object", + "required": [ + "new_password", + "old_password" + ], + "properties": { + "new_password": { + "type": "string", + "minLength": 6 + }, + "old_password": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ChangeUserWorkspaceRequest": { + "type": "object", + "required": [ + "workspace_id" + ], + "properties": { + "workspace_id": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterHealthResponse": { + "type": "object", + "properties": { + "healthy": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterMetricsResponse": { + "type": "object", + "properties": { + "clusterId": { + "type": "string" + }, + "clusterName": { + "type": "string" + }, + "cpuUsage": { + "type": "number" + }, + "gpuUsage": { + "type": "number" + }, + "lastCheck": { + "type": "string" + }, + "maxNodeCpu": { + "type": "string" + }, + "maxNodeCpuUsage": { + "type": "number" + }, + "maxNodeGpu": { + "type": "integer" + }, + "maxNodeGpuUsage": { + "type": "number" + }, + "maxNodeMemUsage": { + "type": "number" + }, + "maxNodeMemory": { + "type": "string" + }, + "memoryUsage": { + "type": "number" + }, + "nodeCount": { + "type": "integer" + }, + "nodes": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.NodeMetricsResponse" + } + }, + "podCount": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "totalCpu": { + "type": "string" + }, + "totalGpu": { + "type": "integer" + }, + "totalMemory": { + "type": "string" + }, + "uptime": { + "type": "string" + }, + "usedCpu": { + "type": "string" + }, + "usedGpu": { + "type": "integer" + }, + "usedMemory": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse": { + "type": "object", + "properties": { + "caData": { + "description": "脱敏数据(仅用于前端显示,实际值为掩码)", + "type": "string" + }, + "certData": { + "description": "脱敏显示(••••••••)", + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "defaultNamespace": { + "description": "默认 namespace 前缀", + "type": "string" + }, + "description": { + "type": "string" + }, + "hasCaData": { + "description": "认证配置状态(不返回实际证书数据,仅返回是否已配置)", + "type": "boolean" + }, + "hasCertData": { + "type": "boolean" + }, + "hasKeyData": { + "type": "boolean" + }, + "hasToken": { + "type": "boolean" + }, + "host": { + "type": "string" + }, + "id": { + "type": "string" + }, + "isShared": { + "description": "是否为共享集群", + "type": "boolean" + }, + "isolationMode": { + "description": "'namespace' | 'cluster'", + "type": "string" + }, + "keyData": { + "description": "脱敏显示(••••••••)", + "type": "string" + }, + "name": { + "type": "string" + }, + "ownerId": { + "type": "string" + }, + "token": { + "description": "脱敏显示(••••••••)", + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "workspaceId": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateClusterRequest": { + "type": "object", + "required": [ + "host", + "name" + ], + "properties": { + "caData": { + "type": "string" + }, + "ca_data": { + "type": "string" + }, + "certData": { + "type": "string" + }, + "cert_data": { + "type": "string" + }, + "defaultNamespace": { + "description": "默认 namespace 前缀", + "type": "string" + }, + "description": { + "type": "string" + }, + "host": { + "type": "string" + }, + "isShared": { + "description": "是否为共享集群", + "type": "boolean" + }, + "isolationMode": { + "description": "'namespace' | 'cluster'", + "type": "string" + }, + "keyData": { + "type": "string" + }, + "key_data": { + "type": "string" + }, + "name": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateInstanceRequest": { + "type": "object", + "required": [ + "name", + "namespace", + "registryId", + "repository", + "tag" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "registryId": { + "type": "string" + }, + "registry_id": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "values": { + "type": "object", + "additionalProperties": true + }, + "valuesYaml": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateRegistryRequest": { + "type": "object", + "required": [ + "name", + "url" + ], + "properties": { + "description": { + "type": "string" + }, + "insecure": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "url": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateUserRequest": { + "type": "object", + "required": [ + "password", + "role", + "username" + ], + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string", + "minLength": 6 + }, + "role": { + "type": "string", + "enum": [ + "admin", + "user" + ] + }, + "username": { + "type": "string" + }, + "workspace_id": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateWorkspaceRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "error": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryHostResponse": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "paths": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryPathResponse" + } + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryPathResponse": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "serviceName": { + "type": "string" + }, + "servicePort": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryPortResponse": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "nodePort": { + "type": "integer" + }, + "port": { + "type": "integer" + }, + "protocol": { + "type": "string" + }, + "targetPort": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryResponse": { + "type": "object", + "properties": { + "clusterIP": { + "type": "string" + }, + "externalIPs": { + "type": "array", + "items": { + "type": "string" + } + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryHostResponse" + } + }, + "kind": { + "type": "string" + }, + "loadBalancerIngress": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "ports": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryPortResponse" + } + }, + "tls": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryTLSResponse" + } + }, + "type": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryTLSResponse": { + "type": "object", + "properties": { + "hosts": { + "type": "array", + "items": { + "type": "string" + } + }, + "secretName": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceListResponse": { + "type": "object", + "properties": { + "instances": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse" + } + }, + "total": { + "type": "integer" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse": { + "type": "object", + "properties": { + "chart": { + "type": "string" + }, + "clusterId": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "lastError": { + "type": "string" + }, + "lastOperation": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "registryId": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "statusReason": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "values": { + "type": "object", + "additionalProperties": true + }, + "version": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.LoginRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.MonitoringSummaryResponse": { + "type": "object", + "properties": { + "errorClusters": { + "type": "integer" + }, + "healthyClusters": { + "type": "integer" + }, + "lastUpdate": { + "type": "string" + }, + "totalClusters": { + "type": "integer" + }, + "totalNodes": { + "type": "integer" + }, + "totalPods": { + "type": "integer" + }, + "warningClusters": { + "type": "integer" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.NodeMetricsResponse": { + "type": "object", + "properties": { + "age": { + "type": "string" + }, + "containerRuntime": { + "type": "string" + }, + "cpuAllocatable": { + "type": "string" + }, + "cpuCapacity": { + "type": "string" + }, + "cpuPercent": { + "type": "number" + }, + "cpuUsage": { + "type": "string" + }, + "gpuCapacity": { + "type": "integer" + }, + "gpuPercent": { + "type": "number" + }, + "gpuType": { + "type": "string" + }, + "gpuUsage": { + "type": "integer" + }, + "kernelVersion": { + "type": "string" + }, + "kubeletVersion": { + "type": "string" + }, + "memoryAllocatable": { + "type": "string" + }, + "memoryCapacity": { + "type": "string" + }, + "memoryPercent": { + "type": "number" + }, + "memoryUsage": { + "type": "string" + }, + "nodeName": { + "type": "string" + }, + "osImage": { + "type": "string" + }, + "podCount": { + "type": "integer" + }, + "role": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaDTO": { + "type": "object", + "properties": { + "hard_limit": { + "type": "number" + }, + "id": { + "type": "string" + }, + "resource_type": { + "type": "string" + }, + "soft_limit": { + "type": "number" + }, + "used": { + "type": "number" + }, + "workspace_id": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaValue": { + "type": "object", + "properties": { + "hard_limit": { + "type": "number" + }, + "soft_limit": { + "type": "number" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RefreshTokenRequest": { + "type": "object", + "required": [ + "refreshToken" + ], + "properties": { + "refreshToken": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegisterRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string", + "minLength": 6 + }, + "username": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryHealthResponse": { + "type": "object", + "properties": { + "healthy": { + "type": "boolean" + }, + "message": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hasPassword": { + "description": "是否已设置密码", + "type": "boolean" + }, + "id": { + "type": "string" + }, + "insecure": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "password": { + "description": "脱敏显示(••••••••)", + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "url": { + "type": "string" + }, + "username": { + "description": "明文返回用户名(不敏感)", + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RepositoryListResponse": { + "type": "object", + "properties": { + "catalogSupported": { + "description": "Whether _catalog API is supported", + "type": "boolean" + }, + "message": { + "description": "User-friendly message", + "type": "string" + }, + "registryId": { + "type": "string" + }, + "registryUrl": { + "type": "string" + }, + "repositories": { + "type": "array", + "items": { + "type": "string" + } + }, + "source": { + "description": "Data source: \"catalog\" | \"preconfigured\" | \"unavailable\"", + "type": "string" + }, + "total": { + "type": "integer" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ResetPasswordRequest": { + "type": "object", + "required": [ + "new_password" + ], + "properties": { + "new_password": { + "type": "string", + "minLength": 6 + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.SetQuotasRequest": { + "type": "object", + "properties": { + "cpu": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaValue" + }, + "gpu": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaValue" + }, + "gpu_memory": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaValue" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.SetUserActiveRequest": { + "type": "object", + "properties": { + "is_active": { + "type": "boolean" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.TagResponse": { + "type": "object", + "properties": { + "mediaType": { + "type": "string" + }, + "repositoryName": { + "description": "Repository name", + "type": "string" + }, + "size": { + "description": "Artifact size (bytes)", + "type": "integer" + }, + "tag": { + "description": "Tag name (e.g. \"1.0.0\", \"latest\")", + "type": "string" + }, + "type": { + "description": "Artifact type: chart, image, other", + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateClusterRequest": { + "type": "object", + "properties": { + "caData": { + "type": "string" + }, + "ca_data": { + "type": "string" + }, + "certData": { + "type": "string" + }, + "cert_data": { + "type": "string" + }, + "defaultNamespace": { + "type": "string" + }, + "description": { + "type": "string" + }, + "host": { + "type": "string" + }, + "isShared": { + "type": "boolean" + }, + "isolationMode": { + "type": "string" + }, + "keyData": { + "type": "string" + }, + "key_data": { + "type": "string" + }, + "name": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateInstanceRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "values": { + "type": "object", + "additionalProperties": true + }, + "valuesYaml": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateRegistryRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "insecure": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "url": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateUserRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "is_active": { + "type": "boolean" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateWorkspaceRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserDTO": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "must_change_password": { + "type": "boolean" + }, + "role": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "username": { + "type": "string" + }, + "workspace_id": { + "type": "string" + }, + "workspace_name": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserListResponse": { + "type": "object", + "properties": { + "total": { + "type": "integer" + }, + "users": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserDTO" + } + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserResponse": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ValuesResponse": { + "type": "object", + "properties": { + "values": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ValuesSchemaResponse": { + "type": "object", + "properties": { + "schema": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceDTO": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "created_by": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceListResponse": { + "type": "object", + "properties": { + "total": { + "type": "integer" + }, + "workspaces": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceDTO" + } + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceResponse": { + "type": "object", + "properties": { + "quotas": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaDTO" + } + }, + "workspace": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceDTO" + } + } + } + }, + "securityDefinitions": { + "BearerAuth": { + "description": "Type \"Bearer\" followed by a space and JWT token.", + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "localhost:8080", + BasePath: "/api/v1", + Schemes: []string{"http", "https"}, + Title: "OCDP Backend API", + Description: "OCDP (Open Cloud Development Platform) Backend API\n\nRESTful API for managing Kubernetes clusters, OCI registries, and Helm deployments.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json new file mode 100644 index 0000000..9853c4e --- /dev/null +++ b/backend/docs/swagger.json @@ -0,0 +1,3027 @@ +{ + "schemes": [ + "http", + "https" + ], + "swagger": "2.0", + "info": { + "description": "OCDP (Open Cloud Development Platform) Backend API\n\nRESTful API for managing Kubernetes clusters, OCI registries, and Helm deployments.", + "title": "OCDP Backend API", + "contact": { + "name": "API Support", + "email": "support@ocdp.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0" + }, + "host": "localhost:8080", + "basePath": "/api/v1", + "paths": { + "/admin/users": { + "get": { + "description": "获取所有用户列表(Admin 专用),可按 workspace_id 筛选", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "列出用户", + "parameters": [ + { + "type": "string", + "description": "工作空间 ID", + "name": "workspace_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserListResponse" + } + } + } + }, + "post": { + "description": "创建新用户(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "创建用户", + "parameters": [ + { + "description": "创建用户请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserResponse" + } + } + } + } + }, + "/admin/users/{user_id}": { + "get": { + "description": "获取指定用户信息(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "获取用户", + "parameters": [ + { + "type": "string", + "description": "用户 ID", + "name": "user_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserResponse" + } + } + } + }, + "put": { + "description": "更新用户信息(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "更新用户", + "parameters": [ + { + "type": "string", + "description": "用户 ID", + "name": "user_id", + "in": "path", + "required": true + }, + { + "description": "更新用户请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserResponse" + } + } + } + }, + "delete": { + "description": "删除指定用户(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "删除用户", + "parameters": [ + { + "type": "string", + "description": "用户 ID", + "name": "user_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/admin/users/{user_id}/active": { + "put": { + "description": "设置用户是否启用(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "启用/禁用用户", + "parameters": [ + { + "type": "string", + "description": "用户 ID", + "name": "user_id", + "in": "path", + "required": true + }, + { + "description": "启用状态", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.SetUserActiveRequest" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/admin/users/{user_id}/password": { + "put": { + "description": "重置指定用户的密码(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "重置用户密码", + "parameters": [ + { + "type": "string", + "description": "用户 ID", + "name": "user_id", + "in": "path", + "required": true + }, + { + "description": "重置密码请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ResetPasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/admin/users/{user_id}/workspace": { + "put": { + "description": "将用户分配到指定工作空间(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "分配用户到工作空间", + "parameters": [ + { + "type": "string", + "description": "用户 ID", + "name": "user_id", + "in": "path", + "required": true + }, + { + "description": "工作空间分配请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ChangeUserWorkspaceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/auth/login": { + "post": { + "description": "使用用户名和密码获取访问令牌", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "用户登录", + "parameters": [ + { + "description": "登录信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.AuthResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/auth/refresh": { + "post": { + "description": "使用刷新令牌获取新的访问令牌", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "刷新访问令牌", + "parameters": [ + { + "description": "刷新令牌", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RefreshTokenRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.AuthResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/auth/register": { + "post": { + "description": "创建一个新的后台用户", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "用户注册", + "parameters": [ + { + "description": "注册信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegisterRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/clusters": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Clusters" + ], + "summary": "列出所有集群", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建一个新的 Kubernetes 集群配置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Clusters" + ], + "summary": "创建集群", + "parameters": [ + { + "description": "集群信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateClusterRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/clusters/{cluster_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Clusters" + ], + "summary": "获取集群详情", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Clusters" + ], + "summary": "更新集群", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + }, + { + "description": "更新内容", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateClusterRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Clusters" + ], + "summary": "删除集群", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/clusters/{cluster_id}/health": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Clusters" + ], + "summary": "获取集群健康状态", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterHealthResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/clusters/{cluster_id}/instances": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instances" + ], + "summary": "列出实例", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceListResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "在指定集群上部署一个 artifact", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instances" + ], + "summary": "创建实例", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + }, + { + "description": "实例配置", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateInstanceRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/clusters/{cluster_id}/instances/{instance_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instances" + ], + "summary": "获取实例详情", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "实例 ID", + "name": "instance_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instances" + ], + "summary": "更新实例", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "实例 ID", + "name": "instance_id", + "in": "path", + "required": true + }, + { + "description": "更新内容", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateInstanceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instances" + ], + "summary": "删除实例", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "实例 ID", + "name": "instance_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/clusters/{cluster_id}/instances/{instance_id}/entries": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instances" + ], + "summary": "获取实例 Service/Ingress 入口", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "实例 ID", + "name": "instance_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryResponse" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/monitoring/clusters": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Monitoring" + ], + "summary": "列出集群监控", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterMetricsResponse" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/monitoring/clusters/{cluster_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Monitoring" + ], + "summary": "获取集群监控", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterMetricsResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/monitoring/clusters/{cluster_id}/nodes": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Monitoring" + ], + "summary": "获取节点指标", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.NodeMetricsResponse" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/monitoring/summary": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Monitoring" + ], + "summary": "获取监控汇总", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.MonitoringSummaryResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/registries": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Registries" + ], + "summary": "列出所有 Registries", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "新增 OCI Registry 配置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Registries" + ], + "summary": "创建 Registry", + "parameters": [ + { + "description": "Registry 信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateRegistryRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/registries/{registry_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Registries" + ], + "summary": "获取 Registry", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Registries" + ], + "summary": "更新 Registry", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + }, + { + "description": "更新内容", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateRegistryRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Registries" + ], + "summary": "删除 Registry", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/registries/{registry_id}/health": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Registries" + ], + "summary": "检查 Registry 健康", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryHealthResponse" + } + } + } + } + }, + "/registries/{registry_id}/repositories": { + "get": { + "description": "列出指定 Registry 中的所有 Repository", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Artifacts" + ], + "summary": "列出 Registry 中的所有 Repositories", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RepositoryListResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/registries/{registry_id}/repositories/{repository_name}/artifacts": { + "get": { + "description": "列出指定 Repository 中的所有 Artifact,支持按类型过滤", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Artifacts" + ], + "summary": "列出 Repository 中的所有 Artifacts", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Repository Name (URL encoded, e.g. charts%2Fnginx)", + "name": "repository_name", + "in": "path", + "required": true + }, + { + "type": "string", + "default": "all", + "description": "过滤 Artifact 类型 (all, chart, image, other)", + "name": "media_type", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.TagResponse" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/registries/{registry_id}/repositories/{repository_name}/artifacts/{reference}": { + "get": { + "description": "获取指定 Artifact 的详细信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Artifacts" + ], + "summary": "获取 Artifact 详情", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Repository Name (URL encoded)", + "name": "repository_name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Artifact Reference (tag or digest)", + "name": "reference", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ArtifactResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/registries/{registry_id}/repositories/{repository_name}/artifacts/{reference}/values": { + "get": { + "description": "获取 Helm Chart 的 values.yaml 文件内容 (仅支持 Chart 类型)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Artifacts" + ], + "summary": "获取 Helm Chart Values", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Repository Name (URL encoded)", + "name": "repository_name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Artifact Reference (tag or digest)", + "name": "reference", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ValuesResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/registries/{registry_id}/repositories/{repository_name}/artifacts/{reference}/values-schema": { + "get": { + "description": "获取 Helm Chart 的 values.schema.json (仅支持 Chart 类型)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Artifacts" + ], + "summary": "获取 Helm Chart Values Schema", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Repository Name (URL encoded)", + "name": "repository_name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Artifact Reference (tag or digest)", + "name": "reference", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ValuesSchemaResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/users/me": { + "get": { + "description": "获取当前登录用户的基本信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "获取当前用户信息", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserResponse" + } + } + } + } + }, + "/users/me/password": { + "put": { + "description": "修改当前登录用户的密码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "修改当前用户密码", + "parameters": [ + { + "description": "修改密码请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ChangePasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/users/me/workspace": { + "get": { + "description": "获取当前用户所属工作空间的详细信息和配额", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "获取当前用户所属工作空间", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceResponse" + } + } + } + } + }, + "/workspaces": { + "get": { + "description": "获取所有工作空间列表(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "summary": "列出所有工作空间", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceListResponse" + } + } + } + }, + "post": { + "description": "创建新的工作空间(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "summary": "创建工作空间", + "parameters": [ + { + "description": "创建工作空间请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateWorkspaceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceDTO" + } + } + } + } + }, + "/workspaces/{workspace_id}": { + "get": { + "description": "获取指定工作空间的详细信息和配额", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "summary": "获取工作空间", + "parameters": [ + { + "type": "string", + "description": "工作空间 ID", + "name": "workspace_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceResponse" + } + } + } + }, + "put": { + "description": "更新工作空间信息(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "summary": "更新工作空间", + "parameters": [ + { + "type": "string", + "description": "工作空间 ID", + "name": "workspace_id", + "in": "path", + "required": true + }, + { + "description": "更新工作空间请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateWorkspaceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceDTO" + } + } + } + }, + "delete": { + "description": "删除指定工作空间(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "summary": "删除工作空间", + "parameters": [ + { + "type": "string", + "description": "工作空间 ID", + "name": "workspace_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/workspaces/{workspace_id}/quotas": { + "get": { + "description": "获取指定工作空间的资源配额", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "summary": "获取工作空间配额", + "parameters": [ + { + "type": "string", + "description": "工作空间 ID", + "name": "workspace_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaDTO" + } + } + } + } + }, + "put": { + "description": "设置指定工作空间的 CPU/GPU/GPU Memory 配额(Admin 专用)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "summary": "设置工作空间配额", + "parameters": [ + { + "type": "string", + "description": "工作空间 ID", + "name": "workspace_id", + "in": "path", + "required": true + }, + { + "description": "配额设置请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.SetQuotasRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaDTO" + } + } + } + } + } + } + }, + "definitions": { + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ArtifactResponse": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "digest": { + "type": "string" + }, + "repositoryName": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "tag": { + "type": "string" + }, + "type": { + "description": "chart | image | other", + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.AuthResponse": { + "type": "object", + "properties": { + "accessToken": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ChangePasswordRequest": { + "type": "object", + "required": [ + "new_password", + "old_password" + ], + "properties": { + "new_password": { + "type": "string", + "minLength": 6 + }, + "old_password": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ChangeUserWorkspaceRequest": { + "type": "object", + "required": [ + "workspace_id" + ], + "properties": { + "workspace_id": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterHealthResponse": { + "type": "object", + "properties": { + "healthy": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterMetricsResponse": { + "type": "object", + "properties": { + "clusterId": { + "type": "string" + }, + "clusterName": { + "type": "string" + }, + "cpuUsage": { + "type": "number" + }, + "gpuUsage": { + "type": "number" + }, + "lastCheck": { + "type": "string" + }, + "maxNodeCpu": { + "type": "string" + }, + "maxNodeCpuUsage": { + "type": "number" + }, + "maxNodeGpu": { + "type": "integer" + }, + "maxNodeGpuUsage": { + "type": "number" + }, + "maxNodeMemUsage": { + "type": "number" + }, + "maxNodeMemory": { + "type": "string" + }, + "memoryUsage": { + "type": "number" + }, + "nodeCount": { + "type": "integer" + }, + "nodes": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.NodeMetricsResponse" + } + }, + "podCount": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "totalCpu": { + "type": "string" + }, + "totalGpu": { + "type": "integer" + }, + "totalMemory": { + "type": "string" + }, + "uptime": { + "type": "string" + }, + "usedCpu": { + "type": "string" + }, + "usedGpu": { + "type": "integer" + }, + "usedMemory": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse": { + "type": "object", + "properties": { + "caData": { + "description": "脱敏数据(仅用于前端显示,实际值为掩码)", + "type": "string" + }, + "certData": { + "description": "脱敏显示(••••••••)", + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "defaultNamespace": { + "description": "默认 namespace 前缀", + "type": "string" + }, + "description": { + "type": "string" + }, + "hasCaData": { + "description": "认证配置状态(不返回实际证书数据,仅返回是否已配置)", + "type": "boolean" + }, + "hasCertData": { + "type": "boolean" + }, + "hasKeyData": { + "type": "boolean" + }, + "hasToken": { + "type": "boolean" + }, + "host": { + "type": "string" + }, + "id": { + "type": "string" + }, + "isShared": { + "description": "是否为共享集群", + "type": "boolean" + }, + "isolationMode": { + "description": "'namespace' | 'cluster'", + "type": "string" + }, + "keyData": { + "description": "脱敏显示(••••••••)", + "type": "string" + }, + "name": { + "type": "string" + }, + "ownerId": { + "type": "string" + }, + "token": { + "description": "脱敏显示(••••••••)", + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "workspaceId": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateClusterRequest": { + "type": "object", + "required": [ + "host", + "name" + ], + "properties": { + "caData": { + "type": "string" + }, + "ca_data": { + "type": "string" + }, + "certData": { + "type": "string" + }, + "cert_data": { + "type": "string" + }, + "defaultNamespace": { + "description": "默认 namespace 前缀", + "type": "string" + }, + "description": { + "type": "string" + }, + "host": { + "type": "string" + }, + "isShared": { + "description": "是否为共享集群", + "type": "boolean" + }, + "isolationMode": { + "description": "'namespace' | 'cluster'", + "type": "string" + }, + "keyData": { + "type": "string" + }, + "key_data": { + "type": "string" + }, + "name": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateInstanceRequest": { + "type": "object", + "required": [ + "name", + "namespace", + "registryId", + "repository", + "tag" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "registryId": { + "type": "string" + }, + "registry_id": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "values": { + "type": "object", + "additionalProperties": true + }, + "valuesYaml": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateRegistryRequest": { + "type": "object", + "required": [ + "name", + "url" + ], + "properties": { + "description": { + "type": "string" + }, + "insecure": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "url": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateUserRequest": { + "type": "object", + "required": [ + "password", + "role", + "username" + ], + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string", + "minLength": 6 + }, + "role": { + "type": "string", + "enum": [ + "admin", + "user" + ] + }, + "username": { + "type": "string" + }, + "workspace_id": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateWorkspaceRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "error": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryHostResponse": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "paths": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryPathResponse" + } + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryPathResponse": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "serviceName": { + "type": "string" + }, + "servicePort": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryPortResponse": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "nodePort": { + "type": "integer" + }, + "port": { + "type": "integer" + }, + "protocol": { + "type": "string" + }, + "targetPort": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryResponse": { + "type": "object", + "properties": { + "clusterIP": { + "type": "string" + }, + "externalIPs": { + "type": "array", + "items": { + "type": "string" + } + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryHostResponse" + } + }, + "kind": { + "type": "string" + }, + "loadBalancerIngress": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "ports": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryPortResponse" + } + }, + "tls": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryTLSResponse" + } + }, + "type": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryTLSResponse": { + "type": "object", + "properties": { + "hosts": { + "type": "array", + "items": { + "type": "string" + } + }, + "secretName": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceListResponse": { + "type": "object", + "properties": { + "instances": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse" + } + }, + "total": { + "type": "integer" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse": { + "type": "object", + "properties": { + "chart": { + "type": "string" + }, + "clusterId": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "lastError": { + "type": "string" + }, + "lastOperation": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "registryId": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "statusReason": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "values": { + "type": "object", + "additionalProperties": true + }, + "version": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.LoginRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.MonitoringSummaryResponse": { + "type": "object", + "properties": { + "errorClusters": { + "type": "integer" + }, + "healthyClusters": { + "type": "integer" + }, + "lastUpdate": { + "type": "string" + }, + "totalClusters": { + "type": "integer" + }, + "totalNodes": { + "type": "integer" + }, + "totalPods": { + "type": "integer" + }, + "warningClusters": { + "type": "integer" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.NodeMetricsResponse": { + "type": "object", + "properties": { + "age": { + "type": "string" + }, + "containerRuntime": { + "type": "string" + }, + "cpuAllocatable": { + "type": "string" + }, + "cpuCapacity": { + "type": "string" + }, + "cpuPercent": { + "type": "number" + }, + "cpuUsage": { + "type": "string" + }, + "gpuCapacity": { + "type": "integer" + }, + "gpuPercent": { + "type": "number" + }, + "gpuType": { + "type": "string" + }, + "gpuUsage": { + "type": "integer" + }, + "kernelVersion": { + "type": "string" + }, + "kubeletVersion": { + "type": "string" + }, + "memoryAllocatable": { + "type": "string" + }, + "memoryCapacity": { + "type": "string" + }, + "memoryPercent": { + "type": "number" + }, + "memoryUsage": { + "type": "string" + }, + "nodeName": { + "type": "string" + }, + "osImage": { + "type": "string" + }, + "podCount": { + "type": "integer" + }, + "role": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaDTO": { + "type": "object", + "properties": { + "hard_limit": { + "type": "number" + }, + "id": { + "type": "string" + }, + "resource_type": { + "type": "string" + }, + "soft_limit": { + "type": "number" + }, + "used": { + "type": "number" + }, + "workspace_id": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaValue": { + "type": "object", + "properties": { + "hard_limit": { + "type": "number" + }, + "soft_limit": { + "type": "number" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RefreshTokenRequest": { + "type": "object", + "required": [ + "refreshToken" + ], + "properties": { + "refreshToken": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegisterRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string", + "minLength": 6 + }, + "username": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryHealthResponse": { + "type": "object", + "properties": { + "healthy": { + "type": "boolean" + }, + "message": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hasPassword": { + "description": "是否已设置密码", + "type": "boolean" + }, + "id": { + "type": "string" + }, + "insecure": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "password": { + "description": "脱敏显示(••••••••)", + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "url": { + "type": "string" + }, + "username": { + "description": "明文返回用户名(不敏感)", + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RepositoryListResponse": { + "type": "object", + "properties": { + "catalogSupported": { + "description": "Whether _catalog API is supported", + "type": "boolean" + }, + "message": { + "description": "User-friendly message", + "type": "string" + }, + "registryId": { + "type": "string" + }, + "registryUrl": { + "type": "string" + }, + "repositories": { + "type": "array", + "items": { + "type": "string" + } + }, + "source": { + "description": "Data source: \"catalog\" | \"preconfigured\" | \"unavailable\"", + "type": "string" + }, + "total": { + "type": "integer" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ResetPasswordRequest": { + "type": "object", + "required": [ + "new_password" + ], + "properties": { + "new_password": { + "type": "string", + "minLength": 6 + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.SetQuotasRequest": { + "type": "object", + "properties": { + "cpu": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaValue" + }, + "gpu": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaValue" + }, + "gpu_memory": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaValue" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.SetUserActiveRequest": { + "type": "object", + "properties": { + "is_active": { + "type": "boolean" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.TagResponse": { + "type": "object", + "properties": { + "mediaType": { + "type": "string" + }, + "repositoryName": { + "description": "Repository name", + "type": "string" + }, + "size": { + "description": "Artifact size (bytes)", + "type": "integer" + }, + "tag": { + "description": "Tag name (e.g. \"1.0.0\", \"latest\")", + "type": "string" + }, + "type": { + "description": "Artifact type: chart, image, other", + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateClusterRequest": { + "type": "object", + "properties": { + "caData": { + "type": "string" + }, + "ca_data": { + "type": "string" + }, + "certData": { + "type": "string" + }, + "cert_data": { + "type": "string" + }, + "defaultNamespace": { + "type": "string" + }, + "description": { + "type": "string" + }, + "host": { + "type": "string" + }, + "isShared": { + "type": "boolean" + }, + "isolationMode": { + "type": "string" + }, + "keyData": { + "type": "string" + }, + "key_data": { + "type": "string" + }, + "name": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateInstanceRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "values": { + "type": "object", + "additionalProperties": true + }, + "valuesYaml": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateRegistryRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "insecure": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "url": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateUserRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "is_active": { + "type": "boolean" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateWorkspaceRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserDTO": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "must_change_password": { + "type": "boolean" + }, + "role": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "username": { + "type": "string" + }, + "workspace_id": { + "type": "string" + }, + "workspace_name": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserListResponse": { + "type": "object", + "properties": { + "total": { + "type": "integer" + }, + "users": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserDTO" + } + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserResponse": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ValuesResponse": { + "type": "object", + "properties": { + "values": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ValuesSchemaResponse": { + "type": "object", + "properties": { + "schema": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceDTO": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "created_by": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceListResponse": { + "type": "object", + "properties": { + "total": { + "type": "integer" + }, + "workspaces": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceDTO" + } + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceResponse": { + "type": "object", + "properties": { + "quotas": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaDTO" + } + }, + "workspace": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceDTO" + } + } + } + }, + "securityDefinitions": { + "BearerAuth": { + "description": "Type \"Bearer\" followed by a space and JWT token.", + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml new file mode 100644 index 0000000..c4ec43c --- /dev/null +++ b/backend/docs/swagger.yaml @@ -0,0 +1,1975 @@ +basePath: /api/v1 +definitions: + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ArtifactResponse: + properties: + createdAt: + type: string + digest: + type: string + repositoryName: + type: string + size: + type: integer + tag: + type: string + type: + description: chart | image | other + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.AuthResponse: + properties: + accessToken: + type: string + refreshToken: + type: string + userId: + type: string + username: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ChangePasswordRequest: + properties: + new_password: + minLength: 6 + type: string + old_password: + type: string + required: + - new_password + - old_password + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ChangeUserWorkspaceRequest: + properties: + workspace_id: + type: string + required: + - workspace_id + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterHealthResponse: + properties: + healthy: + type: boolean + message: + type: string + version: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterMetricsResponse: + properties: + clusterId: + type: string + clusterName: + type: string + cpuUsage: + type: number + gpuUsage: + type: number + lastCheck: + type: string + maxNodeCpu: + type: string + maxNodeCpuUsage: + type: number + maxNodeGpu: + type: integer + maxNodeGpuUsage: + type: number + maxNodeMemUsage: + type: number + maxNodeMemory: + type: string + memoryUsage: + type: number + nodeCount: + type: integer + nodes: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.NodeMetricsResponse' + type: array + podCount: + type: integer + status: + type: string + totalCpu: + type: string + totalGpu: + type: integer + totalMemory: + type: string + uptime: + type: string + usedCpu: + type: string + usedGpu: + type: integer + usedMemory: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse: + properties: + caData: + description: 脱敏数据(仅用于前端显示,实际值为掩码) + type: string + certData: + description: 脱敏显示(••••••••) + type: string + createdAt: + type: string + defaultNamespace: + description: 默认 namespace 前缀 + type: string + description: + type: string + hasCaData: + description: 认证配置状态(不返回实际证书数据,仅返回是否已配置) + type: boolean + hasCertData: + type: boolean + hasKeyData: + type: boolean + hasToken: + type: boolean + host: + type: string + id: + type: string + isShared: + description: 是否为共享集群 + type: boolean + isolationMode: + description: '''namespace'' | ''cluster''' + type: string + keyData: + description: 脱敏显示(••••••••) + type: string + name: + type: string + ownerId: + type: string + token: + description: 脱敏显示(••••••••) + type: string + updatedAt: + type: string + workspaceId: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateClusterRequest: + properties: + ca_data: + type: string + caData: + type: string + cert_data: + type: string + certData: + type: string + defaultNamespace: + description: 默认 namespace 前缀 + type: string + description: + type: string + host: + type: string + isShared: + description: 是否为共享集群 + type: boolean + isolationMode: + description: '''namespace'' | ''cluster''' + type: string + key_data: + type: string + keyData: + type: string + name: + type: string + token: + type: string + required: + - host + - name + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateInstanceRequest: + properties: + description: + type: string + name: + type: string + namespace: + type: string + registry_id: + type: string + registryId: + type: string + repository: + type: string + tag: + type: string + values: + additionalProperties: true + type: object + valuesYaml: + type: string + required: + - name + - namespace + - registryId + - repository + - tag + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateRegistryRequest: + properties: + description: + type: string + insecure: + type: boolean + name: + type: string + password: + type: string + url: + type: string + username: + type: string + required: + - name + - url + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateUserRequest: + properties: + email: + type: string + password: + minLength: 6 + type: string + role: + enum: + - admin + - user + type: string + username: + type: string + workspace_id: + type: string + required: + - password + - role + - username + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateWorkspaceRequest: + properties: + description: + type: string + name: + type: string + required: + - name + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse: + properties: + code: + type: integer + error: + type: string + message: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryHostResponse: + properties: + host: + type: string + paths: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryPathResponse' + type: array + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryPathResponse: + properties: + path: + type: string + serviceName: + type: string + servicePort: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryPortResponse: + properties: + name: + type: string + nodePort: + type: integer + port: + type: integer + protocol: + type: string + targetPort: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryResponse: + properties: + clusterIP: + type: string + externalIPs: + items: + type: string + type: array + hosts: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryHostResponse' + type: array + kind: + type: string + loadBalancerIngress: + items: + type: string + type: array + name: + type: string + namespace: + type: string + ports: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryPortResponse' + type: array + tls: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryTLSResponse' + type: array + type: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryTLSResponse: + properties: + hosts: + items: + type: string + type: array + secretName: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceListResponse: + properties: + instances: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse' + type: array + total: + type: integer + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse: + properties: + chart: + type: string + clusterId: + type: string + createdAt: + type: string + description: + type: string + id: + type: string + lastError: + type: string + lastOperation: + type: string + name: + type: string + namespace: + type: string + registryId: + type: string + repository: + type: string + revision: + type: integer + status: + type: string + statusReason: + type: string + updatedAt: + type: string + values: + additionalProperties: true + type: object + version: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.LoginRequest: + properties: + password: + type: string + username: + type: string + required: + - password + - username + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.MonitoringSummaryResponse: + properties: + errorClusters: + type: integer + healthyClusters: + type: integer + lastUpdate: + type: string + totalClusters: + type: integer + totalNodes: + type: integer + totalPods: + type: integer + warningClusters: + type: integer + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.NodeMetricsResponse: + properties: + age: + type: string + containerRuntime: + type: string + cpuAllocatable: + type: string + cpuCapacity: + type: string + cpuPercent: + type: number + cpuUsage: + type: string + gpuCapacity: + type: integer + gpuPercent: + type: number + gpuType: + type: string + gpuUsage: + type: integer + kernelVersion: + type: string + kubeletVersion: + type: string + memoryAllocatable: + type: string + memoryCapacity: + type: string + memoryPercent: + type: number + memoryUsage: + type: string + nodeName: + type: string + osImage: + type: string + podCount: + type: integer + role: + type: string + status: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaDTO: + properties: + hard_limit: + type: number + id: + type: string + resource_type: + type: string + soft_limit: + type: number + used: + type: number + workspace_id: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaValue: + properties: + hard_limit: + type: number + soft_limit: + type: number + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RefreshTokenRequest: + properties: + refreshToken: + type: string + required: + - refreshToken + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegisterRequest: + properties: + password: + minLength: 6 + type: string + username: + type: string + required: + - password + - username + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryHealthResponse: + properties: + healthy: + type: boolean + message: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse: + properties: + createdAt: + type: string + description: + type: string + hasPassword: + description: 是否已设置密码 + type: boolean + id: + type: string + insecure: + type: boolean + name: + type: string + password: + description: 脱敏显示(••••••••) + type: string + updatedAt: + type: string + url: + type: string + username: + description: 明文返回用户名(不敏感) + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RepositoryListResponse: + properties: + catalogSupported: + description: Whether _catalog API is supported + type: boolean + message: + description: User-friendly message + type: string + registryId: + type: string + registryUrl: + type: string + repositories: + items: + type: string + type: array + source: + description: 'Data source: "catalog" | "preconfigured" | "unavailable"' + type: string + total: + type: integer + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ResetPasswordRequest: + properties: + new_password: + minLength: 6 + type: string + required: + - new_password + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.SetQuotasRequest: + properties: + cpu: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaValue' + gpu: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaValue' + gpu_memory: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaValue' + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.SetUserActiveRequest: + properties: + is_active: + type: boolean + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.TagResponse: + properties: + mediaType: + type: string + repositoryName: + description: Repository name + type: string + size: + description: Artifact size (bytes) + type: integer + tag: + description: Tag name (e.g. "1.0.0", "latest") + type: string + type: + description: 'Artifact type: chart, image, other' + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateClusterRequest: + properties: + ca_data: + type: string + caData: + type: string + cert_data: + type: string + certData: + type: string + defaultNamespace: + type: string + description: + type: string + host: + type: string + isShared: + type: boolean + isolationMode: + type: string + key_data: + type: string + keyData: + type: string + name: + type: string + token: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateInstanceRequest: + properties: + description: + type: string + values: + additionalProperties: true + type: object + valuesYaml: + type: string + version: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateRegistryRequest: + properties: + description: + type: string + insecure: + type: boolean + name: + type: string + password: + type: string + url: + type: string + username: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateUserRequest: + properties: + email: + type: string + is_active: + type: boolean + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateWorkspaceRequest: + properties: + description: + type: string + name: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserDTO: + properties: + created_at: + type: string + email: + type: string + id: + type: string + is_active: + type: boolean + must_change_password: + type: boolean + role: + type: string + updated_at: + type: string + username: + type: string + workspace_id: + type: string + workspace_name: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserListResponse: + properties: + total: + type: integer + users: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserDTO' + type: array + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserResponse: + properties: + createdAt: + type: string + email: + type: string + id: + type: string + updatedAt: + type: string + username: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ValuesResponse: + properties: + values: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ValuesSchemaResponse: + properties: + schema: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceDTO: + properties: + created_at: + type: string + created_by: + type: string + description: + type: string + id: + type: string + name: + type: string + updated_at: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceListResponse: + properties: + total: + type: integer + workspaces: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceDTO' + type: array + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceResponse: + properties: + quotas: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaDTO' + type: array + workspace: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceDTO' + type: object +host: localhost:8080 +info: + contact: + email: support@ocdp.io + name: API Support + description: |- + OCDP (Open Cloud Development Platform) Backend API + + RESTful API for managing Kubernetes clusters, OCI registries, and Helm deployments. + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + title: OCDP Backend API + version: "1.0" +paths: + /admin/users: + get: + consumes: + - application/json + description: 获取所有用户列表(Admin 专用),可按 workspace_id 筛选 + parameters: + - description: 工作空间 ID + in: query + name: workspace_id + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserListResponse' + summary: 列出用户 + tags: + - admin + post: + consumes: + - application/json + description: 创建新用户(Admin 专用) + parameters: + - description: 创建用户请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateUserRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserResponse' + summary: 创建用户 + tags: + - admin + /admin/users/{user_id}: + delete: + consumes: + - application/json + description: 删除指定用户(Admin 专用) + parameters: + - description: 用户 ID + in: path + name: user_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + summary: 删除用户 + tags: + - admin + get: + consumes: + - application/json + description: 获取指定用户信息(Admin 专用) + parameters: + - description: 用户 ID + in: path + name: user_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserResponse' + summary: 获取用户 + tags: + - admin + put: + consumes: + - application/json + description: 更新用户信息(Admin 专用) + parameters: + - description: 用户 ID + in: path + name: user_id + required: true + type: string + - description: 更新用户请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateUserRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserResponse' + summary: 更新用户 + tags: + - admin + /admin/users/{user_id}/active: + put: + consumes: + - application/json + description: 设置用户是否启用(Admin 专用) + parameters: + - description: 用户 ID + in: path + name: user_id + required: true + type: string + - description: 启用状态 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.SetUserActiveRequest' + produces: + - application/json + responses: + "200": + description: OK + summary: 启用/禁用用户 + tags: + - admin + /admin/users/{user_id}/password: + put: + consumes: + - application/json + description: 重置指定用户的密码(Admin 专用) + parameters: + - description: 用户 ID + in: path + name: user_id + required: true + type: string + - description: 重置密码请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ResetPasswordRequest' + produces: + - application/json + responses: + "200": + description: OK + summary: 重置用户密码 + tags: + - admin + /admin/users/{user_id}/workspace: + put: + consumes: + - application/json + description: 将用户分配到指定工作空间(Admin 专用) + parameters: + - description: 用户 ID + in: path + name: user_id + required: true + type: string + - description: 工作空间分配请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ChangeUserWorkspaceRequest' + produces: + - application/json + responses: + "200": + description: OK + summary: 分配用户到工作空间 + tags: + - admin + /auth/login: + post: + consumes: + - application/json + description: 使用用户名和密码获取访问令牌 + parameters: + - description: 登录信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.LoginRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.AuthResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + summary: 用户登录 + tags: + - Auth + /auth/refresh: + post: + consumes: + - application/json + description: 使用刷新令牌获取新的访问令牌 + parameters: + - description: 刷新令牌 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RefreshTokenRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.AuthResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + summary: 刷新访问令牌 + tags: + - Auth + /auth/register: + post: + consumes: + - application/json + description: 创建一个新的后台用户 + parameters: + - description: 注册信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegisterRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + summary: 用户注册 + tags: + - Auth + /clusters: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 列出所有集群 + tags: + - Clusters + post: + consumes: + - application/json + description: 创建一个新的 Kubernetes 集群配置 + parameters: + - description: 集群信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateClusterRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 创建集群 + tags: + - Clusters + /clusters/{cluster_id}: + delete: + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + schema: + type: string + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 删除集群 + tags: + - Clusters + get: + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取集群详情 + tags: + - Clusters + put: + consumes: + - application/json + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + - description: 更新内容 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateClusterRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 更新集群 + tags: + - Clusters + /clusters/{cluster_id}/health: + get: + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterHealthResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取集群健康状态 + tags: + - Clusters + /clusters/{cluster_id}/instances: + get: + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceListResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 列出实例 + tags: + - Instances + post: + consumes: + - application/json + description: 在指定集群上部署一个 artifact + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + - description: 实例配置 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateInstanceRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 创建实例 + tags: + - Instances + /clusters/{cluster_id}/instances/{instance_id}: + delete: + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + - description: 实例 ID + in: path + name: instance_id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + schema: + type: string + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 删除实例 + tags: + - Instances + get: + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + - description: 实例 ID + in: path + name: instance_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取实例详情 + tags: + - Instances + put: + consumes: + - application/json + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + - description: 实例 ID + in: path + name: instance_id + required: true + type: string + - description: 更新内容 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateInstanceRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 更新实例 + tags: + - Instances + /clusters/{cluster_id}/instances/{instance_id}/entries: + get: + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + - description: 实例 ID + in: path + name: instance_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryResponse' + type: array + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取实例 Service/Ingress 入口 + tags: + - Instances + /monitoring/clusters: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterMetricsResponse' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 列出集群监控 + tags: + - Monitoring + /monitoring/clusters/{cluster_id}: + get: + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterMetricsResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取集群监控 + tags: + - Monitoring + /monitoring/clusters/{cluster_id}/nodes: + get: + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.NodeMetricsResponse' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取节点指标 + tags: + - Monitoring + /monitoring/summary: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.MonitoringSummaryResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取监控汇总 + tags: + - Monitoring + /registries: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 列出所有 Registries + tags: + - Registries + post: + consumes: + - application/json + description: 新增 OCI Registry 配置 + parameters: + - description: Registry 信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateRegistryRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 创建 Registry + tags: + - Registries + /registries/{registry_id}: + delete: + parameters: + - description: Registry ID + in: path + name: registry_id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + schema: + type: string + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 删除 Registry + tags: + - Registries + get: + parameters: + - description: Registry ID + in: path + name: registry_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取 Registry + tags: + - Registries + put: + consumes: + - application/json + parameters: + - description: Registry ID + in: path + name: registry_id + required: true + type: string + - description: 更新内容 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateRegistryRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 更新 Registry + tags: + - Registries + /registries/{registry_id}/health: + get: + parameters: + - description: Registry ID + in: path + name: registry_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryHealthResponse' + security: + - BearerAuth: [] + summary: 检查 Registry 健康 + tags: + - Registries + /registries/{registry_id}/repositories: + get: + consumes: + - application/json + description: 列出指定 Registry 中的所有 Repository + parameters: + - description: Registry ID + in: path + name: registry_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RepositoryListResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + summary: 列出 Registry 中的所有 Repositories + tags: + - Artifacts + /registries/{registry_id}/repositories/{repository_name}/artifacts: + get: + consumes: + - application/json + description: 列出指定 Repository 中的所有 Artifact,支持按类型过滤 + parameters: + - description: Registry ID + in: path + name: registry_id + required: true + type: string + - description: Repository Name (URL encoded, e.g. charts%2Fnginx) + in: path + name: repository_name + required: true + type: string + - default: all + description: 过滤 Artifact 类型 (all, chart, image, other) + in: query + name: media_type + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.TagResponse' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + summary: 列出 Repository 中的所有 Artifacts + tags: + - Artifacts + /registries/{registry_id}/repositories/{repository_name}/artifacts/{reference}: + get: + consumes: + - application/json + description: 获取指定 Artifact 的详细信息 + parameters: + - description: Registry ID + in: path + name: registry_id + required: true + type: string + - description: Repository Name (URL encoded) + in: path + name: repository_name + required: true + type: string + - description: Artifact Reference (tag or digest) + in: path + name: reference + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ArtifactResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + summary: 获取 Artifact 详情 + tags: + - Artifacts + /registries/{registry_id}/repositories/{repository_name}/artifacts/{reference}/values: + get: + consumes: + - application/json + description: 获取 Helm Chart 的 values.yaml 文件内容 (仅支持 Chart 类型) + parameters: + - description: Registry ID + in: path + name: registry_id + required: true + type: string + - description: Repository Name (URL encoded) + in: path + name: repository_name + required: true + type: string + - description: Artifact Reference (tag or digest) + in: path + name: reference + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ValuesResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + summary: 获取 Helm Chart Values + tags: + - Artifacts + /registries/{registry_id}/repositories/{repository_name}/artifacts/{reference}/values-schema: + get: + consumes: + - application/json + description: 获取 Helm Chart 的 values.schema.json (仅支持 Chart 类型) + parameters: + - description: Registry ID + in: path + name: registry_id + required: true + type: string + - description: Repository Name (URL encoded) + in: path + name: repository_name + required: true + type: string + - description: Artifact Reference (tag or digest) + in: path + name: reference + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ValuesSchemaResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + summary: 获取 Helm Chart Values Schema + tags: + - Artifacts + /users/me: + get: + consumes: + - application/json + description: 获取当前登录用户的基本信息 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserResponse' + summary: 获取当前用户信息 + tags: + - user + /users/me/password: + put: + consumes: + - application/json + description: 修改当前登录用户的密码 + parameters: + - description: 修改密码请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ChangePasswordRequest' + produces: + - application/json + responses: + "200": + description: OK + summary: 修改当前用户密码 + tags: + - user + /users/me/workspace: + get: + consumes: + - application/json + description: 获取当前用户所属工作空间的详细信息和配额 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceResponse' + summary: 获取当前用户所属工作空间 + tags: + - user + /workspaces: + get: + consumes: + - application/json + description: 获取所有工作空间列表(Admin 专用) + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceListResponse' + summary: 列出所有工作空间 + tags: + - workspace + post: + consumes: + - application/json + description: 创建新的工作空间(Admin 专用) + parameters: + - description: 创建工作空间请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateWorkspaceRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceDTO' + summary: 创建工作空间 + tags: + - workspace + /workspaces/{workspace_id}: + delete: + consumes: + - application/json + description: 删除指定工作空间(Admin 专用) + parameters: + - description: 工作空间 ID + in: path + name: workspace_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + summary: 删除工作空间 + tags: + - workspace + get: + consumes: + - application/json + description: 获取指定工作空间的详细信息和配额 + parameters: + - description: 工作空间 ID + in: path + name: workspace_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceResponse' + summary: 获取工作空间 + tags: + - workspace + put: + consumes: + - application/json + description: 更新工作空间信息(Admin 专用) + parameters: + - description: 工作空间 ID + in: path + name: workspace_id + required: true + type: string + - description: 更新工作空间请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateWorkspaceRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.WorkspaceDTO' + summary: 更新工作空间 + tags: + - workspace + /workspaces/{workspace_id}/quotas: + get: + consumes: + - application/json + description: 获取指定工作空间的资源配额 + parameters: + - description: 工作空间 ID + in: path + name: workspace_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaDTO' + type: array + summary: 获取工作空间配额 + tags: + - workspace + put: + consumes: + - application/json + description: 设置指定工作空间的 CPU/GPU/GPU Memory 配额(Admin 专用) + parameters: + - description: 工作空间 ID + in: path + name: workspace_id + required: true + type: string + - description: 配额设置请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.SetQuotasRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.QuotaDTO' + type: array + summary: 设置工作空间配额 + tags: + - workspace +schemes: +- http +- https +securityDefinitions: + BearerAuth: + description: Type "Bearer" followed by a space and JWT token. + in: header + name: Authorization + type: apiKey +swagger: "2.0" diff --git a/backend/go.mod b/backend/go.mod index c13c94c..7ee09f8 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -22,6 +22,7 @@ require ( dario.cat/mergo v1.0.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/BurntSushi/toml v1.5.0 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect @@ -46,6 +47,7 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/spec v0.20.4 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -93,10 +95,12 @@ require ( github.com/spf13/cast v1.7.0 // indirect github.com/spf13/cobra v1.10.1 // indirect github.com/spf13/pflag v1.0.9 // indirect + github.com/swaggo/swag v1.16.5 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xlab/treeprint v1.2.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/mod v0.28.0 // indirect golang.org/x/net v0.45.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.17.0 // indirect @@ -104,6 +108,7 @@ require ( golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect golang.org/x/time v0.12.0 // indirect + golang.org/x/tools v0.37.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/grpc v1.72.1 // indirect google.golang.org/protobuf v1.36.9 // indirect diff --git a/backend/go.sum b/backend/go.sum index edc1b29..971064f 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -10,6 +10,8 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -20,6 +22,8 @@ github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= @@ -94,11 +98,18 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= @@ -160,6 +171,7 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -175,6 +187,9 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -213,6 +228,7 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= @@ -280,6 +296,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/swaggo/swag v1.16.5 h1:nMf2fEV1TetMTJb4XzD0Lz7jFfKJmJKGTygEey8NSxM= +github.com/swaggo/swag v1.16.5/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= @@ -351,6 +369,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= @@ -365,16 +384,21 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= @@ -399,15 +423,19 @@ google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3i google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= helm.sh/helm/v3 v3.19.0 h1:krVyCGa8fa/wzTZgqw0DUiXuRT5BPdeqE/sQXujQ22k= diff --git a/backend/hash.go b/backend/hash.go new file mode 100644 index 0000000..a6a70e6 --- /dev/null +++ b/backend/hash.go @@ -0,0 +1,34 @@ +//go:build ignore + +package main + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + + "golang.org/x/crypto/argon2" +) + +func main() { + password := "admin123" + + memory := 64 * 1024 + iterations := 3 + parallelism := 2 + saltLength := 16 + keyLength := 32 + + salt := make([]byte, saltLength) + rand.Read(salt) + + hash := argon2.IDKey([]byte(password), salt, uint32(iterations), uint32(memory), uint8(parallelism), uint32(keyLength)) + + b64Salt := base64.RawStdEncoding.EncodeToString(salt) + b64Hash := base64.RawStdEncoding.EncodeToString(hash) + + encodedHash := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", + argon2.Version, memory, iterations, parallelism, b64Salt, b64Hash) + + fmt.Println(encodedHash) +} \ No newline at end of file diff --git a/backend/internal/adapter/input/http/dto/artifact_dto.go b/backend/internal/adapter/input/http/dto/artifact_dto.go index cd71cca..d03b0ee 100644 --- a/backend/internal/adapter/input/http/dto/artifact_dto.go +++ b/backend/internal/adapter/input/http/dto/artifact_dto.go @@ -42,3 +42,8 @@ type ValuesSchemaResponse struct { Schema string `json:"schema"` } +// ValuesResponse Values 响应 +type ValuesResponse struct { + Values string `json:"values"` +} + diff --git a/backend/internal/adapter/input/http/dto/chart_reference_dto.go b/backend/internal/adapter/input/http/dto/chart_reference_dto.go new file mode 100644 index 0000000..f22af06 --- /dev/null +++ b/backend/internal/adapter/input/http/dto/chart_reference_dto.go @@ -0,0 +1,31 @@ +package dto + +// CreateChartReferenceRequest 创建 Chart 引用请求 +type CreateChartReferenceRequest struct { + RegistryID string `json:"registry_id" binding:"required"` + Repository string `json:"repository" binding:"required"` + ChartName string `json:"chart_name" binding:"required"` + Description string `json:"description"` +} + +// UpdateChartReferenceRequest 更新 Chart 引用请求 +type UpdateChartReferenceRequest struct { + RegistryID string `json:"registry_id"` + Repository string `json:"repository"` + ChartName string `json:"chart_name"` + Description string `json:"description"` + IsEnabled *bool `json:"is_enabled"` +} + +// ChartReferenceResponse Chart 引用响应 +type ChartReferenceResponse struct { + ID string `json:"id"` + WorkspaceID string `json:"workspace_id,omitempty"` + RegistryID string `json:"registry_id"` + Repository string `json:"repository"` + ChartName string `json:"chart_name"` + Description string `json:"description"` + IsEnabled bool `json:"is_enabled"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} \ No newline at end of file diff --git a/backend/internal/adapter/input/http/dto/cluster_dto.go b/backend/internal/adapter/input/http/dto/cluster_dto.go index d84a816..774d760 100644 --- a/backend/internal/adapter/input/http/dto/cluster_dto.go +++ b/backend/internal/adapter/input/http/dto/cluster_dto.go @@ -2,30 +2,36 @@ package dto // CreateClusterRequest 创建集群请求 type CreateClusterRequest struct { - Name string `json:"name" binding:"required"` - Host string `json:"host" binding:"required"` - CAData string `json:"caData"` - CADataAlt string `json:"ca_data"` - CertData string `json:"certData"` - CertDataAlt string `json:"cert_data"` - KeyData string `json:"keyData"` - KeyDataAlt string `json:"key_data"` - Token string `json:"token"` - Description string `json:"description"` + Name string `json:"name" binding:"required"` + Host string `json:"host" binding:"required"` + CAData string `json:"caData"` + CADataAlt string `json:"ca_data"` + CertData string `json:"certData"` + CertDataAlt string `json:"cert_data"` + KeyData string `json:"keyData"` + KeyDataAlt string `json:"key_data"` + Token string `json:"token"` + Description string `json:"description"` + IsolationMode string `json:"isolationMode"` // 'namespace' | 'cluster' + DefaultNamespace string `json:"defaultNamespace"` // 默认 namespace 前缀 + IsShared bool `json:"isShared"` // 是否为共享集群 } // UpdateClusterRequest 更新集群请求 type UpdateClusterRequest struct { - Name string `json:"name"` - Host string `json:"host"` - CAData string `json:"caData"` - CADataAlt string `json:"ca_data"` - CertData string `json:"certData"` - CertDataAlt string `json:"cert_data"` - KeyData string `json:"keyData"` - KeyDataAlt string `json:"key_data"` - Token string `json:"token"` - Description string `json:"description"` + Name string `json:"name"` + Host string `json:"host"` + CAData string `json:"caData"` + CADataAlt string `json:"ca_data"` + CertData string `json:"certData"` + CertDataAlt string `json:"cert_data"` + KeyData string `json:"keyData"` + KeyDataAlt string `json:"key_data"` + Token string `json:"token"` + Description string `json:"description"` + IsolationMode string `json:"isolationMode"` + DefaultNamespace string `json:"defaultNamespace"` + IsShared *bool `json:"isShared"` } // Normalize 将多种命名风格的字段合并到统一字段 @@ -56,10 +62,16 @@ func (r *UpdateClusterRequest) Normalize() { // ClusterResponse 集群响应(敏感数据已脱敏) type ClusterResponse struct { - ID string `json:"id"` - Name string `json:"name"` - Host string `json:"host"` - Description string `json:"description"` + ID string `json:"id"` + WorkspaceID string `json:"workspaceId,omitempty"` + OwnerID string `json:"ownerId,omitempty"` + Name string `json:"name"` + Host string `json:"host"` + Description string `json:"description"` + IsolationMode string `json:"isolationMode"` // 'namespace' | 'cluster' + DefaultNamespace string `json:"defaultNamespace"` // 默认 namespace 前缀 + IsShared bool `json:"isShared"` // 是否为共享集群 + // 认证配置状态(不返回实际证书数据,仅返回是否已配置) HasCAData bool `json:"hasCaData"` HasCertData bool `json:"hasCertData"` diff --git a/backend/internal/adapter/input/http/dto/converter.go b/backend/internal/adapter/input/http/dto/converter.go index 40fa9b6..7b6766c 100644 --- a/backend/internal/adapter/input/http/dto/converter.go +++ b/backend/internal/adapter/input/http/dto/converter.go @@ -1,6 +1,8 @@ package dto import ( + "time" + "github.com/ocdp/cluster-service/internal/domain/entity" "github.com/ocdp/cluster-service/internal/pkg/crypto" ) @@ -8,42 +10,50 @@ import ( // ToRegistryResponse 转换 Registry 实体为响应 DTO(脱敏) func ToRegistryResponse(registry *entity.Registry) *RegistryResponse { response := &RegistryResponse{ - ID: registry.ID, - Name: registry.Name, - URL: registry.URL, - Description: registry.Description, - Username: registry.Username, - Insecure: registry.Insecure, - CreatedAt: registry.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), - UpdatedAt: registry.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), + ID: registry.ID, + WorkspaceID: registry.WorkspaceID, + OwnerID: registry.OwnerID, + Name: registry.Name, + URL: registry.URL, + Description: registry.Description, + Username: registry.Username, + Insecure: registry.Insecure, + IsShared: registry.IsShared, + CreatedAt: registry.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), + UpdatedAt: registry.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), } - + // 脱敏处理密码 if registry.Password != "" { response.HasPassword = true response.Password = crypto.MaskSensitiveData(registry.Password) } - + return response } // ToClusterResponse 转换 Cluster 实体为响应 DTO(脱敏) func ToClusterResponse(cluster *entity.Cluster) *ClusterResponse { response := &ClusterResponse{ - ID: cluster.ID, - Name: cluster.Name, - Host: cluster.Host, - Description: cluster.Description, - CreatedAt: cluster.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), - UpdatedAt: cluster.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), + ID: cluster.ID, + WorkspaceID: cluster.WorkspaceID, + OwnerID: cluster.OwnerID, + Name: cluster.Name, + Host: cluster.Host, + Description: cluster.Description, + IsolationMode: string(cluster.IsolationMode), + DefaultNamespace: cluster.DefaultNamespace, + IsShared: cluster.IsShared, + CreatedAt: cluster.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), + UpdatedAt: cluster.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), } - + // 设置认证配置状态标志 response.HasCAData = cluster.CAData != "" response.HasCertData = cluster.CertData != "" response.HasKeyData = cluster.KeyData != "" response.HasToken = cluster.Token != "" - + // 脱敏处理敏感数据(仅显示掩码) if cluster.CAData != "" { response.CAData = crypto.MaskSensitiveData(cluster.CAData) @@ -57,7 +67,86 @@ func ToClusterResponse(cluster *entity.Cluster) *ClusterResponse { if cluster.Token != "" { response.Token = crypto.MaskSensitiveData(cluster.Token) } - + return response } +// WorkspaceDTOFromEntity 转换 Workspace 实体为 DTO +func WorkspaceDTOFromEntity(workspace *entity.Workspace) *WorkspaceDTO { + return &WorkspaceDTO{ + ID: workspace.ID, + Name: workspace.Name, + Description: workspace.Description, + CreatedBy: workspace.CreatedBy, + CreatedAt: workspace.CreatedAt, + UpdatedAt: workspace.UpdatedAt, + } +} + +// WorkspaceDTOsFromEntities 批量转换 +func WorkspaceDTOsFromEntities(workspaces []*entity.Workspace) []*WorkspaceDTO { + result := make([]*WorkspaceDTO, len(workspaces)) + for i, w := range workspaces { + result[i] = WorkspaceDTOFromEntity(w) + } + return result +} + +// QuotaDTOFromEntity 转换 Quota 实体为 DTO +func QuotaDTOFromEntity(quota *entity.WorkspaceQuota) *QuotaDTO { + return &QuotaDTO{ + ID: quota.ID, + WorkspaceID: quota.WorkspaceID, + ResourceType: string(quota.ResourceType), + HardLimit: quota.HardLimit, + SoftLimit: quota.SoftLimit, + Used: quota.Used, + } +} + +// QuotaDTOsFromEntities 批量转换 +func QuotaDTOsFromEntities(quotas []*entity.WorkspaceQuota) []*QuotaDTO { + result := make([]*QuotaDTO, len(quotas)) + for i, q := range quotas { + result[i] = QuotaDTOFromEntity(q) + } + return result +} + +// UserDTOFromEntity 转换 User 实体为 DTO +func UserDTOFromEntity(user *entity.User, workspaceName string) *UserDTO { + return &UserDTO{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + Role: string(user.Role), + WorkspaceID: user.WorkspaceID, + WorkspaceName: workspaceName, + IsActive: user.IsActive, + MustChangePassword: user.MustChangePassword, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + } +} + +// UserDTOsFromEntities 批量转换 +func UserDTOsFromEntities(users []*entity.User, workspaceNames map[string]string) []*UserDTO { + result := make([]*UserDTO, len(users)) + for i, u := range users { + workspaceName := "" + if u.WorkspaceID != "" { + workspaceName = workspaceNames[u.WorkspaceID] + } + result[i] = UserDTOFromEntity(u, workspaceName) + } + return result +} + +// TimeToString 转换时间 +func TimeToString(t time.Time) string { + if t.IsZero() { + return "" + } + return t.Format("2006-01-02T15:04:05Z07:00") +} + diff --git a/backend/internal/adapter/input/http/dto/registry_dto.go b/backend/internal/adapter/input/http/dto/registry_dto.go index 25de40a..a40d255 100644 --- a/backend/internal/adapter/input/http/dto/registry_dto.go +++ b/backend/internal/adapter/input/http/dto/registry_dto.go @@ -23,12 +23,15 @@ type UpdateRegistryRequest struct { // RegistryResponse Registry 响应(敏感数据已脱敏) type RegistryResponse struct { ID string `json:"id"` + WorkspaceID string `json:"workspace_id,omitempty"` + OwnerID string `json:"owner_id,omitempty"` Name string `json:"name"` URL string `json:"url"` Description string `json:"description"` Username string `json:"username,omitempty"` // 明文返回用户名(不敏感) Password string `json:"password,omitempty"` // 脱敏显示(••••••••) HasPassword bool `json:"hasPassword"` // 是否已设置密码 + IsShared bool `json:"is_shared"` Insecure bool `json:"insecure"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` diff --git a/backend/internal/adapter/input/http/dto/storage_dto.go b/backend/internal/adapter/input/http/dto/storage_dto.go new file mode 100644 index 0000000..0dee6b5 --- /dev/null +++ b/backend/internal/adapter/input/http/dto/storage_dto.go @@ -0,0 +1,73 @@ +package dto + +// CreateStorageRequest 创建存储后端请求 +type CreateStorageRequest struct { + Name string `json:"name" binding:"required"` + Type string `json:"type" binding:"required"` // nfs, pv, hostPath + Description string `json:"description"` + IsDefault bool `json:"is_default"` + IsShared bool `json:"is_shared"` + + // NFS 配置 + NFS NFSConfigDTO `json:"nfs,omitempty"` + // PV 配置 + PV PVConfigDTO `json:"pv,omitempty"` + // HostPath 配置 + HostPath HostPathConfigDTO `json:"hostPath,omitempty"` +} + +// UpdateStorageRequest 更新存储后端请求 +type UpdateStorageRequest struct { + Name string `json:"name"` + Type string `json:"type"` + Description string `json:"description"` + IsDefault bool `json:"is_default"` + IsShared bool `json:"is_shared"` + + // NFS 配置 + NFS NFSConfigDTO `json:"nfs,omitempty"` + // PV 配置 + PV PVConfigDTO `json:"pv,omitempty"` + // HostPath 配置 + HostPath HostPathConfigDTO `json:"hostPath,omitempty"` +} + +// NFSConfigDTO NFS 配置 +type NFSConfigDTO struct { + Server string `json:"server"` + Path string `json:"path"` +} + +// PVConfigDTO PV 配置 +type PVConfigDTO struct { + StorageClassName string `json:"storageClassName"` + Capacity string `json:"capacity"` + AccessModes []string `json:"accessModes"` +} + +// HostPathConfigDTO HostPath 配置 +type HostPathConfigDTO struct { + Path string `json:"path"` +} + +// StorageResponse 存储后端响应 +type StorageResponse struct { + ID string `json:"id"` + WorkspaceID string `json:"workspace_id,omitempty"` + OwnerID string `json:"owner_id,omitempty"` + Name string `json:"name"` + Type string `json:"type"` + Config StorageConfigDTO `json:"config"` + Description string `json:"description"` + IsDefault bool `json:"is_default"` + IsShared bool `json:"is_shared"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +// StorageConfigDTO 存储配置(脱敏后) +type StorageConfigDTO struct { + NFS *NFSConfigDTO `json:"nfs,omitempty"` + PV *PVConfigDTO `json:"pv,omitempty"` + HostPath *HostPathConfigDTO `json:"hostPath,omitempty"` +} \ No newline at end of file diff --git a/backend/internal/adapter/input/http/dto/user_dto.go b/backend/internal/adapter/input/http/dto/user_dto.go new file mode 100644 index 0000000..73d3b17 --- /dev/null +++ b/backend/internal/adapter/input/http/dto/user_dto.go @@ -0,0 +1,78 @@ +package dto + +import "time" + +// UserDTO 用户 DTO +type UserDTO struct { + ID string `json:"id"` + Username string `json:"username"` + Email string `json:"email,omitempty"` + Role string `json:"role"` + WorkspaceID string `json:"workspace_id,omitempty"` + WorkspaceName string `json:"workspace_name,omitempty"` + IsActive bool `json:"is_active"` + MustChangePassword bool `json:"must_change_password"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// CreateUserRequest 创建用户请求(Admin 操作) +type CreateUserRequest struct { + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required,min=6"` + Email string `json:"email"` + Role string `json:"role" validate:"required,oneof=admin user"` + WorkspaceID string `json:"workspace_id"` +} + +// UpdateUserRequest 更新用户请求 +type UpdateUserRequest struct { + Email string `json:"email"` + IsActive *bool `json:"is_active"` +} + +// ChangeUserWorkspaceRequest 分配用户到 Workspace 请求 +type ChangeUserWorkspaceRequest struct { + WorkspaceID string `json:"workspace_id" validate:"required"` +} + +// ResetPasswordRequest 重置密码请求 +type ResetPasswordRequest struct { + NewPassword string `json:"new_password" validate:"required,min=6"` +} + +// ChangePasswordRequest 修改密码请求 +type ChangePasswordRequest struct { + OldPassword string `json:"old_password" validate:"required"` + NewPassword string `json:"new_password" validate:"required,min=6"` +} + +// SetUserActiveRequest 启用/禁用用户请求 +type SetUserActiveRequest struct { + IsActive bool `json:"is_active"` +} + +// UserListResponse 用户列表响应 +type UserListResponse struct { + Users []*UserDTO `json:"users"` + Total int `json:"total"` +} + +// UserWithWorkspaceResponse 用户及其 Workspace 响应 +type UserWithWorkspaceResponse struct { + User *UserDTO `json:"user"` + Workspace *WorkspaceDTO `json:"workspace,omitempty"` +} + +// LoginResponse 登录响应 +type LoginResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int `json:"expires_in"` + MustChangePassword bool `json:"must_change_password"` +} + +// UserResponseWithDTO 用户响应(包含完整DTO) +type UserResponseWithDTO struct { + User *UserDTO `json:"user"` +} \ No newline at end of file diff --git a/backend/internal/adapter/input/http/dto/values_template_dto.go b/backend/internal/adapter/input/http/dto/values_template_dto.go new file mode 100644 index 0000000..97d01e8 --- /dev/null +++ b/backend/internal/adapter/input/http/dto/values_template_dto.go @@ -0,0 +1,37 @@ +package dto + +// CreateValuesTemplateRequest 创建 Values 模板请求 +type CreateValuesTemplateRequest struct { + ChartReferenceID string `json:"chart_reference_id" binding:"required"` + Name string `json:"name" binding:"required"` + Description string `json:"description"` + ValuesYAML string `json:"values_yaml" binding:"required"` + IsDefault bool `json:"is_default"` +} + +// UpdateValuesTemplateRequest 更新 Values 模板请求 +type UpdateValuesTemplateRequest struct { + Description string `json:"description"` + ValuesYAML string `json:"values_yaml"` + IsDefault *bool `json:"is_default"` +} + +// ValuesTemplateResponse Values 模板响应 +type ValuesTemplateResponse struct { + ID string `json:"id"` + WorkspaceID string `json:"workspace_id,omitempty"` + OwnerID string `json:"owner_id,omitempty"` + ChartReferenceID string `json:"chart_reference_id"` + Name string `json:"name"` + Description string `json:"description"` + ValuesYAML string `json:"values_yaml"` + Version int `json:"version"` + IsDefault bool `json:"is_default"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +// RollbackValuesTemplateRequest 回滚请求 +type RollbackValuesTemplateRequest struct { + TemplateID string `json:"template_id" binding:"required"` +} \ No newline at end of file diff --git a/backend/internal/adapter/input/http/dto/workspace_dto.go b/backend/internal/adapter/input/http/dto/workspace_dto.go new file mode 100644 index 0000000..4986934 --- /dev/null +++ b/backend/internal/adapter/input/http/dto/workspace_dto.go @@ -0,0 +1,67 @@ +package dto + +import "time" + +// WorkspaceDTO 工作空间 DTO +type WorkspaceDTO struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + CreatedBy string `json:"created_by"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// CreateWorkspaceRequest 创建工作空间请求 +type CreateWorkspaceRequest struct { + Name string `json:"name" validate:"required"` + Description string `json:"description"` +} + +// UpdateWorkspaceRequest 更新工作空间请求 +type UpdateWorkspaceRequest struct { + Name string `json:"name"` + Description string `json:"description"` +} + +// QuotaDTO 配额 DTO +type QuotaDTO struct { + ID string `json:"id"` + WorkspaceID string `json:"workspace_id"` + ResourceType string `json:"resource_type"` + HardLimit float64 `json:"hard_limit"` + SoftLimit float64 `json:"soft_limit"` + Used float64 `json:"used"` +} + +// SetQuotaRequest 设置配额请求 +type SetQuotaRequest struct { + ResourceType string `json:"resource_type" validate:"required"` + HardLimit float64 `json:"hard_limit" validate:"required"` + SoftLimit float64 `json:"soft_limit"` +} + +// SetQuotasRequest 批量设置配额请求 +type SetQuotasRequest struct { + CPU *QuotaValue `json:"cpu"` + GPU *QuotaValue `json:"gpu"` + GPUMemory *QuotaValue `json:"gpu_memory"` +} + +// QuotaValue 配额值 +type QuotaValue struct { + HardLimit float64 `json:"hard_limit"` + SoftLimit float64 `json:"soft_limit"` +} + +// WorkspaceResponse 工作空间响应 +type WorkspaceResponse struct { + Workspace *WorkspaceDTO `json:"workspace"` + Quotas []*QuotaDTO `json:"quotas,omitempty"` +} + +// WorkspaceListResponse 工作空间列表响应 +type WorkspaceListResponse struct { + Workspaces []*WorkspaceDTO `json:"workspaces"` + Total int `json:"total"` +} \ No newline at end of file diff --git a/backend/internal/adapter/input/http/middleware/authz.go b/backend/internal/adapter/input/http/middleware/authz.go new file mode 100644 index 0000000..67e8c18 --- /dev/null +++ b/backend/internal/adapter/input/http/middleware/authz.go @@ -0,0 +1,283 @@ +package middleware + +import ( + "context" + "net/http" + "strings" + + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/repository" + "github.com/ocdp/cluster-service/internal/domain/service" +) + +// Context keys +type contextKey string + +const ( + ContextKeyUserID contextKey = "user_id" + ContextKeyUsername contextKey = "username" + ContextKeyUserRole contextKey = "user_role" + ContextKeyWorkspaceID contextKey = "workspace_id" +) + +// UserClaims 用户声明(从 JWT 解析) +type UserClaims struct { + UserID string + Username string + Role entity.UserRole + WorkspaceID string +} + +// WorkspaceMiddleware 工作空间中间件 +// 从 JWT 获取用户角色和 workspace_id,进行权限检查 +func WorkspaceMiddleware(userRepo repository.UserRepository) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // 从 Header 获取 Token + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + http.Error(w, "Missing authorization header", http.StatusUnauthorized) + return + } + + // 解析 Bearer Token + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || parts[0] != "Bearer" { + http.Error(w, "Invalid authorization header", http.StatusUnauthorized) + return + } + token := parts[1] + + // 这里需要从 AuthService 获取验证方法 + // 简化处理:假设 token 包含 user_id 和 username + // 实际实现需要调用 JWT 验证服务 + + // 从数据库获取用户信息 + // 注意:这里需要通过 token 解析出 userID + // 实际实现应该在 AuthService 中完成 + _ = userRepo + + next.ServeHTTP(w, r) + }) + } +} + +// RequireWorkspace 强制要求 workspace 上下文 +// 用于非 Admin 用户的资源操作 +func RequireWorkspace(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + workspaceID := r.Header.Get("X-Workspace-ID") + userRole := r.Header.Get("X-User-Role") + + // Admin 可以没有 workspace + if userRole == string(entity.RoleAdmin) { + next.ServeHTTP(w, r) + return + } + + // 普通用户必须有 workspace + if workspaceID == "" { + http.Error(w, "Workspace context required", http.StatusForbidden) + return + } + + // 将 workspace_id 放入 context + ctx := context.WithValue(r.Context(), ContextKeyWorkspaceID, workspaceID) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +// RequireAdmin 要求 Admin 角色 +func RequireAdmin(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + userRole := r.Header.Get("X-User-Role") + + if userRole != string(entity.RoleAdmin) { + http.Error(w, "Admin access required", http.StatusForbidden) + return + } + + next.ServeHTTP(w, r) + }) +} + +// GetUserClaims 从 Context 获取用户声明 +func GetUserClaims(ctx context.Context) *UserClaims { + userID, _ := ctx.Value(ContextKeyUserID).(string) + username, _ := ctx.Value(ContextKeyUsername).(string) + roleStr, _ := ctx.Value(ContextKeyUserRole).(string) + workspaceID, _ := ctx.Value(ContextKeyWorkspaceID).(string) + + return &UserClaims{ + UserID: userID, + Username: username, + Role: entity.UserRole(roleStr), + WorkspaceID: workspaceID, + } +} + +// GetWorkspaceID 从 Context 获取 workspace ID +func GetWorkspaceID(ctx context.Context) string { + workspaceID, _ := ctx.Value(ContextKeyWorkspaceID).(string) + return workspaceID +} + +// GetUserID 从 Context 获取用户 ID +func GetUserID(ctx context.Context) string { + userID, _ := ctx.Value(ContextKeyUserID).(string) + return userID +} + +// GetUserRole 从 Context 获取用户角色 +func GetUserRole(ctx context.Context) entity.UserRole { + roleStr, _ := ctx.Value(ContextKeyUserRole).(string) + return entity.UserRole(roleStr) +} + +// FilterByWorkspace 根据用户角色过滤资源 +// Admin: 返回所有资源(workspaceID 忽略) +// User: 仅返回属于自己 workspace 的资源 +func FilterByWorkspace(workspaceID, userRole string) (filterWorkspaceID string, isAdmin bool) { + if userRole == string(entity.RoleAdmin) { + return "", true + } + return workspaceID, false +} + +// AuthorizationService 授权服务 +type AuthorizationService struct { + userRepo repository.UserRepository +} + +// NewAuthorizationService 创建授权服务 +func NewAuthorizationService(userRepo repository.UserRepository) *AuthorizationService { + return &AuthorizationService{ + userRepo: userRepo, + } +} + +// CheckResourceAccess 检查用户是否有权访问指定资源 +func (s *AuthorizationService) CheckResourceAccess(ctx context.Context, userID, resourceWorkspaceID string) error { + user, err := s.userRepo.GetByID(ctx, userID) + if err != nil { + return err + } + + // Admin 可以访问所有资源 + if user.Role == entity.RoleAdmin { + return nil + } + + // 普通用户只能访问自己 workspace 的资源 + if user.WorkspaceID != resourceWorkspaceID { + return entity.ErrPermissionDenied + } + + return nil +} + +// CanAccessWorkspace 检查用户是否可以访问指定 workspace +func (s *AuthorizationService) CanAccessWorkspace(ctx context.Context, userID, targetWorkspaceID string) error { + user, err := s.userRepo.GetByID(ctx, userID) + if err != nil { + return err + } + + // Admin 可以访问所有 workspace + if user.Role == entity.RoleAdmin { + return nil + } + + // 普通用户只能访问自己的 workspace + if user.WorkspaceID != targetWorkspaceID { + return entity.ErrPermissionDenied + } + + return nil +} + +// GetAccessibleWorkspaces 获取用户可访问的 workspace 列表 +func (s *AuthorizationService) GetAccessibleWorkspaces(ctx context.Context, userID string) ([]string, error) { + user, err := s.userRepo.GetByID(ctx, userID) + if err != nil { + return nil, err + } + + // Admin 可以访问所有 workspace + if user.Role == entity.RoleAdmin { + return nil, nil // nil 表示所有 + } + + // 普通用户只能访问自己的 workspace + if user.WorkspaceID != "" { + return []string{user.WorkspaceID}, nil + } + + return []string{}, nil +} + +// RequireRole 要求特定角色 +func RequireRole(roles ...entity.UserRole) func(http.Handler) http.Handler { + roleSet := make(map[entity.UserRole]bool) + for _, r := range roles { + roleSet[r] = true + } + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + userRole := r.Header.Get("X-User-Role") + + if !roleSet[entity.UserRole(userRole)] { + http.Error(w, "Insufficient permissions", http.StatusForbidden) + return + } + + next.ServeHTTP(w, r) + }) + } +} + +// WithUserClaims 将用户声明注入到 Context +func WithUserClaims(claims *UserClaims) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + ctx = context.WithValue(ctx, ContextKeyUserID, claims.UserID) + ctx = context.WithValue(ctx, ContextKeyUsername, claims.Username) + ctx = context.WithValue(ctx, ContextKeyUserRole, string(claims.Role)) + if claims.WorkspaceID != "" { + ctx = context.WithValue(ctx, ContextKeyWorkspaceID, claims.WorkspaceID) + } + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} + +// LoginRequired 要求登录 +func LoginRequired(authService *service.AuthService) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + http.Error(w, "Authorization required", http.StatusUnauthorized) + return + } + + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || parts[0] != "Bearer" { + http.Error(w, "Invalid authorization header", http.StatusUnauthorized) + return + } + + token := parts[1] + userID, _, err := authService.VerifyAccessToken(r.Context(), token) + if err != nil { + http.Error(w, "Invalid token", http.StatusUnauthorized) + return + } + + ctx := context.WithValue(r.Context(), ContextKeyUserID, userID) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} \ No newline at end of file diff --git a/backend/internal/adapter/input/http/rest/artifact_handler.go b/backend/internal/adapter/input/http/rest/artifact_handler.go index d04bdbc..09bdff8 100644 --- a/backend/internal/adapter/input/http/rest/artifact_handler.go +++ b/backend/internal/adapter/input/http/rest/artifact_handler.go @@ -191,3 +191,42 @@ func (h *ArtifactHandler) GetArtifactValuesSchema(w http.ResponseWriter, r *http respondJSON(w, http.StatusOK, response) } + +// GetArtifactValues 获取 Helm Chart 的 values.yaml +// @Summary 获取 Helm Chart Values +// @Description 获取 Helm Chart 的 values.yaml 文件内容 (仅支持 Chart 类型) +// @Tags Artifacts +// @Accept json +// @Produce json +// @Param registry_id path string true "Registry ID" +// @Param repository_name path string true "Repository Name (URL encoded)" +// @Param reference path string true "Artifact Reference (tag or digest)" +// @Success 200 {object} dto.ValuesResponse +// @Failure 500 {object} dto.ErrorResponse +// @Router /registries/{registry_id}/repositories/{repository_name}/artifacts/{reference}/values [get] +func (h *ArtifactHandler) GetArtifactValues(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + registryID := vars["registry_id"] + repositoryName := vars["repository_name"] + reference := vars["reference"] + + values, err := h.artifactService.GetValues(r.Context(), registryID, repositoryName, reference) + if err != nil { + switch { + case errors.Is(err, entity.ErrRegistryNotFound), + errors.Is(err, entity.ErrRepositoryNotFound), + errors.Is(err, entity.ErrArtifactNotFound), + errors.Is(err, entity.ErrValuesNotFound): + respondError(w, http.StatusNotFound, "Values not found", err.Error()) + default: + respondError(w, http.StatusInternalServerError, "Failed to get values", err.Error()) + } + return + } + + response := &dto.ValuesResponse{ + Values: values, + } + + respondJSON(w, http.StatusOK, response) +} diff --git a/backend/internal/adapter/input/http/rest/chart_reference_handler.go b/backend/internal/adapter/input/http/rest/chart_reference_handler.go new file mode 100644 index 0000000..b9d1cba --- /dev/null +++ b/backend/internal/adapter/input/http/rest/chart_reference_handler.go @@ -0,0 +1,229 @@ +package rest + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" + "github.com/ocdp/cluster-service/internal/adapter/input/http/dto" + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/service" +) + +// ChartReferenceHandler Chart Reference Handler +type ChartReferenceHandler struct { + chartRefService *service.ChartReferenceService +} + +// NewChartReferenceHandler 创建 Chart Reference Handler +func NewChartReferenceHandler(chartRefService *service.ChartReferenceService) *ChartReferenceHandler { + return &ChartReferenceHandler{ + chartRefService: chartRefService, + } +} + +// CreateChartReference 创建 Chart 引用 +// @Summary 创建 Chart 引用 +// @Description 新增 Chart 引用配置 +// @Tags Chart References +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body dto.CreateChartReferenceRequest true "Chart 引用信息" +// @Success 201 {object} dto.ChartReferenceResponse +// @Failure 400 {object} dto.ErrorResponse +// @Router /chart-references [post] +func (h *ChartReferenceHandler) CreateChartReference(w http.ResponseWriter, r *http.Request) { + var req dto.CreateChartReferenceRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", err.Error()) + return + } + + // 获取用户信息 + workspaceID := r.Header.Get("X-Workspace-ID") + + chartRef, err := h.chartRefService.Create( + r.Context(), + workspaceID, + req.RegistryID, + req.Repository, + req.ChartName, + req.Description, + ) + if err != nil { + respondError(w, http.StatusBadRequest, "Failed to create chart reference", err.Error()) + return + } + + response := toChartReferenceResponse(chartRef) + respondJSON(w, http.StatusCreated, response) +} + +// GetChartReference 获取 Chart 引用详情 +// @Summary 获取 Chart 引用 +// @Tags Chart References +// @Produce json +// @Security BearerAuth +// @Param chart_reference_id path string true "Chart Reference ID" +// @Success 200 {object} dto.ChartReferenceResponse +// @Failure 404 {object} dto.ErrorResponse +// @Router /chart-references/{chart_reference_id} [get] +func (h *ChartReferenceHandler) GetChartReference(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + chartRefID := vars["chart_reference_id"] + + chartRef, err := h.chartRefService.GetByID(r.Context(), chartRefID) + if err != nil { + respondError(w, http.StatusNotFound, "Chart reference not found", err.Error()) + return + } + + response := toChartReferenceResponse(chartRef) + respondJSON(w, http.StatusOK, response) +} + +// GetAllChartReferences 获取所有 Chart 引用 +// @Summary 列出所有 Chart 引用 +// @Tags Chart References +// @Produce json +// @Security BearerAuth +// @Success 200 {array} dto.ChartReferenceResponse +// @Failure 500 {object} dto.ErrorResponse +// @Router /chart-references [get] +func (h *ChartReferenceHandler) GetAllChartReferences(w http.ResponseWriter, r *http.Request) { + workspaceID := r.Header.Get("X-Workspace-ID") + role := r.Header.Get("X-User-Role") + + var chartRefs []*dto.ChartReferenceResponse + + // Admin 可以看到所有,其他用户只看自己 workspace + if role == "admin" { + allChartRefs, err := h.chartRefService.List(r.Context()) + if err != nil { + respondError(w, http.StatusInternalServerError, "Failed to list chart references", err.Error()) + return + } + for _, cr := range allChartRefs { + chartRefs = append(chartRefs, toChartReferenceResponse(cr)) + } + } else if workspaceID != "" { + workspaceChartRefs, err := h.chartRefService.GetByWorkspace(r.Context(), workspaceID) + if err != nil { + respondError(w, http.StatusInternalServerError, "Failed to list chart references", err.Error()) + return + } + for _, cr := range workspaceChartRefs { + chartRefs = append(chartRefs, toChartReferenceResponse(cr)) + } + } + + respondJSON(w, http.StatusOK, chartRefs) +} + +// UpdateChartReference 更新 Chart 引用 +// @Summary 更新 Chart 引用 +// @Tags Chart References +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param chart_reference_id path string true "Chart Reference ID" +// @Param request body dto.UpdateChartReferenceRequest true "更新内容" +// @Success 200 {object} dto.ChartReferenceResponse +// @Failure 404 {object} dto.ErrorResponse +// @Router /chart-references/{chart_reference_id} [put] +func (h *ChartReferenceHandler) UpdateChartReference(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + chartRefID := vars["chart_reference_id"] + + var req dto.UpdateChartReferenceRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", err.Error()) + return + } + + isEnabled := true + if req.IsEnabled != nil { + isEnabled = *req.IsEnabled + } + + chartRef, err := h.chartRefService.Update( + r.Context(), + chartRefID, + req.RegistryID, + req.Repository, + req.ChartName, + req.Description, + isEnabled, + ) + if err != nil { + respondError(w, http.StatusBadRequest, "Failed to update chart reference", err.Error()) + return + } + + response := toChartReferenceResponse(chartRef) + respondJSON(w, http.StatusOK, response) +} + +// DeleteChartReference 删除 Chart 引用 +// @Summary 删除 Chart 引用 +// @Tags Chart References +// @Produce json +// @Security BearerAuth +// @Param chart_reference_id path string true "Chart Reference ID" +// @Success 204 {string} string "No Content" +// @Failure 404 {object} dto.ErrorResponse +// @Router /chart-references/{chart_reference_id} [delete] +func (h *ChartReferenceHandler) DeleteChartReference(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + chartRefID := vars["chart_reference_id"] + + if err := h.chartRefService.Delete(r.Context(), chartRefID); err != nil { + respondError(w, http.StatusNotFound, "Failed to delete chart reference", err.Error()) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// GetChartReferencesByRegistry 获取 Registry 的所有 Chart 引用 +// @Summary 获取 Registry 的 Chart 引用 +// @Tags Chart References +// @Produce json +// @Security BearerAuth +// @Param registry_id path string true "Registry ID" +// @Success 200 {array} dto.ChartReferenceResponse +// @Failure 500 {object} dto.ErrorResponse +// @Router /registries/{registry_id}/chart-references [get] +func (h *ChartReferenceHandler) GetChartReferencesByRegistry(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + registryID := vars["registry_id"] + + chartRefs, err := h.chartRefService.GetByRegistry(r.Context(), registryID) + if err != nil { + respondError(w, http.StatusInternalServerError, "Failed to list chart references", err.Error()) + return + } + + responses := make([]*dto.ChartReferenceResponse, 0, len(chartRefs)) + for _, cr := range chartRefs { + responses = append(responses, toChartReferenceResponse(cr)) + } + + respondJSON(w, http.StatusOK, responses) +} + +// toChartReferenceResponse 转换为响应 DTO +func toChartReferenceResponse(chartRef *entity.ChartReference) *dto.ChartReferenceResponse { + return &dto.ChartReferenceResponse{ + ID: chartRef.ID, + WorkspaceID: chartRef.WorkspaceID, + RegistryID: chartRef.RegistryID, + Repository: chartRef.Repository, + ChartName: chartRef.ChartName, + Description: chartRef.Description, + IsEnabled: chartRef.IsEnabled, + CreatedAt: chartRef.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), + UpdatedAt: chartRef.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), + } +} \ No newline at end of file diff --git a/backend/internal/adapter/input/http/rest/cluster_handler.go b/backend/internal/adapter/input/http/rest/cluster_handler.go index c887f8e..07833d1 100644 --- a/backend/internal/adapter/input/http/rest/cluster_handler.go +++ b/backend/internal/adapter/input/http/rest/cluster_handler.go @@ -40,13 +40,20 @@ func (h *ClusterHandler) CreateCluster(w http.ResponseWriter, r *http.Request) { respondError(w, http.StatusBadRequest, "Invalid request body", err.Error()) return } - req.Normalize() + req.Normalize() // 创建实体 - cluster := entity.NewCluster(req.Name, req.Host) + cluster := entity.NewCluster("", "", req.Name, req.Host) cluster.Description = req.Description - if req.CertData != "" && req.KeyData != "" { + // 设置认证信息 + hasKubeconfig := req.CAData != "" && (len(req.CAData) > 100 && (req.CAData[:11] == "apiVersion:" || req.CAData[:5] == "kind:")) + hasCertAuth := req.CertData != "" && req.KeyData != "" + + if hasKubeconfig { + // 使用完整的 kubeconfig 格式 + cluster.CAData = req.CAData + } else if hasCertAuth { cluster.SetCertAuth(req.CAData, req.CertData, req.KeyData) } else if req.Token != "" { cluster.SetTokenAuth(req.Token) @@ -57,6 +64,18 @@ func (h *ClusterHandler) CreateCluster(w http.ResponseWriter, r *http.Request) { "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1vY2sgQ2xpZW50IENlcnRpZmljYXRlCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=", "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNb2NrIFByaXZhdGUgS2V5Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0t", ) + } else { + // 生产模式:没有提供凭证,尝试使用本地 kubeconfig + // 不再返回错误,让 TestConnection 尝试使用本地 kubeconfig + // cluster 保持空的认证信息,TestConnection 会使用 KUBECONFIG 环境变量 + } + + // 测试集群连接(非 mock 模式下) + if os.Getenv("ADAPTER_MODE") != "mock" { + if err := h.clusterService.TestConnection(r.Context(), cluster); err != nil { + respondError(w, http.StatusBadRequest, "Failed to connect to cluster", err.Error()) + return + } } // 调用领域服务 @@ -198,18 +217,24 @@ func (h *ClusterHandler) GetClusterHealth(w http.ResponseWriter, r *http.Request vars := mux.Vars(r) clusterID := vars["cluster_id"] - // 检查集群是否存在 - _, err := h.clusterService.GetCluster(r.Context(), clusterID) + // 获取集群 + cluster, err := h.clusterService.GetCluster(r.Context(), clusterID) if err != nil { respondError(w, http.StatusNotFound, "Cluster not found", err.Error()) return } - // TODO: 实现真实的健康检查 + // 测试连接 + err = h.clusterService.TestConnection(r.Context(), cluster) + response := &dto.ClusterHealthResponse{ - Healthy: true, - Message: "Cluster is healthy", - Version: "v1.28.0", + Healthy: err == nil, + } + + if err != nil { + response.Message = err.Error() + } else { + response.Message = "Cluster is healthy" } respondJSON(w, http.StatusOK, response) diff --git a/backend/internal/adapter/input/http/rest/instance_handler.go b/backend/internal/adapter/input/http/rest/instance_handler.go index 777e965..cb03b46 100644 --- a/backend/internal/adapter/input/http/rest/instance_handler.go +++ b/backend/internal/adapter/input/http/rest/instance_handler.go @@ -54,10 +54,14 @@ func (h *InstanceHandler) CreateInstance(w http.ResponseWriter, r *http.Request) // 创建实体 instance := entity.NewInstance( + "", // workspaceID - will be set based on user + "", // ownerID - will be set based on user clusterID, + req.RegistryID, + "", // chartReferenceID - not used in legacy API + "", // valuesTemplateID - not used in legacy API req.Name, req.Namespace, - req.RegistryID, req.Repository, chart, // Extracted chart name req.Tag, // Tag mapped to version diff --git a/backend/internal/adapter/input/http/rest/registry_handler.go b/backend/internal/adapter/input/http/rest/registry_handler.go index f6d1a03..bd03bb3 100644 --- a/backend/internal/adapter/input/http/rest/registry_handler.go +++ b/backend/internal/adapter/input/http/rest/registry_handler.go @@ -41,7 +41,7 @@ func (h *RegistryHandler) CreateRegistry(w http.ResponseWriter, r *http.Request) } // 创建实体 - registry := entity.NewRegistry(req.Name, req.URL) + registry := entity.NewRegistry("", "", req.Name, req.URL) registry.Description = req.Description registry.Insecure = req.Insecure registry.SetCredentials(req.Username, req.Password) diff --git a/backend/internal/adapter/input/http/rest/storage_handler.go b/backend/internal/adapter/input/http/rest/storage_handler.go new file mode 100644 index 0000000..e1e749c --- /dev/null +++ b/backend/internal/adapter/input/http/rest/storage_handler.go @@ -0,0 +1,291 @@ +package rest + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" + "github.com/ocdp/cluster-service/internal/adapter/input/http/dto" + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/service" +) + +// StorageHandler Storage Backend Handler +type StorageHandler struct { + storageService *service.StorageService +} + +// NewStorageHandler 创建 Storage Handler +func NewStorageHandler(storageService *service.StorageService) *StorageHandler { + return &StorageHandler{ + storageService: storageService, + } +} + +// CreateStorage 创建存储后端 +// @Summary 创建存储后端 +// @Description 新增存储后端配置(NFS/PV/hostPath) +// @Tags Storage +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body dto.CreateStorageRequest true "存储后端信息" +// @Success 201 {object} dto.StorageResponse +// @Failure 400 {object} dto.ErrorResponse +// @Router /storage-backends [post] +func (h *StorageHandler) CreateStorage(w http.ResponseWriter, r *http.Request) { + var req dto.CreateStorageRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", err.Error()) + return + } + + // 获取用户信息 + workspaceID := r.Header.Get("X-Workspace-ID") + ownerID := r.Header.Get("X-User-ID") + + // 构建配置 + storageType := entity.StorageType(req.Type) + config := entity.StorageConfig{} + + switch storageType { + case entity.StorageTypeNFS: + config.NFS = &entity.NFSConfig{ + Server: req.NFS.Server, + Path: req.NFS.Path, + } + case entity.StorageTypePV: + config.PV = &entity.PVConfig{ + StorageClassName: req.PV.StorageClassName, + Capacity: req.PV.Capacity, + AccessModes: req.PV.AccessModes, + } + case entity.StorageTypeHostPath: + config.HostPath = &entity.HostPathConfig{ + Path: req.HostPath.Path, + } + } + + // 调用领域服务 + storage, err := h.storageService.Create( + r.Context(), + workspaceID, + ownerID, + req.Name, + storageType, + config, + req.Description, + req.IsDefault, + req.IsShared, + ) + if err != nil { + respondError(w, http.StatusBadRequest, "Failed to create storage backend", err.Error()) + return + } + + // 返回响应 + response := toStorageResponse(storage) + respondJSON(w, http.StatusCreated, response) +} + +// GetStorage 获取存储后端详情 +// @Summary 获取存储后端 +// @Tags Storage +// @Produce json +// @Security BearerAuth +// @Param storage_id path string true "Storage ID" +// @Success 200 {object} dto.StorageResponse +// @Failure 404 {object} dto.ErrorResponse +// @Router /storage-backends/{storage_id} [get] +func (h *StorageHandler) GetStorage(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + storageID := vars["storage_id"] + + storage, err := h.storageService.GetByID(r.Context(), storageID) + if err != nil { + respondError(w, http.StatusNotFound, "Storage backend not found", err.Error()) + return + } + + response := toStorageResponse(storage) + respondJSON(w, http.StatusOK, response) +} + +// GetAllStorage 获取所有存储后端 +// @Summary 列出所有存储后端 +// @Tags Storage +// @Produce json +// @Security BearerAuth +// @Success 200 {array} dto.StorageResponse +// @Failure 500 {object} dto.ErrorResponse +// @Router /storage-backends [get] +func (h *StorageHandler) GetAllStorage(w http.ResponseWriter, r *http.Request) { + // 获取 workspace_id(从 JWT) + workspaceID := r.Header.Get("X-Workspace-ID") + role := r.Header.Get("X-User-Role") + + var storages []*entity.StorageBackend + var err error + + // Admin 可以看到所有,其他用户只看自己 workspace + 共享的 + if role == "admin" { + storages, err = h.storageService.List(r.Context()) + } else if workspaceID != "" { + // 获取 workspace 的存储 + 共享存储 + workspaceStorages, _ := h.storageService.GetByWorkspace(r.Context(), workspaceID) + sharedStorages, _ := h.storageService.GetShared(r.Context()) + + // 合并去重 + seen := make(map[string]bool) + for _, s := range workspaceStorages { + if !seen[s.ID] { + storages = append(storages, s) + seen[s.ID] = true + } + } + for _, s := range sharedStorages { + if !seen[s.ID] { + storages = append(storages, s) + seen[s.ID] = true + } + } + } else { + // 没有 workspace 的用户只能看到共享存储 + storages, err = h.storageService.GetShared(r.Context()) + } + + if err != nil { + respondError(w, http.StatusInternalServerError, "Failed to list storage backends", err.Error()) + return + } + + responses := make([]*dto.StorageResponse, 0, len(storages)) + for _, storage := range storages { + responses = append(responses, toStorageResponse(storage)) + } + + respondJSON(w, http.StatusOK, responses) +} + +// UpdateStorage 更新存储后端 +// @Summary 更新存储后端 +// @Tags Storage +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param storage_id path string true "Storage ID" +// @Param request body dto.UpdateStorageRequest true "更新内容" +// @Success 200 {object} dto.StorageResponse +// @Failure 404 {object} dto.ErrorResponse +// @Router /storage-backends/{storage_id} [put] +func (h *StorageHandler) UpdateStorage(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + storageID := vars["storage_id"] + + var req dto.UpdateStorageRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", err.Error()) + return + } + + // 构建配置 + var storageType entity.StorageType + if req.Type != "" { + storageType = entity.StorageType(req.Type) + } + + config := entity.StorageConfig{} + if storageType == entity.StorageTypeNFS && (req.NFS.Server != "" || req.NFS.Path != "") { + config.NFS = &entity.NFSConfig{ + Server: req.NFS.Server, + Path: req.NFS.Path, + } + } else if storageType == entity.StorageTypePV && req.PV.StorageClassName != "" { + config.PV = &entity.PVConfig{ + StorageClassName: req.PV.StorageClassName, + Capacity: req.PV.Capacity, + AccessModes: req.PV.AccessModes, + } + } else if storageType == entity.StorageTypeHostPath && req.HostPath.Path != "" { + config.HostPath = &entity.HostPathConfig{ + Path: req.HostPath.Path, + } + } + + storage, err := h.storageService.Update( + r.Context(), + storageID, + req.Name, + req.Description, + storageType, + config, + req.IsDefault, + req.IsShared, + ) + if err != nil { + respondError(w, http.StatusBadRequest, "Failed to update storage backend", err.Error()) + return + } + + response := toStorageResponse(storage) + respondJSON(w, http.StatusOK, response) +} + +// DeleteStorage 删除存储后端 +// @Summary 删除存储后端 +// @Tags Storage +// @Produce json +// @Security BearerAuth +// @Param storage_id path string true "Storage ID" +// @Success 204 {string} string "No Content" +// @Failure 404 {object} dto.ErrorResponse +// @Router /storage-backends/{storage_id} [delete] +func (h *StorageHandler) DeleteStorage(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + storageID := vars["storage_id"] + + if err := h.storageService.Delete(r.Context(), storageID); err != nil { + respondError(w, http.StatusNotFound, "Failed to delete storage backend", err.Error()) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// toStorageResponse 转换为响应 DTO +func toStorageResponse(storage *entity.StorageBackend) *dto.StorageResponse { + config := dto.StorageConfigDTO{} + + if storage.Config.NFS != nil { + config.NFS = &dto.NFSConfigDTO{ + Server: storage.Config.NFS.Server, + Path: storage.Config.NFS.Path, + } + } + if storage.Config.PV != nil { + config.PV = &dto.PVConfigDTO{ + StorageClassName: storage.Config.PV.StorageClassName, + Capacity: storage.Config.PV.Capacity, + AccessModes: storage.Config.PV.AccessModes, + } + } + if storage.Config.HostPath != nil { + config.HostPath = &dto.HostPathConfigDTO{ + Path: storage.Config.HostPath.Path, + } + } + + return &dto.StorageResponse{ + ID: storage.ID, + WorkspaceID: storage.WorkspaceID, + OwnerID: storage.OwnerID, + Name: storage.Name, + Type: string(storage.Type), + Config: config, + Description: storage.Description, + IsDefault: storage.IsDefault, + IsShared: storage.IsShared, + CreatedAt: storage.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), + UpdatedAt: storage.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), + } +} \ No newline at end of file diff --git a/backend/internal/adapter/input/http/rest/user_handler.go b/backend/internal/adapter/input/http/rest/user_handler.go new file mode 100644 index 0000000..eb202e6 --- /dev/null +++ b/backend/internal/adapter/input/http/rest/user_handler.go @@ -0,0 +1,152 @@ +package rest + +import ( + "encoding/json" + "net/http" + + "github.com/ocdp/cluster-service/internal/adapter/input/http/dto" + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/service" +) + +// UserHandler 用户 HTTP 处理程序 +type UserHandler struct { + authService *service.AuthService + workspaceService *service.WorkspaceService +} + +// NewUserHandler 创建用户处理程序 +func NewUserHandler(authService *service.AuthService, workspaceService *service.WorkspaceService) *UserHandler { + return &UserHandler{ + authService: authService, + workspaceService: workspaceService, + } +} + +// GetCurrentUser 获取当前用户信息 +// @Summary 获取当前用户信息 +// @Description 获取当前登录用户的基本信息 +// @Tags user +// @Accept json +// @Produce json +// @Success 200 {object} dto.UserResponseWithDTO +// @Router /users/me [get] +func (h *UserHandler) GetCurrentUser(w http.ResponseWriter, r *http.Request) { + userID := GetUserIDFromRequest(r) + if userID == "" { + respondError(w, http.StatusUnauthorized, "Not authenticated", "") + return + } + + user, err := h.authService.GetUserByID(r.Context(), userID) + if err != nil { + respondError(w, http.StatusNotFound, "User not found", "") + return + } + + // 获取 workspace 名称 + workspaceName := "" + if user.WorkspaceID != "" { + ws, _ := h.workspaceService.GetByID(r.Context(), user.WorkspaceID) + if ws != nil { + workspaceName = ws.Name + } + } + + respondSuccess(w, "", dto.UserResponseWithDTO{User: dto.UserDTOFromEntity(user, workspaceName)}) +} + +// ChangePassword 修改当前用户密码 +// @Summary 修改当前用户密码 +// @Description 修改当前登录用户的密码 +// @Tags user +// @Accept json +// @Produce json +// @Param request body dto.ChangePasswordRequest true "修改密码请求" +// @Success 200 +// @Router /users/me/password [put] +func (h *UserHandler) ChangePassword(w http.ResponseWriter, r *http.Request) { + userID := GetUserIDFromRequest(r) + if userID == "" { + respondError(w, http.StatusUnauthorized, "Not authenticated", "") + return + } + + var req dto.ChangePasswordRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", "") + return + } + + err := h.authService.ChangePassword(r.Context(), userID, req.OldPassword, req.NewPassword) + if err != nil { + respondError(w, http.StatusBadRequest, err.Error(), "") + return + } + + respondSuccess(w, "Password changed successfully", map[string]string{"message": "Password changed successfully"}) +} + +// GetCurrentUserWorkspace 获取当前用户所属的 Workspace +// @Summary 获取当前用户所属工作空间 +// @Description 获取当前用户所属工作空间的详细信息和配额 +// @Tags user +// @Accept json +// @Produce json +// @Success 200 {object} dto.WorkspaceResponse +// @Router /users/me/workspace [get] +func (h *UserHandler) GetCurrentUserWorkspace(w http.ResponseWriter, r *http.Request) { + userID := GetUserIDFromRequest(r) + if userID == "" { + respondError(w, http.StatusUnauthorized, "Not authenticated", "") + return + } + + user, err := h.authService.GetUserByID(r.Context(), userID) + if err != nil { + respondError(w, http.StatusNotFound, "User not found", "") + return + } + + // Admin 没有 workspace + if user.Role == entity.RoleAdmin { + respondSuccess(w, "", nil) + return + } + + if user.WorkspaceID == "" { + respondSuccess(w, "", nil) + return + } + + workspace, err := h.workspaceService.GetByID(r.Context(), user.WorkspaceID) + if err != nil { + respondError(w, http.StatusNotFound, "Workspace not found", "") + return + } + + // 获取配额 + quotas, _ := h.workspaceService.GetQuotas(r.Context(), workspace.ID) + + response := dto.WorkspaceResponse{ + Workspace: dto.WorkspaceDTOFromEntity(workspace), + Quotas: dto.QuotaDTOsFromEntities(quotas), + } + + respondSuccess(w, "", response) +} + +// GetUserIDFromRequest 从请求中获取用户 ID +func GetUserIDFromRequest(r *http.Request) string { + // 尝试从 Header 获取(由中间件设置) + userID := r.Header.Get("X-User-ID") + if userID != "" { + return userID + } + + // 尝试从 Context 获取(安全类型断言) + if uid, ok := r.Context().Value("user_id").(string); ok { + return uid + } + return "" +} \ No newline at end of file diff --git a/backend/internal/adapter/input/http/rest/user_management_handler.go b/backend/internal/adapter/input/http/rest/user_management_handler.go new file mode 100644 index 0000000..1d1a404 --- /dev/null +++ b/backend/internal/adapter/input/http/rest/user_management_handler.go @@ -0,0 +1,332 @@ +package rest + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" + "github.com/ocdp/cluster-service/internal/adapter/input/http/dto" + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/service" +) + +// UserManagementHandler 用户管理 HTTP 处理程序 +type UserManagementHandler struct { + userManagementService *service.UserManagementService + authService *service.AuthService + workspaceService *service.WorkspaceService +} + +// NewUserManagementHandler 创建用户管理处理程序 +func NewUserManagementHandler( + userManagementService *service.UserManagementService, + authService *service.AuthService, + workspaceService *service.WorkspaceService, +) *UserManagementHandler { + return &UserManagementHandler{ + userManagementService: userManagementService, + authService: authService, + workspaceService: workspaceService, + } +} + +// CreateUser 创建用户(Admin 操作) +// @Summary 创建用户 +// @Description 创建新用户(Admin 专用) +// @Tags admin +// @Accept json +// @Produce json +// @Param request body dto.CreateUserRequest true "创建用户请求" +// @Success 200 {object} dto.UserResponseWithDTO +// @Router /admin/users [post] +func (h *UserManagementHandler) CreateUser(w http.ResponseWriter, r *http.Request) { + // 检查权限(Admin) + if !h.requireAdmin(w, r) { + return + } + + var req dto.CreateUserRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", "") + return + } + + user, err := h.userManagementService.CreateUser(r.Context(), req.Username, req.Password, req.Email, req.Role, req.WorkspaceID) + if err != nil { + respondError(w, http.StatusBadRequest, err.Error(), "") + return + } + + // 获取 workspace 名称 + workspaceName := "" + if user.WorkspaceID != "" { + ws, _ := h.workspaceService.GetByID(r.Context(), user.WorkspaceID) + if ws != nil { + workspaceName = ws.Name + } + } + + respondSuccess(w, "", dto.UserResponseWithDTO{User: dto.UserDTOFromEntity(user, workspaceName)}) +} + +// GetUser 获取用户 +// @Summary 获取用户 +// @Description 获取指定用户信息(Admin 专用) +// @Tags admin +// @Accept json +// @Produce json +// @Param user_id path string true "用户 ID" +// @Success 200 {object} dto.UserResponseWithDTO +// @Router /admin/users/{user_id} [get] +func (h *UserManagementHandler) GetUser(w http.ResponseWriter, r *http.Request) { + // 检查权限(Admin) + if !h.requireAdmin(w, r) { + return + } + + vars := mux.Vars(r) + userID := vars["user_id"] + + user, err := h.userManagementService.GetUser(r.Context(), userID) + if err != nil { + respondError(w, http.StatusNotFound, "User not found", "") + return + } + + // 获取 workspace 名称 + workspaceName := "" + if user.WorkspaceID != "" { + ws, _ := h.workspaceService.GetByID(r.Context(), user.WorkspaceID) + if ws != nil { + workspaceName = ws.Name + } + } + + respondSuccess(w, "", dto.UserResponseWithDTO{User: dto.UserDTOFromEntity(user, workspaceName)}) +} + +// ListUsers 列出用户 +// @Summary 列出用户 +// @Description 获取所有用户列表(Admin 专用),可按 workspace_id 筛选 +// @Tags admin +// @Accept json +// @Produce json +// @Param workspace_id query string false "工作空间 ID" +// @Success 200 {object} dto.UserListResponse +// @Router /admin/users [get] +func (h *UserManagementHandler) ListUsers(w http.ResponseWriter, r *http.Request) { + // 检查权限(Admin) + if !h.requireAdmin(w, r) { + return + } + + workspaceID := r.URL.Query().Get("workspace_id") + + users, err := h.userManagementService.ListUsers(r.Context(), workspaceID) + if err != nil { + respondError(w, http.StatusInternalServerError, err.Error(), "") + return + } + + // 获取所有 workspace 名称 + workspaceNames := make(map[string]string) + workspaces, _ := h.workspaceService.List(r.Context()) + for _, ws := range workspaces { + workspaceNames[ws.ID] = ws.Name + } + + respondSuccess(w, "", dto.UserListResponse{ + Users: dto.UserDTOsFromEntities(users, workspaceNames), + Total: len(users), + }) +} + +// UpdateUser 更新用户 +// @Summary 更新用户 +// @Description 更新用户信息(Admin 专用) +// @Tags admin +// @Accept json +// @Produce json +// @Param user_id path string true "用户 ID" +// @Param request body dto.UpdateUserRequest true "更新用户请求" +// @Success 200 {object} dto.UserResponseWithDTO +// @Router /admin/users/{user_id} [put] +func (h *UserManagementHandler) UpdateUser(w http.ResponseWriter, r *http.Request) { + // 检查权限(Admin) + if !h.requireAdmin(w, r) { + return + } + + vars := mux.Vars(r) + userID := vars["user_id"] + + user, err := h.userManagementService.GetUser(r.Context(), userID) + if err != nil { + respondError(w, http.StatusNotFound, "User not found", "") + return + } + + var req dto.UpdateUserRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", "") + return + } + + if req.Email != "" { + user.Email = req.Email + } + if req.IsActive != nil { + user.IsActive = *req.IsActive + } + + if err := h.userManagementService.UpdateUser(r.Context(), user); err != nil { + respondError(w, http.StatusBadRequest, err.Error(), "") + return + } + + // 获取 workspace 名称 + workspaceName := "" + if user.WorkspaceID != "" { + ws, _ := h.workspaceService.GetByID(r.Context(), user.WorkspaceID) + if ws != nil { + workspaceName = ws.Name + } + } + + respondSuccess(w, "", dto.UserResponseWithDTO{User: dto.UserDTOFromEntity(user, workspaceName)}) +} + +// SetUserActive 启用/禁用用户 +// @Summary 启用/禁用用户 +// @Description 设置用户是否启用(Admin 专用) +// @Tags admin +// @Accept json +// @Produce json +// @Param user_id path string true "用户 ID" +// @Param request body dto.SetUserActiveRequest true "启用状态" +// @Success 200 +// @Router /admin/users/{user_id}/active [put] +func (h *UserManagementHandler) SetUserActive(w http.ResponseWriter, r *http.Request) { + // 检查权限(Admin) + if !h.requireAdmin(w, r) { + return + } + + vars := mux.Vars(r) + userID := vars["user_id"] + + var req dto.SetUserActiveRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", "") + return + } + + if err := h.userManagementService.SetUserActive(r.Context(), userID, req.IsActive); err != nil { + respondError(w, http.StatusBadRequest, err.Error(), "") + return + } + + respondSuccess(w, "", nil) +} + +// ChangeUserWorkspace 分配用户到 Workspace +// @Summary 分配用户到工作空间 +// @Description 将用户分配到指定工作空间(Admin 专用) +// @Tags admin +// @Accept json +// @Produce json +// @Param user_id path string true "用户 ID" +// @Param request body dto.ChangeUserWorkspaceRequest true "工作空间分配请求" +// @Success 200 +// @Router /admin/users/{user_id}/workspace [put] +func (h *UserManagementHandler) ChangeUserWorkspace(w http.ResponseWriter, r *http.Request) { + // 检查权限(Admin) + if !h.requireAdmin(w, r) { + return + } + + vars := mux.Vars(r) + userID := vars["user_id"] + + var req dto.ChangeUserWorkspaceRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", "") + return + } + + if err := h.userManagementService.ChangeUserWorkspace(r.Context(), userID, req.WorkspaceID); err != nil { + respondError(w, http.StatusBadRequest, err.Error(), "") + return + } + + respondSuccess(w, "", nil) +} + +// ResetPassword 重置用户密码(Admin 操作) +// @Summary 重置用户密码 +// @Description 重置指定用户的密码(Admin 专用) +// @Tags admin +// @Accept json +// @Produce json +// @Param user_id path string true "用户 ID" +// @Param request body dto.ResetPasswordRequest true "重置密码请求" +// @Success 200 +// @Router /admin/users/{user_id}/password [put] +func (h *UserManagementHandler) ResetPassword(w http.ResponseWriter, r *http.Request) { + // 检查权限(Admin) + if !h.requireAdmin(w, r) { + return + } + + vars := mux.Vars(r) + userID := vars["user_id"] + + var req dto.ResetPasswordRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", "") + return + } + + if err := h.userManagementService.ResetPassword(r.Context(), userID, req.NewPassword); err != nil { + respondError(w, http.StatusBadRequest, err.Error(), "") + return + } + + respondSuccess(w, "", nil) +} + +// DeleteUser 删除用户 +// @Summary 删除用户 +// @Description 删除指定用户(Admin 专用) +// @Tags admin +// @Accept json +// @Produce json +// @Param user_id path string true "用户 ID" +// @Success 200 +// @Router /admin/users/{user_id} [delete] +func (h *UserManagementHandler) DeleteUser(w http.ResponseWriter, r *http.Request) { + // 检查权限(Admin) + if !h.requireAdmin(w, r) { + return + } + + vars := mux.Vars(r) + userID := vars["user_id"] + + if err := h.userManagementService.DeleteUser(r.Context(), userID); err != nil { + respondError(w, http.StatusBadRequest, err.Error(), "") + return + } + + respondSuccess(w, "", nil) +} + +// requireAdmin 检查是否为 Admin +func (h *UserManagementHandler) requireAdmin(w http.ResponseWriter, r *http.Request) bool { + userRole := r.Header.Get("X-User-Role") + if userRole != string(entity.RoleAdmin) { + respondError(w, http.StatusForbidden, "Admin access required", "") + return false + } + return true +} \ No newline at end of file diff --git a/backend/internal/adapter/input/http/rest/values_template_handler.go b/backend/internal/adapter/input/http/rest/values_template_handler.go new file mode 100644 index 0000000..df941be --- /dev/null +++ b/backend/internal/adapter/input/http/rest/values_template_handler.go @@ -0,0 +1,294 @@ +package rest + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" + "github.com/ocdp/cluster-service/internal/adapter/input/http/dto" + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/service" +) + +// ValuesTemplateHandler Values Template Handler +type ValuesTemplateHandler struct { + valuesTemplateService *service.ValuesTemplateService +} + +// NewValuesTemplateHandler 创建 Values Template Handler +func NewValuesTemplateHandler(valuesTemplateService *service.ValuesTemplateService) *ValuesTemplateHandler { + return &ValuesTemplateHandler{ + valuesTemplateService: valuesTemplateService, + } +} + +// CreateValuesTemplate 创建 Values 模板 +// @Summary 创建 Values 模板 +// @Description 新增 Values 模板配置(带版本管理) +// @Tags Values Templates +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body dto.CreateValuesTemplateRequest true "Values 模板信息" +// @Success 201 {object} dto.ValuesTemplateResponse +// @Failure 400 {object} dto.ErrorResponse +// @Router /values-templates [post] +func (h *ValuesTemplateHandler) CreateValuesTemplate(w http.ResponseWriter, r *http.Request) { + var req dto.CreateValuesTemplateRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", err.Error()) + return + } + + // 获取用户信息 + workspaceID := r.Header.Get("X-Workspace-ID") + ownerID := r.Header.Get("X-User-ID") + + template, err := h.valuesTemplateService.Create( + r.Context(), + workspaceID, + ownerID, + req.ChartReferenceID, + req.Name, + req.Description, + req.ValuesYAML, + req.IsDefault, + ) + if err != nil { + respondError(w, http.StatusBadRequest, "Failed to create values template", err.Error()) + return + } + + response := toValuesTemplateResponse(template) + respondJSON(w, http.StatusCreated, response) +} + +// GetValuesTemplate 获取 Values 模板详情 +// @Summary 获取 Values 模板 +// @Tags Values Templates +// @Produce json +// @Security BearerAuth +// @Param template_id path string true "Template ID" +// @Success 200 {object} dto.ValuesTemplateResponse +// @Failure 404 {object} dto.ErrorResponse +// @Router /values-templates/{template_id} [get] +func (h *ValuesTemplateHandler) GetValuesTemplate(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + templateID := vars["template_id"] + + template, err := h.valuesTemplateService.GetByID(r.Context(), templateID) + if err != nil { + respondError(w, http.StatusNotFound, "Values template not found", err.Error()) + return + } + + response := toValuesTemplateResponse(template) + respondJSON(w, http.StatusOK, response) +} + +// GetAllValuesTemplates 获取所有 Values 模板 +// @Summary 列出所有 Values 模板 +// @Tags Values Templates +// @Produce json +// @Security BearerAuth +// @Success 200 {array} dto.ValuesTemplateResponse +// @Failure 500 {object} dto.ErrorResponse +// @Router /values-templates [get] +func (h *ValuesTemplateHandler) GetAllValuesTemplates(w http.ResponseWriter, r *http.Request) { + workspaceID := r.Header.Get("X-Workspace-ID") + role := r.Header.Get("X-User-Role") + + var templates []*dto.ValuesTemplateResponse + + // Admin 可以看到所有,其他用户只看自己 workspace + if role == "admin" { + allTemplates, err := h.valuesTemplateService.List(r.Context()) + if err != nil { + respondError(w, http.StatusInternalServerError, "Failed to list values templates", err.Error()) + return + } + for _, t := range allTemplates { + templates = append(templates, toValuesTemplateResponse(t)) + } + } else if workspaceID != "" { + workspaceTemplates, err := h.valuesTemplateService.GetByWorkspace(r.Context(), workspaceID) + if err != nil { + respondError(w, http.StatusInternalServerError, "Failed to list values templates", err.Error()) + return + } + for _, t := range workspaceTemplates { + templates = append(templates, toValuesTemplateResponse(t)) + } + } + + respondJSON(w, http.StatusOK, templates) +} + +// GetValuesTemplatesByChartReference 获取 Chart Reference 的所有 Values 模板 +// @Summary 获取 Chart Reference 的 Values 模板 +// @Tags Values Templates +// @Produce json +// @Security BearerAuth +// @Param chart_reference_id path string true "Chart Reference ID" +// @Success 200 {array} dto.ValuesTemplateResponse +// @Failure 500 {object} dto.ErrorResponse +// @Router /chart-references/{chart_reference_id}/values-templates [get] +func (h *ValuesTemplateHandler) GetValuesTemplatesByChartReference(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + chartRefID := vars["chart_reference_id"] + + templates, err := h.valuesTemplateService.GetByChartReference(r.Context(), chartRefID) + if err != nil { + respondError(w, http.StatusInternalServerError, "Failed to list values templates", err.Error()) + return + } + + responses := make([]*dto.ValuesTemplateResponse, 0, len(templates)) + for _, t := range templates { + responses = append(responses, toValuesTemplateResponse(t)) + } + + respondJSON(w, http.StatusOK, responses) +} + +// GetValuesTemplateHistory 获取模板的版本历史 +// @Summary 获取 Values 模板版本历史 +// @Tags Values Templates +// @Produce json +// @Security BearerAuth +// @Param chart_reference_id path string true "Chart Reference ID" +// @Param name query string true "Template Name" +// @Success 200 {array} dto.ValuesTemplateResponse +// @Failure 500 {object} dto.ErrorResponse +// @Router /chart-references/{chart_reference_id}/values-templates/history [get] +func (h *ValuesTemplateHandler) GetValuesTemplateHistory(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + chartRefID := vars["chart_reference_id"] + name := r.URL.Query().Get("name") + + if name == "" { + respondError(w, http.StatusBadRequest, "Template name is required", "") + return + } + + templates, err := h.valuesTemplateService.GetHistory(r.Context(), chartRefID, name) + if err != nil { + respondError(w, http.StatusInternalServerError, "Failed to get values template history", err.Error()) + return + } + + responses := make([]*dto.ValuesTemplateResponse, 0, len(templates)) + for _, t := range templates { + responses = append(responses, toValuesTemplateResponse(t)) + } + + respondJSON(w, http.StatusOK, responses) +} + +// UpdateValuesTemplate 更新 Values 模板 +// @Summary 更新 Values 模板 +// @Tags Values Templates +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param template_id path string true "Template ID" +// @Param request body dto.UpdateValuesTemplateRequest true "更新内容" +// @Success 200 {object} dto.ValuesTemplateResponse +// @Failure 404 {object} dto.ErrorResponse +// @Router /values-templates/{template_id} [put] +func (h *ValuesTemplateHandler) UpdateValuesTemplate(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + templateID := vars["template_id"] + + var req dto.UpdateValuesTemplateRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", err.Error()) + return + } + + isDefault := false + if req.IsDefault != nil { + isDefault = *req.IsDefault + } + + template, err := h.valuesTemplateService.Update( + r.Context(), + templateID, + req.Description, + req.ValuesYAML, + isDefault, + ) + if err != nil { + respondError(w, http.StatusBadRequest, "Failed to update values template", err.Error()) + return + } + + response := toValuesTemplateResponse(template) + respondJSON(w, http.StatusOK, response) +} + +// DeleteValuesTemplate 删除 Values 模板 +// @Summary 删除 Values 模板 +// @Tags Values Templates +// @Produce json +// @Security BearerAuth +// @Param template_id path string true "Template ID" +// @Success 204 {string} string "No Content" +// @Failure 404 {object} dto.ErrorResponse +// @Router /values-templates/{template_id} [delete] +func (h *ValuesTemplateHandler) DeleteValuesTemplate(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + templateID := vars["template_id"] + + if err := h.valuesTemplateService.Delete(r.Context(), templateID); err != nil { + respondError(w, http.StatusNotFound, "Failed to delete values template", err.Error()) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// RollbackValuesTemplate 回滚到指定版本 +// @Summary 回滚 Values 模板 +// @Tags Values Templates +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param chart_reference_id path string true "Chart Reference ID" +// @Param request body dto.RollbackValuesTemplateRequest true "回滚信息" +// @Success 200 {object} dto.ValuesTemplateResponse +// @Failure 404 {object} dto.ErrorResponse +// @Router /chart-references/{chart_reference_id}/values-templates/rollback [post] +func (h *ValuesTemplateHandler) RollbackValuesTemplate(w http.ResponseWriter, r *http.Request) { + var req dto.RollbackValuesTemplateRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", err.Error()) + return + } + + template, err := h.valuesTemplateService.Rollback(r.Context(), req.TemplateID) + if err != nil { + respondError(w, http.StatusBadRequest, "Failed to rollback values template", err.Error()) + return + } + + response := toValuesTemplateResponse(template) + respondJSON(w, http.StatusOK, response) +} + +// toValuesTemplateResponse 转换为响应 DTO +func toValuesTemplateResponse(template *entity.ValuesTemplate) *dto.ValuesTemplateResponse { + return &dto.ValuesTemplateResponse{ + ID: template.ID, + WorkspaceID: template.WorkspaceID, + OwnerID: template.OwnerID, + ChartReferenceID: template.ChartReferenceID, + Name: template.Name, + Description: template.Description, + ValuesYAML: template.ValuesYAML, + Version: template.Version, + IsDefault: template.IsDefault, + CreatedAt: template.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), + UpdatedAt: template.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), + } +} \ No newline at end of file diff --git a/backend/internal/adapter/input/http/rest/workspace_handler.go b/backend/internal/adapter/input/http/rest/workspace_handler.go new file mode 100644 index 0000000..66e0cc4 --- /dev/null +++ b/backend/internal/adapter/input/http/rest/workspace_handler.go @@ -0,0 +1,306 @@ +package rest + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" + "github.com/ocdp/cluster-service/internal/adapter/input/http/dto" + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/service" +) + +// WorkspaceHandler 工作空间 HTTP 处理程序 +type WorkspaceHandler struct { + workspaceService *service.WorkspaceService + authService *service.AuthService +} + +// NewWorkspaceHandler 创建工作空间处理程序 +func NewWorkspaceHandler(workspaceService *service.WorkspaceService, authService *service.AuthService) *WorkspaceHandler { + return &WorkspaceHandler{ + workspaceService: workspaceService, + authService: authService, + } +} + +// CreateWorkspace 创建工作空间 +// @Summary 创建工作空间 +// @Description 创建新的工作空间(Admin 专用) +// @Tags workspace +// @Accept json +// @Produce json +// @Param request body dto.CreateWorkspaceRequest true "创建工作空间请求" +// @Success 200 {object} dto.WorkspaceDTO +// @Router /workspaces [post] +func (h *WorkspaceHandler) CreateWorkspace(w http.ResponseWriter, r *http.Request) { + // 检查权限(Admin) + if !h.requireAdmin(w, r) { + return + } + + var req dto.CreateWorkspaceRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", "") + return + } + + // 获取创建者 ID + userID := GetUserIDFromRequest(r) + + workspace, err := h.workspaceService.Create(r.Context(), req.Name, req.Description, userID) + if err != nil { + respondError(w, http.StatusBadRequest, err.Error(), "") + return + } + + respondSuccess(w, "", dto.WorkspaceDTOFromEntity(workspace)) +} + +// GetWorkspace 获取工作空间 +// @Summary 获取工作空间 +// @Description 获取指定工作空间的详细信息和配额 +// @Tags workspace +// @Accept json +// @Produce json +// @Param workspace_id path string true "工作空间 ID" +// @Success 200 {object} dto.WorkspaceResponse +// @Router /workspaces/{workspace_id} [get] +func (h *WorkspaceHandler) GetWorkspace(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + workspaceID := vars["workspace_id"] + + workspace, err := h.workspaceService.GetByID(r.Context(), workspaceID) + if err != nil { + respondError(w, http.StatusNotFound, "Workspace not found", "") + return + } + + // 检查访问权限 + if !h.canAccessWorkspace(w, r, workspace.ID) { + return + } + + // 获取配额 + quotas, _ := h.workspaceService.GetQuotas(r.Context(), workspace.ID) + + response := dto.WorkspaceResponse{ + Workspace: dto.WorkspaceDTOFromEntity(workspace), + Quotas: dto.QuotaDTOsFromEntities(quotas), + } + + respondSuccess(w, "", response) +} + +// UpdateWorkspace 更新工作空间 +// @Summary 更新工作空间 +// @Description 更新工作空间信息(Admin 专用) +// @Tags workspace +// @Accept json +// @Produce json +// @Param workspace_id path string true "工作空间 ID" +// @Param request body dto.UpdateWorkspaceRequest true "更新工作空间请求" +// @Success 200 {object} dto.WorkspaceDTO +// @Router /workspaces/{workspace_id} [put] +func (h *WorkspaceHandler) UpdateWorkspace(w http.ResponseWriter, r *http.Request) { + // 检查权限(Admin) + if !h.requireAdmin(w, r) { + return + } + + vars := mux.Vars(r) + workspaceID := vars["workspace_id"] + + workspace, err := h.workspaceService.GetByID(r.Context(), workspaceID) + if err != nil { + respondError(w, http.StatusNotFound, "Workspace not found", "") + return + } + + var req dto.UpdateWorkspaceRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", "") + return + } + + if req.Name != "" { + workspace.Name = req.Name + } + if req.Description != "" { + workspace.Description = req.Description + } + + if err := h.workspaceService.Update(r.Context(), workspace); err != nil { + respondError(w, http.StatusBadRequest, err.Error(), "") + return + } + + respondSuccess(w, "", dto.WorkspaceDTOFromEntity(workspace)) +} + +// DeleteWorkspace 删除工作空间 +// @Summary 删除工作空间 +// @Description 删除指定工作空间(Admin 专用) +// @Tags workspace +// @Accept json +// @Produce json +// @Param workspace_id path string true "工作空间 ID" +// @Success 200 +// @Router /workspaces/{workspace_id} [delete] +func (h *WorkspaceHandler) DeleteWorkspace(w http.ResponseWriter, r *http.Request) { + // 检查权限(Admin) + if !h.requireAdmin(w, r) { + return + } + + vars := mux.Vars(r) + workspaceID := vars["workspace_id"] + + if err := h.workspaceService.Delete(r.Context(), workspaceID); err != nil { + respondError(w, http.StatusBadRequest, err.Error(), "") + return + } + + respondSuccess(w, "", nil) +} + +// ListWorkspaces 列出所有工作空间 +// @Summary 列出所有工作空间 +// @Description 获取所有工作空间列表(Admin 专用) +// @Tags workspace +// @Accept json +// @Produce json +// @Success 200 {object} dto.WorkspaceListResponse +// @Router /workspaces [get] +func (h *WorkspaceHandler) ListWorkspaces(w http.ResponseWriter, r *http.Request) { + // 检查权限(Admin) + if !h.requireAdmin(w, r) { + return + } + + workspaces, err := h.workspaceService.List(r.Context()) + if err != nil { + respondError(w, http.StatusInternalServerError, err.Error(), "") + return + } + + respondSuccess(w, "", dto.WorkspaceListResponse{ + Workspaces: dto.WorkspaceDTOsFromEntities(workspaces), + Total: len(workspaces), + }) +} + +// GetWorkspaceQuotas 获取工作空间配额 +// @Summary 获取工作空间配额 +// @Description 获取指定工作空间的资源配额 +// @Tags workspace +// @Accept json +// @Produce json +// @Param workspace_id path string true "工作空间 ID" +// @Success 200 {array} dto.QuotaDTO +// @Router /workspaces/{workspace_id}/quotas [get] +func (h *WorkspaceHandler) GetWorkspaceQuotas(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + workspaceID := vars["workspace_id"] + + // 检查访问权限 + if !h.canAccessWorkspace(w, r, workspaceID) { + return + } + + quotas, err := h.workspaceService.GetQuotas(r.Context(), workspaceID) + if err != nil { + respondError(w, http.StatusInternalServerError, err.Error(), "") + return + } + + respondSuccess(w, "", dto.QuotaDTOsFromEntities(quotas)) +} + +// SetWorkspaceQuotas 设置工作空间配额 +// @Summary 设置工作空间配额 +// @Description 设置指定工作空间的 CPU/GPU/GPU Memory 配额(Admin 专用) +// @Tags workspace +// @Accept json +// @Produce json +// @Param workspace_id path string true "工作空间 ID" +// @Param request body dto.SetQuotasRequest true "配额设置请求" +// @Success 200 {array} dto.QuotaDTO +// @Router /workspaces/{workspace_id}/quotas [put] +func (h *WorkspaceHandler) SetWorkspaceQuotas(w http.ResponseWriter, r *http.Request) { + // 检查权限(Admin) + if !h.requireAdmin(w, r) { + return + } + + vars := mux.Vars(r) + workspaceID := vars["workspace_id"] + + var req dto.SetQuotasRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", "") + return + } + + quotas := make(map[entity.ResourceType]struct { + HardLimit float64 + SoftLimit float64 + }) + + if req.CPU != nil { + quotas[entity.ResourceCPU] = struct { + HardLimit float64 + SoftLimit float64 + }{req.CPU.HardLimit, req.CPU.SoftLimit} + } + if req.GPU != nil { + quotas[entity.ResourceGPU] = struct { + HardLimit float64 + SoftLimit float64 + }{req.GPU.HardLimit, req.GPU.SoftLimit} + } + if req.GPUMemory != nil { + quotas[entity.ResourceGPUMemory] = struct { + HardLimit float64 + SoftLimit float64 + }{req.GPUMemory.HardLimit, req.GPUMemory.SoftLimit} + } + + if err := h.workspaceService.SetQuotas(r.Context(), workspaceID, quotas); err != nil { + respondError(w, http.StatusBadRequest, err.Error(), "") + return + } + + // 返回更新后的配额 + updatedQuotas, _ := h.workspaceService.GetQuotas(r.Context(), workspaceID) + respondSuccess(w, "", dto.QuotaDTOsFromEntities(updatedQuotas)) +} + +// requireAdmin 检查是否为 Admin +func (h *WorkspaceHandler) requireAdmin(w http.ResponseWriter, r *http.Request) bool { + userRole := r.Header.Get("X-User-Role") + if userRole != string(entity.RoleAdmin) { + respondError(w, http.StatusForbidden, "Admin access required", "") + return false + } + return true +} + +// canAccessWorkspace 检查是否可以访问工作空间 +func (h *WorkspaceHandler) canAccessWorkspace(w http.ResponseWriter, r *http.Request, workspaceID string) bool { + userRole := r.Header.Get("X-User-Role") + userWorkspaceID := r.Header.Get("X-Workspace-ID") + + // Admin 可以访问所有 + if userRole == string(entity.RoleAdmin) { + return true + } + + // 普通用户只能访问自己的 workspace + if userWorkspaceID != workspaceID { + respondError(w, http.StatusForbidden, "Access denied", "") + return false + } + + return true +} \ No newline at end of file diff --git a/backend/internal/adapter/output/factory.go b/backend/internal/adapter/output/factory.go index 8565112..1bb32d8 100644 --- a/backend/internal/adapter/output/factory.go +++ b/backend/internal/adapter/output/factory.go @@ -127,6 +127,69 @@ func (f *AdapterFactory) CreateEntryClient() repository.InstanceEntryClient { return k8s.NewEntryClient() } +// CreateWorkspaceRepository 创建 Workspace 仓储 +func (f *AdapterFactory) CreateWorkspaceRepository() (repository.WorkspaceRepository, error) { + if f.mode == ModeMock { + return nil, fmt.Errorf("workspace repository mock not implemented") + } + + // 默认:真实实现(PostgreSQL) + if err := f.ensureDBConnection(); err != nil { + return nil, err + } + return postgres.NewWorkspaceRepository(f.db), nil +} + +// CreateQuotaRepository 创建 Quota 仓储 +// CreateStorageRepository 创建存储后端仓储 +func (f *AdapterFactory) CreateStorageRepository() (repository.StorageRepository, error) { + if f.mode == ModeMock { + return mock.NewStorageRepositoryMock(), nil + } + + if err := f.ensureDBConnection(); err != nil { + return nil, err + } + return postgres.NewStorageRepository(f.db), nil +} + +// CreateQuotaRepository 创建配额仓储 +func (f *AdapterFactory) CreateQuotaRepository() (repository.QuotaRepository, error) { + if f.mode == ModeMock { + return nil, fmt.Errorf("quota repository mock not implemented") + } + + // 默认:真实实现(PostgreSQL) + if err := f.ensureDBConnection(); err != nil { + return nil, err + } + return postgres.NewQuotaRepository(f.db), nil +} + +// CreateChartReferenceRepository 创建 Chart 引用仓储 +func (f *AdapterFactory) CreateChartReferenceRepository() (repository.ChartReferenceRepository, error) { + if f.mode == ModeMock { + return nil, fmt.Errorf("chart reference repository mock not implemented") + } + + if err := f.ensureDBConnection(); err != nil { + return nil, err + } + return postgres.NewChartReferenceRepository(f.db), nil +} + +// CreateValuesTemplateRepository 创建 Values 模板仓储 +func (f *AdapterFactory) CreateValuesTemplateRepository() (repository.ValuesTemplateRepository, error) { + if f.mode == ModeMock { + return nil, fmt.Errorf("values template repository mock not implemented") + } + + if err := f.ensureDBConnection(); err != nil { + return nil, err + } + return postgres.NewValuesTemplateRepository(f.db), nil +} + // CreateAllRepositories 一次性创建所有 Repositories func (f *AdapterFactory) CreateAllRepositories() (*Repositories, error) { userRepo, err := f.CreateUserRepository() @@ -149,6 +212,21 @@ func (f *AdapterFactory) CreateAllRepositories() (*Repositories, error) { return nil, fmt.Errorf("failed to create instance repository: %w", err) } + workspaceRepo, err := f.CreateWorkspaceRepository() + if err != nil { + return nil, fmt.Errorf("failed to create workspace repository: %w", err) + } + + storageRepo, err := f.CreateStorageRepository() + if err != nil { + return nil, fmt.Errorf("failed to create storage repository: %w", err) + } + + quotaRepo, err := f.CreateQuotaRepository() + if err != nil { + return nil, fmt.Errorf("failed to create quota repository: %w", err) + } + ociClient, err := f.CreateOCIClient() if err != nil { return nil, fmt.Errorf("failed to create OCI client: %w", err) @@ -163,28 +241,48 @@ func (f *AdapterFactory) CreateAllRepositories() (*Repositories, error) { metricsClient := f.CreateMetricsClient(clusterRepo) entryClient := f.CreateEntryClient() + chartRefRepo, err := f.CreateChartReferenceRepository() + if err != nil { + return nil, fmt.Errorf("failed to create chart reference repository: %w", err) + } + + valuesTemplateRepo, err := f.CreateValuesTemplateRepository() + if err != nil { + return nil, fmt.Errorf("failed to create values template repository: %w", err) + } + return &Repositories{ - UserRepo: userRepo, - ClusterRepo: clusterRepo, - RegistryRepo: registryRepo, - InstanceRepo: instanceRepo, - OCIClient: ociClient, - HelmClient: helmClient, - MetricsClient: metricsClient, - EntryClient: entryClient, + UserRepo: userRepo, + ClusterRepo: clusterRepo, + RegistryRepo: registryRepo, + InstanceRepo: instanceRepo, + WorkspaceRepo: workspaceRepo, + StorageRepo: storageRepo, + ChartRefRepo: chartRefRepo, + ValuesTemplateRepo: valuesTemplateRepo, + QuotaRepo: quotaRepo, + OCIClient: ociClient, + HelmClient: helmClient, + MetricsClient: metricsClient, + EntryClient: entryClient, }, nil } // Repositories 所有仓储的集合 type Repositories struct { - UserRepo repository.UserRepository - ClusterRepo repository.ClusterRepository - RegistryRepo repository.RegistryRepository - InstanceRepo repository.InstanceRepository - OCIClient repository.OCIClient - HelmClient repository.HelmClient - MetricsClient repository.MetricsClient - EntryClient repository.InstanceEntryClient + UserRepo repository.UserRepository + ClusterRepo repository.ClusterRepository + RegistryRepo repository.RegistryRepository + InstanceRepo repository.InstanceRepository + WorkspaceRepo repository.WorkspaceRepository + StorageRepo repository.StorageRepository + ChartRefRepo repository.ChartReferenceRepository + ValuesTemplateRepo repository.ValuesTemplateRepository + QuotaRepo repository.QuotaRepository + OCIClient repository.OCIClient + HelmClient repository.HelmClient + MetricsClient repository.MetricsClient + EntryClient repository.InstanceEntryClient } // ensureDBConnection 确保数据库连接已建立 diff --git a/backend/internal/adapter/output/oci/mock/oci_client_mock.go b/backend/internal/adapter/output/oci/mock/oci_client_mock.go index a31baaa..fc4c558 100644 --- a/backend/internal/adapter/output/oci/mock/oci_client_mock.go +++ b/backend/internal/adapter/output/oci/mock/oci_client_mock.go @@ -262,12 +262,40 @@ func (c *OCIClientMock) GetValuesSchema(ctx context.Context, registry *entity.Re return mockSchema, nil } +func (c *OCIClientMock) GetValues(ctx context.Context, registry *entity.Registry, repository, reference string) (string, error) { + artifact, err := c.GetArtifact(ctx, registry, repository, reference) + if err != nil { + return "", err + } + + if !artifact.IsChart() { + return "", fmt.Errorf("not a helm chart") + } + + // 返回 Mock values.yaml + mockValues := `# Default values for the chart +replicaCount: 1 + +image: + repository: nginx + tag: latest + pullPolicy: IfNotPresent + +service: + type: ClusterIP + port: 80 + +resources: {} +` + return mockValues, nil +} + func (c *OCIClientMock) PullArtifact(ctx context.Context, registry *entity.Registry, repository, reference, destPath string) error { _, err := c.GetArtifact(ctx, registry, repository, reference) if err != nil { return err } - + // Mock 实现,不实际下载 return nil } diff --git a/backend/internal/adapter/output/oci/real/oci_client.go b/backend/internal/adapter/output/oci/real/oci_client.go index f9a9e76..924f173 100644 --- a/backend/internal/adapter/output/oci/real/oci_client.go +++ b/backend/internal/adapter/output/oci/real/oci_client.go @@ -43,13 +43,26 @@ func (c *OCIClient) getRegistry(reg *entity.Registry) (*remote.Registry, error) return nil, fmt.Errorf("failed to create registry client: %w", err) } - // 设置认证 - if reg.Username != "" && reg.Password != "" { + // 设置认证 - 优先使用 registry 自己的凭证,否则使用 .env 中的默认凭证 + username := reg.Username + password := reg.Password + + // 如果没有提供凭证,尝试从环境变量加载 + if (username == "" || password == "") && strings.Contains(reg.URL, "harbor") { + if envUser := os.Getenv("HARBOR_USERNAME"); envUser != "" { + username = envUser + } + if envPass := os.Getenv("HARBOR_PASSWORD"); envPass != "" { + password = envPass + } + } + + if username != "" && password != "" { registry.Client = &auth.Client{ Client: c.httpClient, Credential: auth.StaticCredential(registryURL, auth.Credential{ - Username: reg.Username, - Password: reg.Password, + Username: username, + Password: password, }), } } @@ -370,6 +383,105 @@ func (c *OCIClient) GetValuesSchema(ctx context.Context, registry *entity.Regist return "", entity.ErrValuesSchemaNotFound } +// GetValues 获取 Helm Chart 的 values.yaml +func (c *OCIClient) GetValues(ctx context.Context, registry *entity.Registry, repository, reference string) (string, error) { + reg, err := c.getRegistry(registry) + if err != nil { + return "", err + } + + repo, err := reg.Repository(ctx, repository) + if err != nil { + return "", fmt.Errorf("failed to get repository: %w", err) + } + + // 解析 reference (tag 或 digest) + desc, err := repo.Resolve(ctx, reference) + if err != nil { + return "", fmt.Errorf("failed to resolve artifact: %w", err) + } + + manifestReader, err := repo.Fetch(ctx, desc) + if err != nil { + return "", fmt.Errorf("failed to fetch manifest: %w", err) + } + defer manifestReader.Close() + + manifestBytes, err := io.ReadAll(manifestReader) + if err != nil { + return "", fmt.Errorf("failed to read manifest: %w", err) + } + + var manifest ocispec.Manifest + if err := json.Unmarshal(manifestBytes, &manifest); err != nil { + return "", fmt.Errorf("failed to unmarshal manifest: %w", err) + } + + // 查找 Helm Chart layer(tar+gzip 包含 chart 内容)并从中读取 values.yaml + var chartLayer *ocispec.Descriptor + for i := range manifest.Layers { + layer := manifest.Layers[i] + if strings.Contains(layer.MediaType, "cncf.helm.chart") || + strings.Contains(layer.MediaType, "helm.chart.content") { + chartLayer = &manifest.Layers[i] + break + } + } + + if chartLayer == nil { + return "", entity.ErrValuesNotFound + } + + if chartLayer.Digest == "" { + return "", fmt.Errorf("chart layer digest is empty") + } + if _, err := digest.Parse(string(chartLayer.Digest)); err != nil { + return "", fmt.Errorf("invalid chart layer digest: %w", err) + } + + layerReader, err := repo.Fetch(ctx, *chartLayer) + if err != nil { + return "", fmt.Errorf("failed to fetch chart layer: %w", err) + } + defer layerReader.Close() + + gzipReader, err := gzip.NewReader(layerReader) + if err != nil { + return "", fmt.Errorf("failed to create gzip reader: %w", err) + } + defer gzipReader.Close() + + tarReader := tar.NewReader(gzipReader) + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return "", fmt.Errorf("failed to read chart archive: %w", err) + } + + if header.Typeflag != tar.TypeReg { + continue + } + + // 查找 values.yaml 文件(可能在 chart 根目录或子目录中) + // 通常路径格式为: {chart-name}/values.yaml + if strings.HasSuffix(header.Name, "values.yaml") { + data, err := io.ReadAll(tarReader) + if err != nil { + return "", fmt.Errorf("failed to read values.yaml: %w", err) + } + if len(data) == 0 { + return "", entity.ErrValuesNotFound + } + return string(data), nil + } + } + + return "", entity.ErrValuesNotFound +} + // PullArtifact 下载 artifact 到本地 func (c *OCIClient) PullArtifact(ctx context.Context, registry *entity.Registry, repository, reference, destPath string) error { reg, err := c.getRegistry(registry) diff --git a/backend/internal/adapter/output/persistence/mock/cluster_repository_mock.go b/backend/internal/adapter/output/persistence/mock/cluster_repository_mock.go index 90e031b..d61ee81 100644 --- a/backend/internal/adapter/output/persistence/mock/cluster_repository_mock.go +++ b/backend/internal/adapter/output/persistence/mock/cluster_repository_mock.go @@ -104,13 +104,41 @@ func (r *ClusterRepositoryMock) Delete(ctx context.Context, id string) error { func (r *ClusterRepositoryMock) List(ctx context.Context) ([]*entity.Cluster, error) { r.mu.RLock() defer r.mu.RUnlock() - + clusters := make([]*entity.Cluster, 0, len(r.clusters)) for _, cluster := range r.clusters { // 解密敏感数据后返回 clusters = append(clusters, r.decryptCluster(cluster)) } - + + return clusters, nil +} + +func (r *ClusterRepositoryMock) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Cluster, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + clusters := make([]*entity.Cluster, 0) + for _, cluster := range r.clusters { + if cluster.WorkspaceID == workspaceID { + clusters = append(clusters, r.decryptCluster(cluster)) + } + } + + return clusters, nil +} + +func (r *ClusterRepositoryMock) GetShared(ctx context.Context) ([]*entity.Cluster, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + clusters := make([]*entity.Cluster, 0) + for _, cluster := range r.clusters { + if cluster.IsShared { + clusters = append(clusters, r.decryptCluster(cluster)) + } + } + return clusters, nil } diff --git a/backend/internal/adapter/output/persistence/mock/instance_repository_mock.go b/backend/internal/adapter/output/persistence/mock/instance_repository_mock.go index 907401d..9c467ae 100644 --- a/backend/internal/adapter/output/persistence/mock/instance_repository_mock.go +++ b/backend/internal/adapter/output/persistence/mock/instance_repository_mock.go @@ -102,12 +102,26 @@ func (r *InstanceRepositoryMock) ListByCluster(ctx context.Context, clusterID st func (r *InstanceRepositoryMock) List(ctx context.Context) ([]*entity.Instance, error) { r.mu.RLock() defer r.mu.RUnlock() - + instances := make([]*entity.Instance, 0, len(r.instances)) for _, instance := range r.instances { instances = append(instances, instance) } - + + return instances, nil +} + +func (r *InstanceRepositoryMock) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Instance, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + instances := make([]*entity.Instance, 0) + for _, instance := range r.instances { + if instance.WorkspaceID == workspaceID { + instances = append(instances, instance) + } + } + return instances, nil } diff --git a/backend/internal/adapter/output/persistence/mock/storage_repository_mock.go b/backend/internal/adapter/output/persistence/mock/storage_repository_mock.go new file mode 100644 index 0000000..437f991 --- /dev/null +++ b/backend/internal/adapter/output/persistence/mock/storage_repository_mock.go @@ -0,0 +1,96 @@ +package mock + +import ( + "context" + + "github.com/ocdp/cluster-service/internal/domain/entity" +) + +// StorageRepositoryMock Storage 仓储 Mock +type StorageRepositoryMock struct { + storages map[string]*entity.StorageBackend +} + +// NewStorageRepositoryMock 创建 Mock +func NewStorageRepositoryMock() *StorageRepositoryMock { + return &StorageRepositoryMock{ + storages: make(map[string]*entity.StorageBackend), + } +} + +// Create 创建存储 +func (r *StorageRepositoryMock) Create(ctx context.Context, storage *entity.StorageBackend) error { + r.storages[storage.ID] = storage + return nil +} + +// GetByID 获取存储 +func (r *StorageRepositoryMock) GetByID(ctx context.Context, id string) (*entity.StorageBackend, error) { + if s, ok := r.storages[id]; ok { + return s, nil + } + return nil, entity.ErrStorageNotFound +} + +// GetByWorkspace 获取工作空间的存储 +func (r *StorageRepositoryMock) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.StorageBackend, error) { + var result []*entity.StorageBackend + for _, s := range r.storages { + if s.WorkspaceID == workspaceID { + result = append(result, s) + } + } + return result, nil +} + +// GetByName 按名称获取 +func (r *StorageRepositoryMock) GetByName(ctx context.Context, workspaceID, name string) (*entity.StorageBackend, error) { + for _, s := range r.storages { + if s.WorkspaceID == workspaceID && s.Name == name { + return s, nil + } + } + return nil, entity.ErrStorageNotFound +} + +// Update 更新存储 +func (r *StorageRepositoryMock) Update(ctx context.Context, storage *entity.StorageBackend) error { + r.storages[storage.ID] = storage + return nil +} + +// Delete 删除存储 +func (r *StorageRepositoryMock) Delete(ctx context.Context, id string) error { + delete(r.storages, id) + return nil +} + +// GetShared 获取所有共享存储后端 +func (r *StorageRepositoryMock) GetShared(ctx context.Context) ([]*entity.StorageBackend, error) { + var result []*entity.StorageBackend + for _, s := range r.storages { + if s.IsShared { + result = append(result, s) + } + } + return result, nil +} + +// GetDefault 获取 workspace 的默认存储后端 +func (r *StorageRepositoryMock) GetDefault(ctx context.Context, workspaceID string) (*entity.StorageBackend, error) { + for _, s := range r.storages { + if s.WorkspaceID == workspaceID && s.IsDefault { + return s, nil + } + } + return nil, entity.ErrStorageNotFound +} + +// List 列出所有存储(管理员用) +func (r *StorageRepositoryMock) List(ctx context.Context) ([]*entity.StorageBackend, error) { + var result []*entity.StorageBackend + for _, s := range r.storages { + result = append(result, s) + } + return result, nil +} \ No newline at end of file diff --git a/backend/internal/adapter/output/persistence/mock/user_repository_mock.go b/backend/internal/adapter/output/persistence/mock/user_repository_mock.go index 3dc5cf5..4ac37f3 100644 --- a/backend/internal/adapter/output/persistence/mock/user_repository_mock.go +++ b/backend/internal/adapter/output/persistence/mock/user_repository_mock.go @@ -88,12 +88,40 @@ func (r *UserRepositoryMock) Delete(ctx context.Context, id string) error { func (r *UserRepositoryMock) List(ctx context.Context) ([]*entity.User, error) { r.mu.RLock() defer r.mu.RUnlock() - + users := make([]*entity.User, 0, len(r.users)) for _, user := range r.users { users = append(users, user) } - + + return users, nil +} + +func (r *UserRepositoryMock) ListByWorkspace(ctx context.Context, workspaceID string) ([]*entity.User, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + users := make([]*entity.User, 0) + for _, user := range r.users { + if user.WorkspaceID == workspaceID { + users = append(users, user) + } + } + + return users, nil +} + +func (r *UserRepositoryMock) ListActive(ctx context.Context) ([]*entity.User, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + users := make([]*entity.User, 0) + for _, user := range r.users { + if user.IsActive { + users = append(users, user) + } + } + return users, nil } diff --git a/backend/internal/adapter/output/persistence/postgres/audit_log_repository.go b/backend/internal/adapter/output/persistence/postgres/audit_log_repository.go new file mode 100644 index 0000000..85bab89 --- /dev/null +++ b/backend/internal/adapter/output/persistence/postgres/audit_log_repository.go @@ -0,0 +1,200 @@ +package postgres + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + + "github.com/google/uuid" + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/repository" +) + +// AuditLogRepository PostgreSQL 审计日志仓储实现 +type AuditLogRepository struct { + db *DB +} + +// NewAuditLogRepository 创建 PostgreSQL 审计日志仓储 +func NewAuditLogRepository(db *DB) repository.AuditLogRepository { + return &AuditLogRepository{db: db} +} + +// Create 创建审计日志 +func (r *AuditLogRepository) Create(ctx context.Context, log *entity.AuditLog) error { + if log.ID == "" { + log.ID = uuid.New().String() + } + + detailsJSON, err := json.Marshal(log.Details) + if err != nil { + return fmt.Errorf("failed to marshal details: %w", err) + } + + query := ` + INSERT INTO audit_logs (id, workspace_id, user_id, action, resource_type, resource_id, resource_name, details, ip_address, user_agent, created_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + ` + + _, err = r.db.conn.ExecContext(ctx, query, + log.ID, + log.WorkspaceID, + log.UserID, + log.Action, + log.ResourceType, + log.ResourceID, + log.ResourceName, + detailsJSON, + log.IPAddress, + log.UserAgent, + log.CreatedAt, + ) + + if err != nil { + return fmt.Errorf("failed to create audit log: %w", err) + } + + return nil +} + +// GetByWorkspace 获取 workspace 的审计日志 +func (r *AuditLogRepository) GetByWorkspace(ctx context.Context, workspaceID string, limit int) ([]*entity.AuditLog, error) { + if limit <= 0 { + limit = 100 + } + + query := ` + SELECT id, workspace_id, user_id, action, resource_type, resource_id, resource_name, details, ip_address, user_agent, created_at + FROM audit_logs + WHERE workspace_id = $1 + ORDER BY created_at DESC + LIMIT $2 + ` + + rows, err := r.db.conn.QueryContext(ctx, query, workspaceID, limit) + if err != nil { + return nil, fmt.Errorf("failed to get audit logs: %w", err) + } + defer rows.Close() + + return r.scanAuditLogs(rows) +} + +// GetByUser 获取用户的审计日志 +func (r *AuditLogRepository) GetByUser(ctx context.Context, userID string, limit int) ([]*entity.AuditLog, error) { + if limit <= 0 { + limit = 100 + } + + query := ` + SELECT id, workspace_id, user_id, action, resource_type, resource_id, resource_name, details, ip_address, user_agent, created_at + FROM audit_logs + WHERE user_id = $1 + ORDER BY created_at DESC + LIMIT $2 + ` + + rows, err := r.db.conn.QueryContext(ctx, query, userID, limit) + if err != nil { + return nil, fmt.Errorf("failed to get audit logs: %w", err) + } + defer rows.Close() + + return r.scanAuditLogs(rows) +} + +// GetByResource 获取资源的审计日志 +func (r *AuditLogRepository) GetByResource(ctx context.Context, resourceType entity.AuditResourceType, resourceID string, limit int) ([]*entity.AuditLog, error) { + if limit <= 0 { + limit = 100 + } + + query := ` + SELECT id, workspace_id, user_id, admin action, resource_type, resource_id, resource_name, details, ip_address, user_agent, created_at + FROM audit_logs + WHERE resource_type = $1 AND resource_id = $2 + ORDER BY created_at DESC + LIMIT $3 + ` + + rows, err := r.db.conn.QueryContext(ctx, query, resourceType, resourceID, limit) + if err != nil { + return nil, fmt.Errorf("failed to get audit logs: %w", err) + } + defer rows.Close() + + return r.scanAuditLogs(rows) +} + +// List 列出审计日志(分页) +func (r *AuditLogRepository) List(ctx context.Context, limit, offset int) ([]*entity.AuditLog, error) { + if limit <= 0 { + limit = 100 + } + if offset < 0 { + offset = 0 + } + + query := ` + SELECT id, workspace_id, user_id, action, resource_type, resource_id, resource_name, details, ip_address, user_agent, created_at + FROM audit_logs + ORDER BY created_at DESC + LIMIT $1 OFFSET $2 + ` + + rows, err := r.db.conn.QueryContext(ctx, query, limit, offset) + if err != nil { + return nil, fmt.Errorf("failed to list audit logs: %w", err) + } + defer rows.Close() + + return r.scanAuditLogs(rows) +} + +// DeleteByWorkspace 删除 workspace 的审计日志 +func (r *AuditLogRepository) DeleteByWorkspace(ctx context.Context, workspaceID string) error { + query := `DELETE FROM audit_logs WHERE workspace_id = $1` + + _, err := r.db.conn.ExecContext(ctx, query, workspaceID) + if err != nil { + return fmt.Errorf("failed to delete audit logs: %w", err) + } + + return nil +} + +// scanAuditLogs 扫描多行结果 +func (r *AuditLogRepository) scanAuditLogs(rows *sql.Rows) ([]*entity.AuditLog, error) { + logs := make([]*entity.AuditLog, 0) + for rows.Next() { + log := &entity.AuditLog{} + var detailsJSON []byte + err := rows.Scan( + &log.ID, + &log.WorkspaceID, + &log.UserID, + &log.Action, + &log.ResourceType, + &log.ResourceID, + &log.ResourceName, + &detailsJSON, + &log.IPAddress, + &log.UserAgent, + &log.CreatedAt, + ) + if err != nil { + return nil, fmt.Errorf("failed to scan audit log: %w", err) + } + if err := json.Unmarshal(detailsJSON, &log.Details); err != nil { + log.Details = make(map[string]interface{}) + } + logs = append(logs, log) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows iteration error: %w", err) + } + + return logs, nil +} \ No newline at end of file diff --git a/backend/internal/adapter/output/persistence/postgres/chart_reference_repository.go b/backend/internal/adapter/output/persistence/postgres/chart_reference_repository.go new file mode 100644 index 0000000..dadae75 --- /dev/null +++ b/backend/internal/adapter/output/persistence/postgres/chart_reference_repository.go @@ -0,0 +1,253 @@ +package postgres + +import ( + "context" + "database/sql" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/repository" +) + +// ChartReferenceRepository PostgreSQL Chart 引用仓储实现 +type ChartReferenceRepository struct { + db *DB +} + +// NewChartReferenceRepository 创建 PostgreSQL Chart 引用仓储 +func NewChartReferenceRepository(db *DB) repository.ChartReferenceRepository { + return &ChartReferenceRepository{db: db} +} + +// Create 创建 Chart 引用 +func (r *ChartReferenceRepository) Create(ctx context.Context, chartRef *entity.ChartReference) error { + if chartRef.ID == "" { + chartRef.ID = uuid.New().String() + } + + query := ` + INSERT INTO chart_references (id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + ` + + _, err := r.db.conn.ExecContext(ctx, query, + chartRef.ID, + chartRef.WorkspaceID, + chartRef.RegistryID, + chartRef.Repository, + chartRef.ChartName, + chartRef.Description, + chartRef.IsEnabled, + chartRef.CreatedAt, + chartRef.UpdatedAt, + ) + + if err != nil { + return fmt.Errorf("failed to create chart reference: %w", err) + } + + return nil +} + +// GetByID 根据 ID 获取 Chart 引用 +func (r *ChartReferenceRepository) GetByID(ctx context.Context, id string) (*entity.ChartReference, error) { + query := ` + SELECT id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at + FROM chart_references + WHERE id = $1 + ` + + chartRef := &entity.ChartReference{} + err := r.db.conn.QueryRowContext(ctx, query, id).Scan( + &chartRef.ID, + &chartRef.WorkspaceID, + &chartRef.RegistryID, + &chartRef.Repository, + &chartRef.ChartName, + &chartRef.Description, + &chartRef.IsEnabled, + &chartRef.CreatedAt, + &chartRef.UpdatedAt, + ) + + if err == sql.ErrNoRows { + return nil, entity.ErrChartReferenceNotFound + } + if err != nil { + return nil, fmt.Errorf("failed to get chart reference: %w", err) + } + + return chartRef, nil +} + +// GetByWorkspace 获取 workspace 的所有 Chart 引用 +func (r *ChartReferenceRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.ChartReference, error) { + query := ` + SELECT id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at + FROM chart_references + WHERE workspace_id = $1 + ORDER BY chart_name + ` + + rows, err := r.db.conn.QueryContext(ctx, query, workspaceID) + if err != nil { + return nil, fmt.Errorf("failed to list chart references: %w", err) + } + defer rows.Close() + + return r.scanChartReferences(rows) +} + +// GetByRegistry 获取 registry 的所有 Chart 引用 +func (r *ChartReferenceRepository) GetByRegistry(ctx context.Context, registryID string) ([]*entity.ChartReference, error) { + query := ` + SELECT id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at + FROM chart_references + WHERE registry_id = $1 + ORDER BY chart_name + ` + + rows, err := r.db.conn.QueryContext(ctx, query, registryID) + if err != nil { + return nil, fmt.Errorf("failed to list chart references: %w", err) + } + defer rows.Close() + + return r.scanChartReferences(rows) +} + +// GetByName 根据名称获取 Chart 引用 +func (r *ChartReferenceRepository) GetByName(ctx context.Context, workspaceID, chartName string) (*entity.ChartReference, error) { + query := ` + SELECT id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at + FROM chart_references + WHERE workspace_id = $1 AND chart_name = $2 + ` + + chartRef := &entity.ChartReference{} + err := r.db.conn.QueryRowContext(ctx, query, workspaceID, chartName).Scan( + &chartRef.ID, + &chartRef.WorkspaceID, + &chartRef.RegistryID, + &chartRef.Repository, + &chartRef.ChartName, + &chartRef.Description, + &chartRef.IsEnabled, + &chartRef.CreatedAt, + &chartRef.UpdatedAt, + ) + + if err == sql.ErrNoRows { + return nil, entity.ErrChartReferenceNotFound + } + if err != nil { + return nil, fmt.Errorf("failed to get chart reference: %w", err) + } + + return chartRef, nil +} + +// Update 更新 Chart 引用 +func (r *ChartReferenceRepository) Update(ctx context.Context, chartRef *entity.ChartReference) error { + chartRef.UpdatedAt = time.Now() + + query := ` + UPDATE chart_references + SET registry_id = $1, repository = $2, chart_name = $3, description = $4, is_enabled = $5, updated_at = $6 + WHERE id = $7 + ` + + result, err := r.db.conn.ExecContext(ctx, query, + chartRef.RegistryID, + chartRef.Repository, + chartRef.ChartName, + chartRef.Description, + chartRef.IsEnabled, + chartRef.UpdatedAt, + chartRef.ID, + ) + + if err != nil { + return fmt.Errorf("failed to update chart reference: %w", err) + } + + rows, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get affected rows: %w", err) + } + + if rows == 0 { + return entity.ErrChartReferenceNotFound + } + + return nil +} + +// Delete 删除 Chart 引用 +func (r *ChartReferenceRepository) Delete(ctx context.Context, id string) error { + query := `DELETE FROM chart_references WHERE id = $1` + + result, err := r.db.conn.ExecContext(ctx, query, id) + if err != nil { + return fmt.Errorf("failed to delete chart reference: %w", err) + } + + rows, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get affected rows: %w", err) + } + + if rows == 0 { + return entity.ErrChartReferenceNotFound + } + + return nil +} + +// List 列出所有 Chart 引用(管理员用) +func (r *ChartReferenceRepository) List(ctx context.Context) ([]*entity.ChartReference, error) { + query := ` + SELECT id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at + FROM chart_references + ORDER BY workspace_id, chart_name + ` + + rows, err := r.db.conn.QueryContext(ctx, query) + if err != nil { + return nil, fmt.Errorf("failed to list chart references: %w", err) + } + defer rows.Close() + + return r.scanChartReferences(rows) +} + +// scanChartReferences 扫描多行结果 +func (r *ChartReferenceRepository) scanChartReferences(rows *sql.Rows) ([]*entity.ChartReference, error) { + chartRefs := make([]*entity.ChartReference, 0) + for rows.Next() { + chartRef := &entity.ChartReference{} + err := rows.Scan( + &chartRef.ID, + &chartRef.WorkspaceID, + &chartRef.RegistryID, + &chartRef.Repository, + &chartRef.ChartName, + &chartRef.Description, + &chartRef.IsEnabled, + &chartRef.CreatedAt, + &chartRef.UpdatedAt, + ) + if err != nil { + return nil, fmt.Errorf("failed to scan chart reference: %w", err) + } + chartRefs = append(chartRefs, chartRef) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows iteration error: %w", err) + } + + return chartRefs, nil +} \ No newline at end of file diff --git a/backend/internal/adapter/output/persistence/postgres/cluster_repository.go b/backend/internal/adapter/output/persistence/postgres/cluster_repository.go index fcd9f6d..1baf2f6 100644 --- a/backend/internal/adapter/output/persistence/postgres/cluster_repository.go +++ b/backend/internal/adapter/output/persistence/postgres/cluster_repository.go @@ -32,6 +32,11 @@ func (r *ClusterRepository) Create(ctx context.Context, cluster *entity.Cluster) cluster.ID = uuid.New().String() } + // 设置默认值 + if cluster.IsolationMode == "" { + cluster.IsolationMode = entity.IsolationModeNamespace + } + // 加密敏感数据 encryptedCAData, err := r.encryptor.Encrypt(cluster.CAData) if err != nil { @@ -54,12 +59,14 @@ func (r *ClusterRepository) Create(ctx context.Context, cluster *entity.Cluster) } query := ` - INSERT INTO clusters (id, name, host, ca_data, cert_data, key_data, token, description, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + INSERT INTO clusters (id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) ` _, err = r.db.conn.ExecContext(ctx, query, cluster.ID, + cluster.WorkspaceID, + cluster.OwnerID, cluster.Name, cluster.Host, encryptedCAData, @@ -67,6 +74,9 @@ func (r *ClusterRepository) Create(ctx context.Context, cluster *entity.Cluster) encryptedKeyData, encryptedToken, cluster.Description, + cluster.IsolationMode, + cluster.DefaultNamespace, + cluster.IsShared, cluster.CreatedAt, cluster.UpdatedAt, ) @@ -81,7 +91,7 @@ func (r *ClusterRepository) Create(ctx context.Context, cluster *entity.Cluster) // GetByID 根据 ID 获取集群 func (r *ClusterRepository) GetByID(ctx context.Context, id string) (*entity.Cluster, error) { query := ` - SELECT id, name, host, ca_data, cert_data, key_data, token, description, created_at, updated_at + SELECT id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at FROM clusters WHERE id = $1 ` @@ -91,6 +101,8 @@ func (r *ClusterRepository) GetByID(ctx context.Context, id string) (*entity.Clu err := r.db.conn.QueryRowContext(ctx, query, id).Scan( &cluster.ID, + &cluster.WorkspaceID, + &cluster.OwnerID, &cluster.Name, &cluster.Host, &encryptedCAData, @@ -98,6 +110,9 @@ func (r *ClusterRepository) GetByID(ctx context.Context, id string) (*entity.Clu &encryptedKeyData, &encryptedToken, &cluster.Description, + &cluster.IsolationMode, + &cluster.DefaultNamespace, + &cluster.IsShared, &cluster.CreatedAt, &cluster.UpdatedAt, ) @@ -110,25 +125,10 @@ func (r *ClusterRepository) GetByID(ctx context.Context, id string) (*entity.Clu } // 解密敏感数据 - cluster.CAData, err = r.encryptor.Decrypt(encryptedCAData) - if err != nil { - return nil, fmt.Errorf("failed to decrypt CA data: %w", err) - } - - cluster.CertData, err = r.encryptor.Decrypt(encryptedCertData) - if err != nil { - return nil, fmt.Errorf("failed to decrypt cert data: %w", err) - } - - cluster.KeyData, err = r.encryptor.Decrypt(encryptedKeyData) - if err != nil { - return nil, fmt.Errorf("failed to decrypt key data: %w", err) - } - - cluster.Token, err = r.encryptor.Decrypt(encryptedToken) - if err != nil { - return nil, fmt.Errorf("failed to decrypt token: %w", err) - } + cluster.CAData, _ = r.encryptor.Decrypt(encryptedCAData) + cluster.CertData, _ = r.encryptor.Decrypt(encryptedCertData) + cluster.KeyData, _ = r.encryptor.Decrypt(encryptedKeyData) + cluster.Token, _ = r.encryptor.Decrypt(encryptedToken) return cluster, nil } @@ -136,7 +136,7 @@ func (r *ClusterRepository) GetByID(ctx context.Context, id string) (*entity.Clu // GetByName 根据名称获取集群 func (r *ClusterRepository) GetByName(ctx context.Context, name string) (*entity.Cluster, error) { query := ` - SELECT id, name, host, ca_data, cert_data, key_data, token, description, created_at, updated_at + SELECT id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at FROM clusters WHERE name = $1 ` @@ -146,6 +146,8 @@ func (r *ClusterRepository) GetByName(ctx context.Context, name string) (*entity err := r.db.conn.QueryRowContext(ctx, query, name).Scan( &cluster.ID, + &cluster.WorkspaceID, + &cluster.OwnerID, &cluster.Name, &cluster.Host, &encryptedCAData, @@ -153,6 +155,9 @@ func (r *ClusterRepository) GetByName(ctx context.Context, name string) (*entity &encryptedKeyData, &encryptedToken, &cluster.Description, + &cluster.IsolationMode, + &cluster.DefaultNamespace, + &cluster.IsShared, &cluster.CreatedAt, &cluster.UpdatedAt, ) @@ -165,25 +170,10 @@ func (r *ClusterRepository) GetByName(ctx context.Context, name string) (*entity } // 解密敏感数据 - cluster.CAData, err = r.encryptor.Decrypt(encryptedCAData) - if err != nil { - return nil, fmt.Errorf("failed to decrypt CA data: %w", err) - } - - cluster.CertData, err = r.encryptor.Decrypt(encryptedCertData) - if err != nil { - return nil, fmt.Errorf("failed to decrypt cert data: %w", err) - } - - cluster.KeyData, err = r.encryptor.Decrypt(encryptedKeyData) - if err != nil { - return nil, fmt.Errorf("failed to decrypt key data: %w", err) - } - - cluster.Token, err = r.encryptor.Decrypt(encryptedToken) - if err != nil { - return nil, fmt.Errorf("failed to decrypt token: %w", err) - } + cluster.CAData, _ = r.encryptor.Decrypt(encryptedCAData) + cluster.CertData, _ = r.encryptor.Decrypt(encryptedCertData) + cluster.KeyData, _ = r.encryptor.Decrypt(encryptedKeyData) + cluster.Token, _ = r.encryptor.Decrypt(encryptedToken) return cluster, nil } @@ -215,9 +205,10 @@ func (r *ClusterRepository) Update(ctx context.Context, cluster *entity.Cluster) query := ` UPDATE clusters - SET name = $1, host = $2, ca_data = $3, cert_data = $4, key_data = $5, - token = $6, description = $7, updated_at = $8 - WHERE id = $9 + SET name = $1, host = $2, ca_data = $3, cert_data = $4, key_data = $5, + token = $6, description = $7, isolation_mode = $8, default_namespace = $9, + is_shared = $10, updated_at = $11 + WHERE id = $12 ` result, err := r.db.conn.ExecContext(ctx, query, @@ -228,6 +219,9 @@ func (r *ClusterRepository) Update(ctx context.Context, cluster *entity.Cluster) encryptedKeyData, encryptedToken, cluster.Description, + cluster.IsolationMode, + cluster.DefaultNamespace, + cluster.IsShared, cluster.UpdatedAt, cluster.ID, ) @@ -272,7 +266,7 @@ func (r *ClusterRepository) Delete(ctx context.Context, id string) error { // List 列出所有集群 func (r *ClusterRepository) List(ctx context.Context) ([]*entity.Cluster, error) { query := ` - SELECT id, name, host, ca_data, cert_data, key_data, token, description, created_at, updated_at + SELECT id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at FROM clusters ORDER BY created_at DESC ` @@ -283,13 +277,59 @@ func (r *ClusterRepository) List(ctx context.Context) ([]*entity.Cluster, error) } defer rows.Close() + return r.scanClusters(rows) +} + +// GetByWorkspace 获取 workspace 的所有集群(包括共享集群) +func (r *ClusterRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Cluster, error) { + query := ` + SELECT id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at + FROM clusters + WHERE workspace_id = $1 OR is_shared = TRUE + ORDER BY is_shared, created_at DESC + ` + + rows, err := r.db.conn.QueryContext(ctx, query, workspaceID) + if err != nil { + return nil, fmt.Errorf("failed to list clusters by workspace: %w", err) + } + defer rows.Close() + + return r.scanClusters(rows) +} + +// GetShared 获取所有共享集群 +func (r *ClusterRepository) GetShared(ctx context.Context) ([]*entity.Cluster, error) { + query := ` + SELECT id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at + FROM clusters + WHERE is_shared = TRUE + ORDER BY created_at DESC + ` + + rows, err := r.db.conn.QueryContext(ctx, query) + if err != nil { + return nil, fmt.Errorf("failed to list shared clusters: %w", err) + } + defer rows.Close() + + return r.scanClusters(rows) +} + +// scanClusters 扫描多行结果 +func (r *ClusterRepository) scanClusters(rows *sql.Rows) ([]*entity.Cluster, error) { clusters := make([]*entity.Cluster, 0) for rows.Next() { cluster := &entity.Cluster{} - var encryptedCAData, encryptedCertData, encryptedKeyData, encryptedToken string + var ( + encryptedCAData, encryptedCertData, encryptedKeyData, encryptedToken sql.NullString + workspaceID, ownerID, defaultNamespace sql.NullString + ) err := rows.Scan( &cluster.ID, + &workspaceID, + &ownerID, &cluster.Name, &cluster.Host, &encryptedCAData, @@ -297,6 +337,9 @@ func (r *ClusterRepository) List(ctx context.Context) ([]*entity.Cluster, error) &encryptedKeyData, &encryptedToken, &cluster.Description, + &cluster.IsolationMode, + &defaultNamespace, + &cluster.IsShared, &cluster.CreatedAt, &cluster.UpdatedAt, ) @@ -304,25 +347,23 @@ func (r *ClusterRepository) List(ctx context.Context) ([]*entity.Cluster, error) return nil, fmt.Errorf("failed to scan cluster: %w", err) } + // 处理 NULL 值 + cluster.WorkspaceID = workspaceID.String + cluster.OwnerID = ownerID.String + cluster.DefaultNamespace = defaultNamespace.String + // 解密敏感数据 - cluster.CAData, err = r.encryptor.Decrypt(encryptedCAData) - if err != nil { - return nil, fmt.Errorf("failed to decrypt CA data: %w", err) + if encryptedCAData.Valid { + cluster.CAData, _ = r.encryptor.Decrypt(encryptedCAData.String) } - - cluster.CertData, err = r.encryptor.Decrypt(encryptedCertData) - if err != nil { - return nil, fmt.Errorf("failed to decrypt cert data: %w", err) + if encryptedCertData.Valid { + cluster.CertData, _ = r.encryptor.Decrypt(encryptedCertData.String) } - - cluster.KeyData, err = r.encryptor.Decrypt(encryptedKeyData) - if err != nil { - return nil, fmt.Errorf("failed to decrypt key data: %w", err) + if encryptedKeyData.Valid { + cluster.KeyData, _ = r.encryptor.Decrypt(encryptedKeyData.String) } - - cluster.Token, err = r.encryptor.Decrypt(encryptedToken) - if err != nil { - return nil, fmt.Errorf("failed to decrypt token: %w", err) + if encryptedToken.Valid { + cluster.Token, _ = r.encryptor.Decrypt(encryptedToken.String) } clusters = append(clusters, cluster) diff --git a/backend/internal/adapter/output/persistence/postgres/db.go b/backend/internal/adapter/output/persistence/postgres/db.go index 67fcc76..71f4eb1 100644 --- a/backend/internal/adapter/output/persistence/postgres/db.go +++ b/backend/internal/adapter/output/persistence/postgres/db.go @@ -124,6 +124,58 @@ func (db *DB) InitSchema() error { CREATE INDEX IF NOT EXISTS idx_instances_cluster ON instances(cluster_id); CREATE INDEX IF NOT EXISTS idx_instances_registry ON instances(registry_id); CREATE INDEX IF NOT EXISTS idx_instances_name ON instances(name); + + -- Storage Backends 表 + CREATE TABLE IF NOT EXISTS storage_backends ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36), + owner_id VARCHAR(36), + name VARCHAR(255) NOT NULL, + type VARCHAR(50) NOT NULL, + config JSONB NOT NULL, + description TEXT, + is_default BOOLEAN DEFAULT FALSE, + is_shared BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + + CREATE INDEX IF NOT EXISTS idx_storage_workspace ON storage_backends(workspace_id); + + -- Chart References 表 + CREATE TABLE IF NOT EXISTS chart_references ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36), + registry_id VARCHAR(36), + repository VARCHAR(500) NOT NULL, + chart_name VARCHAR(255) NOT NULL, + description TEXT, + is_enabled BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + + CREATE INDEX IF NOT EXISTS idx_chart_workspace ON chart_references(workspace_id); + CREATE INDEX IF NOT EXISTS idx_chart_registry ON chart_references(registry_id); + + -- Values Templates 表 - 使用复合唯一键替代主键,允许同一模板的多个版本 + CREATE TABLE IF NOT EXISTS values_templates ( + id VARCHAR(36), + workspace_id VARCHAR(36), + owner_id VARCHAR(36), + chart_reference_id VARCHAR(36), + name VARCHAR(255) NOT NULL, + description TEXT, + values_yaml TEXT NOT NULL, + version INTEGER NOT NULL DEFAULT 1, + is_default BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE (chart_reference_id, name, version) + ); + + CREATE INDEX IF NOT EXISTS idx_values_template_chart ON values_templates(chart_reference_id); + CREATE INDEX IF NOT EXISTS idx_values_template_workspace ON values_templates(workspace_id); ` _, err := db.conn.Exec(schema) diff --git a/backend/internal/adapter/output/persistence/postgres/instance_repository.go b/backend/internal/adapter/output/persistence/postgres/instance_repository.go index d12400b..66e0660 100644 --- a/backend/internal/adapter/output/persistence/postgres/instance_repository.go +++ b/backend/internal/adapter/output/persistence/postgres/instance_repository.go @@ -431,3 +431,105 @@ func (r *InstanceRepository) List(ctx context.Context) ([]*entity.Instance, erro return instances, nil } + +// GetByWorkspace 列出指定工作空间的所有实例(用于配额检查) +func (r *InstanceRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Instance, error) { + query := ` + SELECT id, cluster_id, workspace_id, owner_id, name, namespace, registry_id, repository, chart, version, + description, values, values_yaml, values_template_id, user_override_yaml, + status, status_reason, last_operation, last_error, revision, + cpu_requested, memory_requested, gpu_requested, gpu_memory_requested, + created_at, updated_at + FROM instances + WHERE workspace_id = $1 + ORDER BY created_at DESC + ` + + rows, err := r.db.conn.QueryContext(ctx, query, workspaceID) + if err != nil { + return nil, fmt.Errorf("failed to get instances by workspace: %w", err) + } + defer rows.Close() + + instances := make([]*entity.Instance, 0) + for rows.Next() { + instance := &entity.Instance{} + var ( + valuesJSON []byte + statusReason sql.NullString + lastOperation sql.NullString + lastError sql.NullString + valuesTemplateID sql.NullString + userOverrideYAML sql.NullString + memoryRequested sql.NullString + gpuMemoryRequested sql.NullString + ) + + err := rows.Scan( + &instance.ID, + &instance.ClusterID, + &instance.WorkspaceID, + &instance.OwnerID, + &instance.Name, + &instance.Namespace, + &instance.RegistryID, + &instance.Repository, + &instance.Chart, + &instance.Version, + &instance.Description, + &valuesJSON, + &instance.ValuesYAML, + &valuesTemplateID, + &userOverrideYAML, + &instance.Status, + &statusReason, + &lastOperation, + &lastError, + &instance.Revision, + &instance.CPURequested, + &memoryRequested, + &instance.GPURequested, + &gpuMemoryRequested, + &instance.CreatedAt, + &instance.UpdatedAt, + ) + if err != nil { + return nil, fmt.Errorf("failed to scan instance: %w", err) + } + + if valuesJSON != nil { + if err := json.Unmarshal(valuesJSON, &instance.Values); err != nil { + return nil, fmt.Errorf("failed to unmarshal values: %w", err) + } + } + if valuesTemplateID.Valid { + instance.ValuesTemplateID = valuesTemplateID.String + } + if userOverrideYAML.Valid { + instance.UserOverrideYAML = userOverrideYAML.String + } + if statusReason.Valid { + instance.StatusReason = statusReason.String + } + if lastOperation.Valid { + instance.LastOperation = entity.InstanceOperation(lastOperation.String) + } + if lastError.Valid { + instance.LastError = lastError.String + } + if memoryRequested.Valid { + instance.MemoryRequested = memoryRequested.String + } + if gpuMemoryRequested.Valid { + instance.GPUMemoryRequested = gpuMemoryRequested.String + } + + instances = append(instances, instance) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows iteration error: %w", err) + } + + return instances, nil +} diff --git a/backend/internal/adapter/output/persistence/postgres/quota_repository.go b/backend/internal/adapter/output/persistence/postgres/quota_repository.go new file mode 100644 index 0000000..1829343 --- /dev/null +++ b/backend/internal/adapter/output/persistence/postgres/quota_repository.go @@ -0,0 +1,212 @@ +package postgres + +import ( + "context" + "database/sql" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/repository" +) + +// QuotaRepository PostgreSQL 配额仓储实现 +type QuotaRepository struct { + db *DB +} + +// NewQuotaRepository 创建 PostgreSQL 配额仓储 +func NewQuotaRepository(db *DB) repository.QuotaRepository { + return &QuotaRepository{db: db} +} + +// Create 创建配额 +func (r *QuotaRepository) Create(ctx context.Context, quota *entity.WorkspaceQuota) error { + if quota.ID == "" { + quota.ID = uuid.New().String() + } + + query := ` + INSERT INTO workspace_quotas (id, workspace_id, resource_type, hard_limit, soft_limit, used, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + ON CONFLICT (workspace_id, resource_type) DO UPDATE + SET hard_limit = $4, soft_limit = $5, updated_at = $8 + ` + + _, err := r.db.conn.ExecContext(ctx, query, + quota.ID, + quota.WorkspaceID, + quota.ResourceType, + quota.HardLimit, + quota.SoftLimit, + quota.Used, + quota.CreatedAt, + quota.UpdatedAt, + ) + + if err != nil { + return fmt.Errorf("failed to create quota: %w", err) + } + + return nil +} + +// GetByID 根据 ID 获取配额 +func (r *QuotaRepository) GetByID(ctx context.Context, id string) (*entity.WorkspaceQuota, error) { + query := ` + SELECT id, workspace_id, resource_type, hard_limit, soft_limit, used, created_at, updated_at + FROM workspace_quotas + WHERE id = $1 + ` + + quota := &entity.WorkspaceQuota{} + err := r.db.conn.QueryRowContext(ctx, query, id).Scan( + "a.ID, + "a.WorkspaceID, + "a.ResourceType, + "a.HardLimit, + "a.SoftLimit, + "a.Used, + "a.CreatedAt, + "a.UpdatedAt, + ) + + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("failed to get quota: %w", err) + } + + return quota, nil +} + +// GetByWorkspaceAndType 根据 workspace 和资源类型获取配额 +func (r *QuotaRepository) GetByWorkspaceAndType(ctx context.Context, workspaceID string, resourceType entity.ResourceType) (*entity.WorkspaceQuota, error) { + query := ` + SELECT id, workspace_id, resource_type, hard_limit, soft_limit, used, created_at, updated_at + FROM workspace_quotas + WHERE workspace_id = $1 AND resource_type = $2 + ` + + quota := &entity.WorkspaceQuota{} + err := r.db.conn.QueryRowContext(ctx, query, workspaceID, resourceType).Scan( + "a.ID, + "a.WorkspaceID, + "a.ResourceType, + "a.HardLimit, + "a.SoftLimit, + "a.Used, + "a.CreatedAt, + "a.UpdatedAt, + ) + + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("failed to get quota: %w", err) + } + + return quota, nil +} + +// GetByWorkspace 获取 workspace 的所有配额 +func (r *QuotaRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.WorkspaceQuota, error) { + query := ` + SELECT id, workspace_id, resource_type, hard_limit, soft_limit, used, created_at, updated_at + FROM workspace_quotas + WHERE workspace_id = $1 + ORDER BY resource_type + ` + + rows, err := r.db.conn.QueryContext(ctx, query, workspaceID) + if err != nil { + return nil, fmt.Errorf("failed to list quotas: %w", err) + } + defer rows.Close() + + quotas := make([]*entity.WorkspaceQuota, 0) + for rows.Next() { + quota := &entity.WorkspaceQuota{} + err := rows.Scan( + "a.ID, + "a.WorkspaceID, + "a.ResourceType, + "a.HardLimit, + "a.SoftLimit, + "a.Used, + "a.CreatedAt, + "a.UpdatedAt, + ) + if err != nil { + return nil, fmt.Errorf("failed to scan quota: %w", err) + } + quotas = append(quotas, quota) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows iteration error: %w", err) + } + + return quotas, nil +} + +// Update 更新配额 +func (r *QuotaRepository) Update(ctx context.Context, quota *entity.WorkspaceQuota) error { + quota.UpdatedAt = time.Now() + + query := ` + UPDATE workspace_quotas + SET hard_limit = $1, soft_limit = $2, used = $3, updated_at = $4 + WHERE id = $5 + ` + + result, err := r.db.conn.ExecContext(ctx, query, + quota.HardLimit, + quota.SoftLimit, + quota.Used, + quota.UpdatedAt, + quota.ID, + ) + + if err != nil { + return fmt.Errorf("failed to update quota: %w", err) + } + + rows, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get affected rows: %w", err) + } + + if rows == 0 { + return fmt.Errorf("quota not found") + } + + return nil +} + +// Delete 删除配额 +func (r *QuotaRepository) Delete(ctx context.Context, id string) error { + query := `DELETE FROM workspace_quotas WHERE id = $1` + + _, err := r.db.conn.ExecContext(ctx, query, id) + if err != nil { + return fmt.Errorf("failed to delete quota: %w", err) + } + + return nil +} + +// DeleteByWorkspace 删除 workspace 的所有配额 +func (r *QuotaRepository) DeleteByWorkspace(ctx context.Context, workspaceID string) error { + query := `DELETE FROM workspace_quotas WHERE workspace_id = $1` + + _, err := r.db.conn.ExecContext(ctx, query, workspaceID) + if err != nil { + return fmt.Errorf("failed to delete quotas: %w", err) + } + + return nil +} \ No newline at end of file diff --git a/backend/internal/adapter/output/persistence/postgres/registry_repository.go b/backend/internal/adapter/output/persistence/postgres/registry_repository.go index 78fb6fd..7802380 100644 --- a/backend/internal/adapter/output/persistence/postgres/registry_repository.go +++ b/backend/internal/adapter/output/persistence/postgres/registry_repository.go @@ -208,7 +208,7 @@ func (r *RegistryRepository) Delete(ctx context.Context, id string) error { // List 列出所有 Registries func (r *RegistryRepository) List(ctx context.Context) ([]*entity.Registry, error) { query := ` - SELECT id, name, url, description, username, password, insecure, created_at, updated_at + SELECT id, workspace_id, owner_id, name, url, description, username, password, insecure, is_shared, created_at, updated_at FROM registries ORDER BY created_at DESC ` @@ -222,16 +222,19 @@ func (r *RegistryRepository) List(ctx context.Context) ([]*entity.Registry, erro registries := make([]*entity.Registry, 0) for rows.Next() { registry := &entity.Registry{} - var encryptedPassword string + var encryptedPassword, workspaceID, ownerID sql.NullString err := rows.Scan( ®istry.ID, + &workspaceID, + &ownerID, ®istry.Name, ®istry.URL, ®istry.Description, ®istry.Username, &encryptedPassword, ®istry.Insecure, + ®istry.IsShared, ®istry.CreatedAt, ®istry.UpdatedAt, ) @@ -239,10 +242,13 @@ func (r *RegistryRepository) List(ctx context.Context) ([]*entity.Registry, erro return nil, fmt.Errorf("failed to scan registry: %w", err) } + // 处理 NULL 值 + registry.WorkspaceID = workspaceID.String + registry.OwnerID = ownerID.String + // 解密密码 - registry.Password, err = r.encryptor.Decrypt(encryptedPassword) - if err != nil { - return nil, fmt.Errorf("failed to decrypt password: %w", err) + if encryptedPassword.Valid { + registry.Password, _ = r.encryptor.Decrypt(encryptedPassword.String) } registries = append(registries, registry) diff --git a/backend/internal/adapter/output/persistence/postgres/storage_repository.go b/backend/internal/adapter/output/persistence/postgres/storage_repository.go new file mode 100644 index 0000000..2481e44 --- /dev/null +++ b/backend/internal/adapter/output/persistence/postgres/storage_repository.go @@ -0,0 +1,326 @@ +package postgres + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/repository" +) + +// StorageRepository PostgreSQL 存储后端仓储实现 +type StorageRepository struct { + db *DB +} + +// NewStorageRepository 创建 PostgreSQL 存储后端仓储 +func NewStorageRepository(db *DB) repository.StorageRepository { + return &StorageRepository{db: db} +} + +// Create 创建存储后端 +func (r *StorageRepository) Create(ctx context.Context, storage *entity.StorageBackend) error { + if storage.ID == "" { + storage.ID = uuid.New().String() + } + + configJSON, err := json.Marshal(storage.Config) + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + query := ` + INSERT INTO storage_backends (id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + ` + + _, err = r.db.conn.ExecContext(ctx, query, + storage.ID, + storage.WorkspaceID, + storage.OwnerID, + storage.Name, + storage.Type, + configJSON, + storage.Description, + storage.IsDefault, + storage.IsShared, + storage.CreatedAt, + storage.UpdatedAt, + ) + + if err != nil { + return fmt.Errorf("failed to create storage: %w", err) + } + + return nil +} + +// GetByID 根据 ID 获取存储后端 +func (r *StorageRepository) GetByID(ctx context.Context, id string) (*entity.StorageBackend, error) { + query := ` + SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at + FROM storage_backends + WHERE id = $1 + ` + + storage := &entity.StorageBackend{} + var configJSON []byte + err := r.db.conn.QueryRowContext(ctx, query, id).Scan( + &storage.ID, + &storage.WorkspaceID, + &storage.OwnerID, + &storage.Name, + &storage.Type, + &configJSON, + &storage.Description, + &storage.IsDefault, + &storage.IsShared, + &storage.CreatedAt, + &storage.UpdatedAt, + ) + + if err == sql.ErrNoRows { + return nil, entity.ErrStorageNotFound + } + if err != nil { + return nil, fmt.Errorf("failed to get storage: %w", err) + } + + if err := json.Unmarshal(configJSON, &storage.Config); err != nil { + return nil, fmt.Errorf("failed to unmarshal config: %w", err) + } + + return storage, nil +} + +// GetByName 根据名称获取存储后端 +func (r *StorageRepository) GetByName(ctx context.Context, workspaceID, name string) (*entity.StorageBackend, error) { + query := ` + SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at + FROM storage_backends + WHERE workspace_id = $1 AND name = $2 + ` + + storage := &entity.StorageBackend{} + var configJSON []byte + err := r.db.conn.QueryRowContext(ctx, query, workspaceID, name).Scan( + &storage.ID, + &storage.WorkspaceID, + &storage.OwnerID, + &storage.Name, + &storage.Type, + &configJSON, + &storage.Description, + &storage.IsDefault, + &storage.IsShared, + &storage.CreatedAt, + &storage.UpdatedAt, + ) + + if err == sql.ErrNoRows { + return nil, entity.ErrStorageNotFound + } + if err != nil { + return nil, fmt.Errorf("failed to get storage: %w", err) + } + + if err := json.Unmarshal(configJSON, &storage.Config); err != nil { + return nil, fmt.Errorf("failed to unmarshal config: %w", err) + } + + return storage, nil +} + +// GetByWorkspace 获取 workspace 的所有存储后端 +func (r *StorageRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.StorageBackend, error) { + query := ` + SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at + FROM storage_backends + WHERE workspace_id = $1 OR is_shared = TRUE + ORDER BY is_default DESC, name + ` + + rows, err := r.db.conn.QueryContext(ctx, query, workspaceID) + if err != nil { + return nil, fmt.Errorf("failed to list storage: %w", err) + } + defer rows.Close() + + return r.scanStorages(rows) +} + +// GetShared 获取所有共享存储后端 +func (r *StorageRepository) GetShared(ctx context.Context) ([]*entity.StorageBackend, error) { + query := ` + SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at + FROM storage_backends + WHERE is_shared = TRUE + ORDER BY name + ` + + rows, err := r.db.conn.QueryContext(ctx, query) + if err != nil { + return nil, fmt.Errorf("failed to list shared storage: %w", err) + } + defer rows.Close() + + return r.scanStorages(rows) +} + +// GetDefault 获取 workspace 的默认存储后端 +func (r *StorageRepository) GetDefault(ctx context.Context, workspaceID string) (*entity.StorageBackend, error) { + query := ` + SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at + FROM storage_backends + WHERE workspace_id = $1 AND is_default = TRUE + LIMIT 1 + ` + + storage := &entity.StorageBackend{} + var configJSON []byte + err := r.db.conn.QueryRowContext(ctx, query, workspaceID).Scan( + &storage.ID, + &storage.WorkspaceID, + &storage.OwnerID, + &storage.Name, + &storage.Type, + &configJSON, + &storage.Description, + &storage.IsDefault, + &storage.IsShared, + &storage.CreatedAt, + &storage.UpdatedAt, + ) + + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("failed to get default storage: %w", err) + } + + if err := json.Unmarshal(configJSON, &storage.Config); err != nil { + return nil, fmt.Errorf("failed to unmarshal config: %w", err) + } + + return storage, nil +} + +// Update 更新存储后端 +func (r *StorageRepository) Update(ctx context.Context, storage *entity.StorageBackend) error { + storage.UpdatedAt = time.Now() + + configJSON, err := json.Marshal(storage.Config) + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + query := ` + UPDATE storage_backends + SET name = $1, type = $2, config = $3, description = $4, is_default = $5, is_shared = $6, updated_at = $7 + WHERE id = $8 + ` + + result, err := r.db.conn.ExecContext(ctx, query, + storage.Name, + storage.Type, + configJSON, + storage.Description, + storage.IsDefault, + storage.IsShared, + storage.UpdatedAt, + storage.ID, + ) + + if err != nil { + return fmt.Errorf("failed to update storage: %w", err) + } + + rows, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get affected rows: %w", err) + } + + if rows == 0 { + return entity.ErrStorageNotFound + } + + return nil +} + +// Delete 删除存储后端 +func (r *StorageRepository) Delete(ctx context.Context, id string) error { + query := `DELETE FROM storage_backends WHERE id = $1` + + result, err := r.db.conn.ExecContext(ctx, query, id) + if err != nil { + return fmt.Errorf("failed to delete storage: %w", err) + } + + rows, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get affected rows: %w", err) + } + + if rows == 0 { + return entity.ErrStorageNotFound + } + + return nil +} + +// List 列出所有存储后端(管理员用) +func (r *StorageRepository) List(ctx context.Context) ([]*entity.StorageBackend, error) { + query := ` + SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at + FROM storage_backends + ORDER BY workspace_id, name + ` + + rows, err := r.db.conn.QueryContext(ctx, query) + if err != nil { + return nil, fmt.Errorf("failed to list storage: %w", err) + } + defer rows.Close() + + return r.scanStorages(rows) +} + +// scanStorages 扫描多行结果 +func (r *StorageRepository) scanStorages(rows *sql.Rows) ([]*entity.StorageBackend, error) { + storages := make([]*entity.StorageBackend, 0) + for rows.Next() { + storage := &entity.StorageBackend{} + var configJSON []byte + err := rows.Scan( + &storage.ID, + &storage.WorkspaceID, + &storage.OwnerID, + &storage.Name, + &storage.Type, + &configJSON, + &storage.Description, + &storage.IsDefault, + &storage.IsShared, + &storage.CreatedAt, + &storage.UpdatedAt, + ) + if err != nil { + return nil, fmt.Errorf("failed to scan storage: %w", err) + } + if err := json.Unmarshal(configJSON, &storage.Config); err != nil { + return nil, fmt.Errorf("failed to unmarshal config: %w", err) + } + storages = append(storages, storage) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows iteration error: %w", err) + } + + return storages, nil +} \ No newline at end of file diff --git a/backend/internal/adapter/output/persistence/postgres/user_repository.go b/backend/internal/adapter/output/persistence/postgres/user_repository.go index ea766ee..89a10aa 100644 --- a/backend/internal/adapter/output/persistence/postgres/user_repository.go +++ b/backend/internal/adapter/output/persistence/postgres/user_repository.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "log" "time" "github.com/google/uuid" @@ -27,9 +28,14 @@ func (r *UserRepository) Create(ctx context.Context, user *entity.User) error { user.ID = uuid.New().String() } + // 设置默认值 + if user.IsActive { + user.IsActive = true + } + query := ` - INSERT INTO users (id, username, password_hash, email, revoked_after, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7) + INSERT INTO users (id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) ` _, err := r.db.conn.ExecContext(ctx, query, @@ -37,6 +43,10 @@ func (r *UserRepository) Create(ctx context.Context, user *entity.User) error { user.Username, user.PasswordHash, user.Email, + user.Role, + user.WorkspaceID, + user.IsActive, + user.MustChangePassword, user.RevokedAfter, user.CreatedAt, user.UpdatedAt, @@ -52,22 +62,34 @@ func (r *UserRepository) Create(ctx context.Context, user *entity.User) error { // GetByID 根据 ID 获取用户 func (r *UserRepository) GetByID(ctx context.Context, id string) (*entity.User, error) { query := ` - SELECT id, username, password_hash, email, revoked_after, created_at, updated_at + SELECT id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at FROM users WHERE id = $1 ` user := &entity.User{} + var workspaceID sql.NullString err := r.db.conn.QueryRowContext(ctx, query, id).Scan( &user.ID, &user.Username, &user.PasswordHash, &user.Email, + &user.Role, + &workspaceID, + &user.IsActive, + &user.MustChangePassword, &user.RevokedAfter, &user.CreatedAt, &user.UpdatedAt, ) + // Handle NULL workspace_id + if workspaceID.Valid { + user.WorkspaceID = workspaceID.String + } else { + user.WorkspaceID = "" + } + if err == sql.ErrNoRows { return nil, entity.ErrUserNotFound } @@ -80,30 +102,50 @@ func (r *UserRepository) GetByID(ctx context.Context, id string) (*entity.User, // GetByUsername 根据用户名获取用户 func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*entity.User, error) { + log.Printf("[DEBUG] GetByUsername called with username: %q", username) query := ` - SELECT id, username, password_hash, email, revoked_after, created_at, updated_at + SELECT id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at FROM users WHERE username = $1 ` + log.Printf("[DEBUG] Executing query: %s with param: %s", query, username) + user := &entity.User{} + var workspaceID sql.NullString err := r.db.conn.QueryRowContext(ctx, query, username).Scan( &user.ID, &user.Username, &user.PasswordHash, &user.Email, + &user.Role, + &workspaceID, + &user.IsActive, + &user.MustChangePassword, &user.RevokedAfter, &user.CreatedAt, &user.UpdatedAt, ) + // Handle NULL workspace_id + if workspaceID.Valid { + user.WorkspaceID = workspaceID.String + } else { + user.WorkspaceID = "" + } + + log.Printf("[DEBUG] Query result - err: %v", err) if err == sql.ErrNoRows { + log.Printf("[DEBUG] User not found in DB") return nil, entity.ErrUserNotFound } if err != nil { + log.Printf("[DEBUG] Scan error: %v", err) return nil, fmt.Errorf("failed to get user: %w", err) } + log.Printf("[DEBUG] Found user: %+v", user) + return user, nil } @@ -113,14 +155,18 @@ func (r *UserRepository) Update(ctx context.Context, user *entity.User) error { query := ` UPDATE users - SET username = $1, password_hash = $2, email = $3, revoked_after = $4, updated_at = $5 - WHERE id = $6 + SET username = $1, password_hash = $2, email = $3, role = $4, workspace_id = $5, is_active = $6, must_change_password = $7, revoked_after = $8, updated_at = $9 + WHERE id = $10 ` result, err := r.db.conn.ExecContext(ctx, query, user.Username, user.PasswordHash, user.Email, + user.Role, + user.WorkspaceID, + user.IsActive, + user.MustChangePassword, user.RevokedAfter, user.UpdatedAt, user.ID, @@ -166,7 +212,7 @@ func (r *UserRepository) Delete(ctx context.Context, id string) error { // List 列出所有用户 func (r *UserRepository) List(ctx context.Context) ([]*entity.User, error) { query := ` - SELECT id, username, password_hash, email, revoked_after, created_at, updated_at + SELECT id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at FROM users ORDER BY created_at DESC ` @@ -185,6 +231,98 @@ func (r *UserRepository) List(ctx context.Context) ([]*entity.User, error) { &user.Username, &user.PasswordHash, &user.Email, + &user.Role, + &user.WorkspaceID, + &user.IsActive, + &user.MustChangePassword, + &user.RevokedAfter, + &user.CreatedAt, + &user.UpdatedAt, + ) + if err != nil { + return nil, fmt.Errorf("failed to scan user: %w", err) + } + users = append(users, user) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows iteration error: %w", err) + } + + return users, nil +} + +// ListByWorkspace 列出指定 workspace 的用户 +func (r *UserRepository) ListByWorkspace(ctx context.Context, workspaceID string) ([]*entity.User, error) { + query := ` + SELECT id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at + FROM users + WHERE workspace_id = $1 + ORDER BY created_at DESC + ` + + rows, err := r.db.conn.QueryContext(ctx, query, workspaceID) + if err != nil { + return nil, fmt.Errorf("failed to list users by workspace: %w", err) + } + defer rows.Close() + + users := make([]*entity.User, 0) + for rows.Next() { + user := &entity.User{} + err := rows.Scan( + &user.ID, + &user.Username, + &user.PasswordHash, + &user.Email, + &user.Role, + &user.WorkspaceID, + &user.IsActive, + &user.MustChangePassword, + &user.RevokedAfter, + &user.CreatedAt, + &user.UpdatedAt, + ) + if err != nil { + return nil, fmt.Errorf("failed to scan user: %w", err) + } + users = append(users, user) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows iteration error: %w", err) + } + + return users, nil +} + +// ListActive 仅列出活跃用户 +func (r *UserRepository) ListActive(ctx context.Context) ([]*entity.User, error) { + query := ` + SELECT id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at + FROM users + WHERE is_active = TRUE + ORDER BY created_at DESC + ` + + rows, err := r.db.conn.QueryContext(ctx, query) + if err != nil { + return nil, fmt.Errorf("failed to list active users: %w", err) + } + defer rows.Close() + + users := make([]*entity.User, 0) + for rows.Next() { + user := &entity.User{} + err := rows.Scan( + &user.ID, + &user.Username, + &user.PasswordHash, + &user.Email, + &user.Role, + &user.WorkspaceID, + &user.IsActive, + &user.MustChangePassword, &user.RevokedAfter, &user.CreatedAt, &user.UpdatedAt, diff --git a/backend/internal/adapter/output/persistence/postgres/values_template_repository.go b/backend/internal/adapter/output/persistence/postgres/values_template_repository.go new file mode 100644 index 0000000..cc7f34d --- /dev/null +++ b/backend/internal/adapter/output/persistence/postgres/values_template_repository.go @@ -0,0 +1,287 @@ +package postgres + +import ( + "context" + "database/sql" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/repository" +) + +// ValuesTemplateRepository PostgreSQL Values 模板仓储实现 +type ValuesTemplateRepository struct { + db *DB +} + +// NewValuesTemplateRepository 创建 PostgreSQL Values 模板仓储 +func NewValuesTemplateRepository(db *DB) repository.ValuesTemplateRepository { + return &ValuesTemplateRepository{db: db} +} + +// Create 创建 Values 模板 +func (r *ValuesTemplateRepository) Create(ctx context.Context, template *entity.ValuesTemplate) error { + if template.ID == "" { + template.ID = uuid.New().String() + } + + query := ` + INSERT INTO values_templates (id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + ` + + _, err := r.db.conn.ExecContext(ctx, query, + template.ID, + template.WorkspaceID, + template.OwnerID, + template.ChartReferenceID, + template.Name, + template.Description, + template.ValuesYAML, + template.Version, + template.IsDefault, + template.CreatedAt, + template.UpdatedAt, + ) + + if err != nil { + return fmt.Errorf("failed to create values template: %w", err) + } + + return nil +} + +// GetByID 根据 ID 获取 Values 模板 +func (r *ValuesTemplateRepository) GetByID(ctx context.Context, id string) (*entity.ValuesTemplate, error) { + query := ` + SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at + FROM values_templates + WHERE id = $1 + ` + + template := &entity.ValuesTemplate{} + err := r.db.conn.QueryRowContext(ctx, query, id).Scan( + &template.ID, + &template.WorkspaceID, + &template.OwnerID, + &template.ChartReferenceID, + &template.Name, + &template.Description, + &template.ValuesYAML, + &template.Version, + &template.IsDefault, + &template.CreatedAt, + &template.UpdatedAt, + ) + + if err == sql.ErrNoRows { + return nil, entity.ErrTemplateNotFound + } + if err != nil { + return nil, fmt.Errorf("failed to get values template: %w", err) + } + + return template, nil +} + +// GetByWorkspace 获取 workspace 的所有 Values 模板 +func (r *ValuesTemplateRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.ValuesTemplate, error) { + query := ` + SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at + FROM values_templates + WHERE workspace_id = $1 + ORDER BY chart_reference_id, name, version DESC + ` + + rows, err := r.db.conn.QueryContext(ctx, query, workspaceID) + if err != nil { + return nil, fmt.Errorf("failed to list values templates: %w", err) + } + defer rows.Close() + + return r.scanValuesTemplates(rows) +} + +// GetByChartReference 获取 Chart Reference 的所有 Values 模板 +func (r *ValuesTemplateRepository) GetByChartReference(ctx context.Context, chartRefID string) ([]*entity.ValuesTemplate, error) { + query := ` + SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at + FROM values_templates + WHERE chart_reference_id = $1 + ORDER BY name, version DESC + ` + + rows, err := r.db.conn.QueryContext(ctx, query, chartRefID) + if err != nil { + return nil, fmt.Errorf("failed to list values templates: %w", err) + } + defer rows.Close() + + return r.scanValuesTemplates(rows) +} + +// GetByName 根据名称获取 Values 模板(获取最新版本) +func (r *ValuesTemplateRepository) GetByName(ctx context.Context, workspaceID, chartRefID, name string) (*entity.ValuesTemplate, error) { + query := ` + SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at + FROM values_templates + WHERE workspace_id = $1 AND chart_reference_id = $2 AND name = $3 + ORDER BY version DESC + LIMIT 1 + ` + + template := &entity.ValuesTemplate{} + err := r.db.conn.QueryRowContext(ctx, query, workspaceID, chartRefID, name).Scan( + &template.ID, + &template.WorkspaceID, + &template.OwnerID, + &template.ChartReferenceID, + &template.Name, + &template.Description, + &template.ValuesYAML, + &template.Version, + &template.IsDefault, + &template.CreatedAt, + &template.UpdatedAt, + ) + + if err == sql.ErrNoRows { + return nil, entity.ErrTemplateNotFound + } + if err != nil { + return nil, fmt.Errorf("failed to get values template: %w", err) + } + + return template, nil +} + +// GetHistory 获取模板的版本历史 +func (r *ValuesTemplateRepository) GetHistory(ctx context.Context, chartRefID, name string) ([]*entity.ValuesTemplate, error) { + query := ` + SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at + FROM values_templates + WHERE chart_reference_id = $1 AND name = $2 + ORDER BY version DESC + ` + + rows, err := r.db.conn.QueryContext(ctx, query, chartRefID, name) + if err != nil { + return nil, fmt.Errorf("failed to get values template history: %w", err) + } + defer rows.Close() + + return r.scanValuesTemplates(rows) +} + +// Update 更新 Values 模板(自动递增版本) +func (r *ValuesTemplateRepository) Update(ctx context.Context, template *entity.ValuesTemplate) error { + // 获取当前最大版本号 + var maxVersion int + err := r.db.conn.QueryRowContext(ctx, + "SELECT COALESCE(MAX(version), 0) FROM values_templates WHERE chart_reference_id = $1 AND name = $2", + template.ChartReferenceID, template.Name, + ).Scan(&maxVersion) + + if err != nil { + return fmt.Errorf("failed to get max version: %w", err) + } + + // 生成新 ID 用于新版本 + newID := uuid.New().String() + template.Version = maxVersion + 1 + template.UpdatedAt = time.Now() + template.CreatedAt = time.Now() // 新版本的创建时间 + + query := ` + INSERT INTO values_templates (id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + ` + + _, err = r.db.conn.ExecContext(ctx, query, + newID, + template.WorkspaceID, + template.OwnerID, + template.ChartReferenceID, + template.Name, + template.Description, + template.ValuesYAML, + template.Version, + template.IsDefault, + template.CreatedAt, + template.UpdatedAt, + ) + + if err != nil { + return fmt.Errorf("failed to update values template: %w", err) + } + + return nil +} + +// Delete 删除 Values 模板 +func (r *ValuesTemplateRepository) Delete(ctx context.Context, id string) error { + // 获取模板信息 + template, err := r.GetByID(ctx, id) + if err != nil { + return err + } + + // 删除该名称的所有版本 + query := `DELETE FROM values_templates WHERE chart_reference_id = $1 AND name = $2` + _, err = r.db.conn.ExecContext(ctx, query, template.ChartReferenceID, template.Name) + if err != nil { + return fmt.Errorf("failed to delete values template: %w", err) + } + + return nil +} + +// List 列出所有 Values 模板(管理员用) +func (r *ValuesTemplateRepository) List(ctx context.Context) ([]*entity.ValuesTemplate, error) { + query := ` + SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at + FROM values_templates + ORDER BY workspace_id, chart_reference_id, name, version DESC + ` + + rows, err := r.db.conn.QueryContext(ctx, query) + if err != nil { + return nil, fmt.Errorf("failed to list values templates: %w", err) + } + defer rows.Close() + + return r.scanValuesTemplates(rows) +} + +// scanValuesTemplates 扫描多行结果 +func (r *ValuesTemplateRepository) scanValuesTemplates(rows *sql.Rows) ([]*entity.ValuesTemplate, error) { + templates := make([]*entity.ValuesTemplate, 0) + for rows.Next() { + template := &entity.ValuesTemplate{} + err := rows.Scan( + &template.ID, + &template.WorkspaceID, + &template.OwnerID, + &template.ChartReferenceID, + &template.Name, + &template.Description, + &template.ValuesYAML, + &template.Version, + &template.IsDefault, + &template.CreatedAt, + &template.UpdatedAt, + ) + if err != nil { + return nil, fmt.Errorf("failed to scan values template: %w", err) + } + templates = append(templates, template) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows iteration error: %w", err) + } + + return templates, nil +} \ No newline at end of file diff --git a/backend/internal/adapter/output/persistence/postgres/workspace_repository.go b/backend/internal/adapter/output/persistence/postgres/workspace_repository.go new file mode 100644 index 0000000..449bd79 --- /dev/null +++ b/backend/internal/adapter/output/persistence/postgres/workspace_repository.go @@ -0,0 +1,197 @@ +package postgres + +import ( + "context" + "database/sql" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/repository" +) + +// WorkspaceRepository PostgreSQL Workspace 仓储实现 +type WorkspaceRepository struct { + db *DB +} + +// NewWorkspaceRepository 创建 PostgreSQL Workspace 仓储 +func NewWorkspaceRepository(db *DB) repository.WorkspaceRepository { + return &WorkspaceRepository{db: db} +} + +// Create 创建 Workspace +func (r *WorkspaceRepository) Create(ctx context.Context, workspace *entity.Workspace) error { + if workspace.ID == "" { + workspace.ID = uuid.New().String() + } + + query := ` + INSERT INTO workspaces (id, name, description, created_by, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6) + ` + + _, err := r.db.conn.ExecContext(ctx, query, + workspace.ID, + workspace.Name, + workspace.Description, + workspace.CreatedBy, + workspace.CreatedAt, + workspace.UpdatedAt, + ) + + if err != nil { + return fmt.Errorf("failed to create workspace: %w", err) + } + + return nil +} + +// GetByID 根据 ID 获取 Workspace +func (r *WorkspaceRepository) GetByID(ctx context.Context, id string) (*entity.Workspace, error) { + query := ` + SELECT id, name, description, created_by, created_at, updated_at + FROM workspaces + WHERE id = $1 + ` + + workspace := &entity.Workspace{} + err := r.db.conn.QueryRowContext(ctx, query, id).Scan( + &workspace.ID, + &workspace.Name, + &workspace.Description, + &workspace.CreatedBy, + &workspace.CreatedAt, + &workspace.UpdatedAt, + ) + + if err == sql.ErrNoRows { + return nil, entity.ErrWorkspaceNotFound + } + if err != nil { + return nil, fmt.Errorf("failed to get workspace: %w", err) + } + + return workspace, nil +} + +// GetByName 根据名称获取 Workspace +func (r *WorkspaceRepository) GetByName(ctx context.Context, name string) (*entity.Workspace, error) { + query := ` + SELECT id, name, description, created_by, created_at, updated_at + FROM workspaces + WHERE name = $1 + ` + + workspace := &entity.Workspace{} + err := r.db.conn.QueryRowContext(ctx, query, name).Scan( + &workspace.ID, + &workspace.Name, + &workspace.Description, + &workspace.CreatedBy, + &workspace.CreatedAt, + &workspace.UpdatedAt, + ) + + if err == sql.ErrNoRows { + return nil, entity.ErrWorkspaceNotFound + } + if err != nil { + return nil, fmt.Errorf("failed to get workspace: %w", err) + } + + return workspace, nil +} + +// Update 更新 Workspace +func (r *WorkspaceRepository) Update(ctx context.Context, workspace *entity.Workspace) error { + workspace.UpdatedAt = time.Now() + + query := ` + UPDATE workspaces + SET name = $1, description = $2, updated_at = $3 + WHERE id = $4 + ` + + result, err := r.db.conn.ExecContext(ctx, query, + workspace.Name, + workspace.Description, + workspace.UpdatedAt, + workspace.ID, + ) + + if err != nil { + return fmt.Errorf("failed to update workspace: %w", err) + } + + rows, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get affected rows: %w", err) + } + + if rows == 0 { + return entity.ErrWorkspaceNotFound + } + + return nil +} + +// Delete 删除 Workspace +func (r *WorkspaceRepository) Delete(ctx context.Context, id string) error { + query := `DELETE FROM workspaces WHERE id = $1` + + result, err := r.db.conn.ExecContext(ctx, query, id) + if err != nil { + return fmt.Errorf("failed to delete workspace: %w", err) + } + + rows, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get affected rows: %w", err) + } + + if rows == 0 { + return entity.ErrWorkspaceNotFound + } + + return nil +} + +// List 列出所有 Workspace +func (r *WorkspaceRepository) List(ctx context.Context) ([]*entity.Workspace, error) { + query := ` + SELECT id, name, description, created_by, created_at, updated_at + FROM workspaces + ORDER BY created_at DESC + ` + + rows, err := r.db.conn.QueryContext(ctx, query) + if err != nil { + return nil, fmt.Errorf("failed to list workspaces: %w", err) + } + defer rows.Close() + + workspaces := make([]*entity.Workspace, 0) + for rows.Next() { + workspace := &entity.Workspace{} + err := rows.Scan( + &workspace.ID, + &workspace.Name, + &workspace.Description, + &workspace.CreatedBy, + &workspace.CreatedAt, + &workspace.UpdatedAt, + ) + if err != nil { + return nil, fmt.Errorf("failed to scan workspace: %w", err) + } + workspaces = append(workspaces, workspace) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows iteration error: %w", err) + } + + return workspaces, nil +} \ No newline at end of file diff --git a/backend/internal/domain/entity/audit_log.go b/backend/internal/domain/entity/audit_log.go new file mode 100644 index 0000000..c8d457c --- /dev/null +++ b/backend/internal/domain/entity/audit_log.go @@ -0,0 +1,90 @@ +package entity + +import ( + "encoding/json" + "time" +) + +// AuditAction 审计操作类型 +type AuditAction string + +const ( + AuditActionCreate AuditAction = "create" + AuditActionUpdate AuditAction = "update" + AuditActionDelete AuditAction = "delete" + AuditActionDeploy AuditAction = "deploy" + AuditActionScale AuditAction = "scale" + AuditActionLogin AuditAction = "login" + AuditActionLogout AuditAction = "logout" + AuditActionChangePassword AuditAction = "change_password" +) + +// AuditResourceType 审计资源类型 +type AuditResourceType string + +const ( + AuditResourceUser AuditResourceType = "user" + AuditResourceWorkspace AuditResourceType = "workspace" + AuditResourceQuota AuditResourceType = "quota" + AuditResourceCluster AuditResourceType = "cluster" + AuditResourceRegistry AuditResourceType = "registry" + AuditResourceInstance AuditResourceType = "instance" + AuditResourceStorage AuditResourceType = "storage" + AuditResourceTemplate AuditResourceType = "template" +) + +// AuditLog 审计日志实体 +type AuditLog struct { + ID string + WorkspaceID string + UserID string + Action AuditAction + ResourceType AuditResourceType + ResourceID string + ResourceName string + Details map[string]interface{} + IPAddress string + UserAgent string + CreatedAt time.Time +} + +// NewAuditLog 创建新审计日志 +func NewAuditLog(workspaceID, userID string, action AuditAction, resourceType AuditResourceType) *AuditLog { + now := time.Now() + return &AuditLog{ + WorkspaceID: workspaceID, + UserID: userID, + Action: action, + ResourceType: resourceType, + CreatedAt: now, + } +} + +// SetResource 设置关联资源 +func (a *AuditLog) SetResource(resourceID, resourceName string) { + a.ResourceID = resourceID + a.ResourceName = resourceName +} + +// SetDetails 设置详细信息 +func (a *AuditLog) SetDetails(details map[string]interface{}) { + a.Details = details +} + +// SetClientInfo 设置客户端信息 +func (a *AuditLog) SetClientInfo(ipAddress, userAgent string) { + a.IPAddress = ipAddress + a.UserAgent = userAgent +} + +// DetailsJSON 将详情转为 JSON 字符串 +func (a *AuditLog) DetailsJSON() (string, error) { + if a.Details == nil { + return "{}", nil + } + data, err := json.Marshal(a.Details) + if err != nil { + return "", err + } + return string(data), nil +} \ No newline at end of file diff --git a/backend/internal/domain/entity/chart_reference.go b/backend/internal/domain/entity/chart_reference.go new file mode 100644 index 0000000..d301c64 --- /dev/null +++ b/backend/internal/domain/entity/chart_reference.go @@ -0,0 +1,41 @@ +package entity + +import ( + "time" +) + +// ChartReference Chart 引用实体 +type ChartReference struct { + ID string + WorkspaceID string + RegistryID string + Repository string + ChartName string + Description string + IsEnabled bool + CreatedAt time.Time + UpdatedAt time.Time +} + +// NewChartReference 创建新 Chart 引用 +func NewChartReference(workspaceID, registryID, repository, chartName, description string) *ChartReference { + now := time.Now() + return &ChartReference{ + WorkspaceID: workspaceID, + RegistryID: registryID, + Repository: repository, + ChartName: chartName, + Description: description, + IsEnabled: true, + CreatedAt: now, + UpdatedAt: now, + } +} + +// Validate 验证 Chart 引用数据 +func (c *ChartReference) Validate() error { + if c.Repository == "" { + return ErrInvalidChart + } + return nil +} \ No newline at end of file diff --git a/backend/internal/domain/entity/cluster.go b/backend/internal/domain/entity/cluster.go index 486031a..a0bed32 100644 --- a/backend/internal/domain/entity/cluster.go +++ b/backend/internal/domain/entity/cluster.go @@ -4,28 +4,49 @@ import ( "time" ) +// IsolationMode 集群隔离模式 +type IsolationMode string + +const ( + IsolationModeNamespace IsolationMode = "namespace" // 共享集群模式,多 workspace 使用不同 namespace + IsolationModeCluster IsolationMode = "cluster" // 私有集群模式,每个 workspace 独立集群 +) + // Cluster Kubernetes 集群领域实体 type Cluster struct { - ID string - Name string - Host string // Kubernetes API Server URL - CAData string // Base64 encoded CA certificate - CertData string // Base64 encoded client certificate - KeyData string // Base64 encoded client key - Token string // Bearer token (alternative to cert auth) - Description string - CreatedAt time.Time - UpdatedAt time.Time + ID string + WorkspaceID string // 所属 workspace,NULL 表示全局共享 + OwnerID string // 创建者用户 ID + Name string + Host string // Kubernetes API Server URL + CAData string // Base64 encoded CA certificate + CertData string // Base64 encoded client certificate + KeyData string // Base64 encoded client key + Token string // Bearer token (alternative to cert auth) + Description string + + // 隔离模式 + IsolationMode IsolationMode // 'namespace' | 'cluster' + DefaultNamespace string // 当 isolation_mode=namespace 时的默认 namespace 前缀 + + IsShared bool // 是否为共享集群(admin 创建供多 workspace 使用) + CreatedAt time.Time + UpdatedAt time.Time } // NewCluster 创建新集群 -func NewCluster(name, host string) *Cluster { +func NewCluster(workspaceID, ownerID, name, host string) *Cluster { now := time.Now() return &Cluster{ - Name: name, - Host: host, - CreatedAt: now, - UpdatedAt: now, + WorkspaceID: workspaceID, + OwnerID: ownerID, + Name: name, + Host: host, + IsolationMode: IsolationModeNamespace, // 默认 namespace 隔离模式 + DefaultNamespace: workspaceID, // 默认使用 workspace ID 作为 namespace 前缀 + IsShared: false, + CreatedAt: now, + UpdatedAt: now, } } @@ -63,11 +84,35 @@ func (c *Cluster) Validate() error { if c.Host == "" { return ErrInvalidClusterHost } - // 必须有认证方式:证书或 Token - if (c.CertData == "" || c.KeyData == "") && c.Token == "" { - return ErrInvalidClusterAuth + + // 检查是否有 kubeconfig 格式(完整的 kubeconfig 在 CAData 中) + hasKubeconfig := len(c.CAData) > 100 && (c.CAData[:11] == "apiVersion:" || c.CAData[:5] == "kind:") + + // 认证方式:证书、Token、kubeconfig 或空(使用本地 kubeconfig) + hasCertAuth := c.CertData != "" && c.KeyData != "" + hasToken := c.Token != "" + hasNoAuth := c.CertData == "" && c.KeyData == "" && c.Token == "" + + // 如果有 kubeconfig 格式,或有证书,或有 token,或没有凭证(依赖 TestConnection 使用本地 kubeconfig),都是有效的 + if hasKubeconfig || hasCertAuth || hasToken || hasNoAuth { + return nil } - return nil + + return ErrInvalidClusterAuth +} + +// GetNamespace 获取部署用的 namespace +// namespace 隔离模式: {workspace_id}-{instance_name} 或 {default_namespace}-{username} +// cluster 隔离模式: 使用 workspace 的默认 namespace +func (c *Cluster) GetNamespace(workspaceName, instanceName string) string { + if c.IsolationMode == IsolationModeCluster { + return c.DefaultNamespace + } + // namespace 隔离模式 + if c.DefaultNamespace != "" { + return c.DefaultNamespace + "-" + instanceName + } + return workspaceName + "-" + instanceName } // GetKubeConfig 生成 kubeconfig 内容 diff --git a/backend/internal/domain/entity/errors.go b/backend/internal/domain/entity/errors.go index 91a65ad..4c0036c 100644 --- a/backend/internal/domain/entity/errors.go +++ b/backend/internal/domain/entity/errors.go @@ -37,4 +37,32 @@ var ( ErrArtifactNotFound = errors.New("artifact not found") ErrRepositoryNotFound = errors.New("repository not found") ErrValuesSchemaNotFound = errors.New("values schema not found") + ErrValuesNotFound = errors.New("values not found") + + // Workspace errors + ErrInvalidWorkspaceName = errors.New("invalid workspace name") + ErrWorkspaceNotFound = errors.New("workspace not found") + ErrWorkspaceExists = errors.New("workspace already exists") + + // Quota errors + ErrQuotaExceeded = errors.New("quota exceeded") + ErrInvalidQuota = errors.New("invalid quota") + + // Storage errors + ErrInvalidStorageName = errors.New("invalid storage name") + ErrStorageNotFound = errors.New("storage not found") + ErrStorageExists = errors.New("storage already exists") + + // Chart Reference errors + ErrInvalidChartReferenceName = errors.New("invalid chart reference name") + ErrChartReferenceNotFound = errors.New("chart reference not found") + ErrChartReferenceExists = errors.New("chart reference already exists") + + // Template errors + ErrInvalidTemplateName = errors.New("invalid template name") + ErrTemplateNotFound = errors.New("template not found") + ErrTemplateExists = errors.New("template already exists") + + // Permission errors + ErrPermissionDenied = errors.New("permission denied") ) diff --git a/backend/internal/domain/entity/instance.go b/backend/internal/domain/entity/instance.go index 1bc3c37..e20c6aa 100644 --- a/backend/internal/domain/entity/instance.go +++ b/backend/internal/domain/entity/instance.go @@ -1,7 +1,9 @@ package entity import ( + "strings" "time" + "unicode" ) // InstanceStatus 实例状态 @@ -33,43 +35,65 @@ const ( // Instance Helm 应用实例领域实体 type Instance struct { - ID string - ClusterID string - Name string // Helm Release Name - Namespace string - RegistryID string - Repository string // OCI Repository (e.g., charts/app) - Chart string // Chart Name - Version string // Chart Version - Description string - Values map[string]interface{} // Helm Values (JSON) - ValuesYAML string // Helm Values (YAML format) - Status InstanceStatus - StatusReason string - LastOperation InstanceOperation - LastError string - Revision int // Helm Release Revision - CreatedAt time.Time - UpdatedAt time.Time + ID string + WorkspaceID string // 所属 workspace + OwnerID string // 创建者用户 ID + ClusterID string + RegistryID string + ChartReferenceID string // 引用的 Chart 引用 + ValuesTemplateID string // 使用的 Values 模板 + + Name string // Helm Release Name + Namespace string + Repository string // OCI Repository (e.g., charts/app) + Chart string // Chart Name + Version string // Chart Version + Description string + Values map[string]interface{} // Helm Values (JSON) + ValuesYAML string // Helm Values (YAML format) + UserOverrideYAML string // 用户额外覆盖的配置 + + Status InstanceStatus + StatusReason string + LastOperation InstanceOperation + LastError string + Revision int // Helm Release Revision + + // 资源使用统计(Helm 安装时从集群获取并更新) + CPURequested float64 // CPU 请求量 (cores) + MemoryRequested string // 内存请求量 (e.g., "2Gi") + GPURequested float64 // GPU 请求量 (cards) + GPUMemoryRequested string // GPU 内存请求量 (e.g., "16Gi") + + CreatedAt time.Time + UpdatedAt time.Time } // NewInstance 创建新实例 -func NewInstance(clusterID, name, namespace, registryID, repository, chart, version string) *Instance { +func NewInstance(workspaceID, ownerID, clusterID, registryID, chartReferenceID, valuesTemplateID, name, namespace, repository, chart, version string) *Instance { now := time.Now() return &Instance{ - ClusterID: clusterID, - Name: name, - Namespace: namespace, - RegistryID: registryID, - Repository: repository, - Chart: chart, - Version: version, - Status: StatusPending, - StatusReason: "Pending install", - LastOperation: OperationInstall, - Revision: 1, - CreatedAt: now, - UpdatedAt: now, + WorkspaceID: workspaceID, + OwnerID: ownerID, + ClusterID: clusterID, + RegistryID: registryID, + ChartReferenceID: chartReferenceID, + ValuesTemplateID: valuesTemplateID, + Name: name, + Namespace: namespace, + Repository: repository, + Chart: chart, + Version: version, + Status: StatusPending, + StatusReason: "Pending install", + LastOperation: OperationInstall, + Revision: 1, + CPURequested: 0, + MemoryRequested: "0Mi", + GPURequested: 0, + GPUMemoryRequested: "0Mi", + CreatedAt: now, + UpdatedAt: now, } } @@ -154,13 +178,43 @@ func (i *Instance) Upgrade(version string, values map[string]interface{}) { i.BeginOperation(OperationUpgrade, "Pending upgrade") } +// ValidateReleaseName 验证 Helm Release 名称是否符合 RFC 1123 DNS 子域名规范 +// Helm release 名称必须: +// - 只能包含小写字母(a-z)、数字(0-9)和连字符(-) +// - 不能以连字符开头或结尾 +// - 长度不超过 53 个字符 +func ValidateReleaseName(name string) error { + if name == "" { + return ErrInvalidInstanceName + } + + // 检查长度(RFC 1123 DNS 子域名最大长度为 63,但 Helm 限制为 53) + if len(name) > 53 { + return ErrInvalidInstanceName + } + + // 不能以连字符开头或结尾 + if strings.HasPrefix(name, "-") || strings.HasSuffix(name, "-") { + return ErrInvalidInstanceName + } + + // 只能包含小写字母、数字和连字符 + for _, r := range name { + if !(unicode.IsLower(r) || unicode.IsDigit(r) || r == '-') { + return ErrInvalidInstanceName + } + } + + return nil +} + // Validate 验证实例配置 func (i *Instance) Validate() error { if i.ClusterID == "" { return ErrInvalidClusterID } - if i.Name == "" { - return ErrInvalidInstanceName + if err := ValidateReleaseName(i.Name); err != nil { + return err } if i.Namespace == "" { return ErrInvalidNamespace diff --git a/backend/internal/domain/entity/quota.go b/backend/internal/domain/entity/quota.go new file mode 100644 index 0000000..b206834 --- /dev/null +++ b/backend/internal/domain/entity/quota.go @@ -0,0 +1,79 @@ +package entity + +import ( + "time" +) + +// ResourceType 资源类型 +type ResourceType string + +const ( + ResourceCPU ResourceType = "cpu" + ResourceGPU ResourceType = "gpu" + ResourceGPUMemory ResourceType = "gpu_memory" +) + +// WorkspaceQuota 工作空间配额实体 +type WorkspaceQuota struct { + ID string + WorkspaceID string + ResourceType ResourceType + HardLimit float64 // 硬限制(0表示无限制) + SoftLimit float64 // 软限制(警告阈值) + Used float64 // 当前使用量 + CreatedAt time.Time + UpdatedAt time.Time +} + +// NewWorkspaceQuota 创建新配额 +func NewWorkspaceQuota(workspaceID string, resourceType ResourceType, hardLimit, softLimit float64) *WorkspaceQuota { + now := time.Now() + return &WorkspaceQuota{ + WorkspaceID: workspaceID, + ResourceType: resourceType, + HardLimit: hardLimit, + SoftLimit: softLimit, + Used: 0, + CreatedAt: now, + UpdatedAt: now, + } +} + +// CanAllocate 检查是否可以分配指定资源量 +func (q *WorkspaceQuota) CanAllocate(amount float64) bool { + if q.HardLimit == 0 { + return true // 无限制 + } + return q.Used+amount <= q.HardLimit +} + +// Allocate 分配资源 +func (q *WorkspaceQuota) Allocate(amount float64) { + q.Used += amount + q.UpdatedAt = time.Now() +} + +// Release 释放资源 +func (q *WorkspaceQuota) Release(amount float64) { + q.Used -= amount + if q.Used < 0 { + q.Used = 0 + } + q.UpdatedAt = time.Now() +} + +// IsOverLimit 检查是否超过硬限制 +func (q *WorkspaceQuota) IsOverLimit() bool { + if q.HardLimit == 0 { + return false + } + return q.Used > q.HardLimit +} + +// IsOverSoftLimit 检查是否超过软限制 +func (q *WorkspaceQuota) IsOverSoftLimit() bool { + if q.SoftLimit == 0 { + return false + } + return q.Used > q.SoftLimit +} \ No newline at end of file diff --git a/backend/internal/domain/entity/registry.go b/backend/internal/domain/entity/registry.go index 1b7ee56..4956b3b 100644 --- a/backend/internal/domain/entity/registry.go +++ b/backend/internal/domain/entity/registry.go @@ -7,24 +7,30 @@ import ( // Registry OCI Registry 领域实体 type Registry struct { ID string + WorkspaceID string // 所属 workspace,NULL 表示全局共享 + OwnerID string // 创建者用户 ID Name string URL string Description string Username string Password string - Insecure bool // 是否跳过 TLS 验证 + Insecure bool // 是否跳过 TLS 验证 + IsShared bool // 是否为共享 Registry(admin 创建供多 workspace 使用) CreatedAt time.Time UpdatedAt time.Time } // NewRegistry 创建新 Registry -func NewRegistry(name, url string) *Registry { +func NewRegistry(workspaceID, ownerID, name, url string) *Registry { now := time.Now() return &Registry{ - Name: name, - URL: url, - CreatedAt: now, - UpdatedAt: now, + WorkspaceID: workspaceID, + OwnerID: ownerID, + Name: name, + URL: url, + IsShared: false, + CreatedAt: now, + UpdatedAt: now, } } diff --git a/backend/internal/domain/entity/storage.go b/backend/internal/domain/entity/storage.go new file mode 100644 index 0000000..740d745 --- /dev/null +++ b/backend/internal/domain/entity/storage.go @@ -0,0 +1,98 @@ +package entity + +import ( + "encoding/json" + "time" +) + +// StorageType 存储类型 +type StorageType string + +const ( + StorageTypeNFS StorageType = "nfs" + StorageTypePV StorageType = "pv" + StorageTypeHostPath StorageType = "hostPath" +) + +// StorageConfig 存储配置 +type StorageConfig struct { + NFS *NFSConfig `json:"nfs,omitempty"` + PV *PVConfig `json:"pv,omitempty"` + HostPath *HostPathConfig `json:"hostPath,omitempty"` +} + +// NFSConfig NFS 配置 +type NFSConfig struct { + Server string `json:"server"` + Path string `json:"path"` +} + +// PVConfig PV 配置 +type PVConfig struct { + StorageClassName string `json:"storageClassName"` + Capacity string `json:"capacity"` + AccessModes []string `json:"accessModes"` +} + +// HostPathConfig HostPath 配置 +type HostPathConfig struct { + Path string `json:"path"` +} + +// StorageBackend 存储后端实体 +type StorageBackend struct { + ID string + WorkspaceID string + OwnerID string + Name string + Type StorageType + Config StorageConfig + Description string + IsDefault bool + IsShared bool + CreatedAt time.Time + UpdatedAt time.Time +} + +// NewStorageBackend 创建新存储后端 +func NewStorageBackend(workspaceID, ownerID, name string, storageType StorageType, config StorageConfig) *StorageBackend { + now := time.Now() + return &StorageBackend{ + WorkspaceID: workspaceID, + OwnerID: ownerID, + Name: name, + Type: storageType, + Config: config, + IsDefault: false, + IsShared: false, + CreatedAt: now, + UpdatedAt: now, + } +} + +// Validate 验证存储后端数据 +func (s *StorageBackend) Validate() error { + if s.Name == "" { + return ErrInvalidStorageName + } + if s.Type == "" { + return ErrInvalidStorageName + } + return nil +} + +// ConfigJSON 将配置转为 JSON 字符串 +func (s *StorageBackend) ConfigJSON() (string, error) { + data, err := json.Marshal(s.Config) + if err != nil { + return "", err + } + return string(data), nil +} + +// ParseConfigJSON 从 JSON 解析配置 +func ParseConfigJSON(jsonStr string) (*StorageConfig, error) { + var config StorageConfig + err := json.Unmarshal([]byte(jsonStr), &config) + return &config, err +} \ No newline at end of file diff --git a/backend/internal/domain/entity/user.go b/backend/internal/domain/entity/user.go index bf6387f..070d185 100644 --- a/backend/internal/domain/entity/user.go +++ b/backend/internal/domain/entity/user.go @@ -4,30 +4,58 @@ import ( "time" ) +// UserRole 用户角色 +type UserRole string + +const ( + RoleAdmin UserRole = "admin" + RoleUser UserRole = "user" +) + // User 用户领域实体 type User struct { - ID string - Username string - PasswordHash string - Email string - RevokedAfter time.Time // 全局 Token 撤销时间 - CreatedAt time.Time - UpdatedAt time.Time + ID string + Username string + PasswordHash string + Email string + Role UserRole // 用户角色: admin, user + WorkspaceID string // 所属工作空间,admin 为空表示全局 + IsActive bool // 账户是否激活 + MustChangePassword bool // 首次登录必须修改密码 + RevokedAfter time.Time // 全局 Token 撤销时间 + CreatedAt time.Time + UpdatedAt time.Time } // NewUser 创建新用户 func NewUser(username, passwordHash, email string) *User { now := time.Now() return &User{ - Username: username, - PasswordHash: passwordHash, - Email: email, - RevokedAfter: time.Unix(0, 0), // 初始值:1970-01-01 - CreatedAt: now, - UpdatedAt: now, + Username: username, + PasswordHash: passwordHash, + Email: email, + Role: RoleUser, // 默认普通用户 + IsActive: true, + MustChangePassword: true, // 首次登录必须修改密码 + RevokedAfter: time.Unix(0, 0), // 初始值:1970-01-01 + CreatedAt: now, + UpdatedAt: now, } } +// IsAdmin 判断是否为管理员 +func (u *User) IsAdmin() bool { + return u.Role == RoleAdmin +} + +// CanAccessWorkspace 检查是否可以访问指定工作空间 +func (u *User) CanAccessWorkspace(workspaceID string) bool { + if u.IsAdmin() { + return true // Admin 可以访问所有工作空间 + } + return u.WorkspaceID == workspaceID +} + // UpdatePassword 更新密码(会触发全局登出) func (u *User) UpdatePassword(newPasswordHash string) { u.PasswordHash = newPasswordHash diff --git a/backend/internal/domain/entity/values_template.go b/backend/internal/domain/entity/values_template.go new file mode 100644 index 0000000..87d03dd --- /dev/null +++ b/backend/internal/domain/entity/values_template.go @@ -0,0 +1,83 @@ +package entity + +import ( + "time" +) + +// ValuesTemplate Values 模板实体(带版本管理) +type ValuesTemplate struct { + ID string + WorkspaceID string + OwnerID string + ChartReferenceID string + Name string + Description string + ValuesYAML string + Version int // 模板版本号 + IsDefault bool + CreatedAt time.Time + UpdatedAt time.Time +} + +// NewValuesTemplate 创建新 Values 模板 +func NewValuesTemplate(workspaceID, ownerID, chartReferenceID, name, valuesYAML string) *ValuesTemplate { + now := time.Now() + return &ValuesTemplate{ + WorkspaceID: workspaceID, + OwnerID: ownerID, + ChartReferenceID: chartReferenceID, + Name: name, + ValuesYAML: valuesYAML, + Version: 1, + IsDefault: false, + CreatedAt: now, + UpdatedAt: now, + } +} + +// Validate 验证 Values 模板数据 +func (v *ValuesTemplate) Validate() error { + if v.Name == "" { + return ErrInvalidTemplateName + } + if v.ValuesYAML == "" { + return ErrInvalidTemplateName + } + return nil +} + +// IncrementVersion 递增版本号 +func (v *ValuesTemplate) IncrementVersion() { + v.Version++ + v.UpdatedAt = time.Now() +} + +// UserConfigOverride 用户配置覆盖实体 +type UserConfigOverride struct { + ID string + WorkspaceID string + UserID string + TargetType string // 'storage', 'template', 'global' + TargetID string + Config map[string]interface{} + Priority int + IsActive bool + CreatedAt time.Time + UpdatedAt time.Time +} + +// NewUserConfigOverride 创建新用户配置覆盖 +func NewUserConfigOverride(workspaceID, userID, targetType, targetID string, config map[string]interface{}) *UserConfigOverride { + now := time.Now() + return &UserConfigOverride{ + WorkspaceID: workspaceID, + UserID: userID, + TargetType: targetType, + TargetID: targetID, + Config: config, + Priority: 0, + IsActive: true, + CreatedAt: now, + UpdatedAt: now, + } +} \ No newline at end of file diff --git a/backend/internal/domain/entity/workspace.go b/backend/internal/domain/entity/workspace.go new file mode 100644 index 0000000..b18c802 --- /dev/null +++ b/backend/internal/domain/entity/workspace.go @@ -0,0 +1,35 @@ +package entity + +import ( + "time" +) + +// Workspace 工作空间实体 +type Workspace struct { + ID string + Name string + Description string + CreatedBy string // 创建者用户 ID + CreatedAt time.Time + UpdatedAt time.Time +} + +// NewWorkspace 创建新工作空间 +func NewWorkspace(name, description, createdBy string) *Workspace { + now := time.Now() + return &Workspace{ + Name: name, + Description: description, + CreatedBy: createdBy, + CreatedAt: now, + UpdatedAt: now, + } +} + +// Validate 验证工作空间数据 +func (w *Workspace) Validate() error { + if w.Name == "" { + return ErrInvalidWorkspaceName + } + return nil +} \ No newline at end of file diff --git a/backend/internal/domain/repository/audit_log_repository.go b/backend/internal/domain/repository/audit_log_repository.go new file mode 100644 index 0000000..c323755 --- /dev/null +++ b/backend/internal/domain/repository/audit_log_repository.go @@ -0,0 +1,27 @@ +package repository + +import ( + "context" + "github.com/ocdp/cluster-service/internal/domain/entity" +) + +// AuditLogRepository 审计日志仓储接口 +type AuditLogRepository interface { + // Create 创建审计日志 + Create(ctx context.Context, log *entity.AuditLog) error + + // GetByWorkspace 获取 workspace 的审计日志 + GetByWorkspace(ctx context.Context, workspaceID string, limit int) ([]*entity.AuditLog, error) + + // GetByUser 获取用户的审计日志 + GetByUser(ctx context.Context, userID string, limit int) ([]*entity.AuditLog, error) + + // GetByResource 获取资源的审计日志 + GetByResource(ctx context.Context, resourceType entity.AuditResourceType, resourceID string, limit int) ([]*entity.AuditLog, error) + + // List 列出审计日志(分页) + List(ctx context.Context, limit, offset int) ([]*entity.AuditLog, error) + + // DeleteByWorkspace 删除 workspace 的审计日志 + DeleteByWorkspace(ctx context.Context, workspaceID string) error +} \ No newline at end of file diff --git a/backend/internal/domain/repository/chart_reference_repository.go b/backend/internal/domain/repository/chart_reference_repository.go new file mode 100644 index 0000000..685ef83 --- /dev/null +++ b/backend/internal/domain/repository/chart_reference_repository.go @@ -0,0 +1,33 @@ +package repository + +import ( + "context" + "github.com/ocdp/cluster-service/internal/domain/entity" +) + +// ChartReferenceRepository Chart 引用仓储接口 +type ChartReferenceRepository interface { + // Create 创建 Chart 引用 + Create(ctx context.Context, chartRef *entity.ChartReference) error + + // GetByID 根据 ID 获取 Chart 引用 + GetByID(ctx context.Context, id string) (*entity.ChartReference, error) + + // GetByWorkspace 获取 workspace 的所有 Chart 引用 + GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.ChartReference, error) + + // GetByRegistry 获取 registry 的所有 Chart 引用 + GetByRegistry(ctx context.Context, registryID string) ([]*entity.ChartReference, error) + + // GetByName 根据名称获取 Chart 引用 + GetByName(ctx context.Context, workspaceID, chartName string) (*entity.ChartReference, error) + + // Update 更新 Chart 引用 + Update(ctx context.Context, chartRef *entity.ChartReference) error + + // Delete 删除 Chart 引用 + Delete(ctx context.Context, id string) error + + // List 列出所有 Chart 引用(管理员用) + List(ctx context.Context) ([]*entity.ChartReference, error) +} \ No newline at end of file diff --git a/backend/internal/domain/repository/cluster_repository.go b/backend/internal/domain/repository/cluster_repository.go index b598025..8817223 100644 --- a/backend/internal/domain/repository/cluster_repository.go +++ b/backend/internal/domain/repository/cluster_repository.go @@ -9,20 +9,26 @@ import ( type ClusterRepository interface { // Create 创建集群 Create(ctx context.Context, cluster *entity.Cluster) error - + // GetByID 根据 ID 获取集群 GetByID(ctx context.Context, id string) (*entity.Cluster, error) - + // GetByName 根据名称获取集群 GetByName(ctx context.Context, name string) (*entity.Cluster, error) - + // Update 更新集群 Update(ctx context.Context, cluster *entity.Cluster) error - + // Delete 删除集群 Delete(ctx context.Context, id string) error - + // List 列出所有集群 List(ctx context.Context) ([]*entity.Cluster, error) + + // GetByWorkspace 获取 workspace 的所有集群(包括共享集群) + GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Cluster, error) + + // GetShared 获取所有共享集群 + GetShared(ctx context.Context) ([]*entity.Cluster, error) } diff --git a/backend/internal/domain/repository/instance_repository.go b/backend/internal/domain/repository/instance_repository.go index 9cd955b..673d66b 100644 --- a/backend/internal/domain/repository/instance_repository.go +++ b/backend/internal/domain/repository/instance_repository.go @@ -9,23 +9,26 @@ import ( type InstanceRepository interface { // Create 创建实例 Create(ctx context.Context, instance *entity.Instance) error - + // GetByID 根据 ID 获取实例 GetByID(ctx context.Context, id string) (*entity.Instance, error) - + // GetByClusterAndName 根据集群 ID 和名称获取实例 GetByClusterAndName(ctx context.Context, clusterID, name string) (*entity.Instance, error) - + // Update 更新实例 Update(ctx context.Context, instance *entity.Instance) error - + // Delete 删除实例 Delete(ctx context.Context, id string) error - + // ListByCluster 列出指定集群的所有实例 ListByCluster(ctx context.Context, clusterID string) ([]*entity.Instance, error) - + // List 列出所有实例 List(ctx context.Context) ([]*entity.Instance, error) + + // GetByWorkspace 列出指定工作空间的所有实例(用于配额检查) + GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Instance, error) } diff --git a/backend/internal/domain/repository/oci_client.go b/backend/internal/domain/repository/oci_client.go index 1cd6c9a..d583c5a 100644 --- a/backend/internal/domain/repository/oci_client.go +++ b/backend/internal/domain/repository/oci_client.go @@ -19,7 +19,10 @@ type OCIClient interface { // GetValuesSchema 获取 Helm Chart 的 values schema GetValuesSchema(ctx context.Context, registry *entity.Registry, repository, reference string) (string, error) - + + // GetValues 获取 Helm Chart 的 values.yaml + GetValues(ctx context.Context, registry *entity.Registry, repository, reference string) (string, error) + // PullArtifact 下载 artifact 到本地 PullArtifact(ctx context.Context, registry *entity.Registry, repository, reference, destPath string) error diff --git a/backend/internal/domain/repository/quota_repository.go b/backend/internal/domain/repository/quota_repository.go new file mode 100644 index 0000000..d93e15e --- /dev/null +++ b/backend/internal/domain/repository/quota_repository.go @@ -0,0 +1,30 @@ +package repository + +import ( + "context" + "github.com/ocdp/cluster-service/internal/domain/entity" +) + +// QuotaRepository 配额仓储接口 +type QuotaRepository interface { + // Create 创建配额 + Create(ctx context.Context, quota *entity.WorkspaceQuota) error + + // GetByID 根据 ID 获取配额 + GetByID(ctx context.Context, id string) (*entity.WorkspaceQuota, error) + + // GetByWorkspaceAndType 根据 workspace 和资源类型获取配额 + GetByWorkspaceAndType(ctx context.Context, workspaceID string, resourceType entity.ResourceType) (*entity.WorkspaceQuota, error) + + // GetByWorkspace 获取 workspace 的所有配额 + GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.WorkspaceQuota, error) + + // Update 更新配额 + Update(ctx context.Context, quota *entity.WorkspaceQuota) error + + // Delete 删除配额 + Delete(ctx context.Context, id string) error + + // DeleteByWorkspace 删除 workspace 的所有配额 + DeleteByWorkspace(ctx context.Context, workspaceID string) error +} \ No newline at end of file diff --git a/backend/internal/domain/repository/storage_repository.go b/backend/internal/domain/repository/storage_repository.go new file mode 100644 index 0000000..9454d01 --- /dev/null +++ b/backend/internal/domain/repository/storage_repository.go @@ -0,0 +1,36 @@ +package repository + +import ( + "context" + "github.com/ocdp/cluster-service/internal/domain/entity" +) + +// StorageRepository 存储后端仓储接口 +type StorageRepository interface { + // Create 创建存储后端 + Create(ctx context.Context, storage *entity.StorageBackend) error + + // GetByID 根据 ID 获取存储后端 + GetByID(ctx context.Context, id string) (*entity.StorageBackend, error) + + // GetByName 根据名称获取存储后端 + GetByName(ctx context.Context, workspaceID, name string) (*entity.StorageBackend, error) + + // GetByWorkspace 获取 workspace 的所有存储后端 + GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.StorageBackend, error) + + // GetShared 获取所有共享存储后端 + GetShared(ctx context.Context) ([]*entity.StorageBackend, error) + + // GetDefault 获取 workspace 的默认存储后端 + GetDefault(ctx context.Context, workspaceID string) (*entity.StorageBackend, error) + + // Update 更新存储后端 + Update(ctx context.Context, storage *entity.StorageBackend) error + + // Delete 删除存储后端 + Delete(ctx context.Context, id string) error + + // List 列出所有存储后端(管理员用) + List(ctx context.Context) ([]*entity.StorageBackend, error) +} \ No newline at end of file diff --git a/backend/internal/domain/repository/user_repository.go b/backend/internal/domain/repository/user_repository.go index eff3479..b1c4353 100644 --- a/backend/internal/domain/repository/user_repository.go +++ b/backend/internal/domain/repository/user_repository.go @@ -9,20 +9,26 @@ import ( type UserRepository interface { // Create 创建用户 Create(ctx context.Context, user *entity.User) error - + // GetByID 根据 ID 获取用户 GetByID(ctx context.Context, id string) (*entity.User, error) - + // GetByUsername 根据用户名获取用户 GetByUsername(ctx context.Context, username string) (*entity.User, error) - + // Update 更新用户 Update(ctx context.Context, user *entity.User) error - + // Delete 删除用户 Delete(ctx context.Context, id string) error - + // List 列出所有用户 List(ctx context.Context) ([]*entity.User, error) + + // ListByWorkspace 列出指定 workspace 的用户 + ListByWorkspace(ctx context.Context, workspaceID string) ([]*entity.User, error) + + // ListActive 仅列出活跃用户 + ListActive(ctx context.Context) ([]*entity.User, error) } diff --git a/backend/internal/domain/repository/values_template_repository.go b/backend/internal/domain/repository/values_template_repository.go new file mode 100644 index 0000000..baefeec --- /dev/null +++ b/backend/internal/domain/repository/values_template_repository.go @@ -0,0 +1,36 @@ +package repository + +import ( + "context" + "github.com/ocdp/cluster-service/internal/domain/entity" +) + +// ValuesTemplateRepository Values 模板仓储接口 +type ValuesTemplateRepository interface { + // Create 创建 Values 模板 + Create(ctx context.Context, template *entity.ValuesTemplate) error + + // GetByID 根据 ID 获取 Values 模板 + GetByID(ctx context.Context, id string) (*entity.ValuesTemplate, error) + + // GetByWorkspace 获取 workspace 的所有 Values 模板 + GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.ValuesTemplate, error) + + // GetByChartReference 获取 Chart Reference 的所有 Values 模板 + GetByChartReference(ctx context.Context, chartRefID string) ([]*entity.ValuesTemplate, error) + + // GetByName 根据名称获取 Values 模板 + GetByName(ctx context.Context, workspaceID, chartRefID, name string) (*entity.ValuesTemplate, error) + + // GetHistory 获取模板的版本历史 + GetHistory(ctx context.Context, chartRefID, name string) ([]*entity.ValuesTemplate, error) + + // Update 更新 Values 模板(自动递增版本) + Update(ctx context.Context, template *entity.ValuesTemplate) error + + // Delete 删除 Values 模板 + Delete(ctx context.Context, id string) error + + // List 列出所有 Values 模板(管理员用) + List(ctx context.Context) ([]*entity.ValuesTemplate, error) +} \ No newline at end of file diff --git a/backend/internal/domain/repository/workspace_repository.go b/backend/internal/domain/repository/workspace_repository.go new file mode 100644 index 0000000..7fea73c --- /dev/null +++ b/backend/internal/domain/repository/workspace_repository.go @@ -0,0 +1,27 @@ +package repository + +import ( + "context" + "github.com/ocdp/cluster-service/internal/domain/entity" +) + +// WorkspaceRepository Workspace 仓储接口 +type WorkspaceRepository interface { + // Create 创建 Workspace + Create(ctx context.Context, workspace *entity.Workspace) error + + // GetByID 根据 ID 获取 Workspace + GetByID(ctx context.Context, id string) (*entity.Workspace, error) + + // GetByName 根据名称获取 Workspace + GetByName(ctx context.Context, name string) (*entity.Workspace, error) + + // Update 更新 Workspace + Update(ctx context.Context, workspace *entity.Workspace) error + + // Delete 删除 Workspace + Delete(ctx context.Context, id string) error + + // List 列出所有 Workspace + List(ctx context.Context) ([]*entity.Workspace, error) +} \ No newline at end of file diff --git a/backend/internal/domain/service/artifact_service.go b/backend/internal/domain/service/artifact_service.go index deb1363..9f6cba3 100644 --- a/backend/internal/domain/service/artifact_service.go +++ b/backend/internal/domain/service/artifact_service.go @@ -68,6 +68,16 @@ func (s *ArtifactService) GetValuesSchema(ctx context.Context, registryID, repos return s.ociClient.GetValuesSchema(ctx, registry, repository, reference) } +// GetValues 获取 Helm Chart 的 values.yaml +func (s *ArtifactService) GetValues(ctx context.Context, registryID, repository, reference string) (string, error) { + registry, err := s.registryRepo.GetByID(ctx, registryID) + if err != nil { + return "", entity.ErrRegistryNotFound + } + + return s.ociClient.GetValues(ctx, registry, repository, reference) +} + // PullArtifact 下载 artifact func (s *ArtifactService) PullArtifact(ctx context.Context, registryID, repository, reference, destPath string) error { registry, err := s.registryRepo.GetByID(ctx, registryID) diff --git a/backend/internal/domain/service/audit_service.go b/backend/internal/domain/service/audit_service.go new file mode 100644 index 0000000..b588d73 --- /dev/null +++ b/backend/internal/domain/service/audit_service.go @@ -0,0 +1,71 @@ +package service + +import ( + "context" + "time" + + "github.com/google/uuid" + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/repository" +) + +// AuditService 审计日志领域服务 +type AuditService struct { + auditLogRepo repository.AuditLogRepository +} + +// NewAuditService 创建审计服务 +func NewAuditService(auditLogRepo repository.AuditLogRepository) *AuditService { + return &AuditService{ + auditLogRepo: auditLogRepo, + } +} + +// Log 创建审计日志 +func (s *AuditService) Log(ctx context.Context, workspaceID, userID string, action entity.AuditAction, resourceType entity.AuditResourceType, resourceID, resourceName string, details map[string]interface{}, ipAddress, userAgent string) error { + auditLog := &entity.AuditLog{ + ID: uuid.New().String(), + WorkspaceID: workspaceID, + UserID: userID, + Action: action, + ResourceType: resourceType, + ResourceID: resourceID, + ResourceName: resourceName, + Details: details, + IPAddress: ipAddress, + UserAgent: userAgent, + CreatedAt: time.Now(), + } + + return s.auditLogRepo.Create(ctx, auditLog) +} + +// LogAction 简化版日志记录 +func (s *AuditService) LogAction(ctx context.Context, workspaceID, userID string, action entity.AuditAction, resourceType entity.AuditResourceType, resourceName string) error { + return s.Log(ctx, workspaceID, userID, action, resourceType, "", resourceName, nil, "", "") +} + +// LogWithDetails 带详情的日志记录 +func (s *AuditService) LogWithDetails(ctx context.Context, workspaceID, userID string, action entity.AuditAction, resourceType entity.AuditResourceType, resourceID, resourceName string, details map[string]interface{}) error { + return s.Log(ctx, workspaceID, userID, action, resourceType, resourceID, resourceName, details, "", "") +} + +// GetLogs 获取审计日志 +func (s *AuditService) GetLogs(ctx context.Context, workspaceID string, limit int) ([]*entity.AuditLog, error) { + return s.auditLogRepo.GetByWorkspace(ctx, workspaceID, limit) +} + +// GetUserLogs 获取用户的审计日志 +func (s *AuditService) GetUserLogs(ctx context.Context, userID string, limit int) ([]*entity.AuditLog, error) { + return s.auditLogRepo.GetByUser(ctx, userID, limit) +} + +// GetResourceLogs 获取资源的审计日志 +func (s *AuditService) GetResourceLogs(ctx context.Context, resourceType entity.AuditResourceType, resourceID string, limit int) ([]*entity.AuditLog, error) { + return s.auditLogRepo.GetByResource(ctx, resourceType, resourceID, limit) +} + +// GetAllLogs 获取所有审计日志(Admin) +func (s *AuditService) GetAllLogs(ctx context.Context, limit int, offset int) ([]*entity.AuditLog, error) { + return s.auditLogRepo.List(ctx, limit, offset) +} \ No newline at end of file diff --git a/backend/internal/domain/service/auth_service.go b/backend/internal/domain/service/auth_service.go index fd7340a..610d616 100644 --- a/backend/internal/domain/service/auth_service.go +++ b/backend/internal/domain/service/auth_service.go @@ -22,9 +22,9 @@ type PasswordHasher interface { // TokenGenerator Token 生成器接口 type TokenGenerator interface { - Generate(userID, username string) (accessToken, refreshToken string, err error) - Verify(token string) (userID, username string, err error) - VerifyWithIssuedAt(token string) (userID, username string, issuedAt int64, err error) + Generate(userID, username, role, workspaceID string) (accessToken, refreshToken string, err error) + Verify(token string) (userID, username, role, workspaceID string, err error) + VerifyWithIssuedAt(token string) (userID, username, role, workspaceID string, issuedAt int64, err error) Refresh(refreshToken string) (newAccessToken string, err error) } @@ -86,8 +86,8 @@ func (s *AuthService) Login(ctx context.Context, username, password string) (acc return "", "", entity.ErrInvalidPassword } - // 生成 Token - accessToken, refreshToken, err = s.tokenGenerator.Generate(user.ID, user.Username) + // 生成 Token (包含 role 和 workspace_id) + accessToken, refreshToken, err = s.tokenGenerator.Generate(user.ID, user.Username, string(user.Role), user.WorkspaceID) if err != nil { return "", "", err } @@ -108,7 +108,7 @@ func (s *AuthService) GetUserByID(ctx context.Context, id string) (*entity.User, // VerifyAccessToken 验证 Access Token(包括 revoked_after 检查) func (s *AuthService) VerifyAccessToken(ctx context.Context, token string) (userID, username string, err error) { // 1. JWT 自验证 - userID, username, issuedAt, err := s.tokenGenerator.VerifyWithIssuedAt(token) + userID, username, _, _, issuedAt, err := s.tokenGenerator.VerifyWithIssuedAt(token) if err != nil { return "", "", err } diff --git a/backend/internal/domain/service/chart_reference_service.go b/backend/internal/domain/service/chart_reference_service.go new file mode 100644 index 0000000..10eb42e --- /dev/null +++ b/backend/internal/domain/service/chart_reference_service.go @@ -0,0 +1,137 @@ +package service + +import ( + "context" + "errors" + + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/repository" +) + +var ( + ErrChartReferenceNotFound = errors.New("chart reference not found") + ErrChartReferenceExists = errors.New("chart reference already exists") +) + +// ChartReferenceService Chart 引用领域服务 +type ChartReferenceService struct { + chartRefRepo repository.ChartReferenceRepository + registryRepo repository.RegistryRepository +} + +// NewChartReferenceService 创建 Chart 引用服务 +func NewChartReferenceService( + chartRefRepo repository.ChartReferenceRepository, + registryRepo repository.RegistryRepository, +) *ChartReferenceService { + return &ChartReferenceService{ + chartRefRepo: chartRefRepo, + registryRepo: registryRepo, + } +} + +// Create 创建 Chart 引用 +func (s *ChartReferenceService) Create( + ctx context.Context, + workspaceID, registryID, repository, chartName, description string, +) (*entity.ChartReference, error) { + // 检查 Registry 是否存在 + registry, err := s.registryRepo.GetByID(ctx, registryID) + if err != nil { + return nil, errors.New("registry not found") + } + + // 检查名称是否已存在 + existing, _ := s.chartRefRepo.GetByName(ctx, workspaceID, chartName) + if existing != nil { + return nil, ErrChartReferenceExists + } + + chartRef := entity.NewChartReference(workspaceID, registry.ID, repository, chartName, description) + chartRef.Description = description + + if err := s.chartRefRepo.Create(ctx, chartRef); err != nil { + return nil, err + } + + return chartRef, nil +} + +// GetByID 获取 Chart 引用 +func (s *ChartReferenceService) GetByID(ctx context.Context, id string) (*entity.ChartReference, error) { + chartRef, err := s.chartRefRepo.GetByID(ctx, id) + if err != nil { + return nil, ErrChartReferenceNotFound + } + return chartRef, nil +} + +// GetByWorkspace 获取工作空间的所有 Chart 引用 +func (s *ChartReferenceService) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.ChartReference, error) { + return s.chartRefRepo.GetByWorkspace(ctx, workspaceID) +} + +// GetByRegistry 获取 Registry 的所有 Chart 引用 +func (s *ChartReferenceService) GetByRegistry(ctx context.Context, registryID string) ([]*entity.ChartReference, error) { + return s.chartRefRepo.GetByRegistry(ctx, registryID) +} + +// Update 更新 Chart 引用 +func (s *ChartReferenceService) Update( + ctx context.Context, + id, registryID, repository, chartName, description string, + isEnabled bool, +) (*entity.ChartReference, error) { + chartRef, err := s.chartRefRepo.GetByID(ctx, id) + if err != nil { + return nil, ErrChartReferenceNotFound + } + + if registryID != "" { + chartRef.RegistryID = registryID + } + if repository != "" { + chartRef.Repository = repository + } + if chartName != "" { + chartRef.ChartName = chartName + } + chartRef.Description = description + chartRef.IsEnabled = isEnabled + + if err := s.chartRefRepo.Update(ctx, chartRef); err != nil { + return nil, err + } + + return chartRef, nil +} + +// Delete 删除 Chart 引用 +func (s *ChartReferenceService) Delete(ctx context.Context, id string) error { + return s.chartRefRepo.Delete(ctx, id) +} + +// List 列出所有 Chart 引用(管理员用) +func (s *ChartReferenceService) List(ctx context.Context) ([]*entity.ChartReference, error) { + return s.chartRefRepo.List(ctx) +} + +// Enable 启用 Chart 引用 +func (s *ChartReferenceService) Enable(ctx context.Context, id string) error { + chartRef, err := s.chartRefRepo.GetByID(ctx, id) + if err != nil { + return ErrChartReferenceNotFound + } + chartRef.IsEnabled = true + return s.chartRefRepo.Update(ctx, chartRef) +} + +// Disable 禁用 Chart 引用 +func (s *ChartReferenceService) Disable(ctx context.Context, id string) error { + chartRef, err := s.chartRefRepo.GetByID(ctx, id) + if err != nil { + return ErrChartReferenceNotFound + } + chartRef.IsEnabled = false + return s.chartRefRepo.Update(ctx, chartRef) +} \ No newline at end of file diff --git a/backend/internal/domain/service/cluster_service.go b/backend/internal/domain/service/cluster_service.go index a6986b8..01f7022 100644 --- a/backend/internal/domain/service/cluster_service.go +++ b/backend/internal/domain/service/cluster_service.go @@ -2,9 +2,16 @@ package service import ( "context" + "encoding/base64" + "fmt" + "os" + "github.com/google/uuid" "github.com/ocdp/cluster-service/internal/domain/entity" "github.com/ocdp/cluster-service/internal/domain/repository" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" ) // ClusterService 集群管理领域服务 @@ -75,3 +82,105 @@ func (s *ClusterService) ListClusters(ctx context.Context) ([]*entity.Cluster, e return s.clusterRepo.List(ctx) } +// ListByWorkspace 列出指定 workspace 的集群(包括共享集群) +func (s *ClusterService) ListByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Cluster, error) { + return s.clusterRepo.GetByWorkspace(ctx, workspaceID) +} + +// GetSharedClusters 获取所有共享集群 +func (s *ClusterService) GetSharedClusters(ctx context.Context) ([]*entity.Cluster, error) { + return s.clusterRepo.GetShared(ctx) +} + +// TestConnection 测试集群连接是否可用 +func (s *ClusterService) TestConnection(ctx context.Context, cluster *entity.Cluster) error { + // Mock 模式直接返回成功 + if os.Getenv("ADAPTER_MODE") == "mock" { + return nil + } + + // 尝试创建 k8s client + config, err := s.createRestConfig(cluster) + if err != nil { + return fmt.Errorf("failed to create k8s config: %w", err) + } + + // 设置超时 + config.Timeout = 30 * 1000000000 // 30秒 (nanoseconds) + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return fmt.Errorf("failed to create k8s client: %w", err) + } + + // 测试连接 - 获取 version 信息 + version, err := clientset.ServerVersion() + if err != nil { + return fmt.Errorf("failed to connect to cluster: %w", err) + } + + if version == nil { + return fmt.Errorf("cluster returned nil version") + } + + return nil +} + +// createRestConfig 从 cluster 实体创建 k8s REST 配置 +func (s *ClusterService) createRestConfig(cluster *entity.Cluster) (*rest.Config, error) { + // 优先使用 kubeconfig 格式(如果 CAData 包含完整的 kubeconfig 内容) + if len(cluster.CAData) > 100 && (cluster.CAData[:11] == "apiVersion:" || cluster.CAData[:5] == "kind:") { + return clientcmd.RESTConfigFromKubeConfig([]byte(cluster.CAData)) + } + + // 使用证书或 token 认证 + config := &rest.Config{ + Host: cluster.Host, + } + + if cluster.CertData != "" && cluster.KeyData != "" { + // 尝试解码 base64 编码的证书,如果失败则尝试原始 PEM + var caData, certData, keyData []byte + var decodeErr error + + // 先尝试 base64 解码 + caData, decodeErr = base64.StdEncoding.DecodeString(cluster.CAData) + if decodeErr != nil { + // base64 解码失败,可能是原始 PEM + caData = []byte(cluster.CAData) + } + + certData, decodeErr = base64.StdEncoding.DecodeString(cluster.CertData) + if decodeErr != nil { + certData = []byte(cluster.CertData) + } + + keyData, decodeErr = base64.StdEncoding.DecodeString(cluster.KeyData) + if decodeErr != nil { + keyData = []byte(cluster.KeyData) + } + + config.TLSClientConfig = rest.TLSClientConfig{ + CAData: caData, + CertData: certData, + KeyData: keyData, + Insecure: false, + } + } else if cluster.Token != "" { + config.BearerToken = cluster.Token + } else { + // 尝试使用本地 kubeconfig + kubeconfig := os.Getenv("KUBECONFIG") + if kubeconfig == "" { + kubeconfig = ".kube/config" + } + // 尝试从文件加载 kubeconfig + if _, err := os.Stat(kubeconfig); err == nil { + return clientcmd.BuildConfigFromFlags("", kubeconfig) + } + return clientcmd.BuildConfigFromFlags("", kubeconfig) + } + + return config, nil +} + diff --git a/backend/internal/domain/service/instance_service.go b/backend/internal/domain/service/instance_service.go index 6e5ec7f..05f757b 100644 --- a/backend/internal/domain/service/instance_service.go +++ b/backend/internal/domain/service/instance_service.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "time" "github.com/google/uuid" @@ -336,9 +337,17 @@ func (s *InstanceService) executeAndSyncRollback(ctx context.Context, instanceID // executeAndSyncUninstall 异步执行卸载并监控状态 func (s *InstanceService) executeAndSyncUninstall(ctx context.Context, instanceID string, cluster *entity.Cluster, releaseName, namespace string) { + // 先验证 release 名称是否有效 + // 如果名称无效,说明这个 release 根本不可能存在于 Helm 中,直接删除数据库记录 + if err := entity.ValidateReleaseName(releaseName); err != nil { + // Release 名称无效,直接删除数据库记录 + _ = s.instanceRepo.Delete(ctx, instanceID) + return + } + // 执行 Helm 卸载 err := s.helmClient.Uninstall(ctx, cluster, releaseName, namespace) - + // 获取实例 instance, getErr := s.instanceRepo.GetByID(ctx, instanceID) if getErr != nil { @@ -346,13 +355,22 @@ func (s *InstanceService) executeAndSyncUninstall(ctx context.Context, instanceI } if err != nil { - // 如果错误不是"未找到",则标记为失败 - if !errors.Is(err, entity.ErrInstanceNotFound) { - instance.MarkFailure("Helm uninstall failed", err) - _ = s.instanceRepo.Update(ctx, instance) - } else { - // 如果未找到,说明已经卸载,直接删除数据库记录 + // 检查错误类型 + if errors.Is(err, entity.ErrInstanceNotFound) { + // 未找到,说明已经卸载,直接删除数据库记录 _ = s.instanceRepo.Delete(ctx, instanceID) + } else { + // 检查是否是 release 名称无效的错误(可能在某些情况下 Helm 会返回这个错误) + errMsg := strings.ToLower(err.Error()) + if strings.Contains(errMsg, "release name is invalid") || + (strings.Contains(errMsg, "invalid") && strings.Contains(errMsg, "release")) { + // Release 名称无效,直接删除数据库记录 + _ = s.instanceRepo.Delete(ctx, instanceID) + } else { + // 其他错误,标记为失败 + instance.MarkFailure("Helm uninstall failed", err) + _ = s.instanceRepo.Update(ctx, instance) + } } return } @@ -360,7 +378,7 @@ func (s *InstanceService) executeAndSyncUninstall(ctx context.Context, instanceI // 卸载成功,标记为已卸载 instance.MarkSuccess(entity.StatusUninstalled, instance.Revision, "Instance uninstalled successfully") _ = s.instanceRepo.Update(ctx, instance) - + // 验证卸载是否完成:尝试获取状态,如果获取不到说明已卸载 time.Sleep(3 * time.Second) _, statusErr := s.helmClient.GetStatus(ctx, cluster, releaseName, namespace) diff --git a/backend/internal/domain/service/quota_service.go b/backend/internal/domain/service/quota_service.go new file mode 100644 index 0000000..f0bdd14 --- /dev/null +++ b/backend/internal/domain/service/quota_service.go @@ -0,0 +1,224 @@ +package service + +import ( + "context" + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/repository" +) + +// QuotaService 配额领域服务 +type QuotaService struct { + quotaRepo repository.QuotaRepository + instanceRepo repository.InstanceRepository + workspaceRepo repository.WorkspaceRepository +} + +// NewQuotaService 创建配额服务 +func NewQuotaService( + quotaRepo repository.QuotaRepository, + instanceRepo repository.InstanceRepository, + workspaceRepo repository.WorkspaceRepository, +) *QuotaService { + return &QuotaService{ + quotaRepo: quotaRepo, + instanceRepo: instanceRepo, + workspaceRepo: workspaceRepo, + } +} + +// CheckQuota 检查配额是否足够 +func (s *QuotaService) CheckQuota(ctx context.Context, workspaceID string, cpu, gpu, gpuMemory float64) error { + // 检查 CPU 配额 + if cpu > 0 { + quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceCPU) + if err != nil { + return err + } + if quota != nil && !quota.CanAllocate(cpu) { + return entity.ErrQuotaExceeded + } + } + + // 检查 GPU 配额 + if gpu > 0 { + quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceGPU) + if err != nil { + return err + } + if quota != nil && !quota.CanAllocate(gpu) { + return entity.ErrQuotaExceeded + } + } + + // 检查 GPU Memory 配额 + if gpuMemory > 0 { + quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceGPUMemory) + if err != nil { + return err + } + if quota != nil && !quota.CanAllocate(gpuMemory) { + return entity.ErrQuotaExceeded + } + } + + return nil +} + +// AllocateQuota 分配配额(部署实例成功后调用) +func (s *QuotaService) AllocateQuota(ctx context.Context, workspaceID string, cpu, gpu, gpuMemory float64) error { + // 分配 CPU + if cpu > 0 { + quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceCPU) + if err != nil { + return err + } + if quota != nil { + quota.Allocate(cpu) + if err := s.quotaRepo.Update(ctx, quota); err != nil { + return err + } + } + } + + // 分配 GPU + if gpu > 0 { + quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceGPU) + if err != nil { + return err + } + if quota != nil { + quota.Allocate(gpu) + if err := s.quotaRepo.Update(ctx, quota); err != nil { + return err + } + } + } + + // 分配 GPU Memory + if gpuMemory > 0 { + quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceGPUMemory) + if err != nil { + return err + } + if quota != nil { + quota.Allocate(gpuMemory) + if err := s.quotaRepo.Update(ctx, quota); err != nil { + return err + } + } + } + + return nil +} + +// ReleaseQuota 释放配额(删除实例后调用) +func (s *QuotaService) ReleaseQuota(ctx context.Context, workspaceID string, cpu, gpu, gpuMemory float64) error { + // 释放 CPU + if cpu > 0 { + quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceCPU) + if err != nil { + return err + } + if quota != nil { + quota.Release(cpu) + if err := s.quotaRepo.Update(ctx, quota); err != nil { + return err + } + } + } + + // 释放 GPU + if gpu > 0 { + quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceGPU) + if err != nil { + return err + } + if quota != nil { + quota.Release(gpu) + if err := s.quotaRepo.Update(ctx, quota); err != nil { + return err + } + } + } + + // 释放 GPU Memory + if gpuMemory > 0 { + quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, entity.ResourceGPUMemory) + if err != nil { + return err + } + if quota != nil { + quota.Release(gpuMemory) + if err := s.quotaRepo.Update(ctx, quota); err != nil { + return err + } + } + } + + return nil +} + +// GetQuotaUsage 获取配额使用情况 +func (s *QuotaService) GetQuotaUsage(ctx context.Context, workspaceID string) (map[entity.ResourceType]*entity.WorkspaceQuota, error) { + quotas, err := s.quotaRepo.GetByWorkspace(ctx, workspaceID) + if err != nil { + return nil, err + } + + result := make(map[entity.ResourceType]*entity.WorkspaceQuota) + for _, q := range quotas { + result[q.ResourceType] = q + } + + // 确保所有资源类型都有返回值 + for _, rt := range []entity.ResourceType{entity.ResourceCPU, entity.ResourceGPU, entity.ResourceGPUMemory} { + if _, ok := result[rt]; !ok { + result[rt] = &entity.WorkspaceQuota{ + WorkspaceID: workspaceID, + ResourceType: rt, + HardLimit: 0, + SoftLimit: 0, + Used: 0, + } + } + } + + return result, nil +} + +// RecalculateQuota 重新计算配额使用量(从实例汇总) +func (s *QuotaService) RecalculateQuota(ctx context.Context, workspaceID string) error { + // 获取 workspace 的所有实例 + instances, err := s.instanceRepo.GetByWorkspace(ctx, workspaceID) + if err != nil { + return err + } + + // 汇总资源使用 + var totalCPU, totalGPU, totalGPUMemory float64 + for _, inst := range instances { + totalCPU += inst.CPURequested + totalGPU += inst.GPURequested + // GPU Memory 需要解析字符串 + // 这里简化处理,实际需要解析 "16Gi" 这样的格式 + } + + // 更新配额 + resources := []entity.ResourceType{entity.ResourceCPU, entity.ResourceGPU, entity.ResourceGPUMemory} + values := []float64{totalCPU, totalGPU, totalGPUMemory} + + for i, rt := range resources { + quota, err := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, rt) + if err != nil { + return err + } + if quota != nil { + quota.Used = values[i] + if err := s.quotaRepo.Update(ctx, quota); err != nil { + return err + } + } + } + + return nil +} \ No newline at end of file diff --git a/backend/internal/domain/service/registry_service.go b/backend/internal/domain/service/registry_service.go index 92e7f80..1b0d62d 100644 --- a/backend/internal/domain/service/registry_service.go +++ b/backend/internal/domain/service/registry_service.go @@ -2,6 +2,8 @@ package service import ( "context" + "os" + "github.com/google/uuid" "github.com/ocdp/cluster-service/internal/domain/entity" "github.com/ocdp/cluster-service/internal/domain/repository" @@ -40,6 +42,13 @@ func (s *RegistryService) CreateRegistry(ctx context.Context, registry *entity.R return entity.ErrRegistryExists } + // 非 mock 模式下验证连接 + if os.Getenv("ADAPTER_MODE") != "mock" { + if err := s.ociClient.CheckHealth(ctx, registry); err != nil { + return err + } + } + return s.registryRepo.Create(ctx, registry) } diff --git a/backend/internal/domain/service/storage_service.go b/backend/internal/domain/service/storage_service.go new file mode 100644 index 0000000..bcd1bb5 --- /dev/null +++ b/backend/internal/domain/service/storage_service.go @@ -0,0 +1,116 @@ +package service + +import ( + "context" + "errors" + + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/repository" +) + +var ( + ErrStorageNotFound = errors.New("storage backend not found") + ErrStorageExists = errors.New("storage backend already exists") +) + +// StorageService 存储后端领域服务 +type StorageService struct { + storageRepo repository.StorageRepository +} + +// NewStorageService 创建存储后端服务 +func NewStorageService(storageRepo repository.StorageRepository) *StorageService { + return &StorageService{ + storageRepo: storageRepo, + } +} + +// Create 创建存储后端 +func (s *StorageService) Create( + ctx context.Context, + workspaceID, ownerID, name string, + storageType entity.StorageType, + config entity.StorageConfig, + description string, + isDefault, isShared bool, +) (*entity.StorageBackend, error) { + // 检查名称是否已存在 + existing, _ := s.storageRepo.GetByName(ctx, workspaceID, name) + if existing != nil { + return nil, ErrStorageExists + } + + storage := entity.NewStorageBackend(workspaceID, ownerID, name, storageType, config) + storage.Description = description + storage.IsDefault = isDefault + storage.IsShared = isShared + + if err := s.storageRepo.Create(ctx, storage); err != nil { + return nil, err + } + + return storage, nil +} + +// GetByID 获取存储后端 +func (s *StorageService) GetByID(ctx context.Context, id string) (*entity.StorageBackend, error) { + storage, err := s.storageRepo.GetByID(ctx, id) + if err != nil { + return nil, ErrStorageNotFound + } + return storage, nil +} + +// GetByWorkspace 获取工作空间的所有存储后端 +func (s *StorageService) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.StorageBackend, error) { + return s.storageRepo.GetByWorkspace(ctx, workspaceID) +} + +// GetShared 获取所有共享存储后端 +func (s *StorageService) GetShared(ctx context.Context) ([]*entity.StorageBackend, error) { + return s.storageRepo.GetShared(ctx) +} + +// GetDefault 获取工作空间的默认存储后端 +func (s *StorageService) GetDefault(ctx context.Context, workspaceID string) (*entity.StorageBackend, error) { + return s.storageRepo.GetDefault(ctx, workspaceID) +} + +// Update 更新存储后端 +func (s *StorageService) Update( + ctx context.Context, + id, name, description string, + storageType entity.StorageType, + config entity.StorageConfig, + isDefault, isShared bool, +) (*entity.StorageBackend, error) { + storage, err := s.storageRepo.GetByID(ctx, id) + if err != nil { + return nil, ErrStorageNotFound + } + + if name != "" { + storage.Name = name + } + storage.Description = description + storage.Type = storageType + storage.Config = config + storage.IsDefault = isDefault + storage.IsShared = isShared + + if err := s.storageRepo.Update(ctx, storage); err != nil { + return nil, err + } + + return storage, nil +} + +// Delete 删除存储后端 +func (s *StorageService) Delete(ctx context.Context, id string) error { + return s.storageRepo.Delete(ctx, id) +} + +// List 列出所有存储后端(管理员用) +func (s *StorageService) List(ctx context.Context) ([]*entity.StorageBackend, error) { + return s.storageRepo.List(ctx) +} \ No newline at end of file diff --git a/backend/internal/domain/service/user_management_service.go b/backend/internal/domain/service/user_management_service.go new file mode 100644 index 0000000..c34981e --- /dev/null +++ b/backend/internal/domain/service/user_management_service.go @@ -0,0 +1,298 @@ +package service + +import ( + "context" + "database/sql" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/repository" +) + +// UserManagementService 用户管理领域服务(仅 Admin 可用) +type UserManagementService struct { + userRepo repository.UserRepository + workspaceRepo repository.WorkspaceRepository + passwordHasher PasswordHasher +} + +// NewUserManagementService 创建用户管理服务 +func NewUserManagementService( + userRepo repository.UserRepository, + workspaceRepo repository.WorkspaceRepository, + passwordHasher PasswordHasher, +) *UserManagementService { + return &UserManagementService{ + userRepo: userRepo, + workspaceRepo: workspaceRepo, + passwordHasher: passwordHasher, + } +} + +// CreateUser 创建用户(Admin 操作) +func (s *UserManagementService) CreateUser(ctx context.Context, username, password, email, role string, workspaceID string) (*entity.User, error) { + // 检查用户是否已存在 + existing, _ := s.userRepo.GetByUsername(ctx, username) + if existing != nil { + return nil, entity.ErrUserExists + } + + // 验证角色 + if role != string(entity.RoleAdmin) && role != string(entity.RoleUser) { + return nil, fmt.Errorf("invalid role: %s", role) + } + + // 如果指定了 workspace,验证 workspace 存在 + if workspaceID != "" { + _, err := s.workspaceRepo.GetByID(ctx, workspaceID) + if err != nil { + if err == entity.ErrWorkspaceNotFound { + return nil, entity.ErrWorkspaceNotFound + } + return nil, err + } + } + + // Admin 不能分配到 workspace + if role == string(entity.RoleAdmin) && workspaceID != "" { + workspaceID = "" + } + + // 哈希密码 + passwordHash, err := s.passwordHasher.Hash(password) + if err != nil { + return nil, err + } + + // 生成占位邮箱 + if email == "" { + email = username + "@local.ocdp" + } + + // 创建用户 + user := entity.NewUser(username, passwordHash, email) + user.ID = uuid.New().String() + user.Role = entity.UserRole(role) + user.WorkspaceID = workspaceID + user.IsActive = true + user.MustChangePassword = true // 首次登录必须修改密码 + + if err := user.Validate(); err != nil { + return nil, err + } + + if err := s.userRepo.Create(ctx, user); err != nil { + return nil, err + } + + return user, nil +} + +// GetUser 获取用户 +func (s *UserManagementService) GetUser(ctx context.Context, id string) (*entity.User, error) { + return s.userRepo.GetByID(ctx, id) +} + +// ListUsers 列出用户(可筛选 workspace) +func (s *UserManagementService) ListUsers(ctx context.Context, workspaceID string) ([]*entity.User, error) { + if workspaceID != "" { + return s.userRepo.ListByWorkspace(ctx, workspaceID) + } + return s.userRepo.List(ctx) +} + +// UpdateUser 更新用户信息 +func (s *UserManagementService) UpdateUser(ctx context.Context, user *entity.User) error { + return s.userRepo.Update(ctx, user) +} + +// SetUserActive 启用/禁用用户 +func (s *UserManagementService) SetUserActive(ctx context.Context, userID string, isActive bool) error { + user, err := s.userRepo.GetByID(ctx, userID) + if err != nil { + return err + } + + user.IsActive = isActive + return s.userRepo.Update(ctx, user) +} + +// ChangeUserWorkspace 分配用户到 workspace +func (s *UserManagementService) ChangeUserWorkspace(ctx context.Context, userID, workspaceID string) error { + user, err := s.userRepo.GetByID(ctx, userID) + if err != nil { + return err + } + + // Admin 不能分配到 workspace + if user.Role == entity.RoleAdmin { + return fmt.Errorf("admin user cannot be assigned to workspace") + } + + // 验证 workspace 存在 + if workspaceID != "" { + _, err := s.workspaceRepo.GetByID(ctx, workspaceID) + if err != nil { + if err == sql.ErrNoRows || err == entity.ErrWorkspaceNotFound { + return entity.ErrWorkspaceNotFound + } + return err + } + } + + user.WorkspaceID = workspaceID + return s.userRepo.Update(ctx, user) +} + +// ResetPassword 重置用户密码(Admin 操作) +func (s *UserManagementService) ResetPassword(ctx context.Context, userID, newPassword string) error { + user, err := s.userRepo.GetByID(ctx, userID) + if err != nil { + return err + } + + // 哈希新密码 + passwordHash, err := s.passwordHasher.Hash(newPassword) + if err != nil { + return err + } + + // 更新密码并设置必须修改密码标志 + user.PasswordHash = passwordHash + user.MustChangePassword = true + user.RevokeAllTokens() // 强制登出所有会话 + + return s.userRepo.Update(ctx, user) +} + +// DeleteUser 删除用户 +func (s *UserManagementService) DeleteUser(ctx context.Context, id string) error { + return s.userRepo.Delete(ctx, id) +} + +// GetUserWithWorkspace 获取用户及其 workspace 信息 +type UserWithWorkspace struct { + User *entity.User + Workspace *entity.Workspace +} + +// GetUserWithWorkspace 获取用户及其 workspace 信息 +func (s *UserManagementService) GetUserWithWorkspace(ctx context.Context, userID string) (*UserWithWorkspace, error) { + user, err := s.userRepo.GetByID(ctx, userID) + if err != nil { + return nil, err + } + + result := &UserWithWorkspace{ + User: user, + } + + if user.WorkspaceID != "" { + workspace, _ := s.workspaceRepo.GetByID(ctx, user.WorkspaceID) + result.Workspace = workspace + } + + return result, nil +} + +// ListUsersWithWorkspace 列出用户及其 workspace 信息 +func (s *UserManagementService) ListUsersWithWorkspace(ctx context.Context) ([]*UserWithWorkspace, error) { + users, err := s.userRepo.List(ctx) + if err != nil { + return nil, err + } + + // 预加载所有 workspace + workspaces, err := s.workspaceRepo.List(ctx) + if err != nil { + return nil, err + } + + workspaceMap := make(map[string]*entity.Workspace) + for _, w := range workspaces { + workspaceMap[w.ID] = w + } + + result := make([]*UserWithWorkspace, len(users)) + for i, user := range users { + result[i] = &UserWithWorkspace{ + User: user, + } + if user.WorkspaceID != "" { + result[i].Workspace = workspaceMap[user.WorkspaceID] + } + } + + return result, nil +} + +// EnsureAdminExists 确保存在一个 Admin 用户 +func (s *UserManagementService) EnsureAdminExists(ctx context.Context, defaultPassword string) error { + users, err := s.userRepo.List(ctx) + if err != nil { + return err + } + + // 检查是否已有 admin + for _, u := range users { + if u.Role == entity.RoleAdmin { + return nil + } + } + + // 创建默认 admin 用户 + _, err = s.CreateUser(ctx, "admin", defaultPassword, "", string(entity.RoleAdmin), "") + return err +} + +// CreateInitialUser 创建初始用户(首次启动时调用) +func (s *UserManagementService) CreateInitialUser(ctx context.Context, username, password, role string) (*entity.User, error) { + // 检查是否已有用户 + users, err := s.userRepo.List(ctx) + if err != nil { + return nil, err + } + + if len(users) > 0 { + return nil, fmt.Errorf("initial user already exists") + } + + // 验证角色 + if role != string(entity.RoleAdmin) && role != string(entity.RoleUser) { + return nil, fmt.Errorf("invalid role: %s", role) + } + + // 哈希密码 + passwordHash, err := s.passwordHasher.Hash(password) + if err != nil { + return nil, err + } + + // 生成占位邮箱 + email := username + "@local.ocdp" + + // 创建用户 + user := entity.NewUser(username, passwordHash, email) + user.ID = uuid.New().String() + user.Role = entity.UserRole(role) + // workspace_id 为 NULL(admin)或空(首个普通用户) + user.IsActive = true + user.MustChangePassword = false // 初始用户不需要强制修改密码 + + if err := user.Validate(); err != nil { + return nil, err + } + + // 设置创建时间和更新时间 + now := time.Now() + user.CreatedAt = now + user.UpdatedAt = now + + if err := s.userRepo.Create(ctx, user); err != nil { + return nil, err + } + + return user, nil +} \ No newline at end of file diff --git a/backend/internal/domain/service/values_template_service.go b/backend/internal/domain/service/values_template_service.go new file mode 100644 index 0000000..828e2d9 --- /dev/null +++ b/backend/internal/domain/service/values_template_service.go @@ -0,0 +1,143 @@ +package service + +import ( + "context" + "errors" + + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/repository" +) + +var ( + ErrTemplateNotFound = errors.New("template not found") + ErrTemplateExists = errors.New("template already exists") +) + +// ValuesTemplateService Values 模板领域服务 +type ValuesTemplateService struct { + valuesTemplateRepo repository.ValuesTemplateRepository + chartRefRepo repository.ChartReferenceRepository +} + +// NewValuesTemplateService 创建 Values 模板服务 +func NewValuesTemplateService( + valuesTemplateRepo repository.ValuesTemplateRepository, + chartRefRepo repository.ChartReferenceRepository, +) *ValuesTemplateService { + return &ValuesTemplateService{ + valuesTemplateRepo: valuesTemplateRepo, + chartRefRepo: chartRefRepo, + } +} + +// Create 创建 Values 模板 +func (s *ValuesTemplateService) Create( + ctx context.Context, + workspaceID, ownerID, chartRefID, name, description, valuesYAML string, + isDefault bool, +) (*entity.ValuesTemplate, error) { + // 检查 Chart Reference 是否存在 + chartRef, err := s.chartRefRepo.GetByID(ctx, chartRefID) + if err != nil { + return nil, errors.New("chart reference not found") + } + + // 检查名称是否已存在 + existing, _ := s.valuesTemplateRepo.GetByName(ctx, workspaceID, chartRefID, name) + if existing != nil { + return nil, ErrTemplateExists + } + + template := entity.NewValuesTemplate(workspaceID, ownerID, chartRef.ID, name, valuesYAML) + template.Description = description + template.IsDefault = isDefault + + if err := s.valuesTemplateRepo.Create(ctx, template); err != nil { + return nil, err + } + + return template, nil +} + +// GetByID 获取 Values 模板 +func (s *ValuesTemplateService) GetByID(ctx context.Context, id string) (*entity.ValuesTemplate, error) { + template, err := s.valuesTemplateRepo.GetByID(ctx, id) + if err != nil { + return nil, ErrTemplateNotFound + } + return template, nil +} + +// GetByWorkspace 获取工作空间的所有 Values 模板 +func (s *ValuesTemplateService) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.ValuesTemplate, error) { + return s.valuesTemplateRepo.GetByWorkspace(ctx, workspaceID) +} + +// GetByChartReference 获取 Chart Reference 的所有 Values 模板 +func (s *ValuesTemplateService) GetByChartReference(ctx context.Context, chartRefID string) ([]*entity.ValuesTemplate, error) { + return s.valuesTemplateRepo.GetByChartReference(ctx, chartRefID) +} + +// GetHistory 获取模板的版本历史 +func (s *ValuesTemplateService) GetHistory(ctx context.Context, chartRefID, name string) ([]*entity.ValuesTemplate, error) { + return s.valuesTemplateRepo.GetHistory(ctx, chartRefID, name) +} + +// Update 更新 Values 模板(创建新版本) +func (s *ValuesTemplateService) Update( + ctx context.Context, + id, description, valuesYAML string, + isDefault bool, +) (*entity.ValuesTemplate, error) { + template, err := s.valuesTemplateRepo.GetByID(ctx, id) + if err != nil { + return nil, ErrTemplateNotFound + } + + template.Description = description + template.ValuesYAML = valuesYAML + template.IsDefault = isDefault + + if err := s.valuesTemplateRepo.Update(ctx, template); err != nil { + return nil, err + } + + // 获取最新版本 + return s.valuesTemplateRepo.GetByName(ctx, template.WorkspaceID, template.ChartReferenceID, template.Name) +} + +// Delete 删除 Values 模板 +func (s *ValuesTemplateService) Delete(ctx context.Context, id string) error { + return s.valuesTemplateRepo.Delete(ctx, id) +} + +// List 列出所有 Values 模板(管理员用) +func (s *ValuesTemplateService) List(ctx context.Context) ([]*entity.ValuesTemplate, error) { + return s.valuesTemplateRepo.List(ctx) +} + +// Rollback 回滚到指定版本 +func (s *ValuesTemplateService) Rollback(ctx context.Context, templateID string) (*entity.ValuesTemplate, error) { + // 获取历史版本模板 + oldTemplate, err := s.valuesTemplateRepo.GetByID(ctx, templateID) + if err != nil { + return nil, ErrTemplateNotFound + } + + // 重新创建该版本(创建新版本,内容与旧版本相同) + newTemplate := &entity.ValuesTemplate{ + WorkspaceID: oldTemplate.WorkspaceID, + OwnerID: oldTemplate.OwnerID, + ChartReferenceID: oldTemplate.ChartReferenceID, + Name: oldTemplate.Name, + Description: oldTemplate.Description, + ValuesYAML: oldTemplate.ValuesYAML, + } + + if err := s.valuesTemplateRepo.Update(ctx, newTemplate); err != nil { + return nil, err + } + + // 获取最新版本 + return s.valuesTemplateRepo.GetByName(ctx, newTemplate.WorkspaceID, newTemplate.ChartReferenceID, newTemplate.Name) +} \ No newline at end of file diff --git a/backend/internal/domain/service/workspace_service.go b/backend/internal/domain/service/workspace_service.go new file mode 100644 index 0000000..389d05b --- /dev/null +++ b/backend/internal/domain/service/workspace_service.go @@ -0,0 +1,121 @@ +package service + +import ( + "context" + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/repository" +) + +// WorkspaceService 工作空间领域服务 +type WorkspaceService struct { + workspaceRepo repository.WorkspaceRepository + quotaRepo repository.QuotaRepository + userRepo repository.UserRepository +} + +// NewWorkspaceService 创建工作空间服务 +func NewWorkspaceService( + workspaceRepo repository.WorkspaceRepository, + quotaRepo repository.QuotaRepository, + userRepo repository.UserRepository, +) *WorkspaceService { + return &WorkspaceService{ + workspaceRepo: workspaceRepo, + quotaRepo: quotaRepo, + userRepo: userRepo, + } +} + +// Create 创建工作空间 +func (s *WorkspaceService) Create(ctx context.Context, name, description, createdBy string) (*entity.Workspace, error) { + // 检查名称是否已存在 + existing, _ := s.workspaceRepo.GetByName(ctx, name) + if existing != nil { + return nil, entity.ErrWorkspaceExists + } + + workspace := entity.NewWorkspace(name, description, createdBy) + if err := s.workspaceRepo.Create(ctx, workspace); err != nil { + return nil, err + } + + return workspace, nil +} + +// GetByID 获取工作空间 +func (s *WorkspaceService) GetByID(ctx context.Context, id string) (*entity.Workspace, error) { + return s.workspaceRepo.GetByID(ctx, id) +} + +// GetByName 获取工作空间 +func (s *WorkspaceService) GetByName(ctx context.Context, name string) (*entity.Workspace, error) { + return s.workspaceRepo.GetByName(ctx, name) +} + +// Update 更新工作空间 +func (s *WorkspaceService) Update(ctx context.Context, workspace *entity.Workspace) error { + return s.workspaceRepo.Update(ctx, workspace) +} + +// Delete 删除工作空间 +func (s *WorkspaceService) Delete(ctx context.Context, id string) error { + // 删除关联的配额 + if err := s.quotaRepo.DeleteByWorkspace(ctx, id); err != nil { + return err + } + + return s.workspaceRepo.Delete(ctx, id) +} + +// List 列出所有工作空间 +func (s *WorkspaceService) List(ctx context.Context) ([]*entity.Workspace, error) { + return s.workspaceRepo.List(ctx) +} + +// GetQuotas 获取工作空间配额 +func (s *WorkspaceService) GetQuotas(ctx context.Context, workspaceID string) ([]*entity.WorkspaceQuota, error) { + return s.quotaRepo.GetByWorkspace(ctx, workspaceID) +} + +// SetQuota 设置配额 +func (s *WorkspaceService) SetQuota(ctx context.Context, workspaceID string, resourceType entity.ResourceType, hardLimit, softLimit float64) (*entity.WorkspaceQuota, error) { + quota := entity.NewWorkspaceQuota(workspaceID, resourceType, hardLimit, softLimit) + if err := s.quotaRepo.Create(ctx, quota); err != nil { + return nil, err + } + return quota, nil +} + +// SetQuotas 批量设置配额 +func (s *WorkspaceService) SetQuotas(ctx context.Context, workspaceID string, quotas map[entity.ResourceType]struct { + HardLimit float64 + SoftLimit float64 +}) error { + for resourceType, config := range quotas { + quota := entity.NewWorkspaceQuota(workspaceID, resourceType, config.HardLimit, config.SoftLimit) + if err := s.quotaRepo.Create(ctx, quota); err != nil { + return err + } + } + return nil +} + +// GetOrCreateDefaultQuota 获取或创建默认配额 +func (s *WorkspaceService) GetOrCreateDefaultQuota(ctx context.Context, workspaceID string, resourceType entity.ResourceType) (*entity.WorkspaceQuota, error) { + quota, _ := s.quotaRepo.GetByWorkspaceAndType(ctx, workspaceID, resourceType) + if quota != nil { + return quota, nil + } + + // 创建默认配额(无限制) + quota = entity.NewWorkspaceQuota(workspaceID, resourceType, 0, 0) + if err := s.quotaRepo.Create(ctx, quota); err != nil { + return nil, err + } + return quota, nil +} + +// GetUsers 获取工作空间的用户 +func (s *WorkspaceService) GetUsers(ctx context.Context, workspaceID string) ([]*entity.User, error) { + return s.userRepo.ListByWorkspace(ctx, workspaceID) +} \ No newline at end of file diff --git a/backend/internal/pkg/jwt/jwt.go b/backend/internal/pkg/jwt/jwt.go index 5133c73..001093d 100644 --- a/backend/internal/pkg/jwt/jwt.go +++ b/backend/internal/pkg/jwt/jwt.go @@ -26,98 +26,106 @@ func NewJWTManager(secretKey string) *JWTManager { // Claims JWT Claims type Claims struct { - UserID string `json:"user_id"` - Username string `json:"username"` + UserID string `json:"user_id"` + Username string `json:"username"` + Role string `json:"role"` + WorkspaceID string `json:"workspace_id"` jwt.RegisteredClaims } // Generate 生成 Access Token 和 Refresh Token -func (m *JWTManager) Generate(userID, username string) (accessToken, refreshToken string, err error) { +func (m *JWTManager) Generate(userID, username, role, workspaceID string) (accessToken, refreshToken string, err error) { // 生成 Access Token accessClaims := &Claims{ - UserID: userID, - Username: username, + UserID: userID, + Username: username, + Role: role, + WorkspaceID: workspaceID, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(AccessTokenDuration)), IssuedAt: jwt.NewNumericDate(time.Now()), }, } - + accessTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims) accessToken, err = accessTokenObj.SignedString([]byte(m.secretKey)) if err != nil { return "", "", fmt.Errorf("failed to sign access token: %w", err) } - + // 生成 Refresh Token refreshClaims := &Claims{ - UserID: userID, - Username: username, + UserID: userID, + Username: username, + Role: role, + WorkspaceID: workspaceID, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(RefreshTokenDuration)), IssuedAt: jwt.NewNumericDate(time.Now()), }, } - + refreshTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims) refreshToken, err = refreshTokenObj.SignedString([]byte(m.secretKey)) if err != nil { return "", "", fmt.Errorf("failed to sign refresh token: %w", err) } - + return accessToken, refreshToken, nil } // Verify 验证 Token -func (m *JWTManager) Verify(tokenString string) (userID, username string, err error) { - userID, username, _, err = m.VerifyWithIssuedAt(tokenString) - return userID, username, err +func (m *JWTManager) Verify(tokenString string) (userID, username, role, workspaceID string, err error) { + userID, username, role, workspaceID, _, err = m.VerifyWithIssuedAt(tokenString) + return userID, username, role, workspaceID, err } // VerifyWithIssuedAt 验证 Token 并返回签发时间 -func (m *JWTManager) VerifyWithIssuedAt(tokenString string) (userID, username string, issuedAt int64, err error) { +func (m *JWTManager) VerifyWithIssuedAt(tokenString string) (userID, username, role, workspaceID string, issuedAt int64, err error) { token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(m.secretKey), nil }) - + if err != nil { - return "", "", 0, fmt.Errorf("failed to parse token: %w", err) + return "", "", "", "", 0, fmt.Errorf("failed to parse token: %w", err) } - + if claims, ok := token.Claims.(*Claims); ok && token.Valid { - return claims.UserID, claims.Username, claims.IssuedAt.Unix(), nil + return claims.UserID, claims.Username, claims.Role, claims.WorkspaceID, claims.IssuedAt.Unix(), nil } - - return "", "", 0, fmt.Errorf("invalid token") + + return "", "", "", "", 0, fmt.Errorf("invalid token") } // Refresh 刷新 Token func (m *JWTManager) Refresh(refreshToken string) (string, error) { // 验证 Refresh Token - userID, username, err := m.Verify(refreshToken) + userID, username, role, workspaceID, err := m.Verify(refreshToken) if err != nil { return "", fmt.Errorf("invalid refresh token: %w", err) } - + // 生成新的 Access Token accessClaims := &Claims{ - UserID: userID, - Username: username, + UserID: userID, + Username: username, + Role: role, + WorkspaceID: workspaceID, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(AccessTokenDuration)), IssuedAt: jwt.NewNumericDate(time.Now()), }, } - + accessTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims) newAccessToken, err := accessTokenObj.SignedString([]byte(m.secretKey)) if err != nil { return "", fmt.Errorf("failed to sign new access token: %w", err) } - + return newAccessToken, nil } diff --git a/backend/ocdp-backend b/backend/ocdp-backend new file mode 100644 index 0000000..55852c8 Binary files /dev/null and b/backend/ocdp-backend differ diff --git a/backend/scripts/init-db.sql b/backend/scripts/init-db.sql index 3d9a58c..267c90b 100644 --- a/backend/scripts/init-db.sql +++ b/backend/scripts/init-db.sql @@ -6,7 +6,11 @@ CREATE TABLE IF NOT EXISTS users ( id VARCHAR(36) PRIMARY KEY, username VARCHAR(255) NOT NULL UNIQUE, password_hash TEXT NOT NULL, - email VARCHAR(255) NOT NULL, + email VARCHAR(255), + role VARCHAR(20) NOT NULL DEFAULT 'user', + workspace_id VARCHAR(36), + is_active BOOLEAN DEFAULT TRUE, + must_change_password BOOLEAN DEFAULT FALSE, revoked_after TIMESTAMP NOT NULL DEFAULT '1970-01-01 00:00:00', created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP @@ -24,15 +28,21 @@ COMMENT ON COLUMN users.email IS '邮箱'; -- ===== Clusters 表 ===== CREATE TABLE IF NOT EXISTS clusters ( id VARCHAR(36) PRIMARY KEY, - name VARCHAR(255) NOT NULL UNIQUE, + workspace_id VARCHAR(36), + owner_id VARCHAR(36), + name VARCHAR(255) NOT NULL, host TEXT NOT NULL, ca_data TEXT, cert_data TEXT, key_data TEXT, token TEXT, description TEXT, + isolation_mode VARCHAR(20) DEFAULT 'namespace', + default_namespace VARCHAR(255), + is_shared BOOLEAN DEFAULT FALSE, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(workspace_id, name) ); CREATE INDEX IF NOT EXISTS idx_clusters_name ON clusters(name); @@ -116,6 +126,69 @@ COMMENT ON COLUMN instances.last_operation IS '最后一次操作类型'; COMMENT ON COLUMN instances.last_error IS '最近一次错误信息'; COMMENT ON COLUMN instances.revision IS 'Helm Release Revision'; +-- ===== Workspaces 表 ===== +CREATE TABLE IF NOT EXISTS workspaces ( + id VARCHAR(36) PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE, + description TEXT, + created_by VARCHAR(36), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_workspaces_name ON workspaces(name); + +-- ===== Storage Backends 表 ===== +CREATE TABLE IF NOT EXISTS storage_backends ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36), + name VARCHAR(255) NOT NULL, + type VARCHAR(50) NOT NULL, + config JSONB NOT NULL, + description TEXT, + is_default BOOLEAN DEFAULT FALSE, + is_shared BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(workspace_id, name) +); + +CREATE INDEX IF NOT EXISTS idx_storage_workspace ON storage_backends(workspace_id); + +-- ===== Chart References 表 ===== +CREATE TABLE IF NOT EXISTS chart_references ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36), + registry_id VARCHAR(36), + repository VARCHAR(500) NOT NULL, + chart_name VARCHAR(255) NOT NULL, + description TEXT, + is_enabled BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_chart_workspace ON chart_references(workspace_id); +CREATE INDEX IF NOT EXISTS idx_chart_registry ON chart_references(registry_id); + +-- ===== Values Templates 表 ===== +CREATE TABLE IF NOT EXISTS values_templates ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36), + chart_reference_id VARCHAR(36), + name VARCHAR(255) NOT NULL, + description TEXT, + values_yaml TEXT NOT NULL, + version INTEGER NOT NULL DEFAULT 1, + is_default BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(workspace_id, chart_reference_id, name) +); + +CREATE INDEX IF NOT EXISTS idx_values_template_chart ON values_templates(chart_reference_id); +CREATE INDEX IF NOT EXISTS idx_values_template_workspace ON values_templates(workspace_id); + -- ===== 数据库版本表 ===== CREATE TABLE IF NOT EXISTS schema_migrations ( version VARCHAR(50) PRIMARY KEY, diff --git a/backend/scripts/migration_multi_tenant.sql b/backend/scripts/migration_multi_tenant.sql new file mode 100644 index 0000000..c7e650e --- /dev/null +++ b/backend/scripts/migration_multi_tenant.sql @@ -0,0 +1,190 @@ +-- OCDP Multi-Tenant Migration Script +-- Adds multi-tenant fields and new tables for workspace isolation + +-- ===== Phase 1: Add new columns to existing tables ===== + +-- Add multi-tenant fields to users table +ALTER TABLE users ADD COLUMN IF NOT EXISTS role VARCHAR(20) NOT NULL DEFAULT 'user'; +ALTER TABLE users ADD COLUMN IF NOT EXISTS workspace_id VARCHAR(36); +ALTER TABLE users ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT TRUE; +ALTER TABLE users ADD COLUMN IF NOT EXISTS must_change_password BOOLEAN NOT NULL DEFAULT FALSE; + +-- Add indexes for new user fields +CREATE INDEX IF NOT EXISTS idx_users_role ON users(role); +CREATE INDEX IF NOT EXISTS idx_users_workspace_id ON users(workspace_id); +CREATE INDEX IF NOT EXISTS idx_users_is_active ON users(is_active); + +-- Add multi-tenant fields to clusters table +ALTER TABLE clusters ADD COLUMN IF NOT EXISTS workspace_id VARCHAR(36); +ALTER TABLE clusters ADD COLUMN IF NOT EXISTS owner_id VARCHAR(36); +ALTER TABLE clusters ADD COLUMN IF NOT EXISTS isolation_mode VARCHAR(20) NOT NULL DEFAULT 'namespace'; +ALTER TABLE clusters ADD COLUMN IF NOT EXISTS default_namespace VARCHAR(255); +ALTER TABLE clusters ADD COLUMN IF NOT EXISTS is_shared BOOLEAN NOT NULL DEFAULT FALSE; + +-- Add index for cluster workspace +CREATE INDEX IF NOT EXISTS idx_clusters_workspace_id ON clusters(workspace_id); + +-- Add multi-tenant fields to registries table +ALTER TABLE registries ADD COLUMN IF NOT EXISTS workspace_id VARCHAR(36); +ALTER TABLE registries ADD COLUMN IF NOT EXISTS owner_id VARCHAR(36); +ALTER TABLE registries ADD COLUMN IF NOT EXISTS is_shared BOOLEAN NOT NULL DEFAULT FALSE; + +-- Add index for registry workspace +CREATE INDEX IF NOT EXISTS idx_registries_workspace_id ON registries(workspace_id); + +-- Add multi-tenant fields to instances table +ALTER TABLE instances ADD COLUMN IF NOT EXISTS workspace_id VARCHAR(36); +ALTER TABLE instances ADD COLUMN IF NOT EXISTS owner_id VARCHAR(36); +ALTER TABLE instances ADD COLUMN IF NOT EXISTS values_template_id VARCHAR(36); +ALTER TABLE instances ADD COLUMN IF NOT EXISTS user_override_yaml TEXT; +ALTER TABLE instances ADD COLUMN IF NOT EXISTS cpu_requested DECIMAL(10,2) NOT NULL DEFAULT 0; +ALTER TABLE instances ADD COLUMN IF NOT EXISTS memory_requested VARCHAR(50) NOT NULL DEFAULT '0Mi'; +ALTER TABLE instances ADD COLUMN IF NOT EXISTS gpu_requested DECIMAL(10,2) NOT NULL DEFAULT 0; +ALTER TABLE instances ADD COLUMN IF NOT EXISTS gpu_memory_requested VARCHAR(50) NOT NULL DEFAULT '0Mi'; + +-- Add index for instance workspace +CREATE INDEX IF NOT EXISTS idx_instances_workspace_id ON instances(workspace_id); + +-- ===== Phase 2: Create new tables ===== + +-- Create workspaces table +CREATE TABLE IF NOT EXISTS workspaces ( + id VARCHAR(36) PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE, + description TEXT, + created_by VARCHAR(36), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_workspaces_name ON workspaces(name); + +-- Create workspace_quotas table +CREATE TABLE IF NOT EXISTS workspace_quotas ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36) NOT NULL, + resource_type VARCHAR(50) NOT NULL, + hard_limit DECIMAL(10,2) NOT NULL, + soft_limit DECIMAL(10,2) NOT NULL, + used DECIMAL(10,2) NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(workspace_id, resource_type), + CONSTRAINT fk_workspace_quotas_workspace FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_workspace_quotas_workspace_id ON workspace_quotas(workspace_id); + +-- Create storage_backends table +CREATE TABLE IF NOT EXISTS storage_backends ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36), + owner_id VARCHAR(36), + name VARCHAR(255) NOT NULL, + type VARCHAR(50) NOT NULL, + config JSONB NOT NULL, + description TEXT, + is_default BOOLEAN NOT NULL DEFAULT FALSE, + is_shared BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(workspace_id, name) +); + +CREATE INDEX IF NOT EXISTS idx_storage_backends_workspace_id ON storage_backends(workspace_id); + +-- Create chart_references table +CREATE TABLE IF NOT EXISTS chart_references ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36), + registry_id VARCHAR(36), + repository VARCHAR(500) NOT NULL, + chart_name VARCHAR(255) NOT NULL, + description TEXT, + is_enabled BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(workspace_id, registry_id, repository) +); + +CREATE INDEX IF NOT EXISTS idx_chart_references_workspace_id ON chart_references(workspace_id); +CREATE INDEX IF NOT EXISTS idx_chart_references_registry_id ON chart_references(registry_id); + +-- Create values_templates table +CREATE TABLE IF NOT EXISTS values_templates ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36), + owner_id VARCHAR(36), + chart_reference_id VARCHAR(36), + name VARCHAR(255) NOT NULL, + description TEXT, + values_yaml TEXT NOT NULL, + version INTEGER NOT NULL DEFAULT 1, + is_default BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(workspace_id, chart_reference_id, name) +); + +CREATE INDEX IF NOT EXISTS idx_values_templates_workspace_id ON values_templates(workspace_id); +CREATE INDEX IF NOT EXISTS idx_values_templates_chart_reference_id ON values_templates(chart_reference_id); + +-- Create user_config_overrides table +CREATE TABLE IF NOT EXISTS user_config_overrides ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36), + user_id VARCHAR(36), + target_type VARCHAR(50) NOT NULL, + target_id VARCHAR(36), + config JSONB NOT NULL, + priority INTEGER NOT NULL DEFAULT 0, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_user_config_overrides_workspace_id ON user_config_overrides(workspace_id); +CREATE INDEX IF NOT EXISTS idx_user_config_overrides_user_id ON user_config_overrides(user_id); + +-- Create audit_logs table +CREATE TABLE IF NOT EXISTS audit_logs ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36), + user_id VARCHAR(36), + action VARCHAR(100) NOT NULL, + resource_type VARCHAR(50) NOT NULL, + resource_id VARCHAR(36), + resource_name VARCHAR(255), + details JSONB, + ip_address VARCHAR(50), + user_agent TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_audit_logs_workspace_id ON audit_logs(workspace_id); +CREATE INDEX IF NOT EXISTS idx_audit_logs_user_id ON audit_logs(user_id); +CREATE INDEX IF NOT EXISTS idx_audit_logs_created_at ON audit_logs(created_at); + +-- ===== Phase 3: Create admin user ===== +-- Note: Default password is 'admin123' (bcrypt hash will be set by application) +-- The admin user will have NULL workspace_id to indicate global access + +INSERT INTO users (id, username, password_hash, email, role, workspace_id, is_active, must_change_password) +VALUES ( + '00000000-0000-0000-0000-000000000001', + 'admin', + '$2a$10$placeholder', -- Replace with actual bcrypt hash in production + 'admin@ocdp.local', + 'admin', + NULL, + TRUE, + TRUE +) ON CONFLICT (username) DO NOTHING; + +-- Update schema version +INSERT INTO schema_migrations (version) VALUES ('v2.0.0-multi-tenant') +ON CONFLICT (version) DO NOTHING; + +-- Grant permissions (adjust as needed for your setup) +-- GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ocdp_user; +-- GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ocdp_user; \ No newline at end of file diff --git a/backend/scripts/migrations/20250409_add_workspace_quotas.sql b/backend/scripts/migrations/20250409_add_workspace_quotas.sql new file mode 100644 index 0000000..6014e0e --- /dev/null +++ b/backend/scripts/migrations/20250409_add_workspace_quotas.sql @@ -0,0 +1,257 @@ +-- OCDP 多租户权限系统迁移脚本 +-- 版本: v2.0.0 +-- 日期: 2026-04-09 + +-- ===== 1. 修改 users 表 ===== +ALTER TABLE users ADD COLUMN IF NOT EXISTS role VARCHAR(20) NOT NULL DEFAULT 'user'; +ALTER TABLE users ADD COLUMN IF NOT EXISTS workspace_id VARCHAR(36); +ALTER TABLE users ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT TRUE; +ALTER TABLE users ADD COLUMN IF NOT EXISTS must_change_password BOOLEAN DEFAULT FALSE; + +-- 添加索引 +CREATE INDEX IF NOT EXISTS idx_users_role ON users(role); +CREATE INDEX IF NOT EXISTS idx_users_workspace ON users(workspace_id); +CREATE INDEX IF NOT EXISTS idx_users_is_active ON users(is_active); + +COMMENT ON COLUMN users.role IS '用户角色: admin, user'; +COMMENT ON COLUMN users.workspace_id IS '所属工作空间 ID'; +COMMENT ON COLUMN users.is_active IS '账户是否激活'; +COMMENT ON COLUMN users.must_change_password IS '首次登录必须修改密码'; + +-- ===== 2. 创建 workspaces 表 ===== +CREATE TABLE IF NOT EXISTS workspaces ( + id VARCHAR(36) PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE, + description TEXT, + created_by VARCHAR(36) REFERENCES users(id), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_workspaces_name ON workspaces(name); +CREATE INDEX IF NOT EXISTS idx_workspaces_created_by ON workspaces(created_by); + +COMMENT ON TABLE workspaces IS '工作空间/租户表'; + +-- ===== 3. 创建 workspace_quotas 表 ===== +CREATE TABLE IF NOT EXISTS workspace_quotas ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36) NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + resource_type VARCHAR(50) NOT NULL, -- 'cpu', 'gpu', 'gpu_memory' + hard_limit DECIMAL(10,2) NOT NULL DEFAULT 0, -- 硬限制(0表示无限制) + soft_limit DECIMAL(10,2) NOT NULL DEFAULT 0, -- 软限制(警告阈值) + used DECIMAL(10,2) DEFAULT 0, -- 当前使用量 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(workspace_id, resource_type) +); + +CREATE INDEX IF NOT EXISTS idx_workspace_quotas_workspace ON workspace_quotas(workspace_id); +CREATE INDEX IF NOT EXISTS idx_workspace_quotas_type ON workspace_quotas(resource_type); + +COMMENT ON TABLE workspace_quotas IS '工作空间资源配额表'; +COMMENT ON COLUMN workspace_quotas.resource_type IS '资源类型: cpu, gpu, gpu_memory'; +COMMENT ON COLUMN workspace_quotas.hard_limit IS '硬限制(0表示无限制)'; +COMMENT ON COLUMN workspace_quotas.soft_limit IS '软限制(警告阈值)'; +COMMENT ON COLUMN workspace_quotas.used IS '当前使用量'; + +-- ===== 4. 修改 clusters 表 ===== +ALTER TABLE clusters ADD COLUMN IF NOT EXISTS workspace_id VARCHAR(36) REFERENCES workspaces(id) ON DELETE SET NULL; +ALTER TABLE clusters ADD COLUMN IF NOT EXISTS owner_id VARCHAR(36) REFERENCES users(id); +ALTER TABLE clusters ADD COLUMN IF NOT EXISTS isolation_mode VARCHAR(20) DEFAULT 'namespace'; +ALTER TABLE clusters ADD COLUMN IF NOT EXISTS default_namespace VARCHAR(255); +ALTER TABLE clusters ADD COLUMN IF NOT EXISTS is_shared BOOLEAN DEFAULT FALSE; + +CREATE INDEX IF NOT EXISTS idx_clusters_workspace ON clusters(workspace_id); +CREATE INDEX IF NOT EXISTS idx_clusters_owner ON clusters(owner_id); +CREATE INDEX IF NOT EXISTS idx_clusters_is_shared ON clusters(is_shared); + +-- 删除旧的唯一约束,添加新的(允许同一workspace内名称唯一) +ALTER TABLE clusters DROP CONSTRAINT IF EXISTS clusters_name_key; + +COMMENT ON COLUMN clusters.workspace_id IS '所属工作空间 ID'; +COMMENT ON COLUMN clusters.owner_id IS '创建者用户 ID'; +COMMENT ON COLUMN clusters.isolation_mode IS '隔离模式: namespace(共享集群) 或 cluster(独立集群)'; +COMMENT ON COLUMN clusters.default_namespace IS '默认命名空间前缀'; +COMMENT ON COLUMN clusters.is_shared IS '是否为共享集群'; + +-- ===== 5. 修改 registries 表 ===== +ALTER TABLE registries ADD COLUMN IF NOT EXISTS workspace_id VARCHAR(36) REFERENCES workspaces(id) ON DELETE SET NULL; +ALTER TABLE registries ADD COLUMN IF NOT EXISTS owner_id VARCHAR(36) REFERENCES users(id); +ALTER TABLE registries ADD COLUMN IF NOT EXISTS is_shared BOOLEAN DEFAULT FALSE; + +CREATE INDEX IF NOT EXISTS idx_registries_workspace ON registries(workspace_id); +CREATE INDEX IF NOT EXISTS idx_registries_owner ON registries(owner_id); +CREATE INDEX IF NOT EXISTS idx_registries_is_shared ON registries(is_shared); + +-- 删除旧的唯一约束 +ALTER TABLE registries DROP CONSTRAINT IF EXISTS registries_name_key; + +COMMENT ON COLUMN registries.workspace_id IS '所属工作空间 ID'; +COMMENT ON COLUMN registries.owner_id IS '创建者用户 ID'; +COMMENT ON COLUMN registries.is_shared IS '是否为共享注册表'; + +-- ===== 6. 创建 storage_backends 表 ===== +CREATE TABLE IF NOT EXISTS storage_backends ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36) REFERENCES workspaces(id) ON DELETE CASCADE, + owner_id VARCHAR(36) REFERENCES users(id), + name VARCHAR(255) NOT NULL, + type VARCHAR(50) NOT NULL, -- 'nfs', 'pv', 'hostPath' + config JSONB NOT NULL, -- 存储配置 + description TEXT, + is_default BOOLEAN DEFAULT FALSE, + is_shared BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(workspace_id, name) +); + +CREATE INDEX IF NOT EXISTS idx_storage_backends_workspace ON storage_backends(workspace_id); +CREATE INDEX IF NOT EXISTS idx_storage_backends_owner ON storage_backends(owner_id); +CREATE INDEX IF NOT EXISTS idx_storage_backends_type ON storage_backends(type); + +COMMENT ON TABLE storage_backends IS '存储后端表 (NFS/PV/HostPath)'; +COMMENT ON COLUMN storage_backends.type IS '存储类型: nfs, pv, hostPath'; +COMMENT ON COLUMN storage_backends.config IS '存储配置 (JSON)'; + +-- ===== 7. 创建 chart_references 表 ===== +CREATE TABLE IF NOT EXISTS chart_references ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36) REFERENCES workspaces(id) ON DELETE CASCADE, + registry_id VARCHAR(36) REFERENCES registries(id) ON DELETE CASCADE, + repository VARCHAR(500) NOT NULL, -- OCI repository path + chart_name VARCHAR(255) NOT NULL, + description TEXT, + is_enabled BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(workspace_id, registry_id, repository) +); + +CREATE INDEX IF NOT EXISTS idx_chart_references_workspace ON chart_references(workspace_id); +CREATE INDEX IF NOT EXISTS idx_chart_references_registry ON chart_references(registry_id); + +COMMENT ON TABLE chart_references IS 'Helm Chart 引用表'; + +-- ===== 8. 创建 values_templates 表 ===== +CREATE TABLE IF NOT EXISTS values_templates ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36) REFERENCES workspaces(id) ON DELETE CASCADE, + owner_id VARCHAR(36) REFERENCES users(id), + chart_reference_id VARCHAR(36) REFERENCES chart_references(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + description TEXT, + values_yaml TEXT NOT NULL, + version INTEGER NOT NULL DEFAULT 1, + is_default BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(workspace_id, chart_reference_id, name, version) +); + +CREATE INDEX IF NOT EXISTS idx_values_templates_workspace ON values_templates(workspace_id); +CREATE INDEX IF NOT EXISTS idx_values_templates_chart ON values_templates(chart_reference_id); +CREATE INDEX IF NOT EXISTS idx_values_templates_name ON values_templates(name); + +COMMENT ON TABLE values_templates IS 'Values 模板表(带版本管理)'; +COMMENT ON COLUMN values_templates.version IS '模板版本号'; + +-- ===== 9. 创建 user_config_overrides 表 ===== +CREATE TABLE IF NOT EXISTS user_config_overrides ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36) REFERENCES workspaces(id) ON DELETE CASCADE, + user_id VARCHAR(36) REFERENCES users(id), + target_type VARCHAR(50) NOT NULL, -- 'storage', 'template', 'global' + target_id VARCHAR(36), + config JSONB NOT NULL, -- 覆盖配置 + priority INTEGER DEFAULT 0, -- 优先级 + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_user_config_overrides_workspace ON user_config_overrides(workspace_id); +CREATE INDEX IF NOT EXISTS idx_user_config_overrides_user ON user_config_overrides(user_id); +CREATE INDEX IF NOT EXISTS idx_user_config_overrides_target ON user_config_overrides(target_type, target_id); + +COMMENT ON TABLE user_config_overrides IS '用户配置覆盖表'; +COMMENT ON COLUMN user_config_overrides.target_type IS '目标类型: storage, template, global'; +COMMENT ON COLUMN user_config_overrides.priority IS '优先级(越高越优先)'; + +-- ===== 10. 修改 instances 表 ===== +ALTER TABLE instances ADD COLUMN IF NOT EXISTS workspace_id VARCHAR(36) REFERENCES workspaces(id) ON DELETE SET NULL; +ALTER TABLE instances ADD COLUMN IF NOT EXISTS owner_id VARCHAR(36) REFERENCES users(id); +ALTER TABLE instances ADD COLUMN IF NOT EXISTS chart_reference_id VARCHAR(36) REFERENCES chart_references(id) ON DELETE SET NULL; +ALTER TABLE instances ADD COLUMN IF NOT EXISTS values_template_id VARCHAR(36) REFERENCES values_templates(id) ON DELETE SET NULL; +ALTER TABLE instances ADD COLUMN IF NOT EXISTS user_override_yaml TEXT; +ALTER TABLE instances ADD COLUMN IF NOT EXISTS cpu_requested DECIMAL(10,2) DEFAULT 0; +ALTER TABLE instances ADD COLUMN IF NOT EXISTS memory_requested VARCHAR(50) DEFAULT '0Mi'; +ALTER TABLE instances ADD COLUMN IF NOT EXISTS gpu_requested DECIMAL(10,2) DEFAULT 0; +ALTER TABLE instances ADD COLUMN IF NOT EXISTS gpu_memory_requested VARCHAR(50) DEFAULT '0Mi'; + +CREATE INDEX IF NOT EXISTS idx_instances_workspace ON instances(workspace_id); +CREATE INDEX IF NOT EXISTS idx_instances_owner ON instances(owner_id); + +COMMENT ON COLUMN instances.workspace_id IS '所属工作空间 ID'; +COMMENT ON COLUMN instances.owner_id IS '创建者用户 ID'; +COMMENT ON COLUMN instances.chart_reference_id IS 'Chart 引用 ID'; +COMMENT ON COLUMN instances.values_template_id IS 'Values 模板 ID'; +COMMENT ON COLUMN instances.user_override_yaml IS '用户覆盖配置'; +COMMENT ON COLUMN instances.cpu_requested IS '请求的 CPU 核数'; +COMMENT ON COLUMN instances.memory_requested IS '请求的内存'; +COMMENT ON COLUMN instances.gpu_requested IS '请求的 GPU 卡数'; +COMMENT ON COLUMN instances.gpu_memory_requested IS '请求的 GPU 内存'; + +-- ===== 11. 创建 audit_logs 表 ===== +CREATE TABLE IF NOT EXISTS audit_logs ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36), + user_id VARCHAR(36) REFERENCES users(id), + action VARCHAR(100) NOT NULL, -- 'create', 'update', 'delete', 'deploy', 'scale' + resource_type VARCHAR(50) NOT NULL, -- 'cluster', 'registry', 'instance', 'quota', 'user', 'workspace' + resource_id VARCHAR(36), + resource_name VARCHAR(255), + details JSONB, + ip_address VARCHAR(50), + user_agent TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_audit_logs_workspace ON audit_logs(workspace_id); +CREATE INDEX IF NOT EXISTS idx_audit_logs_user ON audit_logs(user_id); +CREATE INDEX IF NOT EXISTS idx_audit_logs_action ON audit_logs(action); +CREATE INDEX IF NOT EXISTS idx_audit_logs_resource ON audit_logs(resource_type, resource_id); +CREATE INDEX IF NOT EXISTS idx_audit_logs_created ON audit_logs(created_at); + +COMMENT ON TABLE audit_logs IS '审计日志表'; +COMMENT ON COLUMN audit_logs.action IS '操作类型: create, update, delete, deploy, scale'; +COMMENT ON COLUMN audit_logs.resource_type IS '资源类型: cluster, registry, instance, quota, user, workspace'; + +-- ===== 12. 插入迁移版本 ===== +INSERT INTO schema_migrations (version) VALUES ('v2.0.0') +ON CONFLICT (version) DO NOTHING; + +-- ===== 13. 更新现有 admin 用户为 admin 角色 ===== +UPDATE users SET role = 'admin', workspace_id = NULL WHERE username = 'admin' AND role = 'user'; + +-- ===== 14. 创建默认 workspace(可选,用于旧数据兼容)===== +-- 如果需要将现有数据迁移到默认 workspace,取消下面注释 +-- INSERT INTO workspaces (id, name, description) +-- VALUES (gen_random_uuid(), 'default', '默认工作空间') +-- ON CONFLICT (name) DO NOTHING; +-- +-- UPDATE clusters SET workspace_id = (SELECT id FROM workspaces WHERE name = 'default') +-- WHERE workspace_id IS NULL; +-- +-- UPDATE registries SET workspace_id = (SELECT id FROM workspaces WHERE name = 'default') +-- WHERE workspace_id IS NULL; +-- +-- UPDATE instances SET workspace_id = (SELECT id FROM workspaces WHERE name = 'default') +-- WHERE workspace_id IS NULL; + +-- ===== 迁移完成 ===== +DO $$ +BEGIN + RAISE NOTICE 'Migration v2.0.0 completed successfully!'; +END $$; \ No newline at end of file diff --git a/database.md b/database.md new file mode 100644 index 0000000..e2291e0 --- /dev/null +++ b/database.md @@ -0,0 +1,598 @@ +# OCDP 数据库结构说明 + +## 概述 + +OCDP (Open Container Deployment Platform) 是一个多租户容器部署平台,支持: +- 多 Workspace 隔离 +- RBAC 权限控制 (Admin / User) +- Kubernetes 集群管理 +- OCI Registry 集成 (Harbor) +- Helm Chart 部署 +- Values 模板版本管理 +- 资源配额控制 +- 审计日志 + +## 数据库配置 + +```yaml +# PostgreSQL 连接信息 +Host: localhost +Port: 5430 (docker) / 5432 (local) +Database: ocdp +User: ocdp +Password: ocdp_password +``` + +--- + +## 表结构 + +### 1. users - 用户表 + +存储用户账户信息,支持多租户和角色管理。 + +```sql +CREATE TABLE users ( + id VARCHAR(36) PRIMARY KEY, + username VARCHAR(255) NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + email VARCHAR(255) NOT NULL, + role VARCHAR(20) NOT NULL DEFAULT 'user', -- 'admin' | 'user' + workspace_id VARCHAR(36), -- 所属工作空间,admin 为 NULL 表示全局 + is_active BOOLEAN NOT NULL DEFAULT TRUE, -- 账户是否激活 + must_change_password BOOLEAN NOT NULL DEFAULT FALSE, -- 首次登录必须修改密码 + revoked_after TIMESTAMP NOT NULL DEFAULT '1970-01-01 00:00:00', -- 全局 Token 撤销时间 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +``` + +| 字段 | 类型 | 说明 | 示例 | +|------|------|------|------| +| id | VARCHAR(36) | 主键 UUID | 550e8400-e29b-41d4-a716-446655440000 | +| username | VARCHAR(255) | 用户名,唯一 | admin | +| password_hash | TEXT | bcrypt 密码哈希 | $2a$10$... | +| email | VARCHAR(255) | 邮箱 | admin@ocdp.local | +| role | VARCHAR(20) | 角色:admin/user | admin | +| workspace_id | VARCHAR(36) | 所属工作空间 ID | workspace-uuid | +| is_active | BOOLEAN | 账户是否激活 | true | +| must_change_password | BOOLEAN | 首次登录必须修改密码 | false | +| revoked_after | TIMESTAMP | Token 撤销时间(修改密码后自动撤销旧 Token) | 2024-01-01 10:00:00 | +| created_at | TIMESTAMP | 创建时间 | 2024-01-01 10:00:00 | +| updated_at | TIMESTAMP | 更新时间 | 2024-01-01 10:00:00 | + +**索引**: +- `idx_users_username` - 用户名查询 +- `idx_users_role` - 角色筛选 +- `idx_users_workspace_id` - 工作空间筛选 +- `idx_users_is_active` - 激活状态筛选 + +**角色说明**: +- `admin`: 管理员,可管理所有 Workspace 和资源,workspace_id 为 NULL +- `user`: 普通用户,仅可访问自己 Workspace 内的资源 + +--- + +### 2. workspaces - 工作空间表 + +租户/团队隔离单元。 + +```sql +CREATE TABLE workspaces ( + id VARCHAR(36) PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE, + description TEXT, + created_by VARCHAR(36), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +``` + +| 字段 | 类型 | 说明 | 示例 | +|------|------|------|------| +| id | VARCHAR(36) | 主键 UUID | workspace-uuid | +| name | VARCHAR(255) | 工作空间名称,唯一 | team-alpha | +| description | TEXT | 描述 | Alpha 团队工作空间 | +| created_by | VARCHAR(36) | 创建者用户 ID | user-uuid | +| created_at | TIMESTAMP | 创建时间 | 2024-01-01 10:00:00 | +| updated_at | TIMESTAMP | 更新时间 | 2024-01-01 10:00:00 | + +**索引**: +- `idx_workspaces_name` - 名称查询 + +--- + +### 3. workspace_quotas - 工作空间配额表 + +每个 Workspace 的资源配额限制。 + +```sql +CREATE TABLE workspace_quotas ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36) NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE, + resource_type VARCHAR(50) NOT NULL, -- 'cpu' | 'gpu' | 'gpu_memory' + hard_limit DECIMAL(10,2) NOT NULL, -- 硬限制(0 表示无限制) + soft_limit DECIMAL(10,2) NOT NULL, -- 软限制(警告阈值) + used DECIMAL(10,2) NOT NULL DEFAULT 0, -- 当前使用量 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(workspace_id, resource_type) +); +``` + +| 字段 | 类型 | 说明 | 示例 | +|------|------|------|------| +| id | VARCHAR(36) | 主键 UUID | quota-uuid | +| workspace_id | VARCHAR(36) | 所属工作空间 ID | workspace-uuid | +| resource_type | VARCHAR(50) | 资源类型:cpu/gpu/gpu_memory | cpu | +| hard_limit | DECIMAL(10,2) | 硬限制(0=无限制) | 10.00 | +| soft_limit | DECIMAL(10,2) | 软限制(警告阈值) | 8.00 | +| used | DECIMAL(10,2) | 当前使用量 | 5.00 | +| created_at | TIMESTAMP | 创建时间 | 2024-01-01 10:00:00 | +| updated_at | TIMESTAMP | 更新时间 | 2024-01-01 10:00:00 | + +**配额检查逻辑**: +1. 部署实例前检查 `used + new_request <= hard_limit` +2. 超过硬限制返回 403 Forbidden +3. 超过软限制发送警告通知 +4. 实例删除后释放配额 + +--- + +### 4. clusters - Kubernetes 集群表 + +管理 Kubernetes 集群连接信息。 + +```sql +CREATE TABLE clusters ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36), -- 所属工作空间,NULL 表示全局共享 + owner_id VARCHAR(36), -- 创建者用户 ID + name VARCHAR(255) NOT NULL UNIQUE, + host TEXT NOT NULL, -- Kubernetes API Server URL + ca_data TEXT, -- CA 证书(Base64 编码) + cert_data TEXT, -- 客户端证书(Base64 编码) + key_data TEXT, -- 客户端密钥(Base64 编码) + token TEXT, -- Bearer Token(与证书认证二选一) + description TEXT, + isolation_mode VARCHAR(20) NOT NULL DEFAULT 'namespace', -- 'namespace' | 'cluster' + default_namespace VARCHAR(255), -- 默认 namespace 前缀 + is_shared BOOLEAN NOT NULL DEFAULT FALSE, -- 是否为共享集群 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +``` + +| 字段 | 类型 | 说明 | 示例 | +|------|------|------|------| +| id | VARCHAR(36) | 主键 UUID | cluster-uuid | +| workspace_id | VARCHAR(36) | 所属工作空间 ID | workspace-uuid | +| owner_id | VARCHAR(36) | 创建者用户 ID | user-uuid | +| name | VARCHAR(255) | 集群名称,唯一 | prod-k8s | +| host | VARCHAR(255) | Kubernetes API URL | https://k8s.example.com:6443 | +| ca_data | TEXT | CA 证书 Base64 | LS0tLS1... | +| cert_data | TEXT | 客户端证书 Base64 | LS0tLS1... | +| key_data | TEXT | 客户端密钥 Base64 | LS0tLS1... | +| token | TEXT | Bearer Token | eyJhbGci... | +| description | TEXT | 描述 | 生产环境集群 | +| isolation_mode | VARCHAR(20) | 隔离模式:namespace/cluster | namespace | +| default_namespace | VARCHAR(255) | 默认 namespace 前缀 | team-alpha | +| is_shared | BOOLEAN | 是否共享(admin 创建供多 Workspace 使用) | false | +| created_at | TIMESTAMP | 创建时间 | 2024-01-01 10:00:00 | +| updated_at | TIMESTAMP | 更新时间 | 2024-01-01 10:00:00 | + +**隔离模式说明**: +- `namespace`: 共享集群模式,多个 Workspace 使用不同 namespace + - 部署时自动分配:`{default_namespace}-{instance_name}` +- `cluster`: 私有集群模式,每个 Workspace 独立集群或独立凭证 + +**认证方式**: +1. 证书认证:`ca_data` + `cert_data` + `key_data` +2. Token 认证:`token` + +--- + +### 5. registries - OCI Registry 表 + +管理 Docker/OCI 镜像仓库(支持 Harbor)。 + +```sql +CREATE TABLE registries ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36), -- 所属工作空间,NULL 表示全局共享 + owner_id VARCHAR(36), -- 创建者用户 ID + name VARCHAR(255) NOT NULL UNIQUE, + url TEXT NOT NULL, -- Registry URL + description TEXT, + username VARCHAR(255), -- 认证用户名 + password TEXT, -- 认证密码(加密存储) + insecure BOOLEAN DEFAULT FALSE, -- 是否跳过 TLS 验证 + is_shared BOOLEAN DEFAULT FALSE, -- 是否为共享 Registry + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +``` + +| 字段 | 类型 | 说明 | 示例 | +|------|------|------|------| +| id | VARCHAR(36) | 主键 UUID | registry-uuid | +| workspace_id | VARCHAR(36) | 所属工作空间 ID | workspace-uuid | +| owner_id | VARCHAR(36) | 创建者用户 ID | user-uuid | +| name | VARCHAR(255) | Registry 名称,唯一 | harbor-prod | +| url | TEXT | Registry URL | https://harbor.example.com | +| description | TEXT | 描述 | 生产环境 Harbor | +| username | VARCHAR(255) | 认证用户名 | admin | +| password | TEXT | 认证密码(加密) | encrypted... | +| insecure | BOOLEAN | 跳过 TLS 验证 | false | +| is_shared | BOOLEAN | 是否共享 | false | +| created_at | TIMESTAMP | 创建时间 | 2024-01-01 10:00:00 | +| updated_at | TIMESTAMP | 更新时间 | 2024-01-01 10:00:00 | + +--- + +### 6. instances - Helm 实例表 + +部署的 Helm Release 管理。 + +```sql +CREATE TABLE instances ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36), -- 所属工作空间 + owner_id VARCHAR(36), -- 创建者用户 ID + cluster_id VARCHAR(36) NOT NULL, + registry_id VARCHAR(36) NOT NULL, + chart_reference_id VARCHAR(36), -- 引用的 Chart 引用 + values_template_id VARCHAR(36), -- 使用的 Values 模板 + + name VARCHAR(255) NOT NULL, -- Helm Release 名称 + namespace VARCHAR(255) NOT NULL, -- Kubernetes 命名空间 + repository TEXT NOT NULL, -- OCI Repository (e.g., charts/app) + chart VARCHAR(255) NOT NULL, -- Chart 名称 + version VARCHAR(255) NOT NULL, -- Chart 版本 + description TEXT, + values JSONB, -- Helm Values (JSON) + values_yaml TEXT, -- Helm Values (YAML) + user_override_yaml TEXT, -- 用户额外覆盖配置 + + status VARCHAR(50) NOT NULL, -- 实例状态 + status_reason TEXT, -- 状态说明 + last_operation VARCHAR(50), -- 最后操作类型 + last_error TEXT, -- 最近错误 + revision INTEGER NOT NULL DEFAULT 1, -- Helm Release Revision + + cpu_requested DECIMAL(10,2) NOT NULL DEFAULT 0, -- CPU 请求量 (cores) + memory_requested VARCHAR(50) NOT NULL DEFAULT '0Mi', -- 内存请求量 + gpu_requested DECIMAL(10,2) NOT NULL DEFAULT 0, -- GPU 请求量 (cards) + gpu_memory_requested VARCHAR(50) NOT NULL DEFAULT '0Mi', -- GPU 内存请求量 + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT fk_cluster FOREIGN KEY (cluster_id) REFERENCES clusters(id) ON DELETE CASCADE, + CONSTRAINT fk_registry FOREIGN KEY (registry_id) REFERENCES registries(id) ON DELETE CASCADE, + UNIQUE (cluster_id, name, namespace) +); +``` + +| 字段 | 类型 | 说明 | 示例 | +|------|------|------|------| +| id | VARCHAR(36) | 主键 UUID | instance-uuid | +| workspace_id | VARCHAR(36) | 所属工作空间 ID | workspace-uuid | +| owner_id | VARCHAR(36) | 创建者用户 ID | user-uuid | +| cluster_id | VARCHAR(36) | 所属集群 ID | cluster-uuid | +| registry_id | VARCHAR(36) | 所属 Registry ID | registry-uuid | +| chart_reference_id | VARCHAR(36) | Chart 引用 ID | chart-ref-uuid | +| values_template_id | VARCHAR(36) | Values 模板 ID | template-uuid | +| name | VARCHAR(255) | Release 名称(RFC 1123) | my-app | +| namespace | VARCHAR(255) | Kubernetes 命名空间 | team-alpha-my-app | +| repository | TEXT | OCI Repository | harbor.example.com/charts/nginx | +| chart | VARCHAR(255) | Chart 名称 | nginx | +| version | VARCHAR(255) | Chart 版本 | 1.0.0 | +| description | TEXT | 描述 | Nginx 应用 | +| values | JSONB | Values JSON | {"replicas": 2} | +| values_yaml | TEXT | Values YAML | replicas: 2 | +| user_override_yaml | TEXT | 用户覆盖配置 | replicas: 3 | +| status | VARCHAR(50) | 状态 | deployed | +| status_reason | TEXT | 状态说明 | Install complete | +| last_operation | VARCHAR(50) | 最后操作 | install | +| last_error | TEXT | 错误信息 | - | +| revision | INTEGER | Helm Revision | 1 | +| cpu_requested | DECIMAL(10,2) | CPU 请求 | 2.00 | +| memory_requested | VARCHAR(50) | 内存请求 | 1Gi | +| gpu_requested | DECIMAL(10,2) | GPU 请求 | 0 | +| gpu_memory_requested | VARCHAR(50) | GPU 内存 | 0Mi | +| created_at | TIMESTAMP | 创建时间 | 2024-01-01 10:00:00 | +| updated_at | TIMESTAMP | 更新时间 | 2024-01-01 10:00:00 | + +**状态说明**: +| 状态 | 说明 | +|------|------| +| deployed | 部署成功 | +| failed | 部署失败 | +| pending-install | 安装中 | +| pending-upgrade | 升级中 | +| pending-rollback | 回滚中 | +| pending-delete | 删除中 | +| uninstalled | 已卸载 | +| superseded | 已被取代 | +| unknown | 未知 | + +--- + +### 7. storage_backends - 存储后端表 + +NFS/PV/HostPath 存储配置。 + +```sql +CREATE TABLE storage_backends ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36), + owner_id VARCHAR(36), + name VARCHAR(255) NOT NULL, + type VARCHAR(50) NOT NULL, -- 'nfs' | 'pv' | 'hostPath' + config JSONB NOT NULL, -- 存储配置 + description TEXT, + is_default BOOLEAN NOT NULL DEFAULT FALSE, -- 是否默认存储 + is_shared BOOLEAN NOT NULL DEFAULT FALSE, -- 是否共享 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(workspace_id, name) +); +``` + +**Config 结构**: +```json +// NFS +{"nfs": {"server": "192.168.1.100", "path": "/data"}} + +// PV +{"pv": {"storageClassName": "nfs", "capacity": "10Gi", "accessModes": ["ReadWriteMany"]}} + +// HostPath +{"hostPath": {"path": "/mnt/data"}} +``` + +--- + +### 8. chart_references - Chart 引用表 + +管理可用的 Helm Chart 引用。 + +```sql +CREATE TABLE chart_references ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36), + registry_id VARCHAR(36), + repository VARCHAR(500) NOT NULL, -- OCI repository path + chart_name VARCHAR(255) NOT NULL, + description TEXT, + is_enabled BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(workspace_id, registry_id, repository) +); +``` + +--- + +### 9. values_templates - Values 模板表 + +Helm Values 模板,支持版本管理。 + +```sql +CREATE TABLE values_templates ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36), + owner_id VARCHAR(36), + chart_reference_id VARCHAR(36), + name VARCHAR(255) NOT NULL, + description TEXT, + values_yaml TEXT NOT NULL, + version INTEGER NOT NULL DEFAULT 1, -- 模板版本号 + is_default BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(workspace_id, chart_reference_id, name) +); +``` + +**版本管理**: +- 每次更新创建新版本(version + 1) +- 支持回滚到历史版本 + +--- + +### 10. user_config_overrides - 用户配置覆盖表 + +用户个人配置覆盖。 + +```sql +CREATE TABLE user_config_overrides ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36), + user_id VARCHAR(36), + target_type VARCHAR(50) NOT NULL, -- 'storage' | 'template' | 'global' + target_id VARCHAR(36), + config JSONB NOT NULL, -- 覆盖配置 + priority INTEGER NOT NULL DEFAULT 0, -- 优先级 + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +``` + +--- + +### 11. audit_logs - 审计日志表 + +记录所有操作行为。 + +```sql +CREATE TABLE audit_logs ( + id VARCHAR(36) PRIMARY KEY, + workspace_id VARCHAR(36), + user_id VARCHAR(36), + action VARCHAR(100) NOT NULL, -- 'create' | 'update' | 'delete' | 'deploy' | 'scale' + resource_type VARCHAR(50) NOT NULL, -- 'cluster' | 'registry' | 'instance' | ... + resource_id VARCHAR(36), + resource_name VARCHAR(255), + details JSONB, + ip_address VARCHAR(50), + user_agent TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +``` + +--- + +### 12. schema_migrations - 迁移版本表 + +数据库版本记录。 + +```sql +CREATE TABLE schema_migrations ( + version VARCHAR(50) PRIMARY KEY, + applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +``` + +--- + +## ER 关系图 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ workspaces │ +│ (id, name, description, created_by, created_at, updated_at) │ +└────────────────────────────────────┬────────────────────────────────────┘ + │ 1:N + ┌────────────────────────────┼────────────────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ +│ workspace_quotas│ │ clusters │ │ registries │ +│ (workspace_id, │ │ (workspace_id, │ │ (workspace_id, │ +│ resource_type, │ │ owner_id, name, │ │ owner_id, name, │ +│ hard_limit, │ │ host, is_shared) │ │ url, is_shared) │ +│ soft_limit, used)│ └─────────┬─────────┘ └────────┬─────────┘ +└───────────────────┘ │ │ + │ │ + ┌───────────────────────────┼───────────────────────┘ + │ │ + ▼ ▼ +┌───────────────────┐ ┌───────────────────┐ +│ instances │ │ storage_backends│ +│ (workspace_id, │ │ (workspace_id, │ +│ owner_id, │ │ owner_id, name, │ +│ cluster_id, │ │ type, config) │ +│ registry_id, │ └───────────────────┘ +│ values_template) │ +└───────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────┐ +│ users │ +│ (id, username, password_hash, email, role, workspace_id, is_active) │ +└────────────────────────────────────┬────────────────────────────────────┘ + │ + ┌────────────────────────────┼────────────────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ +│ chart_references│ │ values_templates │ │ audit_logs │ +│ (workspace_id, │ │ (workspace_id, │ │ (user_id, action,│ +│ registry_id, │ │ owner_id, │ │ resource_type) │ +│ repository) │ │ chart_ref_id) │ └───────────────────┘ +└───────────────────┘ └───────────────────┘ +``` + +--- + +## 资源可见性规则 + +| 用户角色 | 可见范围 | +|---------|---------| +| Admin | 所有 Workspace 的所有资源(workspace_id 为 NULL 或有值都能看到) | +| User | 仅自己 Workspace 的资源 | +| 共享资源 | `is_shared=TRUE` 时,同 Workspace 内可见 | + +--- + +## 常用 SQL 操作 + +### 查询用户及其 Workspace +```sql +SELECT u.id, u.username, u.role, w.name as workspace_name +FROM users u +LEFT JOIN workspaces w ON u.workspace_id = w.id +WHERE u.is_active = TRUE; +``` + +### 查询 Workspace 配额使用情况 +```sql +SELECT w.name as workspace, + q.resource_type, + q.hard_limit, + q.soft_limit, + q.used, + CASE WHEN q.hard_limit > 0 THEN ROUND(q.used / q.hard_limit * 100, 2) ELSE 0 END as usage_percent +FROM workspace_quotas q +JOIN workspaces w ON q.workspace_id = w.id; +``` + +### 查询用户可用的集群 +```sql +-- Admin: 所有集群 +SELECT * FROM clusters; + +-- User: 自己 Workspace 的集群 + 共享集群 +SELECT * FROM clusters +WHERE workspace_id = 'user-workspace-id' + OR is_shared = TRUE; +``` + +### 查询实例状态统计 +```sql +SELECT status, COUNT(*) as count +FROM instances +WHERE workspace_id = 'workspace-id' +GROUP BY status; +``` + +### 查询审计日志 +```sql +SELECT a.created_at, u.username, a.action, a.resource_type, a.resource_name +FROM audit_logs a +JOIN users u ON a.user_id = u.id +WHERE a.workspace_id = 'workspace-id' +ORDER BY a.created_at DESC +LIMIT 50; +``` + +--- + +## 迁移历史 + +| 版本 | 说明 | 日期 | +|------|------|------| +| v1.0.0 | 初始版本(单租户) | 2024-01 | +| v2.0.0-multi-tenant | 多租户迁移:添加 workspaces, quotas, 扩展 users/clusters/registries/instances | 2025-04 | + +--- + +## 初始数据 + +### 创建 Admin 用户 +```sql +-- 默认密码: admin123 (bcrypt hash 需由应用设置) +INSERT INTO users (id, username, password_hash, email, role, workspace_id, is_active, must_change_password) +VALUES ( + '00000000-0000-0000-0000-000000000001', + 'admin', + '$2a$10$placeholder', -- 由应用初始化时设置 + 'admin@ocdp.local', + 'admin', + NULL, -- admin 的 workspace_id 为 NULL,表示全局 + TRUE, + TRUE -- 首次登录必须修改密码 +); +``` \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index b7ce80d..6310907 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,43 +10,35 @@ # - 本文件只负责前端构建和 Nginx。 # - Backend / PostgreSQL / pgAdmin 由 backend/docker-compose.yml 提供。 # - Nginx 统一监听 80/443(默认映射 WEB_HTTP_PORT=80、WEB_HTTPS_PORT=443), -# 根据路径转发:/api/* → backend,其他路径 → 前端静态文件。 +# 根据路径转发:/api/* → backend,其他路径 → 前端服务。 # ================================================== services: # -------------------------------------------------- - # 构建前端静态资源 (一次性 Job) + # Next.js 前端服务 # -------------------------------------------------- - frontend-build: + frontend: image: node:20-alpine - container_name: ocdp-frontend-build + container_name: ocdp-frontend init: true working_dir: /app - restart: "no" + restart: unless-stopped environment: NODE_ENV: production - NPM_CONFIG_PRODUCTION: "false" # ensure devDependencies (tsc, vite) are installed for build - # 默认通过 Nginx 代理到 backend -> /api/v1 - VITE_API_BASE_URL: ${VITE_API_BASE_URL:-/api/v1} - FILE_OWNER_UID: ${FILE_OWNER_UID:-1000} - FILE_OWNER_GID: ${FILE_OWNER_GID:-1000} - command: > - sh -c " - set -eux; - npm ci; - npm run build; - mkdir -p /build; - rm -rf /build/*; - cp -R dist/. /build/; - if [ -d dist ]; then chown -R "$$FILE_OWNER_UID:$$FILE_OWNER_GID" dist; fi; - if [ -d /build ]; then chown -R "$$FILE_OWNER_UID:$$FILE_OWNER_GID" /build; fi - " + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-/api/v1} + command: sh -c "npm install --include=dev && npm run build && npm run start" + expose: + - "3000" volumes: - ./frontend:/app - frontend_node_modules:/app/node_modules - - frontend_dist:/build networks: - ocdp-network + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:3000 || exit 1"] + interval: 30s + timeout: 10s + retries: 3 # -------------------------------------------------- # Nginx - 静态文件 + /api 反向代理统一入口 @@ -55,13 +47,12 @@ services: image: nginx:1.27-alpine container_name: ocdp-nginx depends_on: - frontend-build: - condition: service_completed_successfully + frontend: + condition: service_started ports: - "${WEB_HTTP_PORT:-80}:80" - "${WEB_HTTPS_PORT:-443}:443" volumes: - - frontend_dist:/usr/share/nginx/html:ro - ./infra/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro - ./infra/nginx/certs:/etc/nginx/certs:ro healthcheck: @@ -84,7 +75,5 @@ networks: # Volumes # ================================================== volumes: - frontend_dist: - driver: local frontend_node_modules: driver: local diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 572853e..0000000 --- a/docs/README.md +++ /dev/null @@ -1,212 +0,0 @@ -# OCDP 文档中心 - -欢迎查阅 OCDP 项目文档。本目录包含开发、部署、功能和安全相关的详细文档。 - ---- - -## 📚 文档导航 - -### 🚀 快速开始 - -新用户请先阅读根目录的快速开始文档: - -- **[快速开始指南](../QUICK_START.md)** - 5分钟快速上手 -- **[使用指南](../USAGE_GUIDE.md)** - Docker 统一配置详细说明 -- **[命令速查表](../COMMANDS_CHEATSHEET.md)** - 常用命令快速参考 - ---- - -## 📖 核心文档 - -### 🔧 开发文档 - -- **[开发规范](./development/specification.md)** - - 代码规范和最佳实践 - - 项目架构说明 - - 开发工作流程 -- **[命名约定对照表](./development/naming-conventions.md)** - - 前后端命名一致性 - - JSON 与 OpenAPI 映射 - - 常见字段示例 -- **[Go vs TypeScript 对照](./development/go-vs-typescript.md)** - - 命名约定差异 - - 自动转换方案 - - 双端代码示例 - -### 🎨 功能文档 - -- **[Artifact MediaType 过滤](./features/ARTIFACT_MEDIATYPE_FILTER.md)** - - 功能说明和技术实现 - - API 使用示例 - - 类型识别规则 - -- **[MediaType 过滤测试](./features/TESTING_MEDIATYPE_FILTER.md)** - - 功能测试指南 - - 测试场景和用例 - - 故障排查 - -### 🚢 部署文档 - -- **[Docker 部署指南](./deployment/docker-guide.md)** - - Docker 环境搭建 - - 生产环境部署 - - 配置说明 - -### 🔒 安全文档 - -- **[安全实践](./security/security-implementation.md)** - - 安全配置指南 - - 认证和授权 - - 数据加密 - ---- - -## 📁 文档结构 - -``` -docs/ -├── README.md # 本文档(文档索引) -│ -├── development/ # 开发相关 -│ ├── go-vs-typescript.md # Go / TS 命名对照 -│ ├── naming-conventions.md # 命名约定 -│ └── specification.md # 开发规范 -│ -├── features/ # 功能文档 -│ ├── ARTIFACT_MEDIATYPE_FILTER.md # Artifact 过滤功能 -│ └── TESTING_MEDIATYPE_FILTER.md # 功能测试指南 -│ -├── deployment/ # 部署相关 -│ └── docker-guide.md # Docker 部署指南 -│ -├── security/ # 安全相关 -│ └── security-implementation.md # 安全实践 -│ -└── archive/ # 历史归档 - ├── root-cleanup/ # 根目录清理存档 - └── … # 其他里程碑记录 -``` - ---- - -## 🔗 相关资源 - -### 根目录文档 - -项目根目录还包含以下重要文档: - -- **[README.md](../README.md)** - 项目主页和概述 -- **[QUICK_START.md](../QUICK_START.md)** - 5分钟快速开始 -- **[USAGE_GUIDE.md](../USAGE_GUIDE.md)** - 详细使用指南 -- **[COMMANDS_CHEATSHEET.md](../COMMANDS_CHEATSHEET.md)** - 命令速查表 - -### 归档文档 - -历史报告与结果被移动到 `docs/archive/`,保留查阅但不再在根目录占位。例如: - -- `docs/archive/root-cleanup/` - 命名迁移、测试总结等历史记录 -- `docs/archive/PROJECT_RESTRUCTURE_SUMMARY.md` 等 - -### API 文档 - -- **[OpenAPI 规范](../backend/docs/openapi.yaml)** - RESTful API 定义 - ---- - -## 🎯 按场景查找文档 - -### 我是新手,想快速了解项目 - -1. [README.md](../README.md) - 项目概述 -2. [QUICK_START.md](../QUICK_START.md) - 快速开始 -3. [USAGE_GUIDE.md](../USAGE_GUIDE.md) - 使用指南 - -### 我要开始开发 - -1. [开发规范](./development/specification.md) - 了解开发规范 -2. [USAGE_GUIDE.md](../USAGE_GUIDE.md) - 了解如何运行项目 -3. [COMMANDS_CHEATSHEET.md](../COMMANDS_CHEATSHEET.md) - 常用命令 - -### 我要部署到生产环境 - -1. [Docker 部署指南](./deployment/docker-guide.md) - 部署步骤 -2. [USAGE_GUIDE.md](../USAGE_GUIDE.md) - 运行与配置 -3. [安全实践](./security/security-implementation.md) - 安全配置 - -### 我要了解某个功能 - -1. [功能文档](./features/) - 查看功能列表 -2. [OpenAPI 规范](../backend/docs/openapi.yaml) - API 定义 - ---- - -## 📝 文档编写指南 - -如果您想为项目贡献文档: - -### 文档原则 - -- ✅ **清晰简洁** - 使用简单直接的语言 -- ✅ **结构化** - 使用标题、列表、代码块 -- ✅ **示例丰富** - 提供实际的命令和代码示例 -- ✅ **保持更新** - 及时更新过时的内容 - -### 文档分类 - -- **开发文档** → `docs/development/` -- **功能文档** → `docs/features/` -- **部署文档** → `docs/deployment/` -- **安全文档** → `docs/security/` -- **快速参考** → 项目根目录 - -### Markdown 格式 - -```markdown -# 标题 - -## 二级标题 - -### 三级标题 - -- 列表项 -- 列表项 - -\`\`\`bash -# 代码示例 -make docker-dev -\`\`\` - -**粗体** 和 *斜体* -``` - ---- - -## 🆘 需要帮助? - -如果文档中没有找到您需要的信息: - -1. 💬 查看项目 [GitHub Discussions](https://github.com/your-repo/discussions) -2. 🐛 提交 [GitHub Issue](https://github.com/your-repo/issues) -3. 📧 联系项目维护者 - ---- - -## 📊 文档更新记录 - -### 2025-11-11 -- ✅ 移动根目录历史文档到 `docs/archive/` -- ✅ 新增开发类文档(命名约定、Go/TS 对照) -- ✅ 更新文档索引与结构展示 -- ✅ 保持根目录仅包含核心入门文档 - -### 2025-11-09 -- ✅ 清理重复和过时的文档 -- ✅ 整理文档结构 -- ✅ 更新文档索引 -- ✅ 精简文档数量从 13 个减少到 6 个 - ---- - -
- 保持文档简洁,提升查找效率 -
diff --git a/docs/archive/CLEANUP_SUMMARY.md b/docs/archive/CLEANUP_SUMMARY.md deleted file mode 100644 index 136d3fb..0000000 --- a/docs/archive/CLEANUP_SUMMARY.md +++ /dev/null @@ -1,307 +0,0 @@ -# Docker Compose 文件清理总结 - -## ✅ 清理完成 - -已成功将多个 docker-compose 文件整合为单一配置文件,使用 Docker Compose profiles 功能实现不同运行模式。 - ---- - -## 📦 清理前后对比 - -### 清理前(3个文件) - -``` -ocdp-go/ -├── docker-compose.yml # 生产模式基础配置 -├── docker-compose.dev.yml # 开发模式覆盖配置 -└── docker-compose.mock.yml # Mock 测试模式配置 -``` - -**问题**: -- ❌ 配置分散在多个文件 -- ❌ 需要使用 `-f` 参数组合文件 -- ❌ 维护困难,容易出现配置不一致 -- ❌ 命令复杂:`docker compose -f docker-compose.yml -f docker-compose.dev.yml up` - -### 清理后(1个文件) - -``` -ocdp-go/ -└── docker-compose.yml # 统一配置(使用 profiles) -``` - -**优势**: -- ✅ 所有配置集中在一个文件 -- ✅ 使用 Docker Compose profiles 特性 -- ✅ 易于维护和理解 -- ✅ 命令简洁:`docker compose --profile dev up` - ---- - -## 🔧 技术实现 - -### Profiles 机制 - -使用 Docker Compose 的 `profiles` 功能,定义了三种运行模式: - -| Profile | 服务 | 说明 | -|---------|------|------| -| `production` | postgres, redis, backend-prod, frontend-prod | 生产环境,真实数据库 | -| `dev` | backend-dev, frontend-dev | 开发环境,Mock 数据,热重载 | -| `mock` | backend-mock, frontend-mock | 独立测试,无外部依赖 | -| `tools` | pgadmin, swagger-ui | 可选管理工具 | - -### 服务命名 - -为了避免冲突,不同模式下的服务使用不同名称: - -- **生产模式**:`backend-prod`, `frontend-prod` -- **开发模式**:`backend-dev`, `frontend-dev` -- **Mock 模式**:`backend-mock`, `frontend-mock` - ---- - -## 🚀 使用方式 - -### 旧方式(已弃用) - -```bash -# 开发模式 -docker compose -f docker-compose.yml -f docker-compose.dev.yml up - -# 生产模式 -docker compose -f docker-compose.yml up - -# Mock 模式 -docker compose -f docker-compose.mock.yml up backend -``` - -### 新方式(推荐) - -```bash -# 开发模式 -docker compose --profile dev up -# 或 -make docker-dev - -# 生产模式 -docker compose --profile production up -# 或 -make docker-prod - -# Mock 测试后端 -docker compose --profile mock up backend-mock -# 或 -make docker-test-backend - -# Mock 测试前端 -docker compose --profile mock up frontend-mock -# 或 -make docker-test-frontend -``` - ---- - -## 📊 改进效果 - -### 文件数量 - -| 项目 | 清理前 | 清理后 | 改进 | -|------|--------|--------|------| -| Docker Compose 文件 | 3 | 1 | ⬇️ 66% | -| 配置行数 | ~400 | ~260 | ⬇️ 35% | -| 维护复杂度 | 高 | 低 | ⬇️ 显著降低 | - -### 命令简化 - -| 操作 | 清理前 | 清理后 | -|------|--------|--------| -| 启动开发环境 | `docker compose -f docker-compose.yml -f docker-compose.dev.yml up` | `docker compose --profile dev up` | -| 启动生产环境 | `docker compose -f docker-compose.yml up` | `docker compose --profile production up` | -| 测试后端 | `docker compose -f docker-compose.mock.yml up backend` | `docker compose --profile mock up backend-mock` | - ---- - -## 📝 配置文件结构 - -新的 `docker-compose.yml` 结构: - -```yaml -services: - # 数据库服务(生产模式) - postgres: - profiles: [production] - - redis: - profiles: [production] - - # 后端服务(三种模式) - backend-prod: - profiles: [production] - - backend-dev: - profiles: [dev] - - backend-mock: - profiles: [mock] - - # 前端服务(三种模式) - frontend-prod: - profiles: [production] - - frontend-dev: - profiles: [dev] - - frontend-mock: - profiles: [mock] - - # 可选工具 - pgadmin: - profiles: [tools] - - swagger-ui: - profiles: [tools] -``` - ---- - -## 🔍 迁移指南 - -### 如果您有自定义配置 - -如果您之前有自定义的 docker-compose 配置: - -1. **备份旧文件**(如果需要) -2. **更新环境变量**到新的 `docker-compose.yml` -3. **测试每个模式**确保工作正常 -4. **更新 CI/CD** 脚本使用新的命令 - -### Make 命令保持不变 - -所有 Makefile 命令保持不变,无需修改工作流程: - -```bash -make docker-dev # 仍然有效 -make docker-prod # 仍然有效 -make docker-test-backend # 仍然有效 -``` - ---- - -## 📚 更新的文档 - -已更新以下文档以反映新的配置: - -- ✅ `README.md` - 更新项目结构说明 -- ✅ `USAGE_GUIDE.md` - 新增统一配置使用指南 -- ✅ `Makefile` - 更新 Docker 命令使用 profiles -- ✅ `CLEANUP_SUMMARY.md` - 本文档 - -### 推荐阅读顺序 - -1. 📖 [README.md](./README.md) - 项目概述 -2. 📋 [USAGE_GUIDE.md](./USAGE_GUIDE.md) - 统一配置详细说明 ⭐ -3. 🚀 [QUICK_START.md](./QUICK_START.md) - 快速开始 -4. 💡 [COMMANDS_CHEATSHEET.md](./COMMANDS_CHEATSHEET.md) - 命令速查 - ---- - -## ✨ 优势总结 - -### 对开发者 - -- ✅ **更简单**:只需记住一个文件 -- ✅ **更清晰**:所有服务定义集中 -- ✅ **更灵活**:轻松切换不同模式 -- ✅ **更快速**:命令更短,输入更少 - -### 对运维 - -- ✅ **易维护**:单一配置来源 -- ✅ **易理解**:profiles 语义清晰 -- ✅ **易扩展**:添加新模式很简单 -- ✅ **易调试**:配置集中便于排查问题 - -### 对项目 - -- ✅ **更规范**:使用 Docker Compose 标准特性 -- ✅ **更现代**:符合最佳实践 -- ✅ **更专业**:配置简洁清晰 -- ✅ **更可靠**:减少配置错误的可能 - ---- - -## 🎓 Docker Compose Profiles 说明 - -Docker Compose profiles 是 Docker Compose 1.28+ 引入的特性,用于: - -1. **条件性服务启动**:只启动特定 profile 的服务 -2. **环境隔离**:不同环境使用不同 profiles -3. **配置复用**:共享基础配置,profile 区分差异 - -### 基本用法 - -```bash -# 启动特定 profile -docker compose --profile dev up - -# 启动多个 profiles -docker compose --profile production --profile tools up - -# 查看所有 profiles -docker compose config --profiles - -# 查看特定 profile 的配置 -docker compose --profile dev config -``` - ---- - -## 🔄 回滚方案 - -如果需要回滚到旧的多文件方式(不推荐): - -```bash -# 1. 恢复旧的配置文件(从 git 历史) -git log --all --full-history -- docker-compose.*.yml -git checkout -- docker-compose.dev.yml -git checkout -- docker-compose.mock.yml - -# 2. 恢复旧的 Makefile 命令 -git checkout -- Makefile -``` - -但我们**强烈建议使用新的单文件方式**,它更符合现代 Docker Compose 最佳实践。 - ---- - -## 📞 需要帮助? - -如果遇到问题: - -1. 查看 [USAGE_GUIDE.md](./USAGE_GUIDE.md) 详细说明 -2. 查看 [COMMANDS_CHEATSHEET.md](./COMMANDS_CHEATSHEET.md) 命令参考 -3. 提交 [GitHub Issue](https://github.com/your-repo/issues) - ---- - -## 🎉 总结 - -通过将 3 个 docker-compose 文件整合为 1 个: - -- ✅ **简化了项目结构** -- ✅ **降低了维护成本** -- ✅ **提升了配置清晰度** -- ✅ **保持了所有功能** -- ✅ **兼容了现有工作流** - -**配置更少,效率更高!** 🚀 - ---- - -
- 清理完成于 2025-11-09 -
- 单一配置文件,多种运行模式 -
- diff --git a/docs/archive/COMPLETION_SUMMARY.md b/docs/archive/COMPLETION_SUMMARY.md deleted file mode 100644 index aa566ab..0000000 --- a/docs/archive/COMPLETION_SUMMARY.md +++ /dev/null @@ -1,425 +0,0 @@ -# ✅ OCDP 项目重构完成总结 - -## 🎉 任务完成 - -所有项目重构任务已成功完成!项目现在拥有清晰的服务架构、完善的 Docker 支持和详尽的文档。 - ---- - -## 📦 交付清单 - -### 1. Docker 配置文件(9个) - -#### 后端 Dockerfiles -- ✅ `backend/Dockerfile` - 生产环境(连接真实数据库) -- ✅ `backend/Dockerfile.dev` - 开发环境(Air 热重载) -- ✅ `backend/Dockerfile.mock` - Mock 测试(无外部依赖) -- ✅ `backend/.air.toml` - Air 热重载配置 - -#### 前端 Dockerfiles -- ✅ `frontend/Dockerfile` - 生产环境(Nginx) -- ✅ `frontend/Dockerfile.dev` - 开发环境(Vite HMR) -- ✅ `frontend/Dockerfile.mock` - Mock 测试 - -#### Docker Compose 配置 -- ✅ `docker-compose.yml` - 生产模式(Real Mode) -- ✅ `docker-compose.dev.yml` - 开发模式(Dev Mode) -- ✅ `docker-compose.mock.yml` - Mock 模式(独立测试) - -### 2. 项目文档(7个) - -#### 主要文档 -- ✅ `README.md` - 全新的项目主页(技术栈、架构、特性) -- ✅ `QUICK_START.md` - 5分钟快速开始指南 -- ✅ `DOCKER_SERVICES.md` - 完整的 Docker 服务架构说明 -- ✅ `COMMANDS_CHEATSHEET.md` - 命令速查表 -- ✅ `PROJECT_RESTRUCTURE_SUMMARY.md` - 重构详细说明 -- ✅ `COMPLETION_SUMMARY.md` - 本文档 - -#### 功能文档(已整理到 docs/features/) -- ✅ `docs/features/ARTIFACT_MEDIATYPE_FILTER.md` - Artifact 过滤功能 -- ✅ `docs/features/TESTING_MEDIATYPE_FILTER.md` - 过滤功能测试 - -### 3. Makefile 增强 - -新增的 Docker 命令: -- ✅ `make docker-dev` - 启动开发环境 -- ✅ `make docker-dev-bg` - 后台启动开发环境 -- ✅ `make docker-prod` - 启动生产环境 -- ✅ `make docker-test-backend` - 测试后端 -- ✅ `make docker-test-frontend` - 测试前端 -- ✅ `make docker-test-backend-bg` - 后台测试后端 -- ✅ `make docker-test-frontend-bg` - 后台测试前端 -- ✅ `make docker-logs` - 查看日志 -- ✅ `make docker-down` - 停止服务 -- ✅ 以及其他 Docker 管理命令... - -### 4. 文档整理 - -- ✅ 移动 artifact 功能文档到 `docs/features/` -- ✅ 移动状态文档到 `docs/` -- ✅ 清理根目录,保持整洁 - ---- - -## 🏗️ 最终项目结构 - -``` -ocdp-go/ -├── api/ -│ └── openapi.yaml # OpenAPI 规范 -│ -├── backend/ -│ ├── cmd/api/ # 入口文件 -│ ├── internal/ # 内部代码 -│ ├── config/ # 配置文件 -│ ├── data/ # Mock 数据 -│ ├── Dockerfile # 生产环境 -│ ├── Dockerfile.dev # 开发环境 -│ ├── Dockerfile.mock # Mock 测试 -│ └── .air.toml # 热重载配置 -│ -├── frontend/ -│ ├── src/ # 源代码 -│ ├── Dockerfile # 生产环境 -│ ├── Dockerfile.dev # 开发环境 -│ ├── Dockerfile.mock # Mock 测试 -│ └── nginx.conf # Nginx 配置 -│ -├── docs/ -│ ├── features/ # 功能文档 -│ │ ├── ARTIFACT_MEDIATYPE_FILTER.md -│ │ └── TESTING_MEDIATYPE_FILTER.md -│ ├── deployment/ # 部署文档 -│ ├── development/ # 开发文档 -│ ├── DEPLOYMENT_STATUS.md -│ └── FIXES_SUMMARY.md -│ -├── docker-compose.yml # 生产模式 -├── docker-compose.dev.yml # 开发模式 -├── docker-compose.mock.yml # Mock 模式 -├── Makefile # 便捷命令 -├── README.md # 项目主页 -├── QUICK_START.md # 快速开始 -├── DOCKER_SERVICES.md # Docker 服务说明 -├── COMMANDS_CHEATSHEET.md # 命令速查表 -├── PROJECT_RESTRUCTURE_SUMMARY.md # 重构总结 -└── COMPLETION_SUMMARY.md # 完成总结(本文档) -``` - ---- - -## 🎯 三种运行模式 - -### 模式对比表 - -| 特性 | 开发模式 | 生产模式 | Mock 模式 | -|------|---------|---------|----------| -| **命令** | `make docker-dev` | `make docker-prod` | `make docker-test-backend` | -| **后端数据库** | ❌ Mock | ✅ PostgreSQL | ❌ Mock | -| **热重载** | ✅ Air + Vite | ❌ | ❌ | -| **启动时间** | ~15秒 | ~30秒 | ~5秒 | -| **资源占用** | 中 | 高 | 低 | -| **前端端口** | 5173 | 3000 | 3000 | -| **适用场景** | 日常开发 | 生产部署 | 单元测试 | - -### 快速启动命令 - -```bash -# 1. 开发模式(推荐) -make docker-dev -# 访问:http://localhost:5173 - -# 2. 生产模式 -make docker-prod -# 访问:http://localhost:3000 - -# 3. 测试后端 -make docker-test-backend -# 访问:http://localhost:8080 - -# 4. 测试前端 -make docker-test-frontend -# 访问:http://localhost:3000 -``` - ---- - -## 📚 文档导航 - -### 新手入门 -1. 📖 **开始这里** → [README.md](./README.md) -2. 🚀 **快速体验** → [QUICK_START.md](./QUICK_START.md) -3. 💡 **命令速查** → [COMMANDS_CHEATSHEET.md](./COMMANDS_CHEATSHEET.md) - -### 开发人员 -1. 🐳 **Docker 架构** → [DOCKER_SERVICES.md](./DOCKER_SERVICES.md) -2. 🔧 **重构说明** → [PROJECT_RESTRUCTURE_SUMMARY.md](./PROJECT_RESTRUCTURE_SUMMARY.md) -3. 📋 **OpenAPI** → [backend/docs/openapi.yaml](../../backend/docs/openapi.yaml) - -### 功能文档 -1. 🎨 **Artifact 过滤** → [docs/features/ARTIFACT_MEDIATYPE_FILTER.md](./docs/features/ARTIFACT_MEDIATYPE_FILTER.md) -2. 🧪 **功能测试** → [docs/features/TESTING_MEDIATYPE_FILTER.md](./docs/features/TESTING_MEDIATYPE_FILTER.md) - ---- - -## ✨ 核心特性 - -### 1. 灵活的运行模式 - -``` -开发模式 (Dev Mode) -├── 后端:Mock 适配器,无需数据库 -├── 前端:Vite Dev Server + HMR -├── 热重载:代码修改自动生效 -└── 适用:日常开发,快速迭代 - -生产模式 (Production Mode) -├── 后端:连接真实 PostgreSQL + Redis -├── 前端:Nginx 静态文件服务 -├── 完整功能:所有特性可用 -└── 适用:生产部署,集成测试 - -Mock 模式 (Mock Mode) -├── 后端:独立运行,Mock 所有依赖 -├── 前端:独立运行,可使用前端 Mock -├── 完全独立:无外部依赖 -└── 适用:单元测试,独立调试 -``` - -### 2. 完整的 Docker 支持 - -- ✅ 多阶段构建(优化镜像大小) -- ✅ 健康检查(自动重启失败的服务) -- ✅ 数据持久化(PostgreSQL + Redis volumes) -- ✅ 网络隔离(专用 Docker 网络) -- ✅ 开发优化(热重载支持) - -### 3. 便捷的 Makefile - -```bash -# 只需记住这些命令 -make docker-dev # 开发 -make docker-prod # 生产 -make docker-test-backend # 测试后端 -make docker-test-frontend# 测试前端 -make docker-logs # 查看日志 -make docker-down # 停止 -``` - ---- - -## 🚀 立即开始 - -### 步骤 1: 克隆项目 - -```bash -git clone -cd ocdp-go -``` - -### 步骤 2: 启动开发环境 - -```bash -make docker-dev -``` - -### 步骤 3: 访问应用 - -- **前端**:http://localhost:5173 -- **后端**:http://localhost:8080 -- **默认账号**:admin / admin123 - -### 步骤 4: 开始开发 - -修改代码,保存,自动重载!🎉 - ---- - -## 📊 重构成果 - -### 开发体验提升 -- ✅ **启动速度快 3倍**:开发模式从 30秒 → 10秒 -- ✅ **热重载**:代码修改立即生效,无需重启 -- ✅ **独立测试**:可单独测试任意服务 -- ✅ **清晰文档**:详细的使用指南和示例 - -### 运维效率提升 -- ✅ **标准化部署**:统一的 Docker 镜像 -- ✅ **多环境支持**:一键切换开发/测试/生产 -- ✅ **容器化隔离**:服务间独立,易于调试 -- ✅ **便捷命令**:Makefile 一键操作 - -### 代码质量提升 -- ✅ **清晰结构**:文档和代码分离 -- ✅ **灵活架构**:Mock/Real 双模式 -- ✅ **易于维护**:每个服务独立配置 -- ✅ **完善文档**:详细说明和最佳实践 - ---- - -## 🎓 学习路径 - -### 新手路径 -1. 阅读 [README.md](./README.md) - 了解项目 -2. 运行 [QUICK_START.md](./QUICK_START.md) - 快速体验 -3. 查看 [COMMANDS_CHEATSHEET.md](./COMMANDS_CHEATSHEET.md) - 常用命令 - -### 开发者路径 -1. 阅读 [DOCKER_SERVICES.md](./DOCKER_SERVICES.md) - 理解架构 -2. 运行 `make docker-dev` - 启动开发环境 -3. 修改代码 - 实践开发流程 -4. 阅读 [PROJECT_RESTRUCTURE_SUMMARY.md](./PROJECT_RESTRUCTURE_SUMMARY.md) - 深入理解 - -### 运维路径 -1. 阅读 [DOCKER_SERVICES.md](./DOCKER_SERVICES.md) - 了解部署 -2. 运行 `make docker-prod` - 生产环境 -3. 查看 [docs/deployment/](./docs/deployment/) - 部署指南 -4. 配置监控和日志 - 生产优化 - ---- - -## 🔍 关键文件说明 - -### Docker 配置文件 - -| 文件 | 用途 | 重要性 | -|------|------|--------| -| `backend/Dockerfile` | 生产环境后端镜像 | ⭐⭐⭐⭐⭐ | -| `backend/Dockerfile.dev` | 开发环境后端镜像 | ⭐⭐⭐⭐ | -| `backend/Dockerfile.mock` | Mock 测试后端镜像 | ⭐⭐⭐ | -| `frontend/Dockerfile` | 生产环境前端镜像 | ⭐⭐⭐⭐⭐ | -| `frontend/Dockerfile.dev` | 开发环境前端镜像 | ⭐⭐⭐⭐ | -| `frontend/Dockerfile.mock` | Mock 测试前端镜像 | ⭐⭐⭐ | - -### Compose 配置文件 - -| 文件 | 用途 | 重要性 | -|------|------|--------| -| `docker-compose.yml` | 生产模式配置 | ⭐⭐⭐⭐⭐ | -| `docker-compose.dev.yml` | 开发模式覆盖 | ⭐⭐⭐⭐⭐ | -| `docker-compose.mock.yml` | Mock 模式配置 | ⭐⭐⭐⭐ | - -### 文档文件 - -| 文件 | 用途 | 目标读者 | -|------|------|---------| -| `README.md` | 项目主页 | 所有人 | -| `QUICK_START.md` | 快速开始 | 新手 | -| `DOCKER_SERVICES.md` | Docker 架构 | 开发者 | -| `COMMANDS_CHEATSHEET.md` | 命令速查 | 所有人 | -| `PROJECT_RESTRUCTURE_SUMMARY.md` | 重构说明 | 开发者 | - ---- - -## 💡 最佳实践 - -### 日常开发 - -```bash -# 1. 启动开发环境 -make docker-dev - -# 2. 修改代码(自动重载) - -# 3. 查看日志 -make docker-logs - -# 4. 测试功能 -# 访问 http://localhost:5173 - -# 5. 停止 -make docker-down -``` - -### 功能测试 - -```bash -# 测试后端 API -make docker-test-backend-bg -curl http://localhost:8080/health - -# 测试前端界面 -make docker-test-frontend-bg -open http://localhost:3000 - -# 停止测试 -docker compose -f docker-compose.mock.yml down -``` - -### 生产部署 - -```bash -# 1. 配置环境变量 -export JWT_SECRET="your-secret" -export ENCRYPTION_KEY="your-32-byte-key" - -# 2. 启动生产环境 -make docker-prod - -# 3. 检查状态 -make docker-status - -# 4. 查看日志 -make docker-logs -``` - ---- - -## 🤝 贡献指南 - -欢迎贡献!请遵循: - -1. Fork 项目 -2. 创建功能分支 -3. 提交更改 -4. 推送分支 -5. 创建 Pull Request - -### 提交规范 - -``` -feat: 添加新功能 -fix: 修复 bug -docs: 更新文档 -style: 代码格式 -refactor: 重构代码 -test: 添加测试 -chore: 构建/工具变更 -``` - ---- - -## 📞 需要帮助? - -- 📖 **文档**:查看 [docs/](./docs/) 目录 -- 🐛 **问题**:提交 [GitHub Issues](https://github.com/your-repo/issues) -- 💬 **讨论**:加入社区讨论 - ---- - -## 🎊 总结 - -经过完整的重构,OCDP 项目现在具备: - -✅ **清晰的服务架构** - 前后端分离,容器化部署 -✅ **灵活的运行模式** - 开发/生产/Mock 三种模式 -✅ **完善的文档体系** - 从入门到精通 -✅ **便捷的操作命令** - Makefile 一键操作 -✅ **优秀的开发体验** - 热重载,快速迭代 -✅ **标准化的部署** - Docker Compose 编排 - -**立即开始使用 OCDP!** 🚀 - -```bash -make docker-dev -``` - ---- - -
- 重构完成于 2025-11-09 -
- Built with ❤️ by the OCDP Team -
- diff --git a/docs/archive/DOCKER_SERVICES.md b/docs/archive/DOCKER_SERVICES.md deleted file mode 100644 index 2cf335a..0000000 --- a/docs/archive/DOCKER_SERVICES.md +++ /dev/null @@ -1,435 +0,0 @@ -# OCDP Docker 服务架构 - -## 📋 项目概述 - -OCDP 采用微服务架构,包含以下核心服务: - -### 核心服务 -1. **Backend API** - Go 后端服务(支持 Mock/Production 模式) -2. **Frontend** - React + TypeScript 前端应用 -3. **PostgreSQL** - 主数据库(生产模式) -4. **Redis** - 缓存服务(生产模式) - -### 可选服务 -- **pgAdmin** - PostgreSQL 管理工具 -- **Swagger UI** - API 文档查看器 - ---- - -## 🎯 运行模式说明 - -### 1. **生产模式(Real Mode)** -- 所有服务连接真实的数据库和外部依赖 -- 适用于:生产环境、集成测试 -- 使用配置:`docker-compose.yml` - -### 2. **开发模式(Dev Mode)** -- 支持热重载(后端使用 Air,前端使用 Vite HMR) -- 后端使用 Mock 适配器,不依赖数据库 -- 前端连接后端 Mock 数据 -- 适用于:日常开发、快速迭代 -- 使用配置:`docker-compose.yml` + `docker-compose.dev.yml` - -### 3. **Mock 模式(独立测试)** -- 每个服务完全独立,Mock 所有外部依赖 -- 适用于:单独测试某个服务 -- 使用配置:`docker-compose.mock.yml` - ---- - -## 🐳 Dockerfile 说明 - -### Backend Dockerfiles - -| 文件 | 用途 | 特点 | -|------|------|------| -| `backend/Dockerfile` | 生产环境 | 多阶段构建,连接真实数据库 | -| `backend/Dockerfile.dev` | 开发环境 | 使用 Air 热重载,挂载源代码 | -| `backend/Dockerfile.mock` | Mock 测试 | Mock 所有外部依赖,独立运行 | - -### Frontend Dockerfiles - -| 文件 | 用途 | 特点 | -|------|------|------| -| `frontend/Dockerfile` | 生产环境 | Nginx 静态文件服务 | -| `frontend/Dockerfile.dev` | 开发环境 | Vite Dev Server + HMR | -| `frontend/Dockerfile.mock` | Mock 测试 | 使用前端 Mock 数据 | - ---- - -## 🚀 快速开始 - -### 场景 1: 生产环境部署(Real Mode) - -```bash -# 启动所有服务(包含数据库) -docker compose up -d - -# 查看日志 -docker compose logs -f - -# 访问服务 -# - Frontend: http://localhost:3000 -# - Backend: http://localhost:8080 -# - pgAdmin: http://localhost:5050 (需要 --profile tools) -``` - -**环境变量**: -```bash -export JWT_SECRET="your-production-secret" -export ENCRYPTION_KEY="your-production-encryption-key-32-bytes" -docker compose up -d -``` - -### 场景 2: 开发环境(Dev Mode) - -```bash -# 启动开发环境(不需要数据库,使用 Mock) -docker compose -f docker-compose.yml -f docker-compose.dev.yml up - -# 或后台运行 -docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d - -# 访问服务 -# - Frontend: http://localhost:5173 (Vite Dev Server) -# - Backend: http://localhost:8080 (Mock Mode) -``` - -**特点**: -- ✅ 后端自动重载(Air) -- ✅ 前端 HMR(Vite) -- ✅ 不需要数据库(Mock 模式) -- ✅ 快速启动,适合日常开发 - -### 场景 3: 独立测试后端(Backend Mock) - -```bash -# 只启动后端 Mock 服务 -docker compose -f docker-compose.mock.yml up backend - -# 测试 API -curl http://localhost:8080/health -curl http://localhost:8080/api/v1/registries -``` - -### 场景 4: 独立测试前端(Frontend Mock) - -```bash -# 只启动前端 Mock 服务 -docker compose -f docker-compose.mock.yml up frontend - -# 访问前端 -# http://localhost:3000 -``` - -### 场景 5: 开发模式 + 真实数据库(可选) - -```bash -# 如果需要真实数据库进行开发 -docker compose -f docker-compose.yml -f docker-compose.dev.yml --profile with-db up -``` - ---- - -## 📁 项目结构 - -``` -ocdp-go/ -├── backend/ # 后端服务 -│ ├── Dockerfile # 生产环境 -│ ├── Dockerfile.dev # 开发环境(热重载) -│ ├── Dockerfile.mock # Mock 测试 -│ ├── .air.toml # Air 配置(热重载) -│ ├── cmd/api/main.go # 入口文件 -│ ├── internal/ # 内部代码 -│ ├── config/ # 配置文件 -│ └── data/ # Mock 数据 -│ -├── frontend/ # 前端服务 -│ ├── Dockerfile # 生产环境 -│ ├── Dockerfile.dev # 开发环境(Vite Dev Server) -│ ├── Dockerfile.mock # Mock 测试 -│ ├── nginx.conf # Nginx 配置 -│ ├── src/ # 源代码 -│ └── package.json # 依赖配置 -│ -├── api/ # API 规范 -│ └── openapi.yaml # OpenAPI 定义 -│ -├── docs/ # 文档 -│ ├── features/ # 功能文档 -│ ├── deployment/ # 部署文档 -│ └── development/ # 开发文档 -│ -├── docker-compose.yml # 生产模式配置 -├── docker-compose.dev.yml # 开发模式覆盖 -├── docker-compose.mock.yml # Mock 模式配置 -└── Makefile # 便捷命令 -``` - ---- - -## 🔧 开发工作流 - -### 日常开发流程 - -```bash -# 1. 启动开发环境 -docker compose -f docker-compose.yml -f docker-compose.dev.yml up - -# 2. 修改代码(自动重载) -# - 后端代码修改后自动重新编译 -# - 前端代码修改后 HMR 立即生效 - -# 3. 查看日志 -docker compose -f docker-compose.yml -f docker-compose.dev.yml logs -f backend -docker compose -f docker-compose.yml -f docker-compose.dev.yml logs -f frontend - -# 4. 停止服务 -docker compose -f docker-compose.yml -f docker-compose.dev.yml down -``` - -### 测试后端 API - -```bash -# 启动后端 Mock -docker compose -f docker-compose.mock.yml up backend -d - -# 测试登录 -curl -X POST http://localhost:8080/api/v1/auth/login \ - -H "Content-Type: application/json" \ - -d '{"username":"admin","password":"admin123"}' - -# 测试获取 registries -curl http://localhost:8080/api/v1/registries - -# 停止 -docker compose -f docker-compose.mock.yml down -``` - -### 构建生产镜像 - -```bash -# 构建所有镜像 -docker compose build - -# 只构建后端 -docker compose build backend - -# 只构建前端 -docker compose build frontend - -# 无缓存构建 -docker compose build --no-cache -``` - ---- - -## 🔍 服务详细说明 - -### Backend Service - -**环境变量**: - -| 变量 | 说明 | Mock 模式 | Production 模式 | -|------|------|-----------|-----------------| -| `ADAPTER_MODE` | 适配器模式 | `mock` | `production` | -| `PORT` | 服务端口 | `8080` | `8080` | -| `DATABASE_URL` | 数据库连接 | 不需要 | 必需 | -| `JWT_SECRET` | JWT 密钥 | 任意值 | 强密钥 | -| `ENCRYPTION_KEY` | 加密密钥 | 任意值(32字节) | 强密钥(32字节) | - -**健康检查**: -```bash -curl http://localhost:8080/health -# 返回: {"status":"healthy"} -``` - -**Mock 数据位置**: -- `backend/data/` - Mock 数据文件 -- `backend/internal/adapter/output/persistence/mock/` - Mock 实现 - -### Frontend Service - -**环境变量**: - -| 变量 | 说明 | 默认值 | -|------|------|--------| -| `VITE_API_BASE_URL` | 后端 API 地址 | `http://localhost:8080/api/v1` | -| `VITE_USE_MOCK` | 使用前端 Mock | `false` | - -**端口**: -- 开发模式:`5173`(Vite Dev Server) -- 生产模式:`80`(Nginx) - -### PostgreSQL Service - -**连接信息**: -``` -Host: localhost -Port: 5432 -Database: ocdp -User: postgres -Password: postgres -``` - -**管理工具**: -```bash -# 启动 pgAdmin -docker compose --profile tools up -d pgadmin - -# 访问: http://localhost:5050 -# Email: admin@ocdp.local -# Password: admin -``` - -### Redis Service - -**连接信息**: -``` -Host: localhost -Port: 6379 -``` - ---- - -## 🛠️ 故障排查 - -### 问题 1: 后端无法连接数据库 - -**检查**: -```bash -# 确认数据库是否运行 -docker compose ps postgres - -# 查看数据库日志 -docker compose logs postgres - -# 测试连接 -docker compose exec postgres psql -U postgres -d ocdp -c "SELECT 1;" -``` - -**解决方案**: -- 确保使用生产模式:`ADAPTER_MODE=production` -- 检查 `DATABASE_URL` 环境变量 -- 等待数据库健康检查通过 - -### 问题 2: 前端无法访问后端 - -**检查**: -```bash -# 查看后端状态 -curl http://localhost:8080/health - -# 查看网络 -docker compose ps -docker network inspect ocdp-network -``` - -**解决方案**: -- 确认后端服务运行正常 -- 检查 `VITE_API_BASE_URL` 环境变量 -- 检查 CORS 配置 - -### 问题 3: 开发模式热重载不工作 - -**后端**: -```bash -# 确认 Air 正在运行 -docker compose -f docker-compose.yml -f docker-compose.dev.yml logs backend | grep air - -# 检查文件挂载 -docker compose -f docker-compose.yml -f docker-compose.dev.yml exec backend ls -la -``` - -**前端**: -```bash -# 确认 Vite 正在运行 -docker compose -f docker-compose.yml -f docker-compose.dev.yml logs frontend | grep VITE - -# 检查 HMR -# 浏览器控制台应该显示 [vite] connected -``` - -### 问题 4: 容器启动失败 - -```bash -# 查看详细日志 -docker compose logs --tail=100 - -# 重新构建镜像 -docker compose build --no-cache - -# 清理并重启 -docker compose down -v -docker compose up -d -``` - ---- - -## 📊 性能建议 - -### 开发环境优化 - -1. **使用 Mock 模式**:避免数据库依赖,加快启动速度 -2. **关闭不需要的服务**:只启动正在开发的服务 -3. **使用 Docker volumes**:提高文件 I/O 性能 - -### 生产环境优化 - -1. **使用多阶段构建**:减小镜像体积 -2. **启用健康检查**:自动重启失败的服务 -3. **配置资源限制**:防止服务占用过多资源 -4. **使用 Redis 缓存**:减少数据库查询 - ---- - -## 📖 相关文档 - -- [开发指南](./docs/development/specification.md) -- [部署指南](./docs/deployment/docker-guide.md) -- [API 文档](../../backend/docs/openapi.yaml) -- [Artifact Filter 功能](./docs/features/ARTIFACT_MEDIATYPE_FILTER.md) - ---- - -## 🎓 总结 - -### 各模式对比 - -| 特性 | 生产模式 | 开发模式 | Mock 模式 | -|------|---------|---------|----------| -| 数据库 | ✅ PostgreSQL | ❌ Mock | ❌ Mock | -| 热重载 | ❌ | ✅ Air + Vite | ❌ | -| 启动速度 | 慢 | 中 | 快 | -| 适用场景 | 生产/集成测试 | 日常开发 | 独立测试 | -| 资源占用 | 高 | 中 | 低 | - -### 推荐使用场景 - -- 🚀 **日常开发**:使用开发模式 -- 🧪 **单元测试**:使用 Mock 模式 -- 🔗 **集成测试**:使用生产模式 -- 📦 **部署上线**:使用生产模式 - -### 快速命令参考 - -```bash -# 开发(推荐) -make dev -# 或 -docker compose -f docker-compose.yml -f docker-compose.dev.yml up - -# 测试后端 -docker compose -f docker-compose.mock.yml up backend - -# 测试前端 -docker compose -f docker-compose.mock.yml up frontend - -# 生产部署 -docker compose up -d - -# 停止所有 -docker compose down -``` - diff --git a/docs/archive/PROJECT_RESTRUCTURE_SUMMARY.md b/docs/archive/PROJECT_RESTRUCTURE_SUMMARY.md deleted file mode 100644 index 5ed2f6e..0000000 --- a/docs/archive/PROJECT_RESTRUCTURE_SUMMARY.md +++ /dev/null @@ -1,480 +0,0 @@ -# OCDP 项目重构总结 - -## 📋 重构概述 - -本次重构主要目标是清理项目结构,为各个服务创建独立的 Dockerfile,并通过 docker-compose 实现灵活的服务编排,支持多种运行模式。 - ---- - -## ✅ 完成的工作 - -### 1. 文档整理 - -#### 移动和整理的文档 -- ✅ `ARTIFACT_MEDIATYPE_FILTER.md` → `docs/features/` -- ✅ `TESTING_MEDIATYPE_FILTER.md` → `docs/features/` -- ✅ `DEPLOYMENT_STATUS.md` → `docs/` -- ✅ `FIXES_SUMMARY.md` → `docs/` - -#### 新创建的文档 -- ✅ `DOCKER_SERVICES.md` - 完整的 Docker 服务架构说明 -- ✅ `QUICK_START.md` - 5分钟快速开始指南 -- ✅ `README.md` - 全新的项目主页 -- ✅ `PROJECT_RESTRUCTURE_SUMMARY.md` - 本文档 - -### 2. Docker 配置文件 - -#### Backend Dockerfiles -- ✅ `backend/Dockerfile` - 生产环境(连接真实数据库) -- ✅ `backend/Dockerfile.dev` - 开发环境(Air 热重载) -- ✅ `backend/Dockerfile.mock` - Mock 测试(无外部依赖) -- ✅ `backend/.air.toml` - Air 热重载配置 - -#### Frontend Dockerfiles -- ✅ `frontend/Dockerfile` - 生产环境(Nginx 静态服务) -- ✅ `frontend/Dockerfile.dev` - 开发环境(Vite Dev Server + HMR) -- ✅ `frontend/Dockerfile.mock` - Mock 测试(前端独立运行) - -#### Docker Compose 配置 -- ✅ `docker-compose.yml` - 生产模式(Real Mode) -- ✅ `docker-compose.dev.yml` - 开发模式(Dev Mode) -- ✅ `docker-compose.mock.yml` - Mock 模式(独立测试) - -### 3. Makefile 更新 - -新增的 Docker 相关命令: -- ✅ `make docker-dev` - 启动开发环境 -- ✅ `make docker-dev-bg` - 后台启动开发环境 -- ✅ `make docker-prod` - 启动生产环境 -- ✅ `make docker-test-backend` - 测试后端 -- ✅ `make docker-test-frontend` - 测试前端 -- ✅ `make docker-test-backend-bg` - 后台测试后端 -- ✅ `make docker-test-frontend-bg` - 后台测试前端 -- ✅ `make docker-logs` - 查看日志 -- ✅ `make docker-down` - 停止服务 - ---- - -## 🏗️ 项目结构 - -### 最终目录结构 - -``` -ocdp-go/ -├── api/ -│ └── openapi.yaml # OpenAPI 规范 -│ -├── backend/ -│ ├── cmd/api/ # 入口文件 -│ ├── internal/ # 内部代码 -│ ├── config/ # 配置文件 -│ ├── data/ # Mock 数据 -│ ├── Dockerfile # 生产环境 -│ ├── Dockerfile.dev # 开发环境 -│ ├── Dockerfile.mock # Mock 测试 -│ └── .air.toml # 热重载配置 -│ -├── frontend/ -│ ├── src/ # 源代码 -│ ├── Dockerfile # 生产环境 -│ ├── Dockerfile.dev # 开发环境 -│ ├── Dockerfile.mock # Mock 测试 -│ └── nginx.conf # Nginx 配置 -│ -├── docs/ -│ ├── features/ # 功能文档 -│ │ ├── ARTIFACT_MEDIATYPE_FILTER.md -│ │ └── TESTING_MEDIATYPE_FILTER.md -│ ├── deployment/ # 部署文档 -│ ├── development/ # 开发文档 -│ ├── DEPLOYMENT_STATUS.md -│ └── FIXES_SUMMARY.md -│ -├── docker-compose.yml # 生产模式 -├── docker-compose.dev.yml # 开发模式 -├── docker-compose.mock.yml # Mock 模式 -├── Makefile # 便捷命令 -├── README.md # 项目主页 -├── QUICK_START.md # 快速开始 -├── DOCKER_SERVICES.md # Docker 服务说明 -└── PROJECT_RESTRUCTURE_SUMMARY.md # 本文档 -``` - ---- - -## 🎯 运行模式说明 - -### 模式 1: 开发模式(Dev Mode) - -**特点**: -- 后端使用 Mock 适配器(不需要数据库) -- 支持热重载(后端 Air,前端 Vite HMR) -- 快速启动,适合日常开发 - -**启动命令**: -```bash -make docker-dev -# 或 -docker compose -f docker-compose.yml -f docker-compose.dev.yml up -``` - -**访问地址**: -- 前端:http://localhost:5173 -- 后端:http://localhost:8080 - -**适用场景**: -- ✅ 日常开发 -- ✅ 快速迭代 -- ✅ 功能测试 - -### 模式 2: 生产模式(Production/Real Mode) - -**特点**: -- 连接真实的 PostgreSQL 数据库 -- 连接真实的 Redis 缓存 -- 完整功能,生产环境配置 - -**启动命令**: -```bash -make docker-prod -# 或 -docker compose up -d -``` - -**访问地址**: -- 前端:http://localhost:3000 -- 后端:http://localhost:8080 -- 数据库:localhost:5432 - -**适用场景**: -- ✅ 集成测试 -- ✅ 生产部署 -- ✅ 完整功能验证 - -### 模式 3: Mock 模式(独立测试) - -**特点**: -- 每个服务完全独立 -- Mock 所有外部依赖 -- 用于单独测试某个服务 - -**启动命令**: -```bash -# 测试后端 -make docker-test-backend -# 或 -docker compose -f docker-compose.mock.yml up backend - -# 测试前端 -make docker-test-frontend -# 或 -docker compose -f docker-compose.mock.yml up frontend -``` - -**适用场景**: -- ✅ 单元测试 -- ✅ API 测试 -- ✅ 前端独立开发 - ---- - -## 🔍 各服务 Dockerfile 说明 - -### Backend Dockerfiles - -| 文件 | 基础镜像 | 特点 | ADAPTER_MODE | -|------|---------|------|--------------| -| `Dockerfile` | golang:1.24-alpine | 多阶段构建,生产优化 | production | -| `Dockerfile.dev` | golang:1.24-alpine | 包含 Air,挂载源码 | mock | -| `Dockerfile.mock` | golang:1.24-alpine | 独立运行,Mock 所有依赖 | mock | - -**环境变量对比**: - -| 变量 | Production | Dev | Mock | -|------|-----------|-----|------| -| ADAPTER_MODE | production | mock | mock | -| DATABASE_URL | 必需 | 不需要 | 不需要 | -| JWT_SECRET | 强密钥 | dev-secret | test-secret | -| ENCRYPTION_KEY | 强密钥(32字节) | dev-key | test-key | - -### Frontend Dockerfiles - -| 文件 | 运行时 | 特点 | 开发工具 | -|------|-------|------|---------| -| `Dockerfile` | nginx:alpine | 静态文件服务 | - | -| `Dockerfile.dev` | node:20-alpine | Vite Dev Server | HMR | -| `Dockerfile.mock` | nginx:alpine | 使用前端 Mock 数据 | - | - -**端口对比**: - -| 模式 | 端口 | 说明 | -|------|------|------| -| Production | 80 | Nginx | -| Dev | 5173 | Vite Dev Server | -| Mock | 80 | Nginx | - ---- - -## 📊 服务依赖关系 - -### 生产模式依赖图 - -``` -Frontend (3000) - ↓ -Backend (8080) - ↓ - ├─→ PostgreSQL (5432) - └─→ Redis (6379) -``` - -### 开发模式依赖图 - -``` -Frontend (5173) - ↓ -Backend (8080, Mock Mode) - ↓ - └─→ 无外部依赖 -``` - -### Mock 模式依赖图 - -``` -Backend (8080) Frontend (3000) - ↓ ↓ -无外部依赖 无外部依赖 -``` - ---- - -## 🚀 使用场景示例 - -### 场景 1: 开发新功能 - -```bash -# 1. 启动开发环境 -make docker-dev - -# 2. 修改代码(自动热重载) -vim backend/cmd/api/main.go -vim frontend/src/App.tsx - -# 3. 查看效果 -# 访问 http://localhost:5173 - -# 4. 停止 -make docker-down -``` - -### 场景 2: 测试后端 API - -```bash -# 1. 启动后端 Mock -make docker-test-backend-bg - -# 2. 测试 API -curl http://localhost:8080/health -curl http://localhost:8080/api/v1/registries - -# 3. 停止 -docker compose -f docker-compose.mock.yml down -``` - -### 场景 3: 完整功能测试 - -```bash -# 1. 启动生产环境 -make docker-prod - -# 2. 访问前端 -open http://localhost:3000 - -# 3. 测试完整流程 -# - 登录 -# - 添加 Registry -# - 浏览 Artifacts -# - 部署 Helm Chart - -# 4. 停止 -make docker-down -``` - -### 场景 4: 前端独立开发 - -```bash -# 1. 启动前端 Mock -make docker-test-frontend-bg - -# 2. 修改前端代码 -vim frontend/src/components/NewComponent.tsx - -# 3. 重新构建(如果需要) -docker compose -f docker-compose.mock.yml build frontend -docker compose -f docker-compose.mock.yml up -d frontend - -# 4. 停止 -docker compose -f docker-compose.mock.yml down -``` - ---- - -## 📈 性能对比 - -### 启动时间对比 - -| 模式 | 启动时间 | 服务数量 | 资源占用 | -|------|---------|---------|---------| -| Mock 模式(单服务) | ~5秒 | 1 | 低 | -| 开发模式 | ~15秒 | 2 | 中 | -| 生产模式 | ~30秒 | 4-5 | 高 | - -### 镜像大小对比 - -| 镜像 | 大小 | 说明 | -|------|------|------| -| Backend (Production) | ~20MB | 多阶段构建,只包含二进制 | -| Backend (Dev) | ~500MB | 包含 Go 工具链和源码 | -| Frontend (Production) | ~50MB | Nginx + 静态文件 | -| Frontend (Dev) | ~400MB | Node + 依赖 | - ---- - -## 🔧 故障排查 - -### 常见问题和解决方案 - -#### 1. 端口冲突 - -**错误**:`port is already allocated` - -**解决方案**: -```bash -# 查看占用端口 -sudo lsof -i :8080 -sudo lsof -i :5173 - -# 修改 docker-compose.yml 中的端口映射 -ports: - - "8081:8080" # 改为 8081 -``` - -#### 2. 热重载不工作 - -**后端**: -```bash -# 检查 Air 是否运行 -docker compose logs backend | grep "air" - -# 重启服务 -docker compose restart backend -``` - -**前端**: -```bash -# 检查 Vite 是否运行 -docker compose logs frontend | grep "VITE" - -# 确认浏览器控制台 -# 应该显示:[vite] connected -``` - -#### 3. 数据库连接失败 - -**检查**: -```bash -# 数据库是否运行 -docker compose ps postgres - -# 健康检查 -docker compose exec postgres pg_isready -``` - -**解决方案**: -- 使用生产模式(`docker compose up`) -- 等待数据库启动完成(~10秒) -- 检查 `DATABASE_URL` 环境变量 - ---- - -## 📚 相关资源 - -### 文档链接 -- [快速开始](./QUICK_START.md) - 5分钟上手指南 -- [Docker 服务架构](./DOCKER_SERVICES.md) - 完整服务说明 -- [OpenAPI 规范](../../backend/docs/openapi.yaml) - API 定义 -- [开发指南](./docs/development/specification.md) - 开发规范 - -### 外部资源 -- [Docker Compose 文档](https://docs.docker.com/compose/) -- [Go 官方文档](https://go.dev/doc/) -- [React 官方文档](https://react.dev/) -- [Vite 官方文档](https://vitejs.dev/) - ---- - -## ✨ 重构收益 - -### 开发体验提升 -- ✅ **更快的启动速度**:开发模式启动从 30 秒降至 15 秒 -- ✅ **热重载支持**:代码修改立即生效,无需重启 -- ✅ **独立测试能力**:可单独测试任意服务 -- ✅ **清晰的文档**:完整的使用指南和示例 - -### 运维效率提升 -- ✅ **标准化部署**:统一的 Docker 镜像和配置 -- ✅ **多环境支持**:开发/测试/生产环境一键切换 -- ✅ **容器化隔离**:服务间相互独立,易于调试 -- ✅ **便捷的命令**:Makefile 提供一键操作 - -### 代码质量提升 -- ✅ **清晰的项目结构**:文档和代码分离 -- ✅ **灵活的架构**:支持 Mock/Real 双模式 -- ✅ **易于维护**:每个服务独立的 Dockerfile -- ✅ **完善的文档**:详细的使用说明和最佳实践 - ---- - -## 🎯 下一步计划 - -### 短期目标 -- [ ] 添加 CI/CD 流程 -- [ ] 完善单元测试覆盖 -- [ ] 添加性能监控 -- [ ] 优化镜像大小 - -### 中期目标 -- [ ] Kubernetes 部署支持 -- [ ] 多语言支持(i18n) -- [ ] 权限管理增强 -- [ ] API 版本控制 - -### 长期目标 -- [ ] 插件系统 -- [ ] 多租户支持 -- [ ] 自动扩缩容 -- [ ] 高可用架构 - ---- - -## 👥 贡献者 - -感谢所有参与本次重构的贡献者! - ---- - -## 📝 更新日志 - -### 2025-11-09 -- ✅ 完成项目重构 -- ✅ 创建多种 Dockerfile -- ✅ 配置 docker-compose 多模式 -- ✅ 更新项目文档 -- ✅ 优化 Makefile 命令 - ---- - -
- Project restructured on 2025-11-09 -
- diff --git a/docs/archive/root-cleanup/API_NAMING_CONVENTION_FIX.md b/docs/archive/root-cleanup/API_NAMING_CONVENTION_FIX.md deleted file mode 100644 index ea0b0bb..0000000 --- a/docs/archive/root-cleanup/API_NAMING_CONVENTION_FIX.md +++ /dev/null @@ -1,189 +0,0 @@ -# API 命名规范统一修复总结 - -## 📋 问题描述 - -前后端 API 字段命名风格不一致,导致前后端交互失败: - -- **后端 Go JSON tags**: 使用 `snake_case` (如 `cluster_id`, `ca_data`) -- **OpenAPI 规范**: 使用 `camelCase` (如 `clusterId`, `caData`) -- **前端生成代码**: 使用 `camelCase`,与后端实际返回的JSON不匹配 -- **前端手写代码**: 为了临时修复,手动使用 `snake_case` - -## ✅ 解决方案 - -**选择方案B**: 统一使用 `snake_case` 作为业务数据字段命名规范 - -### 命名规范标准 - -1. **OpenAPI 自身规范字段**: 保持 `camelCase` (如 `operationId`, `requestBody`) -2. **业务数据字段**: 统一使用 `snake_case` (与后端 Go JSON tags 一致) - -### 优势 - -- ✅ 保持后端代码不变,降低改动成本 -- ✅ Go 的 JSON 序列化默认就是字段名,使用 snake_case 更符合 Go 生态 -- ✅ 前端自动生成的代码与后端完全匹配 -- ✅ 无需手动维护类型定义 - -## 🔧 修改内容 - -### 1. 后端 DTO (保持不变) - -```go -// backend/internal/adapter/input/http/dto/cluster_dto.go -type CreateClusterRequest struct { - Name string `json:"name" binding:"required"` - Host string `json:"host" binding:"required"` - CAData string `json:"ca_data"` // ✅ snake_case - CertData string `json:"cert_data"` // ✅ snake_case - KeyData string `json:"key_data"` // ✅ snake_case - Token string `json:"token"` - Description string `json:"description"` -} - -type ClusterResponse struct { - ID string `json:"id"` - Name string `json:"name"` - // ... - CreatedAt string `json:"created_at"` // ✅ snake_case - UpdatedAt string `json:"updated_at"` // ✅ snake_case -} -``` - -### 2. OpenAPI 规范修改 - -**修改文件**: `backend/docs/openapi.yaml` - -#### 修改的 Schema: - -1. **CreateClusterRequest** - - `caData` → `ca_data` - - `certData` → `cert_data` - - `keyData` → `key_data` - -2. **UpdateClusterRequest** - - `caData` → `ca_data` - - `certData` → `cert_data` - - `keyData` → `key_data` - -3. **ClusterResponse** - - `createdAt` → `created_at` - - `updatedAt` → `updated_at` - -4. **RegistryResponse** - - `createdAt` → `created_at` - - `updatedAt` → `updated_at` - -5. **UserResponse** - - `createdAt` → `created_at` - - `updatedAt` → `updated_at` - -6. **InstanceResponse** - - `clusterId` → `cluster_id` ✅ - - `registryId` → `registry_id` ✅ (新增字段) - - `repository` ✅ (新增字段) - -### 3. 前端生成代码 - -重新生成前端 TypeScript API 客户端: - -```bash -bash scripts/sync-openapi-frontend.sh -``` - -**生成结果**: -- ✅ 7 个 API 文件 -- ✅ 25 个 Model 文件 -- ✅ 完全匹配后端 JSON 字段命名 - -**示例生成代码**: - -```typescript -// frontend/src/api/generated/models/create-cluster-request.ts -export interface CreateClusterRequest { - 'name': string; - 'host': string; - 'ca_data'?: string; // ✅ snake_case - 'cert_data'?: string; // ✅ snake_case - 'key_data'?: string; // ✅ snake_case - 'token'?: string; -} - -// frontend/src/api/generated/models/instance-response.ts -export interface InstanceResponse { - 'id'?: string; - 'name'?: string; - 'namespace'?: string; - 'cluster_id'?: string; // ✅ snake_case - 'registry_id'?: string; // ✅ snake_case - 'repository'?: string; - // ... -} -``` - -## 📊 影响范围 - -### 已修改 - -1. ✅ `backend/docs/openapi.yaml` - OpenAPI 规范 -2. ✅ `frontend/src/api/generated/*` - 自动生成的 TypeScript 代码 - -### 无需修改 - -1. ✅ 后端 Go DTO 代码 - 保持原有 snake_case -2. ✅ 前端手写的临时修复代码 - 现在可以使用生成的代码替换 - -## 🎯 后续工作 - -### 可选优化(前端) - -前端中手写的类型定义现在可以删除,直接使用生成的代码: - -**需要清理的文件**: -- `frontend/src/core/types/index.ts` - 包含手写的 snake_case 类型 -- `frontend/src/core/api/instance.api.ts` - 包含手写的接口定义 -- `frontend/src/core/api/unified-api.ts` - 包含手写的接口定义 - -**推荐做法**: -直接导入并使用自动生成的类型: -```typescript -import { - ClusterResponse, - CreateClusterRequest, - InstanceResponse -} from '@/api/generated'; -``` - -## ✅ 验证 - -### 字段命名一致性检查 - -| 字段 | 后端 JSON | OpenAPI | 前端生成 | 状态 | -|------|-----------|---------|----------|------| -| `ca_data` | ✅ | ✅ | ✅ | 一致 | -| `cert_data` | ✅ | ✅ | ✅ | 一致 | -| `key_data` | ✅ | ✅ | ✅ | 一致 | -| `cluster_id` | ✅ | ✅ | ✅ | 一致 | -| `registry_id` | ✅ | ✅ | ✅ | 一致 | -| `created_at` | ✅ | ✅ | ✅ | 一致 | -| `updated_at` | ✅ | ✅ | ✅ | 一致 | - -## 📝 注意事项 - -1. **OpenAPI 规范字段仍使用 camelCase**: 如 `operationId`, `requestBody` 等元字段 -2. **业务数据字段统一 snake_case**: 所有 schemas 中的属性 -3. **前端需更新代码**: 如果有直接使用手写类型的地方,需要切换到生成的类型 - -## 🔗 相关文件 - -- OpenAPI 规范: `backend/docs/openapi.yaml` -- 生成脚本: `scripts/sync-openapi-frontend.sh` -- 前端生成代码: `frontend/src/api/generated/` -- 后端 DTO: `backend/internal/adapter/input/http/dto/` - ---- - -**修复日期**: 2025-11-10 -**修复人**: AI Assistant -**影响版本**: OCDP v1.0.0 - diff --git a/docs/archive/root-cleanup/CAMELCASE-MIGRATION.md b/docs/archive/root-cleanup/CAMELCASE-MIGRATION.md deleted file mode 100644 index ab1bb45..0000000 --- a/docs/archive/root-cleanup/CAMELCASE-MIGRATION.md +++ /dev/null @@ -1,207 +0,0 @@ -# camelCase Migration Summary - -## 🎯 目标 - -将项目从 snake_case JSON 迁移到 camelCase JSON,符合 REST API 最佳实践和 Google JSON Style Guide。 - -## ✅ 实施方案 A:全面 camelCase - -### 架构设计 - -``` -Backend Go -├─ struct fields: PascalCase (Go 规范) -└─ json tags: camelCase → JSON: camelCase - ↓ -OpenAPI -├─ schemas: PascalCase -└─ properties: camelCase - ↓ Orval -Frontend TypeScript -├─ interfaces: PascalCase (TS 规范) -├─ properties: camelCase (TS 规范) -└─ JSON: camelCase (REST 标准) -``` - -## 📝 修改清单 - -### 1. Backend Go - JSON Tags (✅ 完成) - -修改所有 DTO 文件的 JSON tags 从 snake_case → camelCase: - -- ✅ `backend/internal/adapter/input/http/dto/cluster_dto.go` - - `ca_data` → `caData` - - `cert_data` → `certData` - - `key_data` → `keyData` - - `has_ca_data` → `hasCaData` - - `created_at` → `createdAt` - - `updated_at` → `updatedAt` - -- ✅ `backend/internal/adapter/input/http/dto/auth_dto.go` - - `refresh_token` → `refreshToken` - - `access_token` → `accessToken` - - `user_id` → `userId` - -- ✅ `backend/internal/adapter/input/http/dto/registry_dto.go` - - `has_password` → `hasPassword` - - `created_at` → `createdAt` - - `updated_at` → `updatedAt` - -- ✅ `backend/internal/adapter/input/http/dto/instance_dto.go` - - `registry_id` → `registryId` - - `cluster_id` → `clusterId` - - `values_yaml` → `valuesYaml` - - `keep_history` → `keepHistory` - -- ✅ `backend/internal/adapter/input/http/dto/artifact_dto.go` - - `registry_id` → `registryId` - - `registry_url` → `registryUrl` - - `repository_name` → `repositoryName` - - `catalog_supported` → `catalogSupported` - - `media_type` → `mediaType` - -- ✅ `backend/internal/adapter/input/http/dto/monitoring_dto.go` - - All monitoring metrics fields converted to camelCase - -### 2. OpenAPI Specification (✅ 完成) - -- ✅ 创建转换脚本: `backend/scripts/convert-openapi-to-camelcase.cjs` -- ✅ 转换 `backend/docs/openapi.yaml` 所有属性为 camelCase -- ✅ 备份原文件: `backend/docs/openapi.yaml.backup` - -### 3. Frontend Setup (✅ 完成) - -#### 安装 Orval -- ✅ 添加 `orval@7.3.0` 到 `package.json` -- ✅ 运行 `npm install` - -#### 配置 Orval -- ✅ 创建 `frontend/orval.config.ts` -- ✅ 配置生成器指向 OpenAPI 文件 -- ✅ 配置 Axios mutator - -#### 创建 Axios Mutator -- ✅ 创建 `frontend/src/api/axios-mutator.ts` -- ✅ 配置 Axios 实例和拦截器 - -#### 更新脚本 -- ✅ 修改 `package.json` 的 `openapi-gen` 脚本使用 Orval -- ✅ 修改 `Makefile` 的 `openapi-gen-frontend` 使用 Orval - -#### 创建文档和示例 -- ✅ 创建 `frontend/src/api/README.md` -- ✅ 创建 `frontend/src/api/example.ts` -- ✅ 更新 `frontend/src/api/index.ts` - -### 4. 工具文件 (✅ 保留备用) - -- ✅ `frontend/src/api/case-converter.ts` - 保留作为工具函数 -- ✅ `frontend/scripts/post-process-openapi.cjs` - 保留作为备用方案 - -## 🚀 使用方法 - -### 重新生成 API 代码 - -```bash -# 从项目根目录 -make openapi-gen-frontend - -# 或从 frontend 目录 -cd frontend -npm run openapi-gen -``` - -### 前端使用示例 - -```typescript -import { createCluster, setAuthToken } from '@/api'; - -// 设置 token -setAuthToken('your-jwt-token'); - -// 创建集群 - 全部使用 camelCase ✅ -const cluster = await createCluster({ - name: 'my-cluster', - host: 'https://k8s.example.com', - caData: 'base64...', // ✅ camelCase - certData: 'base64...', // ✅ camelCase - keyData: 'base64...', // ✅ camelCase -}); -``` - -## 📊 变更统计 - -- **后端 Go 文件**: 6 个 DTO 文件修改 -- **JSON Tags 转换**: ~50+ 字段 -- **OpenAPI 属性**: ~50+ 属性转换 -- **新增文件**: 7 个 - - `orval.config.ts` - - `axios-mutator.ts` - - `case-converter.ts` - - `api/README.md` - - `api/example.ts` - - `convert-openapi-to-camelcase.cjs` - - `CAMELCASE-MIGRATION.md` - -## ✨ 优势 - -1. **符合标准**: 遵循 REST API 和 JSON 最佳实践 -2. **类型安全**: 完整的 TypeScript 类型支持 -3. **IDE 友好**: 自动补全和类型检查 -4. **无性能损耗**: 无需运行时转换 -5. **维护简单**: OpenAPI 驱动,自动生成 -6. **前后端一致**: 统一的命名规范 - -## 🔍 验证 - -### 检查生成的类型 - -```bash -grep -A 5 "export interface CreateClusterRequest" \ - frontend/src/api/generated-orval/api.schemas.ts -``` - -应该看到: -```typescript -export interface CreateClusterRequest { - caData?: string; // ✅ camelCase - certData?: string; // ✅ camelCase - keyData?: string; // ✅ camelCase - ... -} -``` - -### 测试 API 调用 - -```bash -# 启动后端 -cd backend && make run-mock - -# 启动前端 -cd frontend && npm run dev - -# 测试 API -curl -X POST http://localhost:8080/api/v1/clusters \ - -H "Content-Type: application/json" \ - -d '{"name":"test","host":"https://k8s.example.com","caData":"..."}' -``` - -## 📚 参考文档 - -- `frontend/src/api/README.md` - API 使用文档 -- `frontend/src/api/example.ts` - 代码示例 -- [Orval Documentation](https://orval.dev/) -- [Google JSON Style Guide](https://google.github.io/styleguide/jsoncstyleguide.xml) - -## 🎉 完成状态 - -✅ **方案 A 已全面实施完成!** - -所有代码已修改为使用 camelCase: -- ✅ 后端 Go JSON tags -- ✅ OpenAPI 规范 -- ✅ 前端 TypeScript 类型 -- ✅ JSON 传输格式 - -项目现在符合现代 REST API 最佳实践! - diff --git a/docs/archive/root-cleanup/DOCS_CLEANUP_SUMMARY.md b/docs/archive/root-cleanup/DOCS_CLEANUP_SUMMARY.md deleted file mode 100644 index b7e5fd8..0000000 --- a/docs/archive/root-cleanup/DOCS_CLEANUP_SUMMARY.md +++ /dev/null @@ -1,310 +0,0 @@ -# 文档清理总结 - -## ✅ 清理完成 - -已成功精简项目文档,从 **21 个文档** 减少到 **10 个核心文档** + 4 个归档文档。 - ---- - -## 📊 清理前后对比 - -### 清理前 - -``` -根目录文档:8 个 -├── README.md -├── QUICK_START.md -├── USAGE_GUIDE.md -├── COMMANDS_CHEATSHEET.md -├── DOCKER_SERVICES.md -├── CLEANUP_SUMMARY.md -├── PROJECT_RESTRUCTURE_SUMMARY.md -└── COMPLETION_SUMMARY.md - -docs/ 目录:13 个 -├── deployment/ -│ ├── docker-guide.md -│ ├── cleanup-summary.md -│ └── docker-fixes.md -├── development/ -│ └── specification.md -├── features/ -│ ├── ARTIFACT_MEDIATYPE_FILTER.md -│ └── TESTING_MEDIATYPE_FILTER.md -├── getting-started/ -│ ├── docker-quick-start.md -│ └── quick-start.md -├── security/ -│ └── security-implementation.md -├── DEPLOYMENT_STATUS.md -├── FIXES_SUMMARY.md -├── INTEGRATION_SUMMARY.md -└── README.md - -总计:21 个文档 -``` - -### 清理后 - -``` -根目录文档:4 个 ⭐ -├── README.md # 项目主页 -├── QUICK_START.md # 快速开始 -├── USAGE_GUIDE.md # 使用指南 -└── COMMANDS_CHEATSHEET.md # 命令速查表 - -docs/ 目录:6 个 ⭐ -├── deployment/ -│ └── docker-guide.md # 部署指南 -├── development/ -│ └── specification.md # 开发规范 -├── features/ -│ ├── ARTIFACT_MEDIATYPE_FILTER.md # 功能文档 -│ └── TESTING_MEDIATYPE_FILTER.md # 测试文档 -├── security/ -│ └── security-implementation.md # 安全实践 -└── README.md # 文档索引 - -docs/archive/ 归档:4 个 -├── CLEANUP_SUMMARY.md -├── COMPLETION_SUMMARY.md -├── DOCKER_SERVICES.md -└── PROJECT_RESTRUCTURE_SUMMARY.md - -总计:10 个核心文档 + 4 个归档 -``` - ---- - -## 🗑️ 清理操作 - -### 删除的文档(7个) - -| 文档 | 原因 | 操作 | -|------|------|------| -| `docs/getting-started/docker-quick-start.md` | 与根目录 QUICK_START.md 重复 | ❌ 删除 | -| `docs/getting-started/quick-start.md` | 与根目录 QUICK_START.md 重复 | ❌ 删除 | -| `docs/DEPLOYMENT_STATUS.md` | 临时状态文档,已过时 | ❌ 删除 | -| `docs/FIXES_SUMMARY.md` | 临时修复总结,已过时 | ❌ 删除 | -| `docs/INTEGRATION_SUMMARY.md` | 临时集成总结,已过时 | ❌ 删除 | -| `docs/deployment/cleanup-summary.md` | 过时的清理文档 | ❌ 删除 | -| `docs/deployment/docker-fixes.md` | 临时修复文档,已过时 | ❌ 删除 | - -### 归档的文档(4个) - -| 文档 | 原因 | 操作 | -|------|------|------| -| `CLEANUP_SUMMARY.md` | 历史记录,参考价值 | 📦 归档 | -| `COMPLETION_SUMMARY.md` | 历史记录,参考价值 | 📦 归档 | -| `DOCKER_SERVICES.md` | 被 USAGE_GUIDE.md 替代 | 📦 归档 | -| `PROJECT_RESTRUCTURE_SUMMARY.md` | 历史记录,参考价值 | 📦 归档 | - -### 保留的核心文档(10个) - -| 类型 | 文档 | 说明 | -|------|------|------| -| **根目录** | `README.md` | 项目主页和概述 | -| **根目录** | `QUICK_START.md` | 5分钟快速开始 | -| **根目录** | `USAGE_GUIDE.md` | 详细使用指南 | -| **根目录** | `COMMANDS_CHEATSHEET.md` | 命令速查表 | -| **专业文档** | `docs/development/specification.md` | 开发规范 | -| **专业文档** | `docs/deployment/docker-guide.md` | 部署指南 | -| **专业文档** | `docs/security/security-implementation.md` | 安全实践 | -| **专业文档** | `docs/features/ARTIFACT_MEDIATYPE_FILTER.md` | 功能说明 | -| **专业文档** | `docs/features/TESTING_MEDIATYPE_FILTER.md` | 测试指南 | -| **索引** | `docs/README.md` | 文档中心索引 | - ---- - -## 📁 最终文档结构 - -``` -ocdp-go/ -├── README.md # 🏠 项目主页 -├── QUICK_START.md # 🚀 快速开始 -├── USAGE_GUIDE.md # 📋 使用指南 -├── COMMANDS_CHEATSHEET.md # 💡 命令速查表 -│ -├── api/ -│ └── openapi.yaml # 📋 API 规范 -│ -└── docs/ # 📚 文档中心 - ├── README.md # 📑 文档索引 - │ - ├── development/ # 🔧 开发文档 - │ └── specification.md - │ - ├── features/ # 🎨 功能文档 - │ ├── ARTIFACT_MEDIATYPE_FILTER.md - │ └── TESTING_MEDIATYPE_FILTER.md - │ - ├── deployment/ # 🚢 部署文档 - │ └── docker-guide.md - │ - ├── security/ # 🔒 安全文档 - │ └── security-implementation.md - │ - └── archive/ # 📦 历史归档 - ├── CLEANUP_SUMMARY.md - ├── COMPLETION_SUMMARY.md - ├── DOCKER_SERVICES.md - └── PROJECT_RESTRUCTURE_SUMMARY.md -``` - ---- - -## 🎯 文档分类 - -### 核心文档(根目录) - -适合所有用户快速查阅: - -1. **README.md** - 项目概述,第一印象 -2. **QUICK_START.md** - 5分钟上手,新手必读 -3. **USAGE_GUIDE.md** - 详细使用,日常参考 -4. **COMMANDS_CHEATSHEET.md** - 命令速查,快速检索 - -### 专业文档(docs/) - -适合深入学习和专业开发: - -- **开发类** - 开发规范、架构设计 -- **功能类** - 功能详解、测试指南 -- **部署类** - 生产部署、配置说明 -- **安全类** - 安全实践、加密方案 - -### 历史归档(docs/archive/) - -保留项目演进历史,供参考: - -- 重构总结 -- 清理记录 -- 完成报告 -- 历史架构文档 - ---- - -## ✨ 清理效果 - -### 数量精简 - -| 指标 | 清理前 | 清理后 | 改进 | -|------|--------|--------|------| -| 总文档数 | 21 | 10 | ⬇️ 52% | -| 根目录文档 | 8 | 4 | ⬇️ 50% | -| docs 文档 | 13 | 6 | ⬇️ 54% | - -### 结构优化 - -- ✅ **清晰分类**:根目录核心,docs 专业 -- ✅ **去重合并**:删除重复的快速开始文档 -- ✅ **归档历史**:保留历史,不影响日常使用 -- ✅ **易于维护**:更少的文档,更集中的内容 - -### 用户体验 - -- ✅ **快速找到**:核心文档在根目录,一眼可见 -- ✅ **分级阅读**:新手看根目录,专业看 docs -- ✅ **减少困惑**:去除过时和重复内容 -- ✅ **便于导航**:清晰的文档索引 - ---- - -## 📖 新用户指南 - -### 第一次使用? - -按此顺序阅读: - -1. **[README.md](./README.md)** - 了解项目(2分钟) -2. **[QUICK_START.md](./QUICK_START.md)** - 快速体验(5分钟) -3. **[USAGE_GUIDE.md](./USAGE_GUIDE.md)** - 深入学习(15分钟) - -### 日常开发? - -快速查阅: - -1. **[COMMANDS_CHEATSHEET.md](./COMMANDS_CHEATSHEET.md)** - 命令速查 -2. **[USAGE_GUIDE.md](./USAGE_GUIDE.md)** - 使用参考 - -### 生产部署? - -专业文档: - -1. **[部署指南](./docs/deployment/docker-guide.md)** -2. **[安全实践](./docs/security/security-implementation.md)** - ---- - -## 🔍 文档查找技巧 - -### 按需求查找 - -| 我想... | 应该看... | -|---------|----------| -| 快速上手 | [QUICK_START.md](./QUICK_START.md) | -| 日常使用 | [USAGE_GUIDE.md](./USAGE_GUIDE.md) | -| 查命令 | [COMMANDS_CHEATSHEET.md](./COMMANDS_CHEATSHEET.md) | -| 开发规范 | [docs/development/specification.md](./docs/development/specification.md) | -| 了解功能 | [docs/features/](./docs/features/) | -| 部署生产 | [docs/deployment/docker-guide.md](./docs/deployment/docker-guide.md) | -| 安全配置 | [docs/security/security-implementation.md](./docs/security/security-implementation.md) | - -### 按角色查找 - -| 角色 | 推荐文档 | -|------|----------| -| **新用户** | README → QUICK_START → USAGE_GUIDE | -| **开发者** | USAGE_GUIDE → development/specification | -| **运维** | deployment/docker-guide → security/security-implementation | -| **架构师** | README → docs/ 全部文档 | - ---- - -## 💡 维护建议 - -### 文档更新原则 - -1. **根目录文档** - - 保持简洁,快速阅读 - - 面向所有用户 - - 定期更新保持最新 - -2. **专业文档** - - 详细深入,专业准确 - - 面向特定角色 - - 按需更新 - -3. **归档文档** - - 仅保留不再修改 - - 供历史参考 - - 不影响日常使用 - -### 新增文档规则 - -- **快速参考** → 放根目录 -- **专业内容** → 放 docs/ 对应分类 -- **历史记录** → 放 docs/archive/ - ---- - -## 🎉 总结 - -通过本次清理: - -- ✅ **文档数量减少 52%** - 从 21 个到 10 个核心文档 -- ✅ **结构更加清晰** - 核心、专业、归档分离 -- ✅ **查找更加便捷** - 分类明确,索引完善 -- ✅ **维护更加简单** - 更少的文档,更集中的内容 -- ✅ **用户体验提升** - 快速找到需要的信息 - -**文档更少,效率更高!** 📚 - ---- - -
- 文档清理完成于 2025-11-09 -
- 保持简洁,提升效率 -
- diff --git a/docs/archive/root-cleanup/FRONTEND_REFACTOR_SUMMARY.md b/docs/archive/root-cleanup/FRONTEND_REFACTOR_SUMMARY.md deleted file mode 100644 index 13542c9..0000000 --- a/docs/archive/root-cleanup/FRONTEND_REFACTOR_SUMMARY.md +++ /dev/null @@ -1,213 +0,0 @@ -# 前端代码清理与重构总结 - -生成时间: 2025-11-10 - -## ✅ 完成的工作 - -### 1. 代码清理 - 删除多余文件 - -#### 已删除的文件: -- ✅ `frontend/src/api/client.ts` - 旧的 Mock 客户端(未被使用) -- ✅ `frontend/src/api/examples.ts` - 示例代码(未被使用) - -**原因:** 这些文件是早期开发时的 Mock 实现,现在项目使用 `core/api` 中的统一 API 封装,这些文件已不再被任何地方引用。 - -### 2. 类型错误修复 - -#### 修复的文件和问题: - -**RegistryTreeExplorer.tsx** -- ✅ 修复:`repoTags` 类型从 `null` 改为 `undefined` -- 位置:第 553 行 -- 原因:生成的 OpenAPI 类型使用 `undefined` 而非 `null` - -**RepositoryItem.tsx** -- ✅ 修复:添加空值检查 `selectedTag.repositoryName` 和 `selectedTag.tag` -- 位置:第 184-192 行 -- ✅ 修复:添加空值检查 `tagItem.size` -- 位置:第 340-344 行 -- 原因:这些属性在 OpenAPI 生成的类型中是可选的 - -**TagCard.tsx** -- ✅ 修复:使用默认值 `tag.size || 0` 处理可选的 size 属性 -- 位置:第 99 行 -- ✅ 修复:添加空值检查 `tag.repositoryName` 和 `tag.tag` -- 位置:第 129-138 行 -- 原因:确保传递给子组件的属性不为 undefined - -### 3. 构建验证 - -```bash -✅ TypeScript 编译:通过 -✅ Vite 构建:成功 -✅ 输出大小: - - HTML: 0.40 kB - - CSS: 37.03 kB (gzip: 6.87 kB) - - JS: 394.93 kB (gzip: 110.53 kB) -``` - -**构建时间:** 5.16s - -## 📊 当前代码结构 - -### API 层架构 - -``` -frontend/src/ -├── api/ -│ └── generated/ # OpenAPI 生成的客户端代码 -│ ├── api/ # 7 个 API 接口类 -│ └── models/ # 25 个类型模型 -│ -└── core/ - └── api/ # 业务层 API 封装 - ├── artifact.api.ts # Artifact 相关 API - ├── instance.api.ts # Instance 相关 API - ├── monitoring.api.ts # Monitoring 相关 API - ├── unified-api.ts # 统一 API 封装 - └── index.ts # 统一导出 -``` - -### 页面功能模块 - -#### 1. 认证 (`/features/auth`) -- ✅ 登录页面 -- ✅ JWT Token 管理 - -#### 2. 主页 (`/features/home`) -- ✅ 仪表板展示 - -#### 3. 配置管理 (`/features/configuration`) -- ✅ 集群配置 (`/configuration/clusters`) -- ✅ Registry 配置 (`/configuration/registries`) - -#### 4. Artifact 管理 (`/features/artifact`) -- ✅ Registries 浏览器 (`/artifact/registries`) - - 支持 Harbor, Docker Hub, GHCR, 自定义 Registry - - OCI 标准 Artifact 浏览 - - Helm Chart 和容器镜像过滤 -- ✅ 实例管理 (`/artifact/instances`) - - Helm Release 管理 - - 实例部署、更新、删除 - -#### 5. 监控 (`/features/monitoring`) -- ✅ 集群监控 (`/monitoring/clusters`) - - 节点指标 - - 资源使用情况 - -## 🔄 API 使用统计 - -- **使用 `@/core/api` 的文件:** 18 个 -- **主要使用场景:** - - Artifact 相关操作:7 个文件 - - 配置管理:4 个文件 - - 监控:3 个文件 - - 认证:1 个文件 - - 其他:3 个文件 - -## 🎯 代码质量改进 - -### Before (清理前) -- ❌ 26 个 TypeScript 编译错误 -- ❌ 2 个未使用的文件(507 行代码) -- ❌ 类型不匹配导致的潜在运行时错误 - -### After (清理后) -- ✅ 0 个 TypeScript 编译错误 -- ✅ 删除了 507 行未使用代码 -- ✅ 所有类型安全检查通过 -- ✅ 构建优化完成 - -## 📈 性能优化 - -### 代码体积 -- 删除未使用文件:减少 ~507 行代码 -- 构建输出优化: - - JS 包经过 gzip 压缩:110.53 kB - - CSS 经过 gzip 压缩:6.87 kB - -### 类型安全 -- 所有组件使用 OpenAPI 生成的类型 -- 编译时类型检查确保 API 调用正确性 -- 减少运行时错误风险 - -## 🛠️ 技术栈 - -### 前端框架 -- React 18 -- TypeScript -- Vite 7 -- TailwindCSS - -### API 客户端 -- Axios -- OpenAPI Generator (typescript-axios) -- 自定义封装层 (`core/api`) - -### 路由 -- React Router v6 -- 受保护路由 -- 路径重定向支持 - -## 📝 最佳实践 - -### 1. API 调用 -```typescript -// ✅ 推荐:使用 core/api 封装 -import { getAllClusters, createCluster } from '@/core/api'; - -const clusters = await getAllClusters(); -``` - -### 2. 类型安全 -```typescript -// ✅ 推荐:使用生成的类型 -import type { ClusterResponse, CreateClusterRequest } from '@/api/generated/models'; - -const cluster: ClusterResponse = await createCluster(data); -``` - -### 3. 空值处理 -```typescript -// ✅ 推荐:添加空值检查 -{tag.repositoryName && tag.tag && ( - -)} - -// 或使用默认值 -{formatSize(tag.size || 0)} -``` - -## 🔮 未来改进建议 - -### 短期 -1. ✅ 已完成:清理未使用代码 -2. ✅ 已完成:修复类型错误 -3. ✅ 已完成:确保构建成功 - -### 中期 -1. 添加单元测试覆盖 -2. 实现 API 错误边界处理 -3. 添加加载状态优化 -4. 实现缓存策略优化 - -### 长期 -1. 考虑代码分割优化包体积 -2. 实现渐进式 Web 应用 (PWA) -3. 添加国际化支持 (i18n) -4. 性能监控和分析 - -## 📚 相关文档 - -- [OpenAPI 同步报告](./OPENAPI_SYNC_REPORT.md) -- [前端开发指南](./frontend/README.md) -- [API 文档](./frontend/src/api/generated/docs/) - ---- - -**清理完成时间:** 2025-11-10 -**受影响文件:** 5 个文件(2 删除 + 3 修复) -**删除代码行数:** ~507 行 -**修复类型错误:** 26 个 -**构建状态:** ✅ 成功 - diff --git a/docs/archive/root-cleanup/FRONTEND_SYNC_SUMMARY.md b/docs/archive/root-cleanup/FRONTEND_SYNC_SUMMARY.md deleted file mode 100644 index d1628c4..0000000 --- a/docs/archive/root-cleanup/FRONTEND_SYNC_SUMMARY.md +++ /dev/null @@ -1,168 +0,0 @@ -# Frontend 类型同步总结 - -## 概述 -根据 backend 的最新 DTO 定义,对 frontend 的类型定义和组件代码进行了全面更新,确保前后端数据结构一致。 - -## 修改的文件 - -### 1. 核心类型定义 - -#### `/frontend/src/core/types/index.ts` -- **Cluster 接口**: - - 将 `has_ca_data`, `has_cert_data`, `has_key_data`, `has_token` 改为 camelCase: `hasCAData`, `hasCertData`, `hasKeyData`, `hasToken` - - 将 `ca_data`, `cert_data`, `key_data` 改为 camelCase: `caData`, `certData`, `keyData` - -- **AppRegistry 接口**: - - 将 `has_password` 改为 camelCase: `hasPassword` - - `username` 和 `password` 改为可选字段(与 backend DTO 一致) - -- **AppInstance 接口**: - - 使用 snake_case `cluster_id` 和 `registry_id`(与 backend DTO 一致) - - 添加 `chart` 字段 - - 添加 `version` 字段 - - 添加 `revision` 字段 - -#### `/frontend/src/core/api/instance.api.ts` -- **Instance 接口**: - - 将 `clusterId` 改为 `cluster_id` - - 将 `registryId` 改为 `registry_id` - - 添加 `chart` 字段 - - 将 `tag` 改为 `version` - - 移除 `notes` 和 `deployedAt`(backend DTO 中不存在) - -### 2. 配置组件 - -#### `/frontend/src/features/configuration/clusters/components/ClusterForm.tsx` -- 更新所有对 `cluster.ca_data` 的引用为 `cluster.caData` -- 更新所有对 `cluster.cert_data` 的引用为 `cluster.certData` -- 更新所有对 `cluster.key_data` 的引用为 `cluster.keyData` -- 更新所有对 `cluster.has_ca_data` 的引用为 `cluster.hasCAData` -- 更新所有对 `cluster.has_cert_data` 的引用为 `cluster.hasCertData` -- 更新所有对 `cluster.has_key_data` 的引用为 `cluster.hasKeyData` - -#### `/frontend/src/features/configuration/clusters/components/ClusterList.tsx` -- 更新配置状态检查使用 camelCase 命名 - -#### `/frontend/src/features/configuration/registries/components/RegistryForm.tsx` -- 将 `has_password` 改为 `hasPassword` - -### 3. 实例管理组件 - -#### `/frontend/src/features/artifact/instances/components/EndpointsModal.tsx` -- 将 `instance.clusterId` 改为 `instance.cluster_id` - -#### `/frontend/src/features/artifact/instances/pages/InstancesManagementPage.tsx` -- 将所有 `instance.clusterId` 改为 `instance.cluster_id` - -#### `/frontend/src/features/artifact/instances/components/ModifyModal.tsx` -- 将 `instance.clusterId` 改为 `instance.cluster_id` -- 将 `instance.registryId` 改为 `instance.registry_id` -- 将 `instance.tag` 改为 `instance.version` -- 更新显示文本从 "Current Tag" 改为 "Current Version" - -#### `/frontend/src/features/artifact/instances/components/InstanceCard.tsx` -- 将 `instance.tag` 改为 `instance.version` - -## Backend DTO 参考 - -### ClusterResponse -```go -type ClusterResponse struct { - ID string `json:"id"` - Name string `json:"name"` - Host string `json:"host"` - Description string `json:"description"` - HasCAData bool `json:"hasCAData"` - HasCertData bool `json:"hasCertData"` - HasKeyData bool `json:"hasKeyData"` - HasToken bool `json:"hasToken"` - CAData string `json:"caData,omitempty"` - CertData string `json:"certData,omitempty"` - KeyData string `json:"keyData,omitempty"` - Token string `json:"token,omitempty"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` -} -``` - -### RegistryResponse -```go -type RegistryResponse struct { - ID string `json:"id"` - Name string `json:"name"` - URL string `json:"url"` - Description string `json:"description"` - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - HasPassword bool `json:"hasPassword"` - Insecure bool `json:"insecure"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` -} -``` - -### InstanceResponse -```go -type InstanceResponse struct { - ID string `json:"id"` - ClusterID string `json:"cluster_id"` - Name string `json:"name"` - Namespace string `json:"namespace"` - RegistryID string `json:"registry_id"` - Repository string `json:"repository"` - Chart string `json:"chart"` - Version string `json:"version"` - Description string `json:"description"` - Status string `json:"status"` - Revision int `json:"revision"` - Values map[string]interface{} `json:"values,omitempty"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` -} -``` - -## 命名约定总结 - -1. **响应字段(Response)**: - - Cluster 和 Registry: 使用 **camelCase** (如 `hasCAData`, `hasCertData`, `hasPassword`) - - Instance: 混合使用,`cluster_id` 和 `registry_id` 使用 **snake_case**,其他字段使用 **camelCase** - -2. **请求字段(Request)**: - - 使用 **snake_case** (如 `ca_data`, `cert_data`, `key_data`) - - Backend 会接受这些字段并进行验证 - -## 验证步骤 - -1. **启动 Backend**: -```bash -cd backend -make run-mock # 或 make dev -``` - -2. **启动 Frontend**: -```bash -cd frontend -npm install -npm run dev -``` - -3. **测试功能**: - - ✅ 创建/编辑集群配置 - - ✅ 查看集群证书配置状态 - - ✅ 创建/编辑 Registry 配置 - - ✅ 查看 Registry 密码配置状态 - - ✅ 部署应用实例 - - ✅ 查看实例详情(chart, version 等字段) - - ✅ 修改/升级实例 - - ✅ 查看实例入口信息 - -## 注意事项 - -1. **向后兼容性**: 如果有旧数据,可能需要数据迁移 -2. **GraphQL Schema**: 如果使用 GraphQL,需要同步更新 schema -3. **API 文档**: 建议更新 API 文档以反映最新的字段命名 - -## Linter 检查结果 - -✅ 所有修改的文件都通过了 linter 检查,无错误或警告。 - diff --git a/docs/archive/root-cleanup/IMPLEMENTATION-COMPLETE.md b/docs/archive/root-cleanup/IMPLEMENTATION-COMPLETE.md deleted file mode 100644 index 7ac7f02..0000000 --- a/docs/archive/root-cleanup/IMPLEMENTATION-COMPLETE.md +++ /dev/null @@ -1,371 +0,0 @@ -# 🎉 camelCase 实施完成总结 - -## ✅ 实施状态:100% 完成 - -**项目**: OCDP (Open Cloud Development Platform) -**任务**: 将整个项目从 snake_case JSON 迁移到 camelCase JSON -**完成时间**: 2025-11-10 -**测试状态**: ✅ 全部通过 - ---- - -## 📋 完成清单 - -### 1. 后端修改 ✅ - -#### Go DTO 文件(JSON Tags: snake_case → camelCase) - -- ✅ `backend/internal/adapter/input/http/dto/cluster_dto.go` - - 6 个字段转换(caData, certData, keyData, hasCaData, etc.) -- ✅ `backend/internal/adapter/input/http/dto/auth_dto.go` - - 5 个字段转换(accessToken, refreshToken, userId, createdAt, updatedAt) -- ✅ `backend/internal/adapter/input/http/dto/registry_dto.go` - - 3 个字段转换(hasPassword, createdAt, updatedAt) -- ✅ `backend/internal/adapter/input/http/dto/instance_dto.go` - - 6 个字段转换(registryId, clusterId, valuesYaml, keepHistory, etc.) -- ✅ `backend/internal/adapter/input/http/dto/artifact_dto.go` - - 5 个字段转换(registryId, registryUrl, repositoryName, etc.) -- ✅ `backend/internal/adapter/input/http/dto/monitoring_dto.go` - - 30+ 字段转换(所有监控相关字段) - -**总计**: 6 个文件,50+ 字段转换 - -#### 工具脚本 - -- ✅ `backend/scripts/convert-openapi-to-camelcase.cjs` - - OpenAPI 规范自动转换脚本 - -### 2. OpenAPI 规范 ✅ - -- ✅ `backend/docs/openapi.yaml` - - 所有属性从 snake_case 转换为 camelCase - - 备份文件: `openapi.yaml.backup` -- ✅ 验证:50+ 属性成功转换 - -### 3. 前端配置 ✅ - -#### 依赖和工具 - -- ✅ 安装 `orval@7.3.0` -- ✅ 创建 `frontend/orval.config.ts` -- ✅ 创建 `frontend/src/api/axios-mutator.ts` -- ✅ 创建 `frontend/src/api/case-converter.ts` - -#### API 客户端 - -- ✅ 重新生成 API 代码(使用 Orval) -- ✅ 生成目录: `frontend/src/api/generated-orval/` - - `api.ts` - API 函数 - - `api.schemas.ts` - TypeScript 类型定义 - -#### 文档和示例 - -- ✅ `frontend/src/api/README.md` - API 使用文档 -- ✅ `frontend/src/api/example.ts` - 代码示例 -- ✅ `frontend/src/api/index.ts` - 统一导出 -- ✅ `frontend/src/components/ApiTest.tsx` - 测试组件 - -#### 配置更新 - -- ✅ `frontend/package.json` - 添加 orval 依赖和脚本 -- ✅ `Makefile` - 更新 openapi-gen-frontend 命令 - -### 4. 测试和文档 ✅ - -#### 测试工具 - -- ✅ `scripts/test-api-camelcase.sh` - 自动化测试脚本 -- ✅ API 测试页面: `/api-test` 路由 -- ✅ 完整测试流程验证 - -#### 文档 - -- ✅ `CAMELCASE-MIGRATION.md` - 迁移详细说明 -- ✅ `TEST-GUIDE.md` - 测试指南 -- ✅ `TEST-RESULTS.md` - 测试结果 -- ✅ `IMPLEMENTATION-COMPLETE.md` - 本文档 - ---- - -## 🧪 测试结果 - -### 测试覆盖 - -| 测试类别 | 测试项 | 状态 | -|---------|-------|------| -| 后端 API | 健康检查 | ✅ 通过 | -| 后端 API | 用户注册 | ✅ 通过 | -| 后端 API | 用户登录 | ✅ 通过 | -| 后端 API | 创建集群 | ✅ 通过 | -| 后端 API | 获取集群列表 | ✅ 通过 | -| 后端 API | 创建 Registry | ✅ 通过 | -| 字段验证 | accessToken (camelCase) | ✅ 通过 | -| 字段验证 | refreshToken (camelCase) | ✅ 通过 | -| 字段验证 | userId (camelCase) | ✅ 通过 | -| 字段验证 | caData (camelCase) | ✅ 通过 | -| 字段验证 | certData (camelCase) | ✅ 通过 | -| 字段验证 | keyData (camelCase) | ✅ 通过 | -| 字段验证 | hasCaData (camelCase) | ✅ 通过 | -| 字段验证 | createdAt (camelCase) | ✅ 通过 | -| 字段验证 | updatedAt (camelCase) | ✅ 通过 | -| TypeScript | 类型定义 camelCase | ✅ 通过 | -| TypeScript | IDE 自动补全 | ✅ 通过 | -| 前后端通信 | 请求 JSON camelCase | ✅ 通过 | -| 前后端通信 | 响应 JSON camelCase | ✅ 通过 | - -**总计**: 18/18 测试通过 (100%) - ---- - -## 📊 变更统计 - -### 代码变更 - -``` -修改的文件数: 20+ -新增的文件数: 12 -修改的 Go 代码: 6 个 DTO 文件 -转换的字段数: 50+ -新增文档: 5 个 -测试脚本: 2 个 -``` - -### 架构变更 - -**之前 (snake_case):** -``` -Go Backend -├─ JSON tag: snake_case (ca_data) - ↓ -OpenAPI -├─ Property: snake_case (ca_data) - ↓ -TypeScript Frontend -├─ Property: snake_case (ca_data) ❌ -``` - -**现在 (camelCase) ✅:** -``` -Go Backend -├─ JSON tag: camelCase (caData) - ↓ -OpenAPI -├─ Property: camelCase (caData) - ↓ Orval Generator -TypeScript Frontend -├─ Property: camelCase (caData) ✅ -``` - ---- - -## 🎯 技术亮点 - -### 1. OpenAPI 驱动开发 - -- ✅ 单一真相源(OpenAPI 规范) -- ✅ 自动生成前后端代码 -- ✅ 类型安全保证 - -### 2. 符合标准 - -- ✅ **REST API 最佳实践** -- ✅ **Google JSON Style Guide** -- ✅ **TypeScript 命名规范** -- ✅ **Go 命名规范** - -### 3. 开发效率 - -- ✅ IDE 自动补全 -- ✅ 编译时类型检查 -- ✅ 清晰的错误提示 -- ✅ 统一的命名风格 - -### 4. 零性能损耗 - -- ✅ 无运行时转换 -- ✅ 原生 JSON 序列化/反序列化 -- ✅ 最优性能 - ---- - -## 📚 文件清单 - -### 新增文件 - -``` -📦 ocdp-go/ -├── 📄 CAMELCASE-MIGRATION.md (迁移文档) -├── 📄 TEST-GUIDE.md (测试指南) -├── 📄 TEST-RESULTS.md (测试结果) -├── 📄 IMPLEMENTATION-COMPLETE.md (本文档) -├── 📂 backend/ -│ ├── 📂 scripts/ -│ │ └── 📄 convert-openapi-to-camelcase.cjs (转换脚本) -│ └── 📂 docs/ -│ └── 📄 openapi.yaml.backup (备份) -├── 📂 frontend/ -│ ├── 📄 orval.config.ts (Orval 配置) -│ └── 📂 src/ -│ ├── 📂 api/ -│ │ ├── 📄 axios-mutator.ts (Axios 配置) -│ │ ├── 📄 case-converter.ts (工具函数) -│ │ ├── 📄 example.ts (使用示例) -│ │ ├── 📄 README.md (API 文档) -│ │ └── 📂 generated-orval/ (生成的代码) -│ └── 📂 components/ -│ └── 📄 ApiTest.tsx (测试组件) -└── 📂 scripts/ - └── 📄 test-api-camelcase.sh (测试脚本) -``` - -### 修改文件 - -``` -📦 修改的文件: -├── backend/internal/adapter/input/http/dto/*.go (6 个 DTO 文件) -├── backend/docs/openapi.yaml -├── frontend/package.json -├── frontend/src/api/index.ts -├── frontend/src/app/routes/AppRoutes.tsx -└── Makefile -``` - ---- - -## 🚀 使用方法 - -### 快速开始 - -```bash -# 1. 启动后端(Mock 模式) -cd backend -make run-0 - -# 2. 在新终端:启动前端 -cd frontend -npm run dev - -# 3. 访问测试页面 -open http://localhost:5173/api-test - -# 4. 点击"🚀 完整测试"按钮 -``` - -### 开发工作流 - -```bash -# 1. 修改 OpenAPI 规范 -vim backend/docs/openapi.yaml - -# 2. 重新生成前端代码 -cd frontend -npm run openapi-gen - -# 3. 重启服务 -# 前端会自动热重载 -# 后端使用 air 自动重载 -``` - ---- - -## 📖 参考文档 - -### 内部文档 - -1. **迁移文档**: [`CAMELCASE-MIGRATION.md`](./CAMELCASE-MIGRATION.md) - - 详细的迁移步骤和说明 - -2. **测试指南**: [`TEST-GUIDE.md`](./TEST-GUIDE.md) - - 如何测试整条链路 - -3. **测试结果**: [`TEST-RESULTS.md`](./TEST-RESULTS.md) - - 详细的测试数据和验证结果 - -4. **API 文档**: [`frontend/src/api/README.md`](./frontend/src/api/README.md) - - 前端 API 使用说明 - -5. **代码示例**: [`frontend/src/api/example.ts`](./frontend/src/api/example.ts) - - TypeScript 使用示例 - -### 外部参考 - -- [Google JSON Style Guide](https://google.github.io/styleguide/jsoncstyleguide.xml) -- [REST API Best Practices](https://restfulapi.net/) -- [Orval Documentation](https://orval.dev/) -- [OpenAPI Specification](https://swagger.io/specification/) - ---- - -## 🎊 成就解锁 - -- ✅ **完整 camelCase 支持** -- ✅ **类型安全的 API** -- ✅ **符合行业标准** -- ✅ **优秀的开发体验** -- ✅ **完善的文档** -- ✅ **全面的测试** - ---- - -## 🔮 未来工作 - -### 可选优化 - -1. **添加更多测试** - - 单元测试 - - 集成测试 - - E2E 测试 - -2. **性能监控** - - API 响应时间 - - 前端渲染性能 - -3. **错误处理增强** - - 统一错误格式 - - 错误码标准化 - -4. **文档增强** - - API 使用视频教程 - - 最佳实践指南 - -### 集成到现有功能 - -可以开始将新的 camelCase API 应用到: - -- ✅ 认证功能 -- ✅ 集群管理 -- ✅ Registry 管理 -- ✅ 实例部署 -- ⏳ 监控功能(待集成) - ---- - -## 👏 总结 - -**🎉 恭喜!你的项目现在完全支持 camelCase,符合现代 REST API 最佳实践!** - -### 关键成果 - -1. ✅ **后端**: Go JSON tags 全部使用 camelCase -2. ✅ **规范**: OpenAPI 属性全部使用 camelCase -3. ✅ **前端**: TypeScript 类型全部使用 camelCase -4. ✅ **通信**: JSON 传输使用 camelCase -5. ✅ **测试**: 完整测试验证通过 - -### 项目质量提升 - -- 🎯 **更好的类型安全** -- 🚀 **更高的开发效率** -- 📚 **更清晰的代码** -- 🔧 **更易于维护** -- ⚡ **最优的性能** - ---- - -**实施完成日期**: 2025-11-10 -**测试状态**: ✅ 全部通过 -**文档状态**: ✅ 完整 -**生产就绪**: ✅ 是 - -🎊 **项目升级成功!** - diff --git a/docs/archive/root-cleanup/OPENAPI_SYNC_REPORT.md b/docs/archive/root-cleanup/OPENAPI_SYNC_REPORT.md deleted file mode 100644 index d6e0512..0000000 --- a/docs/archive/root-cleanup/OPENAPI_SYNC_REPORT.md +++ /dev/null @@ -1,102 +0,0 @@ -# OpenAPI 同步报告 - -生成时间: 2025-11-10 - -## ✅ 已完成的工作 - -### 1. OpenAPI 规范验证 -- ✅ 使用 Docker 版本的 OpenAPI Generator (v7.17.0) -- ✅ 验证了 `backend/docs/openapi.yaml` 规范文件 -- ✅ 规范文件无错误,验证通过 - -### 2. 前端客户端代码生成 -- ✅ 使用 typescript-axios 生成器 -- ✅ 生成位置: `frontend/src/api/generated/` -- ✅ 生成内容: - - 7 个 API 接口文件 (artifacts, auth, clusters, health, instances, monitoring, registries) - - 26 个模型文件 (所有请求/响应类型) - - 配置和基础文件 (base.ts, configuration.ts, common.ts等) - -### 3. 依赖安装 -- ✅ 安装了生成代码所需的 npm 依赖 - -### 4. 类型定义修复 -- ✅ 添加了缺失的 `ValuesSchemaResponse` 类型定义 -- ✅ 修复了 `artifact.api.ts` 中的类型导出问题 -- ✅ 更新了 `getValuesSchema` 函数的返回类型 - -## 📊 生成的文件统计 - -### API 接口文件 (7个) -- `api/artifacts-api.ts` - Artifact 相关 API -- `api/auth-api.ts` - 认证相关 API -- `api/clusters-api.ts` - 集群管理 API -- `api/health-api.ts` - 健康检查 API -- `api/instances-api.ts` - 实例管理 API -- `api/monitoring-api.ts` - 监控 API -- `api/registries-api.ts` - Registry 管理 API - -### 模型文件 (26个) -包括所有的请求和响应类型,如: -- ArtifactListItem, ArtifactResponse -- ClusterResponse, CreateClusterRequest, UpdateClusterRequest -- RegistryResponse, CreateRegistryRequest, UpdateRegistryRequest -- InstanceResponse, CreateInstanceRequest, UpdateInstanceRequest -- AuthResponse, LoginRequest, RegisterRequest -- 等等... - -## ⚠️ 已知的非关键问题 - -以下文件中存在一些类型不匹配的问题,但不影响核心功能: - -1. **Mock 客户端 (`src/api/client.ts`)** - 25 个类型错误 - - 这是用于开发模式的 Mock 客户端 - - 需要更新以匹配新的 OpenAPI 类型 - -2. **示例代码 (`src/api/examples.ts`)** - 4 个类型错误 - - 示例/测试代码 - - 需要更新以匹配新的 API 签名 - -3. **组件中的可选属性处理** - 8 个类型错误 - - 一些组件需要添加可选链操作符或空值检查 - - 位于: RegistryTreeExplorer, RepositoryItem, TagCard - -## 🔧 建议的后续步骤 - -### 短期 (可选) -1. 修复 Mock 客户端的类型错误,如果需要使用 Mode 0 (Mock 模式) 进行开发 -2. 更新示例代码以匹配新的 API 签名 -3. 添加空值检查到相关组件 - -### 长期 -1. 建立自动化的 OpenAPI 同步流程 - ```bash - # 使用项目 Makefile - make openapi-gen-frontend - ``` -2. 在 CI/CD 中添加 OpenAPI 规范验证步骤 - -## 📝 使用新生成的 API 客户端 - -```typescript -// 导入生成的 API -import { ArtifactsApi, ClustersApi, RegistriesApi } from '@/api/generated/api'; -import type { ClusterResponse, CreateClusterRequest } from '@/api/generated/models'; - -// 创建 API 实例 -const clustersApi = new ClustersApi(); - -// 使用 API -const clusters: ClusterResponse[] = await clustersApi.listClusters(); -``` - -## 📚 参考文档 - -- OpenAPI 规范: `backend/docs/openapi.yaml` -- 生成的 API 文档: `frontend/src/api/generated/docs/` -- 项目 Makefile: 根目录的 `Makefile` (包含 `openapi-gen-frontend` 命令) - ---- - -生成工具: OpenAPI Generator v7.17.0 -生成器: typescript-axios diff --git a/docs/archive/root-cleanup/QUICK-TEST.md b/docs/archive/root-cleanup/QUICK-TEST.md deleted file mode 100644 index 4d372ae..0000000 --- a/docs/archive/root-cleanup/QUICK-TEST.md +++ /dev/null @@ -1,250 +0,0 @@ -# 🚀 快速测试指南 - -## ✅ 当前状态 - -- ✅ **后端服务**: http://localhost:8080 (运行中) -- ✅ **前端服务**: http://localhost:5175 (运行中) -- ✅ **API 通信**: camelCase 正常工作 -- ✅ **测试通过**: 所有 API 测试通过 - -## 📝 测试结果 - -### 后端 API 测试 - -| 测试项 | 状态 | 说明 | -|-------|------|------| -| 健康检查 | ✅ | `/health` 返回 healthy | -| 登录 API | ✅ | `accessToken` (camelCase) ✓ | -| 获取集群 | ✅ | `createdAt`, `hasCaData` (camelCase) ✓ | -| 创建集群 | ✅ | 请求和响应都是 camelCase ✓ | - -### 前端服务 - -| 项目 | URL | 状态 | -|-----|-----|------| -| 主应用 | http://localhost:5175 | ✅ 可访问 | -| API 测试页面 | http://localhost:5175/api-test | ⚠️ 需要在浏览器中访问 | - -## 🧪 测试方法 - -### 方法 1: 浏览器测试(推荐) - -1. **打开主应用** - ``` - http://localhost:5175 - ``` - - 查看登录页面 - - 测试认证功能 - -2. **打开 API 测试页面** - ``` - http://localhost:5175/api-test - ``` - - 点击 "🚀 完整测试" 按钮 - - 观察所有 API 调用是否成功 - - 验证 camelCase 字段 - -### 方法 2: cURL 测试(快速验证) - -```bash -# 1. 健康检查 -curl http://localhost:8080/health - -# 2. 登录(获取 Token) -curl -X POST http://localhost:8080/api/v1/auth/login \ - -H "Content-Type: application/json" \ - -d '{"username":"admin","password":"admin123"}' - -# 3. 创建集群(使用 camelCase) -TOKEN="你的token" -curl -X POST http://localhost:8080/api/v1/clusters \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{ - "name": "test-cluster", - "host": "https://k8s.example.com:6443", - "caData": "LS0tLS1CRUdJTi1DRVJUSUZJQ0FURS0tLS0t", - "certData": "LS0tLS1CRUdJTi1DRVJUSUZJQ0FURS0tLS0t", - "keyData": "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVkt" - }' -``` - -### 方法 3: 自动化测试脚本 - -```bash -# 运行完整测试 -cd /home/mango/workspace/ocdp-go -./scripts/test-api-camelcase.sh - -# 运行前端集成测试 -./scripts/test-frontend-integration.sh -``` - -## 📖 前端 API 使用示例 - -### 在浏览器控制台测试 - -打开 http://localhost:5175,按 F12 打开控制台,运行: - -```javascript -// 1. 导入 API 函数(如果已经在页面中加载) -// 或者在组件中使用 - -// 2. 登录示例 -const response = await fetch('http://localhost:8080/api/v1/auth/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - username: 'admin', - password: 'admin123' - }) -}); - -const data = await response.json(); -console.log('登录响应 (camelCase):', data); -// 应该看到: { accessToken: "...", refreshToken: "...", userId: "..." } - -// 3. 获取集群列表 -const token = data.accessToken; -const clustersResponse = await fetch('http://localhost:8080/api/v1/clusters', { - headers: { 'Authorization': `Bearer ${token}` } -}); - -const clusters = await clustersResponse.json(); -console.log('集群列表 (camelCase):', clusters); -// 应该看到 camelCase 字段: createdAt, hasCaData, etc. -``` - -## ✨ camelCase 验证清单 - -验证以下字段都是 camelCase 格式: - -### Auth API -- [ ] `accessToken` -- [ ] `refreshToken` -- [ ] `userId` - -### Cluster API -- [ ] `caData` -- [ ] `certData` -- [ ] `keyData` -- [ ] `hasCaData` -- [ ] `hasCertData` -- [ ] `hasKeyData` -- [ ] `hasToken` -- [ ] `createdAt` -- [ ] `updatedAt` - -### Registry API -- [ ] `registryId` -- [ ] `hasPassword` -- [ ] `createdAt` -- [ ] `updatedAt` - -## 🎯 预期结果 - -### API 响应示例(camelCase ✅) - -**登录响应:** -```json -{ - "accessToken": "eyJhbGci...", - "refreshToken": "eyJhbGci...", - "userId": "e0b632e8-...", - "username": "admin" -} -``` - -**集群响应:** -```json -{ - "id": "ed37e2b2-...", - "name": "test-cluster", - "host": "https://k8s.example.com:6443", - "hasCaData": true, - "hasCertData": true, - "hasKeyData": true, - "hasToken": false, - "caData": "••••••••", - "certData": "••••••••", - "keyData": "••••••••", - "createdAt": "2025-11-10T10:03:04Z", - "updatedAt": "2025-11-10T10:03:04Z" -} -``` - -## 🔧 故障排查 - -### 问题:前端无法连接后端 - -**检查:** -```bash -# 检查后端是否运行 -curl http://localhost:8080/health - -# 如果没有运行 -cd backend -make run-0 -``` - -### 问题:前端服务未启动 - -**检查:** -```bash -# 检查前端是否运行 -curl http://localhost:5175 - -# 如果没有运行 -cd frontend -npm run dev -``` - -### 问题:API 返回 401 Unauthorized - -**原因**: Token 过期或未设置 - -**解决**: -1. 重新登录获取新 Token -2. 确保 Authorization header 正确设置 - -### 问题:看到 snake_case 而不是 camelCase - -**检查**: -1. 确认 Go DTO JSON tags 已更新 -2. 确认 OpenAPI 规范已更新 -3. 确认前端代码已重新生成:`npm run openapi-gen` -4. 重启后端和前端服务 - -## 📚 相关文档 - -- **完整文档**: [IMPLEMENTATION-COMPLETE.md](./IMPLEMENTATION-COMPLETE.md) -- **测试指南**: [TEST-GUIDE.md](./TEST-GUIDE.md) -- **测试结果**: [TEST-RESULTS.md](./TEST-RESULTS.md) -- **API 文档**: [frontend/src/api/README.md](./frontend/src/api/README.md) - -## 🎊 成功标志 - -当你看到以下内容时,说明一切正常: - -✅ 后端返回 JSON 使用 camelCase -✅ 前端可以正常解析响应 -✅ 所有 API 调用成功 -✅ TypeScript 类型提示正常 -✅ 浏览器控制台无错误 - ---- - -**当前服务地址:** -- 🌐 前端: http://localhost:5175 -- 🔌 后端: http://localhost:8080 -- 🧪 测试: http://localhost:5175/api-test - -**快速测试命令:** -```bash -# 一键运行所有测试 -cd /home/mango/workspace/ocdp-go -./scripts/test-frontend-integration.sh -``` - -🎉 **测试成功!camelCase API 完全正常工作!** - diff --git a/docs/archive/root-cleanup/README-CAMELCASE.md b/docs/archive/root-cleanup/README-CAMELCASE.md deleted file mode 100644 index df5411b..0000000 --- a/docs/archive/root-cleanup/README-CAMELCASE.md +++ /dev/null @@ -1,385 +0,0 @@ -# 🎉 camelCase API 实施完成 - -> **状态**: ✅ 完成并测试通过 -> **日期**: 2025-11-10 -> **版本**: 1.0.0 - ---- - -## 📊 项目概览 - -已成功将 OCDP 项目的 API 从 **snake_case** 迁移到 **camelCase**,符合现代 REST API 标准。 - -### 核心变更 - -``` -之前 (snake_case): 现在 (camelCase): ✅ -├─ ca_data → caData -├─ cert_data → certData -├─ has_ca_data → hasCaData -├─ created_at → createdAt -├─ access_token → accessToken -└─ refresh_token → refreshToken -``` - ---- - -## 🚀 快速开始 - -### 1. 启动服务 - -```bash -# 终端 1: 启动后端 -cd /home/mango/workspace/ocdp-go/backend -make run-0 - -# 终端 2: 启动前端 -cd /home/mango/workspace/ocdp-go/frontend -npm run dev -``` - -### 2. 测试(3 种方式) - -#### 方式 A: HTML 测试页面(最简单)⭐ - -``` -打开浏览器访问: -http://localhost:5175/api-test.html - -点击"🚀 完整测试"按钮 -``` - -#### 方式 B: 命令行测试 - -```bash -cd /home/mango/workspace/ocdp-go -./scripts/test-api-camelcase.sh -``` - -#### 方式 C: React 应用测试 - -``` -http://localhost:5175 -登录后访问 /api-test 路由 -``` - ---- - -## 📋 实施清单 - -### ✅ 后端修改 - -- [x] 6 个 DTO 文件 JSON tags 更新 -- [x] 50+ 字段转换为 camelCase -- [x] 所有 API 响应使用 camelCase - -### ✅ OpenAPI 规范 - -- [x] 所有属性转换为 camelCase -- [x] 自动转换脚本创建 -- [x] 备份文件保存 - -### ✅ 前端配置 - -- [x] 安装 Orval 生成器 -- [x] 配置 Axios mutator -- [x] 重新生成 API 客户端 -- [x] TypeScript 类型定义更新 - -### ✅ 测试和文档 - -- [x] 自动化测试脚本 -- [x] HTML 测试页面 -- [x] React 测试组件 -- [x] 完整文档创建 - ---- - -## 📚 文档索引 - -| 文档 | 用途 | 路径 | -|------|------|------| -| 🚀 **快速测试** | 立即测试 | [TESTING-COMPLETE.md](./TESTING-COMPLETE.md) | -| 📖 **测试指南** | 详细测试说明 | [TEST-GUIDE.md](./TEST-GUIDE.md) | -| 📊 **测试结果** | 测试数据 | [TEST-RESULTS.md](./TEST-RESULTS.md) | -| 🎯 **实施总结** | 完整实施说明 | [IMPLEMENTATION-COMPLETE.md](./IMPLEMENTATION-COMPLETE.md) | -| 📝 **迁移文档** | 迁移详情 | [CAMELCASE-MIGRATION.md](./CAMELCASE-MIGRATION.md) | -| 💡 **API 文档** | 前端 API 使用 | [frontend/src/api/README.md](./frontend/src/api/README.md) | -| 🧪 **快速指南** | 快速参考 | [QUICK-TEST.md](./QUICK-TEST.md) | - ---- - -## 🎯 测试验证 - -### 自动化测试结果 - -```bash -$ ./scripts/test-api-camelcase.sh - -✅ 后端服务运行正常 -✅ 登录成功 - accessToken ✓ -✅ 集群列表 - createdAt, hasCaData ✓ -✅ 创建集群 - caData, certData, keyData ✓ -✅ 所有字段使用 camelCase - -🎉 测试完成! -``` - -### 测试覆盖 - -- ✅ 18/18 测试用例通过 (100%) -- ✅ 6 个 API 端点验证 -- ✅ 50+ 字段验证 -- ✅ 前后端通信验证 - ---- - -## 🌐 服务地址 - -| 服务 | URL | 状态 | -|------|-----|------| -| 前端应用 | http://localhost:5175 | ✅ 运行中 | -| 后端 API | http://localhost:8080 | ✅ 运行中 | -| HTML 测试 | http://localhost:5175/api-test.html | ✅ 可用 | -| 健康检查 | http://localhost:8080/health | ✅ 正常 | - ---- - -## 💻 代码示例 - -### 前端使用(TypeScript) - -```typescript -import { createCluster, listClusters, setAuthToken } from '@/api'; - -// 设置认证 -setAuthToken('your-jwt-token'); - -// 创建集群 - 使用 camelCase ✅ -const cluster = await createCluster({ - name: 'my-cluster', - host: 'https://k8s.example.com', - caData: 'base64...', // ✅ camelCase - certData: 'base64...', // ✅ camelCase - keyData: 'base64...', // ✅ camelCase -}); - -// 获取集群列表 -const clusters = await listClusters(); -console.log(clusters[0].createdAt); // ✅ camelCase -console.log(clusters[0].hasCaData); // ✅ camelCase -``` - -### 后端定义(Go) - -```go -type CreateClusterRequest struct { - Name string `json:"name"` - Host string `json:"host"` - CAData string `json:"caData"` // ✅ camelCase - CertData string `json:"certData"` // ✅ camelCase - KeyData string `json:"keyData"` // ✅ camelCase -} -``` - -### JSON 传输 - -```json -{ - "name": "my-cluster", - "host": "https://k8s.example.com", - "caData": "base64...", - "certData": "base64...", - "keyData": "base64..." -} -``` - ---- - -## 🎨 技术亮点 - -### 1. OpenAPI 驱动开发 - -- ✅ 单一真相源 -- ✅ 自动代码生成 -- ✅ 类型安全保证 - -### 2. 符合标准 - -- ✅ REST API 最佳实践 -- ✅ Google JSON Style Guide -- ✅ TypeScript/Go 命名规范 - -### 3. 零性能损耗 - -- ✅ 无运行时转换 -- ✅ 原生序列化 -- ✅ 最优性能 - -### 4. 优秀的开发体验 - -- ✅ IDE 自动补全 -- ✅ 编译时类型检查 -- ✅ 清晰的错误提示 - ---- - -## 🔄 工作流程 - -### 开发流程 - -```bash -# 1. 修改 OpenAPI 规范 -vim backend/docs/openapi.yaml - -# 2. 重新生成前端代码 -cd frontend && npm run openapi-gen - -# 3. 服务自动重载 -# 后端: air 自动重载 -# 前端: Vite HMR -``` - -### 后端运行模式 - -```bash -# 模式 0: Mock(无依赖,推荐开发) -make run-0 - -# 模式 1: 真实数据库(PostgreSQL) -make run-1 - -# 模式 2: 全容器(生产模拟) -make run-2 -``` - ---- - -## 📈 项目统计 - -### 代码变更 - -- **修改文件**: 20+ 个 -- **新增文件**: 12 个 -- **转换字段**: 50+ 个 -- **测试用例**: 18 个 -- **文档页数**: 7 个 - -### 时间投入 - -- 后端修改: ✅ 完成 -- OpenAPI 更新: ✅ 完成 -- 前端配置: ✅ 完成 -- 测试验证: ✅ 完成 -- 文档编写: ✅ 完成 - ---- - -## 🎓 学习资源 - -### 标准和最佳实践 - -- [Google JSON Style Guide](https://google.github.io/styleguide/jsoncstyleguide.xml) -- [REST API Best Practices](https://restfulapi.net/) -- [OpenAPI Specification](https://swagger.io/specification/) - -### 工具文档 - -- [Orval Documentation](https://orval.dev/) -- [Go JSON Tags](https://golang.org/pkg/encoding/json/) -- [TypeScript Handbook](https://www.typescriptlang.org/docs/) - ---- - -## 🆘 故障排查 - -### 常见问题 - -**Q: 页面打不开?** -```bash -# 检查服务状态 -curl http://localhost:8080/health -curl http://localhost:5175 - -# 重启服务 -cd backend && make run-0 -cd frontend && npm run dev -``` - -**Q: 仍然看到 snake_case?** -```bash -# 重新生成前端代码 -cd frontend && npm run openapi-gen - -# 重启服务 -``` - -**Q: TypeScript 报错?** -```bash -# 重新安装依赖 -cd frontend && npm install - -# 检查编译 -npx tsc --noEmit -``` - ---- - -## 🎊 总结 - -### 成就解锁 - -- ✅ **完整 camelCase 支持** -- ✅ **类型安全的 API** -- ✅ **符合行业标准** -- ✅ **优秀的开发体验** -- ✅ **完善的文档** -- ✅ **全面的测试** - -### 项目质量提升 - -- 🎯 **更好的类型安全** -- 🚀 **更高的开发效率** -- 📚 **更清晰的代码** -- 🔧 **更易于维护** -- ⚡ **最优的性能** - ---- - -## 📞 快速链接 - -### 立即测试 - -🌐 **HTML 测试页面**: http://localhost:5175/api-test.html -🖥️ **主应用**: http://localhost:5175 -🔌 **API 文档**: http://localhost:8080/health - -### 运行测试 - -```bash -# 快速测试 -./scripts/test-api-camelcase.sh - -# 完整测试 -./scripts/test-frontend-integration.sh -``` - ---- - -## ✨ 特别说明 - -本项目现在完全使用 **camelCase** 进行 JSON 通信,符合: - -- ✅ REST API 最佳实践 -- ✅ Google JSON Style Guide -- ✅ 现代 Web 开发标准 -- ✅ TypeScript/JavaScript 生态 - -🎉 **恭喜!你的项目已升级到现代 API 标准!** - ---- - -**最后更新**: 2025-11-10 -**版本**: 1.0.0 -**状态**: ✅ 生产就绪 - diff --git a/docs/archive/root-cleanup/REFACTOR_COMPLETE.md b/docs/archive/root-cleanup/REFACTOR_COMPLETE.md deleted file mode 100644 index c5ec96c..0000000 --- a/docs/archive/root-cleanup/REFACTOR_COMPLETE.md +++ /dev/null @@ -1,236 +0,0 @@ -# 🎉 前端清理与重构完成报告 - -## ✅ 所有任务完成 - -### 任务清单 -- ✅ 分析当前前端代码结构,识别多余代码 -- ✅ 修复 Mock 客户端的类型错误(已删除未使用文件) -- ✅ 清理示例代码(已删除未使用文件) -- ✅ 修复组件中的类型错误(3 个文件,8 处修复) -- ✅ 验证所有页面功能正常工作 -- ✅ 运行构建测试确保无编译错误 - -## 📊 工作成果 - -### 代码清理 -``` -删除文件:2 个 - - frontend/src/api/client.ts (~450 行) - - frontend/src/api/examples.ts (~57 行) - -修复文件:3 个 - - RegistryTreeExplorer.tsx (1 处修复) - - RepositoryItem.tsx (2 处修复) - - TagCard.tsx (2 处修复) - -总计删除:~507 行未使用代码 -总计修复:26 个 TypeScript 编译错误 -``` - -### 构建状态 -```bash -✅ TypeScript 编译:0 错误 -✅ Vite 构建:成功 -✅ 构建时间:5.16s -✅ 输出大小: - - index.html: 0.40 kB - - CSS: 37.03 kB (gzip: 6.87 kB) - - JS: 394.93 kB (gzip: 110.53 kB) -``` - -## 🚀 如何使用 - -### 开发模式 - -```bash -# 方式 1: 使用 Makefile (推荐) -cd /home/mango/workspace/ocdp-go -make openapi-gen-frontend # 如需更新 API 客户端 -cd frontend && npm run dev - -# 方式 2: 直接运行 -cd /home/mango/workspace/ocdp-go/frontend -npm run dev -``` - -访问地址:`http://localhost:5173` - -### 生产构建 - -```bash -cd /home/mango/workspace/ocdp-go/frontend -npm run build - -# 预览构建产物 -npm run preview -``` - -### Docker 部署 - -```bash -# 开发模式 (Mode 0 - Mock) -cd /home/mango/workspace/ocdp-go/frontend -make run-0 - -# 开发模式 (Mode 1 - Real API) -make run-1 - -# Docker Compose 部署 (Mode 2) -make run-2 -``` - -## 📁 项目结构 - -``` -frontend/ -├── src/ -│ ├── api/ -│ │ └── generated/ # ✅ OpenAPI 生成的客户端(已同步) -│ │ ├── api/ # 7 个 API 类 -│ │ └── models/ # 25 个类型模型 -│ │ -│ ├── core/ -│ │ ├── api/ # ✅ 业务层 API 封装(18 个文件使用) -│ │ ├── types/ # 核心类型定义 -│ │ └── hooks/ # 自定义 Hooks -│ │ -│ ├── features/ # ✅ 功能模块(所有页面正常工作) -│ │ ├── auth/ # 认证 -│ │ ├── home/ # 主页 -│ │ ├── configuration/ # 配置管理 -│ │ │ ├── clusters/ # 集群配置 -│ │ │ └── registries/ # Registry 配置 -│ │ ├── artifact/ # Artifact 管理 -│ │ │ ├── registries/ # Registries 浏览器 -│ │ │ └── instances/ # 实例管理 -│ │ └── monitoring/ # 监控 -│ │ └── clusters/ # 集群监控 -│ │ -│ ├── shared/ # 共享组件和工具 -│ │ ├── components/ # UI 组件库 -│ │ ├── services/ # 共享服务 -│ │ └── utils/ # 工具函数 -│ │ -│ └── app/ # 应用配置 -│ ├── routes/ # 路由配置 -│ └── providers/ # Context Providers -│ -├── dist/ # ✅ 构建产物(已生成) -├── package.json -└── vite.config.ts -``` - -## 🎯 页面功能验证 - -### 已验证的路由 -| 路径 | 功能 | 状态 | -|------|------|------| -| `/` | 登录页面 | ✅ | -| `/home` | 主页仪表板 | ✅ | -| `/configuration/clusters` | 集群配置 | ✅ | -| `/configuration/registries` | Registry 配置 | ✅ | -| `/artifact/registries` | Registries 浏览器 | ✅ | -| `/artifact/instances` | 实例管理 | ✅ | -| `/monitoring/clusters` | 集群监控 | ✅ | - -### 核心功能 -- ✅ JWT 认证和授权 -- ✅ 集群 CRUD 操作 -- ✅ Registry CRUD 操作 -- ✅ OCI Artifact 浏览(Helm Chart & 容器镜像) -- ✅ Artifact 类型过滤 -- ✅ Helm Release 部署管理 -- ✅ 集群监控和指标展示 -- ✅ 响应式设计 - -## 🔧 技术改进 - -### API 层优化 -- ✅ 删除旧的 Mock 客户端 -- ✅ 统一使用 OpenAPI 生成的类型 -- ✅ 所有 API 调用通过 `core/api` 封装 -- ✅ 类型安全得到保证 - -### 类型安全 -- ✅ 修复所有 TypeScript 编译错误 -- ✅ 添加必要的空值检查 -- ✅ 使用正确的 OpenAPI 生成类型 - -### 代码质量 -- ✅ 删除 507 行未使用代码 -- ✅ 提高代码可维护性 -- ✅ 减少潜在运行时错误 - -## 📚 相关文档 - -1. **[OpenAPI 同步报告](./OPENAPI_SYNC_REPORT.md)** - - OpenAPI 规范验证 - - 客户端代码生成详情 - - 生成的 API 和模型统计 - -2. **[前端重构总结](./FRONTEND_REFACTOR_SUMMARY.md)** - - 详细的重构过程 - - 代码结构分析 - - 最佳实践和建议 - -3. **[API 文档](./frontend/src/api/generated/docs/)** - - 所有 API 接口文档 - - 请求/响应模型说明 - -## 🎁 快速开始 - -### 1. 安装依赖(如果还没有) -```bash -cd /home/mango/workspace/ocdp-go/frontend -npm install -``` - -### 2. 启动开发服务器 -```bash -npm run dev -``` - -### 3. 访问应用 -打开浏览器访问:`http://localhost:5173` - -默认登录信息(Mock 模式): -- 用户名:`admin` -- 密码:`admin123` - -### 4. 构建生产版本 -```bash -npm run build -``` - -## 🔄 更新 API 客户端 - -如果后端 OpenAPI 规范有更新: - -```bash -# 在项目根目录 -make openapi-gen-frontend - -# 或使用脚本 -./scripts/sync-openapi-frontend.sh -``` - -## ✨ 总结 - -本次重构成功: -- 🧹 清理了 507 行未使用代码 -- 🔧 修复了 26 个 TypeScript 编译错误 -- ✅ 确保所有页面功能正常工作 -- 📦 成功构建生产版本 -- 📝 完善了文档 - -前端代码现在更加: -- ✨ 简洁清晰 -- 🎯 类型安全 -- 🚀 易于维护 -- 📖 文档完善 - ---- - -**重构完成时间:** 2025-11-10 -**状态:** ✅ 所有任务完成 -**下一步:** 可以开始正常开发或部署 diff --git a/docs/archive/root-cleanup/TEST-GUIDE.md b/docs/archive/root-cleanup/TEST-GUIDE.md deleted file mode 100644 index 46293af..0000000 --- a/docs/archive/root-cleanup/TEST-GUIDE.md +++ /dev/null @@ -1,379 +0,0 @@ -# 🧪 camelCase API 测试指南 - -## 测试目标 - -验证整条链路:**后端 Go → JSON (camelCase) → 前端 TypeScript** 是否正常工作。 - -## 📋 准备工作 - -### 1. 确保依赖已安装 - -```bash -# 后端依赖 -cd backend -go mod download - -# 前端依赖 -cd ../frontend -npm install -``` - -### 2. 确保代码已重新生成 - -```bash -# 从项目根目录 -make openapi-gen-frontend -``` - -## 🎮 后端运行模式 - -后端提供三种运行模式,根据你的需求选择: - -### run-0: Mock 模式(推荐用于测试) -```bash -cd backend -make run-0 -``` -- ✅ **无依赖**:不需要数据库、Redis 等 -- ✅ **快速启动**:立即可用 -- ✅ **热重载**:代码变更自动重启 -- ✅ **适合开发和测试** - -### run-1: 真实数据库模式 -```bash -cd backend -make run-1 -``` -- 🐘 PostgreSQL (Docker) -- 🔥 热重载 -- 📊 真实数据持久化 -- 停止:`Ctrl+C` + `make clean-1` - -### run-2: 全容器模式 -```bash -cd backend -make run-2 -``` -- 🐳 所有服务在 Docker 中 -- 🔄 后台运行 -- 🏭 接近生产环境 -- 停止:`docker compose down` - -## 🚀 测试步骤 - -### 方案 A:自动化测试脚本(推荐) - -#### 步骤 1: 启动后端服务(Mock 模式) - -```bash -# 在终端 1 中运行 -cd /home/mango/workspace/ocdp-go/backend -make run-0 - -# run-0: Mock 模式(无依赖,最简单) -# run-1: 真实 PostgreSQL(Docker) -# run-2: 全部 Docker(后台运行) -``` - -等待看到: -``` -🎭 Run-0: Hot reload + Mock -✓ Server started on :8080 (Mock mode) -``` - -#### 步骤 2: 运行 API 测试脚本 - -```bash -# 在终端 2 中运行 -cd /home/mango/workspace/ocdp-go -./scripts/test-api-camelcase.sh -``` - -**预期结果:** -- ✅ 所有 API 返回 camelCase 字段 -- ✅ `accessToken`, `refreshToken`, `userId` 存在 -- ✅ `createdAt`, `updatedAt` 存在 -- ✅ `caData`, `certData`, `keyData`, `hasCaData` 存在 - -#### 步骤 3: 启动前端服务 - -```bash -# 在终端 3 中运行 -cd /home/mango/workspace/ocdp-go/frontend -npm run dev -``` - -访问: http://localhost:5173 - -#### 步骤 4: 前端测试页面 - -访问: **http://localhost:5173/api-test** - -点击 **"🚀 完整测试"** 按钮,观察: - -1. **注册测试** - 创建新用户 -2. **登录测试** - 获取 JWT token - - 验证响应包含 `accessToken` (camelCase) -3. **获取集群列表** - 测试 GET 请求 - - 验证响应包含 `createdAt`, `updatedAt` (camelCase) -4. **创建集群** - 测试 POST 请求 - - 发送 `caData`, `certData`, `keyData` (camelCase) - - 验证响应包含 `hasCaData` (camelCase) - -**预期结果:** -``` -🧪 开始完整测试流程... - -步骤 1: 注册用户 -✅ 注册成功 - -步骤 2: 登录 -✅ 登录成功 - Token: eyJhbGciOiJIUzI1NiIs... - -步骤 3: 获取集群列表 -✅ 获取成功 - 共 X 个集群 - -步骤 4: 创建测试集群 (camelCase) -✅ 创建成功 - ID: cluster-xxx - -🎉 完整测试流程完成! 所有 API 调用成功,camelCase 工作正常! -``` - -### 方案 B:手动 cURL 测试 - -#### 1. 测试健康检查 - -```bash -curl http://localhost:8080/health -# 预期: {"status":"healthy"} -``` - -#### 2. 测试登录(获取 Token) - -```bash -curl -X POST http://localhost:8080/api/v1/auth/login \ - -H "Content-Type: application/json" \ - -d '{ - "username": "admin", - "password": "admin123" - }' -``` - -**预期响应(注意 camelCase):** -```json -{ - "accessToken": "eyJhbGci...", - "refreshToken": "eyJhbGci...", - "userId": "user-123", - "username": "admin" -} -``` - -✅ **验证点**:字段名是 `accessToken`、`refreshToken`、`userId`(camelCase) - -#### 3. 测试创建集群(使用 camelCase) - -```bash -# 替换 YOUR_TOKEN 为上一步获取的 token -curl -X POST http://localhost:8080/api/v1/clusters \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer YOUR_TOKEN" \ - -d '{ - "name": "test-cluster", - "host": "https://k8s.example.com:6443", - "description": "Test cluster", - "caData": "LS0tLS1CRUdJTi1DRVJUSUZJQ0FURS0tLS0t", - "certData": "LS0tLS1CRUdJTi1DRVJUSUZJQ0FURS0tLS0t", - "keyData": "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVkt" - }' -``` - -**预期响应(注意 camelCase):** -```json -{ - "id": "cluster-abc123", - "name": "test-cluster", - "host": "https://k8s.example.com:6443", - "description": "Test cluster", - "hasCaData": true, - "hasCertData": true, - "hasKeyData": true, - "hasToken": false, - "caData": "••••••••", - "certData": "••••••••", - "keyData": "••••••••", - "createdAt": "2025-11-10T10:00:00Z", - "updatedAt": "2025-11-10T10:00:00Z" -} -``` - -✅ **验证点**: -- 请求使用 `caData`、`certData`、`keyData` (camelCase) -- 响应包含 `hasCaData`、`createdAt`、`updatedAt` (camelCase) - -#### 4. 测试获取集群列表 - -```bash -curl -X GET http://localhost:8080/api/v1/clusters \ - -H "Authorization: Bearer YOUR_TOKEN" -``` - -**预期响应:** -```json -[ - { - "id": "cluster-abc123", - "name": "test-cluster", - "createdAt": "2025-11-10T10:00:00Z", - "updatedAt": "2025-11-10T10:00:00Z", - ... - } -] -``` - -✅ **验证点**:响应字段使用 camelCase - -## 🔍 验证清单 - -### 后端验证 - -- [ ] Go struct JSON tags 使用 camelCase -- [ ] OpenAPI 规范属性使用 camelCase -- [ ] API 响应 JSON 使用 camelCase -- [ ] 后端可以正确解析 camelCase 请求 - -### 前端验证 - -- [ ] TypeScript 类型定义使用 camelCase -- [ ] 前端代码使用 camelCase 属性 -- [ ] API 调用发送 camelCase JSON -- [ ] 响应解析为 camelCase 对象 -- [ ] IDE 自动补全正常工作 - -### 字段验证 - -必须验证这些关键字段都是 camelCase: - -**Auth API:** -- `accessToken` ✅ -- `refreshToken` ✅ -- `userId` ✅ - -**Cluster API:** -- `caData` ✅ -- `certData` ✅ -- `keyData` ✅ -- `hasCaData` ✅ -- `hasCertData` ✅ -- `hasKeyData` ✅ -- `hasToken` ✅ -- `createdAt` ✅ -- `updatedAt` ✅ - -**Registry API:** -- `registryId` ✅ -- `registryUrl` ✅ -- `hasPassword` ✅ -- `createdAt` ✅ -- `updatedAt` ✅ - -**Instance API:** -- `registryId` ✅ -- `clusterId` ✅ -- `valuesYaml` ✅ - -## 📊 测试结果示例 - -### 成功的测试输出 - -``` -🧪 OCDP API camelCase 测试 -================================ - -步骤 1: 检查服务健康状态 -✅ 后端服务运行正常 - -步骤 2: 测试注册 API -✅ 注册成功,发现 accessToken 字段 (camelCase) - -步骤 3: 测试登录 API -✅ 登录成功! -Token (前20字符): eyJhbGciOiJIUzI1NiIs... -✅ 验证: accessToken 字段存在 (camelCase) ✓ -✅ 验证: refreshToken 字段存在 (camelCase) ✓ -✅ 验证: userId 字段存在 (camelCase) ✓ - -步骤 4: 测试创建集群 API (camelCase) -✅ 集群创建成功! -Cluster ID: cluster-1731240123456 -✅ 验证: createdAt 字段存在 (camelCase) ✓ -✅ 验证: hasCaData 字段存在 (camelCase) ✓ - -步骤 5: 测试获取集群列表 -✅ 获取集群列表成功,字段使用 camelCase ✓ - -步骤 6: 测试 Registry API (camelCase) -✅ Registry API 使用 camelCase ✓ - -================================ -🎉 API 测试完成! - -测试总结: -✅ 后端服务运行正常 -✅ JSON 字段使用 camelCase 格式 -✅ 前后端通信正常 -``` - -## 🐛 故障排查 - -### 问题 1: 后端未启动 - -**症状:** `Connection refused` - -**解决:** -```bash -cd backend -make run-mock -``` - -### 问题 2: 字段仍是 snake_case - -**检查:** -1. 确认 Go DTO JSON tags 已更新 -2. 确认 OpenAPI 规范已更新 -3. 重新编译后端:`go build` -4. 清除缓存并重启 - -### 问题 3: 前端类型不匹配 - -**检查:** -1. 确认前端代码已重新生成:`npm run openapi-gen` -2. 检查生成的类型:`cat src/api/generated-orval/api.schemas.ts | grep CreateClusterRequest -A 10` -3. 重启 TypeScript 服务器(VSCode: Cmd/Ctrl + Shift + P → "TypeScript: Restart TS Server") - -### 问题 4: 401 Unauthorized - -**解决:** -1. 确保先调用登录 API 获取 token -2. 在请求头中正确设置:`Authorization: Bearer ` -3. 或使用前端的 `setAuthToken()` 函数 - -## 📚 相关文档 - -- `CAMELCASE-MIGRATION.md` - 迁移总结 -- `frontend/src/api/README.md` - API 使用文档 -- `frontend/src/api/example.ts` - 代码示例 - -## ✅ 完成标志 - -当你看到以下结果时,说明测试成功: - -1. ✅ 后端 API 返回 camelCase JSON -2. ✅ 前端可以正确解析 camelCase 响应 -3. ✅ 前端发送 camelCase 请求 -4. ✅ 后端可以正确解析 camelCase 请求 -5. ✅ TypeScript 类型提示正常工作 -6. ✅ 所有 API 调用成功 - -🎉 **恭喜!你的 camelCase API 已经完全正常工作!** - diff --git a/docs/archive/root-cleanup/TEST-RESULTS.md b/docs/archive/root-cleanup/TEST-RESULTS.md deleted file mode 100644 index a9a0ca1..0000000 --- a/docs/archive/root-cleanup/TEST-RESULTS.md +++ /dev/null @@ -1,251 +0,0 @@ -# ✅ camelCase API 测试结果 - -**测试时间**: 2025-11-10 -**测试环境**: Mock 模式(run-0) - -## 🎯 测试目标 - -验证整条链路的 camelCase 支持: -``` -Go Backend (camelCase JSON tags) - ↓ -OpenAPI Spec (camelCase properties) - ↓ -TypeScript Frontend (camelCase properties) -``` - -## ✅ 测试结果总览 - -| 测试项 | 状态 | 说明 | -|-------|------|------| -| 后端服务启动 | ✅ 通过 | Mock 模式正常运行 | -| 健康检查 | ✅ 通过 | `/health` 返回正常 | -| 注册 API | ✅ 通过 | 返回 camelCase 字段 | -| 登录 API | ✅ 通过 | `accessToken`, `refreshToken`, `userId` | -| 创建集群 API | ✅ 通过 | 请求和响应都使用 camelCase | -| 获取集群列表 | ✅ 通过 | `createdAt`, `updatedAt`, `hasCaData` | -| Registry API | ✅ 通过 | 字段使用 camelCase | - -## 📝 详细测试结果 - -### 1. 登录 API 测试 - -**请求:** -```json -{ - "username": "admin", - "password": "admin123" -} -``` - -**响应(camelCase ✅):** -```json -{ - "accessToken": "eyJhbGci...", - "refreshToken": "eyJhbGci...", - "userId": "e0b632e8-...", - "username": "admin" -} -``` - -**验证结果:** -- ✅ `accessToken` 字段存在(camelCase) -- ✅ `refreshToken` 字段存在(camelCase) -- ✅ `userId` 字段存在(camelCase) - -### 2. 创建集群 API 测试 - -**请求(使用 camelCase ✅):** -```json -{ - "name": "test-cluster-1762768984", - "host": "https://k8s.test.example.com:6443", - "description": "测试集群 - camelCase 验证", - "caData": "LS0tLS1CRUdJTi1DRVJUSUZJQ0FURS0tLS0t", - "certData": "LS0tLS1CRUdJTi1DRVJUSUZJQ0FURS0tLS0t", - "keyData": "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVkt" -} -``` - -**响应(camelCase ✅):** -```json -{ - "id": "ed37e2b2-f2b6-4b22-b8f1-affef7853471", - "name": "test-cluster-1762768984", - "host": "https://k8s.test.example.com:6443", - "description": "测试集群 - camelCase 验证", - "hasCaData": true, - "hasCertData": true, - "hasKeyData": true, - "hasToken": false, - "caData": "••••••••", - "certData": "••••••••", - "keyData": "••••••••", - "createdAt": "2025-11-10T10:03:04Z", - "updatedAt": "2025-11-10T10:03:04Z" -} -``` - -**验证结果:** -- ✅ 请求字段 `caData`, `certData`, `keyData` (camelCase) -- ✅ 响应字段 `hasCaData`, `hasCertData`, `hasKeyData` (camelCase) -- ✅ 响应字段 `createdAt`, `updatedAt` (camelCase) - -### 3. 获取集群列表 API 测试 - -**响应(部分,camelCase ✅):** -```json -[ - { - "id": "ed37e2b2-...", - "name": "test-cluster-1762768984", - "host": "https://k8s.test.example.com:6443", - "hasCaData": true, - "hasCertData": true, - "hasKeyData": true, - "hasToken": false, - "caData": "••••••••", - "createdAt": "2025-11-10T10:03:04Z", - "updatedAt": "2025-11-10T10:03:04Z" - } -] -``` - -**验证结果:** -- ✅ 所有字段使用 camelCase -- ✅ 数组元素正常 - -## 🎨 字段对照表 - -### 后端 Go → JSON → 前端 TS - -| Go Struct Field | JSON Tag (camelCase) | TypeScript Property | -|----------------|----------------------|---------------------| -| `CAData` | `caData` | `caData` | -| `CertData` | `certData` | `certData` | -| `KeyData` | `keyData` | `keyData` | -| `HasCAData` | `hasCaData` | `hasCaData` | -| `HasCertData` | `hasCertData` | `hasCertData` | -| `HasKeyData` | `hasKeyData` | `hasKeyData` | -| `HasToken` | `hasToken` | `hasToken` | -| `CreatedAt` | `createdAt` | `createdAt` | -| `UpdatedAt` | `updatedAt` | `updatedAt` | -| `AccessToken` | `accessToken` | `accessToken` | -| `RefreshToken` | `refreshToken` | `refreshToken` | -| `UserID` | `userId` | `userId` | -| `RegistryID` | `registryId` | `registryId` | -| `ClusterID` | `clusterId` | `clusterId` | -| `ValuesYAML` | `valuesYaml` | `valuesYaml` | -| `HasPassword` | `hasPassword` | `hasPassword` | - -**全部通过 ✅** - -## 🔧 技术栈验证 - -### 后端 -- ✅ Go struct JSON tags 使用 `json:"camelCase"` -- ✅ JSON 序列化输出 camelCase -- ✅ JSON 反序列化接受 camelCase - -### OpenAPI -- ✅ 属性定义使用 camelCase -- ✅ 自动转换脚本正常工作 -- ✅ 备份文件已创建(openapi.yaml.backup) - -### 前端 -- ✅ Orval 生成器正常工作 -- ✅ TypeScript 类型定义使用 camelCase -- ✅ Axios 客户端配置正确 -- ✅ IDE 类型提示正常 - -## 📊 测试覆盖 - -### API 端点测试 - -| 端点 | 方法 | 状态 | camelCase | -|-----|------|------|-----------| -| `/health` | GET | ✅ | N/A | -| `/api/v1/auth/register` | POST | ✅ | ✅ | -| `/api/v1/auth/login` | POST | ✅ | ✅ | -| `/api/v1/clusters` | POST | ✅ | ✅ | -| `/api/v1/clusters` | GET | ✅ | ✅ | -| `/api/v1/registries` | POST | ✅ | ✅ | - -### 关键字段验证 - -**Auth 相关:** -- ✅ `accessToken` -- ✅ `refreshToken` -- ✅ `userId` - -**Cluster 相关:** -- ✅ `caData` -- ✅ `certData` -- ✅ `keyData` -- ✅ `hasCaData` -- ✅ `hasCertData` -- ✅ `hasKeyData` -- ✅ `hasToken` -- ✅ `createdAt` -- ✅ `updatedAt` - -**Registry 相关:** -- ✅ `hasPassword` -- ✅ `createdAt` -- ✅ `updatedAt` - -## 🎉 结论 - -**✅ 所有测试通过!camelCase 链路完全正常工作!** - -### 成就解锁 - -1. ✅ **后端 JSON tags** 全部转换为 camelCase -2. ✅ **OpenAPI 规范** 属性全部使用 camelCase -3. ✅ **前端类型定义** 自动生成为 camelCase -4. ✅ **API 通信** 请求和响应都使用 camelCase -5. ✅ **符合标准** 遵循 REST API 和 Google JSON Style Guide - -### 优势 - -- 🎯 **类型安全**:完整的 TypeScript 支持 -- 🚀 **开发效率**:IDE 自动补全 -- 📚 **代码可读性**:前后端命名统一 -- 🔧 **易于维护**:OpenAPI 驱动 -- ⚡ **无性能损耗**:无需运行时转换 - -## 📝 下一步 - -### 前端测试 - -访问测试页面验证前端集成: -``` -http://localhost:5173/api-test -``` - -### 集成到现有代码 - -可以开始将新的 camelCase API 集成到现有功能中: - -1. 更新认证相关代码 -2. 更新集群管理页面 -3. 更新 Registry 管理页面 -4. 更新实例管理页面 - -### 文档 - -- ✅ `CAMELCASE-MIGRATION.md` - 迁移文档 -- ✅ `TEST-GUIDE.md` - 测试指南 -- ✅ `frontend/src/api/README.md` - API 使用文档 -- ✅ `frontend/src/api/example.ts` - 代码示例 -- ✅ `TEST-RESULTS.md` - 本文档 - ---- - -**测试执行者**: AI Assistant -**测试日期**: 2025-11-10 -**测试工具**: cURL + 自定义测试脚本 -**测试环境**: Mock 模式(run-0) - -🎊 **恭喜!你的项目现在完全使用 camelCase,符合现代 REST API 标准!** - diff --git a/docs/archive/root-cleanup/TESTING-COMPLETE.md b/docs/archive/root-cleanup/TESTING-COMPLETE.md deleted file mode 100644 index 529cf46..0000000 --- a/docs/archive/root-cleanup/TESTING-COMPLETE.md +++ /dev/null @@ -1,267 +0,0 @@ -# ✅ 测试完成 - 如何使用 - -## 🎉 好消息! - -所有服务已启动并测试通过: - -| 服务 | 地址 | 状态 | -|-----|------|------| -| **后端 API** | http://localhost:8080 | ✅ 运行中 | -| **前端应用** | http://localhost:5175 | ✅ 运行中 | -| **健康检查** | http://localhost:8080/health | ✅ 正常 | - -## 🧪 测试方法(3 种方式) - -### 方法 1: 独立 HTML 测试页面(最简单⭐) - -**直接在浏览器打开:** - -``` -http://localhost:5175/api-test.html -``` - -**功能:** -- ✅ 无需登录前端应用 -- ✅ 直接测试所有 API -- ✅ 实时查看 JSON 响应 -- ✅ 验证 camelCase 字段 -- ✅ 一键完整测试 - -**使用步骤:** -1. 打开上面的URL -2. 点击 "🚀 完整测试" 按钮 -3. 观察测试结果和日志 -4. 验证所有字段都是 camelCase - -### 方法 2: React 应用内测试 - -**访问主应用:** - -``` -http://localhost:5175 -``` - -**步骤:** -1. 登录应用(用户名: admin, 密码: admin123) -2. 访问 http://localhost:5175/api-test (React 组件) -3. 点击测试按钮 - -### 方法 3: cURL 命令行测试 - -**快速验证:** - -```bash -# 1. 健康检查 -curl http://localhost:8080/health - -# 2. 登录 -curl -X POST http://localhost:8080/api/v1/auth/login \ - -H "Content-Type: application/json" \ - -d '{"username":"admin","password":"admin123"}' - -# 3. 获取集群列表(需要替换 TOKEN) -curl http://localhost:8080/api/v1/clusters \ - -H "Authorization: Bearer YOUR_TOKEN" -``` - -**或运行测试脚本:** - -```bash -cd /home/mango/workspace/ocdp-go - -# API 测试 -./scripts/test-api-camelcase.sh - -# 前端集成测试 -./scripts/test-frontend-integration.sh -``` - -## 📋 测试清单 - -### 1. API 响应验证 - -打开 http://localhost:5175/api-test.html 并验证: - -**登录响应应该包含:** -- [ ] `accessToken` (不是 access_token) -- [ ] `refreshToken` (不是 refresh_token) -- [ ] `userId` (不是 user_id) - -**集群响应应该包含:** -- [ ] `createdAt` (不是 created_at) -- [ ] `updatedAt` (不是 updated_at) -- [ ] `hasCaData` (不是 has_ca_data) -- [ ] `hasCertData` (不是 has_cert_data) - -**创建集群请求应该发送:** -- [ ] `caData` (不是 ca_data) -- [ ] `certData` (不是 cert_data) -- [ ] `keyData` (不是 key_data) - -### 2. 前端功能验证 - -访问 http://localhost:5175: - -- [ ] 登录页面可以正常访问 -- [ ] 可以成功登录 -- [ ] 可以看到集群列表 -- [ ] 可以创建新集群 -- [ ] 所有操作无错误 - -## 📸 预期结果示例 - -### 登录响应(camelCase ✅) - -```json -{ - "accessToken": "eyJhbGci...", - "refreshToken": "eyJhbGci...", - "userId": "e0b632e8-...", - "username": "admin" -} -``` - -### 集群列表响应(camelCase ✅) - -```json -[ - { - "id": "cluster-123", - "name": "Production Cluster", - "host": "https://k8s.example.com:6443", - "hasCaData": true, - "hasCertData": true, - "hasKeyData": true, - "hasToken": false, - "caData": "••••••••", - "certData": "••••••••", - "keyData": "••••••••", - "createdAt": "2025-11-10T10:00:00Z", - "updatedAt": "2025-11-10T10:00:00Z" - } -] -``` - -## 🎯 快速测试(推荐) - -**一行命令测试所有功能:** - -```bash -cd /home/mango/workspace/ocdp-go && \ -./scripts/test-api-camelcase.sh && \ -./scripts/test-frontend-integration.sh && \ -echo -e "\n✅ 所有测试通过!\n访问: http://localhost:5175/api-test.html" -``` - -## 🔧 如果遇到问题 - -### 问题:页面无法打开 - -**检查服务是否运行:** - -```bash -# 检查后端 -curl http://localhost:8080/health - -# 检查前端 -curl http://localhost:5175 - -# 如果没有运行,启动服务: -# 后端: cd backend && make run-0 -# 前端: cd frontend && npm run dev -``` - -### 问题:看到 snake_case 字段 - -**这不应该发生!如果看到,请:** - -1. 检查 Go DTO JSON tags: - ```bash - grep -r "json:\"ca_data\"" backend/internal/adapter/input/http/dto/ - # 应该没有结果,如果有结果说明没有正确更新 - ``` - -2. 检查 OpenAPI 规范: - ```bash - grep "ca_data:" backend/docs/openapi.yaml - # 应该没有结果,如果有结果说明规范没有更新 - ``` - -3. 重新生成前端代码: - ```bash - cd frontend && npm run openapi-gen - ``` - -4. 重启服务 - -### 问题:CORS 错误 - -HTML 测试页面直接访问 API 可能遇到 CORS,这是正常的。请: - -1. 使用 React 应用内的测试页面 -2. 或检查后端是否允许跨域请求 - -## 📚 相关文档 - -| 文档 | 用途 | -|------|------| -| [QUICK-TEST.md](./QUICK-TEST.md) | 快速测试指南 | -| [TEST-GUIDE.md](./TEST-GUIDE.md) | 完整测试指南 | -| [TEST-RESULTS.md](./TEST-RESULTS.md) | 详细测试结果 | -| [IMPLEMENTATION-COMPLETE.md](./IMPLEMENTATION-COMPLETE.md) | 实施总结 | -| [frontend/src/api/README.md](./frontend/src/api/README.md) | API 使用文档 | - -## 🎊 成功标志 - -当你看到以下结果时,说明一切正常: - -✅ **HTML 测试页面** - 所有测试通过,显示绿色 ✓ -✅ **JSON 响应** - 所有字段使用 camelCase -✅ **前端应用** - 无错误,功能正常 -✅ **浏览器控制台** - 无错误信息 -✅ **TypeScript** - 类型提示正常工作 - ---- - -## 🚀 现在就测试! - -**最简单的方式:** - -1. 打开浏览器 -2. 访问: **http://localhost:5175/api-test.html** -3. 点击 **"🚀 完整测试"** -4. 查看结果! - -**预期看到:** -``` -🧪 开始完整测试流程... - -🔐 开始测试登录... -✅ 登录成功! - ✓ accessToken 字段存在 (camelCase) - ✓ refreshToken 字段存在 (camelCase) - ✓ userId 字段存在 (camelCase) - -📋 开始获取集群列表... -✅ 获取集群列表成功! - ✓ createdAt 字段存在 (camelCase) - ✓ hasCaData 字段存在 (camelCase) - -🚀 开始创建集群 (使用 camelCase)... -✅ 创建集群成功! - ✓ hasCaData 字段存在 (camelCase) - ✓ createdAt 字段存在 (camelCase) - -🎉 完整测试流程完成! -所有 API 调用成功,camelCase 工作正常! -``` - ---- - -**当前服务:** -- 🌐 前端: http://localhost:5175 -- 🔌 后端: http://localhost:8080 -- 🧪 HTML 测试: http://localhost:5175/api-test.html - -🎉 **测试愉快!camelCase API 完美工作!** - diff --git a/docs/deployment/docker-guide.md b/docs/deployment/docker-guide.md deleted file mode 100644 index 155a477..0000000 --- a/docs/deployment/docker-guide.md +++ /dev/null @@ -1,453 +0,0 @@ -# 🐳 Docker Compose 使用指南 - -完整的 Docker Compose 配置,一键启动所有服务! - -## 📋 服务列表 - -### 核心服务(默认启动) - -| 服务 | 端口 | 说明 | -|------|------|------| -| **postgres** | 5432 | PostgreSQL 16 数据库 | -| **redis** | 6379 | Redis 缓存(可选) | -| **backend** | 8080 | Go 后端 API 服务 | -| **frontend** | 3000 | React 前端应用 | - -### 可选服务 - -| 服务 | 端口 | 说明 | Profile | -|------|------|------|---------| -| **nginx** | 80, 443 | 反向代理 | `production` | -| **pgadmin** | 5050 | PostgreSQL 管理工具 | `tools` | -| **swagger-ui** | 8081 | API 文档查看器 | `tools` | - -## 🚀 快速开始 - -### 1. 生产环境启动 - -```bash -# 启动核心服务(PostgreSQL + Redis + Backend + Frontend) -docker compose up -d - -# 查看日志 -docker compose logs -f - -# 查看服务状态 -docker compose ps -``` - -访问: -- 🎨 前端:http://localhost:3000 -- 🔧 后端 API:http://localhost:8080 -- 📊 健康检查:http://localhost:8080/health - -### 2. 开发环境启动(支持热重载) - -```bash -# 使用开发配置启动 -docker compose -f docker-compose.yml -f docker-compose.dev.yml up - -# 或使用 Makefile 命令 -make docker-dev -``` - -开发模式特性: -- ✅ 后端支持热重载(使用 Air) -- ✅ 前端支持热重载(Vite) -- ✅ 自动挂载源代码 -- ✅ 使用 Mock 模式(无需真实数据) - -### 3. 启动可选工具 - -```bash -# 启动 pgAdmin -docker compose --profile tools up -d pgadmin - -# 启动 Swagger UI -docker compose --profile tools up -d swagger-ui - -# 启动所有工具 -docker compose --profile tools up -d - -# 启动生产环境(包含 Nginx) -docker compose --profile production up -d -``` - -访问工具: -- 📊 pgAdmin:http://localhost:5050 - - 邮箱:`admin@ocdp.local` - - 密码:`admin` -- 📖 Swagger UI:http://localhost:8081 - -## 📚 常用命令 - -### 服务管理 - -```bash -# 启动所有服务 -docker compose up -d - -# 启动特定服务 -docker compose up -d postgres redis backend - -# 停止所有服务 -docker compose down - -# 停止并删除数据卷 -docker compose down -v - -# 重启服务 -docker compose restart - -# 重启特定服务 -docker compose restart backend -``` - -### 日志查看 - -```bash -# 查看所有服务日志 -docker compose logs -f - -# 查看特定服务日志 -docker compose logs -f backend - -# 查看最近100行日志 -docker compose logs --tail=100 backend - -# 实时查看日志(带时间戳) -docker compose logs -f --timestamps backend -``` - -### 服务状态 - -```bash -# 查看服务状态 -docker compose ps - -# 查看服务详细信息 -docker compose ps -a - -# 查看服务资源使用 -docker stats -``` - -### 进入容器 - -```bash -# 进入后端容器 -docker compose exec backend sh - -# 进入 PostgreSQL 容器 -docker compose exec postgres psql -U postgres -d ocdp - -# 进入 Redis 容器 -docker compose exec redis redis-cli -``` - -### 构建和更新 - -```bash -# 重新构建镜像 -docker compose build - -# 重新构建特定服务 -docker compose build backend - -# 强制重新构建(不使用缓存) -docker compose build --no-cache - -# 拉取最新镜像 -docker compose pull -``` - -## 🔧 配置说明 - -### 环境变量 - -1. 复制环境变量示例文件: -```bash -cp env.example .env -``` - -2. 编辑 `.env` 文件,修改必要的配置: -```bash -# 修改 JWT 密钥(生产环境必须修改) -JWT_SECRET=your-very-secure-secret-key - -# 修改数据库密码(生产环境建议修改) -DB_PASSWORD=your-secure-password -``` - -### 数据持久化 - -数据卷: -- `postgres_data` - PostgreSQL 数据 -- `redis_data` - Redis 数据 -- `backend_data` - 后端应用数据 -- `pgadmin_data` - pgAdmin 配置 -- `nginx_logs` - Nginx 日志 - -查看数据卷: -```bash -docker volume ls | grep ocdp -``` - -备份数据卷: -```bash -# 备份 PostgreSQL -docker compose exec postgres pg_dump -U postgres ocdp > backup.sql - -# 或使用 Docker 卷备份 -docker run --rm -v ocdp-go_postgres_data:/data -v $(pwd):/backup alpine tar czf /backup/postgres-backup.tar.gz -C /data . -``` - -恢复数据: -```bash -# 恢复 PostgreSQL -docker compose exec -T postgres psql -U postgres ocdp < backup.sql - -# 或恢复卷 -docker run --rm -v ocdp-go_postgres_data:/data -v $(pwd):/backup alpine tar xzf /backup/postgres-backup.tar.gz -C /data -``` - -## 🏗️ 服务架构 - -``` -┌─────────────┐ -│ Client │ -└──────┬──────┘ - │ - ↓ -┌─────────────┐ ┌─────────────┐ -│ Nginx │ │ Swagger UI │ -│ (80) │ │ (8081) │ -└──────┬──────┘ └─────────────┘ - │ - ↓ -┌─────────────┐ -│ Frontend │ -│ (3000) │ -└──────┬──────┘ - │ - ↓ -┌─────────────┐ -│ Backend │ -│ (8080) │ -└──┬────┬─────┘ - │ │ - │ └─────────┐ - ↓ ↓ -┌─────────┐ ┌─────────┐ -│Postgres │ │ Redis │ -│ (5432) │ │ (6379) │ -└────┬────┘ └─────────┘ - │ - ↓ -┌─────────┐ -│pgAdmin │ -│ (5050) │ -└─────────┘ -``` - -## 🔍 健康检查 - -所有服务都配置了健康检查: - -```bash -# 检查所有服务健康状态 -docker compose ps - -# 检查特定服务 -curl http://localhost:8080/health # Backend -curl http://localhost:3000/health # Frontend -``` - -健康状态说明: -- `healthy` - 服务正常运行 -- `starting` - 服务正在启动 -- `unhealthy` - 服务异常 - -## 🐛 故障排查 - -### 问题 1: 端口冲突 - -**错误**:`port is already allocated` - -**解决方案**: -```bash -# 查看端口占用 -lsof -i :8080 - -# 修改 docker-compose.yml 中的端口映射(如需更改) -ports: - - "8081:8080" # 将主机端口改为 8081 -``` - -### 问题 2: 数据库连接失败 - -**错误**:`connection refused` - -**解决方案**: -```bash -# 1. 检查 PostgreSQL 是否启动 -docker compose ps postgres - -# 2. 查看 PostgreSQL 日志 -docker compose logs postgres - -# 3. 等待健康检查通过 -docker compose ps | grep healthy - -# 4. 手动测试连接 -docker compose exec postgres psql -U postgres -d ocdp -``` - -### 问题 3: 构建失败 - -**错误**:`build failed` - -**解决方案**: -```bash -# 1. 清理旧的镜像和容器 -docker compose down -v -docker system prune -a - -# 2. 重新构建(不使用缓存) -docker compose build --no-cache - -# 3. 检查 Dockerfile 和 .dockerignore -``` - -### 问题 4: 容器不断重启 - -**解决方案**: -```bash -# 查看容器日志 -docker compose logs --tail=100 [service-name] - -# 检查健康检查状态 -docker inspect ocdp-backend | grep -A 20 Health - -# 禁用健康检查测试 -# 在 docker-compose.yml 中注释掉 healthcheck 部分 -``` - -### 问题 5: 数据持久化失败 - -**解决方案**: -```bash -# 检查数据卷 -docker volume ls -docker volume inspect ocdp-go_postgres_data - -# 检查挂载权限 -docker compose exec postgres ls -la /var/lib/postgresql/data -``` - -## 🔒 安全建议 - -### 生产环境 - -1. **修改默认密码**: -```bash -# .env 文件中 -DB_PASSWORD=your-secure-password -JWT_SECRET=your-very-secure-secret-key -``` - -2. **使用 secrets**: -```yaml -# docker-compose.yml -secrets: - db_password: - file: ./secrets/db_password.txt -``` - -3. **限制容器权限**: -```yaml -services: - backend: - security_opt: - - no-new-privileges:true - cap_drop: - - ALL - cap_add: - - NET_BIND_SERVICE -``` - -4. **使用专用网络**: -```yaml -networks: - frontend: - driver: bridge - backend: - driver: bridge - internal: true # 不能访问外网 -``` - -## 📊 监控和日志 - -### 日志聚合 - -```bash -# 使用 ELK Stack -docker compose -f docker-compose.yml -f docker-compose.logging.yml up -d - -# 或使用 Loki -docker compose -f docker-compose.yml -f docker-compose.loki.yml up -d -``` - -### 性能监控 - -```bash -# 查看资源使用 -docker stats - -# 限制资源使用 -services: - backend: - deploy: - resources: - limits: - cpus: '1' - memory: 512M - reservations: - cpus: '0.5' - memory: 256M -``` - -## 🎯 最佳实践 - -1. **使用多阶段构建** - 减小镜像大小 -2. **配置健康检查** - 确保服务可用性 -3. **使用 .dockerignore** - 排除不必要的文件 -4. **使用非 root 用户** - 提高安全性 -5. **配置日志驱动** - 集中管理日志 -6. **使用 depends_on 和 healthcheck** - 确保启动顺序 -7. **使用 profiles** - 按需启动服务 - -## 🔗 相关命令(Makefile) - -项目 Makefile 已集成 Docker Compose 命令: - -```bash -make docker-up # 启动所有服务 -make docker-down # 停止所有服务 -make docker-logs # 查看日志 -make docker-dev # 开发模式启动 -make docker-build # 重新构建镜像 -``` - -## 📚 更多资源 - -- [Docker Compose 文档](https://docs.docker.com/compose/) -- [Docker 最佳实践](https://docs.docker.com/develop/dev-best-practices/) -- [项目 README](README.md) - ---- - -**提示**: 首次启动可能需要几分钟来下载镜像和构建容器,请耐心等待! - -有问题?查看日志:`docker compose logs -f` - diff --git a/docs/development/go-vs-typescript.md b/docs/development/go-vs-typescript.md deleted file mode 100644 index 4fc130a..0000000 --- a/docs/development/go-vs-typescript.md +++ /dev/null @@ -1,458 +0,0 @@ -# Go vs TypeScript + class-transformer 对比 - -## 命名约定和自动转换实现 - -本文档展示 Go 和 TypeScript 如何实现类似的自动类型转换机制。 - ---- - -## 📋 命名约定总结 - -### OpenAPI 规范 - -| 元素 | 命名约定 | 示例 | -|------|---------|------| -| Fixed Fields | `camelCase` | `operationId`, `requestBody` | -| Schema 名称 | `PascalCase` | `ClusterResponse`, `CreateClusterRequest` | -| Schema 属性 | `snake_case` | `has_ca_data`, `created_at`, `cluster_id` | - -### Backend Go - -| 元素 | 命名约定 | 示例 | -|------|---------|------| -| 导出变量/字段 | `PascalCase` | `HasCAData`, `CreatedAt` | -| 非导出变量 | `camelCase` | `hasCAData`, `createdAt` | -| 类型名 | `PascalCase` | `ClusterResponse`, `CreateClusterRequest` | -| JSON 标签 | `snake_case` | `json:"has_ca_data"`, `json:"created_at"` | - -### Frontend TypeScript - -| 元素 | 命名约定 | 示例 | -|------|---------|------| -| 变量 | `camelCase` | `hasCAData`, `createdAt` | -| 类型名 | `PascalCase` | `Cluster`, `CreateClusterRequest` | -| JSON | `snake_case` | `has_ca_data`, `created_at` | - ---- - -## 🔄 自动转换对比 - -### Go 的实现 - -```go -// backend/internal/adapter/input/http/dto/cluster_dto.go - -package dto - -// 类型定义:PascalCase -type ClusterResponse struct { - // 导出字段:PascalCase - ID string `json:"id"` - Name string `json:"name"` - Host string `json:"host"` - - // 字段名:PascalCase, JSON:snake_case - HasCAData bool `json:"has_ca_data"` // ← struct tag - HasCertData bool `json:"has_cert_data"` // ← struct tag - HasKeyData bool `json:"has_key_data"` // ← struct tag - - CAData string `json:"ca_data"` // ← struct tag - CertData string `json:"cert_data"` // ← struct tag - KeyData string `json:"key_data"` // ← struct tag - - CreatedAt string `json:"created_at"` // ← struct tag - UpdatedAt string `json:"updated_at"` // ← struct tag -} - -// 使用 - Go 自动转换 -func GetCluster() ClusterResponse { - cluster := ClusterResponse{ - ID: "cluster-123", - Name: "Production", - HasCAData: true, // 内部使用 PascalCase - CAData: "••••••••", - CreatedAt: "2025-11-10", - } - - // json.Marshal 自动转换为 snake_case - // {"id":"cluster-123","has_ca_data":true,"ca_data":"••••••••","created_at":"2025-11-10"} - return cluster -} -``` - -### TypeScript + class-transformer 的实现 - -```typescript -// frontend/src/api/models/cluster.model.ts - -import { Expose } from 'class-transformer'; - -// 类型定义:PascalCase -export class Cluster { - @Expose() - id!: string; - - @Expose() - name!: string; - - @Expose() - host!: string; - - // 字段名:camelCase, JSON:snake_case - @Expose({ name: 'has_ca_data' }) // ← @Expose decorator (类似 struct tag) - hasCAData?: boolean; - - @Expose({ name: 'has_cert_data' }) // ← @Expose decorator - hasCertData?: boolean; - - @Expose({ name: 'has_key_data' }) // ← @Expose decorator - hasKeyData?: boolean; - - @Expose({ name: 'ca_data' }) // ← @Expose decorator - caData?: string; - - @Expose({ name: 'cert_data' }) // ← @Expose decorator - certData?: string; - - @Expose({ name: 'key_data' }) // ← @Expose decorator - keyData?: string; - - @Expose({ name: 'created_at' }) // ← @Expose decorator - createdAt!: string; - - @Expose({ name: 'updated_at' }) // ← @Expose decorator - updatedAt!: string; -} - -// 使用 - TypeScript + class-transformer 自动转换 -import { fromJson, toJson } from '@/api/serializer'; - -function getCluster(): Cluster { - // JSON → 类实例 (snake_case → camelCase) - const apiResponse = { - id: "cluster-123", - name: "Production", - has_ca_data: true, // JSON: snake_case - ca_data: "••••••••", - created_at: "2025-11-10", - }; - - const cluster = fromJson(Cluster, apiResponse); - - // 内部使用 camelCase - console.log(cluster.hasCAData); // true - console.log(cluster.caData); // "••••••••" - console.log(cluster.createdAt); // "2025-11-10" - - return cluster; -} -``` - ---- - -## 🔍 详细对比 - -### 1. 结构定义 - -#### Go -```go -type ClusterResponse struct { - HasCAData bool `json:"has_ca_data"` - CreatedAt string `json:"created_at"` -} -``` - -#### TypeScript + class-transformer -```typescript -class Cluster { - @Expose({ name: 'has_ca_data' }) - hasCAData?: boolean; - - @Expose({ name: 'created_at' }) - createdAt!: string; -} -``` - -**对应关系**: -- Go 的 `struct tag` ↔ TypeScript 的 `@Expose` 装饰器 -- Go 的 `json:"field_name"` ↔ TypeScript 的 `{ name: 'field_name' }` - ---- - -### 2. JSON 序列化(结构体/类 → JSON) - -#### Go -```go -cluster := ClusterResponse{ - HasCAData: true, // PascalCase - CreatedAt: "2025-11-10", -} - -jsonBytes, _ := json.Marshal(cluster) -// 自动转换为: {"has_ca_data":true,"created_at":"2025-11-10"} -``` - -#### TypeScript + class-transformer -```typescript -const cluster = new Cluster(); -cluster.hasCAData = true; // camelCase -cluster.createdAt = "2025-11-10"; - -const json = toJson(cluster); -// 自动转换为: {has_ca_data: true, created_at: "2025-11-10"} -``` - -**对应关系**: -- Go 的 `json.Marshal()` ↔ TypeScript 的 `toJson()` -- 都实现了:内部字段名 → JSON snake_case - ---- - -### 3. JSON 反序列化(JSON → 结构体/类) - -#### Go -```go -jsonStr := `{"has_ca_data":true,"created_at":"2025-11-10"}` -var cluster ClusterResponse -json.Unmarshal([]byte(jsonStr), &cluster) - -// 自动映射到 PascalCase 字段 -fmt.Println(cluster.HasCAData) // true -fmt.Println(cluster.CreatedAt) // "2025-11-10" -``` - -#### TypeScript + class-transformer -```typescript -const apiResponse = { - has_ca_data: true, - created_at: "2025-11-10" -}; - -const cluster = fromJson(Cluster, apiResponse); - -// 自动映射到 camelCase 字段 -console.log(cluster.hasCAData); // true -console.log(cluster.createdAt); // "2025-11-10" -``` - -**对应关系**: -- Go 的 `json.Unmarshal()` ↔ TypeScript 的 `fromJson()` -- 都实现了:JSON snake_case → 内部字段名 - ---- - -## 📊 完整数据流转示例 - -### Scenario: 创建集群 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 1. Frontend 组件 (TypeScript camelCase) │ -├─────────────────────────────────────────────────────────────────┤ -│ const request = new CreateClusterRequest(); │ -│ request.name = "Production"; │ -│ request.caData = "LS0t..."; // camelCase │ -│ request.certData = "LS0t..."; │ -└─────────────────────────────────────────────────────────────────┘ - ↓ toJson(request) -┌─────────────────────────────────────────────────────────────────┐ -│ 2. HTTP Request Body (JSON snake_case) │ -├─────────────────────────────────────────────────────────────────┤ -│ { │ -│ "name": "Production", │ -│ "ca_data": "LS0t...", // snake_case │ -│ "cert_data": "LS0t..." │ -│ } │ -└─────────────────────────────────────────────────────────────────┘ - ↓ HTTP POST -┌─────────────────────────────────────────────────────────────────┐ -│ 3. Backend Go (PascalCase struct) │ -├─────────────────────────────────────────────────────────────────┤ -│ type CreateClusterRequest struct { │ -│ Name string `json:"name"` │ -│ CAData string `json:"ca_data"` // PascalCase │ -│ CertData string `json:"cert_data"` │ -│ } │ -│ │ -│ // json.Unmarshal 自动映射 │ -│ var req CreateClusterRequest │ -│ json.Unmarshal(body, &req) │ -│ // req.CAData = "LS0t..." // 自动转换! │ -└─────────────────────────────────────────────────────────────────┘ - ↓ 处理业务逻辑 -┌─────────────────────────────────────────────────────────────────┐ -│ 4. Backend Response (JSON snake_case) │ -├─────────────────────────────────────────────────────────────────┤ -│ { │ -│ "id": "cluster-123", │ -│ "name": "Production", │ -│ "has_ca_data": true, // snake_case │ -│ "created_at": "2025-11-10T08:00:00Z" │ -│ } │ -└─────────────────────────────────────────────────────────────────┘ - ↓ fromJson(Cluster, response) -┌─────────────────────────────────────────────────────────────────┐ -│ 5. Frontend 使用 (TypeScript camelCase) │ -├─────────────────────────────────────────────────────────────────┤ -│ const cluster: Cluster = await createCluster(request); │ -│ │ -│ // 使用 camelCase │ -│ console.log(cluster.hasCAData); // true │ -│ console.log(cluster.createdAt); // "2025-11-10T08:00:00Z" │ -│ │ -│ // 在 React 组件中 │ -│ {cluster.hasCAData && Has CA} │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 🎯 关键相似点 - -| 特性 | Go | TypeScript + class-transformer | -|------|----|---------------------------------| -| **元数据标记** | `struct tags` | `@Expose` 装饰器 | -| **内部命名** | `PascalCase` | `camelCase` | -| **JSON 命名** | `snake_case` | `snake_case` | -| **序列化** | `json.Marshal()` | `toJson()` | -| **反序列化** | `json.Unmarshal()` | `fromJson()` | -| **自动转换** | ✅ 内置支持 | ✅ 通过 class-transformer | - ---- - -## 🔧 实现代码对比 - -### Go - 完整示例 - -```go -package dto - -import "encoding/json" - -type ClusterResponse struct { - ID string `json:"id"` - Name string `json:"name"` - HasCAData bool `json:"has_ca_data"` - CreatedAt string `json:"created_at"` -} - -func main() { - // 创建实例 - cluster := ClusterResponse{ - ID: "123", - Name: "Test", - HasCAData: true, - CreatedAt: "2025-11-10", - } - - // 序列化 - jsonBytes, _ := json.Marshal(cluster) - // Output: {"id":"123","name":"Test","has_ca_data":true,"created_at":"2025-11-10"} - - // 反序列化 - var newCluster ClusterResponse - json.Unmarshal(jsonBytes, &newCluster) - // newCluster.HasCAData = true -} -``` - -### TypeScript - 完整示例 - -```typescript -import { Expose } from 'class-transformer'; -import { fromJson, toJson } from '@/api/serializer'; - -class Cluster { - @Expose() id!: string; - @Expose() name!: string; - @Expose({ name: 'has_ca_data' }) hasCAData?: boolean; - @Expose({ name: 'created_at' }) createdAt!: string; -} - -function main() { - // 创建实例 - const cluster = new Cluster(); - cluster.id = "123"; - cluster.name = "Test"; - cluster.hasCAData = true; - cluster.createdAt = "2025-11-10"; - - // 序列化 - const json = toJson(cluster); - // Output: {id:"123",name:"Test",has_ca_data:true,created_at:"2025-11-10"} - - // 反序列化 - const newCluster = fromJson(Cluster, json); - // newCluster.hasCAData === true -} -``` - ---- - -## 📝 OpenAPI 驱动开发 - -### OpenAPI 规范 → Go - -```yaml -# backend/docs/openapi.yaml - -components: - schemas: - ClusterResponse: # → type ClusterResponse struct - properties: - id: # → ID string `json:"id"` - type: string - has_ca_data: # → HasCAData bool `json:"has_ca_data"` - type: boolean - created_at: # → CreatedAt string `json:"created_at"` - type: string -``` - -### OpenAPI 规范 → TypeScript - -```yaml -# backend/docs/openapi.yaml - -components: - schemas: - ClusterResponse: # → class Cluster - properties: - id: # → @Expose() id!: string - type: string - has_ca_data: # → @Expose({ name: 'has_ca_data' }) hasCAData?: boolean - type: boolean - created_at: # → @Expose({ name: 'created_at' }) createdAt!: string - type: string -``` - ---- - -## ✨ 总结 - -### Go 的优势 -- ✅ 内置支持,无需额外库 -- ✅ 编译时生成代码 -- ✅ 零运行时开销 - -### TypeScript + class-transformer 的优势 -- ✅ 与 Go 相似的开发体验 -- ✅ 装饰器语法清晰 -- ✅ 类型安全 -- ✅ 运行时开销极小 - -### 共同点 -- ✅ 都使用元数据标记字段映射 -- ✅ 都实现了自动类型转换 -- ✅ 都保持了代码的可读性和可维护性 -- ✅ 都支持 OpenAPI 驱动开发 - ---- - -**结论**: TypeScript + class-transformer 成功复现了 Go 的 struct tags 机制,为前端开发提供了同样优雅的类型转换体验! - ---- - -**创建日期**: 2025-11-10 -**作者**: AI Assistant - - diff --git a/docs/development/naming-conventions.md b/docs/development/naming-conventions.md deleted file mode 100644 index eca3f39..0000000 --- a/docs/development/naming-conventions.md +++ /dev/null @@ -1,339 +0,0 @@ -# OCDP 命名约定对照表 - -## 快速参考 - -| 层级 | 变量/属性 | 类型名 | JSON 字段 | -|-----|----------|-------|----------| -| **Backend Go** | 导出: `PascalCase`
不导出: `camelCase` | `PascalCase` | `snake_case` | -| **OpenAPI Schema** | `snake_case` | `PascalCase` | `snake_case` | -| **Frontend Generated** | `snake_case` (引号) | `PascalCase` | `snake_case` | -| **Frontend Internal** | `camelCase` | `PascalCase` | `snake_case` | - ---- - -## 详细说明 - -### 1. Backend (Go) - -```go -// 文件: backend/internal/adapter/input/http/dto/cluster_dto.go - -type ClusterResponse struct { - ID string `json:"id"` // 导出字段: PascalCase, JSON: snake_case - Name string `json:"name"` - HasCAData bool `json:"has_ca_data"` // Go: PascalCase → JSON: snake_case - CreatedAt string `json:"created_at"` -} - -// 内部变量 -func example() { - var clusterId string // 不导出: camelCase - var clusterName string -} -``` - -**规则**: -- ✅ 导出变量/字段: `PascalCase` (首字母大写) -- ✅ 不导出变量/字段: `camelCase` (首字母小写) -- ✅ 类型名: `PascalCase` -- ✅ JSON 标签: `snake_case` - ---- - -### 2. OpenAPI 规范 (openapi.yaml) - -```yaml -# 文件: backend/docs/openapi.yaml - -components: - schemas: - ClusterResponse: # Schema 名称: PascalCase - type: object - properties: - id: # 属性: snake_case - type: string - name: - type: string - has_ca_data: # 属性: snake_case (与 Go JSON 标签一致) - type: boolean - created_at: # 属性: snake_case - type: string -``` - -**规则**: -- ✅ 固定字段 (operationId, paths, etc.): `camelCase` -- ✅ Schema 本身: `PascalCase` -- ✅ Schema 下面的属性: `snake_case` - ---- - -### 3. Frontend TypeScript - 生成的 API Client - -```typescript -// 文件: frontend/src/api/generated/models/cluster-response.ts -// 自动生成,不要手动修改 - -export interface ClusterResponse { - 'id'?: string; // 属性: snake_case (加引号) - 'name'?: string; - 'has_ca_data'?: boolean; // 保持 snake_case,与 JSON 一致 - 'created_at'?: string; -} -``` - -**规则**: -- ✅ 类型名: `PascalCase` -- ✅ 属性: `snake_case` (带引号) -- ⚠️ 不要手动修改生成的文件 - ---- - -### 4. Frontend TypeScript - 内部类型 - -```typescript -// 文件: frontend/src/core/types/index.ts -// 前端内部使用的类型定义 - -export interface Cluster { - id: string; // 内部变量: camelCase - name: string; - hasCAData?: boolean; // camelCase (前端惯例) - hasCertData?: boolean; - createdAt: string; // camelCase - updatedAt: string; -} - -// API 请求类型 (保持与后端一致) -export interface CreateClusterRequest { - name: string; - host: string; - ca_data: string; // JSON 字段: snake_case - cert_data: string; // 与后端 API 保持一致 - key_data: string; -} -``` - -**规则**: -- ✅ 内部变量: `camelCase` -- ✅ 类型名: `PascalCase` -- ✅ JSON 序列化 (API 通信): `snake_case` - ---- - -## 数据流转示例 - -### 完整的请求-响应流程 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 1. 前端组件 (camelCase) │ -├─────────────────────────────────────────────────────────────┤ -│ const cluster = { │ -│ name: "Production", │ -│ hasCAData: true, // camelCase │ -│ createdAt: "2025-11-10" │ -│ } │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 2. API Request Body (snake_case JSON) │ -├─────────────────────────────────────────────────────────────┤ -│ { │ -│ "name": "Production", │ -│ "ca_data": "LS0t...", // snake_case │ -│ "cert_data": "LS0t..." │ -│ } │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 3. Backend Go 结构体 (PascalCase) │ -├─────────────────────────────────────────────────────────────┤ -│ type CreateClusterRequest struct { │ -│ Name string `json:"name"` │ -│ CAData string `json:"ca_data"` // Go: PascalCase │ -│ CertData string `json:"cert_data"` // JSON: snake_case │ -│ } │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 4. API Response JSON (snake_case) │ -├─────────────────────────────────────────────────────────────┤ -│ { │ -│ "id": "cluster-123", │ -│ "name": "Production", │ -│ "has_ca_data": true, // snake_case │ -│ "created_at": "2025-11-10T08:00:00Z" │ -│ } │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 5. 前端接收 (可以保持 snake_case 或转换为 camelCase) │ -├─────────────────────────────────────────────────────────────┤ -│ // 选项 A: 直接使用生成的类型 (snake_case) │ -│ const cluster: ClusterResponse = response; │ -│ console.log(cluster.has_ca_data); │ -│ │ -│ // 选项 B: 转换为内部类型 (camelCase) │ -│ const cluster: Cluster = { │ -│ id: response.id, │ -│ hasCAData: response.has_ca_data, // 转换 │ -│ createdAt: response.created_at // 转换 │ -│ }; │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## 命名转换对照 - -### 常见字段名转换 - -| Go (PascalCase) | JSON (snake_case) | TS Generated | TS Internal (camelCase) | -|----------------|------------------|--------------|------------------------| -| `ID` | `id` | `'id'` | `id` | -| `Name` | `name` | `'name'` | `name` | -| `ClusterID` | `cluster_id` | `'cluster_id'` | `clusterId` | -| `RegistryID` | `registry_id` | `'registry_id'` | `registryId` | -| `HasCAData` | `has_ca_data` | `'has_ca_data'` | `hasCAData` | -| `CAData` | `ca_data` | `'ca_data'` | `caData` | -| `CertData` | `cert_data` | `'cert_data'` | `certData` | -| `KeyData` | `key_data` | `'key_data'` | `keyData` | -| `CreatedAt` | `created_at` | `'created_at'` | `createdAt` | -| `UpdatedAt` | `updated_at` | `'updated_at'` | `updatedAt` | - ---- - -## 重新生成 OpenAPI Client - -### 安装依赖 - -```bash -# 安装 Java (如果尚未安装) -sudo apt-get install openjdk-11-jdk - -# 安装 OpenAPI Generator CLI (全局) -npm install -g @openapitools/openapi-generator-cli -``` - -### 生成命令 - -```bash -# 方式 1: 使用项目根目录的 Makefile (推荐) -cd /home/mango/workspace/ocdp-go -make openapi-gen-frontend - -# 方式 2: 使用前端目录的 npm 脚本 -cd /home/mango/workspace/ocdp-go/frontend -npm run openapi-gen - -# 方式 3: 直接运行 (如果需要自定义参数) -cd /home/mango/workspace/ocdp-go -openapi-generator-cli generate \ - -i backend/docs/openapi.yaml \ - -g typescript-axios \ - -o frontend/src/api/generated \ - --additional-properties=supportsES6=true,withSeparateModelsAndApi=true,apiPackage=api,modelPackage=models -``` - -### 文件权限问题解决 - -如果遇到权限问题 (文件属于 root): - -```bash -# 修改生成文件的所有权 -sudo chown -R $USER:$USER frontend/src/api/generated - -# 然后重新生成 -make openapi-gen-frontend -``` - ---- - -## 最佳实践 - -### ✅ 推荐做法 - -1. **使用生成的类型进行 API 通信** - ```typescript - import type { ClusterResponse } from "@/api/generated"; - const clusters = await apiRequest("/v1/clusters"); - ``` - -2. **统一使用 apiRequest helper** - ```typescript - import { apiRequest } from "@/shared/utils/api-helpers"; - // 自动处理认证、错误、token 刷新 - ``` - -3. **后端修改 OpenAPI 后,重新生成前端 client** - ```bash - make openapi-gen-frontend - ``` - -### ❌ 避免的做法 - -1. **不要手动修改生成的代码** - ```typescript - // ❌ 不要修改 /frontend/src/api/generated/ 下的文件 - // 这些文件会在重新生成时被覆盖 - ``` - -2. **不要直接使用 fetch** - ```typescript - // ❌ 不推荐 - const response = await fetch("/api/v1/clusters"); - - // ✅ 推荐 - const clusters = await apiRequest("/v1/clusters"); - ``` - -3. **避免混淆命名约定** - ```typescript - // ❌ 不要在 API 请求中使用 camelCase - const request = { - name: "Test", - caData: "xxx", // 错误! 应该是 ca_data - }; - - // ✅ 正确 - const request = { - name: "Test", - ca_data: "xxx", // 与后端 JSON 标签一致 - }; - ``` - ---- - -## 快速检查清单 - -### Backend (Go) - -- [ ] 导出字段使用 `PascalCase` -- [ ] JSON 标签使用 `snake_case` -- [ ] 更新 OpenAPI 规范与代码保持一致 - -### OpenAPI 规范 - -- [ ] Schema 名称使用 `PascalCase` -- [ ] 属性使用 `snake_case` -- [ ] 与后端 DTO 的 JSON 标签一致 - -### Frontend - -- [ ] 从 OpenAPI 重新生成 client -- [ ] 使用生成的类型进行 API 通信 -- [ ] 内部类型可以使用 `camelCase` (可选) -- [ ] 使用 `apiRequest` helper - ---- - -## 相关文档 - -- [API Client 详细说明](./frontend/API_CLIENT_CONVENTIONS.md) -- [OpenAPI 规范](./backend/docs/openapi.yaml) -- [前端 API Helper](./frontend/src/shared/utils/api-helpers.ts) - ---- - -**更新日期**: 2025-11-10 - diff --git a/docs/development/specification.md b/docs/development/specification.md deleted file mode 100644 index 18cb8b2..0000000 --- a/docs/development/specification.md +++ /dev/null @@ -1,348 +0,0 @@ -# 📋 OCDP 开发规范 - -本文档定义了 OCDP 项目的开发规范和架构要求。 - -## 🎯 整体架构 (Full Stack) - -### 1. OpenAPI 驱动开发 - -采用 OpenAPI 规范来驱动前后端开发,确保 API 契约的一致性。 - -**优势**: -- API 设计优先,前后端并行开发 -- 自动生成类型安全的代码 -- 文档和代码永远同步 -- 减少沟通成本 - -**实践**: -```bash -# 1. 设计 API (编辑 backend/docs/openapi.yaml) -# 2. 验证规范 -make openapi-validate - -# 3. 生成代码 -make openapi-gen - -# 4. 实现功能 -``` - -### 2. Docker Compose 部署 - -使用 Docker Compose 进行整个应用的部署。新版的 Docker 已经将 Compose 集成到 Docker 里面了,所以使用 `docker compose`(带空格)而非旧版的 `docker-compose`(带连字符)。 - -**部署服务**: -- PostgreSQL - 数据持久化 -- Redis - 缓存和会话 -- Backend - Go 后端服务 -- Frontend - React 前端应用 -- Nginx - 反向代理(生产环境) - -## 🎨 前端规范 (Frontend) - -### 1. 纯函数渲染 - -**要求**:使用纯函数进行组件渲染,避免不必要的副作用。 - -**原则**: -- 组件应该是可预测的(相同输入→相同输出) -- 避免在渲染过程中修改外部状态 -- 使用 `useEffect` 等 Hook 处理副作用 -- 保持组件的可测试性 - -**示例**: - -```typescript -// ✅ 好的实践 - 纯函数组件 -interface Props { - name: string; - count: number; -} - -const UserCard = ({ name, count }: Props) => { - // 纯函数:只依赖 props,不修改外部状态 - return ( -
-

{name}

-

Count: {count}

-
- ); -}; - -// ✅ 副作用在 useEffect 中处理 -const UserList = () => { - const [users, setUsers] = useState([]); - - useEffect(() => { - // 副作用(API 调用)在这里处理 - fetchUsers().then(setUsers); - }, []); - - return users.map(user => ); -}; - -// ❌ 不好的实践 - 在渲染中产生副作用 -const BadComponent = () => { - // 不要在这里调用 API 或修改外部状态 - globalState.count++; // ❌ 副作用 - fetchData(); // ❌ 副作用 - - return
Bad
; -}; -``` - -### 2. 技术栈 - -- **框架**: React 18+ (使用 Hooks) -- **语言**: TypeScript 5+ -- **构建工具**: Vite -- **样式**: Tailwind CSS -- **路由**: React Router 6+ -- **状态管理**: React Context + Hooks -- **API 客户端**: 从 OpenAPI 自动生成 - -## 🔧 后端规范 (Backend) - -### 1. 六边形架构 (Hexagonal Architecture) - -后端采用六边形架构(也称为端口和适配器架构),将业务逻辑与技术实现解耦。 - -**核心目录结构**: - -``` -backend/internal/ -├── domain/ # 领域层 - 业务逻辑核心 -│ ├── entity/ # 领域实体 -│ ├── service/ # 领域服务 -│ └── repository/ # 仓库接口(端口) -├── application/ # 应用层 - 用例编排 -│ └── usecase/ # 用例实现 -└── adapter/ # 适配器层 - 技术实现 - ├── input/ # 输入适配器 - │ └── http/ # HTTP REST API - └── output/ # 输出适配器 - ├── persistence/ - │ ├── mock/ # Mock 实现 - │ └── postgres/ # PostgreSQL 实现 - ├── oci/ # OCI Registry 客户端 - └── helm/ # Helm SDK 封装 -``` - -**职责划分**: - -- **Domain 层**:纯业务逻辑,不依赖任何框架或外部库 -- **Application 层**:编排 Domain 层的服务,实现具体的用例 -- **Adapter 层**:处理所有技术细节(HTTP、数据库、第三方 API) - -### 2. Mock Adapter 实现 - -**要求**:除了实现 ports 的 adapters 外,还要做 mock。Mock 的是 adapter 的行为反应而非假数据。 - -**Mock 原则**: -- ✅ 模拟真实 adapter 的行为 -- ✅ 可以注入真实数据 -- ✅ 可以通过调用接口自行加入数据 -- ✅ 使用内存来模拟 adapter 的交互 -- ❌ 不是返回固定的假数据 - -**示例**: - -```go -// Mock Repository - 模拟真实的数据库行为 -type RegistryRepositoryMock struct { - registries map[string]*entity.Registry // 内存存储 - mu sync.RWMutex -} - -func (r *RegistryRepositoryMock) Create(ctx context.Context, registry *entity.Registry) error { - r.mu.Lock() - defer r.mu.Unlock() - - // 模拟真实行为:检查重复、生成 ID、加密等 - if _, exists := r.registries[registry.ID]; exists { - return errors.New("registry already exists") - } - - r.registries[registry.ID] = registry - return nil -} - -func (r *RegistryRepositoryMock) GetByID(ctx context.Context, id string) (*entity.Registry, error) { - r.mu.RLock() - defer r.mu.RUnlock() - - registry, exists := r.registries[id] - if !exists { - return nil, errors.New("registry not found") - } - - return registry, nil -} -``` - -### 3. Makefile 支持 - -**要求**:采用 Makefile 来支持 mock 启动以及 real 启动。 - -**命令规范**: - -```makefile -# 开发模式(Mock Adapter) -run-mock: - @echo "Starting backend with Mock adapters..." - MODE=mock go run cmd/api/main.go - -# 生产模式(Real Adapter) -run-real: - @echo "Starting backend with Real adapters..." - MODE=real go run cmd/api/main.go - -# 运行测试 -test: - go test -v ./... - -# 生成代码 -generate: - go generate ./... -``` - -**使用方式**: - -```bash -# 开发模式(无需数据库) -make run-mock - -# 生产模式(需要 PostgreSQL) -make run-real -``` - -### 4. 技术栈 - -- **语言**: Go 1.21+ -- **Web 框架**: Gin (轻量、高性能) -- **ORM**: GORM (可选,用于 PostgreSQL adapter) -- **OCI 客户端**: ORAS Go SDK v2 -- **Helm 客户端**: Helm SDK v3 -- **K8s 客户端**: client-go - -## 📐 架构原则 - -### 1. 依赖方向 - -``` -Adapter → Application → Domain - (技术) (编排) (业务) -``` - -- Domain 层不依赖任何外部库(除了标准库) -- Application 层依赖 Domain 层 -- Adapter 层依赖 Application 和 Domain 层 - -### 2. 端口和适配器 - -**端口(Port)**:接口定义,在 Domain 层 -```go -// domain/repository/registry_repository.go -type RegistryRepository interface { - Create(ctx context.Context, registry *entity.Registry) error - GetByID(ctx context.Context, id string) (*entity.Registry, error) - List(ctx context.Context) ([]*entity.Registry, error) -} -``` - -**适配器(Adapter)**:接口实现,在 Adapter 层 -```go -// adapter/output/persistence/mock/registry_repository_mock.go -type RegistryRepositoryMock struct { - // Mock 实现 -} - -// adapter/output/persistence/postgres/registry_repository_postgres.go -type RegistryRepositoryPostgres struct { - // PostgreSQL 实现 -} -``` - -### 3. 依赖注入 - -使用构造函数注入依赖: - -```go -// 创建 Mock 模式的应用 -func NewMockApp() *App { - // 创建 Mock Repository - registryRepo := mock.NewRegistryRepositoryMock() - - // 创建 Service(注入 Repository) - registryService := service.NewRegistryService(registryRepo) - - // 创建 Handler(注入 Service) - registryHandler := handler.NewRegistryHandler(registryService) - - return &App{ - RegistryHandler: registryHandler, - } -} - -// 创建 Production 模式的应用 -func NewProductionApp(db *gorm.DB) *App { - // 创建 PostgreSQL Repository - registryRepo := postgres.NewRegistryRepositoryPostgres(db) - - // ... 其他相同 -} -``` - -## 🔄 开发工作流 - -### 1. 功能开发流程 - -```bash -# 1. 设计 API -vim backend/docs/openapi.yaml - -# 2. 生成代码 -make openapi-gen - -# 3. 实现 Domain 层 -vim backend/internal/domain/service/xxx_service.go - -# 4. 实现 Mock Adapter -vim backend/internal/adapter/output/persistence/mock/xxx_mock.go - -# 5. 实现 Handler -vim backend/internal/adapter/input/http/handler/xxx_handler.go - -# 6. 启动测试 -make run-mock - -# 7. 实现前端 -vim frontend/src/features/xxx/pages/XxxPage.tsx - -# 8. 集成测试 -make dev - -# 9. 实现 Production Adapter -vim backend/internal/adapter/output/persistence/postgres/xxx_postgres.go - -# 10. 部署测试 -docker compose up -``` - -### 2. 测试策略 - -- **单元测试**:Domain 层和 Service 层(使用 Mock Repository) -- **集成测试**:使用 Mock Adapter 测试完整流程 -- **E2E 测试**:使用真实 Adapter 测试生产环境 - -## 📚 参考文档 - -- [后端六边形架构详解](../../backend/HEXAGONAL_ARCHITECTURE.md) -- [OpenAPI 规范](../../backend/docs/openapi.yaml) -- [Docker 部署指南](../deployment/docker-guide.md) -- [安全实现方案](../security/security-implementation.md) - ---- - -**版本**: 1.0 -**最后更新**: 2025-11-07 - diff --git a/docs/features/ARTIFACT_MEDIATYPE_FILTER.md b/docs/features/ARTIFACT_MEDIATYPE_FILTER.md deleted file mode 100644 index 52d6c35..0000000 --- a/docs/features/ARTIFACT_MEDIATYPE_FILTER.md +++ /dev/null @@ -1,262 +0,0 @@ -# Artifact MediaType Filter 功能实现 - -## 概述 - -实现了 artifact registries 的 mediaType 过滤功能,支持后端返回不同类型的制品,前端在部署时只获取 chart 类型的制品。 - -## 功能特性 - -### 后端功能 - -1. **支持的 MediaType 过滤器**: - - `all` - 返回所有类型的 artifacts(默认) - - `image` - 只返回容器镜像(Docker/OCI) - - `chart` - 只返回 Helm Charts - - `other` - 返回未识别类型的 artifacts - -2. **模糊匹配机制**: - - 使用智能模糊匹配来识别 artifact 类型 - - 兼容不同版本的 media type 规范 - - 支持未来的新 media type 格式 - -3. **类型识别规则**: - - **Helm Chart**:包含 `helm`, `cncf.helm`, `helm.chart`, `chart+json` 等关键词 - - **Docker Image**:包含 `docker`, `vnd.docker`, `docker.distribution` 等关键词 - - **OCI Image**:包含 `vnd.oci`, `oci.image`, `opencontainers`, `container.image` 等关键词 - -### 前端功能 - -1. **API 接口更新**: - - `getTags()` 函数现在接受可选的 `mediaType` 参数 - - 默认获取所有类型(`"all"`)以支持客户端过滤切换 - -2. **部署场景**: - - 前端在部署场景中只会使用 chart 类型 - - LaunchModal 组件专门用于部署 Helm Charts - -3. **性能优化**: - - 客户端缓存所有类型的 tags - - 支持无需重新请求即可切换过滤器 - - 减少不必要的网络请求 - -## 技术实现 - -### 后端实现 - -#### 1. OpenAPI 规范更新 - -```yaml -# backend/docs/openapi.yaml -parameters: - - name: media_type - in: query - description: Filter artifacts by media type (all, image, chart, other) - schema: - type: string - enum: [all, image, chart, other] - default: all -``` - -#### 2. REST Handler 更新 - -```go -// backend/internal/adapter/input/http/rest/artifact_handler.go -func (h *ArtifactHandler) ListArtifacts(w http.ResponseWriter, r *http.Request) { - // 获取 mediaType 过滤参数 - mediaTypeFilter := r.URL.Query().Get("media_type") - if mediaTypeFilter == "" { - mediaTypeFilter = "all" - } - - artifacts, err := h.artifactService.ListArtifacts( - r.Context(), - registryID, - repositoryName, - mediaTypeFilter, - ) - // ... -} -``` - -#### 3. Domain Service 更新 - -```go -// backend/internal/domain/service/artifact_service.go -func (s *ArtifactService) ListArtifacts( - ctx context.Context, - registryID, - repository, - mediaTypeFilter string, -) ([]*entity.Artifact, error) { - // ... - return s.ociClient.ListArtifacts(ctx, registry, repository, mediaTypeFilter) -} -``` - -#### 4. OCI Client 实现 - -```go -// backend/internal/adapter/output/oci/real/oci_client.go -func (c *OCIClient) shouldIncludeArtifact(artifact *entity.Artifact, filter string) bool { - if filter == "" || filter == "all" { - return true - } - - switch filter { - case "chart": - return artifact.Type == entity.ArtifactTypeHelm - case "image": - return artifact.Type == entity.ArtifactTypeDocker || - artifact.Type == entity.ArtifactTypeOCI - case "other": - return artifact.Type == entity.ArtifactTypeUnknown - default: - return true - } -} -``` - -### 前端实现 - -#### 1. API 调用更新 - -```typescript -// frontend/src/core/api/artifact.api.ts -export async function getTags( - registryId: string, - repository: string, - mediaType: string = "all" -): Promise { - // REST mode - const url = `/v1/registries/${registryId}/repositories/${encodeURIComponent(repository)}/artifacts?media_type=${mediaType}`; - const response = await apiRequest(url); - return response; -} -``` - -#### 2. 组件更新 - -所有调用 `getTags` 的组件都已更新为默认获取 `"all"` 类型: -- `RegistryTreeExplorer.tsx` - 主浏览器组件 -- `RepositoryItem.tsx` - 仓库项组件 -- `RegistryCard.tsx` - 注册表卡片组件 - -## API 使用示例 - -### 获取所有类型的 artifacts(默认) - -```bash -GET /api/v1/registries/harbor-prod/repositories/charts%2Fvllm-serve/artifacts -# 或 -GET /api/v1/registries/harbor-prod/repositories/charts%2Fvllm-serve/artifacts?media_type=all -``` - -### 只获取 Helm Charts - -```bash -GET /api/v1/registries/harbor-prod/repositories/charts%2Fvllm-serve/artifacts?media_type=chart -``` - -### 只获取容器镜像 - -```bash -GET /api/v1/registries/harbor-prod/repositories/library%2Falpine/artifacts?media_type=image -``` - -### 只获取未识别类型 - -```bash -GET /api/v1/registries/harbor-prod/repositories/misc%2Fdata/artifacts?media_type=other -``` - -## 测试场景 - -### 场景 1:混合仓库过滤 - -**仓库内容**: -- `charts/vllm-serve:0.1.0` (Helm Chart) -- `charts/vllm-serve:0.2.0` (Helm Chart) -- `library/alpine:3.18` (Docker Image) -- `library/alpine:latest` (Docker Image) - -**测试**: -```bash -# 获取所有 -curl "http://localhost:8080/api/v1/registries/harbor-prod/repositories/charts%2Fvllm-serve/artifacts?media_type=all" -# 返回:2 个 charts - -# 只获取 charts -curl "http://localhost:8080/api/v1/registries/harbor-prod/repositories/charts%2Fvllm-serve/artifacts?media_type=chart" -# 返回:2 个 charts - -# 只获取 images -curl "http://localhost:8080/api/v1/registries/harbor-prod/repositories/library%2Falpine/artifacts?media_type=image" -# 返回:2 个 images -``` - -### 场景 2:前端部署流程 - -1. 用户浏览 artifact registries -2. 前端默认显示 chart 过滤器(但获取所有类型以支持切换) -3. 用户点击 "Launch" 按钮部署 Helm Chart -4. LaunchModal 只处理 chart 类型的 artifacts - -## 兼容性 - -### 向后兼容 - -- 不带 `media_type` 参数的请求默认返回所有类型 -- 前端组件可以正常工作,无需升级 -- 现有的 API 客户端不受影响 - -### 未来扩展 - -该实现支持未来添加新的 artifact 类型: -1. 在后端 `entity.Artifact.SetType()` 中添加新的识别规则 -2. 在 `shouldIncludeArtifact()` 中添加新的过滤条件 -3. 前端自动支持新类型(通过 `type` 字段) - -## 性能考虑 - -1. **后端过滤**: - - 在 OCI client 层面进行过滤,减少内存使用 - - 避免传输不需要的数据 - -2. **前端缓存**: - - 获取所有类型并缓存 - - 客户端快速切换过滤器 - - 减少重复的网络请求 - -3. **并发控制**: - - 批量加载时使用并发限制(3个并发) - - 避免过多同时请求 - -## 相关文件 - -### 后端文件 -- `backend/docs/openapi.yaml` - API 规范定义 -- `backend/internal/adapter/input/http/rest/artifact_handler.go` - HTTP handler -- `backend/internal/domain/service/artifact_service.go` - 领域服务 -- `backend/internal/domain/repository/oci_client.go` - OCI 客户端接口 -- `backend/internal/adapter/output/oci/real/oci_client.go` - 真实 OCI 客户端实现 -- `backend/internal/adapter/output/oci/mock/oci_client_mock.go` - Mock OCI 客户端实现 -- `backend/internal/domain/entity/artifact.go` - Artifact 实体(类型识别逻辑) - -### 前端文件 -- `frontend/src/core/api/artifact.api.ts` - API 客户端 -- `frontend/src/features/artifact/registries/components/RegistryTreeExplorer.tsx` - 主浏览器 -- `frontend/src/features/artifact/registries/components/RepositoryItem.tsx` - 仓库项 -- `frontend/src/features/artifact/registries/components/RegistryCard.tsx` - 注册表卡片 -- `frontend/src/features/artifact/registries/components/LaunchModal.tsx` - 部署模态框 -- `frontend/src/features/artifact/registries/utils/artifactType.ts` - 类型工具 - -## 总结 - -✅ 后端支持返回所有 artifact 制品 -✅ 支持通过 mediaType 参数过滤(image、chart、other、all) -✅ 采用模糊匹配机制,兼容未来版本 -✅ 前端在部署时专注于 chart 类型 -✅ 性能优化:客户端缓存 + 服务端过滤 -✅ 完全向后兼容 -✅ 易于扩展新的 artifact 类型 - diff --git a/docs/features/TESTING_MEDIATYPE_FILTER.md b/docs/features/TESTING_MEDIATYPE_FILTER.md deleted file mode 100644 index 38c4dae..0000000 --- a/docs/features/TESTING_MEDIATYPE_FILTER.md +++ /dev/null @@ -1,225 +0,0 @@ -# MediaType Filter 功能测试指南 - -## 快速测试 - -### 1. 启动服务 - -```bash -# 启动后端(Mock 模式) -cd backend -make run-mock - -# 启动前端(新终端) -cd frontend -npm run dev -``` - -### 2. API 测试 - -#### 测试默认行为(返回所有类型) - -```bash -curl "http://localhost:8080/api/v1/registries/harbor-bwgdi-prod/repositories/charts%2Fvllm-serve/artifacts" -``` - -**预期结果**:返回所有 artifacts(Mock 数据中有 2 个 Helm Charts) - -#### 测试 Chart 过滤 - -```bash -curl "http://localhost:8080/api/v1/registries/harbor-bwgdi-prod/repositories/charts%2Fvllm-serve/artifacts?media_type=chart" -``` - -**预期结果**:只返回 Helm Charts - -#### 测试 Image 过滤 - -```bash -curl "http://localhost:8080/api/v1/registries/harbor-bwgdi-prod/repositories/library%2Falpine/artifacts?media_type=image" -``` - -**预期结果**:只返回 Docker/OCI 镜像(Mock 数据中有 2 个 Alpine 镜像) - -#### 测试混合过滤 - -```bash -# 测试获取所有类型 -curl "http://localhost:8080/api/v1/registries/harbor-bwgdi-prod/repositories/charts%2Fvllm-serve/artifacts?media_type=all" - -# 测试只获取 image(应该返回空,因为这个 repo 只有 charts) -curl "http://localhost:8080/api/v1/registries/harbor-bwgdi-prod/repositories/charts%2Fvllm-serve/artifacts?media_type=image" - -# 测试只获取 chart(应该返回数据) -curl "http://localhost:8080/api/v1/registries/harbor-bwgdi-prod/repositories/charts%2Fvllm-serve/artifacts?media_type=chart" -``` - -### 3. 前端功能测试 - -#### 浏览器测试流程 - -1. **打开浏览器**:http://localhost:5173 - -2. **登录系统**: - - 用户名:`admin` - - 密码:`admin123` - -3. **导航到 Artifact Registries**: - - 点击左侧菜单的 "Artifact Registries" - -4. **测试类型过滤**: - - 观察默认过滤器设置为 "Chart" - - 点击不同的过滤器按钮:Chart, Image, Other, All - - 验证列表根据类型正确过滤 - -5. **测试部署功能**: - - 展开一个 registry - - 选择一个 chart repository(如 `charts/vllm-serve`) - - 点击某个 tag 旁边的 "Launch" 按钮 - - 验证只有 chart 类型显示 Launch 按钮 - -#### 浏览器控制台验证 - -打开浏览器开发者工具(F12),在 Console 中查看: - -``` -[OCI API] Fetching tags for harbor-bwgdi-prod/charts/vllm-serve (mediaType: all) -[RESTful OCI] Got 2 tags for charts/vllm-serve (mediaType: all) -``` - -### 4. Mock 数据说明 - -Mock 实现包含以下测试数据: - -#### Helm Charts -- `charts/vllm-serve:0.1.0` - ArtifactTypeHelm -- `charts/vllm-serve:0.2.0` - ArtifactTypeHelm -- `charts/nginx:1.0.0` - ArtifactTypeHelm -- `charts/redis:6.2.0` - ArtifactTypeHelm - -#### Docker Images -- `library/alpine:3.18` - ArtifactTypeDocker -- `library/alpine:latest` - ArtifactTypeDocker - -### 5. 验证检查点 - -#### ✅ 后端验证 - -- [ ] API 接受 `media_type` 查询参数 -- [ ] 默认行为(无参数)返回所有类型 -- [ ] `media_type=chart` 只返回 Helm Charts -- [ ] `media_type=image` 只返回容器镜像 -- [ ] `media_type=other` 只返回未知类型 -- [ ] `media_type=all` 返回所有类型 - -#### ✅ 前端验证 - -- [ ] 前端调用 API 时传递 `media_type` 参数 -- [ ] 默认获取所有类型(支持客户端过滤切换) -- [ ] 浏览器界面显示正确的过滤结果 -- [ ] Launch Modal 只处理 chart 类型 -- [ ] 无需重新请求即可切换过滤器 - -### 6. 性能测试 - -#### 测试缓存机制 - -1. 打开浏览器开发者工具的 Network 标签 -2. 展开一个 registry,观察网络请求 -3. 切换过滤器(Chart → Image → All) -4. **验证**:切换过滤器时不应该有新的网络请求(使用缓存) - -#### 测试并发加载 - -1. 清除缓存并刷新页面 -2. 展开一个 registry -3. **观察**:repositories 的 tags 按批次加载(每批 3 个并发) -4. **验证**:Console 显示加载进度 - -### 7. 预期行为 - -#### 场景 1:查看 Helm Chart Repository - -``` -Request: GET /api/v1/registries/harbor-prod/repositories/charts%2Fvllm-serve/artifacts?media_type=chart -Response: [ - { - "repositoryName": "charts/vllm-serve", - "tag": "0.1.0", - "type": "helm", - "size": 12345678 - }, - { - "repositoryName": "charts/vllm-serve", - "tag": "0.2.0", - "type": "helm", - "size": 13456789 - } -] -``` - -#### 场景 2:查看 Docker Image Repository - -``` -Request: GET /api/v1/registries/harbor-prod/repositories/library%2Falpine/artifacts?media_type=image -Response: [ - { - "repositoryName": "library/alpine", - "tag": "3.18", - "type": "docker", - "size": 2345678 - }, - { - "repositoryName": "library/alpine", - "tag": "latest", - "type": "docker", - "size": 2456789 - } -] -``` - -#### 场景 3:错误的过滤器(应该过滤掉所有) - -``` -Request: GET /api/v1/registries/harbor-prod/repositories/charts%2Fvllm-serve/artifacts?media_type=image -Response: [] # 空数组,因为这个 repo 只有 charts -``` - -## 故障排查 - -### 问题 1:后端返回所有类型而不是过滤后的 - -**检查**: -```bash -# 验证参数是否正确传递 -curl -v "http://localhost:8080/api/v1/registries/harbor-prod/repositories/charts%2Fvllm-serve/artifacts?media_type=chart" | jq -``` - -**解决方案**:检查 `artifact_handler.go` 中的参数解析 - -### 问题 2:前端过滤器不工作 - -**检查**: -- 打开浏览器控制台查看错误 -- 检查网络请求是否包含 `media_type` 参数 -- 验证 `getTags()` 函数调用是否正确 - -### 问题 3:类型识别错误 - -**检查**: -- 查看 `entity/artifact.go` 中的 `SetType()` 方法 -- 验证 mediaType 值是否匹配识别规则 -- 添加日志输出 artifact 的 MediaType 值 - -## 完成标志 - -当以下所有测试通过时,功能实现完成: - -- ✅ 后端 API 接受并正确处理 `media_type` 参数 -- ✅ 不同类型的 artifacts 被正确过滤 -- ✅ 前端可以成功调用带参数的 API -- ✅ 前端界面正确显示过滤结果 -- ✅ 部署功能只处理 chart 类型 -- ✅ 缓存机制正常工作 -- ✅ 无 linter 错误 -- ✅ 向后兼容(不带参数的请求正常工作) - diff --git a/docs/security/security-implementation.md b/docs/security/security-implementation.md deleted file mode 100644 index 5cb81e1..0000000 --- a/docs/security/security-implementation.md +++ /dev/null @@ -1,356 +0,0 @@ -# 🔒 安全方案文档 - -## 概述 - -本项目实现了一套完整的敏感信息保护方案,确保密码、证书、Token 等敏感数据在存储和传输过程中的安全性。 - -## 🎯 解决的安全问题 - -### 1. 硬编码敏感信息 -**问题**:代码中硬编码了 Harbor 密码、K8s 证书等敏感信息 -**解决方案**:全部移至环境变量,通过 `.env` 文件配置 - -### 2. 明文存储密码和证书 -**问题**:数据库中以明文存储敏感数据 -**解决方案**:使用 AES-256-GCM 加密存储 - -### 3. API 响应泄露敏感信息 -**问题**:API 返回完整的密码和证书数据 -**解决方案**:自动脱敏,仅返回掩码(`••••••••`) - -### 4. 前端显示敏感信息 -**问题**:前端表单可能显示原始密码 -**解决方案**:显示掩码,修改时仅支持覆盖 - -## 🏗️ 架构设计 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 前端 (Frontend) │ -│ • 显示脱敏数据(••••••••) │ -│ • 修改时仅支持覆盖,不能查看原值 │ -└─────────────────────────────────────────────────────────────┘ - │ HTTPS - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ REST API (Handler 层) │ -│ • 接收请求中的明文敏感数据 │ -│ • 响应时自动脱敏(调用 ToRegistryResponse/ToClusterResponse) │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Service 层(业务逻辑) │ -│ • 处理业务逻辑 │ -│ • 不关心加密/解密细节 │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Repository 层(数据持久化) │ -│ • Create/Update: 自动加密敏感数据后存储 │ -│ • GetByID/List: 自动解密敏感数据后返回 │ -│ • 使用 AES-256-GCM 加密算法 │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Database(加密存储) │ -│ • 密码:加密存储(Base64 编码的密文) │ -│ • 证书:加密存储(Base64 编码的密文) │ -│ • Token:加密存储(Base64 编码的密文) │ -└─────────────────────────────────────────────────────────────┘ -``` - -## 🔐 加密实现 - -### 加密算法 -- **算法**:AES-256-GCM (Galois/Counter Mode) -- **密钥长度**:256 bits (由用户提供的密钥通过 SHA256 派生) -- **认证加密**:提供数据机密性和完整性 -- **随机 Nonce**:每次加密使用随机 nonce,同样的明文产生不同的密文 - -### 加密流程 - -```go -// 1. 用户配置加密密钥 -encryptor := crypto.NewAESEncryptor(config.EncryptionKey) - -// 2. Repository 创建时注入加密器 -clusterRepo := mock.NewClusterRepositoryMock(encryptor) -registryRepo := mock.NewRegistryRepositoryMock(encryptor) - -// 3. 存储时自动加密 -func (r *RegistryRepositoryMock) Create(ctx context.Context, registry *entity.Registry) error { - encrypted := r.encryptRegistry(registry) // 加密敏感字段 - r.registries[registry.ID] = encrypted - return nil -} - -// 4. 读取时自动解密 -func (r *RegistryRepositoryMock) GetByID(ctx context.Context, id string) (*entity.Registry, error) { - registry := r.registries[id] - return r.decryptRegistry(registry), nil // 解密敏感字段 -} -``` - -### 加密的字段 - -#### Registry(镜像仓库) -- ✅ `Password` - Harbor/镜像仓库密码 - -#### Cluster(Kubernetes 集群) -- ✅ `CAData` - CA 证书 -- ✅ `CertData` - 客户端证书 -- ✅ `KeyData` - 客户端密钥 -- ✅ `Token` - Bearer Token - -## 🎭 脱敏显示 - -### DTO 转换 - -```go -// Registry 响应 - 自动脱敏 -func ToRegistryResponse(registry *entity.Registry) *RegistryResponse { - response := &RegistryResponse{ - Username: registry.Username, // 用户名不脱敏 - Password: crypto.MaskSensitiveData(registry.Password), // 密码脱敏 - HasPassword: registry.Password != "", - } - return response -} - -// Cluster 响应 - 自动脱敏 -func ToClusterResponse(cluster *entity.Cluster) *ClusterResponse { - response := &ClusterResponse{ - CAData: crypto.MaskSensitiveData(cluster.CAData), // •••••••• - CertData: crypto.MaskSensitiveData(cluster.CertData), // •••••••• - KeyData: crypto.MaskSensitiveData(cluster.KeyData), // •••••••• - Token: crypto.MaskSensitiveData(cluster.Token), // •••••••• - HasCAData: cluster.CAData != "", - HasCertData: cluster.CertData != "", - } - return response -} -``` - -### API 响应示例 - -```json -{ - "id": "registry-123", - "name": "Harbor Production", - "url": "https://harbor.example.com", - "username": "admin", - "password": "••••••••", - "has_password": true -} -``` - -## 🔧 配置指南 - -### 1. 生成加密密钥 - -```bash -# 生成强加密密钥 -openssl rand -base64 32 - -# 生成 JWT 密钥 -openssl rand -base64 32 -``` - -### 2. 创建 .env 文件 - -```bash -# 复制模板 -cp backend/.env.example backend/.env - -# 编辑配置 -nano backend/.env -``` - -### 3. 必填配置项 - -```bash -# 生产环境必须修改这些配置! -ENCRYPTION_KEY= -JWT_SECRET= -``` - -### 4. 可选:配置默认资源 - -```bash -# 默认用户 -DEFAULT_USER_USERNAME=admin -DEFAULT_USER_PASSWORD=your-secure-password -DEFAULT_USER_EMAIL=admin@example.com - -# 默认集群 -DEFAULT_CLUSTER_NAME=Production K8s -DEFAULT_CLUSTER_HOST=https://k8s.example.com:6443 -DEFAULT_CLUSTER_CA_DATA= -DEFAULT_CLUSTER_CERT_DATA= -DEFAULT_CLUSTER_KEY_DATA= - -# 默认镜像仓库 -DEFAULT_REGISTRY_NAME=Harbor Production -DEFAULT_REGISTRY_URL=https://harbor.example.com -DEFAULT_REGISTRY_USERNAME=admin -DEFAULT_REGISTRY_PASSWORD=your-harbor-password -``` - -## 🚀 使用指南 - -### 启动应用 - -```bash -cd backend - -# 开发模式(使用 Mock 存储) -make run-mock - -# 生产模式(使用实际数据库) -ADAPTER_MODE=prod DATABASE_URL="postgresql://..." make run-prod -``` - -### 验证加密 - -```bash -# 1. 创建一个 Registry -curl -X POST http://localhost:8080/api/v1/registries \ - -H "Content-Type: application/json" \ - -d '{ - "name": "Test Registry", - "url": "https://registry.example.com", - "username": "admin", - "password": "MySecretPassword123" - }' - -# 2. 获取 Registry(密码已脱敏) -curl http://localhost:8080/api/v1/registries/ - -# 响应示例: -# { -# "password": "••••••••", // 已脱敏 -# "has_password": true -# } -``` - -## 📝 前端集成 - -### 显示脱敏数据 - -```typescript -// Registry 表单 - setPassword(e.target.value)} -/> - -// 说明文字 -{registry.has_password && ( -

- 当前已设置密码(加密存储)。输入新密码以覆盖。 -

-)} -``` - -### 修改策略 - -- **查看时**:显示掩码 `••••••••`,不显示实际值 -- **修改时**: - - 输入新值 → 覆盖原值 - - 留空 → 保持原值不变 - - 无法查看原值 - -## 🔒 安全最佳实践 - -### ✅ DO(应该做) - -1. **生产环境必须使用强密钥** - ```bash - ENCRYPTION_KEY=$(openssl rand -base64 32) - JWT_SECRET=$(openssl rand -base64 32) - ``` - -2. **妥善保管 .env 文件** - - 不要提交到 Git(已加入 `.gitignore`) - - 使用密钥管理服务(如 AWS Secrets Manager, HashiCorp Vault) - - 定期轮换密钥 - -3. **使用 HTTPS** - - 生产环境必须启用 TLS/SSL - - 使用有效的 SSL 证书 - -4. **定期审计** - - 定期检查访问日志 - - 监控异常访问 - -### ❌ DON'T(不应该做) - -1. ❌ 不要在代码中硬编码敏感信息 -2. ❌ 不要将 `.env` 文件提交到版本控制 -3. ❌ 不要在日志中打印敏感数据 -4. ❌ 不要在前端缓存敏感信息 -5. ❌ 不要使用默认密钥用于生产环境 - -## 🧪 测试 - -### 加密/解密测试 - -```bash -cd backend -go test ./internal/pkg/crypto -v -``` - -### 集成测试 - -```bash -# 运行所有测试 -make test - -# 测试加密存储 -go test ./internal/adapter/output/persistence/mock -v -``` - -## 📊 性能考虑 - -- **加密开销**:AES-GCM 加密非常快,对性能影响可忽略 -- **内存使用**:每次读取时解密,不在内存中缓存明文 -- **并发安全**:Repository 使用 RWMutex 保护并发访问 - -## 🆘 故障排查 - -### 问题:解密失败 - -**原因**:`ENCRYPTION_KEY` 发生变化 -**解决方案**: -1. 确保使用相同的加密密钥 -2. 如果密钥丢失,需要重新创建所有敏感数据 - -### 问题:API 返回空密码 - -**原因**:密码未设置或解密失败 -**解决方案**: -1. 检查 `has_password` 字段 -2. 查看后端日志确认是否有解密错误 - -## 📚 相关文档 - -- [AES-GCM 加密算法](https://en.wikipedia.org/wiki/Galois/Counter_Mode) -- [Go crypto 包文档](https://pkg.go.dev/crypto) -- [OWASP 安全编码实践](https://owasp.org/www-project-secure-coding-practices-quick-reference-guide/) - -## 🤝 贡献 - -如果发现安全问题,请: -1. 不要公开披露 -2. 通过私密渠道联系维护者 -3. 提供详细的复现步骤 - -## 📄 许可证 - -本项目的安全方案遵循项目主许可证。 - diff --git a/frontend/.gitignore b/frontend/.gitignore index bb4ad46..5ef6a52 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,27 +1,41 @@ -# Logs -logs -*.log +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug npm-debug.log* yarn-debug.log* yarn-error.log* -pnpm-debug.log* -lerna-debug.log* +.pnpm-debug.log* -node_modules -dist -dist-ssr -*.local +# env files (can opt-in for committing if needed) +.env* -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? -src/api/generated -src/api/generated-new -src/api/generated-test +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md new file mode 100644 index 0000000..8bd0e39 --- /dev/null +++ b/frontend/AGENTS.md @@ -0,0 +1,5 @@ + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. + diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/frontend/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/frontend/Makefile b/frontend/Makefile deleted file mode 100644 index 1f86ef5..0000000 --- a/frontend/Makefile +++ /dev/null @@ -1,39 +0,0 @@ -# ================================================================ -# OCDP Frontend - Development Makefile -# ================================================================ -# Two commands: -# make run-0 - Mock, hot reload frontend -# make run-1 - Real API, hot reload frontend -# ================================================================ - -.PHONY: run-0 run-1 - -# ================================================================ -# Mode 0: Mock, Hot Reload Frontend -# ================================================================ -run-0: - @echo "════════════════════════════════════════════════════════════" - @echo "🚀 Mode 0: Mock, Hot Reload Frontend" - @echo "════════════════════════════════════════════════════════════" - @echo "" - @echo "✓ Frontend: localhost:5173 (hot reload)" - @echo "✓ Backend: Mock/Stub (no real API calls)" - @echo "" - @VITE_RUN_MODE=mode-0 npm run dev - -# ================================================================ -# Mode 1: Real API, Hot Reload Frontend -# ================================================================ -run-1: - @echo "════════════════════════════════════════════════════════════" - @echo "🚀 Mode 1: Real API, Hot Reload Frontend" - @echo "════════════════════════════════════════════════════════════" - @echo "" - @echo "✓ Frontend: localhost:5173 (hot reload)" - @echo "✓ Backend: Real API (http://localhost:8080)" - @echo "" - @echo "⚠️ Make sure backend is running at localhost:8080" - @echo "" - @VITE_RUN_MODE=mode-1 npm run dev - -# ================================================================ diff --git a/frontend/README.md b/frontend/README.md index eb3b538..e215bc4 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,260 +1,36 @@ -# OCDP Frontend +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). -OCDP (Open Cloud Development Platform) Frontend - **Helm Chart 管理平台** +## Getting Started -> 🎯 **专注于 Helm Charts**: 基于后端 OpenAPI 的前端应用,用于浏览、部署和管理 Helm Charts - ---- - -## 🚀 快速开始 - -### 1. 安装依赖 +First, run the development server: ```bash -npm install +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev ``` -### 2. 选择开发模式 +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. -本项目支持三种运行模式,通过 `Makefile` 命令启动: +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. -#### **Mode 0** - Mock API,热重载(推荐快速开发) +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. -```bash -make run-0 -``` +## Learn More -- ✅ 前端:`localhost:5173`(Vite 热重载) -- ✅ 后端:Mock/Stub 数据(不需要真实后端) -- ✅ 适用:快速 UI 开发和调试 +To learn more about Next.js, take a look at the following resources: -#### **Mode 1** - Real API,热重载(集成测试) +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. -```bash -make run-1 -``` +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! -- ✅ 前端:`localhost:5173`(Vite 热重载) -- ✅ 后端:真实 API(`http://localhost:8080`) -- ⚠️ 需要后端服务运行 +## Deploy on Vercel -#### **Mode 2** - Docker Compose 部署(生产测试) +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. -```bash -make run-2 -``` - -- ✅ 前端:`localhost:3000`(Nginx + Docker 容器) -- ✅ 后端:容器化部署 -- ✅ 适用:生产环境测试 - -### 3. 清理(Mode 2) - -```bash -make clean-2 -``` - ---- - -## 📋 命令列表 - -| 命令 | 说明 | -|------|------| -| `make run-0` | Mode 0 - Mock API,热重载前端 | -| `make run-1` | Mode 1 - Real API,热重载前端 | -| `make run-2` | Mode 2 - Docker Compose 部署 | -| `make clean-2` | 清理 Mode 2 容器 | - ---- - -## 🔌 Backend OpenAPI 集成 - -### 架构 - -``` -backend/docs/openapi.yaml - ↓ - OpenAPI Generator - ↓ - src/api/generated/ (自动生成的 API 客户端) - ↓ - src/api/client.ts (封装层,支持 Mode 切换) - ↓ - React Components -``` - -### API 客户端使用 - -项目使用 OpenAPI Generator 生成的 TypeScript 客户端,根据运行模式自动切换: - -- **Mode 0**: 使用 Mock 数据 -- **Mode 1**: 调用真实后端 API(`localhost:8080`) -- **Mode 2**: Docker 环境,通过 Nginx 反向代理 - -### 使用示例 - -```typescript -import { api } from '@/api/client'; - -// 获取 Registries -const registries = await api.registries.list(); - -// 获取 Helm Chart 列表(自动过滤,只返回 Charts) -const charts = await api.artifacts.list('registry-id', 'repo-name'); - -// 获取 Chart 详情 -const chart = await api.artifacts.get('registry-id', 'repo-name', 'version'); - -// 获取 Chart 的 values schema(用于部署配置) -const schema = await api.artifacts.getValuesSchema('registry-id', 'repo-name', 'version'); - -// 部署 Helm Chart -const instance = await api.instances.create({ - name: 'my-app', - namespace: 'default', - registry_id: 'registry-id', - artifact_id: 'repo-name', - version: '1.0.0', - cluster_id: 'cluster-id' -}); -``` - -### 重新生成 API Client - -当后端 OpenAPI 规范更新时: - -```bash -cd .. -make openapi-gen-frontend -``` - ---- - -## 🛠 技术栈 - -- **React 19** + **TypeScript** - UI 框架 -- **Vite 7** - 构建工具和开发服务器 -- **Tailwind CSS** - 样式框架 -- **OpenAPI Generator** - API 客户端生成 -- **Axios** - HTTP 客户端 -- **React Router v7** - 路由管理 - ---- - -## 🎯 核心功能 - -- ✅ **Helm Chart 浏览**: 从 OCI Registry 浏览 Helm Charts(自动过滤) -- ✅ **Chart 部署**: 部署 Helm Chart 到 Kubernetes 集群 -- ✅ **实例管理**: 管理已部署的 Helm Chart 实例 -- ✅ **集群管理**: 管理 Kubernetes 集群连接 -- ✅ **Registry 管理**: 管理 OCI Registry 配置 -- ❌ **不支持**: 容器镜像管理(仅 Helm Charts) - ---- - -## 📁 项目结构 - -``` -frontend/ -├── src/ -│ ├── api/ -│ │ ├── generated/ # 🤖 OpenAPI 自动生成的客户端 -│ │ └── client.ts # API 客户端封装(Mode 切换) -│ ├── features/ # 功能模块 -│ │ ├── auth/ # 认证 -│ │ ├── artifact/ # Helm Charts -│ │ ├── configuration/ # 配置管理 -│ │ └── monitoring/ # 监控 -│ ├── shared/ # 共享组件 -│ ├── app/ # 应用配置 -│ └── main.tsx # 入口文件 -├── Makefile # 开发命令(三种模式) -├── package.json # 依赖配置 -├── vite.config.ts # Vite 配置 -├── docker-compose.dev.yml # Docker Compose 配置 -├── Dockerfile # 生产构建 -└── nginx.conf # Nginx 配置 -``` - ---- - -## 💡 开发工作流 - -### 日常开发(Mode 0) - -```bash -make run-0 -# 修改代码 → 自动热重载 -``` - -### 集成测试(Mode 1) - -```bash -# 1. 确保后端运行在 localhost:8080 -cd ../backend -make run - -# 2. 启动前端(Mode 1) -cd ../frontend -make run-1 -``` - -### 生产测试(Mode 2) - -```bash -make run-2 -# 测试生产构建 -# 完成后清理: -make clean-2 -``` - ---- - -## 🐛 常见问题 - -### Mode 0 - 端口占用 - -```bash -lsof -ti:5173 | xargs kill -9 -``` - -### Mode 1 - 后端连接失败 - -确保后端服务运行: - -```bash -curl http://localhost:8080/health -``` - -### Mode 2 - 容器问题 - -```bash -# 查看日志 -docker compose -f docker-compose.dev.yml logs -f - -# 清理并重建 -make clean-2 -make run-2 -``` - -### OpenAPI 客户端更新 - -```bash -# 在项目根目录执行 -cd .. -make openapi-gen-frontend -``` - ---- - -## 📚 更多信息 - -- **后端项目**: `../backend` -- **OpenAPI 规范**: `../backend/docs/openapi.yaml` -- **查看所有命令**: `make help` - ---- - -## 📄 License - -MIT +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js deleted file mode 100644 index 8110ac4..0000000 --- a/frontend/eslint.config.js +++ /dev/null @@ -1,23 +0,0 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { defineConfig, globalIgnores } from 'eslint/config' - -export default defineConfig([ - globalIgnores(['dist', 'src/api/generated']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs['recommended-latest'], - reactRefresh.configs.vite, - ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - }, -]) diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs new file mode 100644 index 0000000..05e726d --- /dev/null +++ b/frontend/eslint.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/frontend/index.html b/frontend/index.html deleted file mode 100644 index 49132b9..0000000 --- a/frontend/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - OCDP Platform - - -
- - - diff --git a/frontend/next.config.ts b/frontend/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/frontend/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/frontend/orval.config.ts b/frontend/orval.config.ts deleted file mode 100644 index 9696eec..0000000 --- a/frontend/orval.config.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { defineConfig } from 'orval'; - -export default defineConfig({ - ocdp: { - input: { - target: '../backend/docs/openapi.yaml', - }, - output: { - target: './src/api/generated-orval/api.ts', - client: 'axios-functions', - mode: 'split', - override: { - mutator: { - path: './src/api/axios-mutator.ts', - name: 'customAxiosInstance', - }, - useDates: true, - useNamedParameters: true, - }, - tsconfig: './tsconfig.json', - clean: true, - }, - }, -}); - diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3dcc6fa..c626764 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,38 +1,30 @@ { - "name": "ocdp", - "version": "0.0.0", + "name": "frontend", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "ocdp", - "version": "0.0.0", + "name": "frontend", + "version": "0.1.0", "dependencies": { - "axios": "^1.13.2", - "class-transformer": "^0.5.1", - "lucide-react": "^0.545.0", - "react": "^19.1.1", - "react-dom": "^19.1.1", - "react-router-dom": "^7.9.4", - "reflect-metadata": "^0.2.2" + "axios": "^1.15.0", + "clsx": "^2.1.1", + "lucide-react": "^1.8.0", + "next": "16.2.3", + "react": "19.2.4", + "react-dom": "19.2.4", + "tailwind-merge": "^3.5.0" }, "devDependencies": { - "@eslint/js": "^9.36.0", - "@types/node": "^24.6.0", - "@types/react": "^19.1.16", - "@types/react-dom": "^19.1.9", - "@vitejs/plugin-react": "^5.0.4", - "autoprefixer": "^10.4.21", - "eslint": "^9.36.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.22", - "globals": "^16.4.0", - "orval": "^7.3.0", - "postcss": "^8.5.6", - "tailwindcss": "^3.4.18", - "typescript": "~5.9.3", - "typescript-eslint": "^8.45.0", - "vite": "^7.1.7" + "@tailwindcss/postcss": "^4.2.2", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.2.3", + "tailwindcss": "^4", + "typescript": "^5" } }, "node_modules/@alloc/quick-lru": { @@ -48,117 +40,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "11.7.2", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz", - "integrity": "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.15", - "js-yaml": "^4.1.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/philsturgeon" - } - }, - "node_modules/@apidevtools/openapi-schemas": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", - "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/@apidevtools/swagger-methods": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", - "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@apidevtools/swagger-parser": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.1.1.tgz", - "integrity": "sha512-u/kozRnsPO/x8QtKYJOqoGtC4kH6yg1lfYkB9Au0WhYB0FNLpyFusttQtvhlwjtG3rOwiRz4D8DnnXa8iEpIKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@apidevtools/json-schema-ref-parser": "11.7.2", - "@apidevtools/openapi-schemas": "^2.1.0", - "@apidevtools/swagger-methods": "^3.0.2", - "@jsdevtools/ono": "^7.1.3", - "ajv": "^8.17.1", - "ajv-draft-04": "^1.0.0", - "call-me-maybe": "^1.0.2" - }, - "peerDependencies": { - "openapi-types": ">=7" - } - }, - "node_modules/@apidevtools/swagger-parser/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@apidevtools/swagger-parser/node_modules/ajv-draft-04": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", - "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^8.5.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/@apidevtools/swagger-parser/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/@asyncapi/specs": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@asyncapi/specs/-/specs-6.10.0.tgz", - "integrity": "sha512-vB5oKLsdrLUORIZ5BXortZTlVyGWWMC1Nud/0LtgxQ3Yn2738HigAD6EVqScvpPsDUI/bcLVsYEXN4dtXQHVng==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.11" - } - }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -167,9 +56,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", - "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "dev": true, "license": "MIT", "engines": { @@ -177,21 +66,21 @@ } }, "node_modules/@babel/core": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", - "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.4", - "@babel/types": "^7.28.4", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -208,14 +97,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -225,13 +114,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -252,29 +141,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -283,16 +172,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -304,9 +183,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -324,27 +203,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.4" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -353,66 +232,34 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", - "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -420,465 +267,56 @@ } }, "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", - "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", - "cpu": [ - "ppc64" - ], + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", - "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", - "cpu": [ - "arm" - ], - "dev": true, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", "license": "MIT", "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", - "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", - "cpu": [ - "arm64" - ], + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", - "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", - "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", - "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", - "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", - "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", - "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", - "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", - "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", - "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", - "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", - "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", - "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", - "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", - "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", - "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", - "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", - "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", - "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", - "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", - "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", - "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", - "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", - "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -908,9 +346,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -918,37 +356,37 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", - "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.16.0" + "@eslint/core": "^0.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", - "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -959,20 +397,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -982,23 +420,10 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@eslint/js": { - "version": "9.37.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", - "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { @@ -1009,9 +434,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1019,26 +444,19 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", - "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.16.0", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@exodus/schemasafe": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", - "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", - "dev": true, - "license": "MIT" - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1091,82 +509,470 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@ibm-cloud/openapi-ruleset": { - "version": "1.33.3", - "resolved": "https://registry.npmjs.org/@ibm-cloud/openapi-ruleset/-/openapi-ruleset-1.33.3.tgz", - "integrity": "sha512-lOxglXIzUZwsw5WsbgZraxxzAYMdXYyiMNOioxYJYTd55ZuN4XEERoPdV5v1oPTdKedHEUSQu5siiSHToENFdA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@ibm-cloud/openapi-ruleset-utilities": "1.9.0", - "@stoplight/spectral-formats": "^1.8.2", - "@stoplight/spectral-functions": "^1.9.3", - "@stoplight/spectral-rulesets": "^1.21.3", - "chalk": "^4.1.2", - "inflected": "^2.1.0", - "jsonschema": "^1.5.0", - "lodash": "^4.17.21", - "loglevel": "^1.9.2", - "loglevel-plugin-prefix": "0.8.4", - "minimatch": "^6.2.0", - "validator": "^13.11.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@ibm-cloud/openapi-ruleset-utilities": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@ibm-cloud/openapi-ruleset-utilities/-/openapi-ruleset-utilities-1.9.0.tgz", - "integrity": "sha512-AoFbSarOqFBYH+1TZ9Ahkm2IWYSi5v0pBk88fpV+5b3qGJukypX8PwvCWADjuyIccKg48/F73a6hTTkBzDQ2UA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@ibm-cloud/openapi-ruleset/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" + "optional": true, + "engines": { + "node": ">=18" } }, - "node_modules/@ibm-cloud/openapi-ruleset/node_modules/minimatch": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-6.2.0.tgz", - "integrity": "sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=10" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "@emnapi/runtime": "^1.7.0" }, "engines": { - "node": ">=12" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@jridgewell/gen-mapping": { @@ -1219,50 +1025,161 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@next/env": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.3.tgz", + "integrity": "sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA==", "license": "MIT" }, - "node_modules/@jsep-plugin/assignment": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", - "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", + "node_modules/@next/eslint-plugin-next": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.3.tgz", + "integrity": "sha512-nE/b9mht28XJxjTwKs/yk7w4XTaU3t40UHVAky6cjiijdP/SEy3hGsnQMPxmXPTpC7W4/97okm6fngKnvCqVaA==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 10.16.0" - }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" + "dependencies": { + "fast-glob": "3.3.1" } }, - "node_modules/@jsep-plugin/regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", - "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", - "dev": true, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.3.tgz", + "integrity": "sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg==", + "cpu": [ + "arm64" + ], "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 10.16.0" - }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" + "node": ">= 10" } }, - "node_modules/@jsep-plugin/ternary": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@jsep-plugin/ternary/-/ternary-1.1.4.tgz", - "integrity": "sha512-ck5wiqIbqdMX6WRQztBL7ASDty9YLgJ3sSAK5ZpBzXeySvFGCzIvM6UiAI4hTZ22fEcYQVV/zhUbNscggW+Ukg==", - "dev": true, + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.3.tgz", + "integrity": "sha512-gHjL/qy6Q6CG3176FWbAKyKh9IfntKZTB3RY/YOJdDFpHGsUDXVH38U4mMNpHVGXmeYW4wj22dMp1lTfmu/bTQ==", + "cpu": [ + "x64" + ], "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 10.16.0" - }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.3.tgz", + "integrity": "sha512-U6vtblPtU/P14Y/b/n9ZY0GOxbbIhTFuaFR7F4/uMBidCi2nSdaOFhA0Go81L61Zd6527+yvuX44T4ksnf8T+Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.3.tgz", + "integrity": "sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.3.tgz", + "integrity": "sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.3.tgz", + "integrity": "sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.3.tgz", + "integrity": "sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.3.tgz", + "integrity": "sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" } }, "node_modules/@nodelib/fs.scandir": { @@ -1303,660 +1220,76 @@ "node": ">= 8" } }, - "node_modules/@orval/angular": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@orval/angular/-/angular-7.3.0.tgz", - "integrity": "sha512-SL13jfqhuHbC9XKLGVup3XXoEKvdzGjWOA+NuiRSx79CAo7XiYGsXs9KZpAQVPw2uMrOEwHeBicMW/4yJfQfdw==", + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", "dev": true, "license": "MIT", "dependencies": { - "@orval/core": "7.3.0" + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" } }, - "node_modules/@orval/axios": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@orval/axios/-/axios-7.3.0.tgz", - "integrity": "sha512-k+mxADO/uzIk4yFZkSXJghUWhGi0hLrqz/GZhx62MTlWEyrKsq/c+ZyoU5t+y4RHIn/jqCdQf4hpWWIxH+eVWQ==", + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", "dev": true, "license": "MIT", - "dependencies": { - "@orval/core": "7.3.0" - } - }, - "node_modules/@orval/core": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@orval/core/-/core-7.3.0.tgz", - "integrity": "sha512-WoFrkEkaS9pMEDd9oujP/9kvUFb++LJ7fhJ01/gJNFJuaHWTVSi9SkKpitGKzEd8aRlNMQ0LvseGMlu4tEcXpQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@apidevtools/swagger-parser": "^10.1.0", - "@ibm-cloud/openapi-ruleset": "^1.25.1", - "acorn": "^8.14.0", - "ajv": "^8.17.1", - "chalk": "^4.1.2", - "compare-versions": "^6.1.1", - "debug": "^4.3.7", - "esbuild": "^0.24.0", - "esutils": "2.0.3", - "fs-extra": "^11.2.0", - "globby": "11.1.0", - "lodash.get": "^4.4.2", - "lodash.isempty": "^4.4.0", - "lodash.omit": "^4.5.0", - "lodash.uniq": "^4.5.0", - "lodash.uniqby": "^4.7.0", - "lodash.uniqwith": "^4.5.0", - "micromatch": "^4.0.8", - "openapi3-ts": "4.4.0", - "swagger2openapi": "^7.0.8" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@orval/core/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@orval/core/node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" + "node": ">= 20" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, - "node_modules/@orval/core/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/@orval/core/node_modules/openapi3-ts": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.4.0.tgz", - "integrity": "sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==", - "dev": true, - "license": "MIT", - "dependencies": { - "yaml": "^2.5.0" - } - }, - "node_modules/@orval/fetch": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@orval/fetch/-/fetch-7.3.0.tgz", - "integrity": "sha512-g1nN3FSz9H+b4O9g9S6spM9D6Zp3z4CGEv9+FHvoMxmuWt08WYIvNVR72tJPPNDNEsuaZrpXg+OVaOdQhPgh3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.3.0" - } - }, - "node_modules/@orval/hono": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@orval/hono/-/hono-7.3.0.tgz", - "integrity": "sha512-52zIIhEHDF90RnvV3azsISO5oigxRc0yti04T/0u+mE34gUkmmxHHfIaMDzAKcHhagqDM+a0vwPhbyESxsGPhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.3.0", - "@orval/zod": "7.3.0", - "lodash.uniq": "^4.5.0" - } - }, - "node_modules/@orval/mock": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@orval/mock/-/mock-7.3.0.tgz", - "integrity": "sha512-v0EfTCr7fhbb4hUch/EbfXjY+thsxwIir8sbekWaE3SzYhsIYtyy2wdb4ZR+jMkTHVOdtk80n1mw6XlH64oLkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.3.0", - "lodash.get": "^4.4.2", - "lodash.omit": "^4.5.0", - "openapi3-ts": "^4.2.2" - } - }, - "node_modules/@orval/query": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@orval/query/-/query-7.3.0.tgz", - "integrity": "sha512-9zELueslEoSuolxJlTzXz1rLaAsI5JRbeutPH3scJvtzePo4r6paYbhe/14E5u1+gzEEBYZCi+Rnh7zyCEgTKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.3.0", - "@orval/fetch": "7.3.0", - "lodash.omitby": "^4.6.0" - } - }, - "node_modules/@orval/swr": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@orval/swr/-/swr-7.3.0.tgz", - "integrity": "sha512-genJTqn+7ssamC4KPBdshAgxjrt9gsxwrylPTBoEz/gPVeLO2i4USgQOLZq/r6H+ArCejbaVK/9sfRZEDmp2sA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.3.0", - "@orval/fetch": "7.3.0" - } - }, - "node_modules/@orval/zod": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@orval/zod/-/zod-7.3.0.tgz", - "integrity": "sha512-Aegt8RaA+vzu9XefTYKTRUt5Vm7woh1wrJFHRpMxTGx2giu3DLVkJ/dGp4O9eIVtLK4wAXlgW4QNQsmBJQT3eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@orval/core": "7.3.0", - "lodash.uniq": "^4.5.0" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.38", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz", - "integrity": "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", - "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", - "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", "cpu": [ "arm64" ], @@ -1965,12 +1298,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", - "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", "cpu": [ "arm64" ], @@ -1979,12 +1315,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", - "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", "cpu": [ "x64" ], @@ -1993,26 +1332,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", - "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", - "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", "cpu": [ "x64" ], @@ -2021,12 +1349,15 @@ "optional": true, "os": [ "freebsd" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", - "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", "cpu": [ "arm" ], @@ -2035,26 +1366,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", - "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", - "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", "cpu": [ "arm64" ], @@ -2063,12 +1383,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", - "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", "cpu": [ "arm64" ], @@ -2077,82 +1400,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", - "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", - "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", - "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", - "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", - "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", - "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", "cpu": [ "x64" ], @@ -2161,12 +1417,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", - "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", "cpu": [ "x64" ], @@ -2175,26 +1434,45 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", - "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], "cpu": [ - "arm64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "openharmony" - ] + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", - "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", "cpu": [ "arm64" ], @@ -2203,26 +1481,15 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", - "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", - "cpu": [ - "ia32" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": ">= 20" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", - "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", "cpu": [ "x64" ], @@ -2231,544 +1498,33 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", - "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", - "cpu": [ - "x64" ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz", + "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "postcss": "^8.5.6", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@stoplight/json": { - "version": "3.21.7", - "resolved": "https://registry.npmjs.org/@stoplight/json/-/json-3.21.7.tgz", - "integrity": "sha512-xcJXgKFqv/uCEgtGlPxy3tPA+4I+ZI4vAuMJ885+ThkTHFVkC+0Fm58lA9NlsyjnkpxFh4YiQWpH+KefHdbA0A==", - "dev": true, - "license": "Apache-2.0", "dependencies": { - "@stoplight/ordered-object-literal": "^1.0.3", - "@stoplight/path": "^1.3.2", - "@stoplight/types": "^13.6.0", - "jsonc-parser": "~2.2.1", - "lodash": "^4.17.21", - "safe-stable-stringify": "^1.1" - }, - "engines": { - "node": ">=8.3.0" - } - }, - "node_modules/@stoplight/json-ref-readers": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@stoplight/json-ref-readers/-/json-ref-readers-1.2.2.tgz", - "integrity": "sha512-nty0tHUq2f1IKuFYsLM4CXLZGHdMn+X/IwEUIpeSOXt0QjMUbL0Em57iJUDzz+2MkWG83smIigNZ3fauGjqgdQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-fetch": "^2.6.0", - "tslib": "^1.14.1" - }, - "engines": { - "node": ">=8.3.0" - } - }, - "node_modules/@stoplight/json-ref-readers/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@stoplight/json-ref-resolver": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@stoplight/json-ref-resolver/-/json-ref-resolver-3.1.6.tgz", - "integrity": "sha512-YNcWv3R3n3U6iQYBsFOiWSuRGE5su1tJSiX6pAPRVk7dP0L7lqCteXGzuVRQ0gMZqUl8v1P0+fAKxF6PLo9B5A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/json": "^3.21.0", - "@stoplight/path": "^1.3.2", - "@stoplight/types": "^12.3.0 || ^13.0.0", - "@types/urijs": "^1.19.19", - "dependency-graph": "~0.11.0", - "fast-memoize": "^2.5.2", - "immer": "^9.0.6", - "lodash": "^4.17.21", - "tslib": "^2.6.0", - "urijs": "^1.19.11" - }, - "engines": { - "node": ">=8.3.0" - } - }, - "node_modules/@stoplight/ordered-object-literal": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.5.tgz", - "integrity": "sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/@stoplight/path": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@stoplight/path/-/path-1.3.2.tgz", - "integrity": "sha512-lyIc6JUlUA8Ve5ELywPC8I2Sdnh1zc1zmbYgVarhXIp9YeAB0ReeqmGEOWNtlHkbP2DAA1AL65Wfn2ncjK/jtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/@stoplight/spectral-core": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-core/-/spectral-core-1.20.0.tgz", - "integrity": "sha512-5hBP81nCC1zn1hJXL/uxPNRKNcB+/pEIHgCjPRpl/w/qy9yC9ver04tw1W0l/PMiv0UeB5dYgozXVQ4j5a6QQQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/better-ajv-errors": "1.0.3", - "@stoplight/json": "~3.21.0", - "@stoplight/path": "1.3.2", - "@stoplight/spectral-parsers": "^1.0.0", - "@stoplight/spectral-ref-resolver": "^1.0.4", - "@stoplight/spectral-runtime": "^1.1.2", - "@stoplight/types": "~13.6.0", - "@types/es-aggregate-error": "^1.0.2", - "@types/json-schema": "^7.0.11", - "ajv": "^8.17.1", - "ajv-errors": "~3.0.0", - "ajv-formats": "~2.1.1", - "es-aggregate-error": "^1.0.7", - "jsonpath-plus": "^10.3.0", - "lodash": "~4.17.21", - "lodash.topath": "^4.5.2", - "minimatch": "3.1.2", - "nimma": "0.2.3", - "pony-cause": "^1.1.1", - "simple-eval": "1.0.1", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/spectral-core/node_modules/@stoplight/better-ajv-errors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", - "integrity": "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jsonpointer": "^5.0.0", - "leven": "^3.1.0" - }, - "engines": { - "node": "^12.20 || >= 14.13" - }, - "peerDependencies": { - "ajv": ">=8" - } - }, - "node_modules/@stoplight/spectral-core/node_modules/@stoplight/types": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.6.0.tgz", - "integrity": "sha512-dzyuzvUjv3m1wmhPfq82lCVYGcXG0xUYgqnWfCq3PCVR4BKFhjdkHrnJ+jIDoMKvXb05AZP/ObQF6+NpDo29IQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.4", - "utility-types": "^3.10.0" - }, - "engines": { - "node": "^12.20 || >=14.13" - } - }, - "node_modules/@stoplight/spectral-core/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@stoplight/spectral-core/node_modules/ajv-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", - "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^8.0.1" - } - }, - "node_modules/@stoplight/spectral-core/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/@stoplight/spectral-formats": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-formats/-/spectral-formats-1.8.2.tgz", - "integrity": "sha512-c06HB+rOKfe7tuxg0IdKDEA5XnjL2vrn/m/OVIIxtINtBzphZrOgtRn7epQ5bQF5SWp84Ue7UJWaGgDwVngMFw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/json": "^3.17.0", - "@stoplight/spectral-core": "^1.19.2", - "@types/json-schema": "^7.0.7", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/spectral-functions": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-functions/-/spectral-functions-1.10.1.tgz", - "integrity": "sha512-obu8ZfoHxELOapfGsCJixKZXZcffjg+lSoNuttpmUFuDzVLT3VmH8QkPXfOGOL5Pz80BR35ClNAToDkdnYIURg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/better-ajv-errors": "1.0.3", - "@stoplight/json": "^3.17.1", - "@stoplight/spectral-core": "^1.19.4", - "@stoplight/spectral-formats": "^1.8.1", - "@stoplight/spectral-runtime": "^1.1.2", - "ajv": "^8.17.1", - "ajv-draft-04": "~1.0.0", - "ajv-errors": "~3.0.0", - "ajv-formats": "~2.1.1", - "lodash": "~4.17.21", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/spectral-functions/node_modules/@stoplight/better-ajv-errors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", - "integrity": "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jsonpointer": "^5.0.0", - "leven": "^3.1.0" - }, - "engines": { - "node": "^12.20 || >= 14.13" - }, - "peerDependencies": { - "ajv": ">=8" - } - }, - "node_modules/@stoplight/spectral-functions/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@stoplight/spectral-functions/node_modules/ajv-draft-04": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", - "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^8.5.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/@stoplight/spectral-functions/node_modules/ajv-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", - "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^8.0.1" - } - }, - "node_modules/@stoplight/spectral-functions/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/@stoplight/spectral-parsers": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-parsers/-/spectral-parsers-1.0.5.tgz", - "integrity": "sha512-ANDTp2IHWGvsQDAY85/jQi9ZrF4mRrA5bciNHX+PUxPr4DwS6iv4h+FVWJMVwcEYdpyoIdyL+SRmHdJfQEPmwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/json": "~3.21.0", - "@stoplight/types": "^14.1.1", - "@stoplight/yaml": "~4.3.0", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/spectral-parsers/node_modules/@stoplight/types": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz", - "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.4", - "utility-types": "^3.10.0" - }, - "engines": { - "node": "^12.20 || >=14.13" - } - }, - "node_modules/@stoplight/spectral-ref-resolver": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-ref-resolver/-/spectral-ref-resolver-1.0.5.tgz", - "integrity": "sha512-gj3TieX5a9zMW29z3mBlAtDOCgN3GEc1VgZnCVlr5irmR4Qi5LuECuFItAq4pTn5Zu+sW5bqutsCH7D4PkpyAA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/json-ref-readers": "1.2.2", - "@stoplight/json-ref-resolver": "~3.1.6", - "@stoplight/spectral-runtime": "^1.1.2", - "dependency-graph": "0.11.0", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/spectral-rulesets": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-rulesets/-/spectral-rulesets-1.22.0.tgz", - "integrity": "sha512-l2EY2jiKKLsvnPfGy+pXC0LeGsbJzcQP5G/AojHgf+cwN//VYxW1Wvv4WKFx/CLmLxc42mJYF2juwWofjWYNIQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@asyncapi/specs": "^6.8.0", - "@stoplight/better-ajv-errors": "1.0.3", - "@stoplight/json": "^3.17.0", - "@stoplight/spectral-core": "^1.19.4", - "@stoplight/spectral-formats": "^1.8.1", - "@stoplight/spectral-functions": "^1.9.1", - "@stoplight/spectral-runtime": "^1.1.2", - "@stoplight/types": "^13.6.0", - "@types/json-schema": "^7.0.7", - "ajv": "^8.17.1", - "ajv-formats": "~2.1.1", - "json-schema-traverse": "^1.0.0", - "leven": "3.1.0", - "lodash": "~4.17.21", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/spectral-rulesets/node_modules/@stoplight/better-ajv-errors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", - "integrity": "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jsonpointer": "^5.0.0", - "leven": "^3.1.0" - }, - "engines": { - "node": "^12.20 || >= 14.13" - }, - "peerDependencies": { - "ajv": ">=8" - } - }, - "node_modules/@stoplight/spectral-rulesets/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@stoplight/spectral-rulesets/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/@stoplight/spectral-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-runtime/-/spectral-runtime-1.1.4.tgz", - "integrity": "sha512-YHbhX3dqW0do6DhiPSgSGQzr6yQLlWybhKwWx0cqxjMwxej3TqLv3BXMfIUYFKKUqIwH4Q2mV8rrMM8qD2N0rQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/json": "^3.20.1", - "@stoplight/path": "^1.3.2", - "@stoplight/types": "^13.6.0", - "abort-controller": "^3.0.0", - "lodash": "^4.17.21", - "node-fetch": "^2.7.0", - "tslib": "^2.8.1" - }, - "engines": { - "node": "^16.20 || ^18.18 || >= 20.17" - } - }, - "node_modules/@stoplight/types": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.20.0.tgz", - "integrity": "sha512-2FNTv05If7ib79VPDA/r9eUet76jewXFH2y2K5vuge6SXbRHtWBhcaRmu+6QpF4/WRNoJj5XYRSwLGXDxysBGA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.4", - "utility-types": "^3.10.0" - }, - "engines": { - "node": "^12.20 || >=14.13" - } - }, - "node_modules/@stoplight/yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@stoplight/yaml/-/yaml-4.3.0.tgz", - "integrity": "sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@stoplight/ordered-object-literal": "^1.0.5", - "@stoplight/types": "^14.1.1", - "@stoplight/yaml-ast-parser": "0.0.50", - "tslib": "^2.2.0" - }, - "engines": { - "node": ">=10.8" - } - }, - "node_modules/@stoplight/yaml-ast-parser": { - "version": "0.0.50", - "resolved": "https://registry.npmjs.org/@stoplight/yaml-ast-parser/-/yaml-ast-parser-0.0.50.tgz", - "integrity": "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@stoplight/yaml/node_modules/@stoplight/types": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz", - "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.4", - "utility-types": "^3.10.0" - }, - "engines": { - "node": "^12.20 || >=14.13" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/es-aggregate-error": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/es-aggregate-error/-/es-aggregate-error-1.0.6.tgz", - "integrity": "sha512-qJ7LIFp06h1QE1aVxbVd+zJP2wdaugYXYfd6JxsyRMrYHaxb6itXPogW2tz+ylUJ1n1b+JF1PHyYCfYHm0dvUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" + "tslib": "^2.4.0" } }, "node_modules/@types/estree": { @@ -2785,59 +1541,58 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { - "version": "24.7.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz", - "integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==", + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.14.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/react": { - "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", - "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", "dependencies": { - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.1.tgz", - "integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" } }, - "node_modules/@types/urijs": { - "version": "1.19.26", - "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.26.tgz", - "integrity": "sha512-wkXrVzX5yoqLnndOwFsieJA7oKM8cNkOKJtf/3vVGSUFkWDKZvFHpIl9Pvqb/T9UsawBBFMTTD8xu7sK5MWuvg==", - "dev": true, - "license": "MIT" - }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz", - "integrity": "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz", + "integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.46.0", - "@typescript-eslint/type-utils": "8.46.0", - "@typescript-eslint/utils": "8.46.0", - "@typescript-eslint/visitor-keys": "8.46.0", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/type-utils": "8.58.1", + "@typescript-eslint/utils": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2847,9 +1602,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.46.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "@typescript-eslint/parser": "^8.58.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -2863,17 +1618,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.0.tgz", - "integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.1.tgz", + "integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.46.0", - "@typescript-eslint/types": "8.46.0", - "@typescript-eslint/typescript-estree": "8.46.0", - "@typescript-eslint/visitor-keys": "8.46.0", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2883,20 +1638,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.0.tgz", - "integrity": "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz", + "integrity": "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.0", - "@typescript-eslint/types": "^8.46.0", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.58.1", + "@typescript-eslint/types": "^8.58.1", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2906,18 +1661,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.0.tgz", - "integrity": "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz", + "integrity": "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.0", - "@typescript-eslint/visitor-keys": "8.46.0" + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2928,9 +1683,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.0.tgz", - "integrity": "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz", + "integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==", "dev": true, "license": "MIT", "engines": { @@ -2941,21 +1696,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.0.tgz", - "integrity": "sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz", + "integrity": "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.0", - "@typescript-eslint/typescript-estree": "8.46.0", - "@typescript-eslint/utils": "8.46.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/utils": "8.58.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2965,14 +1720,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz", - "integrity": "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz", + "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==", "dev": true, "license": "MIT", "engines": { @@ -2984,22 +1739,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.0.tgz", - "integrity": "sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz", + "integrity": "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.46.0", - "@typescript-eslint/tsconfig-utils": "8.46.0", - "@typescript-eslint/types": "8.46.0", - "@typescript-eslint/visitor-keys": "8.46.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/project-service": "8.58.1", + "@typescript-eslint/tsconfig-utils": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3009,39 +1763,52 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.5" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -3052,16 +1819,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.0.tgz", - "integrity": "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz", + "integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.0", - "@typescript-eslint/types": "8.46.0", - "@typescript-eslint/typescript-estree": "8.46.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3071,19 +1838,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.0.tgz", - "integrity": "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz", + "integrity": "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.58.1", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3093,44 +1860,292 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@vitejs/plugin-react": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.4.tgz", - "integrity": "sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==", + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.4", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.38", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, + "license": "Apache-2.0", "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, "dependencies": { - "event-target-shim": "^5.0.0" + "@napi-rs/wasm-runtime": "^0.2.11" }, "engines": { - "node": ">=6.5" + "node": ">=14.0.0" } }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -3151,9 +2166,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -3167,71 +2182,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -3248,34 +2198,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -3283,6 +2205,16 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", @@ -3300,14 +2232,125 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/arraybuffer.prototype.slice": { @@ -3332,15 +2375,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/astring": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", - "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", "dev": true, - "license": "MIT", - "bin": { - "astring": "bin/astring" - } + "license": "MIT" }, "node_modules/async-function": { "version": "1.0.0", @@ -3358,44 +2398,6 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -3412,15 +2414,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axe-core": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.2.tgz", + "integrity": "sha512-byD6KPdvo72y/wj2T/4zGEvvlis+PsZsn/yPS3pEO+sFpcrqRpX/TJCxvVaEsNeMrfQbCr7w163YqoD9IYwHXw==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" } }, "node_modules/balanced-match": { @@ -3431,32 +2453,21 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.14", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.14.tgz", - "integrity": "sha512-GM9c0cWWR8Ga7//Ves/9KRgTS8nLausCkP3CGiFLrnwA2CDUluXgaQqvrULoR2Ujrd/mz/lkX87F5BHFsNr5sQ==", - "dev": true, + "version": "2.10.17", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.17.tgz", + "integrity": "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA==", "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + "baseline-browser-mapping": "dist/cli.cjs" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=6.0.0" } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -3478,9 +2489,9 @@ } }, "node_modules/browserslist": { - "version": "4.26.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", - "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, "funding": [ { @@ -3498,11 +2509,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.9", - "caniuse-lite": "^1.0.30001746", - "electron-to-chromium": "^1.5.227", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -3511,26 +2522,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" }, "engines": { @@ -3570,13 +2571,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/call-me-maybe": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", - "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", - "dev": true, - "license": "MIT" - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3587,21 +2581,10 @@ "node": ">=6" } }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/caniuse-lite": { - "version": "1.0.30001749", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001749.tgz", - "integrity": "sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==", - "dev": true, + "version": "1.0.30001787", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", "funding": [ { "type": "opencollective", @@ -3635,126 +2618,19 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/class-transformer": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", - "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=6" } }, "node_modules/color-convert": { @@ -3789,23 +2665,6 @@ "node": ">= 0.8" } }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/compare-versions": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", - "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", - "dev": true, - "license": "MIT" - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3820,15 +2679,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3844,26 +2694,20 @@ "node": ">= 8" } }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "dev": true, "license": "MIT" }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -3988,42 +2832,28 @@ "node": ">=0.4.0" } }, - "node_modules/dependency-graph": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", - "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", "engines": { "node": ">=8" } }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, - "license": "MIT" + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } }, "node_modules/dunder-proto": { "version": "1.0.1", @@ -4039,17 +2869,10 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/electron-to-chromium": { - "version": "1.5.233", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.233.tgz", - "integrity": "sha512-iUdTQSf7EFXsDdQsp8MwJz5SVk4APEFqXU/S47OtQ0YLqacSwPXdZ5vRlMX3neb07Cy2vgioNuRnWUXFwuslkg==", + "version": "1.5.334", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz", + "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==", "dev": true, "license": "ISC" }, @@ -4060,47 +2883,24 @@ "dev": true, "license": "MIT" }, - "node_modules/enquirer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", - "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-colors": "^4.1.1", - "strip-ansi": "^6.0.1" + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" }, "engines": { - "node": ">=8.6" - } - }, - "node_modules/enquirer/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/enquirer/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" + "node": ">=10.13.0" } }, "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", "dev": true, "license": "MIT", "dependencies": { @@ -4166,29 +2966,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-aggregate-error": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.14.tgz", - "integrity": "sha512-3YxX6rVb07B5TV11AV5wsL7nQCHXNwoHPsQC8S4AmBiqYhyNCJ5BRKXkXyDJvs8QzXN20NgRtxe3dEEQD9NLHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "globalthis": "^1.0.4", - "has-property-descriptors": "^1.0.2", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -4207,6 +2984,35 @@ "node": ">= 0.4" } }, + "node_modules/es-iterator-helpers": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.1.tgz", + "integrity": "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -4234,6 +3040,19 @@ "node": ">= 0.4" } }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-to-primitive": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", @@ -4252,55 +3071,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es6-promise": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", - "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", - "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.10", - "@esbuild/android-arm": "0.25.10", - "@esbuild/android-arm64": "0.25.10", - "@esbuild/android-x64": "0.25.10", - "@esbuild/darwin-arm64": "0.25.10", - "@esbuild/darwin-x64": "0.25.10", - "@esbuild/freebsd-arm64": "0.25.10", - "@esbuild/freebsd-x64": "0.25.10", - "@esbuild/linux-arm": "0.25.10", - "@esbuild/linux-arm64": "0.25.10", - "@esbuild/linux-ia32": "0.25.10", - "@esbuild/linux-loong64": "0.25.10", - "@esbuild/linux-mips64el": "0.25.10", - "@esbuild/linux-ppc64": "0.25.10", - "@esbuild/linux-riscv64": "0.25.10", - "@esbuild/linux-s390x": "0.25.10", - "@esbuild/linux-x64": "0.25.10", - "@esbuild/netbsd-arm64": "0.25.10", - "@esbuild/netbsd-x64": "0.25.10", - "@esbuild/openbsd-arm64": "0.25.10", - "@esbuild/openbsd-x64": "0.25.10", - "@esbuild/openharmony-arm64": "0.25.10", - "@esbuild/sunos-x64": "0.25.10", - "@esbuild/win32-arm64": "0.25.10", - "@esbuild/win32-ia32": "0.25.10", - "@esbuild/win32-x64": "0.25.10" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -4325,26 +3095,25 @@ } }, "node_modules/eslint": { - "version": "9.37.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", - "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.4.0", - "@eslint/core": "^0.16.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.37.0", - "@eslint/plugin-kit": "^0.4.0", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -4363,7 +3132,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -4385,29 +3154,258 @@ } } }, - "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "node_modules/eslint-config-next": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.3.tgz", + "integrity": "sha512-Dnkrylzjof/Az7iNoIQJqD18zTxQZcngir19KJaiRsMnnjpQSVoa6aEg/1Q4hQC+cW90uTlgQYadwL1CYNwFWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "16.2.3", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^7.0.0", + "globals": "16.4.0", + "typescript-eslint": "^8.46.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.23.tgz", - "integrity": "sha512-G4j+rv0NmbIR45kni5xJOrYvCtyD3/7LjpVH8MPPcudXDcNu8gv+4ATTDXTtbRR8rTCM5HxECvCSsRmxKnWDsA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": ">=8.40" - } - }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -4457,9 +3455,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -4502,47 +3500,6 @@ "node": ">=0.10.0" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/execa/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4551,9 +3508,9 @@ "license": "MIT" }, "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dev": true, "license": "MIT", "dependencies": { @@ -4561,7 +3518,7 @@ "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.8" + "micromatch": "^4.0.4" }, "engines": { "node": ">=8.6.0" @@ -4594,41 +3551,10 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-memoize": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz", - "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -4693,9 +3619,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -4735,27 +3661,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -4768,50 +3677,6 @@ "node": ">= 6" } }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fs-extra": { - "version": "11.3.2", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", - "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4872,16 +3737,6 @@ "node": ">=6.9.0" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -4919,19 +3774,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -4950,25 +3792,17 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" + "resolve-pkg-maps": "^1.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, "node_modules/glob-parent": { @@ -4984,36 +3818,10 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/globals": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", - "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", "engines": { @@ -5040,27 +3848,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -5080,13 +3867,6 @@ "dev": true, "license": "ISC" }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -5178,21 +3958,21 @@ "node": ">= 0.4" } }, - "node_modules/http2-client": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", - "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==", + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", "dev": true, "license": "MIT" }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" } }, "node_modules/ignore": { @@ -5205,17 +3985,6 @@ "node": ">= 4" } }, - "node_modules/immer": { - "version": "9.0.21", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", - "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -5243,13 +4012,6 @@ "node": ">=0.8.19" } }, - "node_modules/inflected": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/inflected/-/inflected-2.1.0.tgz", - "integrity": "sha512-hAEKNxvHf2Iq3H60oMBHkB4wl5jn3TPF3+fXek/sRwAB5gP9xWs4r7aweSF95f99HFoz69pnZTcu8f0SIHV18w==", - "dev": true, - "license": "MIT" - }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -5319,19 +4081,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-boolean-object": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", @@ -5349,6 +4098,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -5439,16 +4211,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -5583,19 +4345,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-string": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", @@ -5707,30 +4456,32 @@ "dev": true, "license": "ISC" }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "@isaacs/cliui": "^8.0.2" + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "engines": { + "node": ">= 0.4" } }, "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", "bin": { - "jiti": "bin/jiti.js" + "jiti": "lib/jiti-cli.mjs" } }, "node_modules/js-tokens": { @@ -5741,9 +4492,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -5753,16 +4504,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsep": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", - "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.16.0" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -5810,63 +4551,20 @@ "node": ">=6" } }, - "node_modules/jsonc-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.2.1.tgz", - "integrity": "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, "license": "MIT", "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonpath-plus": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz", - "integrity": "sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jsep-plugin/assignment": "^1.3.0", - "@jsep-plugin/regex": "^1.0.4", - "jsep": "^1.4.0" - }, - "bin": { - "jsonpath": "bin/jsonpath-cli.js", - "jsonpath-plus": "bin/jsonpath-cli.js" + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/jsonpointer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", - "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jsonschema": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz", - "integrity": "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" + "node": ">=4.0" } }, "node_modules/keyv": { @@ -5879,14 +4577,24 @@ "json-buffer": "3.0.1" } }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", "dev": true, "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, "engines": { - "node": ">=6" + "node": ">=0.10" } }, "node_modules/levn": { @@ -5903,25 +4611,266 @@ "node": ">= 0.8.0" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, "engines": { - "node": ">=14" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/antonk52" + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, "node_modules/locate-path": { "version": "6.0.0", @@ -5939,28 +4888,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isempty": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", - "integrity": "sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5968,70 +4895,19 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.omit": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", - "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==", - "deprecated": "This package is deprecated. Use destructuring assignment syntax instead.", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.omitby": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.omitby/-/lodash.omitby-4.6.0.tgz", - "integrity": "sha512-5OrRcIVR75M288p4nbI2WLAf3ndw2GD9fyNv3Bc15+WCxJDdZ4lYndSxGd7hnG6PVjiJTeJE2dHEGhIuKGicIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.topath": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", - "integrity": "sha512-1/W4dM+35DwvE/iEd1M9ekewOSTlpFekhw9mhAtrwjVqUr83/ilQiyAvmg4tVX7Unkcfl1KC+i9WdaT4B6aQcg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.uniqby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", - "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.uniqwith": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniqwith/-/lodash.uniqwith-4.5.0.tgz", - "integrity": "sha512-7lYL8bLopMoy4CTICbxygAUq6CdRJ36vFc80DucPueUee+d5NBRxz3FdT9Pes/HEx5mPoT9jwnsEJWz1N7uq7Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/loglevel": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", - "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.6.0" + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" }, - "funding": { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/loglevel" + "bin": { + "loose-envify": "cli.js" } }, - "node_modules/loglevel-plugin-prefix": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", - "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", - "dev": true, - "license": "MIT" - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -6043,14 +4919,24 @@ } }, "node_modules/lucide-react": { - "version": "0.545.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.545.0.tgz", - "integrity": "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.8.0.tgz", + "integrity": "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -6060,13 +4946,6 @@ "node": ">= 0.4" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6112,20 +4991,10 @@ "node": ">= 0.6" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -6135,14 +5004,14 @@ "node": "*" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/ms": { @@ -6152,23 +5021,10 @@ "dev": true, "license": "MIT" }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -6183,6 +5039,22 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -6190,215 +5062,113 @@ "dev": true, "license": "MIT" }, - "node_modules/nimma": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/nimma/-/nimma-0.2.3.tgz", - "integrity": "sha512-1ZOI8J+1PKKGceo/5CT5GfQOG6H8I2BencSK06YarZ2wXwH37BSSUWldqJmMJYA5JfqDqffxDXynt6f11AyKcA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsep-plugin/regex": "^1.0.1", - "@jsep-plugin/ternary": "^1.0.2", - "astring": "^1.8.1", - "jsep": "^1.2.0" - }, - "engines": { - "node": "^12.20 || >=14.13" - }, - "optionalDependencies": { - "jsonpath-plus": "^6.0.1 || ^10.1.0", - "lodash.topath": "^4.5.2" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, + "node_modules/next": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.3.tgz", + "integrity": "sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA==", "license": "MIT", "dependencies": { - "whatwg-url": "^5.0.0" + "@next/env": "16.2.3", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" }, "engines": { - "node": "4.x || >=6.0.0" + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.2.3", + "@next/swc-darwin-x64": "16.2.3", + "@next/swc-linux-arm64-gnu": "16.2.3", + "@next/swc-linux-arm64-musl": "16.2.3", + "@next/swc-linux-x64-gnu": "16.2.3", + "@next/swc-linux-x64-musl": "16.2.3", + "@next/swc-win32-arm64-msvc": "16.2.3", + "@next/swc-win32-x64-msvc": "16.2.3", + "sharp": "^0.34.5" }, "peerDependencies": { - "encoding": "^0.1.0" + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" }, "peerDependenciesMeta": { - "encoding": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { "optional": true } } }, - "node_modules/node-fetch-h2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", - "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", - "dev": true, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "http2-client": "^1.2.5" + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" }, "engines": { - "node": "4.x || >=6.0.0" + "node": "^10 || ^12 || >=14" } }, - "node_modules/node-readfiles": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", - "integrity": "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==", + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", "dev": true, "license": "MIT", "dependencies": { - "es6-promise": "^3.2.1" + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/node-releases": { - "version": "2.0.23", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", - "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", "dev": true, "license": "MIT" }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/oas-kit-common": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", - "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "fast-safe-stringify": "^2.0.7" - } - }, - "node_modules/oas-linter": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.2.2.tgz", - "integrity": "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@exodus/schemasafe": "^1.0.0-rc.2", - "should": "^13.2.1", - "yaml": "^1.10.0" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-linter/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/oas-resolver": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.5.6.tgz", - "integrity": "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "node-fetch-h2": "^2.3.0", - "oas-kit-common": "^1.0.8", - "reftools": "^1.1.9", - "yaml": "^1.10.0", - "yargs": "^17.0.1" - }, - "bin": { - "resolve": "resolve.js" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-resolver/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/oas-schema-walker": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz", - "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==", - "dev": true, - "license": "BSD-3-Clause", - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-validator": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-5.0.8.tgz", - "integrity": "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "call-me-maybe": "^1.0.1", - "oas-kit-common": "^1.0.8", - "oas-linter": "^3.2.2", - "oas-resolver": "^2.5.6", - "oas-schema-walker": "^1.1.5", - "reftools": "^1.1.9", - "should": "^13.2.1", - "yaml": "^1.10.0" - }, - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/oas-validator/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6409,16 +5179,6 @@ "node": ">=0.10.0" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -6463,38 +5223,73 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, "license": "MIT", "dependencies": { - "mimic-fn": "^2.1.0" + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.4" } }, - "node_modules/openapi-types": { - "version": "12.1.3", - "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", - "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/openapi3-ts": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.2.2.tgz", - "integrity": "sha512-+9g4actZKeb3czfi9gVQ4Br2Ju3KwhCAQJBNaKgye5KggqcBLIhFHH+nIkcm0BUX00TrAJl6dH4JWgM4G4JWrw==", + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, "license": "MIT", "dependencies": { - "yaml": "^2.3.4" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/optionator": { @@ -6515,94 +5310,6 @@ "node": ">= 0.8.0" } }, - "node_modules/orval": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/orval/-/orval-7.3.0.tgz", - "integrity": "sha512-NzLCItKhblP9Ks3RyLQmvLna0kWBLedzew+arHyWnHTi35L4LLkXo8gZxJbucxlQWpdXxjwFQemLuBSEmt6M3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@apidevtools/swagger-parser": "^10.1.0", - "@orval/angular": "7.3.0", - "@orval/axios": "7.3.0", - "@orval/core": "7.3.0", - "@orval/fetch": "7.3.0", - "@orval/hono": "7.3.0", - "@orval/mock": "7.3.0", - "@orval/query": "7.3.0", - "@orval/swr": "7.3.0", - "@orval/zod": "7.3.0", - "ajv": "^8.12.0", - "cac": "^6.7.14", - "chalk": "^4.1.2", - "chokidar": "^4.0.1", - "enquirer": "^2.4.1", - "execa": "^5.1.1", - "find-up": "5.0.0", - "fs-extra": "^11.2.0", - "lodash.uniq": "^4.5.0", - "openapi3-ts": "4.2.2", - "string-argv": "^0.3.2", - "tsconfck": "^2.0.1" - }, - "bin": { - "orval": "dist/bin/orval.js" - } - }, - "node_modules/orval/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/orval/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/orval/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/orval/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -6653,13 +5360,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6700,51 +5400,16 @@ "dev": true, "license": "MIT" }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -6754,36 +5419,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pony-cause": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-1.1.1.tgz", - "integrity": "sha512-PxkIc/2ZpLiEzQXu5YRDOUgBlfGYBY8156HY5ZcRAwwonMk5W/MrJP2LLkG/hF7GEQzaHo2aS7ho6ZLCOvf+6g==", - "dev": true, - "license": "0BSD", - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -6795,9 +5430,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", "dev": true, "funding": [ { @@ -6823,140 +5458,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", - "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -6967,11 +5468,26 @@ "node": ">= 0.8.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/punycode": { "version": "2.3.1", @@ -7005,102 +5521,32 @@ "license": "MIT" }, "node_modules/react": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", - "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", - "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.0" + "react": "^19.2.4" } }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz", - "integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==", - "license": "MIT", - "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/react-router-dom": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.4.tgz", - "integrity": "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==", - "license": "MIT", - "dependencies": { - "react-router": "7.9.4" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/reflect-metadata": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0" + "license": "MIT" }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -7125,16 +5571,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/reftools": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.9.tgz", - "integrity": "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==", - "dev": true, - "license": "BSD-3-Clause", - "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -7156,34 +5592,17 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -7207,6 +5626,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -7218,48 +5647,6 @@ "node": ">=0.10.0" } }, - "node_modules/rollup": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", - "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.4", - "@rollup/rollup-android-arm64": "4.52.4", - "@rollup/rollup-darwin-arm64": "4.52.4", - "@rollup/rollup-darwin-x64": "4.52.4", - "@rollup/rollup-freebsd-arm64": "4.52.4", - "@rollup/rollup-freebsd-x64": "4.52.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", - "@rollup/rollup-linux-arm-musleabihf": "4.52.4", - "@rollup/rollup-linux-arm64-gnu": "4.52.4", - "@rollup/rollup-linux-arm64-musl": "4.52.4", - "@rollup/rollup-linux-loong64-gnu": "4.52.4", - "@rollup/rollup-linux-ppc64-gnu": "4.52.4", - "@rollup/rollup-linux-riscv64-gnu": "4.52.4", - "@rollup/rollup-linux-riscv64-musl": "4.52.4", - "@rollup/rollup-linux-s390x-gnu": "4.52.4", - "@rollup/rollup-linux-x64-gnu": "4.52.4", - "@rollup/rollup-linux-x64-musl": "4.52.4", - "@rollup/rollup-openharmony-arm64": "4.52.4", - "@rollup/rollup-win32-arm64-msvc": "4.52.4", - "@rollup/rollup-win32-ia32-msvc": "4.52.4", - "@rollup/rollup-win32-x64-gnu": "4.52.4", - "@rollup/rollup-win32-x64-msvc": "4.52.4", - "fsevents": "~2.3.2" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -7339,13 +5726,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-stable-stringify": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz", - "integrity": "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw==", - "dev": true, - "license": "MIT" - }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -7362,12 +5742,6 @@ "semver": "bin/semver.js" } }, - "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", - "license": "MIT" - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -7417,6 +5791,64 @@ "node": ">= 0.4" } }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7440,66 +5872,6 @@ "node": ">=8" } }, - "node_modules/should": { - "version": "13.2.3", - "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", - "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "should-equal": "^2.0.0", - "should-format": "^3.0.3", - "should-type": "^1.4.0", - "should-type-adaptors": "^1.0.1", - "should-util": "^1.0.0" - } - }, - "node_modules/should-equal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", - "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "should-type": "^1.4.0" - } - }, - "node_modules/should-format": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", - "integrity": "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "should-type": "^1.3.0", - "should-type-adaptors": "^1.0.1" - } - }, - "node_modules/should-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", - "integrity": "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/should-type-adaptors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", - "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "should-type": "^1.3.0", - "should-util": "^1.0.0" - } - }, - "node_modules/should-util": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", - "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==", - "dev": true, - "license": "MIT" - }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -7521,14 +5893,14 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -7576,52 +5948,22 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/simple-eval": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-eval/-/simple-eval-1.0.1.tgz", - "integrity": "sha512-LH7FpTAkeD+y5xQC4fzS+tFtaNlvt3Ib1zKzvhjv/Y+cioV4zIuw4IZr2yhRLu67CWL7FR9/6KXKnjRoZTvGGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "jsep": "^1.3.6" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -7636,78 +5978,58 @@ "node": ">= 0.4" } }, - "node_modules/string-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.19" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", "dev": true, "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" }, "engines": { - "node": ">=12" + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" } }, "node_modules/string.prototype.trim": { @@ -7769,54 +6091,14 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" + "node": ">=4" } }, "node_modules/strip-json-comments": { @@ -7832,27 +6114,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" + "client-only": "0.0.1" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } } }, "node_modules/supports-color": { @@ -7881,114 +6163,46 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/swagger2openapi": { - "version": "7.0.8", - "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-7.0.8.tgz", - "integrity": "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "call-me-maybe": "^1.0.1", - "node-fetch": "^2.6.1", - "node-fetch-h2": "^2.3.0", - "node-readfiles": "^0.2.0", - "oas-kit-common": "^1.0.8", - "oas-resolver": "^2.5.6", - "oas-schema-walker": "^1.1.5", - "oas-validator": "^5.0.8", - "reftools": "^1.1.9", - "yaml": "^1.10.0", - "yargs": "^17.0.1" - }, - "bin": { - "boast": "boast.js", - "oas-validate": "oas-validate.js", - "swagger2openapi": "swagger2openapi.js" - }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", "funding": { - "url": "https://github.com/Mermade/oas-kit?sponsor=1" - } - }, - "node_modules/swagger2openapi/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" + "type": "github", + "url": "https://github.com/sponsors/dcastil" } }, "node_modules/tailwindcss": { - "version": "3.4.18", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", - "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.7", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } + "license": "MIT" }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "node_modules/tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", "dev": true, "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, "engines": { - "node": ">=0.8" + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -8016,9 +6230,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -8041,17 +6255,10 @@ "node": ">=8.0" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true, - "license": "MIT" - }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -8061,39 +6268,36 @@ "typescript": ">=4.8.4" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/tsconfck": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-2.1.2.tgz", - "integrity": "sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg==", + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, "bin": { - "tsconfck": "bin/tsconfck.js" - }, - "engines": { - "node": "^14.13.1 || ^16 || >=18" - }, - "peerDependencies": { - "typescript": "^4.3.5 || ^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "json5": "lib/cli.js" } }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/type-check": { @@ -8202,16 +6406,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.46.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.0.tgz", - "integrity": "sha512-6+ZrB6y2bT2DX3K+Qd9vn7OFOJR+xSLDj+Aw/N3zBwUt27uTw2sw2TE2+UcY1RiyBZkaGbTkVg9SSdPNUG6aUw==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.1.tgz", + "integrity": "sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.46.0", - "@typescript-eslint/parser": "8.46.0", - "@typescript-eslint/typescript-estree": "8.46.0", - "@typescript-eslint/utils": "8.46.0" + "@typescript-eslint/eslint-plugin": "8.58.1", + "@typescript-eslint/parser": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/utils": "8.58.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -8221,8 +6425,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/unbox-primitive": { @@ -8245,26 +6449,51 @@ } }, "node_modules/undici-types": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", - "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "engines": { - "node": ">= 10.0.0" + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -8302,164 +6531,6 @@ "punycode": "^2.1.0" } }, - "node_modules/urijs": { - "version": "1.19.11", - "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", - "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/utility-types": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", - "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/validator": { - "version": "13.15.22", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.22.tgz", - "integrity": "sha512-uT/YQjiyLJP7HSrv/dPZqK9L28xf8hsNca01HSz1dfmI0DgMfjopp1rO/z13NeGF1tVystF0Ejx3y4rUKPw+bQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vite": { - "version": "7.1.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz", - "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -8544,9 +6615,9 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -8575,111 +6646,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -8687,93 +6653,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -8786,6 +6665,29 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 00edbe2..96e6424 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,41 +1,30 @@ { - "name": "ocdp", + "name": "frontend", + "version": "0.1.0", "private": true, - "version": "0.0.0", - "type": "module", "scripts": { - "dev": "vite --host", - "build": "tsc -b && vite build", - "lint": "eslint .", - "preview": "vite preview", - "openapi-gen": "orval", - "openapi-gen-old": "cd .. && make openapi-gen-frontend" + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint" }, "dependencies": { - "axios": "^1.13.2", - "class-transformer": "^0.5.1", - "lucide-react": "^0.545.0", - "react": "^19.1.1", - "react-dom": "^19.1.1", - "react-router-dom": "^7.9.4", - "reflect-metadata": "^0.2.2" + "axios": "^1.15.0", + "clsx": "^2.1.1", + "lucide-react": "^1.8.0", + "next": "16.2.3", + "react": "19.2.4", + "react-dom": "19.2.4", + "tailwind-merge": "^3.5.0" }, "devDependencies": { - "@eslint/js": "^9.36.0", - "@types/node": "^24.6.0", - "@types/react": "^19.1.16", - "@types/react-dom": "^19.1.9", - "@vitejs/plugin-react": "^5.0.4", - "autoprefixer": "^10.4.21", - "eslint": "^9.36.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.22", - "globals": "^16.4.0", - "orval": "^7.3.0", - "postcss": "^8.5.6", - "tailwindcss": "^3.4.18", - "typescript": "~5.9.3", - "typescript-eslint": "^8.45.0", - "vite": "^7.1.7" + "@tailwindcss/postcss": "^4.2.2", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.2.3", + "tailwindcss": "^4", + "typescript": "^5" } } diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js deleted file mode 100644 index 2e7af2b..0000000 --- a/frontend/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/frontend/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/frontend/public/api-test.html b/frontend/public/api-test.html deleted file mode 100644 index 17f11c0..0000000 --- a/frontend/public/api-test.html +++ /dev/null @@ -1,431 +0,0 @@ - - - - - - OCDP API camelCase 测试 - - - -
-
-

🧪 OCDP API camelCase 测试

-

测试前后端 camelCase 通信是否正常

-
- -
-
-

⚙️ 配置

-
- - -
-
- - -
-
- -
-

🎮 测试操作

-
- - - - - -
-
- - - -
-

📋 测试日志

-
点击按钮开始测试...
-
-
-
- - - - - diff --git a/frontend/public/file.svg b/frontend/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/frontend/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/globe.svg b/frontend/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/frontend/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/next.svg b/frontend/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/frontend/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/vercel.svg b/frontend/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/frontend/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/window.svg b/frontend/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/frontend/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/api/axios-mutator.ts b/frontend/src/api/axios-mutator.ts deleted file mode 100644 index ce47bca..0000000 --- a/frontend/src/api/axios-mutator.ts +++ /dev/null @@ -1,99 +0,0 @@ -import Axios from "axios"; -import type { AxiosRequestConfig } from "axios"; -import { keysToCamel, keysToSnake } from "@/shared/utils/case-converter"; - -/** - * Axios instance for Orval-generated API client - * - * Features: - * - Configurable baseURL from environment variable - * - Auth token management - * - Request cancellation support - */ -const FALLBACK_API_BASE = '/api/v1'; -const configuredBaseUrl = import.meta.env.VITE_API_BASE_URL?.trim(); - -export const AXIOS_INSTANCE = Axios.create({ - // Default to /api/v1 so local dev still hits the proxy when no env is provided - baseURL: configuredBaseUrl && configuredBaseUrl.length > 0 ? configuredBaseUrl : FALLBACK_API_BASE, -}); - -const isTransformablePayload = (payload: unknown) => { - if (payload === null || payload === undefined) { - return false; - } - if (payload instanceof FormData || payload instanceof Blob || payload instanceof ArrayBuffer) { - return false; - } - return typeof payload === "object"; -}; - -AXIOS_INSTANCE.interceptors.request.use((config) => { - if (isTransformablePayload(config.data)) { - config.data = keysToSnake(config.data); - } - if (isTransformablePayload(config.params)) { - config.params = keysToSnake(config.params); - } - return config; -}); - -AXIOS_INSTANCE.interceptors.response.use( - (response) => { - if (isTransformablePayload(response?.data)) { - response.data = keysToCamel(response.data); - } - return response; - }, - (error) => { - if (isTransformablePayload(error?.response?.data)) { - error.response.data = keysToCamel(error.response.data); - } - return Promise.reject(error); - } -); - -/** - * Custom axios instance for Orval - * Provides request cancellation support - */ -export const customAxiosInstance = ( - config: AxiosRequestConfig, - options?: AxiosRequestConfig -): Promise => { - const source = Axios.CancelToken.source(); - const promise = AXIOS_INSTANCE({ - ...config, - ...options, - cancelToken: source.token, - }).then(({ data }) => data); - - // @ts-ignore - promise.cancel = () => { - source.cancel('Query was cancelled'); - }; - - return promise as any; -}; - -/** - * Set authentication token for all requests - * - * @param token JWT token or null to remove - * - * @example - * import { setAuthToken } from '@/api/axios-mutator'; - * - * // Set token - * setAuthToken('your-jwt-token'); - * - * // Remove token - * setAuthToken(null); - */ -export function setAuthToken(token: string | null) { - if (token) { - AXIOS_INSTANCE.defaults.headers.common['Authorization'] = `Bearer ${token}`; - } else { - delete AXIOS_INSTANCE.defaults.headers.common['Authorization']; - } -} diff --git a/frontend/src/api/generated-orval/api.schemas.ts b/frontend/src/api/generated-orval/api.schemas.ts deleted file mode 100644 index 41c7b7d..0000000 --- a/frontend/src/api/generated-orval/api.schemas.ts +++ /dev/null @@ -1,439 +0,0 @@ -/** - * Generated by orval v7.3.0 🍺 - * Do not edit manually. - * OCDP Backend API - * OCDP (Open Cloud Development Platform) Backend API - -RESTful API for managing Kubernetes clusters, OCI registries, and Helm deployments. - * OpenAPI spec version: 1.0 - */ -export type GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferenceValuesSchemaPathParameters = { - registryId: string, - repositoryName: string, - reference: string, - } -export type GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferencePathParameters = { - registryId: string, - repositoryName: string, - reference: string, - } -export type GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsParams = { -/** - * 过滤 Artifact 类型 (all, chart, image, other) - */ -media_type?: string; -}; - -export type GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsPathParameters = { - registryId: string, - repositoryName: string, - } -export type GetRegistriesRegistryIdRepositoriesPathParameters = { - registryId: string, - } -export type GetRegistriesRegistryIdHealthPathParameters = { - registryId: string, - } -export type PutRegistriesRegistryIdPathParameters = { - registryId: string, - } -export type GetRegistriesRegistryIdPathParameters = { - registryId: string, - } -export type DeleteRegistriesRegistryIdPathParameters = { - registryId: string, - } -export type GetMonitoringClustersClusterIdNodesPathParameters = { - clusterId: string, - } -export type GetMonitoringClustersClusterIdPathParameters = { - clusterId: string, - } -export type GetClustersClusterIdInstancesInstanceIdEntriesPathParameters = { - clusterId: string, - instanceId: string, - } -export type PutClustersClusterIdInstancesInstanceIdPathParameters = { - clusterId: string, - instanceId: string, - } -export type GetClustersClusterIdInstancesInstanceIdPathParameters = { - clusterId: string, - instanceId: string, - } -export type DeleteClustersClusterIdInstancesInstanceIdPathParameters = { - clusterId: string, - instanceId: string, - } -export type PostClustersClusterIdInstancesPathParameters = { - clusterId: string, - } -export type GetClustersClusterIdInstancesPathParameters = { - clusterId: string, - } -export type GetClustersClusterIdHealthPathParameters = { - clusterId: string, - } -export type PutClustersClusterIdPathParameters = { - clusterId: string, - } -export type GetClustersClusterIdPathParameters = { - clusterId: string, - } -export type DeleteClustersClusterIdPathParameters = { - clusterId: string, - } -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoValuesSchemaResponse { - schema?: string; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUserResponse { - createdAt?: string; - email?: string; - id?: string; - updatedAt?: string; - username?: string; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateRegistryRequest { - description?: string; - insecure?: boolean; - name?: string; - password?: string; - url?: string; - username?: string; -} - -export type GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateInstanceRequestValues = { [key: string]: unknown }; - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateInstanceRequest { - description?: string; - values?: GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateInstanceRequestValues; - valuesYaml?: string; - version?: string; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateClusterRequest { - /** Base64 CA data (also accepts legacy field "ca_data") */ - caData?: string; - /** Base64 client certificate (also accepts legacy field "cert_data") */ - certData?: string; - description?: string; - host?: string; - /** Base64 client key (also accepts legacy field "key_data") */ - keyData?: string; - name?: string; - token?: string; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoTagResponse { - mediaType?: string; - /** Repository name */ - repositoryName?: string; - /** Artifact size (bytes) */ - size?: number; - /** Tag name (e.g. "1.0.0", "latest") */ - tag?: string; - /** Artifact type: chart, image, other */ - type?: string; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRepositoryListResponse { - /** Whether _catalog API is supported */ - catalogSupported?: boolean; - /** User-friendly message */ - message?: string; - registryId?: string; - registryUrl?: string; - repositories?: string[]; - /** Data source: "catalog" | "preconfigured" | "unavailable" */ - source?: string; - total?: number; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRegistryResponse { - createdAt?: string; - description?: string; - /** 是否已设置密码 */ - hasPassword?: boolean; - id?: string; - insecure?: boolean; - name?: string; - /** 脱敏显示(••••••••) */ - password?: string; - updatedAt?: string; - url?: string; - /** 明文返回用户名(不敏感) */ - username?: string; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRegistryHealthResponse { - healthy?: boolean; - message?: string; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRegisterRequest { - /** @minLength 6 */ - password: string; - username: string; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRefreshTokenRequest { - refreshToken: string; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoNodeMetricsResponse { - age?: string; - containerRuntime?: string; - cpuAllocatable?: string; - cpuCapacity?: string; - cpuPercent?: number; - cpuUsage?: string; - gpuCapacity?: number; - gpuPercent?: number; - gpuType?: string; - gpuUsage?: number; - kernelVersion?: string; - kubeletVersion?: string; - memoryAllocatable?: string; - memoryCapacity?: string; - memoryPercent?: number; - memoryUsage?: string; - nodeName?: string; - osImage?: string; - podCount?: number; - role?: string; - status?: string; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoMonitoringSummaryResponse { - errorClusters?: number; - healthyClusters?: number; - lastUpdate?: string; - totalClusters?: number; - totalNodes?: number; - totalPods?: number; - warningClusters?: number; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoLoginRequest { - password: string; - username: string; -} - -export type GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponseValues = { [key: string]: unknown }; - -/** - * 实例当前状态 - */ -export type GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponseStatus = typeof GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponseStatus[keyof typeof GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponseStatus]; - - -// eslint-disable-next-line @typescript-eslint/no-redeclare -export const GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponseStatus = { - deployed: 'deployed', - uninstalled: 'uninstalled', - superseded: 'superseded', - failed: 'failed', - 'pending-install': 'pending-install', - 'pending-upgrade': 'pending-upgrade', - 'pending-rollback': 'pending-rollback', - 'pending-delete': 'pending-delete', - unknown: 'unknown', -} as const; - -/** - * 最后一次操作类型 - */ -export type GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponseLastOperation = typeof GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponseLastOperation[keyof typeof GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponseLastOperation]; - - -// eslint-disable-next-line @typescript-eslint/no-redeclare -export const GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponseLastOperation = { - '': '', - install: 'install', - upgrade: 'upgrade', - rollback: 'rollback', - delete: 'delete', - sync: 'sync', -} as const; - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponse { - chart?: string; - clusterId?: string; - createdAt?: string; - description?: string; - id?: string; - /** 最近一次错误信息 */ - lastError?: string; - /** 最后一次操作类型 */ - lastOperation?: GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponseLastOperation; - name?: string; - namespace?: string; - registryId?: string; - repository?: string; - revision?: number; - /** 实例当前状态 */ - status?: GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponseStatus; - /** 状态说明 */ - statusReason?: string; - updatedAt?: string; - values?: GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponseValues; - version?: string; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceListResponse { - instances?: GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponse[]; - total?: number; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceEntryTLSResponse { - hosts?: string[]; - secretName?: string; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceEntryPortResponse { - name?: string; - nodePort?: number; - port?: number; - protocol?: string; - targetPort?: string; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceEntryResponse { - clusterIP?: string; - externalIPs?: string[]; - hosts?: GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceEntryHostResponse[]; - kind?: string; - loadBalancerIngress?: string[]; - name?: string; - namespace?: string; - ports?: GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceEntryPortResponse[]; - tls?: GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceEntryTLSResponse[]; - type?: string; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceEntryPathResponse { - path?: string; - serviceName?: string; - servicePort?: string; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceEntryHostResponse { - host?: string; - paths?: GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceEntryPathResponse[]; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoErrorResponse { - code?: number; - error?: string; - message?: string; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateRegistryRequest { - description?: string; - insecure?: boolean; - name: string; - password?: string; - url: string; - username?: string; -} - -export type GithubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateInstanceRequestValues = { [key: string]: unknown }; - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateInstanceRequest { - description?: string; - name: string; - namespace: string; - /** Registry identifier (also accepts legacy field "registry_id") */ - registryId: string; - repository: string; - tag: string; - values?: GithubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateInstanceRequestValues; - valuesYaml?: string; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateClusterRequest { - /** Base64 CA data (also accepts legacy field "ca_data") */ - caData?: string; - /** Base64 client certificate (also accepts legacy field "cert_data") */ - certData?: string; - description?: string; - host: string; - /** Base64 client key (also accepts legacy field "key_data") */ - keyData?: string; - name: string; - token?: string; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoClusterResponse { - /** 脱敏数据(仅用于前端显示,实际值为掩码) */ - caData?: string; - /** 脱敏显示(••••••••) */ - certData?: string; - createdAt?: string; - description?: string; - /** 认证配置状态(不返回实际证书数据,仅返回是否已配置) */ - hasCaData?: boolean; - hasCertData?: boolean; - hasKeyData?: boolean; - hasToken?: boolean; - host?: string; - id?: string; - /** 脱敏显示(••••••••) */ - keyData?: string; - name?: string; - /** 脱敏显示(••••••••) */ - token?: string; - updatedAt?: string; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoClusterMetricsResponse { - clusterId?: string; - clusterName?: string; - cpuUsage?: number; - gpuUsage?: number; - lastCheck?: string; - maxNodeCpu?: string; - maxNodeCpuUsage?: number; - maxNodeGpu?: number; - maxNodeGpuUsage?: number; - maxNodeMemory?: string; - maxNodeMemUsage?: number; - memoryUsage?: number; - nodeCount?: number; - nodes?: GithubComOcdpClusterServiceInternalAdapterInputHttpDtoNodeMetricsResponse[]; - podCount?: number; - status?: string; - totalCpu?: string; - totalGpu?: number; - totalMemory?: string; - uptime?: string; - usedCpu?: string; - usedGpu?: number; - usedMemory?: string; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoClusterHealthResponse { - healthy?: boolean; - message?: string; - version?: string; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoAuthResponse { - accessToken?: string; - refreshToken?: string; - userId?: string; - username?: string; -} - -export interface GithubComOcdpClusterServiceInternalAdapterInputHttpDtoArtifactResponse { - createdAt?: string; - digest?: string; - repositoryName?: string; - size?: number; - tag?: string; - /** chart | image | other */ - type?: string; -} - diff --git a/frontend/src/api/generated-orval/api.ts b/frontend/src/api/generated-orval/api.ts deleted file mode 100644 index e1d7606..0000000 --- a/frontend/src/api/generated-orval/api.ts +++ /dev/null @@ -1,474 +0,0 @@ -/** - * Generated by orval v7.3.0 🍺 - * Do not edit manually. - * OCDP Backend API - * OCDP (Open Cloud Development Platform) Backend API - -RESTful API for managing Kubernetes clusters, OCI registries, and Helm deployments. - * OpenAPI spec version: 1.0 - */ -import type { - DeleteClustersClusterIdInstancesInstanceIdPathParameters, - DeleteClustersClusterIdPathParameters, - DeleteRegistriesRegistryIdPathParameters, - GetClustersClusterIdHealthPathParameters, - GetClustersClusterIdInstancesInstanceIdEntriesPathParameters, - GetClustersClusterIdInstancesInstanceIdPathParameters, - GetClustersClusterIdInstancesPathParameters, - GetClustersClusterIdPathParameters, - GetMonitoringClustersClusterIdNodesPathParameters, - GetMonitoringClustersClusterIdPathParameters, - GetRegistriesRegistryIdHealthPathParameters, - GetRegistriesRegistryIdPathParameters, - GetRegistriesRegistryIdRepositoriesPathParameters, - GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsParams, - GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsPathParameters, - GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferencePathParameters, - GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferenceValuesSchemaPathParameters, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoArtifactResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoAuthResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoClusterHealthResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoClusterMetricsResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoClusterResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateClusterRequest, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateInstanceRequest, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateRegistryRequest, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceEntryResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceListResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoLoginRequest, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoMonitoringSummaryResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoNodeMetricsResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRefreshTokenRequest, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRegisterRequest, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRegistryHealthResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRegistryResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRepositoryListResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoTagResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateClusterRequest, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateInstanceRequest, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateRegistryRequest, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUserResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoValuesSchemaResponse, - PostClustersClusterIdInstancesPathParameters, - PutClustersClusterIdInstancesInstanceIdPathParameters, - PutClustersClusterIdPathParameters, - PutRegistriesRegistryIdPathParameters -} from './api.schemas' -import { customAxiosInstance } from '../axios-mutator'; - - -type SecondParameter any> = Parameters[1]; - - - /** - * 使用用户名和密码获取访问令牌 - * @summary 用户登录 - */ -export const postAuthLogin = ( - githubComOcdpClusterServiceInternalAdapterInputHttpDtoLoginRequest: GithubComOcdpClusterServiceInternalAdapterInputHttpDtoLoginRequest, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/auth/login`, method: 'POST', - headers: {'Content-Type': 'application/json', }, - data: githubComOcdpClusterServiceInternalAdapterInputHttpDtoLoginRequest - }, - options); - } - -/** - * 使用刷新令牌获取新的访问令牌 - * @summary 刷新访问令牌 - */ -export const postAuthRefresh = ( - githubComOcdpClusterServiceInternalAdapterInputHttpDtoRefreshTokenRequest: GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRefreshTokenRequest, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/auth/refresh`, method: 'POST', - headers: {'Content-Type': 'application/json', }, - data: githubComOcdpClusterServiceInternalAdapterInputHttpDtoRefreshTokenRequest - }, - options); - } - -/** - * 创建一个新的后台用户 - * @summary 用户注册 - */ -export const postAuthRegister = ( - githubComOcdpClusterServiceInternalAdapterInputHttpDtoRegisterRequest: GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRegisterRequest, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/auth/register`, method: 'POST', - headers: {'Content-Type': 'application/json', }, - data: githubComOcdpClusterServiceInternalAdapterInputHttpDtoRegisterRequest - }, - options); - } - -/** - * @summary 列出所有集群 - */ -export const getClusters = ( - - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/clusters`, method: 'GET' - }, - options); - } - -/** - * 创建一个新的 Kubernetes 集群配置 - * @summary 创建集群 - */ -export const postClusters = ( - githubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateClusterRequest: GithubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateClusterRequest, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/clusters`, method: 'POST', - headers: {'Content-Type': 'application/json', }, - data: githubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateClusterRequest - }, - options); - } - -/** - * @summary 删除集群 - */ -export const deleteClustersClusterId = ( - { clusterId }: DeleteClustersClusterIdPathParameters, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/clusters/${clusterId}`, method: 'DELETE' - }, - options); - } - -/** - * @summary 获取集群详情 - */ -export const getClustersClusterId = ( - { clusterId }: GetClustersClusterIdPathParameters, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/clusters/${clusterId}`, method: 'GET' - }, - options); - } - -/** - * @summary 更新集群 - */ -export const putClustersClusterId = ( - { clusterId }: PutClustersClusterIdPathParameters, - githubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateClusterRequest: GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateClusterRequest, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/clusters/${clusterId}`, method: 'PUT', - headers: {'Content-Type': 'application/json', }, - data: githubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateClusterRequest - }, - options); - } - -/** - * @summary 获取集群健康状态 - */ -export const getClustersClusterIdHealth = ( - { clusterId }: GetClustersClusterIdHealthPathParameters, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/clusters/${clusterId}/health`, method: 'GET' - }, - options); - } - -/** - * @summary 列出实例 - */ -export const getClustersClusterIdInstances = ( - { clusterId }: GetClustersClusterIdInstancesPathParameters, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/clusters/${clusterId}/instances`, method: 'GET' - }, - options); - } - -/** - * 在指定集群上部署一个 artifact - * @summary 创建实例 - */ -export const postClustersClusterIdInstances = ( - { clusterId }: PostClustersClusterIdInstancesPathParameters, - githubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateInstanceRequest: GithubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateInstanceRequest, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/clusters/${clusterId}/instances`, method: 'POST', - headers: {'Content-Type': 'application/json', }, - data: githubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateInstanceRequest - }, - options); - } - -/** - * @summary 删除实例 - */ -export const deleteClustersClusterIdInstancesInstanceId = ( - { clusterId, instanceId }: DeleteClustersClusterIdInstancesInstanceIdPathParameters, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/clusters/${clusterId}/instances/${instanceId}`, method: 'DELETE' - }, - options); - } - -/** - * @summary 获取实例详情 - */ -export const getClustersClusterIdInstancesInstanceId = ( - { clusterId, instanceId }: GetClustersClusterIdInstancesInstanceIdPathParameters, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/clusters/${clusterId}/instances/${instanceId}`, method: 'GET' - }, - options); - } - -/** - * @summary 更新实例 - */ -export const putClustersClusterIdInstancesInstanceId = ( - { clusterId, instanceId }: PutClustersClusterIdInstancesInstanceIdPathParameters, - githubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateInstanceRequest: GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateInstanceRequest, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/clusters/${clusterId}/instances/${instanceId}`, method: 'PUT', - headers: {'Content-Type': 'application/json', }, - data: githubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateInstanceRequest - }, - options); - } - -/** - * @summary 获取实例 Service/Ingress 入口 - */ -export const getClustersClusterIdInstancesInstanceIdEntries = ( - { clusterId, instanceId }: GetClustersClusterIdInstancesInstanceIdEntriesPathParameters, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/clusters/${clusterId}/instances/${instanceId}/entries`, method: 'GET' - }, - options); - } - -/** - * @summary 列出集群监控 - */ -export const getMonitoringClusters = ( - - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/monitoring/clusters`, method: 'GET' - }, - options); - } - -/** - * @summary 获取集群监控 - */ -export const getMonitoringClustersClusterId = ( - { clusterId }: GetMonitoringClustersClusterIdPathParameters, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/monitoring/clusters/${clusterId}`, method: 'GET' - }, - options); - } - -/** - * @summary 获取节点指标 - */ -export const getMonitoringClustersClusterIdNodes = ( - { clusterId }: GetMonitoringClustersClusterIdNodesPathParameters, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/monitoring/clusters/${clusterId}/nodes`, method: 'GET' - }, - options); - } - -/** - * @summary 获取监控汇总 - */ -export const getMonitoringSummary = ( - - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/monitoring/summary`, method: 'GET' - }, - options); - } - -/** - * @summary 列出所有 Registries - */ -export const getRegistries = ( - - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/registries`, method: 'GET' - }, - options); - } - -/** - * 新增 OCI Registry 配置 - * @summary 创建 Registry - */ -export const postRegistries = ( - githubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateRegistryRequest: GithubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateRegistryRequest, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/registries`, method: 'POST', - headers: {'Content-Type': 'application/json', }, - data: githubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateRegistryRequest - }, - options); - } - -/** - * @summary 删除 Registry - */ -export const deleteRegistriesRegistryId = ( - { registryId }: DeleteRegistriesRegistryIdPathParameters, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/registries/${registryId}`, method: 'DELETE' - }, - options); - } - -/** - * @summary 获取 Registry - */ -export const getRegistriesRegistryId = ( - { registryId }: GetRegistriesRegistryIdPathParameters, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/registries/${registryId}`, method: 'GET' - }, - options); - } - -/** - * @summary 更新 Registry - */ -export const putRegistriesRegistryId = ( - { registryId }: PutRegistriesRegistryIdPathParameters, - githubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateRegistryRequest: GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateRegistryRequest, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/registries/${registryId}`, method: 'PUT', - headers: {'Content-Type': 'application/json', }, - data: githubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateRegistryRequest - }, - options); - } - -/** - * @summary 检查 Registry 健康 - */ -export const getRegistriesRegistryIdHealth = ( - { registryId }: GetRegistriesRegistryIdHealthPathParameters, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/registries/${registryId}/health`, method: 'GET' - }, - options); - } - -/** - * 列出指定 Registry 中的所有 Repository - * @summary 列出 Registry 中的所有 Repositories - */ -export const getRegistriesRegistryIdRepositories = ( - { registryId }: GetRegistriesRegistryIdRepositoriesPathParameters, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/registries/${registryId}/repositories`, method: 'GET' - }, - options); - } - -/** - * 列出指定 Repository 中的所有 Artifact,支持按类型过滤 - * @summary 列出 Repository 中的所有 Artifacts - */ -export const getRegistriesRegistryIdRepositoriesRepositoryNameArtifacts = ( - { registryId, repositoryName }: GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsPathParameters, - params?: GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsParams, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/registries/${registryId}/repositories/${repositoryName}/artifacts`, method: 'GET', - params - }, - options); - } - -/** - * 获取指定 Artifact 的详细信息 - * @summary 获取 Artifact 详情 - */ -export const getRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReference = ( - { registryId, repositoryName, reference }: GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferencePathParameters, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/registries/${registryId}/repositories/${repositoryName}/artifacts/${reference}`, method: 'GET' - }, - options); - } - -/** - * 获取 Helm Chart 的 values.schema.json (仅支持 Chart 类型) - * @summary 获取 Helm Chart Values Schema - */ -export const getRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferenceValuesSchema = ( - { registryId, repositoryName, reference }: GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferenceValuesSchemaPathParameters, - options?: SecondParameter,) => { - return customAxiosInstance( - {url: `/registries/${registryId}/repositories/${repositoryName}/artifacts/${reference}/values-schema`, method: 'GET' - }, - options); - } - -export type PostAuthLoginResult = NonNullable>> -export type PostAuthRefreshResult = NonNullable>> -export type PostAuthRegisterResult = NonNullable>> -export type GetClustersResult = NonNullable>> -export type PostClustersResult = NonNullable>> -export type DeleteClustersClusterIdResult = NonNullable>> -export type GetClustersClusterIdResult = NonNullable>> -export type PutClustersClusterIdResult = NonNullable>> -export type GetClustersClusterIdHealthResult = NonNullable>> -export type GetClustersClusterIdInstancesResult = NonNullable>> -export type PostClustersClusterIdInstancesResult = NonNullable>> -export type DeleteClustersClusterIdInstancesInstanceIdResult = NonNullable>> -export type GetClustersClusterIdInstancesInstanceIdResult = NonNullable>> -export type PutClustersClusterIdInstancesInstanceIdResult = NonNullable>> -export type GetClustersClusterIdInstancesInstanceIdEntriesResult = NonNullable>> -export type GetMonitoringClustersResult = NonNullable>> -export type GetMonitoringClustersClusterIdResult = NonNullable>> -export type GetMonitoringClustersClusterIdNodesResult = NonNullable>> -export type GetMonitoringSummaryResult = NonNullable>> -export type GetRegistriesResult = NonNullable>> -export type PostRegistriesResult = NonNullable>> -export type DeleteRegistriesRegistryIdResult = NonNullable>> -export type GetRegistriesRegistryIdResult = NonNullable>> -export type PutRegistriesRegistryIdResult = NonNullable>> -export type GetRegistriesRegistryIdHealthResult = NonNullable>> -export type GetRegistriesRegistryIdRepositoriesResult = NonNullable>> -export type GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsResult = NonNullable>> -export type GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferenceResult = NonNullable>> -export type GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferenceValuesSchemaResult = NonNullable>> diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts deleted file mode 100644 index 7ce62ff..0000000 --- a/frontend/src/api/index.ts +++ /dev/null @@ -1,197 +0,0 @@ -/** - * API Client entry point - * Export configured API client, generated functions, and friendly aliases. - */ - -type AxiosOptions any> = Parameters[2]; - -import { - deleteClustersClusterId, - deleteClustersClusterIdInstancesInstanceId, - deleteRegistriesRegistryId, - getClusters, - getClustersClusterId, - getClustersClusterIdHealth, - getClustersClusterIdInstances, - getClustersClusterIdInstancesInstanceId, - getClustersClusterIdInstancesInstanceIdEntries, - getMonitoringClusters, - getMonitoringClustersClusterId, - getMonitoringClustersClusterIdNodes, - getMonitoringSummary, - getRegistries, - getRegistriesRegistryId, - getRegistriesRegistryIdHealth, - getRegistriesRegistryIdRepositories, - getRegistriesRegistryIdRepositoriesRepositoryNameArtifacts, - getRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReference, - getRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferenceValuesSchema, - postAuthLogin, - postAuthRefresh, - postAuthRegister, - postClusters, - postClustersClusterIdInstances, - postRegistries, - putClustersClusterId, - putClustersClusterIdInstancesInstanceId, - putRegistriesRegistryId, -} from './generated-orval/api'; - -import type { - DeleteClustersClusterIdInstancesInstanceIdPathParameters, - DeleteClustersClusterIdPathParameters, - DeleteRegistriesRegistryIdPathParameters, - GetClustersClusterIdInstancesInstanceIdPathParameters, - GetClustersClusterIdInstancesPathParameters, - GetRegistriesRegistryIdHealthPathParameters, - GetRegistriesRegistryIdPathParameters, - GetRegistriesRegistryIdRepositoriesPathParameters, - GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsPathParameters, - GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferencePathParameters, - GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferenceValuesSchemaPathParameters, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoArtifactResponse as GeneratedArtifactResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoAuthResponse as GeneratedAuthResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoClusterMetricsResponse as GeneratedClusterMetricsResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoClusterResponse as GeneratedClusterResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateClusterRequest as GeneratedCreateClusterRequest, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateInstanceRequest as GeneratedCreateInstanceRequest, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoCreateRegistryRequest as GeneratedCreateRegistryRequest, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceEntryResponse as GeneratedInstanceEntry, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponse as GeneratedInstanceResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoLoginRequest as GeneratedLoginRequest, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoMonitoringSummaryResponse as GeneratedMonitoringSummary, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoNodeMetricsResponse as GeneratedNodeMetricsResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRefreshTokenRequest as GeneratedRefreshTokenRequest, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRegisterRequest as GeneratedRegisterRequest, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRegistryHealthResponse as GeneratedRegistryHealthResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRegistryResponse as GeneratedRegistryResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoRepositoryListResponse as GeneratedRepositoryListResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoTagResponse as GeneratedTagResponse, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateClusterRequest as GeneratedUpdateClusterRequest, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateInstanceRequest as GeneratedUpdateInstanceRequest, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUpdateRegistryRequest as GeneratedUpdateRegistryRequest, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoUserResponse as GeneratedUserResponse, - PutClustersClusterIdInstancesInstanceIdPathParameters, - PutClustersClusterIdPathParameters, - PutRegistriesRegistryIdPathParameters, -} from './generated-orval/api.schemas'; - -import { - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponseLastOperation as GeneratedInstanceLastOperationEnum, - GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponseStatus as GeneratedInstanceStatusEnum, -} from './generated-orval/api.schemas'; - -export { AXIOS_INSTANCE, customAxiosInstance, setAuthToken } from './axios-mutator'; -export { keysToCamel, keysToSnake, snakeToCamel, camelToSnake } from '@/shared/utils/case-converter'; - -// Re-export raw generated APIs/types for advanced usages -export * from './generated-orval/api'; -export type * from './generated-orval/api.schemas'; - -// ---------- Friendly type aliases ---------- -export type AuthResponse = GeneratedAuthResponse; -export type RegisterBody = GeneratedRegisterRequest; -export type LoginBody = GeneratedLoginRequest; -export type RefreshTokenBody = GeneratedRefreshTokenRequest; -export type UserResponse = GeneratedUserResponse; - -export type ClusterResponse = GeneratedClusterResponse; -export type CreateClusterRequest = GeneratedCreateClusterRequest; -export type UpdateClusterRequest = GeneratedUpdateClusterRequest; - -export type RegistryResponse = GeneratedRegistryResponse; -export type CreateRegistryRequest = GeneratedCreateRegistryRequest; -export type UpdateRegistryRequest = GeneratedUpdateRegistryRequest; -export type RegistryHealthResponse = GeneratedRegistryHealthResponse; - -export type InstanceResponse = GeneratedInstanceResponse; -export type CreateInstanceRequest = GeneratedCreateInstanceRequest; -export type UpdateInstanceRequest = GeneratedUpdateInstanceRequest; -export type InstanceEntry = GeneratedInstanceEntry; -export const INSTANCE_STATUS = GeneratedInstanceStatusEnum; -export type InstanceStatus = NonNullable; -export const INSTANCE_LAST_OPERATION = GeneratedInstanceLastOperationEnum; -export type InstanceLastOperation = NonNullable; - -export type ArtifactResponse = GeneratedArtifactResponse; -export type ArtifactListItem = GeneratedTagResponse; -export type ListRepositories200Item = - | { - name?: string; - artifact_count?: number; - artifactCount?: number; - } - | string; -export type RepositoryListResponse = GeneratedRepositoryListResponse; -export type ListArtifactsFilter = 'all' | 'chart' | 'image' | 'other'; - -export type ClusterMonitoring = GeneratedClusterMetricsResponse; -export type ClusterMonitoringStatus = ClusterMonitoring['status']; -export type MonitoringSummary = GeneratedMonitoringSummary; -export type NodeMetricsResponse = GeneratedNodeMetricsResponse; - -// ---------- Friendly function aliases ---------- -export const login = postAuthLogin; -export const register = postAuthRegister; -export const refreshAuth = postAuthRefresh; - -export const listClusters = getClusters; -export const createCluster = postClusters; -export const getCluster = getClustersClusterId; -export const updateCluster = putClustersClusterId; -export const deleteCluster = deleteClustersClusterId; -export const getClusterHealth = getClustersClusterIdHealth; - -export const listInstances = getClustersClusterIdInstances; -export const createInstance = postClustersClusterIdInstances; -export const getInstance = getClustersClusterIdInstancesInstanceId; -export const updateInstance = putClustersClusterIdInstancesInstanceId; -export const deleteInstance = deleteClustersClusterIdInstancesInstanceId; -export const listInstanceEntries = getClustersClusterIdInstancesInstanceIdEntries; - -export const listRegistries = getRegistries; -export const createRegistry = postRegistries; -export const getRegistry = getRegistriesRegistryId; -export const updateRegistry = putRegistriesRegistryId; -export const deleteRegistry = deleteRegistriesRegistryId; -export const checkRegistryHealth = getRegistriesRegistryIdHealth; - -export const listRepositories = getRegistriesRegistryIdRepositories; -type ListArtifactsRequestOptions = AxiosOptions; - -export const listArtifacts = ( - params: GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsPathParameters, - options?: { filter?: ListArtifactsFilter }, - axiosOptions?: ListArtifactsRequestOptions, -) => { - const query = - options?.filter && options.filter !== 'all' - ? { media_type: options.filter } - : undefined; - return getRegistriesRegistryIdRepositoriesRepositoryNameArtifacts(params, query, axiosOptions); -}; - -export const getArtifact = getRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReference; -export const getValuesSchema = getRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferenceValuesSchema; - -export const listClusterMonitoring = getMonitoringClusters; -export const getClusterMonitoring = getMonitoringClustersClusterId; -export const getClusterNodeMetrics = getMonitoringClustersClusterIdNodes; -export const getMonitoringSummaryData = getMonitoringSummary; - -// Re-export parameter types with friendly names for caller convenience -export type DeleteClusterPathParameters = DeleteClustersClusterIdPathParameters; -export type UpdateClusterPathParameters = PutClustersClusterIdPathParameters; -export type ClusterInstancesPathParameters = GetClustersClusterIdInstancesPathParameters; -export type InstancePathParameters = GetClustersClusterIdInstancesInstanceIdPathParameters; -export type UpdateInstancePathParameters = PutClustersClusterIdInstancesInstanceIdPathParameters; -export type DeleteInstancePathParameters = DeleteClustersClusterIdInstancesInstanceIdPathParameters; -export type RegistryPathParameters = GetRegistriesRegistryIdPathParameters; -export type UpdateRegistryPathParameters = PutRegistriesRegistryIdPathParameters; -export type DeleteRegistryPathParameters = DeleteRegistriesRegistryIdPathParameters; -export type RegistryHealthPathParameters = GetRegistriesRegistryIdHealthPathParameters; -export type ListRepositoriesPathParameters = GetRegistriesRegistryIdRepositoriesPathParameters; -export type ListArtifactsPathParameters = GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsPathParameters; -export type ListArtifactsParams = { filter?: ListArtifactsFilter }; -export type GetArtifactPathParameters = GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferencePathParameters; -export type GetValuesSchemaPathParameters = GetRegistriesRegistryIdRepositoriesRepositoryNameArtifactsReferenceValuesSchemaPathParameters; diff --git a/frontend/src/app/App.tsx b/frontend/src/app/App.tsx deleted file mode 100644 index c24d891..0000000 --- a/frontend/src/app/App.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Main Application Component - * 主应用组件 - */ - -import { useLocation, useNavigate } from "react-router-dom"; -import { useMemo } from "react"; -import { AppRoutes } from "./routes/AppRoutes"; -import { useAuth } from "./providers"; -import { getNavItems } from "./constants/navigation"; - -/** - * Application root component - * Manages routing and global state - */ -export default function App() { - const location = useLocation(); - const navigate = useNavigate(); - const { isAuthenticated, login, logout } = useAuth(); - - // Generate navigation items based on current location - const navItems = useMemo( - () => getNavItems(location.pathname, navigate), - [location.pathname, navigate] - ); - - return ( - - ); -} - - diff --git a/frontend/src/app/admin/users/page.tsx b/frontend/src/app/admin/users/page.tsx new file mode 100644 index 0000000..f0af7fc --- /dev/null +++ b/frontend/src/app/admin/users/page.tsx @@ -0,0 +1,334 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useAuth } from '@/lib/auth-context'; +import { adminApi, workspaceApi } from '@/lib/api'; +import type { UserDTO, WorkspaceDTO } from '@/lib/types'; +import { Users, Plus, Trash2, Edit, Shield, ShieldOff } from 'lucide-react'; + +export default function UsersManagementPage() { + const { user: currentUser } = useAuth(); + const [users, setUsers] = useState([]); + const [workspaces, setWorkspaces] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [showForm, setShowForm] = useState(false); + const [editingUser, setEditingUser] = useState(null); + const [formData, setFormData] = useState({ + username: '', + password: '', + email: '', + role: 'user', + workspace_id: '', + }); + + const fetchData = async () => { + try { + const [usersRes, workspacesRes] = await Promise.all([ + adminApi.listUsers(), + workspaceApi.list(), + ]); + setUsers(usersRes.data.users || []); + setWorkspaces(workspacesRes.data.workspaces || []); + } catch (error) { + console.error('Failed to fetch data:', error); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchData(); + }, []); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + if (editingUser) { + await adminApi.updateUser(editingUser.id, { + email: formData.email || undefined, + }); + } else { + await adminApi.createUser({ + username: formData.username, + password: formData.password, + email: formData.email || undefined, + role: formData.role, + workspace_id: formData.workspace_id || undefined, + }); + } + setShowForm(false); + setEditingUser(null); + setFormData({ username: '', password: '', email: '', role: 'user', workspace_id: '' }); + fetchData(); + } catch (error) { + console.error('Failed to save user:', error); + alert('Failed to save user'); + } + }; + + const handleEdit = (user: UserDTO) => { + setEditingUser(user); + setFormData({ + username: user.username, + password: '', + email: user.email || '', + role: user.role, + workspace_id: user.workspace_id || '', + }); + setShowForm(true); + }; + + const handleDelete = async (userId: string) => { + if (!confirm('Are you sure you want to delete this user?')) return; + try { + await adminApi.deleteUser(userId); + fetchData(); + } catch (error) { + console.error('Failed to delete user:', error); + alert('Failed to delete user'); + } + }; + + const handleToggleActive = async (user: UserDTO) => { + try { + await adminApi.setUserActive(user.id, !user.is_active); + fetchData(); + } catch (error) { + console.error('Failed to toggle user status:', error); + alert('Failed to toggle user status'); + } + }; + + const handleResetPassword = async (userId: string) => { + const newPassword = prompt('Enter new password:'); + if (!newPassword || newPassword.length < 6) { + alert('Password must be at least 6 characters'); + return; + } + try { + await adminApi.resetPassword(userId, newPassword); + alert('Password reset successfully'); + } catch (error) { + console.error('Failed to reset password:', error); + alert('Failed to reset password'); + } + }; + + if (currentUser?.role !== 'admin') { + return ( +
+

Access denied. Admin only.

+
+ ); + } + + if (isLoading) { + return ( +
+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+

User Management

+

Manage users and permissions

+
+ +
+ + {/* Form Modal */} + {showForm && ( +
+
+

+ {editingUser ? 'Edit User' : 'Add User'} +

+
+
+ + setFormData({ ...formData, username: e.target.value })} + className="input" + required + disabled={!!editingUser} + /> +
+ {!editingUser && ( +
+ + setFormData({ ...formData, password: e.target.value })} + className="input" + required={!editingUser} + minLength={6} + /> +
+ )} +
+ + setFormData({ ...formData, email: e.target.value })} + className="input" + /> +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ )} + + {/* Users Table */} + {users.length === 0 ? ( +
+ +

No users created

+ +
+ ) : ( +
+ + + + + + + + + + + + + {users.map((user) => ( + + + + + + + + + ))} + +
UserEmailRoleWorkspaceStatusActions
+
+
+ + {user.username.charAt(0).toUpperCase()} + +
+ {user.username} +
+
{user.email || '-'} + + {user.role} + + + {user.workspace_name || user.workspace_id || '-'} + + + {user.is_active ? 'Active' : 'Inactive'} + + +
+ + + + +
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/app/admin/workspaces/page.tsx b/frontend/src/app/admin/workspaces/page.tsx new file mode 100644 index 0000000..40ea96e --- /dev/null +++ b/frontend/src/app/admin/workspaces/page.tsx @@ -0,0 +1,401 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useAuth } from '@/lib/auth-context'; +import { workspaceApi } from '@/lib/api'; +import type { WorkspaceDTO, QuotaDTO, CreateWorkspaceRequest, SetQuotasRequest } from '@/lib/types'; +import { FolderKanban, Plus, Trash2, Edit, Settings } from 'lucide-react'; + +export default function WorkspacesPage() { + const { user } = useAuth(); + const [workspaces, setWorkspaces] = useState([]); + const [quotas, setQuotas] = useState>({}); + const [isLoading, setIsLoading] = useState(true); + const [showForm, setShowForm] = useState(false); + const [showQuotaForm, setShowQuotaForm] = useState(false); + const [editingWorkspace, setEditingWorkspace] = useState(null); + const [selectedWorkspace, setSelectedWorkspace] = useState(null); + const [formData, setFormData] = useState({ + name: '', + description: '', + }); + const [quotaFormData, setQuotaFormData] = useState({ + cpu: { hard_limit: 10, soft_limit: 8 }, + gpu: { hard_limit: 2, soft_limit: 1 }, + gpu_memory: { hard_limit: 16, soft_limit: 8 }, + }); + + const fetchWorkspaces = async () => { + try { + const response = await workspaceApi.list(); + setWorkspaces(response.data.workspaces || []); + } catch (error) { + console.error('Failed to fetch workspaces:', error); + } finally { + setIsLoading(false); + } + }; + + const fetchQuotas = async (workspaceId: string) => { + try { + const response = await workspaceApi.getQuotas(workspaceId); + setQuotas((prev) => ({ ...prev, [workspaceId]: response.data.quotas || [] })); + } catch (error) { + console.error('Failed to fetch quotas:', error); + } + }; + + useEffect(() => { + fetchWorkspaces(); + }, []); + + useEffect(() => { + workspaces.forEach((ws) => { + if (!quotas[ws.id]) { + fetchQuotas(ws.id); + } + }); + }, [workspaces]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + if (editingWorkspace) { + await workspaceApi.update(editingWorkspace.id, formData); + } else { + await workspaceApi.create(formData); + } + setShowForm(false); + setEditingWorkspace(null); + setFormData({ name: '', description: '' }); + fetchWorkspaces(); + } catch (error) { + console.error('Failed to save workspace:', error); + alert('Failed to save workspace'); + } + }; + + const handleEdit = (workspace: WorkspaceDTO) => { + setEditingWorkspace(workspace); + setFormData({ name: workspace.name, description: workspace.description || '' }); + setShowForm(true); + }; + + const handleDelete = async (workspaceId: string) => { + if (!confirm('Are you sure you want to delete this workspace?')) return; + try { + await workspaceApi.delete(workspaceId); + fetchWorkspaces(); + } catch (error) { + console.error('Failed to delete workspace:', error); + alert('Failed to delete workspace'); + } + }; + + const handleSaveQuotas = async () => { + if (!selectedWorkspace) return; + try { + await workspaceApi.setQuotas(selectedWorkspace.id, quotaFormData); + setShowQuotaForm(false); + fetchQuotas(selectedWorkspace.id); + } catch (error) { + console.error('Failed to save quotas:', error); + alert('Failed to save quotas'); + } + }; + + if (user?.role !== 'admin') { + return ( +
+

Access denied. Admin only.

+
+ ); + } + + if (isLoading) { + return ( +
+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+

Workspaces

+

Manage workspaces and quotas

+
+ +
+ + {/* Form Modal */} + {showForm && ( +
+
+

+ {editingWorkspace ? 'Edit Workspace' : 'Add Workspace'} +

+
+
+ + setFormData({ ...formData, name: e.target.value })} + className="input" + required + /> +
+
+ +