diff --git a/.cursor/rules/ai-read.mdc b/.cursor/rules/ai-read.mdc index 5929781..a7a7f08 100644 --- a/.cursor/rules/ai-read.mdc +++ b/.cursor/rules/ai-read.mdc @@ -37,6 +37,12 @@ alwaysApply: true - 使用 pinia element-plus typescript vite Axios dayjs vueuse - Using dynamic import() to code-split the application +### 最新架构说明(2025-11-29) +- 旧版 UI 迁移到 `vue-legacy/`,新 `vue/` 目录仅保留:仪表板、全部邮件、转发管理、系统设置、调试模式、统一登录页,继续沿用动态加载与统一刷新机制。 +- 项目切换到 **单管理员模式**:系统中只存在一个管理员账号,用户注册、邮箱申请/私信、用户管理、安全概览等功能全部移除。 +- `/api/auth/register`、`/api/admin/**`、`/api/mailboxes/**`、`/api/user-info/**`、`/api/security-audit/**` 等接口均已下线,请以 `api/api-doc.yml` 最新版为准。 +- 登录页仍根据 `allow_registration` 字段展示提示,但该字段只读,用于未来是否重新开放注册的判断;后端始终拒绝注册请求。 + ### 📋 开发规则 1. **数据库操作** - 无需复杂迁移,直接修改 `db/schema.sql` 后执行 `npm run db:init` @@ -57,9 +63,9 @@ alwaysApply: true ### 🚀 开发环境 #### 环境配置 -- **前端**:使用 Docker 容器运行,通过 `docker-compose up -d` 启动 -- **后端**:使用 Docker 容器运行,通过 `docker-compose up -d` 启动 -- **数据库**:通过 Docker 容器执行数据库操作,避免字符编码问题 +- **前端**:在宿主机运行 `cd vue && npm run dev` +- **后端**:在宿主机运行 `cd worker && npx wrangler dev` +- **数据库**:直接使用 `wrangler d1` 命令执行操作 #### 目录结构 @@ -82,7 +88,6 @@ alwaysApply: true │ ├── deploy.js 完整部署脚本 │ ├── dev.js 开发环境启动 │ └── db.js 数据库操作脚本 -└── docker-compose.yml Docker 配置 ``` ### 前端目录结构介绍 @@ -94,16 +99,13 @@ MainLayoutView负责 登录后的页面展示 #### 开发命令 ```bash -# 启动开发环境 -docker-compose up -d - -# 查看容器状态 -docker-compose ps +# 启动前端 +cd vue && npm run dev -# 停止容器 -docker-compose down +# 启动 Worker +cd worker && npx wrangler dev -# 数据库操作(通过 Docker 容器执行) +# 数据库操作 npm run db:init # 初始化数据库 npm run db:init:remote # 初始化远程数据库 npm run db:migrate # 执行数据库迁移 @@ -117,11 +119,11 @@ npm run clean # 清理所有资源 #### 数据库操作 -数据库操作通过 Docker 容器执行: -- 使用 `docker exec -it worker zsh -c "node scripts/db.js 'SELECT * FROM users'"` 执行 SQL 命令 -- 使用 `docker exec -it worker zsh -c "node scripts/db.js U0VMRUNUICogRlJPTSB1c2Vycwo="` 执行 base64 编码的 SQL -- 使用 `docker exec -it worker zsh -c "node scripts/db.js init"` 初始化数据库 -- 使用 `docker exec -it worker zsh -c "node scripts/db.js init --remote"` 初始化远程数据库 +数据库操作直接通过 npm 脚本执行: +- 使用 `npm run db -- "SELECT * FROM users"` 执行 SQL 命令 +- 使用 `npm run db -- U0VMRUNUICogRlJPTSB1c2Vycwo=` 执行 base64 编码的 SQL +- 使用 `npm run db:init` 初始化数据库 +- 使用 `npm run db:init:remote` 初始化远程数据库 - 所有数据库操作都通过修改 `db/schema.sql` 完成,无需额外迁移脚本 #### 故障排除 diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 496d630..0000000 --- a/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM node:slim - -# 强制指定 npm 安装路径 -RUN npm config set registry https://registry.npmmirror.com \ - && npm i -g wrangler - -RUN apt-get -qq update \ - && apt-get -qq install zsh curl git procps net-tools -y \ - && apt-get -qq autoclean \ - && rm -rf /var/lib/apt/lists/* - -# 安装 zsh 和插件 -RUN zsh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" \ - && git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting \ - && git clone https://github.com/zsh-users/zsh-autosuggestions.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions \ - && sed -i "s/plugins=(git)/plugins=(git zsh-syntax-highlighting zsh-autosuggestions docker kubectl brew golang history nmap node npm pip pipenv pyenv pylint python screen sublime)/g" ~/.zshrc - -EXPOSE 8787 5173 -CMD [] -ENTRYPOINT ["/usr/bin/zsh"] - -# docker build -t node:cem --progress=plain . diff --git a/api/api-doc.yml b/api/api-doc.yml index 199ebac..a5c246f 100644 --- a/api/api-doc.yml +++ b/api/api-doc.yml @@ -1,50 +1,14 @@ openapi: 3.0.3 info: title: Cloudflare 邮箱管理系统 API + version: 3.0.0 description: | - 基于 Cloudflare Workers 的邮箱管理系统,提供完整的邮件接收、存储、发送、转发和管理功能。 + Cloudflare Email Manager (CEM) 的统一 API 规范。系统已精简为 **单管理员模式**,不再开放多用户注册与邮箱申请,所有管理操作均由管理员完成。 - ## 核心设计 - - 所有资源接口保持统一路径 - - 管理员与普通用户通过后端鉴权区分,不再分叉路径 - - 错误响应、分页参数统一复用 - - 邮件发送接口统一 `/api/emails/send` - - 邮件查询支持 `scope` 参数控制(all/user) - - 转发规则查询支持 `scope` 参数控制(all/user) - - ### 邮件 - ```http - GET /api/emails # 获取邮件(scope=all/user) - GET /api/emails/{id} # 获取详情 - DELETE /api/emails/{id} # 删除邮件 - GET /api/emails/{id}/attachments/{attachmentId} # 下载附件 - POST /api/emails/send # 发送邮件(管理员/用户都能用) - ``` - - ### 转发规则 - ```http - GET /api/forward-rules # 获取转发规则(scope=all/user) - GET /api/forward-rules/{id} # 获取详情 - POST /api/forward-rules # 创建规则 - PUT /api/forward-rules/{id} # 更新规则 - DELETE /api/forward-rules/{id} # 删除规则 - ``` - - ## 核心逻辑 - - 资源路径不区分角色 → 统一 /api/emails, /api/forward-rules - - 角色差异用参数控制 → scope=all/user - - 业务差异前端封装 → send-info 只是 send 的一层包装 - - version: 2.0.0 - contact: - name: XRSec - email: admin@example.com - license: - name: MIT - url: https://opensource.org/licenses/MIT + - 所有接口前缀统一为 `/api` + - 统一使用 HttpOnly Cookie 维持会话 + - localStorage 仅缓存 UI 状态,业务数据一律来自接口 + - `allow_registration` 字段在健康检查与系统配置中继续返回,用于未来扩展(当前永远为只读) servers: - url: http://localhost:8787 @@ -53,49 +17,30 @@ servers: description: 生产环境 tags: - - name: 认证 - description: 用户登录、注册、登出等认证相关操作 - - name: 用户 - description: 用户信息管理 - - name: 邮件 - description: 邮件查看、发送、删除、附件下载等操作 - - name: 邮箱 - description: 邮箱申请、管理、审核等功能 - - name: 转发规则 - description: 邮件转发与 Webhook 管理 - - name: 系统 - description: 系统配置与健康检查 - - name: 调试 - description: 调试和开发相关功能 + - name: Auth + description: 登录 / 登出 + - name: User + description: 管理员个人信息维护 + - name: Emails + description: 邮件收取、查看、删除、附件下载 + - name: ForwardRules + description: 全局转发与 webhook 管理 + - name: System + description: 健康检查与系统配置 + - name: Database + description: 调试模式下的数据库辅助接口 paths: - # ==================== 认证 ==================== /api/auth/login: post: - tags: - - 认证 - summary: 用户登录 - description: 使用用户名和密码进行登录,返回 JWT token。登录成功后需要调用 `/api/users/me` 获取用户详细信息。 + tags: [Auth] + summary: 管理员登录 requestBody: required: true content: application/json: schema: - type: object - required: - - username - - password - properties: - username: - type: string - minLength: 3 - example: admin - description: 用户名 - password: - type: string - minLength: 6 - description: 邮箱密码 - example: 1S*Etuh1pjM$7g + $ref: '#/components/schemas/LoginRequest' responses: '200': description: 登录成功 @@ -103,50 +48,12 @@ paths: application/json: schema: $ref: '#/components/schemas/LoginResponse' - examples: - success: - summary: 登录成功示例 - value: - success: true - message: "登录成功" - data: - token: "xxxxx.xxxx.xxx" - user: - id: 1 - username: "admin" - user_type: "admin" - webhook_url: null - created_at: "2025-09-12 18:03:08" - '401': - description: 认证失败 - content: - application/json: - schema: - type: object - properties: - success: - type: boolean - example: false - message: - type: string - example: 用户不存在或密码错误 - examples: - wrong_password: - summary: 密码错误 - value: - success: false - message: "用户不存在或密码错误" - user_not_found: - summary: 用户不存在 - value: - success: false - message: "用户不存在或密码错误" + '401': { $ref: '#/components/responses/UnauthorizedError' } /api/auth/logout: post: - tags: - - 认证 - summary: 用户登出 + tags: [Auth] + summary: 退出登录 security: - cookieAuth: [] responses: @@ -155,113 +62,25 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/SuccessResponse' - examples: - success: - summary: 登出成功示例 - value: - success: true - message: "登出成功" - '401': - $ref: '#/components/responses/UnauthorizedError' - - /api/auth/register: - post: - tags: - - 认证 - summary: 用户注册 - description: 注册新用户 - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - username - - password - - email - properties: - username: - type: string - minLength: 3 - example: admin - description: 用户名 - password: - type: string - minLength: 6 - description: 邮箱密码 - example: 1S*Etuh1pjM$7g - email: - type: string - format: email - responses: - '201': - description: 注册成功 - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - '400': - description: 请求参数错误 - '403': - description: 系统不允许注册 - '409': - description: 用户已存在 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' + $ref: '#/components/schemas/BasicResponse' - # ==================== 用户 ==================== /api/users/me: get: - tags: - - 用户 - summary: 获取当前用户信息 - description: 获取当前用户信息 以及设置信息 + tags: [User] + summary: 获取管理员信息 security: - cookieAuth: [] responses: '200': - description: 获取成功 + description: 当前管理员信息 content: application/json: schema: - $ref: '#/components/schemas/UserProfile' - examples: - admin_user: - summary: 管理员用户信息 - value: - id: 1 - username: "admin" - email: "admin@example.com" - user_type: "admin" - created_at: "2025-09-12 18:03:08" - updated_at: "2025-09-12 18:09:44" - settings: - webhook_url: null - webhook_secret: null - normal_user: - summary: 普通用户信息 - value: - id: 2 - username: "user1" - email: "user1@example.com" - user_type: "user" - created_at: "2025-09-12 18:05:00" - updated_at: "2025-09-12 18:05:00" - settings: - webhook_url: "https://hooks.slack.com/services/xxx" - webhook_secret: "secret123" - '401': - $ref: '#/components/responses/UnauthorizedError' - + $ref: '#/components/schemas/UserProfileResponse' + '401': { $ref: '#/components/responses/UnauthorizedError' } put: - tags: - - 用户 - summary: 更新当前用户信息 - description: 包含密码更新、Webhook 设置等 + tags: [User] + summary: 更新管理员信息 security: - cookieAuth: [] requestBody: @@ -276,250 +95,193 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/SuccessResponse' - '400': - description: 请求参数错误 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - - /api/users: - get: - tags: - - 用户 - summary: 获取用户列表(仅管理员) - security: - - cookieAuth: [] - parameters: - - $ref: '#/components/parameters/PageParam' - - $ref: '#/components/parameters/LimitParam' - - name: query - in: query - schema: - type: string - description: 搜索关键词(用户名、邮箱) - responses: - '200': - description: 获取成功 - content: - application/json: - schema: - $ref: '#/components/schemas/UserListResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' - - post: - tags: - - 用户 - summary: 管理员创建用户 - security: - - cookieAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/AdminUserCreateRequest' - responses: - '201': - description: 创建成功 - content: - application/json: - schema: - $ref: '#/components/schemas/UserResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' - - /api/users/{id}: - get: - tags: - - 用户 - summary: 获取指定用户信息(仅管理员) - security: - - cookieAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - responses: - '200': - description: 获取成功 - content: - application/json: - schema: - $ref: '#/components/schemas/UserProfile' - '404': - $ref: '#/components/responses/NotFoundError' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' + $ref: '#/components/schemas/BasicResponse' + '400': { $ref: '#/components/responses/BadRequest' } - delete: - tags: - - 用户 - summary: 删除用户(仅管理员) - security: - - cookieAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - responses: - '200': - description: 删除成功 - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - '404': - $ref: '#/components/responses/NotFoundError' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' - # ==================== 邮件 ==================== /api/emails: get: - tags: - - 邮件 + tags: [Emails] summary: 获取邮件列表 - description: | - - 普通用户:默认只能获取自己的邮件,可使用 scope=user 明确指定 - - 管理员:可以使用 scope=all 获取全部邮件,或 scope=user 获取自己的邮件 security: - cookieAuth: [] parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/LimitParam' - - name: scope - in: query - description: 查询范围,all=所有邮件(需管理员权限),user=当前用户邮件,不传则默认返回当前用户邮件 - required: false - schema: - type: string - enum: [all, user] - name: search in: query - schema: - type: string - description: 搜索关键词 + schema: { type: string } + description: 主题 / 发件人模糊搜索 - name: status in: query - schema: - type: string - enum: [received, processed, forwarded, failed] - description: 邮件状态 + schema: { type: string, enum: ['unread','read'] } + - name: scope + in: query + schema: { type: string, enum: ['all'] } + description: 仅管理员可传 `all` responses: '200': - description: 获取成功 + description: 邮件列表 content: application/json: schema: $ref: '#/components/schemas/EmailListResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' + '401': { $ref: '#/components/responses/UnauthorizedError' } /api/emails/{id}: get: - tags: - - 邮件 + tags: [Emails] summary: 获取邮件详情 security: - cookieAuth: [] parameters: - - name: id - in: path - required: true - schema: - type: string + - $ref: '#/components/parameters/EmailIdParam' responses: '200': - description: 获取成功 + description: 邮件详情 content: application/json: schema: - $ref: '#/components/schemas/EmailDetail' - '404': - $ref: '#/components/responses/NotFoundError' - '401': - $ref: '#/components/responses/UnauthorizedError' - + $ref: '#/components/schemas/EmailDetailResponse' + '404': { $ref: '#/components/responses/NotFoundError' } delete: - tags: - - 邮件 + tags: [Emails] summary: 删除邮件 security: - cookieAuth: [] parameters: - - name: id - in: path - required: true - schema: - type: string + - $ref: '#/components/parameters/EmailIdParam' + responses: + '200': { $ref: '#/components/schemas/BasicResponse' } + '404': { $ref: '#/components/responses/NotFoundError' } + + /api/emails/{id}/raw: + get: + tags: [Emails] + summary: 下载原始邮件(.eml 格式,支持 HTTP 缓存) + description: | + 获取邮件的原始 RFC 822 格式内容,支持完整的 HTTP 缓存机制: + - 返回剔除附件后的完整邮件(包含邮件头和正文) + - **ETag**: 基于邮件 ID 和更新时间生成 + - **Last-Modified**: 邮件的最后修改时间 + - **Cache-Control**: public, max-age=31536000, immutable(缓存 1 年) + - **304 Not Modified**: 当客户端缓存有效时返回 + security: + - cookieAuth: [] + parameters: + - $ref: '#/components/parameters/EmailIdParam' + - name: If-None-Match + in: header + required: false + description: ETag 值,用于条件请求 + schema: { type: string } + - name: If-Modified-Since + in: header + required: false + description: 最后修改时间,用于条件请求 + schema: { type: string } responses: '200': - description: 删除成功 - content: - application/json: + description: 原始邮件内容(RFC 822 格式) + headers: + ETag: + description: 邮件的 ETag 标识 + schema: { type: string } + Last-Modified: + description: 邮件的最后修改时间 + schema: { type: string } + Cache-Control: + description: 缓存策略 + schema: { type: string } + example: 'public, max-age=31536000, immutable' + content: + message/rfc822: schema: - $ref: '#/components/schemas/SuccessResponse' - '404': - $ref: '#/components/responses/NotFoundError' - '401': - $ref: '#/components/responses/UnauthorizedError' + type: string + format: binary + '304': + description: Not Modified(缓存仍然有效) + headers: + ETag: + schema: { type: string } + Last-Modified: + schema: { type: string } + '404': { $ref: '#/components/responses/NotFoundError' } /api/emails/{id}/attachments/{attachmentId}: get: - tags: - - 邮件 - summary: 下载邮件附件 + tags: [Emails] + summary: 下载附件(支持 HTTP 缓存) + description: | + 下载邮件附件,支持完整的 HTTP 缓存机制: + - **ETag**: 基于附件 ID 和更新时间生成 + - **Last-Modified**: 附件的最后修改时间 + - **Cache-Control**: public, max-age=31536000, immutable(缓存 1 年) + - **304 Not Modified**: 当客户端缓存有效时返回 + + **条件请求支持**: + - 客户端可发送 `If-None-Match` 头(ETag 值) + - 客户端可发送 `If-Modified-Since` 头(时间戳) + - 如果缓存有效,服务器返回 304,无响应体 security: - cookieAuth: [] parameters: - - name: id - in: path - required: true - schema: - type: string + - $ref: '#/components/parameters/EmailIdParam' - name: attachmentId in: path required: true - schema: - type: string + schema: { type: integer } + - name: If-None-Match + in: header + required: false + description: ETag 值,用于条件请求 + schema: { type: string } + example: '"abc123"' + - name: If-Modified-Since + in: header + required: false + description: 最后修改时间,用于条件请求 + schema: { type: string } + example: 'Sun, 30 Nov 2025 12:00:00 GMT' responses: '200': - description: 下载成功 - content: - application/octet-stream: - schema: - type: string - format: binary - '404': - $ref: '#/components/responses/NotFoundError' - '401': - $ref: '#/components/responses/UnauthorizedError' + description: 附件文件流(首次请求或缓存失效) + headers: + ETag: + description: 附件的 ETag 标识 + schema: { type: string } + example: '"abc123"' + Last-Modified: + description: 附件的最后修改时间 + schema: { type: string } + example: 'Sun, 30 Nov 2025 12:00:00 GMT' + Cache-Control: + description: 缓存策略 + schema: { type: string } + example: 'public, max-age=31536000, immutable' + Vary: + description: 缓存隔离策略 + schema: { type: string } + example: 'Authorization' + content: + '*/*': {} + '304': + description: Not Modified(缓存仍然有效) + headers: + ETag: + description: 附件的 ETag 标识 + schema: { type: string } + Last-Modified: + description: 附件的最后修改时间 + schema: { type: string } + Cache-Control: + description: 缓存策略 + schema: { type: string } + '404': { $ref: '#/components/responses/NotFoundError' } /api/emails/send: post: - tags: - - 邮件 - summary: 发送邮件 - description: | - 通用发送接口: - - 普通用户:可以发邮件(如果系统允许) - - 管理员:可用来向用户发送账户信息(前端封装即可) + tags: [Emails] + summary: 发送邮件(调试) security: - cookieAuth: [] requestBody: @@ -527,151 +289,31 @@ paths: content: application/json: schema: - type: object - required: - - to - - subject - - content - properties: - to: - type: string - format: email - description: 收件人邮箱 - from: - type: string - format: email - description: 发件人邮箱(不传默认使用系统邮箱) - subject: - type: string - description: 邮件主题 - content: - type: string - description: 邮件正文 - content_type: - type: string - enum: [text, html, markdown] - default: markdown + $ref: '#/components/schemas/SendEmailRequest' responses: - '200': - description: 发送成功 - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - '400': - description: 请求参数错误 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - # ==================== 邮箱 ==================== - /api/mailboxes: + '200': { $ref: '#/components/schemas/BasicResponse' } + '400': { $ref: '#/components/responses/BadRequest' } + + /api/forward-rules: get: - tags: - - 邮箱 - summary: 获取邮箱列表 - description: | - - 默认行为:始终返回当前用户的邮箱 - - 当 scope=all 时:需要管理员权限,返回所有邮箱 - - 当 scope=user 时:明确指定返回当前用户的邮箱 - - 支持分页查询 + tags: [ForwardRules] + summary: 获取转发规则 security: - cookieAuth: [] parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/LimitParam' - - name: scope - in: query - description: 查询范围,all=所有邮箱(需管理员权限),user=当前用户邮箱,不传则默认返回当前用户邮箱 - required: false - schema: - type: string - enum: [all, user] - example: all responses: '200': - description: 获取成功 - content: - application/json: - schema: - $ref: '#/components/schemas/MailboxListResponse' - examples: - user_mailboxes: - summary: 普通用户邮箱列表 - description: 普通用户只能看到自己的邮箱 - value: - total: 2 - items: - - id: 1 - owner_id: 2 - owner_username: "user1" - owner_usertype: 0 - address: "user1@example.com" - status: "active" - created_at: "2025-09-12 18:05:00" - updated_at: "2025-09-12 18:05:00" - - id: 2 - owner_id: 2 - owner_username: "user1" - owner_usertype: 0 - address: "test@example.com" - status: "active" - created_at: "2025-09-12 18:06:00" - updated_at: "2025-09-12 18:06:00" - admin_mailboxes: - summary: 管理员查看所有邮箱 - description: 管理员需要看到所有用户的邮箱信息 - value: - total: 5 - items: - - id: 1 - owner_id: 1 - owner_username: "admin" - owner_usertype: 1 - address: "admin@example.com" - status: "active" - created_at: "2025-09-12 18:03:08" - updated_at: "2025-09-12 18:03:08" - - id: 2 - owner_id: 2 - owner_username: "user1" - owner_usertype: 0 - address: "user1@example.com" - status: "active" - created_at: "2025-09-12 18:05:00" - updated_at: "2025-09-12 18:05:00" - - id: 3 - owner_id: 2 - owner_username: "user1" - owner_usertype: 0 - address: "test@example.com" - status: "disabled" - created_at: "2025-09-12 18:06:00" - updated_at: "2025-09-12 18:06:00" - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - description: 权限不足 + description: 规则列表 content: application/json: schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - insufficient_permission: - summary: 权限不足 - value: - success: false - error: "权限不足,只有管理员可以查看所有邮箱" - + $ref: '#/components/schemas/ForwardRuleListResponse' + '403': { $ref: '#/components/responses/ForbiddenError' } post: - tags: - - 邮箱 - summary: 创建邮箱 - description: | - - 普通用户:提交申请 - - 管理员:直接创建邮箱 + tags: [ForwardRules] + summary: 创建转发规则 security: - cookieAuth: [] requestBody: @@ -679,190 +321,133 @@ paths: content: application/json: schema: - oneOf: - - $ref: '#/components/schemas/MailboxApplicationRequest' - - $ref: '#/components/schemas/AdminMailboxCreateRequest' + $ref: '#/components/schemas/ForwardRuleInput' responses: - '201': - description: 创建/申请成功 - content: - application/json: - schema: - oneOf: - - $ref: '#/components/schemas/MailboxApplicationResponse' - - $ref: '#/components/schemas/MailboxResponse' - '400': - description: 请求参数错误 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' + '200': { $ref: '#/components/schemas/ForwardRuleResponse' } + '400': { $ref: '#/components/responses/BadRequest' } - /api/mailboxes/{id}: + /api/forward-rules/{id}: get: - tags: - - 邮箱 - summary: 获取邮箱详情 + tags: [ForwardRules] + summary: 规则详情 security: - cookieAuth: [] parameters: - - name: id - in: path - required: true - schema: - type: integer + - $ref: '#/components/parameters/ForwardRuleIdParam' responses: - '200': - description: 获取成功 - content: - application/json: - schema: - $ref: '#/components/schemas/Mailbox' - '404': - $ref: '#/components/responses/NotFoundError' - '401': - $ref: '#/components/responses/UnauthorizedError' - + '200': { $ref: '#/components/schemas/ForwardRuleResponse' } + '404': { $ref: '#/components/responses/NotFoundError' } + put: + tags: [ForwardRules] + summary: 更新规则 + security: + - cookieAuth: [] + parameters: + - $ref: '#/components/parameters/ForwardRuleIdParam' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ForwardRuleInput' + responses: + '200': { $ref: '#/components/schemas/BasicResponse' } + '404': { $ref: '#/components/responses/NotFoundError' } delete: - tags: - - 邮箱 - summary: 删除邮箱 - description: | - - 普通用户:只能删除自己的邮箱(不能删除默认邮箱) - - 管理员:可以删除任意邮箱 + tags: [ForwardRules] + summary: 删除规则 security: - cookieAuth: [] parameters: - - name: id - in: path - required: true - schema: - type: integer + - $ref: '#/components/parameters/ForwardRuleIdParam' + responses: + '200': { $ref: '#/components/schemas/BasicResponse' } + '404': { $ref: '#/components/responses/NotFoundError' } + + /api/system/health: + get: + tags: [System] + summary: 系统健康检查(公开) + description: 返回数据库 / R2 / KV 状态以及 `allow_registration` 只读标志。 responses: '200': - description: 删除成功 - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - '400': - description: 不能删除默认邮箱 + description: 健康信息 content: application/json: schema: - $ref: '#/components/schemas/ErrorResponse' - '404': - $ref: '#/components/responses/NotFoundError' - '401': - $ref: '#/components/responses/UnauthorizedError' + $ref: '#/components/schemas/SystemHealthResponse' - /api/mailboxes/applications: + /api/system/registration-status: get: - tags: - - 邮箱 - summary: 获取邮箱申请列表 - description: | - - 普通用户:查看自己的申请记录 - - 管理员:查看所有申请(可分页) - security: - - cookieAuth: [] - parameters: - - $ref: '#/components/parameters/PageParam' - - $ref: '#/components/parameters/LimitParam' + tags: [System] + summary: 是否允许注册(只读) responses: '200': - description: 获取成功 + description: 当前注册策略 content: application/json: schema: - $ref: '#/components/schemas/MailboxApplicationListResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' + $ref: '#/components/schemas/RegistrationStatusResponse' - /api/mailboxes/applications/{id}/process: - post: - tags: - - 邮箱 - summary: 处理邮箱申请(仅管理员) + /api/system/config: + get: + tags: [System] + summary: 获取系统配置 + security: + - cookieAuth: [] + responses: + '200': { $ref: '#/components/schemas/SystemConfigResponse' } + put: + tags: [System] + summary: 更新系统配置 + description: 仅管理员可调用;`allow_registration` 为只读,保留未来开关。 security: - cookieAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/ApplicationProcessRequest' + $ref: '#/components/schemas/SystemConfigUpdate' responses: - '200': - description: 处理成功 - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - '400': - description: 请求参数错误 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '404': - $ref: '#/components/responses/NotFoundError' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' - # ==================== 转发规则 ==================== - /api/forward-rules: + '200': { $ref: '#/components/schemas/BasicResponse' } + + /api/database/info: get: - tags: - - 转发规则 - summary: 获取转发规则列表 - description: | - - 普通用户:仅返回自己的规则,可使用 scope=user 明确指定 - - 管理员:可以返回全部规则(scope=all)或自己的规则(scope=user) - - 通过 scope 参数控制查询范围 + tags: [Database] + summary: 获取 SQLite 基础信息 + security: + - cookieAuth: [] + responses: + '200': { $ref: '#/components/schemas/DatabaseInfoResponse' } + + /api/database/tables: + get: + tags: [Database] + summary: 查看各表最近数据 security: - cookieAuth: [] - parameters: - - $ref: '#/components/parameters/PageParam' - - $ref: '#/components/parameters/LimitParam' - - name: scope - in: query - description: 查询范围,all=所有规则(需管理员权限),user=当前用户规则,不传则默认返回当前用户规则 - required: false - schema: - type: string - enum: [all, user] - example: all - - name: search - in: query - schema: - type: string - description: 搜索关键词(规则名称、描述) responses: '200': - description: 获取成功 + description: 各数据表结构 / 最新记录 content: application/json: schema: - $ref: '#/components/schemas/ForwardRuleListResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' + $ref: '#/components/schemas/DatabaseTablesResponse' + + /api/database/stats: + get: + tags: [Database] + summary: 数据库统计 + security: + - cookieAuth: [] + responses: + '200': { $ref: '#/components/schemas/DatabaseStatsResponse' } + /api/database/init: post: - tags: - - 转发规则 - summary: 创建转发规则 + tags: [Database] + summary: 初始化数据库(危险操作) security: - cookieAuth: [] requestBody: @@ -870,534 +455,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ForwardRuleCreateRequest' + type: object + properties: + confirmText: + type: string responses: - '201': - description: 创建成功 - content: - application/json: - schema: - $ref: '#/components/schemas/ForwardRuleResponse' - '400': - description: 请求参数错误 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - - /api/forward-rules/{id}: - get: - tags: - - 转发规则 - summary: 获取转发规则详情 - security: - - cookieAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - responses: - '200': - description: 获取成功 - content: - application/json: - schema: - $ref: '#/components/schemas/ForwardRuleResponse' - '404': - $ref: '#/components/responses/NotFoundError' - '401': - $ref: '#/components/responses/UnauthorizedError' - - put: - tags: - - 转发规则 - summary: 更新转发规则 - security: - - cookieAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/ForwardRuleUpdateRequest' - responses: - '200': - description: 更新成功 - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - '400': - description: 请求参数错误 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '404': - $ref: '#/components/responses/NotFoundError' - '401': - $ref: '#/components/responses/UnauthorizedError' - - delete: - tags: - - 转发规则 - summary: 删除转发规则 - security: - - cookieAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - responses: - '200': - description: 删除成功 - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - '404': - $ref: '#/components/responses/NotFoundError' - '401': - $ref: '#/components/responses/UnauthorizedError' - # ==================== 邮箱历史 ==================== - /api/mailbox-history/{mailboxId}: - get: - tags: - - 邮箱历史 - summary: 获取指定邮箱的历史记录 - description: 获取指定邮箱的所有操作历史记录 - security: - - cookieAuth: [] - parameters: - - name: mailboxId - in: path - required: true - schema: - type: integer - description: 邮箱ID - - $ref: '#/components/parameters/PageParam' - - $ref: '#/components/parameters/LimitParam' - responses: - '200': - description: 获取成功 - content: - application/json: - schema: - $ref: '#/components/schemas/MailboxHistoryListResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - '404': - $ref: '#/components/responses/NotFoundError' - - /api/mailbox-history/user/me: - get: - tags: - - 邮箱历史 - summary: 获取当前用户的邮箱历史记录 - description: 获取当前用户所有邮箱的操作历史记录 - security: - - cookieAuth: [] - parameters: - - $ref: '#/components/parameters/PageParam' - - $ref: '#/components/parameters/LimitParam' - responses: - '200': - description: 获取成功 - content: - application/json: - schema: - $ref: '#/components/schemas/UserMailboxHistoryListResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - - /api/mailbox-history/admin/all: - get: - tags: - - 邮箱历史 - summary: 获取所有邮箱历史记录(仅管理员) - description: 获取系统中所有邮箱的操作历史记录 - security: - - cookieAuth: [] - parameters: - - $ref: '#/components/parameters/PageParam' - - $ref: '#/components/parameters/LimitParam' - responses: - '200': - description: 获取成功 - content: - application/json: - schema: - $ref: '#/components/schemas/UserMailboxHistoryListResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' - - # ==================== 用户信息 ==================== - /api/user-info/username/{userId}: - get: - tags: - - 用户信息 - summary: 根据用户ID获取用户名 - description: 根据用户ID获取用户名和用户类型信息 - security: - - cookieAuth: [] - parameters: - - name: userId - in: path - required: true - schema: - type: integer - description: 用户ID - responses: - '200': - description: 获取成功 - content: - application/json: - schema: - type: object - properties: - success: - type: boolean - data: - type: object - properties: - id: - type: integer - username: - type: string - user_type: - type: string - '404': - description: 用户不存在 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - - /api/user-info/usernames: - post: - tags: - - 用户信息 - summary: 批量获取用户名 - description: 根据用户ID列表批量获取用户名信息 - security: - - cookieAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - user_ids: - type: array - items: - type: integer - description: 用户ID列表 - responses: - '200': - description: 获取成功 - content: - application/json: - schema: - type: object - properties: - success: - type: boolean - data: - type: object - additionalProperties: - type: object - properties: - id: - type: integer - username: - type: string - user_type: - type: string - '400': - description: 请求参数错误 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - - # ==================== 系统 ==================== - /api/system/registration-status: - get: - tags: - - 系统 - summary: 获取注册状态 - description: 查询系统是否允许新用户注册 - responses: - '200': - description: 获取成功 - content: - application/json: - schema: - type: object - properties: - success: - type: boolean - example: true - data: - type: object - properties: - allow_registration: - type: boolean - description: 是否允许注册 - example: true - '500': - $ref: '#/components/responses/InternalServerError' - - /api/system/health: - get: - tags: - - 系统 - summary: 获取系统健康状态 - description: 获取无需认证的系统健康状态,包括系统状态、健康检查、配置信息等,使用数字格式 - responses: - '200': - description: 获取成功 - content: - application/json: - schema: - type: object - properties: - success: - type: boolean - example: true - data: - type: object - properties: - health: - type: object - properties: - status: - type: integer - description: 系统状态 (1=健康, 0=不健康) - example: 1 - timestamp: - type: string - format: date-time - description: 检查时间 - example: "2024-01-01T00:00:00.000Z" - services: - type: object - properties: - database: - type: object - properties: - status: - type: integer - description: 数据库状态 (1=正常, 0=异常) - example: 1 - latency_ms: - type: integer - description: 数据库响应延迟(毫秒) - example: 15 - storage: - type: object - properties: - status: - type: integer - description: R2存储状态 (1=正常, 0=异常) - example: 1 - provider: - type: integer - description: 存储提供商 (1=R2, 0=其他) - example: 1 - kv: - type: object - properties: - status: - type: integer - description: KV存储状态 (1=正常, 0=异常) - example: 1 - provider: - type: integer - description: KV提供商 (1=KV, 0=其他) - example: 1 - total_latency_ms: - type: integer - description: 总检查延迟(毫秒) - example: 25 - config: - type: object - properties: - allow_registration: - type: integer - description: 是否允许注册 (1=是, 0=否) - example: 1 - debug_mode: - type: integer - description: 调试模式状态 (1=开启, 0=关闭) - example: 0 - uptime: - type: integer - description: 系统运行时间(秒) - example: 0 - '500': - description: 系统不健康 - content: - application/json: - schema: - type: object - properties: - success: - type: boolean - example: false - data: - type: object - properties: - health: - type: object - properties: - status: - type: integer - example: 0 - timestamp: - type: string - format: date-time - services: - type: object - properties: - database: - type: object - properties: - status: - type: integer - example: 0 - latency_ms: - type: integer - example: 0 - storage: - type: object - properties: - status: - type: integer - example: 0 - provider: - type: integer - example: 1 - kv: - type: object - properties: - status: - type: integer - example: 0 - provider: - type: integer - example: 1 - total_latency_ms: - type: integer - example: 0 - error: - type: string - example: "系统服务异常" - config: - type: object - properties: - allow_registration: - type: integer - example: 0 - debug_mode: - type: integer - example: 0 - - /api/system/clear-cache: - post: - tags: - - 系统 - summary: 清理系统缓存 - description: 清理系统公开信息缓存(仅管理员) - security: - - cookieAuth: [] - responses: - '200': - description: 清理成功 - content: - application/json: - schema: - type: object - properties: - success: - type: boolean - example: true - message: - type: string - example: "系统缓存已清理" - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' - '500': - $ref: '#/components/responses/InternalServerError' - - /api/system/config: - get: - tags: - - 系统 - summary: 获取系统配置 - description: 普通用户只能获取公共配置,管理员可以获取完整配置 - security: - - cookieAuth: [] - responses: - '200': - description: 获取成功 - content: - application/json: - schema: - $ref: '#/components/schemas/SystemConfig' - '401': - $ref: '#/components/responses/UnauthorizedError' - - put: - tags: - - 系统 - summary: 更新系统配置(仅管理员) - security: - - cookieAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SystemConfigUpdate' - responses: - '200': - description: 更新成功 - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - '400': - description: 请求参数错误 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '401': - $ref: '#/components/responses/UnauthorizedError' - '403': - $ref: '#/components/responses/ForbiddenError' + '200': { $ref: '#/components/schemas/DatabaseInitResponse' } + '400': { $ref: '#/components/responses/BadRequest' } components: securitySchemes: @@ -1406,589 +470,369 @@ components: in: cookie name: session_cookies - responses: - UnauthorizedError: - description: 未授权 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - no_token: - summary: 未提供认证令牌 - value: - success: false - message: "未提供认证令牌" - invalid_token: - summary: 无效的认证令牌 - value: - success: false - message: "无效的认证令牌" - expired_token: - summary: 认证令牌已过期 - value: - success: false - message: "认证令牌已过期" - ForbiddenError: - description: 权限不足 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - admin_required: - summary: 需要管理员权限 - value: - success: false - message: "需要管理员权限" - insufficient_permissions: - summary: 权限不足 - value: - success: false - message: "权限不足" - NotFoundError: - description: 资源不存在 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - user_not_found: - summary: 用户不存在 - value: - success: false - message: "用户不存在" - mailbox_not_found: - summary: 邮箱不存在 - value: - success: false - message: "邮箱不存在" - email_not_found: - summary: 邮件不存在 - value: - success: false - message: "邮件不存在" - parameters: PageParam: name: page in: query - schema: - type: integer - default: 1 - minimum: 1 - description: 页码 + schema: { type: integer, minimum: 1, default: 1 } LimitParam: name: limit in: query - schema: - type: integer - default: 20 - minimum: 1 - maximum: 100 - description: 每页数量 + schema: { type: integer, minimum: 1, maximum: 100, default: 20 } + EmailIdParam: + name: id + in: path + required: true + schema: { type: string } + ForwardRuleIdParam: + name: id + in: path + required: true + schema: { type: integer } + schemas: - # ==================== 通用 ==================== - SuccessResponse: + BasicResponse: type: object properties: - success: - type: boolean - example: true - message: - type: string - example: 操作成功 + success: { type: boolean } + message: { type: string } - ErrorResponse: + LoginRequest: type: object + required: [username, password] properties: - success: - type: boolean - example: false - message: - type: string - example: 错误信息 + username: { type: string, example: admin } + password: { type: string, example: 123456 } - # ==================== 认证 ==================== LoginResponse: - type: object - properties: - token: - type: string - description: JWT token - message: - type: string - example: 登录成功 + allOf: + - $ref: '#/components/schemas/BasicResponse' + - type: object + properties: + data: + type: object + properties: + token: { type: string } + user: + $ref: '#/components/schemas/UserProfile' + + UserProfileResponse: + allOf: + - $ref: '#/components/schemas/BasicResponse' + - type: object + properties: + data: + $ref: '#/components/schemas/UserProfile' - # ==================== 用户 ==================== UserProfile: type: object properties: - id: - type: integer - username: - type: string - email: - type: string - format: email - user_type: - type: string - enum: [user, admin] - status: - type: integer - enum: [1, 2, 3] - description: 用户状态:1=激活, 2=停用, 3=删除 - created_at: - type: string - format: date-time - updated_at: - type: string - format: date-time + id: { type: integer } + username: { type: string } + email: { type: string, format: email } + user_type: { type: integer, enum: [1], description: 固定为管理员 } + created_at: { type: string } + updated_at: { type: string } settings: - $ref: '#/components/schemas/UserSettings' - - UserSettings: - type: object - properties: - webhook_url: - type: string - format: uri - nullable: true - webhook_secret: - type: string - nullable: true + type: object + properties: + webhook_url: { type: string, nullable: true } + webhook_secret: { type: string, nullable: true } UserSettingsUpdate: type: object properties: - password: - type: string - webhook_url: - type: string - format: uri - webhook_secret: - type: string - - UserListResponse: - type: object - properties: - total: - type: integer - items: - type: array - items: - $ref: '#/components/schemas/UserProfile' + password: { type: string, minLength: 6 } + webhook_url: { type: string } + webhook_secret: { type: string } - UserResponse: - type: object - properties: - user: - $ref: '#/components/schemas/UserProfile' - - AdminUserCreateRequest: - type: object - required: - - username - - password - - email - properties: - username: - type: string - password: - type: string - email: - type: string - format: email - user_type: - type: string - enum: [user, admin] - default: user - - # ==================== 邮件 ==================== EmailListResponse: - type: object - properties: - total: - type: integer - items: - type: array - items: - $ref: '#/components/schemas/EmailSummary' + allOf: + - $ref: '#/components/schemas/BasicResponse' + - type: object + properties: + data: + type: object + properties: + total: { type: integer } + items: + type: array + items: { $ref: '#/components/schemas/EmailSummary' } EmailSummary: type: object properties: - id: - type: string - subject: - type: string - from: - type: string - to: - type: string - status: - type: string - enum: [received, processed, forwarded, failed] - received_at: - type: string - format: date-time - - EmailDetail: + id: { type: string } + subject: { type: string } + sender_email: { type: string } + recipient_email: { type: string } + received_at: { type: string } + has_attachments: { type: integer, enum: [0,1] } + + EmailDetailResponse: allOf: - - $ref: '#/components/schemas/EmailSummary' + - $ref: '#/components/schemas/BasicResponse' - type: object properties: - content: - type: string - attachments: - type: array - items: - $ref: '#/components/schemas/Attachment' - - Attachment: - type: object - properties: - id: - type: string - filename: - type: string - size: - type: integer + data: + type: object + properties: + id: { type: string } + subject: { type: string } + content: { type: string } + content_type: { type: string } + attachments: + type: array + items: + type: object + properties: + id: { type: integer } + filename: { type: string } + size_bytes: { type: integer } - # ==================== 邮箱 ==================== - Mailbox: + SendEmailRequest: type: object + required: [to, subject, content] properties: - id: - type: integer - owner_id: - type: integer - owner_username: - type: string - description: 邮箱所有者用户名 - owner_usertype: - type: integer - enum: [0, 1] - description: 邮箱所有者用户类型(0: 普通用户, 1: 管理员) - address: - type: string - status: - type: integer - enum: [1, 2, 3] - description: 邮箱状态(1: 正常, 2: 停用(管理员封禁), 3: 已删除(用户删除) - deleted_at: + to: { type: string } + from: { type: string } + subject: { type: string } + content: { type: string } + content_type: type: string - format: date-time - nullable: true - description: 删除时间 - created_at: - type: string - format: date-time - updated_at: - type: string - format: date-time + enum: [text, html, markdown] - MailboxResponse: + ForwardRuleInput: type: object + required: [rule_name] properties: - mailbox: - $ref: '#/components/schemas/Mailbox' - - MailboxListResponse: - type: object - properties: - total: - type: integer - items: + rule_name: { type: string } + sender_filter: { type: string } + keyword_filter: { type: string } + recipient_filter: { type: string } + enabled: { type: integer, enum: [0, 1] } + webhooks: type: array + description: 规则关联的多个 webhook 配置 items: - $ref: '#/components/schemas/Mailbox' + $ref: '#/components/schemas/ForwardRuleWebhook' - MailboxApplicationRequest: - type: object - properties: - reason: - type: string - description: 申请理由 + ForwardRuleResponse: + allOf: + - $ref: '#/components/schemas/BasicResponse' + - type: object + properties: + data: + type: object + properties: + rule: { $ref: '#/components/schemas/ForwardRule' } - MailboxApplicationResponse: - type: object - properties: - application_id: - type: integer - status: - type: integer - enum: [0, 1, 2] - description: "0=待审核, 1=已批准, 2=已拒绝" + ForwardRuleListResponse: + allOf: + - $ref: '#/components/schemas/BasicResponse' + - type: object + properties: + data: + type: object + properties: + total: { type: integer } + items: + type: array + items: { $ref: '#/components/schemas/ForwardRule' } - MailboxApplicationListResponse: + ForwardRule: type: object properties: - total: - type: integer - items: + id: { type: integer } + rule_name: { type: string } + sender_filter: { type: string } + keyword_filter: { type: string } + recipient_filter: { type: string } + webhook_url: { type: string, description: 保留字段以兼容旧数据 } + webhook_secret: { type: string, description: 保留字段以兼容旧数据 } + webhook_type: { type: string, enum: [dingtalk, feishu, bark, custom], description: 保留字段以兼容旧数据 } + enabled: { type: integer, enum: [0, 1] } + created_at: { type: string } + updated_at: { type: string } + webhooks: type: array + description: 规则关联的多个 webhook 配置 items: - $ref: '#/components/schemas/MailboxApplicationResponse' - - ApplicationProcessRequest: - type: object - required: - - action - properties: - action: - type: integer - enum: [1, 2] - description: "1=批准, 2=拒绝" - - AdminMailboxCreateRequest: - type: object - required: - - address - - owner_id - properties: - address: - type: string - owner_id: - type: integer + $ref: '#/components/schemas/ForwardRuleWebhook' - # ==================== 转发规则 ==================== - ForwardRuleBase: + ForwardRuleWebhook: type: object properties: - rule_name: - type: string - description: 规则名称 - description: - type: string - nullable: true - description: 规则描述 - source_email: - type: string - description: 源邮箱地址 - target_email: - type: string - description: 目标邮箱地址 - sender_filter: - type: string - nullable: true - description: 发件人过滤条件 - keyword_filter: - type: string - nullable: true - description: 关键词过滤条件 - recipient_filter: - type: string - nullable: true - description: 收件人过滤条件 - webhook_url: - type: string - format: uri - description: Webhook URL - webhook_secret: - type: string - nullable: true - description: Webhook 密钥 - webhook_type: - type: string - enum: [dingtalk, feishu, custom] - description: Webhook 类型 - enabled: - type: boolean - description: 是否启用 - - ForwardRuleCreateRequest: + id: { type: integer } + rule_id: { type: integer } + webhook_url: { type: string } + webhook_secret: { type: string } + webhook_type: { type: string, enum: [dingtalk, feishu, bark, custom] } + enabled: { type: integer, enum: [0, 1] } + created_at: { type: string } + updated_at: { type: string } + + SystemHealthResponse: allOf: - - $ref: '#/components/schemas/ForwardRuleBase' + - $ref: '#/components/schemas/BasicResponse' - type: object - required: - - rule_name - - source_email - - target_email - - webhook_url + properties: + data: + type: object + properties: + health: + type: object + properties: + status: { type: integer } + timestamp: { type: string } + services: + type: object + properties: + database: + type: object + properties: + status: { type: integer } + latency_ms: { type: integer } + storage: + type: object + properties: + status: { type: integer } + provider: { type: integer } + kv: + type: object + properties: + status: { type: integer } + provider: { type: integer } + config: + type: object + properties: + allow_registration: + type: integer + description: 只读标志,当前固定为禁止注册 + debug_mode: { type: integer } - ForwardRuleUpdateRequest: + RegistrationStatusResponse: allOf: - - $ref: '#/components/schemas/ForwardRuleBase' + - $ref: '#/components/schemas/BasicResponse' + - type: object + properties: + data: + type: object + properties: + allow_registration: { type: integer } - ForwardRuleResponse: - type: object - properties: - id: - type: integer - description: 规则ID - name: - type: string - description: 规则名称(rule_name的别名) - description: - type: string - nullable: true - description: 规则描述 - source_email: - type: string - description: 源邮箱地址 - target_email: - type: string - description: 目标邮箱地址 - enabled: - type: boolean - description: 是否启用 - created_at: - type: string - format: date-time - description: 创建时间 - updated_at: - type: string - format: date-time - description: 更新时间 - user_id: - type: integer - description: 用户ID(仅管理员可见) - username: - type: string - description: 用户名(仅管理员可见) + SystemConfigResponse: + allOf: + - $ref: '#/components/schemas/BasicResponse' + - type: object + properties: + data: + type: object + properties: + config: + type: object + additionalProperties: true + user_role: + type: string + enum: ['admin'] - ForwardRuleListResponse: + SystemConfigUpdate: type: object properties: - total: - type: integer - items: + debug_mode: { type: integer, enum: [0,1] } + mail_retention_days: { type: integer, description: 邮件保留天数 } + attachment_retention_days: { type: integer, description: 附件保留天数 } + max_attachment_size: { type: integer, description: 单位:字节 } + cookie_max_age: { type: integer, description: 单位:秒 } + primary_domain: { type: string } + admin_email: { type: string } + domains: type: array - items: - $ref: '#/components/schemas/ForwardRuleResponse' + items: { type: string } + default_webhook_url: { type: string, description: 默认 Webhook URL(系统级别,所有邮件都会发送到此 webhook,如果为空则不通知) } + default_webhook_secret: { type: string, description: 默认 Webhook 密钥(可选) } + default_webhook_type: { type: string, enum: [dingtalk, feishu, bark, custom], description: 默认 Webhook 类型 } - # ==================== 邮箱历史 ==================== - MailboxHistory: - type: object - properties: - id: - type: integer - mailbox_id: - type: integer - user_id: - type: integer - description: 操作人用户ID - owner_id: - type: integer - description: 邮箱所有者ID - action_type: - type: integer - enum: [1, 2, 3] - description: 操作类型(1=创建, 2=删除, 3=停用) - created_at: - type: string - format: date-time - user_username: - type: string - description: 操作人用户名 - owner_username: - type: string - description: 邮箱所有者用户名 - mailbox_address: - type: string - description: 邮箱地址(仅在用户历史记录中包含) - - MailboxHistoryListResponse: - type: object - properties: - total: - type: integer - items: - type: array - items: - $ref: '#/components/schemas/MailboxHistory' + DatabaseInfoResponse: + allOf: + - $ref: '#/components/schemas/BasicResponse' + - type: object + properties: + data: + type: object + properties: + sqliteVersion: { type: string } + tables: + type: object + additionalProperties: + type: object + properties: + recordCount: { type: integer } + error: { type: string } - UserMailboxHistoryListResponse: - type: object - properties: - total: - type: integer - items: - type: array - items: - $ref: '#/components/schemas/MailboxHistory' + DatabaseTablesResponse: + allOf: + - $ref: '#/components/schemas/BasicResponse' + - type: object + properties: + data: + type: array + items: + type: object + properties: + table: { type: string } + columns: + type: array + items: { type: string } + records: + type: array + items: { type: object } + total: { type: integer } + error: { type: string } + + DatabaseStatsResponse: + allOf: + - $ref: '#/components/schemas/BasicResponse' + - type: object + properties: + data: + type: object + additionalProperties: true - # ==================== 系统 ==================== - SystemConfig: - type: object - properties: - debug_mode: - type: integer - description: 调试模式状态 (1=开启, 0=关闭) - default: 0 - allow_registration: - type: integer - description: 是否允许新用户注册 (1=是, 0=否) - default: 1 - auto_approve_mailbox: - type: integer - description: 是否自动审核邮箱申请 (1=是, 0=否) - default: 0 - supported_domains: - type: array - items: - type: string - description: 支持的邮箱域名列表 - example: ["example.com", "test.com"] - mail_retention_days: - type: integer - minimum: 0 - maximum: 365 - description: 邮件保留天数,0表示永不过期 - default: 30 - attachment_max_size: - type: integer - minimum: 1 - maximum: 100 - description: 单个附件最大大小(MB) - default: 50 - allow_user_send: - type: integer - description: 是否允许普通用户发送邮件 (1=是, 0=否)(预留接口,暂未实现) - default: 1 - max_mailboxes_per_user: - type: integer - minimum: 1 - description: 每个用户最大邮箱数量(预留接口,暂未实现) - default: 5 - storage_provider: - type: string - enum: [r2, s3, local] - description: 存储提供商(预留接口,暂未实现,默认使用 R2) + DatabaseInitResponse: + allOf: + - $ref: '#/components/schemas/BasicResponse' + - type: object + properties: + data: + type: object + properties: + steps: + type: array + items: { type: string } - SystemConfigUpdate: - type: object - properties: - debug_mode: - type: integer - description: 调试模式状态 (1=开启, 0=关闭) - allow_registration: - type: integer - description: 是否允许新用户注册 (1=是, 0=否) - auto_approve_mailbox: - type: integer - description: 是否自动审核邮箱申请 (1=是, 0=否) - supported_domains: - type: array - items: - type: string - mail_retention_days: - type: integer - minimum: 0 - maximum: 365 - attachment_max_size: - type: integer - minimum: 1 - maximum: 100 - allow_user_send: - type: integer - description: 是否允许普通用户发送邮件 (1=是, 0=否)(预留接口,暂未实现) - max_mailboxes_per_user: - type: integer - minimum: 1 - description: 每个用户最大邮箱数量(预留接口,暂未实现) - storage_provider: - type: string - enum: [r2, s3, local] - description: 存储提供商(预留接口,暂未实现,默认使用 R2) \ No newline at end of file + responses: + UnauthorizedError: + description: 未登录或会话失效 + content: + application/json: + schema: + $ref: '#/components/schemas/BasicResponse' + ForbiddenError: + description: 权限不足 + content: + application/json: + schema: + $ref: '#/components/schemas/BasicResponse' + BadRequest: + description: 请求参数不合法 + content: + application/json: + schema: + $ref: '#/components/schemas/BasicResponse' + NotFoundError: + description: 资源不存在 + content: + application/json: + schema: + $ref: '#/components/schemas/BasicResponse' diff --git a/cloudflare_temp_email/.flake8 b/cloudflare_temp_email/.flake8 deleted file mode 100644 index b6cd654..0000000 --- a/cloudflare_temp_email/.flake8 +++ /dev/null @@ -1,3 +0,0 @@ -[flake8] -max-line-length = 180 -exclude = .git,__pycache__,build,dist diff --git "a/cloudflare_temp_email/.github/ISSUE_TEMPLATE/bug-\345\217\215\351\246\210.md" "b/cloudflare_temp_email/.github/ISSUE_TEMPLATE/bug-\345\217\215\351\246\210.md" deleted file mode 100644 index 4678fed..0000000 --- "a/cloudflare_temp_email/.github/ISSUE_TEMPLATE/bug-\345\217\215\351\246\210.md" +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: Bug 反馈 -about: Create a report to help us improve -title: "[BUG]" -labels: bug -assignees: '' - ---- - -## 复现步骤 - - - -## 预期行为 - - - -## 部署方式 - -- [ ] cli 部署 -- [ ] 用户界面部署 - -## 浏览器环境 diff --git a/cloudflare_temp_email/.github/ISSUE_TEMPLATE/feature-request.md b/cloudflare_temp_email/.github/ISSUE_TEMPLATE/feature-request.md deleted file mode 100644 index d99141e..0000000 --- a/cloudflare_temp_email/.github/ISSUE_TEMPLATE/feature-request.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: Feature Request -about: Suggest an idea for this project -title: "[Feature]" -labels: enhancement, good first issue -assignees: '' - ---- - -## 请描述您的 Feature - -## 描述您想要的解决方案 - -## 描述您考虑过的替代方案 - -## 附加上下文 diff --git a/cloudflare_temp_email/.github/config/mail-parser-wasm-worker.patch b/cloudflare_temp_email/.github/config/mail-parser-wasm-worker.patch deleted file mode 100644 index 289b9a8..0000000 --- a/cloudflare_temp_email/.github/config/mail-parser-wasm-worker.patch +++ /dev/null @@ -1,44 +0,0 @@ -diff --git a/worker/src/common.ts b/worker/src/common.ts -index bd9bcc9..e7e2748 100644 ---- a/worker/src/common.ts -+++ b/worker/src/common.ts -@@ -273,23 +273,23 @@ export const commonParseMail = async (parsedEmailContext: ParsedEmailContext): P - } - const raw_mail = parsedEmailContext.rawEmail; - // TODO: WASM parse email -- // try { -- // const { parse_message_wrapper } = await import('mail-parser-wasm-worker'); -+ try { -+ const { parse_message_wrapper } = await import('mail-parser-wasm-worker'); - -- // const parsedEmail = parse_message_wrapper(raw_mail); -- // parsedEmailContext.parsedEmail = { -- // sender: parsedEmail.sender || "", -- // subject: parsedEmail.subject || "", -- // text: parsedEmail.text || "", -- // headers: parsedEmail.headers?.map( -- // (header) => ({ key: header.key, value: header.value }) -- // ) || [], -- // html: parsedEmail.body_html || "", -- // }; -- // return parsedEmailContext.parsedEmail; -- // } catch (e) { -- // console.error("Failed use mail-parser-wasm-worker to parse email", e); -- // } -+ const parsedEmail = parse_message_wrapper(raw_mail); -+ parsedEmailContext.parsedEmail = { -+ sender: parsedEmail.sender || "", -+ subject: parsedEmail.subject || "", -+ text: parsedEmail.text || "", -+ headers: parsedEmail.headers?.map( -+ (header) => ({ key: header.key, value: header.value }) -+ ) || [], -+ html: parsedEmail.body_html || "", -+ }; -+ return parsedEmailContext.parsedEmail; -+ } catch (e) { -+ console.error("Failed use mail-parser-wasm-worker to parse email", e); -+ } - try { - const { default: PostalMime } = await import('postal-mime'); - const parsedEmail = await PostalMime.parse(raw_mail); diff --git a/cloudflare_temp_email/.github/workflows/docs_deploy.yml b/cloudflare_temp_email/.github/workflows/docs_deploy.yml deleted file mode 100644 index 7739b1c..0000000 --- a/cloudflare_temp_email/.github/workflows/docs_deploy.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Deploy Docs - -on: - push: - paths: - - "vitepress-docs/**" - tags: - - "*" - workflow_dispatch: - -jobs: - deploy: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - uses: pnpm/action-setup@v3 - name: Install pnpm - id: pnpm-install - with: - version: 8 - run_install: false - - - name: check github release done - run: | - for ((attempt=1; attempt<=10; attempt++)); do - if wget -q --spider "https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest/download/frontend.zip"; then - echo "frontend.zip found." - break - else - if [ $attempt -eq 10 ]; then - echo "Exceeded maximum retries. frontend.zip not found." - else - echo "frontend.zip not found. Retrying in 30 seconds..." - sleep 30 - fi - fi - done - - - name: Deploy Docs for ${{github.ref_name}} - run: | - cd vitepress-docs/ - wget https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest/download/frontend.zip -O docs/public/ui_install/frontend.zip - pnpm install --no-frozen-lockfile - if [[ ${{github.ref}} == refs/tags/* ]]; then - export TAG_NAME=${{github.ref_name}} - else - export TAG_NAME=$(git describe --tags --abbrev=0) - fi - echo "Deploying docs for tag $TAG_NAME" - pnpm run deploy - env: - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} diff --git a/cloudflare_temp_email/.github/workflows/frontend_deploy.yaml b/cloudflare_temp_email/.github/workflows/frontend_deploy.yaml deleted file mode 100644 index ba8605d..0000000 --- a/cloudflare_temp_email/.github/workflows/frontend_deploy.yaml +++ /dev/null @@ -1,64 +0,0 @@ -name: Deploy Frontend - -on: - workflow_run: - workflows: [Upstream Sync] - types: [completed] - push: - tags: - - "*" - workflow_dispatch: - -jobs: - deploy: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - uses: pnpm/action-setup@v3 - name: Install pnpm - id: pnpm-install - with: - version: 8 - run_install: false - - - name: Deploy Frontend for ${{ github.ref_name }} - run: | - cd frontend/ - echo "${{ secrets.FRONTEND_ENV }}" > .env.prod - export project_name=${{ secrets.FRONTEND_NAME }} - pnpm install --no-frozen-lockfile - export frontend_branch=${{ secrets.FRONTEND_BRANCH }} - if [ -n "$frontend_branch" ]; then - echo "Deploying branch $frontend_branch" - pnpm run deploy:actions --project-name=$project_name --branch $frontend_branch - else - echo "Deploying branch production" - pnpm run deploy --project-name=$project_name - fi - echo "Deploying production for ${{ github.ref_name }}" - echo "Deployed for tag ${{ github.ref_name }}" - - export tg_mini_app_project_name=${{ secrets.TG_FRONTEND_NAME }} - if [ -n "$tg_mini_app_project_name" ]; then - echo "Deploying telegram mini app $tg_mini_app_project_name" - if [ -n "$frontend_branch" ]; then - echo "Deploying telegram mini app branch $frontend_branch" - pnpm run deploy:actions:telegram --project-name=$tg_mini_app_project_name --branch $frontend_branch - else - echo "Deploying telegram mini app branch production" - pnpm run deploy:telegram --project-name=$tg_mini_app_project_name - fi - echo "Deployed telegram mini app for ${{ github.ref_name }}" - fi - env: - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} diff --git a/cloudflare_temp_email/.github/workflows/frontend_pagefunction_deploy.yaml b/cloudflare_temp_email/.github/workflows/frontend_pagefunction_deploy.yaml deleted file mode 100644 index e5b2960..0000000 --- a/cloudflare_temp_email/.github/workflows/frontend_pagefunction_deploy.yaml +++ /dev/null @@ -1,39 +0,0 @@ -name: Deploy Frontend with page function - -on: - workflow_dispatch: - -jobs: - deploy: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - uses: pnpm/action-setup@v3 - name: Install pnpm - id: pnpm-install - with: - version: 8 - run_install: false - - - name: Deploy Frontend for ${{ github.ref_name }} - run: | - cd frontend/ - pnpm install --no-frozen-lockfile - pnpm build:pages - cd ../pages/ - echo '${{ secrets.PAGE_TOML }}' > wrangler.toml - pnpm install --no-frozen-lockfile - pnpm run deploy - echo "Deploying prodcution for ${{ github.ref_name }}" - env: - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} diff --git a/cloudflare_temp_email/.github/workflows/pr_agent.yml b/cloudflare_temp_email/.github/workflows/pr_agent.yml deleted file mode 100644 index 7cd371d..0000000 --- a/cloudflare_temp_email/.github/workflows/pr_agent.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Codium PR Agent - -on: - pull_request: - types: [opened, reopened, ready_for_review] -jobs: - pr_agent_job: - if: ${{ github.event.sender.type != 'Bot' }} - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - name: Run pr agent on every pull request, respond to user comments - steps: - - name: PR Agent action step - id: pragent - uses: docker://codiumai/pr-agent:0.29-github_action - env: - PR_REVIEWER.REQUIRE_TESTS_REVIEW: "false" - OPENAI_KEY: ${{ secrets.OPENAI_KEY }} - OPENAI_API_BASE: ${{ secrets.OPENAI_API_BASE }} - CONFIG.MODEL: "gpt-4o" - CONFIG.MODEL_TURBO: "gpt-4o" - OPENAI.API_BASE: ${{ secrets.OPENAI_API_BASE }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/cloudflare_temp_email/.github/workflows/smtp_proxy_server.yml b/cloudflare_temp_email/.github/workflows/smtp_proxy_server.yml deleted file mode 100644 index 1a2d7f6..0000000 --- a/cloudflare_temp_email/.github/workflows/smtp_proxy_server.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: SMTP Proxy Server Docker Image CI - -on: - push: - paths: - - "smtp_proxy_server/**" - tags: - - "*" - branches: - - main - workflow_dispatch: - -env: - REGISTRY: ghcr.io - IMAGE_NAME: smtp_proxy_server - -jobs: - build-and-push-image: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - uses: actions/checkout@v4 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and push Docker images - uses: docker/build-push-action@v5 - with: - context: ./smtp_proxy_server - file: ./smtp_proxy_server/dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: | - ${{ env.REGISTRY }}/${{ github.repository }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} - ${{ env.REGISTRY }}/${{ github.repository }}/${{ env.IMAGE_NAME }}:latest diff --git a/cloudflare_temp_email/.github/workflows/sync.yaml b/cloudflare_temp_email/.github/workflows/sync.yaml deleted file mode 100644 index 0ab1407..0000000 --- a/cloudflare_temp_email/.github/workflows/sync.yaml +++ /dev/null @@ -1,25 +0,0 @@ -name: Upstream Sync - -on: - schedule: - - cron: "0 0 * * 1" - workflow_dispatch: - -jobs: - sync_latest_from_upstream: - name: Sync latest commits from upstream repo - runs-on: ubuntu-latest - if: ${{ github.event.repository.fork }} - - steps: - - uses: actions/checkout@v4 - - - name: Sync upstream changes - id: sync - uses: aormsby/Fork-Sync-With-Upstream-action@v3.4 - with: - upstream_sync_repo: dreamhunter2333/cloudflare_temp_email - upstream_sync_branch: main - target_sync_branch: main - target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set - test_mode: false diff --git a/cloudflare_temp_email/.github/workflows/tag_build.yml b/cloudflare_temp_email/.github/workflows/tag_build.yml deleted file mode 100644 index 3032706..0000000 --- a/cloudflare_temp_email/.github/workflows/tag_build.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Tag Build CI - -on: - push: - tags: - - "*" - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - uses: pnpm/action-setup@v3 - name: Install pnpm - id: pnpm-install - with: - version: 8 - run_install: false - - - name: Build Frontend - run: cd frontend && pnpm install --no-frozen-lockfile && pnpm build:release - - - name: Zip Frontend dist - run: cd frontend/dist/ && zip -r frontend.zip * && mv frontend.zip ../ - - - name: Build Telegram Frontend - run: cd frontend && pnpm install --no-frozen-lockfile && pnpm build:telegram:release - - - name: Zip Telegram Frontend dist - run: cd frontend/dist/ && zip -r telegram-frontend.zip * && mv telegram-frontend.zip ../ - - - name: cp wrangler.toml - run: cd worker && cp wrangler.toml.template wrangler.toml - - - name: Build Backend - run: cd worker && pnpm install --no-frozen-lockfile && pnpm build - - - name: Move worker.js - run: cd worker/dist && mv worker.js ../ - - - name: Build Worker with wasm mail parser - run: | - cd worker - echo "Using mail-parser-wasm-worker" - pnpm add mail-parser-wasm-worker - git apply ../.github/config/mail-parser-wasm-worker.patch - echo "Applied mail-parser-wasm-worker patch" - pnpm build - zip -r worker-with-wasm-mail-parser.zip dist/worker.js dist/*.wasm - - - name: Upload to Release - uses: softprops/action-gh-release@v2 - with: - files: | - frontend/frontend.zip - frontend/telegram-frontend.zip - worker/worker.js - worker/worker-with-wasm-mail-parser.zip diff --git a/cloudflare_temp_email/.gitignore b/cloudflare_temp_email/.gitignore deleted file mode 100644 index 42fc586..0000000 --- a/cloudflare_temp_email/.gitignore +++ /dev/null @@ -1,139 +0,0 @@ -.DS_Store -dist/ -test/ -.vscode/ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* - -.wrangler -wrangler.toml -.dev.vars -pnpm-lock.yaml diff --git a/cloudflare_temp_email/CHANGELOG.md b/cloudflare_temp_email/CHANGELOG.md deleted file mode 100644 index e501f39..0000000 --- a/cloudflare_temp_email/CHANGELOG.md +++ /dev/null @@ -1,535 +0,0 @@ - -# CHANGE LOG - -## main(v1.0.5) - -- feat: 新增 `DISABLE_CUSTOM_ADDRESS_NAME` 配置: 禁用自定义邮箱地址名称功能 -- feat: 新增 `CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST` 配置: 创建地址时优先使用第一个域名 -- feat: |UI| 主页增加进入极简模式按钮 -- feat: |Webhook| 增加白名单开关功能,支持灵活控制访问权限 - -## v1.0.4 - -- feat: |UI| 优化极简模式主页, 增加全部邮件页面功能(删除/下载/附件/...), 可在 `外观` 中切换 -- feat: admin 账号设置页面增加 `邮件转发规则` 配置 -- feat: admin 账号设置页面增加 `禁止接收未知地址邮件` 配置 -- feat: 邮件页面增加 上一封/下一封 按钮 - -## v1.0.3 - -- fix: 修复 github actions 部署问题 -- feat: telegram /new 不指定域名时, 使用随机地址 - -## v1.0.2 - -- fix: 修复 oauth2 登录失败的问题 - -## v1.0.1 - -- feat: |UI| 增加极简模式主页, 可在 `外观` 中切换 -- fix: 修复 oauth2 登录时,default role 不生效的问题 - -## v1.0.0 - -- fix: |UI| 修复 User 查看收件箱,不选择地址时,关键词查询不生效 -- fix: 修复自动清理任务,时间为 0 时不生效的问题 -- feat: 清理功能增加 创建 n 天前地址清理,n 天前未活跃地址清理 -- fix: |IMAP Proxy| 修复 IMAP Proxy 服务器,无法查看新邮件的问题 - -## v0.10.0 - -- feat: 支持 User 查看收件箱,`/user_api/mails` 接口, 支持 `address` 和 `keyword` 过滤 -- fix: 修复 Oauth2 登录获取 Token 时,一些 Oauth2 需要 `redirect_uri` 参数的问题 -- feat: 用户访问网页时,如果 `user token` 在 7 天内过期,自动刷新 -- feat: admin portal 中增加初始化 db 的功能 -- feat: 增加 `ALWAYS_SHOW_ANNOUNCEMENT` 变量,用于配置是否总是显示公告 - -## v0.9.1 - -- feat: |UI| support google ads -- feat: |UI| 使用 shadow DOM 防止样式污染 -- feat: |UI| 支持 URL jwt 参数自动登录邮箱,jwt 参数会覆盖浏览器中的 jwt -- fix: |CleanUP| 修复清理邮件时,清理时间超过 30 天报错的 bug -- feat: admin 用户管理页面: 增加 用户地址查看功能 -- feat: | S3 附件| 增加 S3 附件删除功能 -- feat: | Admin API| 增加 admin 绑定用户和地址的 api -- feat: | Oauth2 | Oatuh2 获取用户信息时,支持 `JSONPATH` 表达式 - -## v0.9.0 - -- feat: | Worker | 支持多语言 -- feat: | Worker | `NO_LIMIT_SEND_ROLE` 配置支持多角色, 逗号分割 -- feat: | Actions | build 里增加 `worker-with-wasm-mail-parser.zip` 支持 UI 部署带 `wasm` 的 worker - -## v0.8.7 - -- fix: |UI| 修复移动设备日期显示问题 -- feat: |Worker| 支持通过 `SMTP` 发送邮件, 使用 [zou-yu/worker-mailer](https://github.com/zou-yu/worker-mailer/blob/main/README_zh-CN.md) - -## v0.8.6 - -- feat: |UI| 公告支持 html 格式 -- feat: |UI| `COPYRIGHT` 支持 html 格式 -- feat: |Doc| 优化部署文档,补充了 `Github Actions 部署文档`,增加了 `Worker 变量说明` - -## v0.8.5 - -- feat: |mail-parser-wasm-worker| 修复 `initSync` 函数调用时的 `deprecated` 参数警告 -- feat: rpc headers covert & typo (#559) -- fix: telegram mail page use iframe show email (#561) -- feat: |Worker| 增加 `REMOVE_ALL_ATTACHMENT` 和 `REMOVE_EXCEED_SIZE_ATTACHMENT` 用于移除邮件附件,由于是解析邮件的一些信息会丢失,比如图片等. - -## v0.8.4 - -- fix: |UI| 修复 admin portal 无收件人邮箱删除调用api 错误 -- feat: |Telegram Bot| 增加 telegram bot 清理无效地址凭证命令 -- feat: 增加 worker 配置 `DISABLE_ANONYMOUS_USER_CREATE_EMAIL` 禁用匿名用户创建邮箱地址,只允许登录用户创建邮箱地址 -- feat: 增加 worker 配置 `ENABLE_ANOTHER_WORKER` 及 `ANOTHER_WORKER_LIST` ,用于调用其他 worker 的 rpc 接口 (#547) -- feat: |UI| 自动刷新配置保存到浏览器,可配置刷新间隔 -- feat: 垃圾邮件检测增加存在时才检查的列表 `JUNK_MAIL_CHECK_LIST` 配置 -- feat: | Worker | 增加 `ParsedEmailContext` 类用于缓存解析后的邮件内容,减少解析次数 -- feat: |Github Action| Worker 部署增加 `DEBUG_MODE` 输出日志, `BACKEND_USE_MAIL_WASM_PARSER` 配置是否使用 wasm 解析邮件 - -## v0.8.3 - -- feat: |Github Action| 增加自动更新并部署功能 -- feat: |UI| admin 用户设置,支持 oauth2 配置的删除 -- feat: 增加垃圾邮件检测必须通过的列表 `JUNK_MAIL_FORCE_PASS_LIST` 配置 - -## v0.8.2 - -- fix: |Doc| 修复文档中的一些错误 -- fix: |Github Action| 修复 frontend 部署分支错误的问题 -- feat: admin 发送邮件功能 -- feat: admin 后台,账号配置页面添加无限发送邮件的地址列表 - -## v0.8.1 - -- feat: |Doc| 更新 UI 安装的文档 -- feat: |UI| 对用户隐藏邮箱账号的 ID -- feat: |UI| 增加邮件详情页的 `转发` 按钮 - -## v0.8.0 - -- feat: |UI| 随机生成地址时不超过最大长度 -- feat: |UI| 邮件时间显示浏览器时区,可在设置中切换显示为 UTC 时间 -- feat: 支持转移邮件到其他用户 - -## v0.7.6 - -### Breaking Changes - -UI 部署 worker 需要点击 Settings -> Runtime, 修改 Compatibility flags, 增加 `nodejs_compat` - -![worker-runtime](vitepress-docs/docs/public/ui_install/worker-runtime.png) - -### Changes - -- feat: 支持提前设置 bot info, 降低 telegram 回调延迟 (#441) -- feat: 增加 telegram mini app 的 build 压缩包 -- feat: 增加是否启用垃圾邮件检查 `ENABLE_CHECK_JUNK_MAIL` 配置 - -## v0.7.5 - -- fix: 修复 `name` 的校验检查 - -## v0.7.4 - -- feat: UI 列表页面增加最小宽度 -- fix: 修复 `name` 的校验检查 -- fix: 修复 `DEFAULT_DOMAINS` 配置为空不生效的问题 - -## v0.7.3 - -- feat: worker 增加 `ADDRESS_CHECK_REGEX`, address name 的正则表达式, 只用于检查,符合条件将通过检查 -- fix: UI 修复登录页面 tab 激活图标错位 -- fix: UI 修复 admin 页面刷新弹框输入密码的问题 -- feat: support `Oath2` 登录, 可以通过 `Github` `Authentik` 等第三方登录, 详情查看 [OAuth2 第三方登录](https://temp-mail-docs.awsl.uk/zh/guide/feature/user-oauth2.html) - -## v0.7.2 - -### Breaking Changes - -`webhook` 的结构增加了 `enabled` 字段,已经配置了的需要重新在页面开启并保存。 - -### Changes - -- fix: worker 增加 `NO_LIMIT_SEND_ROLE` 配置, 加载失败的问题 -- feat: worker 增加 `# ADDRESS_REGEX = "[^a-z.0-9]"` 配置, 替换非法符号的正则表达式,如果不设置,默认为 [^a-z0-9], 需谨慎使用, 有些符号可能导致无法收件 -- feat: worker 优化 webhook 逻辑, 支持 admin 配置全局 webhook, 添加 `message pusher` 集成示例 - -## v0.7.1 - -- fix: 修复用户角色加载失败的问题 -- feat: admin 账号设置增加来源邮件地址黑名单配置 - -## v0.7.0 - -### Breaking Changes - -DB changes: 增加用户 `passkey` 表, 需要执行 `db/2024-08-10-patch.sql` 更新 `D1` 数据库 - -### Changes - -- Docs: Update new-address-api.md (#360) -- feat: worker 增加 `ADMIN_USER_ROLE` 配置, 用于配置管理员用户角色,此角色的用户可访问 admin 管理页面 (#363) -- feat: worker 增加 `DISABLE_SHOW_GITHUB` 配置, 用于配置是否显示 github 链接 -- feat: worker 增加 `NO_LIMIT_SEND_ROLE` 配置, 用于配置可以无限发送邮件的角色 -- feat: 用户增加 `passkey` 登录方式, 用于用户登录, 无需输入密码 -- feat: worker 增加 `DISABLE_ADMIN_PASSWORD_CHECK` 配置, 用于配置是否禁用 admin 控制台密码检查, 若你的网站只可私人访问,可通过此禁用检查 - -## v0.6.1 - -- pages github actions && 修复清理邮件天数为 0 不生效 by @tqjason (#355) -- fix: imap proxy server 不支持 密码 by @dreamhunter2333 (#356) -- worker 新增 `ANNOUNCEMENT` 配置, 用于配置公告信息 by @dreamhunter2333 (#357) -- fix: telegram bot 新建地址默认选择第一个域名 by @dreamhunter2333 (#358) - -## v0.6.0 - -### Breaking Changes - -DB changes: 增加用户角色表, 需要执行 `db/2024-07-14-patch.sql` 更新 `D1` 数据库 - -### Changes - -worker 配置文件新增 `DEFAULT_DOMAINS`, `USER_ROLES`, `USER_DEFAULT_ROLE`, 具体查看文档 [worker配置](https://temp-mail-docs.awsl.uk/zh/guide/cli/worker.html#%E4%BF%AE%E6%94%B9-wrangler-toml-%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6) - -- 移除 `apiV1` 相关代码和相关的数据库表 -- 更新 `admin/statistics` api, 添加用户统计信息 -- 更新地址的规则,只允许小写+数字,对于历史的地址在查询邮件时会进行 `lowercase` 处理 -- 增加用户角色功能,`admin` 可以设置用户角色(目前可配置每个角色域名和前缀) -- admin 页面搜索优化, 回车自动搜索, 输入内容自动 trim - -## v0.5.4 - -- 点击 logo 5 次进入 admin 页面 -- 修复 401 时无法跳转登录页面(admin 和 网站认证) - -## v0.5.3 - -- 修复 smtp imap proxy sever 的一些 bug -- 完善用户/admin 删除收件箱/发件箱的功能 -- admin 可以删除 发件权限记录 -- 添加中文邮件别名配置 `DOMAIN_LABELS` [文档](https://temp-mail-docs.awsl.uk/zh/guide/cli/worker.html) -- 移除 `mail channels` 相关代码 -- github actions 增加 `FRONTEND_BRANCH` 变量用于指定部署的分支 (#324) - -## v0.5.1 - -- 添加 `mail-parser-wasm-worker` 用于 worker 解析邮件, [文档](https://temp-mail-docs.awsl.uk/zh/guide/feature/mail_parser_wasm_worker.html) -- 添加校验用户邮箱长度配置 `MIN_ADDRESS_LEN` 和 `MAX_ADDRESS_LEN` -- 修复 `pages function` 未转发 `telegram` api 问题 - -## v0.5.0 - -- UI: 增加本地缓存进行地址管理 -- worker: 增加 `FORWARD_ADDRESS_LIST` 全局邮件转发地址(等同于 `catch all`) -- UI: 多语言使用路由进行切换 -- 添加保存附件到 S3 的功能 -- UI: 增加收取邮件列表 `批量删除` 和 `批量下载` - -## v0.4.6 - -- worker 配置文件添加 `TITLE = "Custom Title"`, 可自定义网站标题 -- 修复 KV 未绑定无法删除地址的问题 - -## v0.4.5 - -- UI lazy load 懒加载 -- telegram bot 添加用户全局推送功能(admin 用户) -- 增加对 cloudflare verified 用户发送邮件 -- 增加使用 `resend` 发送邮件, `resend` 提供 http 和 smtp api, 使用更加方便, 文档: https://temp-mail-docs.awsl.uk/zh/guide/config-send-mail.html - -## v0.4.4 - -- 增加 telegram mini app -- telegram bot 增加 `ubind`, `delete` 指令 -- 修复 webhook 多行文本的问题 - -## v0.4.3 - -### Breaking Changes - -配置文件 `main = "src/worker.js"` 改为 `main = "src/worker.ts"` - -### Changes - -- `telegram bot` 白名单配置 -- `ENABLE_WEBHOOK` 添加 webhook -- UI: admin 页面使用双层 tab -- UI: 登录后可直接主页切换地址 -- UI: 发件箱也采用左右分栏显示(类似收件箱) -- `SMTP IMAP Proxy` 添加发件箱查看 - -* feat: telegram bot TelegramSettings && webhook by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/244 -* fix build by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/245 -* feat: UI changes by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/247 -* feat: SMTP IMAP Proxy: add sendbox && UI: sendbox use split view by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/248 - -## v0.4.2 - -- 修复 smtp imap proxy sever 的一些 bug -- 修复 UI 界面文字错误, 界面增加版本号 -- 增加 telegram bot 文档 https://temp-mail-docs.awsl.uk/zh/guide/feature/telegram.html - -* fix: imap server by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/227 -* fix: Maintenance wrong label by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/229 -* feat: add version for frontend && backend by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/230 -* feat: add page functions proxy to make response faster by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/234 -* feat: add about page by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/235 -* feat: remove mailV1Alert && fix mobile showSideMargin by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/236 -* feat: telegram bot by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/238 -* fix: remove cleanup address due to many table need to be clean by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/240 -* feat: docs: Telegram Bot by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/241 -* fix: smtp_proxy: cannot decode 8bit && tg bot new random address by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/242 -* fix: smtp_proxy: update raise imap4.NoSuchMailbox by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/243 - -### v0.4.1 - -- 用户名限制最长30个字符 -- 修复 `/external/api/send_mail` 未返回的 bug (#222) -- 添加 `IMAP proxy` 服务,支持 `IMAP` 查看邮件 -- UI 界面增加版本号显示 - -* feat: use common function handleListQuery when query by page by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/220 -* fix: typos by @lwd-temp in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/221 -* fix: name max 30 && /external/api/send_mail not return result by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/222 -* fix: smtp_proxy_server support decode from mail charset by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/223 -* feat: add imap proxy server by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/225 -* feat: UI show version by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/226 - -### New Contributors - -* @lwd-temp made their first contribution in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/221 - -## v0.4.0 - -### DB Changes/Breaking changes - -新增 user 相关表,用于存储用户信息 - -- `db/2024-05-08-patch.sql` - -### config changs - -启用用户注册邮箱验证需要 `KV` - -```toml -# kv config for send email verification code -# [[kv_namespaces]] -# binding = "KV" -# id = "xxxx" -``` - -### function changs - -- 增加用户注册功能,可绑定邮箱地址,绑定后可自动获取邮箱JWT凭证 -- 增加默认以文本显示邮件,文本和HTML邮箱显示方式切换按钮 -- 修复 `BUG` 随机生成的邮箱名字不合法 #211 -- `admin` 邮件页面支持邮件内容搜索 #210 -- 修复删除地址时邮件未删除的BUG #213 -- UI 增加全局标签页位置配置, 侧边距配置 - -* feat: update docs by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/204 -* feat: add Deploy to Cloudflare Workers button by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/205 -* feat: add Deploy to Cloudflare Workers docs by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/206 -* feat: add UserLogin by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/209 -* feat: admin search mailbox && fix generateName multi dot && user jwt exp in 30 days && UI globalTabplacement && useSideMargin by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/214 -* feat: UI check openSettings in Login page by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/215 -* feat: UI move AdminContact to common by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/217 -* feat: docs by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/218 - -## v0.3.3 - -- 修复 Admin 删除邮件报错 -- UI: 回复邮件按钮, 引用原始邮件文本 #186 -- 添加发送邮件地址黑名单 -- 添加 `CF Turnstile` 人机验证配置 -- 添加 `/external/api/send_mail` 发送邮件 api, 使用 body 验证 #194 - -## v0.3.2 - -## What's Changed - -- UI: 添加回复邮件按钮 -- 添加定时清理功能,可在 admin 页面配置(需要在配置文件启用定时任务) -- 修复删除账户无反应的问题 - -* feat: UI: MailBox add reply button by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/187 -* feat: add cron auto clean up by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/189 -* fix: delete account by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/190 - -## v0.3.1 - -### DB Changes - -新增 `settings` 表,用于存储通用配置信息 - -- `db/2024-05-01-patch.sql` - -### Changes - -- `ENABLE_USER_CREATE_EMAIL` 是否允许用户创建邮件 -- 允许 admin 创建无前缀的邮件 -- 添加 `SMTP proxy server`,支持 SMTP 发送邮件 -- 修复某些情况浏览器无法加载 `wasm` 时使用 js 解析邮件 -- 页脚添加 `COPYRIGHT` -- UI 允许用户切换邮件展示模式 `v-html` / `iframe` -- 添加 `admin` 账户配置页面,支持配置用户注册名称黑名单 - -* feat: support admin create address && add ENABLE_USER_CREATE_EMAIL co… by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/175 -* feat: add SMTP proxy server by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/177 -* fix: cf ui var is string by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/178 -* fix: UI mailbox 100vh to 80vh by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/179 -* fix: smtp_proxy_server hostname && add docker image for linux/arm64 by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/180 -* fix: some browser do not support wasm by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/182 -* feat: add COPYRIGHT by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/183 -* feat: UI: add user page: useIframeShowMail && mailboxSplitSize by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/184 -* feat: add address_block_list for new address by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/185 - -## v0.3.0 - -### Breaking Changes - -`address` 表的前缀将从代码中迁移到 db 中,请将下面 sql 中的 `tmp` 替换为你的前缀,然后执行。 -如果你的数据很重要,请先备份数据库。 - -**注意替换前缀** - -```sql -update - address -set - name = 'tmp' || name; -``` - -### Changes - -- `address` 表的前缀将从代码中迁移到 db 中 -- `admin` 账户页面添加收发邮件数量 -- `admin` 发件页面默认显示全部 -- `admin` 发件权限页面支持搜索地址 -- `admin` 邮件页面使用左右分栏 UI - -* feat: remove PREFIX logic in db by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/171 -* feat: admin page add account mail count && sendbox default all && sen… by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/172 -* feat: all mail use MailBox Component by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/173 - -**Full Changelog**: https://github.com/dreamhunter2333/cloudflare_temp_email/compare/0.2.10...v0.3.0 - -## v0.2.10 - -- `ENABLE_USER_DELETE_EMAIL` 是否允许用户删除账户和邮件 -- `ENABLE_AUTO_REPLY` 是否启用自动回复 -- fetchAddressError 提示改进 -- 自动刷新显示倒计时 - -* feat: docs update by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/165 -* feat: add ENABLE_USER_DELETE_EMAIL && ENABLE_AUTO_REPLY && modify fetchAddressError i18n && UI: show autoRefreshInterval by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/169 - -## v0.2.9 - -- 添加富文本编辑器 -- admin 联系方式,不配置则不显示,可配置任意字符串 `ADMIN_CONTACT = "xx@xx.xxx"` -- 默认发送邮件余额,如果不设置,将为 0 `DEFAULT_SEND_BALANCE = 1` - -## v0.2.8 - -- 允许用户删除邮件 -- admin 修改发件权限时邮件通知用户 -- 发件权限默认 1 条 -- 添加 RATE_LIMITER 限流 发送邮件 和 新建地址 -- 一些 bug 修复 - -- feat: allow user delete mail && notify when send access changed by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/132 -- feat: requset_send_mail_access default 1 balance by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/143 -- fix: RATE_LIMITER not call jwt by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/146 -- fix: delete_address not delete address_sender by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/153 -- fix: send_balance not update when click sendmail by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/155 - -## v0.2.7 - -- Added user interface installation documentation -- Support email DKIM -- Rate limiting configuration for `/api/new_address` - -## v0.2.6 - -- Added admin query outbox page -- Add admin data cleaning page - -## 2024-04-12 v0.2.5 - -- support send email - -DB changes: - -- `db/2024-04-12-patch.sql` - -## 2024-04-10 v0.2.0 - -### Breaking Changes - -- remove `ENABLE_ATTACHMENT` config -- use rust wasm to parse email in frontend -- deprecated api moved to `/api/v1` - -### Rust Mail Parser - -由于 nodejs 解析 email 的库有些问题,此版本切换到使用 rust wasm 调用 rust 的mail 解析库 - -- 速度更快,附件支持好,可以显示邮件的附件图片 -- 解析支持更多 rfc 规范 - -Due to some problems with nodejs' email parsing library, this version switches to using rust wasm to call rust's mail parsing library. - -- Faster speed, good attachment support, can display attachment images of emails -- Parsing supports more rfc specifications - -### DB changs - -将 `mails` 表废弃,新的 `mail` 的 `raw` 文本将直接存入 `raw_mails` 表. -The `mails` table will be discarded, and the `raw` text of the new `mail` will be directly stored in the `raw_mails` table - -## Upgrade Step - -```bash -git checkout v0.2.0 -cd worker -wrangler d1 execute dev --file=../db/2024-04-09-patch.sql --remote -pnpm run deploy -cd ../frontend -pnpm run deploy -``` - -注意:对于历史邮件,请使用部署新网页查看旧数据。 -Note: For historical messages, use the Deploy New web page to view old data. - -```bash -git checkout feature/backup -cd frontend -# 创建一个新的 pages, 用于访问旧数据 -pnpm run deploy --project-name temp-email-v1 -``` - -## 2024-04-09 v0.0.0 - -release v0.0.0 - -## 2024-04-03 - -DB changes - -- `db/2024-04-03-patch.sql` - -Changes: - -- add delete account -- add admin panel search - -## 2024-01-13 - -DB changes - -- `db/2024-01-13-patch.sql` diff --git a/cloudflare_temp_email/LICENSE b/cloudflare_temp_email/LICENSE deleted file mode 100644 index 7fac17a..0000000 --- a/cloudflare_temp_email/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2023 Dream Hunter - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/cloudflare_temp_email/README.md b/cloudflare_temp_email/README.md deleted file mode 100644 index ebc959d..0000000 --- a/cloudflare_temp_email/README.md +++ /dev/null @@ -1,187 +0,0 @@ - -# Cloudflare 临时邮箱 - 免费搭建临时邮件服务 - -

- - docs - - - - - - MIT License - - - GitHub contributors - - - GitHub top language - - - - -

- -

- - Featured|HelloGitHub - -

- -

- 🇨🇳 中文文档 | - 🇺🇸 English Document -

- -> 本项目仅供学习和个人用途,请勿将其用于任何违法行为,否则后果自负。 - -**🎉 一个功能完整的临时邮箱服务!** - -- 🆓 **完全免费** - 基于 Cloudflare 免费服务构建,零成本运行 -- ⚡ **高性能** - Rust WASM 邮件解析,响应速度极快 -- 🎨 **现代化界面** - 响应式设计,支持多语言,操作简便 - -## 📚 部署文档 - 快速开始 - -[📖 部署文档](https://temp-mail-docs.awsl.uk) | [🚀 Github Action 部署文档](https://temp-mail-docs.awsl.uk/zh/guide/actions/github-action.html) - - - Deploy to Cloudflare Workers - - -## 📝 更新日志 - -查看 [CHANGELOG](CHANGELOG.md) 了解最新更新内容。 - -## 🎯 在线体验 - -立即体验 → [https://mail.awsl.uk/](https://mail.awsl.uk/) - -
-📊 服务状态监控(点击收缩/展开) - -| | | -| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [Backend](https://temp-email-api.awsl.uk/) | [![Deploy Backend Production](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/backend_deploy.yaml/badge.svg)](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/backend_deploy.yaml) ![](https://uptime.aks.awsl.icu/api/badge/10/status) ![](https://uptime.aks.awsl.icu/api/badge/10/uptime) ![](https://uptime.aks.awsl.icu/api/badge/10/ping) ![](https://uptime.aks.awsl.icu/api/badge/10/avg-response) ![](https://uptime.aks.awsl.icu/api/badge/10/cert-exp) ![](https://uptime.aks.awsl.icu/api/badge/10/response) | -| [Frontend](https://mail.awsl.uk/) | [![Deploy Frontend](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/frontend_deploy.yaml/badge.svg)](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/frontend_deploy.yaml) ![](https://uptime.aks.awsl.icu/api/badge/12/status) ![](https://uptime.aks.awsl.icu/api/badge/12/uptime) ![](https://uptime.aks.awsl.icu/api/badge/12/ping) ![](https://uptime.aks.awsl.icu/api/badge/12/avg-response) ![](https://uptime.aks.awsl.icu/api/badge/12/cert-exp) ![](https://uptime.aks.awsl.icu/api/badge/12/response) | - -
- -
-⭐ Star History(点击收缩/展开) - - - - - Star History Chart - - -
- -
-📖 目录(点击收缩/展开) - -- [Cloudflare 临时邮箱 - 免费搭建临时邮件服务](#cloudflare-临时邮箱---免费搭建临时邮件服务) - - [📚 部署文档 - 快速开始](#-部署文档---快速开始) - - [📝 更新日志](#-更新日志) - - [🎯 在线体验](#-在线体验) - - [✨ 核心功能](#-核心功能) - - [📧 邮件处理](#-邮件处理) - - [👥 用户管理](#-用户管理) - - [🔧 管理功能](#-管理功能) - - [🌐 多语言与界面](#-多语言与界面) - - [🤖 集成与扩展](#-集成与扩展) - - [🏗️ 技术架构](#️-技术架构) - - [🏛️ 系统架构](#️-系统架构) - - [🛠️ 技术栈](#️-技术栈) - - [📦 主要组件](#-主要组件) - - [🌟 加入社区](#-加入社区) - -
- -## ✨ 核心功能 - -
-✨ 核心功能详情(点击收缩/展开) - -### 📧 邮件处理 - -- [x] 使用 `rust wasm` 解析邮件,解析速度快,几乎所有邮件都能解析,node 的解析模块解析邮件失败的邮件,rust wasm 也能解析成功 -- [x] 支持发送邮件,支持 `DKIM` 验证 -- [x] 支持 `SMTP` 和 `Resend` 等多种发送方式 -- [x] 增加查看 `附件` 功能,支持附件图片显示 -- [x] 支持 S3 附件存储和删除功能 -- [x] 垃圾邮件检测和黑白名单配置 -- [x] 邮件转发功能,支持全局转发地址 - -### 👥 用户管理 - -- [x] 使用 `凭证` 重新登录之前的邮箱 -- [x] 添加完整的用户注册登录功能,可绑定邮箱地址,绑定后可自动获取邮箱JWT凭证切换不同邮箱 -- [x] 支持 `OAuth2` 第三方登录(Github、Authentik 等) -- [x] 支持 `Passkey` 无密码登录 -- [x] 用户角色管理,支持多角色域名和前缀配置 -- [x] 用户收件箱查看,支持地址和关键词过滤 - -### 🔧 管理功能 - -- [x] 完整的 admin 控制台 -- [x] `admin` 后台创建无前缀邮箱 -- [x] admin 用户管理页面,增加用户地址查看功能 -- [x] 定时清理功能,支持多种清理策略 -- [x] 获取自定义名字的邮箱,`admin` 可配置黑名单 -- [x] 增加访问密码,可作为私人站点 - -### 🌐 多语言与界面 - -- [x] 前后台均支持多语言 -- [x] 现代化 UI 设计,支持响应式布局 -- [x] 支持 Google Ads 集成 -- [x] 使用 shadow DOM 防止样式污染 -- [x] 支持 URL JWT 参数自动登录 - -### 🤖 集成与扩展 - -- [x] 完整的 `Telegram Bot` 支持,以及 `Telegram` 推送,Telegram Bot 小程序 -- [x] 添加 `SMTP proxy server`,支持 `SMTP` 发送邮件,`IMAP` 查看邮件 -- [x] Webhook 支持,消息推送集成 -- [x] 支持 `CF Turnstile` 人机验证 -- [x] 限流配置,防止滥用 - -
- -## 🏗️ 技术架构 - -
-🏗️ 技术架构详情(点击收缩/展开) - -### 🏛️ 系统架构 - -- **数据库**: Cloudflare D1 作为主数据库 -- **前端部署**: 使用 Cloudflare Pages 部署前端 -- **后端部署**: 使用 Cloudflare Workers 部署后端 -- **邮件转发**: 使用 Cloudflare Email Routing - -### 🛠️ 技术栈 - -- **前端**: Vue 3 + Vite + TypeScript -- **后端**: TypeScript + Cloudflare Workers -- **邮件解析**: Rust WASM (mail-parser-wasm) -- **数据库**: Cloudflare D1 (SQLite) -- **存储**: Cloudflare KV + R2 (可选 S3) -- **代理服务**: Python SMTP/IMAP Proxy Server - -### 📦 主要组件 - -- **Worker**: 核心后端服务 -- **Frontend**: Vue 3 用户界面 -- **Mail Parser WASM**: Rust 邮件解析模块 -- **SMTP Proxy Server**: Python 邮件代理服务 -- **Pages Functions**: Cloudflare Pages 中间件 -- **Documentation**: VitePress 文档站点 - -
- -## 🌟 加入社区 - -- [Telegram](https://t.me/cloudflare_temp_email) diff --git a/cloudflare_temp_email/README_EN.md b/cloudflare_temp_email/README_EN.md deleted file mode 100644 index 75fe1cf..0000000 --- a/cloudflare_temp_email/README_EN.md +++ /dev/null @@ -1,46 +0,0 @@ - -# Cloudflare Temp Email - -

- 🇨🇳 中文 | - 🇺🇸 English -

- -**A fully-featured temporary email service built on Cloudflare's free services.** - -> This project is for learning and personal use only. - -## 🚀 Quick Start - -- [📖 Documentation](https://temp-mail-docs.awsl.uk/en/) -- [🎯 Live Demo](https://mail.awsl.uk/) -- [📝 CHANGELOG](CHANGELOG.md) - -

- - Deploy to Cloudflare Workers - -

- -## ✨ Key Features - -- **� Email Processing**: Rust WASM parser, SMTP/IMAP support, attachments, auto-reply -- **👥 User Management**: OAuth2 login, Passkey authentication, role management -- **🌐 Admin Panel**: Complete admin console, user management, scheduled cleanup -- **🤖 Integrations**: Telegram Bot, webhooks, CAPTCHA, rate limiting -- **� Modern UI**: Multi-language, responsive design, JWT auto-login - -## 🏗️ Tech Stack - -- **Frontend**: Vue 3 + TypeScript + Vite -- **Backend**: Cloudflare Workers + D1 Database -- **Email**: Cloudflare Email Routing + Rust WASM Parser -- **Storage**: Cloudflare KV + R2 (optional S3) - -## 🌟 Community - -- [Telegram](https://t.me/cloudflare_temp_email) - -## 📄 License - -MIT License - see [LICENSE](LICENSE) for details. diff --git a/cloudflare_temp_email/db/2024-01-13-patch.sql b/cloudflare_temp_email/db/2024-01-13-patch.sql deleted file mode 100644 index 5394129..0000000 --- a/cloudflare_temp_email/db/2024-01-13-patch.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE - mails -ADD - message_id TEXT; diff --git a/cloudflare_temp_email/db/2024-04-03-patch.sql b/cloudflare_temp_email/db/2024-04-03-patch.sql deleted file mode 100644 index d79c5ae..0000000 --- a/cloudflare_temp_email/db/2024-04-03-patch.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE - address -ADD - updated_at DATETIME; diff --git a/cloudflare_temp_email/db/2024-04-09-patch.sql b/cloudflare_temp_email/db/2024-04-09-patch.sql deleted file mode 100644 index 27999d3..0000000 --- a/cloudflare_temp_email/db/2024-04-09-patch.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE IF NOT EXISTS raw_mails ( - id INTEGER PRIMARY KEY, - message_id TEXT, - source TEXT, - address TEXT, - raw TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); diff --git a/cloudflare_temp_email/db/2024-04-12-patch.sql b/cloudflare_temp_email/db/2024-04-12-patch.sql deleted file mode 100644 index b5874f4..0000000 --- a/cloudflare_temp_email/db/2024-04-12-patch.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE TABLE IF NOT EXISTS address_sender ( - id INTEGER PRIMARY KEY, - address TEXT UNIQUE, - balance INTEGER DEFAULT 0, - enabled INTEGER DEFAULT 1, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE IF NOT EXISTS sendbox ( - id INTEGER PRIMARY KEY, - address TEXT, - raw TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); diff --git a/cloudflare_temp_email/db/2024-05-01-patch.sql b/cloudflare_temp_email/db/2024-05-01-patch.sql deleted file mode 100644 index 542d268..0000000 --- a/cloudflare_temp_email/db/2024-05-01-patch.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE IF NOT EXISTS settings ( - key TEXT PRIMARY KEY, - value TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); diff --git a/cloudflare_temp_email/db/2024-05-08-patch.sql b/cloudflare_temp_email/db/2024-05-08-patch.sql deleted file mode 100644 index ac3b560..0000000 --- a/cloudflare_temp_email/db/2024-05-08-patch.sql +++ /dev/null @@ -1,21 +0,0 @@ -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY, - user_email TEXT UNIQUE NOT NULL, - password TEXT NOT NULL, - user_info TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_users_user_email ON users(user_email); - -CREATE TABLE IF NOT EXISTS users_address ( - id INTEGER PRIMARY KEY, - user_id INTEGER, - address_id INTEGER UNIQUE, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_users_address_user_id ON users_address(user_id); - -CREATE INDEX IF NOT EXISTS idx_users_address_address_id ON users_address(address_id); diff --git a/cloudflare_temp_email/db/2024-07-14-patch.sql b/cloudflare_temp_email/db/2024-07-14-patch.sql deleted file mode 100644 index 29c16fe..0000000 --- a/cloudflare_temp_email/db/2024-07-14-patch.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE TABLE IF NOT EXISTS user_roles ( - id INTEGER PRIMARY KEY, - user_id INTEGER UNIQUE NOT NULL, - role_text TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id); diff --git a/cloudflare_temp_email/db/2024-08-10-patch.sql b/cloudflare_temp_email/db/2024-08-10-patch.sql deleted file mode 100644 index 38a1241..0000000 --- a/cloudflare_temp_email/db/2024-08-10-patch.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE TABLE IF NOT EXISTS user_passkeys ( - id INTEGER PRIMARY KEY, - user_id INTEGER NOT NULL, - passkey_name TEXT NOT NULL, - passkey_id TEXT NOT NULL, - passkey TEXT NOT NULL, - counter INTEGER DEFAULT 0, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_user_passkeys_user_id ON user_passkeys(user_id); - -CREATE UNIQUE INDEX IF NOT EXISTS idx_user_passkeys_user_id_passkey_id ON user_passkeys(user_id, passkey_id); diff --git a/cloudflare_temp_email/db/schema.sql b/cloudflare_temp_email/db/schema.sql deleted file mode 100644 index 153f66f..0000000 --- a/cloudflare_temp_email/db/schema.sql +++ /dev/null @@ -1,105 +0,0 @@ -CREATE TABLE IF NOT EXISTS raw_mails ( - id INTEGER PRIMARY KEY, - message_id TEXT, - source TEXT, - address TEXT, - raw TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_raw_mails_address ON raw_mails(address); - -CREATE TABLE IF NOT EXISTS address ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT UNIQUE, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_address_name ON address(name); - -CREATE TABLE IF NOT EXISTS auto_reply_mails ( - id INTEGER PRIMARY KEY, - source_prefix TEXT, - name TEXT, - address TEXT UNIQUE, - subject TEXT, - message TEXT, - enabled INTEGER DEFAULT 1, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_auto_reply_mails_address ON auto_reply_mails(address); - -CREATE TABLE IF NOT EXISTS address_sender ( - id INTEGER PRIMARY KEY, - address TEXT UNIQUE, - balance INTEGER DEFAULT 0, - enabled INTEGER DEFAULT 1, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_address_sender_address ON address_sender(address); - -CREATE TABLE IF NOT EXISTS sendbox ( - id INTEGER PRIMARY KEY, - address TEXT, - raw TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_sendbox_address ON sendbox(address); - -CREATE TABLE IF NOT EXISTS settings ( - key TEXT PRIMARY KEY, - value TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY, - user_email TEXT UNIQUE NOT NULL, - password TEXT NOT NULL, - user_info TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_users_user_email ON users(user_email); - -CREATE TABLE IF NOT EXISTS users_address ( - id INTEGER PRIMARY KEY, - user_id INTEGER, - address_id INTEGER UNIQUE, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_users_address_user_id ON users_address(user_id); - -CREATE INDEX IF NOT EXISTS idx_users_address_address_id ON users_address(address_id); - -CREATE TABLE IF NOT EXISTS user_roles ( - id INTEGER PRIMARY KEY, - user_id INTEGER UNIQUE NOT NULL, - role_text TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id); - -CREATE TABLE IF NOT EXISTS user_passkeys ( - id INTEGER PRIMARY KEY, - user_id INTEGER NOT NULL, - passkey_name TEXT NOT NULL, - passkey_id TEXT NOT NULL, - passkey TEXT NOT NULL, - counter INTEGER DEFAULT 0, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_user_passkeys_user_id ON user_passkeys(user_id); - -CREATE UNIQUE INDEX IF NOT EXISTS idx_user_passkeys_user_id_passkey_id ON user_passkeys(user_id, passkey_id); diff --git a/cloudflare_temp_email/frontend/.env.example b/cloudflare_temp_email/frontend/.env.example deleted file mode 100644 index e2bdffe..0000000 --- a/cloudflare_temp_email/frontend/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -VITE_API_BASE=https://temp-email-api.xxx.xxx -VITE_CF_WEB_ANALY_TOKEN= -VITE_IS_TELEGRAM=false diff --git a/cloudflare_temp_email/frontend/.env.pages b/cloudflare_temp_email/frontend/.env.pages deleted file mode 100644 index 7a56d96..0000000 --- a/cloudflare_temp_email/frontend/.env.pages +++ /dev/null @@ -1,2 +0,0 @@ -VITE_API_BASE= -VITE_CF_WEB_ANALY_TOKEN= diff --git a/cloudflare_temp_email/frontend/README.md b/cloudflare_temp_email/frontend/README.md deleted file mode 100644 index 2a46112..0000000 --- a/cloudflare_temp_email/frontend/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# cloudflare_temp_email - -This template should help get you started developing with Vue 3 in Vite. - -## Recommended IDE Setup - -[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). - -## Customize configuration - -See [Vite Configuration Reference](https://vitejs.dev/config/). - -## Project Setup - -```sh -npm install -``` - -### Compile and Hot-Reload for Development - -```sh -npm run dev -``` - -### Compile and Minify for Production - -```sh -npm run build -``` diff --git a/cloudflare_temp_email/frontend/index.html b/cloudflare_temp_email/frontend/index.html deleted file mode 100644 index f32967b..0000000 --- a/cloudflare_temp_email/frontend/index.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - Temp Email - - - - - - - - - - - - -
- - - - diff --git a/cloudflare_temp_email/frontend/package.json b/cloudflare_temp_email/frontend/package.json deleted file mode 100644 index 7e0c2c3..0000000 --- a/cloudflare_temp_email/frontend/package.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "name": "cloudflare_temp_email", - "version": "1.0.5", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build -m prod --emptyOutDir", - "build:release": "vite build -m example --emptyOutDir", - "build:pages": "vite build -m pages --emptyOutDir", - "build:pages:nopwa": "VITE_PWA_DISABLED=true vite build -m pages --emptyOutDir", - "build:telegram": "VITE_IS_TELEGRAM=true vite build -m prod --emptyOutDir", - "build:telegram:release": "VITE_IS_TELEGRAM=true vite build -m example --emptyOutDir", - "preview": "vite preview", - "deploy:telegram": "npm run build:telegram && wrangler pages deploy ./dist --branch production", - "deploy:actions:telegram": "npm run build:telegram && wrangler pages deploy ./dist", - "deploy:preview": "npm run build && wrangler pages deploy ./dist --branch preview", - "deploy": "npm run build && wrangler pages deploy ./dist --branch production", - "deploy:actions": "npm run build && wrangler pages deploy ./dist" - }, - "dependencies": { - "@simplewebauthn/browser": "10.0.0", - "@unhead/vue": "^1.11.20", - "@vueuse/core": "^12.8.2", - "@wangeditor/editor": "^5.1.23", - "@wangeditor/editor-for-vue": "^5.1.12", - "axios": "^1.11.0", - "jszip": "^3.10.1", - "mail-parser-wasm": "^0.2.1", - "naive-ui": "^2.42.0", - "postal-mime": "^2.4.4", - "vooks": "^0.2.12", - "vue": "^3.5.21", - "vue-clipboard3": "^2.0.0", - "vue-i18n": "^11.1.11", - "vue-router": "^4.5.1" - }, - "devDependencies": { - "@vicons/fa": "^0.13.0", - "@vicons/material": "^0.13.0", - "@vitejs/plugin-vue": "^5.2.4", - "unplugin-auto-import": "^19.3.0", - "unplugin-vue-components": "^28.8.0", - "vite": "^6.3.5", - "vite-plugin-pwa": "^1.0.3", - "vite-plugin-top-level-await": "^1.6.0", - "vite-plugin-wasm": "^3.5.0", - "workbox-build": "^7.3.0", - "workbox-window": "^7.3.0", - "wrangler": "^4.34.0" - }, - "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39" -} diff --git a/cloudflare_temp_email/frontend/public/logo.png b/cloudflare_temp_email/frontend/public/logo.png deleted file mode 100644 index 97829b1..0000000 Binary files a/cloudflare_temp_email/frontend/public/logo.png and /dev/null differ diff --git a/cloudflare_temp_email/frontend/src/App.vue b/cloudflare_temp_email/frontend/src/App.vue deleted file mode 100644 index 35c621a..0000000 --- a/cloudflare_temp_email/frontend/src/App.vue +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - - - diff --git a/cloudflare_temp_email/frontend/src/api/index.js b/cloudflare_temp_email/frontend/src/api/index.js deleted file mode 100644 index 2e1bfb7..0000000 --- a/cloudflare_temp_email/frontend/src/api/index.js +++ /dev/null @@ -1,206 +0,0 @@ -import { useGlobalState } from '../store' -import { h } from 'vue' -import axios from 'axios' - -import i18n from '../i18n' - -const API_BASE = import.meta.env.VITE_API_BASE || ""; -const { - loading, auth, jwt, settings, openSettings, - userOpenSettings, userSettings, announcement, - showAuth, adminAuth, showAdminAuth, userJwt -} = useGlobalState(); - -const instance = axios.create({ - baseURL: API_BASE, - timeout: 30000, - validateStatus: (status) => status >= 200 && status <= 500 -}); - -const apiFetch = async (path, options = {}) => { - loading.value = true; - try { - const response = await instance.request(path, { - method: options.method || 'GET', - data: options.body || null, - headers: { - 'x-lang': i18n.global.locale.value, - 'x-user-token': options.userJwt || userJwt.value, - 'x-user-access-token': userSettings.value.access_token, - 'x-custom-auth': auth.value, - 'x-admin-auth': adminAuth.value, - 'Authorization': `Bearer ${jwt.value}`, - 'Content-Type': 'application/json', - }, - }); - if (response.status === 401 && path.startsWith("/admin")) { - showAdminAuth.value = true; - } - if (response.status === 401 && openSettings.value.auth) { - showAuth.value = true; - } - if (response.status >= 300) { - throw new Error(`[${response.status}]: ${response.data}` || "error"); - } - const data = response.data; - return data; - } catch (error) { - if (error.response) { - throw new Error(`Code ${error.response.status}: ${error.response.data}` || "error"); - } - throw error; - } finally { - loading.value = false; - } -} - -const getOpenSettings = async (message, notification) => { - try { - const res = await api.fetch("/open_api/settings"); - const domainLabels = res["domainLabels"] || []; - if (res["domains"]?.length < 1) { - message.error("No domains found, please check your worker settings"); - } - Object.assign(openSettings.value, { - ...res, - title: res["title"] || "", - prefix: res["prefix"] || "", - minAddressLen: res["minAddressLen"] || 1, - maxAddressLen: res["maxAddressLen"] || 30, - needAuth: res["needAuth"] || false, - defaultDomains: res["defaultDomains"] || [], - domains: res["domains"].map((domain, index) => { - return { - label: domainLabels.length > index ? domainLabels[index] : domain, - value: domain - } - }), - adminContact: res["adminContact"] || "", - enableUserCreateEmail: res["enableUserCreateEmail"] || false, - disableAnonymousUserCreateEmail: res["disableAnonymousUserCreateEmail"] || false, - disableCustomAddressName: res["disableCustomAddressName"] || false, - enableUserDeleteEmail: res["enableUserDeleteEmail"] || false, - enableAutoReply: res["enableAutoReply"] || false, - enableIndexAbout: res["enableIndexAbout"] || false, - copyright: res["copyright"] || openSettings.value.copyright, - cfTurnstileSiteKey: res["cfTurnstileSiteKey"] || "", - enableWebhook: res["enableWebhook"] || false, - isS3Enabled: res["isS3Enabled"] || false, - }); - if (openSettings.value.needAuth) { - showAuth.value = true; - } - if (openSettings.value.announcement - && !openSettings.value.fetched - && (openSettings.value.announcement != announcement.value - || openSettings.value.alwaysShowAnnouncement) - ) { - announcement.value = openSettings.value.announcement; - notification.info({ - content: () => { - return h("div", { - innerHTML: announcement.value - }); - } - }); - } - } catch (error) { - message.error(error.message || "error"); - } finally { - openSettings.value.fetched = true; - } -} - -const getSettings = async () => { - try { - if (typeof jwt.value != 'string' || jwt.value.trim() === '' || jwt.value === 'undefined') { - return ""; - } - const res = await apiFetch("/api/settings");; - settings.value = { - address: res["address"], - auto_reply: res["auto_reply"], - send_balance: res["send_balance"], - }; - } finally { - settings.value.fetched = true; - } -} - - -const getUserOpenSettings = async (message) => { - try { - const res = await api.fetch(`/user_api/open_settings`); - Object.assign(userOpenSettings.value, res); - } catch (error) { - message.error(error.message || "fetch settings failed"); - } finally { - userOpenSettings.value.fetched = true; - } -} - -const getUserSettings = async (message) => { - try { - if (!userJwt.value) return; - const res = await api.fetch("/user_api/settings") - Object.assign(userSettings.value, res) - // auto refresh user jwt - if (userSettings.value.new_user_token) { - try { - await api.fetch("/user_api/settings", { - userJwt: userSettings.value.new_user_token, - }) - userJwt.value = userSettings.value.new_user_token; - console.log("User JWT updated successfully"); - } - catch (error) { - console.error("Failed to update user JWT", error); - } - } - } catch (error) { - message?.error(error.message || "error"); - } finally { - userSettings.value.fetched = true; - } -} - -const adminShowAddressCredential = async (id) => { - try { - const { jwt: addressCredential } = await apiFetch(`/admin/show_password/${id}`); - return addressCredential; - } catch (error) { - throw error; - } -} - -const adminDeleteAddress = async (id) => { - try { - await apiFetch(`/admin/delete_address/${id}`, { - method: 'DELETE' - }); - } catch (error) { - throw error; - } -} - -const bindUserAddress = async () => { - if (!userJwt.value) return; - try { - await apiFetch(`/user_api/bind_address`, { - method: 'POST', - }); - } catch (error) { - throw error; - } -} - -export const api = { - fetch: apiFetch, - getSettings, - getOpenSettings, - getUserOpenSettings, - getUserSettings, - adminShowAddressCredential, - adminDeleteAddress, - bindUserAddress, -} diff --git a/cloudflare_temp_email/frontend/src/components/MailBox.vue b/cloudflare_temp_email/frontend/src/components/MailBox.vue deleted file mode 100644 index 0da47f1..0000000 --- a/cloudflare_temp_email/frontend/src/components/MailBox.vue +++ /dev/null @@ -1,587 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/components/MailContentRenderer.vue b/cloudflare_temp_email/frontend/src/components/MailContentRenderer.vue deleted file mode 100644 index ab5b143..0000000 --- a/cloudflare_temp_email/frontend/src/components/MailContentRenderer.vue +++ /dev/null @@ -1,282 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/components/SendBox.vue b/cloudflare_temp_email/frontend/src/components/SendBox.vue deleted file mode 100644 index 8da45a9..0000000 --- a/cloudflare_temp_email/frontend/src/components/SendBox.vue +++ /dev/null @@ -1,405 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/components/ShadowHtmlComponent.vue b/cloudflare_temp_email/frontend/src/components/ShadowHtmlComponent.vue deleted file mode 100644 index 7340712..0000000 --- a/cloudflare_temp_email/frontend/src/components/ShadowHtmlComponent.vue +++ /dev/null @@ -1,75 +0,0 @@ - - - diff --git a/cloudflare_temp_email/frontend/src/components/Turnstile.vue b/cloudflare_temp_email/frontend/src/components/Turnstile.vue deleted file mode 100644 index 89df493..0000000 --- a/cloudflare_temp_email/frontend/src/components/Turnstile.vue +++ /dev/null @@ -1,90 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/components/WebhookComponent.vue b/cloudflare_temp_email/frontend/src/components/WebhookComponent.vue deleted file mode 100644 index 0361ce8..0000000 --- a/cloudflare_temp_email/frontend/src/components/WebhookComponent.vue +++ /dev/null @@ -1,179 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/constant/index.ts b/cloudflare_temp_email/frontend/src/constant/index.ts deleted file mode 100644 index 36cb8ab..0000000 --- a/cloudflare_temp_email/frontend/src/constant/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -const COMMOM_MAIL = [ - "gmail.com", "163.com", "126.com", "qq.com", "outlook.com", "hotmail.com", - "icloud.com", "yahoo.com", "foxmail.com" -] - -export default { - COMMOM_MAIL -} diff --git a/cloudflare_temp_email/frontend/src/i18n.ts b/cloudflare_temp_email/frontend/src/i18n.ts deleted file mode 100644 index c11520a..0000000 --- a/cloudflare_temp_email/frontend/src/i18n.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createI18n } from 'vue-i18n' - -const i18n = createI18n({ - legacy: false, // you must set `false`, to use Composition API - locale: 'zh', // set locale - fallbackLocale: 'en', // set fallback locale - 'en': { - messages: {} - }, - 'zh': { - messages: {} - } -}) - -export default i18n; diff --git a/cloudflare_temp_email/frontend/src/main.js b/cloudflare_temp_email/frontend/src/main.js deleted file mode 100644 index 45271d0..0000000 --- a/cloudflare_temp_email/frontend/src/main.js +++ /dev/null @@ -1,13 +0,0 @@ -import { createApp } from 'vue' -import { createHead } from '@unhead/vue' - -import App from './App.vue' -import router from './router' -import i18n from './i18n' - -const head = createHead() -const app = createApp(App) -app.use(i18n) -app.use(router) -app.use(head) -app.mount('#app') diff --git a/cloudflare_temp_email/frontend/src/models/index.ts b/cloudflare_temp_email/frontend/src/models/index.ts deleted file mode 100644 index 1fbaf1a..0000000 --- a/cloudflare_temp_email/frontend/src/models/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type UserOauth2Settings = { - name: string; - clientID: string; - clientSecret: string; - authorizationURL: string; - accessTokenURL: string; - accessTokenFormat?: string; - userInfoURL: string; - redirectURL: string; - logoutURL?: string; - userEmailKey: string; - scope: string; - enableMailAllowList?: boolean | undefined; - mailAllowList?: string[] | undefined; -} diff --git a/cloudflare_temp_email/frontend/src/router/index.js b/cloudflare_temp_email/frontend/src/router/index.js deleted file mode 100644 index 078464b..0000000 --- a/cloudflare_temp_email/frontend/src/router/index.js +++ /dev/null @@ -1,60 +0,0 @@ -import { createRouter, createWebHistory } from 'vue-router' -import Index from '../views/Index.vue' -import User from '../views/User.vue' -import UserOauth2Callback from '../views/user/UserOauth2Callback.vue' -import i18n from '../i18n' -import { useGlobalState } from '../store' - -const { jwt } = useGlobalState() - -const router = createRouter({ - history: createWebHistory(), - routes: [ - { - path: '/', - alias: "/:lang/", - component: Index - }, - { - path: '/user', - alias: "/:lang/user", - component: User - }, - { - path: '/user/oauth2/callback', - alias: "/:lang/user/oauth2/callback", - component: UserOauth2Callback - }, - { - path: '/admin', - alias: "/:lang/admin", - component: () => import('../views/Admin.vue') - }, - { - path: '/telegram_mail', - alias: "/:lang/telegram_mail", - component: () => import('../views/telegram/Mail.vue') - }, - { - name: 'not-found', - path: '/:pathMatch(.*)*', - redirect: '/' - } - ] -}); - - -router.beforeEach((to, from, next) => { - if (to.params.lang && ['en', 'zh'].includes(to.params.lang)) { - i18n.global.locale.value = to.params.lang - } else { - i18n.global.locale.value = 'zh' - } - // check if query parameter has jwt, set it to store - if (to.query.jwt) { - jwt.value = to.query.jwt; - } - next() -}); - -export default router diff --git a/cloudflare_temp_email/frontend/src/store/index.js b/cloudflare_temp_email/frontend/src/store/index.js deleted file mode 100644 index a5a18f7..0000000 --- a/cloudflare_temp_email/frontend/src/store/index.js +++ /dev/null @@ -1,150 +0,0 @@ -import { computed, ref } from "vue"; -import { - createGlobalState, useStorage, useDark, useToggle, - useLocalStorage, useSessionStorage -} from '@vueuse/core' - -export const useGlobalState = createGlobalState( - () => { - const isDark = useDark() - const toggleDark = useToggle(isDark) - const loading = ref(false); - const announcement = useLocalStorage('announcement', ''); - const useSimpleIndex = useLocalStorage('useSimpleIndex', false); - const openSettings = ref({ - fetched: false, - title: '', - announcement: '', - alwaysShowAnnouncement: false, - prefix: '', - addressRegex: '', - needAuth: false, - adminContact: '', - enableUserCreateEmail: false, - disableAnonymousUserCreateEmail: false, - disableCustomAddressName: false, - enableUserDeleteEmail: false, - enableAutoReply: false, - enableIndexAbout: false, - /** @type {string[]} */ - defaultDomains: [], - /** @type {Array<{label: string, value: string}>} */ - domains: [], - copyright: 'Dream Hunter', - cfTurnstileSiteKey: '', - enableWebhook: false, - isS3Enabled: false, - showGithub: true, - disableAdminPasswordCheck: false, - }) - const settings = ref({ - fetched: false, - send_balance: 0, - address: '', - auto_reply: { - subject: '', - message: '', - enabled: false, - source_prefix: '', - name: '', - } - }); - const sendMailModel = useSessionStorage('sendMailModel', { - fromName: "", - toName: "", - toMail: "", - subject: "", - contentType: 'text', - content: "", - }); - const showAuth = ref(false); - const showAddressCredential = ref(false); - const showAdminAuth = ref(false); - const auth = useStorage('auth', ''); - const adminAuth = useStorage('adminAuth', ''); - const jwt = useStorage('jwt', ''); - const adminTab = useSessionStorage('adminTab', "account"); - const adminMailTabAddress = ref(""); - const adminSendBoxTabAddress = ref(""); - const mailboxSplitSize = useStorage('mailboxSplitSize', 0.25); - const useIframeShowMail = useStorage('useIframeShowMail', false); - const preferShowTextMail = useStorage('preferShowTextMail', false); - const userJwt = useStorage('userJwt', ''); - const userTab = useSessionStorage('userTab', 'address_management'); - const indexTab = useSessionStorage('indexTab', 'mailbox'); - const globalTabplacement = useStorage('globalTabplacement', 'top'); - const useSideMargin = useStorage('useSideMargin', true); - const useUTCDate = useStorage('useUTCDate', false); - const autoRefresh = useStorage('autoRefresh', false); - const configAutoRefreshInterval = useStorage("configAutoRefreshInterval", 60); - const userOpenSettings = ref({ - fetched: false, - enable: false, - enableMailVerify: false, - /** @type {{ clientID: string, name: string }[]} */ - oauth2ClientIDs: [], - }); - const userSettings = ref({ - /** @type {boolean} */ - fetched: false, - /** @type {string} */ - user_email: '', - /** @type {number} */ - user_id: 0, - /** @type {boolean} */ - is_admin: false, - /** @type {string | null} */ - access_token: null, - /** @type {string | null} */ - new_user_token: null, - /** @type {null | {domains: string[] | undefined | null, role: string, prefix: string | undefined | null}} */ - user_role: null, - }); - const showAdminPage = computed(() => - !!adminAuth.value - || userSettings.value.is_admin - || openSettings.value.disableAdminPasswordCheck - ); - const telegramApp = ref(window.Telegram?.WebApp || {}); - const isTelegram = ref(!!window.Telegram?.WebApp?.initData); - const userOauth2SessionState = useSessionStorage('userOauth2SessionState', ''); - const userOauth2SessionClientID = useSessionStorage('userOauth2SessionClientID', ''); - return { - isDark, - toggleDark, - loading, - settings, - sendMailModel, - announcement, - openSettings, - showAuth, - showAddressCredential, - auth, - jwt, - adminAuth, - showAdminAuth, - adminTab, - adminMailTabAddress, - adminSendBoxTabAddress, - mailboxSplitSize, - useIframeShowMail, - preferShowTextMail, - userJwt, - userTab, - indexTab, - userOpenSettings, - userSettings, - globalTabplacement, - useSideMargin, - useUTCDate, - autoRefresh, - configAutoRefreshInterval, - telegramApp, - isTelegram, - showAdminPage, - userOauth2SessionState, - userOauth2SessionClientID, - useSimpleIndex, - } - }, -) diff --git a/cloudflare_temp_email/frontend/src/utils/composables.js b/cloudflare_temp_email/frontend/src/utils/composables.js deleted file mode 100644 index 85c3184..0000000 --- a/cloudflare_temp_email/frontend/src/utils/composables.js +++ /dev/null @@ -1,8 +0,0 @@ -import { useBreakpoint, useMemo } from 'vooks' - -export function useIsMobile() { - const breakpointRef = useBreakpoint() - return useMemo(() => { - return breakpointRef.value === 'xs' - }) -} diff --git a/cloudflare_temp_email/frontend/src/utils/email-parser.js b/cloudflare_temp_email/frontend/src/utils/email-parser.js deleted file mode 100644 index f25b095..0000000 --- a/cloudflare_temp_email/frontend/src/utils/email-parser.js +++ /dev/null @@ -1,82 +0,0 @@ -import PostalMime from 'postal-mime'; - -function humanFileSize(size) { - const i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); - return parseFloat((size / Math.pow(1024, i)).toFixed(2)) + ' ' + ['B', 'KB', 'MB', 'GB', 'TB'][i]; -} - -export async function processItem(item) { - // Try to parse the email using mail-parser-wasm - item.originalSource = item.source; - try { - const { parse_message } = await import('mail-parser-wasm'); - const parsedEmail = parse_message(item.raw); - item.source = parsedEmail.sender || item.source; - item.subject = parsedEmail.subject || ''; - item.message = parsedEmail.body_html || parsedEmail.text || ''; - item.text = parsedEmail.text || ''; - item.attachments = parsedEmail.attachments?.map((a_item) => { - const blob = new Blob( - [a_item.content], - { type: a_item.content_type || 'application/octet-stream' } - ); - const blob_url = URL.createObjectURL(blob); - if (a_item.content_id && a_item.content_id.length > 0) { - item.message = item.message.replace(`cid:${a_item.content_id}`, blob_url); - } - return { - id: a_item.content_id || Math.random().toString(36).substring(2, 15), - filename: a_item.filename || a_item.content_id || "", - size: humanFileSize(a_item.content?.length || 0), - url: blob_url, - blob: blob - } - }) || []; - } catch (error) { - console.log('Error parsing email with mail-parser-wasm'); - console.error(error); - } - if (item.subject && item.subject.length > 0 && item.message && item.message.length > 0) { - return item; - } - // Fallback to PostalMime - try { - const parsedEmail = await PostalMime.parse(item.raw); - item.source = parsedEmail.from.address || item.source; - if (parsedEmail.from.address && parsedEmail.from.name) { - item.source = `${parsedEmail.from.name} <${parsedEmail.from.address}>`; - } - item.subject = parsedEmail.subject || 'No Subject'; - item.message = parsedEmail.html || parsedEmail.text || item.raw; - item.text = parsedEmail.text || ''; - item.attachments = parsedEmail.attachments?.map((a_item) => { - const blob = new Blob( - [a_item.content], - { type: a_item.mimeType || 'application/octet-stream' } - ); - const blob_url = URL.createObjectURL(blob) - if (a_item.contentId && a_item.contentId.length > 0) { - item.message = item.message.replace(`cid:${a_item.contentId}`, blob_url); - } - return { - id: a_item.contentId || Math.random().toString(36).substring(2, 15), - filename: a_item.filename || a_item.contentId || "", - size: humanFileSize(a_item.content?.length || 0), - url: blob_url, - blob: blob - } - }) || []; - } catch (error) { - console.log('Error parsing email with PostalMime'); - console.error(error); - item.subject = 'No Subject'; - item.message = item.raw; - } - return item; -} - -export function getDownloadEmlUrl(raw) { - return URL.createObjectURL( - new Blob([raw], { type: 'text/plain' } - )) -} diff --git a/cloudflare_temp_email/frontend/src/utils/index.ts b/cloudflare_temp_email/frontend/src/utils/index.ts deleted file mode 100644 index 57400ae..0000000 --- a/cloudflare_temp_email/frontend/src/utils/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const hashPassword = async (password: string) => { - // user crypto to hash password - const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(password)); - const hashArray = Array.from(new Uint8Array(digest)); - return hashArray.map(byte => byte.toString(16).padStart(2, '0')).join(''); -} - -export const getRouterPathWithLang = (path: string, lang: string) => { - if (!lang || lang === 'zh') { - return path; - } - return `/${lang}${path}`; -} - -export const utcToLocalDate = (utcDate: string, useUTCDate: boolean) => { - const utcDateString = `${utcDate} UTC`; - if (useUTCDate) { - return utcDateString; - } - try { - const date = new Date(utcDateString); - // if invalid date string - if (isNaN(date.getTime())) return utcDateString; - - return date.toLocaleString(); - } catch (e) { - console.error(e); - } - return utcDateString; -} diff --git a/cloudflare_temp_email/frontend/src/views/Admin.vue b/cloudflare_temp_email/frontend/src/views/Admin.vue deleted file mode 100644 index 4031874..0000000 --- a/cloudflare_temp_email/frontend/src/views/Admin.vue +++ /dev/null @@ -1,231 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/Footer.vue b/cloudflare_temp_email/frontend/src/views/Footer.vue deleted file mode 100644 index c959038..0000000 --- a/cloudflare_temp_email/frontend/src/views/Footer.vue +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/Header.vue b/cloudflare_temp_email/frontend/src/views/Header.vue deleted file mode 100644 index 489b635..0000000 --- a/cloudflare_temp_email/frontend/src/views/Header.vue +++ /dev/null @@ -1,306 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/Index.vue b/cloudflare_temp_email/frontend/src/views/Index.vue deleted file mode 100644 index d869bb9..0000000 --- a/cloudflare_temp_email/frontend/src/views/Index.vue +++ /dev/null @@ -1,191 +0,0 @@ - - - diff --git a/cloudflare_temp_email/frontend/src/views/User.vue b/cloudflare_temp_email/frontend/src/views/User.vue deleted file mode 100644 index 229f027..0000000 --- a/cloudflare_temp_email/frontend/src/views/User.vue +++ /dev/null @@ -1,53 +0,0 @@ - - - diff --git a/cloudflare_temp_email/frontend/src/views/admin/Account.vue b/cloudflare_temp_email/frontend/src/views/admin/Account.vue deleted file mode 100644 index 4d6b3cf..0000000 --- a/cloudflare_temp_email/frontend/src/views/admin/Account.vue +++ /dev/null @@ -1,398 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/admin/AccountSettings.vue b/cloudflare_temp_email/frontend/src/views/admin/AccountSettings.vue deleted file mode 100644 index 0dab688..0000000 --- a/cloudflare_temp_email/frontend/src/views/admin/AccountSettings.vue +++ /dev/null @@ -1,296 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/admin/CreateAccount.vue b/cloudflare_temp_email/frontend/src/views/admin/CreateAccount.vue deleted file mode 100644 index 3c88bcc..0000000 --- a/cloudflare_temp_email/frontend/src/views/admin/CreateAccount.vue +++ /dev/null @@ -1,108 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/admin/DatabaseManager.vue b/cloudflare_temp_email/frontend/src/views/admin/DatabaseManager.vue deleted file mode 100644 index dc51c0e..0000000 --- a/cloudflare_temp_email/frontend/src/views/admin/DatabaseManager.vue +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/admin/MailWebhook.vue b/cloudflare_temp_email/frontend/src/views/admin/MailWebhook.vue deleted file mode 100644 index f3abef7..0000000 --- a/cloudflare_temp_email/frontend/src/views/admin/MailWebhook.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - diff --git a/cloudflare_temp_email/frontend/src/views/admin/Mails.vue b/cloudflare_temp_email/frontend/src/views/admin/Mails.vue deleted file mode 100644 index 1c64d5d..0000000 --- a/cloudflare_temp_email/frontend/src/views/admin/Mails.vue +++ /dev/null @@ -1,64 +0,0 @@ - - - diff --git a/cloudflare_temp_email/frontend/src/views/admin/MailsUnknow.vue b/cloudflare_temp_email/frontend/src/views/admin/MailsUnknow.vue deleted file mode 100644 index 064186a..0000000 --- a/cloudflare_temp_email/frontend/src/views/admin/MailsUnknow.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/cloudflare_temp_email/frontend/src/views/admin/Maintenance.vue b/cloudflare_temp_email/frontend/src/views/admin/Maintenance.vue deleted file mode 100644 index 6cf2c8f..0000000 --- a/cloudflare_temp_email/frontend/src/views/admin/Maintenance.vue +++ /dev/null @@ -1,189 +0,0 @@ - - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/admin/SendBox.vue b/cloudflare_temp_email/frontend/src/views/admin/SendBox.vue deleted file mode 100644 index 14778d0..0000000 --- a/cloudflare_temp_email/frontend/src/views/admin/SendBox.vue +++ /dev/null @@ -1,54 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/admin/SendMail.vue b/cloudflare_temp_email/frontend/src/views/admin/SendMail.vue deleted file mode 100644 index d9c59ca..0000000 --- a/cloudflare_temp_email/frontend/src/views/admin/SendMail.vue +++ /dev/null @@ -1,199 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/admin/SenderAccess.vue b/cloudflare_temp_email/frontend/src/views/admin/SenderAccess.vue deleted file mode 100644 index 3513622..0000000 --- a/cloudflare_temp_email/frontend/src/views/admin/SenderAccess.vue +++ /dev/null @@ -1,224 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/admin/Statistics.vue b/cloudflare_temp_email/frontend/src/views/admin/Statistics.vue deleted file mode 100644 index c7ea22c..0000000 --- a/cloudflare_temp_email/frontend/src/views/admin/Statistics.vue +++ /dev/null @@ -1,125 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/admin/Telegram.vue b/cloudflare_temp_email/frontend/src/views/admin/Telegram.vue deleted file mode 100644 index 9c846f6..0000000 --- a/cloudflare_temp_email/frontend/src/views/admin/Telegram.vue +++ /dev/null @@ -1,185 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/admin/UserAddressManagement.vue b/cloudflare_temp_email/frontend/src/views/admin/UserAddressManagement.vue deleted file mode 100644 index 8e218f9..0000000 --- a/cloudflare_temp_email/frontend/src/views/admin/UserAddressManagement.vue +++ /dev/null @@ -1,94 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/admin/UserManagement.vue b/cloudflare_temp_email/frontend/src/views/admin/UserManagement.vue deleted file mode 100644 index 408ef38..0000000 --- a/cloudflare_temp_email/frontend/src/views/admin/UserManagement.vue +++ /dev/null @@ -1,434 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/admin/UserOauth2Settings.vue b/cloudflare_temp_email/frontend/src/views/admin/UserOauth2Settings.vue deleted file mode 100644 index 24af487..0000000 --- a/cloudflare_temp_email/frontend/src/views/admin/UserOauth2Settings.vue +++ /dev/null @@ -1,274 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/admin/UserSettings.vue b/cloudflare_temp_email/frontend/src/views/admin/UserSettings.vue deleted file mode 100644 index 0ffafd7..0000000 --- a/cloudflare_temp_email/frontend/src/views/admin/UserSettings.vue +++ /dev/null @@ -1,140 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/admin/Webhook.vue b/cloudflare_temp_email/frontend/src/views/admin/Webhook.vue deleted file mode 100644 index 0d31dc4..0000000 --- a/cloudflare_temp_email/frontend/src/views/admin/Webhook.vue +++ /dev/null @@ -1,107 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/admin/WorkerConfig.vue b/cloudflare_temp_email/frontend/src/views/admin/WorkerConfig.vue deleted file mode 100644 index a401e70..0000000 --- a/cloudflare_temp_email/frontend/src/views/admin/WorkerConfig.vue +++ /dev/null @@ -1,42 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/common/About.vue b/cloudflare_temp_email/frontend/src/views/common/About.vue deleted file mode 100644 index 003bf57..0000000 --- a/cloudflare_temp_email/frontend/src/views/common/About.vue +++ /dev/null @@ -1,47 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/common/AdminContact.vue b/cloudflare_temp_email/frontend/src/views/common/AdminContact.vue deleted file mode 100644 index 517731e..0000000 --- a/cloudflare_temp_email/frontend/src/views/common/AdminContact.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/cloudflare_temp_email/frontend/src/views/common/Appearance.vue b/cloudflare_temp_email/frontend/src/views/common/Appearance.vue deleted file mode 100644 index c4650d5..0000000 --- a/cloudflare_temp_email/frontend/src/views/common/Appearance.vue +++ /dev/null @@ -1,106 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/common/Login.vue b/cloudflare_temp_email/frontend/src/views/common/Login.vue deleted file mode 100644 index 77187b3..0000000 --- a/cloudflare_temp_email/frontend/src/views/common/Login.vue +++ /dev/null @@ -1,300 +0,0 @@ - - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/index/AccountSettings.vue b/cloudflare_temp_email/frontend/src/views/index/AccountSettings.vue deleted file mode 100644 index 14dceee..0000000 --- a/cloudflare_temp_email/frontend/src/views/index/AccountSettings.vue +++ /dev/null @@ -1,169 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/index/AddressBar.vue b/cloudflare_temp_email/frontend/src/views/index/AddressBar.vue deleted file mode 100644 index c88c542..0000000 --- a/cloudflare_temp_email/frontend/src/views/index/AddressBar.vue +++ /dev/null @@ -1,183 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/index/Attachment.vue b/cloudflare_temp_email/frontend/src/views/index/Attachment.vue deleted file mode 100644 index 7327537..0000000 --- a/cloudflare_temp_email/frontend/src/views/index/Attachment.vue +++ /dev/null @@ -1,126 +0,0 @@ - - - diff --git a/cloudflare_temp_email/frontend/src/views/index/AutoReply.vue b/cloudflare_temp_email/frontend/src/views/index/AutoReply.vue deleted file mode 100644 index af4aafc..0000000 --- a/cloudflare_temp_email/frontend/src/views/index/AutoReply.vue +++ /dev/null @@ -1,136 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/index/LocalAddress.vue b/cloudflare_temp_email/frontend/src/views/index/LocalAddress.vue deleted file mode 100644 index 8e75b60..0000000 --- a/cloudflare_temp_email/frontend/src/views/index/LocalAddress.vue +++ /dev/null @@ -1,159 +0,0 @@ - - - diff --git a/cloudflare_temp_email/frontend/src/views/index/SendMail.vue b/cloudflare_temp_email/frontend/src/views/index/SendMail.vue deleted file mode 100644 index 3aaeff0..0000000 --- a/cloudflare_temp_email/frontend/src/views/index/SendMail.vue +++ /dev/null @@ -1,237 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/index/SimpleIndex.vue b/cloudflare_temp_email/frontend/src/views/index/SimpleIndex.vue deleted file mode 100644 index 48b89eb..0000000 --- a/cloudflare_temp_email/frontend/src/views/index/SimpleIndex.vue +++ /dev/null @@ -1,283 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/index/TelegramAddress.vue b/cloudflare_temp_email/frontend/src/views/index/TelegramAddress.vue deleted file mode 100644 index fdcc15c..0000000 --- a/cloudflare_temp_email/frontend/src/views/index/TelegramAddress.vue +++ /dev/null @@ -1,158 +0,0 @@ - - - diff --git a/cloudflare_temp_email/frontend/src/views/index/Webhook.vue b/cloudflare_temp_email/frontend/src/views/index/Webhook.vue deleted file mode 100644 index d07c5f9..0000000 --- a/cloudflare_temp_email/frontend/src/views/index/Webhook.vue +++ /dev/null @@ -1,28 +0,0 @@ - - - diff --git a/cloudflare_temp_email/frontend/src/views/telegram/Mail.vue b/cloudflare_temp_email/frontend/src/views/telegram/Mail.vue deleted file mode 100644 index 92ecb64..0000000 --- a/cloudflare_temp_email/frontend/src/views/telegram/Mail.vue +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/user/AddressManagement.vue b/cloudflare_temp_email/frontend/src/views/user/AddressManagement.vue deleted file mode 100644 index 6593666..0000000 --- a/cloudflare_temp_email/frontend/src/views/user/AddressManagement.vue +++ /dev/null @@ -1,247 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/user/BindAddress.vue b/cloudflare_temp_email/frontend/src/views/user/BindAddress.vue deleted file mode 100644 index 42a1d57..0000000 --- a/cloudflare_temp_email/frontend/src/views/user/BindAddress.vue +++ /dev/null @@ -1,45 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/user/UserBar.vue b/cloudflare_temp_email/frontend/src/views/user/UserBar.vue deleted file mode 100644 index 54986dc..0000000 --- a/cloudflare_temp_email/frontend/src/views/user/UserBar.vue +++ /dev/null @@ -1,76 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/user/UserLogin.vue b/cloudflare_temp_email/frontend/src/views/user/UserLogin.vue deleted file mode 100644 index da4f5bd..0000000 --- a/cloudflare_temp_email/frontend/src/views/user/UserLogin.vue +++ /dev/null @@ -1,308 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/src/views/user/UserMailBox.vue b/cloudflare_temp_email/frontend/src/views/user/UserMailBox.vue deleted file mode 100644 index 6926966..0000000 --- a/cloudflare_temp_email/frontend/src/views/user/UserMailBox.vue +++ /dev/null @@ -1,89 +0,0 @@ - - - diff --git a/cloudflare_temp_email/frontend/src/views/user/UserOauth2Callback.vue b/cloudflare_temp_email/frontend/src/views/user/UserOauth2Callback.vue deleted file mode 100644 index 7fe81f8..0000000 --- a/cloudflare_temp_email/frontend/src/views/user/UserOauth2Callback.vue +++ /dev/null @@ -1,65 +0,0 @@ - - - diff --git a/cloudflare_temp_email/frontend/src/views/user/UserSettings.vue b/cloudflare_temp_email/frontend/src/views/user/UserSettings.vue deleted file mode 100644 index 805646a..0000000 --- a/cloudflare_temp_email/frontend/src/views/user/UserSettings.vue +++ /dev/null @@ -1,259 +0,0 @@ - - - - - diff --git a/cloudflare_temp_email/frontend/tsconfig.json b/cloudflare_temp_email/frontend/tsconfig.json deleted file mode 100644 index f2e5e29..0000000 --- a/cloudflare_temp_email/frontend/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Bundler", - "strict": true, - "skipLibCheck": true, - "lib": [ - "ESNext" - ], - "types": [] - }, -} diff --git a/cloudflare_temp_email/frontend/vite.config.js b/cloudflare_temp_email/frontend/vite.config.js deleted file mode 100644 index 0fa8216..0000000 --- a/cloudflare_temp_email/frontend/vite.config.js +++ /dev/null @@ -1,78 +0,0 @@ -import { fileURLToPath, URL } from 'node:url' - -import { defineConfig } from 'vite' -import vue from '@vitejs/plugin-vue' -import { VitePWA } from 'vite-plugin-pwa' -import AutoImport from 'unplugin-auto-import/vite' -import Components from 'unplugin-vue-components/vite' -import { NaiveUiResolver } from 'unplugin-vue-components/resolvers' -import wasm from "vite-plugin-wasm"; -import topLevelAwait from "vite-plugin-top-level-await"; - -// https://vitejs.dev/config/ -export default defineConfig({ - build: { - outDir: './dist', - }, - plugins: [ - vue(), - wasm(), - topLevelAwait(), - AutoImport({ - imports: [ - 'vue', - { - 'naive-ui': [ - 'useMessage', - 'useNotification', - 'NButton', - 'NPopconfirm', - 'NIcon', - ] - } - ] - }), - Components({ - resolvers: [NaiveUiResolver()] - }), - VitePWA({ - registerType: null, - devOptions: { - enabled: false - }, - workbox: { - disableDevLogs: true, - globPatterns: [], - runtimeCaching: [], - navigateFallback: null, - cleanupOutdatedCaches: true, - }, - manifest: { - name: 'Temp Email', - short_name: 'Temp Email', - description: 'Temp Email - Temporary Email', - theme_color: '#ffffff', - icons: [ - { - src: '/logo.png', - sizes: '192x192', - type: 'image/png' - } - ] - } - }) - ], - resolve: { - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)) - } - }, - define: { - 'import.meta.env.PACKAGE_VERSION': JSON.stringify(process.env.npm_package_version), - }, - esbuild: { - supported: { - 'top-level-await': true - }, - } -}) diff --git a/cloudflare_temp_email/mail-parser-wasm/.gitignore b/cloudflare_temp_email/mail-parser-wasm/.gitignore deleted file mode 100644 index f541fb5..0000000 --- a/cloudflare_temp_email/mail-parser-wasm/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -debug/ -target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk - -# MSVC Windows builds of rustc generate these, which store debugging information -*.pdb -web/ diff --git a/cloudflare_temp_email/mail-parser-wasm/Cargo.toml b/cloudflare_temp_email/mail-parser-wasm/Cargo.toml deleted file mode 100644 index 07f5652..0000000 --- a/cloudflare_temp_email/mail-parser-wasm/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "mail-parser-wasm" -version = "0.2.1" -edition = "2021" -description = "A simple mail parser for wasm" -license = "MIT" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -mail-parser = "0.9.4" -wasm-bindgen = "0.2.99" diff --git a/cloudflare_temp_email/mail-parser-wasm/README.md b/cloudflare_temp_email/mail-parser-wasm/README.md deleted file mode 100644 index a72aeb3..0000000 --- a/cloudflare_temp_email/mail-parser-wasm/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# mail-parser-wasm web and cf worker - -## [mail-parser-wasm](https://www.npmjs.com/package/mail-parser-wasm) - -### mail-parser-wasm usage - -```bash -pnpm add mail-parser-wasm -``` - -```js -import { parse_message } from 'mail-parser-wasm' - -const parsedEmail = parse_message(rawEmail); -``` - -### mail-parser-wasm build - -```bash -wasm-pack build --release -wasm-pack publish -``` - -## [mail-parser-wasm-worker](https://www.npmjs.com/package/mail-parser-wasm-worker) - -### mail-parser-wasm-worker usage - -```bash -pnpm add mail-parser-wasm-worker -``` - -```js -import { parse_message_wrapper } from 'mail-parser-wasm-worker' - -const parsedEmail = parse_message_wrapper(rawEmail); -``` - -### mail-parser-wasm-worker build - -```bash -wasm-pack build --out-dir web --target web --release -find web/ -type f ! -name '*.json' ! -name '.gitignore' -exec cp {} worker/ \; -# modify worker/package.json version or whatever -pnpm publish worker --no-git-checks -``` diff --git a/cloudflare_temp_email/mail-parser-wasm/src/lib.rs b/cloudflare_temp_email/mail-parser-wasm/src/lib.rs deleted file mode 100644 index e10b864..0000000 --- a/cloudflare_temp_email/mail-parser-wasm/src/lib.rs +++ /dev/null @@ -1,194 +0,0 @@ -extern crate wasm_bindgen; - -use mail_parser::{MessageParser, MimeHeaders}; -use wasm_bindgen::prelude::*; - -#[derive(Clone)] -#[wasm_bindgen] -pub struct AttachmentResult { - content_id: String, - content_type: String, - filename: String, - content: Vec, -} - -#[wasm_bindgen] -impl AttachmentResult { - #[wasm_bindgen(getter)] - pub fn content_id(&self) -> String { - self.content_id.clone() - } - - #[wasm_bindgen(getter)] - pub fn content_type(&self) -> String { - self.content_type.clone() - } - - #[wasm_bindgen(getter)] - pub fn filename(&self) -> String { - self.filename.clone() - } - - #[wasm_bindgen(getter)] - pub fn content(&self) -> Vec { - self.content.clone() - } -} - -#[derive(Clone)] -#[wasm_bindgen] -pub struct MessageHeader { - key: String, - value: String, -} - -#[wasm_bindgen] -impl MessageHeader { - #[wasm_bindgen(getter)] - pub fn key(&self) -> String { - self.key.clone() - } - - #[wasm_bindgen(getter)] - pub fn value(&self) -> String { - self.value.clone() - } -} - -#[wasm_bindgen] -pub struct MessageResult { - sender: String, - subject: String, - headers: Vec, - body_html: String, - text: String, - attachments: Vec, -} - -#[wasm_bindgen] -impl MessageResult { - #[wasm_bindgen(getter)] - pub fn sender(&self) -> String { - self.sender.clone() - } - - #[wasm_bindgen(getter)] - pub fn subject(&self) -> String { - self.subject.clone() - } - - #[wasm_bindgen(getter)] - pub fn headers(&self) -> Vec { - self.headers.clone() - } - - #[wasm_bindgen(getter)] - pub fn body_html(&self) -> String { - self.body_html.clone() - } - - #[wasm_bindgen(getter)] - pub fn text(&self) -> String { - self.text.clone() - } - - #[wasm_bindgen(getter)] - pub fn attachments(&self) -> Vec { - self.attachments.clone() - } -} - -pub fn parse_attachment(message: &mail_parser::Message) -> Vec { - let mut attachments: Vec = Vec::new(); - for attachment in message.attachments() { - if !attachment.is_message() { - attachments.push(AttachmentResult { - content_id: attachment - .content_id() - .map(|id| id.to_owned()) - .unwrap_or(String::new()), - content_type: attachment - .content_type() - .map(|ct| { - let c_type = ct.c_type.clone().into_owned(); - let c_subtype = ct.c_subtype.clone(); - if c_subtype.is_none() { - return c_type; - } else { - return format!("{}/{}", c_type, c_subtype.unwrap()); - } - }) - .unwrap_or(String::new()), - filename: attachment - .attachment_name() - .map(|name| name.to_owned()) - .unwrap_or(String::new()), - content: attachment.contents().to_vec(), - }); - } else { - attachments.append( - &mut attachment - .message() - .map(|msg| parse_attachment(msg)) - .unwrap_or(Vec::new()), - ); - } - } - attachments -} - -#[wasm_bindgen] -pub fn parse_message(raw_message: &str) -> MessageResult { - // check if the message is valid - let res = MessageParser::default().parse(raw_message); - if res.is_none() { - return MessageResult { - sender: String::new(), - subject: String::new(), - headers: Vec::new(), - body_html: String::new(), - text: String::new(), - attachments: Vec::new(), - }; - } - let message = res.unwrap(); - - MessageResult { - sender: message - .from() - .and_then(|from| from.first()) - .map(|addr| { - if addr.name().is_some() { - return format!( - "{} <{}>", - addr.name().unwrap(), - addr.address().unwrap_or("") - ); - } else { - return addr.address().unwrap_or("").to_owned(); - } - }) - .unwrap_or(String::new()), - subject: message - .subject() - .map(|subject| subject.to_owned()) - .unwrap_or(String::new()), - headers: message - .headers() - .iter() - .map(|header| MessageHeader { - key: header.name().to_owned(), - value: header.value().as_text().unwrap_or("").to_owned(), - }) - .collect(), - body_html: message - .body_html(0) - .map(|html| html.into_owned()) - .unwrap_or(String::new()), - text: message - .body_text(0) - .map(|text| text.into_owned()) - .unwrap_or(String::new()), - attachments: parse_attachment(&message), - } -} diff --git a/cloudflare_temp_email/mail-parser-wasm/worker/index.d.ts b/cloudflare_temp_email/mail-parser-wasm/worker/index.d.ts deleted file mode 100644 index 286ee41..0000000 --- a/cloudflare_temp_email/mail-parser-wasm/worker/index.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import initAsync, { MessageResult } from './mail_parser_wasm'; -import MODULE from './mail_parser_wasm_bg.wasm'; -export { initAsync, MODULE }; -export * from './mail_parser_wasm'; -/** -* @param {string} raw_message -* @returns {MessageResult} -*/ -export function parse_message_wrapper(raw_message: string): MessageResult; diff --git a/cloudflare_temp_email/mail-parser-wasm/worker/index.js b/cloudflare_temp_email/mail-parser-wasm/worker/index.js deleted file mode 100644 index 96500c5..0000000 --- a/cloudflare_temp_email/mail-parser-wasm/worker/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import initAsync, { initSync, parse_message } from './mail_parser_wasm'; -import MODULE from './mail_parser_wasm_bg.wasm'; - -initSync({ module: MODULE }); - - -export { initAsync, MODULE }; -export * from './mail_parser_wasm'; -export const parse_message_wrapper = (raw_message) => { - initSync({ module: MODULE }); - return parse_message(raw_message); -} diff --git a/cloudflare_temp_email/mail-parser-wasm/worker/package.json b/cloudflare_temp_email/mail-parser-wasm/worker/package.json deleted file mode 100644 index 433f77a..0000000 --- a/cloudflare_temp_email/mail-parser-wasm/worker/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "mail-parser-wasm-worker", - "description": "A simple mail parser for worker", - "homepage": "https://github.com/dreamhunter2333/cloudflare_temp_email/tree/main/mail-parser-wasm", - "repository": { - "type": "git", - "url": "https://github.com/dreamhunter2333/cloudflare_temp_email", - "directory": "mail-parser-wasm" - }, - "version": "0.2.1", - "license": "MIT", - "files": [ - "mail_parser_wasm_bg.wasm", - "mail_parser_wasm.js", - "mail_parser_wasm.d.ts", - "index.js", - "index.d.ts" - ], - "module": "index.js", - "types": "index.d.ts", - "sideEffects": [ - "./snippets/*" - ] -} diff --git a/cloudflare_temp_email/pages/.gitignore b/cloudflare_temp_email/pages/.gitignore deleted file mode 100644 index 1cdac82..0000000 --- a/cloudflare_temp_email/pages/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -.DS_Store -dist -dist-ssr -coverage -*.local - -/cypress/videos/ -/cypress/screenshots/ - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? - -.env.* -*-dist/ -components.d.ts -.wrangler/ -pnpm-lock.yaml diff --git a/cloudflare_temp_email/pages/functions/_middleware.js b/cloudflare_temp_email/pages/functions/_middleware.js deleted file mode 100644 index a95df43..0000000 --- a/cloudflare_temp_email/pages/functions/_middleware.js +++ /dev/null @@ -1,16 +0,0 @@ -const API_PATHS = [ - "/api/", - "/open_api/", - "/user_api/", - "/admin/", - "/telegram/", - "/external/", -]; - -export async function onRequest(context) { - const reqPath = new URL(context.request.url).pathname; - if (API_PATHS.map(path => reqPath.startsWith(path)).some(Boolean)) { - return context.env.BACKEND.fetch(context.request); - } - return await context.next(); -} diff --git a/cloudflare_temp_email/pages/package.json b/cloudflare_temp_email/pages/package.json deleted file mode 100644 index c784373..0000000 --- a/cloudflare_temp_email/pages/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "temp-email-pages", - "version": "1.0.5", - "description": "", - "main": "index.js", - "scripts": { - "dev": "wrangler pages dev", - "deploy": "wrangler pages deploy --branch production" - }, - "keywords": [], - "author": "", - "license": "ISC", - "devDependencies": { - "wrangler": "^4.34.0" - }, - "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39" -} diff --git a/cloudflare_temp_email/scripts/update-dependencies.sh b/cloudflare_temp_email/scripts/update-dependencies.sh deleted file mode 100755 index 4eae50f..0000000 --- a/cloudflare_temp_email/scripts/update-dependencies.sh +++ /dev/null @@ -1,19 +0,0 @@ -cd frontend/ -pnpm up -pnpm add -D wrangler@latest -cd .. - -cd worker/ -pnpm up -pnpm add -D wrangler@latest -cd .. - -cd pages/ -pnpm up -pnpm add -D wrangler@latest -cd .. - -cd vitepress-docs/ -pnpm up --latest -pnpm add -D wrangler@latest -cd .. diff --git a/cloudflare_temp_email/smtp_proxy_server/.env.example b/cloudflare_temp_email/smtp_proxy_server/.env.example deleted file mode 100644 index b683f38..0000000 --- a/cloudflare_temp_email/smtp_proxy_server/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -proxy_url=https://temp-email-api.xxx.xxx -port=8025 diff --git a/cloudflare_temp_email/smtp_proxy_server/.gitignore b/cloudflare_temp_email/smtp_proxy_server/.gitignore deleted file mode 100644 index c7cdace..0000000 --- a/cloudflare_temp_email/smtp_proxy_server/.gitignore +++ /dev/null @@ -1,161 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ -test* diff --git a/cloudflare_temp_email/smtp_proxy_server/config.py b/cloudflare_temp_email/smtp_proxy_server/config.py deleted file mode 100644 index 0c6b933..0000000 --- a/cloudflare_temp_email/smtp_proxy_server/config.py +++ /dev/null @@ -1,22 +0,0 @@ -import logging -from pydantic_settings import BaseSettings - -logging.basicConfig( - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', -) - -_logger = logging.getLogger(__name__) -_logger.setLevel(logging.INFO) - - -class Settings(BaseSettings): - proxy_url: str = "http://localhost:8787" - port: int = 8025 - imap_port: int = 11143 - basic_password: str = "" - - class Config: - env_file = ".env" - - -settings = Settings() diff --git a/cloudflare_temp_email/smtp_proxy_server/docker-compose.yaml b/cloudflare_temp_email/smtp_proxy_server/docker-compose.yaml deleted file mode 100644 index 4b753cd..0000000 --- a/cloudflare_temp_email/smtp_proxy_server/docker-compose.yaml +++ /dev/null @@ -1,14 +0,0 @@ -services: - smtp_proxy_server: - image: ghcr.io/dreamhunter2333/cloudflare_temp_email/smtp_proxy_server:latest - # build: - # context: . - # dockerfile: dockerfile - container_name: "smtp_proxy_server" - ports: - - "8025:8025" - - "11143:11143" - environment: - - proxy_url=https://temp-email-api.xxx.xxx - - port=8025 - - imap_port=11143 diff --git a/cloudflare_temp_email/smtp_proxy_server/dockerfile b/cloudflare_temp_email/smtp_proxy_server/dockerfile deleted file mode 100644 index 425490a..0000000 --- a/cloudflare_temp_email/smtp_proxy_server/dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM python:3.12-slim - -WORKDIR /app -COPY requirements.txt /requirements.txt -RUN python3 -m pip install -r /requirements.txt -COPY . /app -ENTRYPOINT [ "python3", "main.py" ] diff --git a/cloudflare_temp_email/smtp_proxy_server/imap_server.py b/cloudflare_temp_email/smtp_proxy_server/imap_server.py deleted file mode 100644 index 93ae36d..0000000 --- a/cloudflare_temp_email/smtp_proxy_server/imap_server.py +++ /dev/null @@ -1,314 +0,0 @@ -import json -import logging -import httpx - -from io import BytesIO -from twisted.mail import imap4 -from zope.interface import implementer -from twisted.cred.portal import Portal, IRealm -from twisted.internet import protocol, reactor, defer -from twisted.cred.checkers import ICredentialsChecker, IUsernamePassword - -from config import settings -from parse_email import generate_email_model, parse_email -from models import EmailModel - -_logger = logging.getLogger(__name__) -_logger.setLevel(logging.INFO) - - -@implementer(imap4.IMessage) -class SimpleMessage: - - def __init__(self, uid=None, email_model: EmailModel = None): - self.uid = uid - self.email = email_model - self.subparts = self.email.subparts - - def getUID(self): - return self.uid - - def getHeaders(self, negate, *names): - self.got_headers = negate, names - return { - k.lower(): v - for k, v in self.email.headers.items() - } - - def isMultipart(self): - return len(self.subparts) > 0 - - def getSubPart(self, part): - self.got_subpart = part - return SimpleMessage(email_model=self.subparts[part]) - - def getBodyFile(self): - return BytesIO(self.email.body.encode("utf-8")) - - def getSize(self): - return self.email.size - - def getFlags(self): - return ["\\Seen"] - - def getInternalDate(self): - return self.email.headers.get("Date", "Mon, 1 Jan 1900 00:00:00 +0000") - - -@implementer(imap4.IMailboxInfo, imap4.IMailbox) -class SimpleMailbox: - - def __init__(self, name, password): - self.name = name - self.password = password - self.listeners = [] - self.addListener = self.listeners.append - self.removeListener = self.listeners.remove - self.message_count = 0 - self._update_message_count() - - def _update_message_count(self): - """主动获取邮件总数""" - try: - if self.name == "INBOX": - endpoint = "/api/mails" - elif self.name == "SENT": - endpoint = "/api/sendbox" - else: - return - - res = httpx.get( - f"{settings.proxy_url}{endpoint}?limit=1&offset=0", - headers={ - "Authorization": f"Bearer {self.password}", - "x-custom-auth": f"{settings.basic_password}", - "Content-Type": "application/json" - } - ) - if res.status_code == 200: - self.message_count = res.json()["count"] - # _logger.info(f"Updated {self.name} message count: {self.message_count}") - except Exception as e: - _logger.error(f"Failed to update message count for {self.name}: {e}") - - def getFlags(self): - return ["\\Seen"] - - def getUIDValidity(self): - return 0 - - def getMessageCount(self): - # 每次请求时更新邮件总数 - self._update_message_count() - return self.message_count - - def getRecentCount(self): - return 0 - - def getUnseenCount(self): - return 0 - - def isWriteable(self): - return 0 - - def destroy(self): - pass - - def getHierarchicalDelimiter(self): - return "/" - - def requestStatus(self, names): - # 在状态请求时也更新邮件总数 - self._update_message_count() - r = {} - if "MESSAGES" in names: - r["MESSAGES"] = self.getMessageCount() - if "RECENT" in names: - r["RECENT"] = self.getRecentCount() - if "UIDNEXT" in names: - r["UIDNEXT"] = self.getMessageCount() + 1 - if "UIDVALIDITY" in names: - r["UIDVALIDITY"] = self.getUIDValidity() - if "UNSEEN" in names: - r["UNSEEN"] = self.getUnseenCount() - return defer.succeed(r) - - def fetch(self, messages, uid): - """边查边返回邮件""" - def email_generator(): - for range_item in messages.ranges: - start, end = range_item - _logger.info(f"Fetching messages: {self.name}, range: {start}-{end}") - - for email_data in self.fetchGenerator(start, end): - yield email_data - - # 返回生成器,让IMAP4服务器逐个处理 - return email_generator() - - def fetchGenerator(self, start, end): - """通用的邮件获取生成器,边查边返回""" - start = max(start, 1) - - # 根据邮箱类型确定API端点 - if self.name == "INBOX": - endpoint = "/api/mails" - elif self.name == "SENT": - endpoint = "/api/sendbox" - else: - return - - # 首先获取服务端邮件总数 - count_res = httpx.get( - f"{settings.proxy_url}{endpoint}?limit=1&offset=0", - headers={ - "Authorization": f"Bearer {self.password}", - "x-custom-auth": f"{settings.basic_password}", - "Content-Type": "application/json" - } - ) - if count_res.status_code != 200: - _logger.error( - f"Failed to get {self.name} email count: " - f"code=[{count_res.status_code}] text=[{count_res.text}]" - ) - return - - total_count = count_res.json()["count"] - self.message_count = total_count - - if total_count == 0 or start > total_count: - return - - # 分批处理,每次获取一小批就立即返回 - batch_size = 20 - current_start = start - current_end = min(end or total_count, total_count) - - while current_start <= current_end: - batch_end = min(current_start + batch_size - 1, current_end) - - # 计算这一批的参数 - limit = batch_end - current_start + 1 - server_offset = total_count - batch_end - server_offset = max(0, server_offset) - - _logger.info( - f"Fetching batch: start={current_start}, end={batch_end}, " - f"total_count={total_count}, limit={limit}, " - f"server_offset={server_offset}" - ) - - res = httpx.get( - f"{settings.proxy_url}{endpoint}?limit={limit}&offset={server_offset}", - headers={ - "Authorization": f"Bearer {self.password}", - "x-custom-auth": f"{settings.basic_password}", - "Content-Type": "application/json" - } - ) - if res.status_code != 200: - _logger.error( - f"Failed to fetch {self.name} emails: " - f"code=[{res.status_code}] text=[{res.text}]" - ) - break - - emails = res.json()["results"] - for i, item in enumerate(reversed(emails)): - uid = total_count - server_offset - len(emails) + i + 1 - if current_start <= uid <= batch_end: - if self.name == "INBOX": - email_model = parse_email(item["raw"]) - elif self.name == "SENT": - email_model = generate_email_model(item) - - # 立即返回这封邮件 - yield (uid, SimpleMessage(uid, email_model)) - - current_start = batch_end + 1 - - def getUID(self, message): - return message.uid - - def store(self, messages, flags, mode, uid): - # IMailboxIMAP.store - raise NotImplementedError - - -class Account(imap4.MemoryAccount): - - def __init__(self, user, password): - self.password = password - super().__init__(user) - - def isSubscribed(self, name): - return name.upper() in ["INBOX", "SENT"] - - def _emptyMailbox(self, name, id): - _logger.info(f"New mailbox: {name}, {id}") - if name == "INBOX": - return SimpleMailbox(name, self.password) - if name == "SENT": - return SimpleMailbox(name, self.password) - raise imap4.NoSuchMailbox(name.encode("utf-8")) - - def select(self, name, rw=1): - return imap4.MemoryAccount.select(self, name) - - -class SimpleIMAPServer(imap4.IMAP4Server): - def __init__(self, factory): - imap4.IMAP4Server.__init__(self) - self.factory = factory - - def lineReceived(self, line): - # _logger.info(f"Received: {line}") - super().lineReceived(line) - - def sendLine(self, line): - # _logger.info(f"Sent: {line}") - super().sendLine(line) - - -@implementer(IRealm) -class SimpleRealm: - def requestAvatar(self, avatarId, mind, *interfaces): - res = json.loads(avatarId) - account = Account(res["username"], res["password"]) - account.addMailbox("INBOX") - account.addMailbox("SENT") - return imap4.IAccount, account, lambda: None - - -class IMAPFactory(protocol.Factory): - def __init__(self, portal): - self.portal = portal - - def buildProtocol(self, addr): - p = SimpleIMAPServer(self) - p.portal = self.portal - return p - - -@implementer(ICredentialsChecker) -class CustomChecker: - credentialInterfaces = (IUsernamePassword,) - - def requestAvatarId(self, credentials): - return defer.succeed(json.dumps({ - "username": credentials.username.decode(), - "password": credentials.password.decode(), - })) - - -def start_imap_server(): - _logger.info(f"Starting IMAP server on port {settings.imap_port}") - portal = Portal(SimpleRealm(), [CustomChecker()]) - reactor.listenTCP(settings.imap_port, IMAPFactory(portal)) - reactor.run() - - -if __name__ == "__main__": - _logger.info(f"Starting server settings[{settings}]") - start_imap_server() diff --git a/cloudflare_temp_email/smtp_proxy_server/main.py b/cloudflare_temp_email/smtp_proxy_server/main.py deleted file mode 100644 index 5f587fc..0000000 --- a/cloudflare_temp_email/smtp_proxy_server/main.py +++ /dev/null @@ -1,24 +0,0 @@ -import logging -import multiprocessing - -from smtp_server import start_smtp_server -from imap_server import start_imap_server -from config import settings - -_logger = logging.getLogger(__name__) -_logger.setLevel(logging.INFO) - -if __name__ == '__main__': - _logger.info(f"Starting server settings[{settings}]") - process_list = [ - multiprocessing.Process(target=start_smtp_server, args=()), - multiprocessing.Process(target=start_imap_server, args=()), - ] - try: - for p in process_list: - p.start() - for p in process_list: - p.join() - except KeyboardInterrupt: - for p in process_list: - p.terminate() diff --git a/cloudflare_temp_email/smtp_proxy_server/models.py b/cloudflare_temp_email/smtp_proxy_server/models.py deleted file mode 100644 index 4791e5e..0000000 --- a/cloudflare_temp_email/smtp_proxy_server/models.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Dict, List -from pydantic import BaseModel - - -class EmailModel(BaseModel): - headers: Dict[str, str] - body: str - content_type: str - subparts: List["EmailModel"] - size: int diff --git a/cloudflare_temp_email/smtp_proxy_server/parse_email.py b/cloudflare_temp_email/smtp_proxy_server/parse_email.py deleted file mode 100644 index 3c1c115..0000000 --- a/cloudflare_temp_email/smtp_proxy_server/parse_email.py +++ /dev/null @@ -1,69 +0,0 @@ -import datetime -import json -import logging -import email - -from email.message import Message -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText - - -from models import EmailModel - -_logger = logging.getLogger(__name__) -_logger.setLevel(logging.INFO) - - -def get_email_model(msg: Message): - subparts = [ - get_email_model(subpart) - for subpart in msg.get_payload() - ] if msg.is_multipart() else [] - body = "" if msg.is_multipart() else msg._payload - return EmailModel( - headers={k: v for k, v in msg.items()}, - body=body, - content_type=msg.get_content_type(), - size=len(body) + sum(subpart.size for subpart in subparts), - subparts=subparts, - ) - - -def parse_email(raw: str) -> EmailModel: - try: - msg = email.message_from_string(raw) - return get_email_model(msg) - except Exception as e: - _logger.error(f"Could not parse email: {e}") - return EmailModel( - headers={}, - body="could not parse email", - content_type="text/plain", - size=len("could not parse email"), - subparts=[], - ) - - -def generate_email_model(item: dict) -> EmailModel: - email_json = json.loads(item["raw"]) - message = MIMEMultipart() - if email_json.get("version") == "v2": - message['From'] = f'{email_json["from_name"]} <{item["address"]}>' if email_json.get("from_name") else item["address"] - message['To'] = f'{email_json["to_name"]} <{email_json["to_mail"]}>' if email_json.get("to_name") else email_json["to_mail"] - message.attach(MIMEText( - email_json["content"], - "html" if email_json.get("is_html") else "plain" - )) - else: - message['From'] = f'{email_json["from"]["name"]} <{email_json["from"]["email"]}>' - message['To'] = ", ".join( - [f"{to['name']} <{to['email']}>" for to in email_json["personalizations"][0]["to"]]) - message.attach(MIMEText( - email_json["content"][0]["value"], - "html" if "html" in email_json["content"][0]["type"] else "plain" - )) - message['Subject'] = email_json["subject"] - message["Date"] = datetime.datetime.strptime( - item["created_at"], "%Y-%m-%d %H:%M:%S" - ).strftime("%a, %d %b %Y %H:%M:%S +0000") - return parse_email(message.as_string()) diff --git a/cloudflare_temp_email/smtp_proxy_server/requirements.txt b/cloudflare_temp_email/smtp_proxy_server/requirements.txt deleted file mode 100644 index 5d902b6..0000000 --- a/cloudflare_temp_email/smtp_proxy_server/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -aiosmtpd==1.4.6 -pydantic-settings==2.9.1 -requests==2.32.4 -Twisted==25.5.0 -httpx==0.28.1 diff --git a/cloudflare_temp_email/smtp_proxy_server/smtp_server.py b/cloudflare_temp_email/smtp_proxy_server/smtp_server.py deleted file mode 100644 index 7095587..0000000 --- a/cloudflare_temp_email/smtp_proxy_server/smtp_server.py +++ /dev/null @@ -1,154 +0,0 @@ -import asyncio -import logging -import email -import httpx - -from aiosmtpd.controller import Controller -from aiosmtpd.smtp import SMTP, Session, Envelope, AuthResult, LoginPassword - -from config import settings - -_logger = logging.getLogger(__name__) -_logger.setLevel(logging.INFO) - - -class CustomSMTPHandler: - - def authenticator(self, server, session, envelope, mechanism, auth_data): - fail_nothandled = AuthResult(success=False, handled=False) - if mechanism not in ("LOGIN", "PLAIN"): - _logger.warning(f"Unsupported mechanism {mechanism}") - return fail_nothandled - if not isinstance(auth_data, LoginPassword): - _logger.warning(f"Invalid auth data {auth_data}") - return fail_nothandled - return AuthResult(success=True, auth_data=auth_data) - - async def handle_DATA(self, server: SMTP, session: Session, envelope: Envelope) -> str: - _logger.info( - f"handle_DATA from {envelope.mail_from} to {envelope.rcpt_tos}" - ) - if not isinstance(session.auth_data, LoginPassword): - return '530 Authentication required' - if len(envelope.rcpt_tos) != 1: - return '500 Only one recipient allowed' - # Only one recipient allowed - to_mail = envelope.rcpt_tos[0] - # Parse email - msg = email.message_from_string(envelope.content) - content_list = [] - if msg.is_multipart(): - for part in msg.walk(): - content_type = part.get_content_type() - charset = part.get_content_charset() - cte = str(part.get('content-transfer-encoding', '')).lower() - if content_type not in ["text/plain", "text/html"]: - _logger.warning(f"Skipping {content_type}") - continue - if cte == "8bit": - value = part.get_payload(decode=False) - else: - payload = part.get_payload(decode=True) - value = payload.decode(charset) if charset else payload - if not value: - continue - content_list.append({ - "type": content_type, - "value": value - }) - elif msg.get_content_type() in ["text/plain", "text/html"] and msg.get_payload(decode=True): - cte = str(msg.get('content-transfer-encoding', '')).lower() - charset = msg.get_content_charset() - if cte == "8bit": - value = msg.get_payload(decode=False) - else: - payload = msg.get_payload(decode=True) - value = payload.decode(charset) if charset else payload - _logger.info(f"Payload {msg._payload} charset {charset}") - content_list.append({ - "type": msg.get_content_type(), - "value": value - }) - - if not content_list: - return '500 Invalid content' - body = max( - content_list, - key=lambda x: (x["type"] == "text/html", len(x["value"])) - ) - from_name, _ = email.utils.parseaddr( - str(email.header.make_header( - email.header.decode_header(msg['From']) - )) - ) - to_mail_map = {} - for to in str(email.header.make_header( - email.header.decode_header(msg['To']) - )).split(","): - tmp_to_name, tmp_to_mail = email.utils.parseaddr(to) - to_mail_map[tmp_to_mail] = tmp_to_name - _logger.info(f"Parsed mail from {from_name} to {to_mail_map}") - # Send mail - send_body = { - "token": session.auth_data.password.decode(), - "from_name": from_name, - "to_name": to_mail_map.get(to_mail), - "to_mail": to_mail, - "subject": str(email.header.make_header( - email.header.decode_header(msg['Subject']) - )), - "is_html": body["type"] == "text/html", - "content": body["value"], - } - _logger.info(f"Send mail {dict(send_body, token='***')}") - try: - res = httpx.post( - f"{settings.proxy_url}/external/api/send_mail", - json=send_body, headers={ - "Content-Type": "application/json" - } - ) - if res.status_code != 200: - _logger.error( - "Failed to send mail " - f"code=[{res.status_code}] text=[{res.text}]" - ) - return f'500 Internal server error code=[{res.status_code}] text=[{res.text}]' - except Exception as e: - _logger.error(e) - return '500 Internal server error' - - return '250 OK' - - -handler = CustomSMTPHandler() -server = Controller( - handler, - hostname="", - port=settings.port, - auth_require_tls=False, - decode_data=True, - authenticator=handler.authenticator, - auth_exclude_mechanism=["DONT"] -) - - -async def start(): - _logger.info(f"Starting server on port {settings.port}") - server.start() - - -def start_smtp_server(): - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - task = loop.create_task(start()) - try: - loop.run_forever() - except KeyboardInterrupt: - _logger.info("Got KeyboardInterrupt, stopping") - server.stop() - - -if __name__ == "__main__": - _logger.info(f"Starting server settings[{settings}]") - start_smtp_server() diff --git a/cloudflare_temp_email/vitepress-docs/.gitignore b/cloudflare_temp_email/vitepress-docs/.gitignore deleted file mode 100644 index 5e23010..0000000 --- a/cloudflare_temp_email/vitepress-docs/.gitignore +++ /dev/null @@ -1,307 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# Custom -dist/ - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ -#**/Properties/launchSettings.json - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Typescript v1 declaration files -typings/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -/coverage -/src/client/shared.ts -/src/node/shared.ts -*.log -.DS_Store -.vite_opt_cache -dist -node_modules -TODOs.md -.vscode -docs/.vitepress/cache/ -docs/.vitepress/dist/ -.idea/ - -*.zip diff --git a/cloudflare_temp_email/vitepress-docs/docs/.vitepress/config.ts b/cloudflare_temp_email/vitepress-docs/docs/.vitepress/config.ts deleted file mode 100644 index 6ca9145..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/.vitepress/config.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { defineConfig } from 'vitepress' -import { zh } from './zh' -import { en } from './en' - -export default defineConfig({ - title: "Temp Mail Doc", - lang: 'zh-CN', - lastUpdated: true, - locales: { - root: { label: '简体中文', ...zh }, - en: { label: 'English', ...en } - }, - head: [ - ['link', { rel: 'icon', type: 'image/png', href: '/logo.png' }], - ['meta', { name: 'theme-color', content: '#5f67ee' }], - ['meta', { property: 'og:type', content: 'website' }], - ['meta', { property: 'og:locale', content: 'Temp Mail Doc' }], - ['meta', { property: 'og:title', content: 'Temp Mail Doc' }], - ['meta', { property: 'og:site_name', content: 'Temp Mail' }], - ['meta', { property: 'og:image', content: 'https://temp-mail-docs.awsl.uk/logo.png' }], - ['meta', { property: 'og:url', content: 'https://temp-mail-docs.awsl.uk' }], - ], - sitemap: { - hostname: 'https://temp-mail-docs.awsl.uk', - transformItems(items) { - return items.filter((item) => !item.url.includes('migration')) - } - }, - themeConfig: { - logo: { src: '/logo.png', width: 24, height: 24 }, - search: { provider: 'local' }, - socialLinks: [ - { - icon: 'discord', - link: 'https://discord.gg/dQEwTWhA6Q' - }, - { - icon: { - svg: '' - }, - link: 'https://t.me/cloudflare_temp_email' - }, - { - icon: 'github', - link: 'https://github.com/dreamhunter2333/cloudflare_temp_email' - } - ] - } -}) diff --git a/cloudflare_temp_email/vitepress-docs/docs/.vitepress/en.ts b/cloudflare_temp_email/vitepress-docs/docs/.vitepress/en.ts deleted file mode 100644 index c40caa0..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/.vitepress/en.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { defineConfig, type DefaultTheme } from 'vitepress' - -export const en = defineConfig({ - title: "Temp Mail Doc", - lang: 'zh-Hans', - description: 'CloudFlare Free sending and receiving of temporary domain name mailboxes', - - themeConfig: { - outline: 'deep', - nav: nav(), - - editLink: { - pattern: 'https://github.com/dreamhunter2333/cloudflare_temp_email/edit/main/vitepress-docs/docs/:path', - text: 'Edit this page on GitHub' - }, - - footer: { - message: 'Based on MIT license', - copyright: `Copyright © 2023-${new Date().getFullYear()} Dream Hunter` - }, - } -}) - -function nav(): DefaultTheme.NavItem[] { - return [ - { - text: 'Home', - link: '/en/', - }, - { - text: 'Guide', - link: '/en/cli', - }, - { - text: 'Service Status', - link: '/status', - }, - { - text: 'Reference', - link: '/reference', - }, - { - text: process.env.TAG_NAME || 'v0.2.2', - items: [ - { - text: 'CHANGELOG', - link: 'https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/CHANGELOG.md' - }, - { - text: 'Contribute', - link: 'https://github.com/dreamhunter2333/cloudflare_temp_email' - } - ] - } - ] -} diff --git a/cloudflare_temp_email/vitepress-docs/docs/.vitepress/zh.ts b/cloudflare_temp_email/vitepress-docs/docs/.vitepress/zh.ts deleted file mode 100644 index 8b86ab6..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/.vitepress/zh.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { defineConfig, type DefaultTheme } from 'vitepress' - -export const zh = defineConfig({ - title: "临时邮箱文档", - lang: 'zh-Hans', - description: 'CloudFlare 免费收发 临时域名邮箱', - - themeConfig: { - nav: nav(), - - sidebar: { - '/zh/guide/': { base: '/zh/guide/', items: sidebarGuide() }, - }, - - editLink: { - pattern: 'https://github.com/dreamhunter2333/cloudflare_temp_email/edit/main/vitepress-docs/docs/:path', - text: '在 GitHub 上编辑此页面' - }, - - footer: { - message: '基于 MIT 许可发布', - copyright: `版权所有 © 2023-${new Date().getFullYear()} Dream Hunter` - }, - - docFooter: { - prev: '上一页', - next: '下一页' - }, - - outline: { - level: 'deep', - label: '页面导航' - }, - - lastUpdated: { - text: '最后更新于', - formatOptions: { - dateStyle: 'short', - timeStyle: 'medium' - } - }, - - langMenuLabel: '多语言', - returnToTopLabel: '回到顶部', - sidebarMenuLabel: '菜单', - darkModeSwitchLabel: '主题', - lightModeSwitchTitle: '切换到浅色模式', - darkModeSwitchTitle: '切换到深色模式' - } -}) - -function nav(): DefaultTheme.NavItem[] { - return [ - { - text: '主页', - link: '/', - }, - { - text: '指南', - link: '/zh/guide/quick-start', - activeMatch: '/zh/guide/' - }, - { - text: '服务状态', - link: '/status', - }, - { - text: '参考', - link: '/reference', - }, - { - text: process.env.TAG_NAME || 'v0.2.2', - items: [ - { - text: '更新日志', - link: 'https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/CHANGELOG.md' - }, - { - text: '参与贡献', - link: 'https://github.com/dreamhunter2333/cloudflare_temp_email' - } - ] - } - ] -} - -function sidebarGuide(): DefaultTheme.SidebarItem[] { - return [ - { - text: '简介', - collapsed: false, - items: [ - { text: '什么是临时邮箱', link: 'what-is-temp-mail' }, - { text: 'Star History', link: 'star-history' }, - { text: '快速开始部署', link: 'quick-start' }, - ] - }, - { - text: '通过命令行部署', - collapsed: false, - items: [ - { text: '命令行部署准备', link: 'cli/pre-requisite' }, - { text: 'D1 数据库', link: 'cli/d1' }, - { text: 'Cloudflare workers 后端', link: 'cli/worker' }, - { text: '配置邮件转发', link: 'email-routing.md' }, - { text: 'Cloudflare Pages 前端', link: 'cli/pages' }, - { text: '配置发送邮件', link: 'config-send-mail' }, - ] - }, - { - text: '通过用户界面部署', - collapsed: false, - items: [ - { text: 'D1 数据库', link: 'ui/d1' }, - { text: 'Cloudflare workers 后端', link: 'ui/worker' }, - { text: '配置邮件转发', link: 'email-routing.md' }, - { text: 'Cloudflare Pages 前端', link: 'ui/pages' }, - { text: '配置发送邮件', link: 'config-send-mail' }, - ] - }, - { - text: '通过 Github Actions 部署', - collapsed: true, - items: [ - { text: 'Github Actions 部署准备', link: 'actions/pre-requisite' }, - { text: 'D1 数据库', link: 'actions/d1' }, - { text: 'Github Actions 配置', link: 'actions/github-action' }, - { text: '配置邮件转发', link: 'email-routing.md' }, - { text: '配置发送邮件', link: 'config-send-mail' }, - { text: '自动更新配置', link: 'actions/auto-update' }, - ] - }, - { - text: '通用', - collapsed: false, - items: [ - { text: 'worker变量说明', link: 'worker-vars' }, - { text: '常见问题', link: 'common-issues' }, - ] - }, - { - text: '附加功能', - collapsed: false, - items: [ - { text: '配置 SMTP IMAP 代理服务', link: 'feature/config-smtp-proxy' }, - { text: '发送邮件 API', link: 'feature/send-mail-api' }, - { text: '查看邮件 API', link: 'feature/mail-api' }, - { text: '配置子域名邮箱', link: 'feature/subdomain' }, - { text: '配置 Telegram Bot', link: 'feature/telegram' }, - { text: '配置 S3 附件', link: 'feature/s3-attachment' }, - { text: '配置 worker 使用 wasm 解析邮件', link: 'feature/mail_parser_wasm_worker' }, - { text: '配置 webhook', link: 'feature/webhook' }, - { text: '新建邮箱地址 API', link: 'feature/new-address-api' }, - { text: 'Oauth2 第三方登录', link: 'feature/user-oauth2' }, - { text: '配置其他worker增强', link: 'feature/another-worker-enhanced' }, - { text: '给网页增加 Google Ads', link: 'feature/google-ads.md' }, - ] - }, - { - text: '功能简介', - collapsed: false, - items: [ - { text: 'Admin 控制台', link: 'feature/admin' }, - { text: 'Admin 用户管理', link: 'feature/admin-user-management' }, - ] - }, - { text: '参考', base: "/", link: 'reference' } - ] -} diff --git a/cloudflare_temp_email/vitepress-docs/docs/en/cli.md b/cloudflare_temp_email/vitepress-docs/docs/en/cli.md deleted file mode 100644 index 3f83cbb..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/en/cli.md +++ /dev/null @@ -1,195 +0,0 @@ -# cloudflare temp email - -This is a temporary email service that uses Cloudflare Workers to create a temporary email address and view the received email in web browser. - -## Features - -- [x] Cloudflare D1 as a database -- [x] Deploy the front end with Cloudflare Pages -- [x] Deploy the backend with Cloudflare Workers -- [x] Email forwarding using Cloudflare Email Routing -- [x] Use password to login to the previous mailbox again. -- [x] Get Custom Name Email -- [x] Support multiple languages -- [x] Add access authorization, which can be used as a private site -- [x] Add auto reply feature -- [x] Add attachment viewing function -- [x] use rust wasm to parse email -- [x] support send email -- [x] support DKIM - -## Deploy - -[Install/Update Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/) - -```bash -npm install wrangler -g -git clone https://github.com/dreamhunter2333/cloudflare_temp_email.git -# Switch to the latest tag or the branch you want to deploy. You can also use the main branch directly. -# git checkout $(git describe --tags $(git rev-list --tags --max-count=1)) -``` - -## DB - Cloudflare D1 - -```bash -# create a database, and copy the output to wrangler.toml in the next step -wrangler d1 create dev -wrangler d1 execute dev --file=db/schema.sql --remote -# schema update, if you have initialized the database before this date, you can execute this command to update -# wrangler d1 execute dev --file=db/2024-01-13-patch.sql --remote -# wrangler d1 execute dev --file=db/2024-04-03-patch.sql --remote -# create a namespace, and copy the output to wrangler.toml in the next step -wrangler kv:namespace create DEV -``` - -![d1](/readme_assets/d1.png) - -### Backend - Cloudflare workers - -The first deployment will prompt you to create a project. Please fill in `production` for the `production` branch. - -```bash -cd worker -pnpm install -cp wrangler.toml.template wrangler.toml -# deploy -pnpm run deploy -``` - -`wrangler.toml` - -```toml -name = "cloudflare_temp_email" -main = "src/worker.ts" -compatibility_date = "2024-09-23" -compatibility_flags = [ "nodejs_compat" ] - -# enable cron if you want set auto clean up -# [triggers] -# crons = [ "0 0 * * *" ] - -# send mail by cf mail -# send_email = [ -# { name = "SEND_MAIL" }, -# ] - -[vars] -# DEFAULT_LANG = "zh" -# TITLE = "Custom Title" # The title of the site -PREFIX = "tmp" # The mailbox name prefix to be processed -# (min, max) length of the adderss, if not set, the default is (1, 30) -# MIN_ADDRESS_LEN = 1 -# MAX_ADDRESS_LEN = 30 -# ANNOUNCEMENT = "Custom Announcement" -# always show ANNOUNCEMENT even no changes -# ALWAYS_SHOW_ANNOUNCEMENT = true -# address check REGEX, if not set, will not check -# ADDRESS_CHECK_REGEX = "^(?!.*admin).*" -# address name replace REGEX, if not set, the default is [^a-z0-9] -# ADDRESS_REGEX = "[^a-z0-9]" -# If you want your site to be private, uncomment below and change your password -# PASSWORDS = ["123", "456"] -# admin console password, if not configured, access to the console is not allowed -# ADMIN_PASSWORDS = ["123", "456"] -# warning: no password or user check for admin portal -# DISABLE_ADMIN_PASSWORD_CHECK = false -# admin contact information. If not configured, it will not be displayed. Any string can be configured. -# ADMIN_CONTACT = "xx@xx.xxx" -DEFAULT_DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # domain name for no role users -DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # all your domain name -# For chinese domain name, you can use DOMAIN_LABELS to show chinese domain name -# DOMAIN_LABELS = ["中文.xxx", "xxx.xxx2"] -# USER_DEFAULT_ROLE = "vip" # default role for new users(only when enable mail verification) -# ADMIN_USER_ROLE = "admin" # the role which can access admin panel -# User roles configuration, if domains is empty will use default_domains, if prefix is null will use default prefix, if prefix is empty string will not use prefix -# USER_ROLES = [ -# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "vip", prefix = "vip" }, -# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "admin", prefix = "" }, -# ] -JWT_SECRET = "xxx" # Key used to generate jwt -BLACK_LIST = "" # Blacklist, used to filter senders, comma separated -# Allow users to create email addresses -ENABLE_USER_CREATE_EMAIL = true -# Disable anonymous user create email, if set true, users can only create email addresses after logging in -# DISABLE_ANONYMOUS_USER_CREATE_EMAIL = true -# Allow users to delete messages -ENABLE_USER_DELETE_EMAIL = true -# Allow automatic replies to emails -ENABLE_AUTO_REPLY = false -# Allow webhook -# ENABLE_WEBHOOK = true -# Footer text -# COPYRIGHT = "Dream Hunter" -# DISABLE_SHOW_GITHUB = true # Disable Show GitHub link -# default send balance, if not set, it will be 0 -# DEFAULT_SEND_BALANCE = 1 -# the role which can send emails without limit, multiple roles can be separated by , -# NO_LIMIT_SEND_ROLE = "vip" -# Turnstile verification configuration -# CF_TURNSTILE_SITE_KEY = "" -# CF_TURNSTILE_SECRET_KEY = "" -# telegram bot -# TG_MAX_ADDRESS = 5 -# telegram bot info, predefined bot info can reduce latency of the webhook -# TG_BOT_INFO = "{}" -# global forward address list, if set, all emails will be forwarded to these addresses -# FORWARD_ADDRESS_LIST = ["xxx@xxx.com"] -# Frontend URL -# FRONTEND_URL = "https://xxxx.xxx" -# Enable check junk mail -# ENABLE_CHECK_JUNK_MAIL = false -# junk mail check list, if status exists and status is not pass, will be marked as junk mail -# JUNK_MAIL_CHECK_LIST = = ["spf", "dkim", "dmarc"] -# junk mail force check pass list, if no status or status is not pass, will be marked as junk mail -# JUNK_MAIL_FORCE_PASS_LIST = ["spf", "dkim", "dmarc"] -# remove attachment if size exceed 2MB, mail maybe mising some information due to parsing -# REMOVE_EXCEED_SIZE_ATTACHMENT = true -# remove all attachment, mail maybe mising some information due to parsing -# REMOVE_ALL_ATTACHMENT = true - -[[d1_databases]] -binding = "DB" -database_name = "xxx" # D1 database name -database_id = "xxx" # D1 database ID - -# kv config for send email verification code -# [[kv_namespaces]] -# binding = "KV" -# id = "xxxx" - -# Create a new address current limiting configuration -# [[unsafe.bindings]] -# name = "RATE_LIMITER" -# type = "ratelimit" -# namespace_id = "1001" -# # 10 requests per minute -# simple = { limit = 10, period = 60 } -``` - -you can find and test the worker's url in the workers dashboard - -![worker](/readme_assets/worker.png) - -## Cloudflare Email Routing - -Before you can bind an email address to your Worker, you need to enable Email Routing and have at least one verified email address. - -enable email route and config email forward catch-all to the worker - -![email](/readme_assets/email.png) - -### Frontend - Cloudflare pages - -The first deployment will prompt you to create a project. Please fill in `production` for the `production` branch. - -```bash -cd frontend -pnpm install -# add .env.local and modify VITE_API_BASE to your worker's url -# VITE_API_BASE=https://xxx.xxx.workers.dev - don't put / in the end -cp .env.example .env.local -pnpm build --emptyOutDir -pnpm run deploy -``` - -![pages](/readme_assets/pages.png) diff --git a/cloudflare_temp_email/vitepress-docs/docs/en/index.md b/cloudflare_temp_email/vitepress-docs/docs/en/index.md deleted file mode 100644 index 2af8c25..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/en/index.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -# https://vitepress.dev/reference/default-theme-home-page -layout: home - -hero: - name: "Temporary mailbox document" - tagline: "Build CloudFlare to send and receive free temporary domain name mailboxes" - actions: - - theme: brand - text: Try it now - link: https://mail.awsl.uk/en - - theme: alt - text: command line deployment - link: /en/cli -features: - - title: Free hosting on CloudFlare, no server required - details: Cloudflare D1 database, Cloudflare Pages frontend, Cloudflare Workers backend, Cloudflare Email Routing - - title: Only domain name required for private deployment - details: Support password login email, access authorization can be used as a private site, support attachment function - - title: Use rust wasm to parse emails - details: Use rust wasm to parse emails, support various RFC standards for emails, support attachments, extremely fast - - title: Support sending emails - details: Support sending txt or html emails through domain name mailboxes,Support DKIM signature ---- diff --git a/cloudflare_temp_email/vitepress-docs/docs/index.md b/cloudflare_temp_email/vitepress-docs/docs/index.md deleted file mode 100644 index e40d43d..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/index.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -# https://vitepress.dev/reference/default-theme-home-page -layout: home - -hero: - name: "临时邮箱文档" - tagline: "搭建 CloudFlare 免费收发 临时域名邮箱" - actions: - - theme: brand - text: 立即试用 - link: https://mail.awsl.uk/ - - theme: alt - text: 命令行部署 - link: /zh/guide/quick-start - - theme: alt - text: 通过用户界面部署 - link: /zh/guide/quick-start - - theme: alt - text: 通过 Github Actions 部署 - link: /zh/guide/quick-start - -features: - - title: 仅需域名即可私有部署, 免费托管在 CloudFlare,无需服务器 - details: 支持 password 登录邮箱, 用户注册,使用访问密码可作为私人站点,支持附件功能。 - - title: 使用 rust wasm 解析邮件 - details: 使用 rust wasm 解析邮件,支持邮件各种RFC标准,支持附件, 速度极快 - - title: 支持 Telegram Bot 和 Webhook - details: 邮件可转发到 Telegram 或者 webhook, Telegram Bot 支持绑定邮箱,查看邮件, Telegram 小程序 - - title: 支持发送邮件(UI/API/SMTP) - details: 支持通过域名邮箱发送 txt 或者 html 邮件,支持 DKIM 签名, UI/API/SMTP 发送邮件 ---- diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/feature/address-webhook.png b/cloudflare_temp_email/vitepress-docs/docs/public/feature/address-webhook.png deleted file mode 100644 index 0f6fa4a..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/feature/address-webhook.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/feature/admin-mail-webhook.png b/cloudflare_temp_email/vitepress-docs/docs/public/feature/admin-mail-webhook.png deleted file mode 100644 index 87baed8..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/feature/admin-mail-webhook.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/feature/admin-user-management.png b/cloudflare_temp_email/vitepress-docs/docs/public/feature/admin-user-management.png deleted file mode 100644 index 2748a3d..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/feature/admin-user-management.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/feature/admin-user-page.png b/cloudflare_temp_email/vitepress-docs/docs/public/feature/admin-user-page.png deleted file mode 100644 index c9c022e..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/feature/admin-user-page.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/feature/admin-webhook-settings.png b/cloudflare_temp_email/vitepress-docs/docs/public/feature/admin-webhook-settings.png deleted file mode 100644 index d1dd7ed..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/feature/admin-webhook-settings.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/feature/admin.png b/cloudflare_temp_email/vitepress-docs/docs/public/feature/admin.png deleted file mode 100644 index 42da75c..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/feature/admin.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/feature/another-worker-enhanced-01.png b/cloudflare_temp_email/vitepress-docs/docs/public/feature/another-worker-enhanced-01.png deleted file mode 100644 index baddc6e..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/feature/another-worker-enhanced-01.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/feature/another-worker-enhanced-02.png b/cloudflare_temp_email/vitepress-docs/docs/public/feature/another-worker-enhanced-02.png deleted file mode 100644 index 534f711..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/feature/another-worker-enhanced-02.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/feature/another-worker-enhanced-03.png b/cloudflare_temp_email/vitepress-docs/docs/public/feature/another-worker-enhanced-03.png deleted file mode 100644 index 1161435..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/feature/another-worker-enhanced-03.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/feature/another-worker-enhanced-04.png b/cloudflare_temp_email/vitepress-docs/docs/public/feature/another-worker-enhanced-04.png deleted file mode 100644 index e2e6a81..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/feature/another-worker-enhanced-04.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/feature/imap.png b/cloudflare_temp_email/vitepress-docs/docs/public/feature/imap.png deleted file mode 100644 index 98ccdac..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/feature/imap.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/feature/oauth2-login.png b/cloudflare_temp_email/vitepress-docs/docs/public/feature/oauth2-login.png deleted file mode 100644 index e24eb88..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/feature/oauth2-login.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/feature/oauth2.png b/cloudflare_temp_email/vitepress-docs/docs/public/feature/oauth2.png deleted file mode 100644 index 675e5f8..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/feature/oauth2.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/feature/s3-download.png b/cloudflare_temp_email/vitepress-docs/docs/public/feature/s3-download.png deleted file mode 100644 index 3d4ae70..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/feature/s3-download.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/feature/s3-save.png b/cloudflare_temp_email/vitepress-docs/docs/public/feature/s3-save.png deleted file mode 100644 index 309530f..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/feature/s3-save.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/feature/telegram.png b/cloudflare_temp_email/vitepress-docs/docs/public/feature/telegram.png deleted file mode 100644 index 34a16ca..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/feature/telegram.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/logo.png b/cloudflare_temp_email/vitepress-docs/docs/public/logo.png deleted file mode 100644 index 97829b1..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/logo.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/readme_assets/d1.png b/cloudflare_temp_email/vitepress-docs/docs/public/readme_assets/d1.png deleted file mode 100644 index 95619ba..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/readme_assets/d1.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/readme_assets/demo.png b/cloudflare_temp_email/vitepress-docs/docs/public/readme_assets/demo.png deleted file mode 100644 index 269bf73..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/readme_assets/demo.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/readme_assets/email.png b/cloudflare_temp_email/vitepress-docs/docs/public/readme_assets/email.png deleted file mode 100644 index 1e3cbbc..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/readme_assets/email.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/readme_assets/pages.png b/cloudflare_temp_email/vitepress-docs/docs/public/readme_assets/pages.png deleted file mode 100644 index 8439d83..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/readme_assets/pages.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/readme_assets/worker.png b/cloudflare_temp_email/vitepress-docs/docs/public/readme_assets/worker.png deleted file mode 100644 index 868390a..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/readme_assets/worker.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/d1-exec.png b/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/d1-exec.png deleted file mode 100644 index fe13113..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/d1-exec.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/d1.png b/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/d1.png deleted file mode 100644 index 8ee4a24..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/d1.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/pages-1.png b/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/pages-1.png deleted file mode 100644 index 2b3aaa5..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/pages-1.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/pages-domain.png b/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/pages-domain.png deleted file mode 100644 index 1e5f147..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/pages-domain.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/pages.png b/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/pages.png deleted file mode 100644 index 76b51d6..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/pages.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-1.png b/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-1.png deleted file mode 100644 index 7a21ba8..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-1.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-2.png b/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-2.png deleted file mode 100644 index e4685aa..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-2.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-3.png b/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-3.png deleted file mode 100644 index af2cb05..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-3.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-bindings.png b/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-bindings.png deleted file mode 100644 index f311eee..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-bindings.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-d1-1.png b/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-d1-1.png deleted file mode 100644 index a1b74f5..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-d1-1.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-d1-2.png b/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-d1-2.png deleted file mode 100644 index 279d854..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-d1-2.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-d1.png b/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-d1.png deleted file mode 100644 index a0b6cac..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-d1.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-kv-0.png b/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-kv-0.png deleted file mode 100644 index 8ad3d59..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-kv-0.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-kv-1.png b/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-kv-1.png deleted file mode 100644 index 8c6380c..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-kv-1.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-kv-2.png b/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-kv-2.png deleted file mode 100644 index 5d601e2..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-kv-2.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-kv.png b/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-kv.png deleted file mode 100644 index 84103aa..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-kv.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-runtime.png b/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-runtime.png deleted file mode 100644 index afed4da..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-runtime.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-upload.png b/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-upload.png deleted file mode 100644 index aa7ded9..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-upload.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-var.png b/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-var.png deleted file mode 100644 index 9658aee..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker-var.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker_home.png b/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker_home.png deleted file mode 100644 index b435200..0000000 Binary files a/cloudflare_temp_email/vitepress-docs/docs/public/ui_install/worker_home.png and /dev/null differ diff --git a/cloudflare_temp_email/vitepress-docs/docs/reference.md b/cloudflare_temp_email/vitepress-docs/docs/reference.md deleted file mode 100644 index 4b779c7..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/reference.md +++ /dev/null @@ -1,6 +0,0 @@ -# Reference - -- https://developers.cloudflare.com/d1/ -- https://developers.cloudflare.com/pages/ -- https://developers.cloudflare.com/workers/ -- https://developers.cloudflare.com/email-routing/ diff --git a/cloudflare_temp_email/vitepress-docs/docs/status.md b/cloudflare_temp_email/vitepress-docs/docs/status.md deleted file mode 100644 index 4b6063e..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/status.md +++ /dev/null @@ -1,8 +0,0 @@ -# Status Page - -[Status Link](https://uptime.aks.awsl.icu/status/temp-email) - -| Service | Status | -| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Backend](https://temp-email-api.awsl.uk/) | ![](https://uptime.aks.awsl.icu/api/badge/10/status) ![](https://uptime.aks.awsl.icu/api/badge/10/uptime) ![](https://uptime.aks.awsl.icu/api/badge/10/ping) ![](https://uptime.aks.awsl.icu/api/badge/10/avg-response) ![](https://uptime.aks.awsl.icu/api/badge/10/cert-exp) ![](https://uptime.aks.awsl.icu/api/badge/10/response) | -| [Frontend](https://mail.awsl.uk/) | ![](https://uptime.aks.awsl.icu/api/badge/12/status) ![](https://uptime.aks.awsl.icu/api/badge/12/uptime) ![](https://uptime.aks.awsl.icu/api/badge/12/ping) ![](https://uptime.aks.awsl.icu/api/badge/12/avg-response) ![](https://uptime.aks.awsl.icu/api/badge/12/cert-exp) ![](https://uptime.aks.awsl.icu/api/badge/12/response) | diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/actions/auto-update.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/actions/auto-update.md deleted file mode 100644 index e110a8f..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/actions/auto-update.md +++ /dev/null @@ -1,10 +0,0 @@ -# Github Actions 部署如何配置自动更新 - -::: warning 注意 -有问题请通过 `Github Issues` 反馈,感谢。 -自动更新不会执行 D1 数据库的 sql 文件,当数据库 schema 变动时,需要手动执行。 -::: - -1. 打开仓库的 `Actions` 页面,找到 `Upstream Sync`,点击 `enable workflow` 启用 `workflow` -2. 如果 `Upstream Sync` 运行失败,到仓库主页点击 `Sync` 手动同步即可 -3. 修改 `Upstream Sync` 的 `schedule` 配置可自定义更新间隔,参考 [cron 表达式](https://crontab.guru/) diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/actions/d1.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/actions/d1.md deleted file mode 100644 index d94be15..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/actions/d1.md +++ /dev/null @@ -1,17 +0,0 @@ -# 初始化/更新 D1 数据库 - -## 创建数据库 - -打开 cloudflare 控制台,选择 `Workers & Pages` -> `D1` -> `Create Database`,点击创建数据库 - -![d1](/ui_install/d1.png) - -创建完成后,我们在 cloudflare 的控制台可以看到 D1 数据库,并获取到数据库的 `名称` 和 `数据库 ID` - -## 初始化数据库 - -在部署完成后,在 admin 页面的 `快速设置` -> `数据库` 中,点击 `初始化数据库` 按钮来初始化数据库 - -## 更新数据库 schema - -参考 [命令行更新 d1](/zh/guide/cli/d1) 或者 [用户界面更新 d1](/zh/guide/ui/d1) diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/actions/github-action.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/actions/github-action.md deleted file mode 100644 index 3820271..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/actions/github-action.md +++ /dev/null @@ -1,60 +0,0 @@ -# 通过 Github Actions 部署 - -::: warning 注意 -目前只支持 worker 和 pages 的部署。 -有问题请通过 `Github Issues` 反馈,感谢。 - -`worker.dev` 域名在中国无法访问,请自定义域名 -::: - -## 部署步骤 - -### Fork 仓库并启用 Actions - -- 在 GitHub fork 本仓库 -- 打开仓库的 `Actions` 页面 -- 找到 `Deploy Backend` 点击 `enable workflow` 启用 `workflow` -- 如果需要前后端分离部署, 找到`Deploy Frontend` 点击 `enable workflow` 启用 `workflow` - -### 配置 Secrets - -然后在仓库页面 `Settings` -> `Secrets and variables` -> `Actions` -> `Repository secrets`, 添加以下 `secrets`: - -- 公共 `secrets` - - | 名称 | 说明 | - | ----------------------- | --------------------------------------------------------------------------------------------------------------- | - | `CLOUDFLARE_ACCOUNT_ID` | Cloudflare 账户 ID, [参考文档](https://developers.cloudflare.com/workers/wrangler/ci-cd/#cloudflare-account-id) | - | `CLOUDFLARE_API_TOKEN` | Cloudflare API Token, [参考文档](https://developers.cloudflare.com/workers/wrangler/ci-cd/#api-token) | - -- worker 后端 `secrets` - - | 名称 | 说明 | - | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | - | `BACKEND_TOML` | 后端配置文件,[参考此处](/zh/guide/cli/worker.html#修改-wrangler-toml-配置文件) | - | `DEBUG_MODE` | (可选) 是否开启调试模式,配置为 `true` 开启, 默认 worker 部署日志不会输出到 Github Actions 页面,开启后会输出 | - | `BACKEND_USE_MAIL_WASM_PARSER` | (可选) 是否使用 wasm 解析邮件,配置为 `true` 开启, 功能参考 [配置 worker 使用 wasm 解析邮件](/zh/guide/feature/mail_parser_wasm_worker) | - | `USE_WORKER_ASSETS` | (可选) 部署带有前端资源的 Worker, 配置为 `true` 开启 | - -- pages 前端 `secrets` - - > [!warning] 注意 - > 如果选择部署带有前端资源的 Worker, 则无须配置这些 `secrets` - - | 名称 | 说明 | - | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | `FRONTEND_ENV` | 前端配置文件,请复制 `frontend/.env.example` 的内容,[并参考此处修改](/zh/guide/cli/pages.html) | - | `FRONTEND_NAME` | 你在 Cloudflare Pages 创建的项目名称,可通过 [用户界面](https://temp-mail-docs.awsl.uk/zh/guide/ui/pages.html) 或者 [命令行](https://temp-mail-docs.awsl.uk/zh/guide/cli/pages.html) 创建 | - | `FRONTEND_BRANCH` | (可选) pages 部署的分支,可不配置,默认 `production` | - | `TG_FRONTEND_NAME` | (可选) 你在 Cloudflare Pages 创建的项目名称,同 `FRONTEND_NAME`,如果需要 Telegram Mini App 功能,请填写 | - -### 部署 - -- 打开仓库的 `Actions` 页面 -- 找到 `Deploy Backend` 点击 `Run workflow` 选择分支手动部署 -- 如果需要前后端分离部署, 找到 `Deploy Frontend`, 点击 `Run workflow` 选择分支手动部署 - -## 如何配置自动更新 - -1. 打开仓库的 `Actions` 页面,找到 `Upstream Sync`,点击 `enable workflow` 启用 `workflow` -2. 如果 `Upstream Sync` 运行失败,到仓库主页点击 `Sync` 手动同步即可 diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/actions/pre-requisite.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/actions/pre-requisite.md deleted file mode 100644 index b7530d1..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/actions/pre-requisite.md +++ /dev/null @@ -1,10 +0,0 @@ -# Gihub Actions 部署准备 - -## GitHub 账户 - -- 需要一个 GitHub 账户 -- 良好的网络环境 - -## Fork 仓库 - -- 在 GitHub fork [本仓库](https://github.com/dreamhunter2333/cloudflare_temp_email.git) diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/cli/d1.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/cli/d1.md deleted file mode 100644 index faad396..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/cli/d1.md +++ /dev/null @@ -1,30 +0,0 @@ -# 初始化/更新 D1 数据库 - -第一次执行登录 wrangler 命令时,会提示登录, 按提示操作即可 - -## 初始化数据库 - -```bash -cd worker -cp wrangler.toml.template wrangler.toml -# 创建 D1 并执行 schema.sql -wrangler d1 create dev -wrangler d1 execute dev --file=../db/schema.sql --remote -``` - -创建完成后,我们在 cloudflare 的控制台可以看到 D1 数据库 - -![D1](/readme_assets/d1.png) - -## 更新数据库 schema - -`schema` 更新,请确认你之前部署的版本, -查看 [更新日志](https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/CHANGELOG.md) - -找到需要执行的 `patch` 文件, 执行, 例如: - -```bash -cd worker -wrangler d1 execute dev --file=../db/2024-01-13-patch.sql --remote -wrangler d1 execute dev --file=../db/2024-04-03-patch.sql --remote -``` diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/cli/pages.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/cli/pages.md deleted file mode 100644 index 897144e..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/cli/pages.md +++ /dev/null @@ -1,51 +0,0 @@ -# Cloudflare Pages 前端 - -> [!warning] 注意 -> 下面几种方式选择一种即可 - -## 部署带有前端资源的 Worker - -参考 [部署 Worker](/zh/guide/cli/worker#部署带有前端页面的-worker-可选) - -## 前后端分离部署 - -第一次部署会提示创建项目, `production` 分支请填写 `production` - -```bash -cd frontend -pnpm install -cp .env.example .env.prod -``` - -修改 `.env.prod` 文件 - -将 `VITE_API_BASE` 修改为上一步创建的 `worker` 的 `url`, 不要在末尾加 `/` - -例如: `VITE_API_BASE=https://xxx.xxx.workers.dev` - -```bash -pnpm build --emptyOutDir -# 第一次部署会提示创建项目, production 分支请填写 production -pnpm run deploy -``` - -部署完成之后你可以在 Cloudflare 控制台看到你的项目, 可以为 `pages` 配置自定义域名 - -![pages](/readme_assets/pages.png) - -## 通过 page functions 转发后端请求 - -从 page functions 转发请求到 worker 后端, 可以获取更快的响应速度 - -第一次部署会提示创建项目, `production` 分支请填写 `production` - -如果你的 worker 后端 名称不为 `cloudflare_temp_email` 请修改 `pages/wrangler.toml` - -```bash -cd frontend -pnpm install -# 如果你要启用 Cloudflare Zero Trust, 需要使用 pnpm build:pages:nopwa 来禁用缓存 -pnpm build:pages -cd ../pages -pnpm run deploy -``` diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/cli/pre-requisite.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/cli/pre-requisite.md deleted file mode 100644 index aa42707..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/cli/pre-requisite.md +++ /dev/null @@ -1,17 +0,0 @@ -# 先决条件 - -## wrangler 的安装 - -安装 wrangler - -```bash -npm install wrangler -g -``` - -## 克隆项目 - -```bash -git clone https://github.com/dreamhunter2333/cloudflare_temp_email.git -# 切换到最新 tag 或者你想部署的分支,你也可以直接使用 main 分支 -# git checkout $(git describe --tags $(git rev-list --tags --max-count=1)) -``` diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/cli/worker.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/cli/worker.md deleted file mode 100644 index d035c35..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/cli/worker.md +++ /dev/null @@ -1,147 +0,0 @@ -# Cloudflare Worker 后端 - -> [!warning] 注意 -> `worker.dev` 域名在中国无法访问,请自定义域名 - -## 初始化项目 - -```bash -cd worker -pnpm install -cp wrangler.toml.template wrangler.toml -``` - -## 创建 KV 缓存 - -> [!NOTE] -> 如果你要启用注册用户功能,并需要发送邮件验证,则需要创建 `KV` 缓存, 不需要可跳过此步骤 -> 如果需要 Telegram Bot,需要创建 `KV` 缓存,不需要可跳过此步骤 - -通过命令行创建 KV 缓存,或者在 Cloudflare 控制台创建,然后复制对应配置到 `wrangler.toml` 文件中 - -```bash -wrangler kv:namespace create DEV -``` - -## 修改 `wrangler.toml` 配置文件 - -> [!NOTE] 注意 -> 更多变量的配置请查看 [worker变量说明](/zh/guide/worker-vars) - -```toml -name = "cloudflare_temp_email" -main = "src/worker.ts" -compatibility_date = "2024-09-23" -compatibility_flags = [ "nodejs_compat" ] - -# 如果你想使用自定义域名,你需要添加 routes 配置 -# routes = [ -# { pattern = "temp-email-api.xxxxx.xyz", custom_domain = true }, -# ] - -# 如果你想要部署带有前端资源的 worker, 你需要添加 assets 配置 -# [assets] -# directory = "../frontend/dist/" -# binding = "ASSETS" -# run_worker_first = true - -# 如果你想要使用定时任务清理邮件,取消下面的注释,并修改 cron 表达式 -# [triggers] -# crons = [ "0 0 * * *" ] - -# 通过 Cloudflare 发送邮件 -# send_email = [ -# { name = "SEND_MAIL" }, -# ] - -[vars] -# 邮箱名称前缀,不需要后缀可配置为空字符串或者不配置 -PREFIX = "tmp" -# 用于临时邮箱的所有域名, 支持多个域名 -DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] -# 用于生成 jwt 的密钥, jwt 用于给用户登录以及鉴权 -JWT_SECRET = "xxx" - -# admin 控制台密码, 不配置则不允许访问控制台 -# ADMIN_PASSWORDS = ["123", "456"] - -# 是否允许用户创建邮件, 不配置则不允许 -ENABLE_USER_CREATE_EMAIL = true -# 允许用户删除邮件, 不配置则不允许 -ENABLE_USER_DELETE_EMAIL = true - -# D1 数据库的名称和 ID 可以在 cloudflare 控制台查看 -[[d1_databases]] -binding = "DB" -database_name = "xxx" # D1 数据库名称 -database_id = "xxx" # D1 数据库 ID - -# kv config 用于用户注册发送邮件验证码,如果不启用用户注册或不启用注册验证,可以不配置 -# [[kv_namespaces]] -# binding = "KV" -# id = "xxxx" - -# 新建地址限流配置 /api/new_address -# [[unsafe.bindings]] -# name = "RATE_LIMITER" -# type = "ratelimit" -# namespace_id = "1001" -# # 10 requests per minute -# simple = { limit = 10, period = 60 } - -# 绑定其他 worker 处理邮件,例如通过 auth-inbox ai 能力解析验证码或激活链接 -# [[services]] -# binding = "AUTH_INBOX" -# service = "auth-inbox" -``` - -## 部署带有前端页面的 worker(可选) - -> [!NOTE] -> 如果不需要 [带有前端页面的 worker],可以跳过此步骤 -> 参考之后部署前端文档,可以进行前后端分离部署 - -确认已构建前端资源到 `frontend/dist` 目录 - -```bash -cd frontend -pnpm install --no-frozen-lockfile -pnpm build:pages -``` - -`worker` 目录下的 `wrangler.toml` 文件中添加下面的配置 - -```toml -[assets] -directory = "../frontend/dist/" -binding = "ASSETS" -run_worker_first = true -``` - -## Telegram Bot 配置 - -> [!NOTE] -> 如果不需要 Telegram Bot, 可跳过此步骤 - -请先创建一个 Telegram Bot,然后获取 `token`,然后执行下面的命令,将 `token` 添加到 secrets 中 - -```bash -pnpm wrangler secret put TELEGRAM_BOT_TOKEN -``` - -## 部署 - -第一次部署会提示创建项目, `production` 分支请填写 `production` - -```bash -pnpm run deploy -``` - -部署成功之后再路由中可以看到 `worker` 的 `url`,控制台也会输出 `worker` 的 `url` - -![worker](/readme_assets/worker.png) - -> [!NOTE] -> 打开 `worker` 的 `url`,如果显示 `OK` 说明部署成功 -> -> 打开 `/health_check`,如果显示 `OK` 说明部署成功 diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/common-issues.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/common-issues.md deleted file mode 100644 index 91f2ea5..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/common-issues.md +++ /dev/null @@ -1,41 +0,0 @@ -# 常见问题 - -> [!NOTE] 注意 -> 如果你的问题没有在这里找到解决方案,请到 `Github Issues` 中搜索或者提问, 或者到 Telegram 群组中提问。 - -## 通用 - -| 问题 | 解决方案 | -| -------------------------------------------------- | ------------------------------------------------------------------------------- | -| 使用 Cloudflare Workers 给已认证的转发邮箱发送邮件 | 使用 cf 的 API 进行发送,只支持绑定到 CF 上的收件地址,即 CF EMAIL 转发目的地址 | -| 绑定多个域名 | 每个域名都需要设置 email 转发到 worker | - -## worker 相关 - -| 问题 | 解决方案 | -| ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| `Uncaught Error: No such module "path". imported from "worker.js"` | [参考](/zh/guide/ui/worker) | -| `No such module "node:stream". imported from "worker.js"` | [参考](/zh/guide/ui/worker) | -| `二级域名无法发送邮件` | [参考](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/515) | -| `Failed to send verify code: No balance` | admin 后台设置无限制邮件或者发件权限页面增加额度 | -| `Github OAuth无法获取到邮箱 400 Failed to get user email` | 需要 github 用户设置公开邮箱 | -| `Cannot read properties of undefined (reading 'map')` | worker 变量没有设置成功 | - -## pages 相关 - -| 问题 | 解决方案 | -| --------------- | ---------------------------------------- | -| `network error` | 使用无痕模式或者清空浏览器缓存,DNS 缓存 | - -## telegram bot - -| 问题 | 解决方案 | -| -------------------------------------------------------------- | -------------------------------------------------- | -| `Telgram Bot获取邮件失败:400:Bad Request:BUTTON_URL_INVALID` | tg mini app 的 URL 填写错误,需要填写 pages 的 URL | -| `Telegram bot bind error: bind adress count reach the limit` | 需要设置 worker 变量 `TG_MAX_ADDRESS` | - -## Github Actions - -| 问题 | 解决方案 | -| ------------------------------------------ | --------------------------------------------------------------------------------- | -| Github Action部署后,cf里始终是preview分支 | 到 cf pages 页面的设置中确认 前端的分支 和 Github Action 的 前端部署分支 是否相同 | diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/config-send-mail.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/config-send-mail.md deleted file mode 100644 index 01b7da5..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/config-send-mail.md +++ /dev/null @@ -1,84 +0,0 @@ - -# 配置发送邮件 - -::: warning 注意 -三种方式可以同时配置,发送邮件时会优先使用 `resend`,如果没有配置 `resend`,则会使用 `smtp`. - -如果配置了 Cloudflare 已认证的转发邮箱地址,会优先使用 cf 内部 API 发送邮件 -::: - -## 使用 resend 发送邮件 - -注册 `https://resend.com/domains` 根据提示添加 DNS 记录, - -`API KEYS` 页面创建 `api key` - -然后执行下面的命令,将 `RESEND_TOKEN` 添加到 secrets 中 - -> [!NOTE] -> 如果你觉得麻烦,也可以直接明文放在 `wrangler.toml` 中 `[vars]` 下面,但是不推荐这样做 - -如果你是通过 UI 部署的,可以在 Cloudflare 的 UI 界面中添加到 `Variables and Secrets` 下面 - -```bash -# 切换到 worker 目录 -cd worker -wrangler secret put RESEND_TOKEN -``` - -如果你有多个域名,对应不同的 `api key`,可以在 `wrangler.toml` 中添加多个 secret, 名称为 `RESEND_TOKEN_` + `<. 换成 _ 的 大写域名>`,例如 - -```bash -wrangler secret put RESEND_TOKEN_XXX_COM -wrangler secret put RESEND_TOKEN_DREAMHUNTER2333_XYZ -``` - -## 使用 SMTP 发送邮件 - -`SMTP_CONFIG` 的格式如下,key 为域名,value 为 SMTP 配置,SMTP 配置格式详情可以参考 [zou-yu/worker-mailer](https://github.com/zou-yu/worker-mailer/blob/main/README_zh-CN.md) - -```json -{ - "awsl.uk": { - "host": "smtp.xxx.com", - "port": 465, - "secure": true, - "authType": [ - "plain", - "login" - ], - "credentials": { - "username": "username", - "password": "password" - } - } -} -``` - -然后执行下面的命令,将 `SMTP_CONFIG` 添加到 secrets 中 - -> [!NOTE] -> 如果你觉得麻烦,也可以直接明文放在 `wrangler.toml` 中 `[vars]` 下面,但是不推荐这样做 - -如果你是通过 UI 部署的,可以在 Cloudflare 的 UI 界面中添加到 `Variables and Secrets` 下面 - -```bash -# 切换到 worker 目录 -cd worker -wrangler secret put SMTP_CONFIG -``` - -## 给 Cloudflare 上已认证的转发邮箱发送邮件 - -仅支持 CLI 部署时使用,在 `wrangler.toml` 中添加 `send_email` 配置 - -发送的目的邮箱地址必须是 Cloudflare 上已认证的邮箱地址,局限性较大,如果需要发送邮件给其他邮箱,可以使用 `resend` 或者 `smtp` 发送邮件 - -```toml -# 通过 Cloudflare 发送邮件 -send_email = [ - { name = "SEND_MAIL" }, -] -``` - -admin 后台 账号配置 `已验证地址列表(可通过 cf 内部 api 发送邮件)` diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/email-routing.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/email-routing.md deleted file mode 100644 index c639f8f..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/email-routing.md +++ /dev/null @@ -1,9 +0,0 @@ -# Cloudflare Email Routing - -1. 在 CF 控制台网页的对应域名的 `Email Routing` 下,配置 `电子邮件 DNS 记录`, 如果是多个域名,需要配置多个域名的 `电子邮件 DNS 记录` - -2. 在将电子邮件地址绑定到您的 Worker 之前,您需要启用电子邮件路由并拥有至少一个经过验证的电子邮件地址(目标地址)。 - -3. 配置每个域名的 `Email Routing` 的路由规则中的 `Catch-all 地址` 发送到 `worker` - -![email](/readme_assets/email.png) diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/admin-user-management.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/admin-user-management.md deleted file mode 100644 index e85c568..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/admin-user-management.md +++ /dev/null @@ -1,11 +0,0 @@ -# Admin 用户相关 - -## 用户管理页面 - -![admin-user-management](/feature/admin-user-management.png) - -## 用户设置 - -此处开启用户登录,以及验证等配置 - -![admin-user-page](/feature/admin-user-page.png) diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/admin.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/admin.md deleted file mode 100644 index 8275415..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/admin.md +++ /dev/null @@ -1,15 +0,0 @@ -# Admin 控制台 - -> [!NOTE] -> 需要配置 `ADMIN_PASSWORDS` 或者 `ADMIN_USER_ROLE` 才可以访问 admin 控制台 -> admin 角色配置, 如果用户角色等于 ADMIN_USER_ROLE 则可以访问 admin 控制台 - -部署前端应用之后,点击 左上角 logo 5 次 或者访问 `/admin` 路径即可进入管理控制台。 - -需要在后端配置 `ADMIN_PASSWORDS` 或者当前用户角色为 `ADMIN_USER_ROLE`, 则不允许访问控制台。 - -![admin](/feature/admin.png) - -## 如果你的网站只可私人访问,可通过此禁用检查 - -`DISABLE_ADMIN_PASSWORD_CHECK = true` diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/another-worker-enhanced.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/another-worker-enhanced.md deleted file mode 100644 index 9b34bfa..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/another-worker-enhanced.md +++ /dev/null @@ -1,144 +0,0 @@ -# 通过其他 worker 增强 - -> 临时邮箱的核心能力在邮件的管理,通过其他 worker 可以增强临时邮箱的功能,例如通过 auth-inbox ai 能力解析验证码或激活链接 -> 该功能仅触发其他 worker ,在 webhook 后执行 -> [!NOTE] -> 如果要使用 worker 增强,请提前创建可以 rpc 调用的 worker,具体下文详述 -> 参考: -> - https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/ -> - https://developers.cloudflare.com/workers/runtime-apis/rpc/ -> - auth-inbox 项目:https://github.com/TooonyChen/AuthInbox - -## 创建其他 worker(以 auth-inbox 项目ai解析验证码为例子) - -### worker 改造为继承 WorkerEntrypoint - -一个简单,作为被调用方,提供 rpc 方法调用的worker代码如下(rpcEmail 方法为样例) -(使用已经修改好的项目 https://github.com/oneisall8955/AuthInbox-fork) - -src/index.ts 文件 -```js -import { WorkerEntrypoint } from "cloudflare:workers"; - -interface Env { - DB: D1Database; - // ... -} - -export default class extends WorkerEntrypoint { - async fetch(request: Request): Promise { - console.log("原本fetch接口入参是request,env,ctx"); - console.log("修改为WorkerEntrypoint风格后,只有一个入参request,获取环境变量和上下文有小改动"); - // 环境变量及上下文改动详见: - // https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#bindings-env - // https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#lifecycle-methods-ctx - const env: Env = this.env; - const ctx: ExecutionContext = this.ctx; - console.log("后续逻辑不变"); - return new Response('ok', { status: 200 }); - } - - // 主要功能 - async email(message: ForwardableEmailMessage): Promise { - console.log("原本fetch接口入参是message,env,ctx"); - console.log("修改为WorkerEntrypoint风格后,只有一个入参message,获取环境变量和上下文和fetch方法一样"); - const env: Env = this.env; - const ctx: ExecutionContext = this.ctx; - console.log("接受email routing请求后,后续逻辑不变"); - } - - // 暴露rpc接口,处理来自其他worker的邮件请求 - async rpcEmail(requestBody: string): Promise { - console.log(`接受其他worker(临时邮件服务cloudflare_temp_email)的请求,request body: ${requestBody}`); - // requestBody json 格式,由临时邮件服务发送,格式如下 - // type RPCEmailMessage = { - // from: string | undefined | null, - // to: string | undefined | null, - // rawEmail: string | undefined | null, - // headers: Map, - // } - // ... todo ... - } -} -``` - -### 部署其他 worker - -修改好或者使用 以auth-inbox 为例,部署到 cloudflare worker 上,详见 https://github.com/TooonyChen/AuthInbox ,或者使用已经修改好的项目 https://github.com/oneisall8955/AuthInbox-fork - -## 配置临时邮件服务,使用指定其他 worker 增强 - -## 绑定服务 - -### 通过 wrangler.toml 配置 - -```toml -[[services]] -binding = "AUTH_INBOX" -service = "auth-inbox" -``` - -这里的 `binding = "AUTH_INBOX"` 可以自定义,可以是任何字符串,`service = "auth-inbox"` 是部署好的提供rpc接口调用的worker名称。 - -### 用户界面配置 - -在设置-绑定,添加绑定,选择绑定服务。 -变量名称填写自定义的名称,可以任意字符串 ,例如 `AUTH_INBOX`。 -服务绑定选择上一步创建好的服务,例如 `auth-inbox`。 - -![another-worker-enhanced-01.png](/feature/another-worker-enhanced-01.png) - -![another-worker-enhanced-02.png](/feature/another-worker-enhanced-02.png) - -## 环境变量配置 - -### 通过 wrangler.toml 配置 - -```toml -ENABLE_ANOTHER_WORKER = true -ANOTHER_WORKER_LIST =""" -[ - { - "binding":"AUTH_INBOX", - "method":"rpcEmail", - "keywords":[ - "验证码","激活码","激活链接","确认链接","验证邮箱","确认邮件","账号激活","邮件验证","账户确认","安全码","认证码","安全验证","登陆码","确认码","启用账户","激活账户","账号验证","注册确认", - "account","activation","verify","verification","activate","confirmation","email","code","validate","registration","login","code","expire","confirm" - ] - } -] -""" -``` - -环境变量解释: -- ENABLE_ANOTHER_WORKER = true:默认为false,true则开启其他 worker 处理邮件 -- ANOTHER_WORKER_LIST 是一个JOSN数组,每个对象包3个字段 - - binding: *必填,必须与services部分指定的 binding = "XXX" 保持一致*,例子中为 AUTH_INBOX - - method: 可选,默认 rpcEmail,指的是调用这个 worker 的哪一个 rpc 方法处理 - - keywords: 关键词数组,忽略大小写。用于过滤,如果*解析后邮件文本*匹配到这些关键词,触发这个 worker,并且调用这个 worker 的 `method` 方法 - -### 用户界面配置 - -在设置-环境变量,添加环境变量 -- ENABLE_ANOTHER_WORKER = true -- ANOTHER_WORKER_LIST 为上面提及的JSON数组字符串,不再复述,详细介绍看上文 -```json -[ - { - "binding":"AUTH_INBOX", - "method":"rpcEmail", - "keywords":[ - "验证码","激活码","激活链接","确认链接","验证邮箱","确认邮件","账号激活","邮件验证","账户确认","安全码","认证码","安全验证","登陆码","确认码","启用账户","激活账户","账号验证","注册确认", - "account","activation","verify","verification","activate","confirmation","email","code","validate","registration","login","code","expire","confirm" - ] - } -] -``` - -![another-worker-enhanced-03.png](/feature/another-worker-enhanced-03.png) - -## 测试 - -发送一个邮件到临时邮箱,观察worker日志到,或者到 auth-inbox 提供的面板上查看验证码 - -![another-worker-enhanced-04.png](/feature/another-worker-enhanced-04.png) diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/config-smtp-proxy.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/config-smtp-proxy.md deleted file mode 100644 index 68f585d..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/config-smtp-proxy.md +++ /dev/null @@ -1,60 +0,0 @@ -# 搭建 SMTP IMAP 代理服务 - -::: warning 注意 -如果你使用了 `resend`, 可直接使用 `resend` 的 `SMTP` 服务,不需要使用此服务 -::: - -## 为什么需要 SMTP IMAP 代理服务 - -`SMTP` `IMAP` 的应用场景更加广泛 - -## 如何搭建 SMTP IMAP 代理服务 - -### Local Run - -```bash -cd smtp_proxy_server/ -# 复制配置文件, 并修改配置文件 -# 你的 worker 地址,proxy_url=https://temp-email-api.xxx.xxx -# 你的 SMTP 服务端口,port=8025 -cp .env.example .env -python3 -m venv venv -./venv/bin/python3 -m pip install -r requirements.txt -./venv/bin/python3 main.py -``` - -### Docker Run - -```bash -cd smtp_proxy_server/ -docker-compose up -d -``` - -修改 docker-compose.yaml 中的环境变量, 注意选择合适的 `tag` - -`proxy_url` 为 `worker` 的 URL 地址 - -```yaml -services: - smtp_proxy_server: - image: ghcr.io/dreamhunter2333/cloudflare_temp_email/smtp_proxy_server:latest - # build: - # context: . - # dockerfile: dockerfile - container_name: "smtp_proxy_server" - ports: - - "8025:8025" - - "11143:11143" - environment: - - proxy_url=https://temp-email-api.xxx.xxx - - port=8025 - - imap_port=11143 -``` - -## 使用 Thunderbird 登录 - -下载 [Thunderbird](https://www.thunderbird.net/en-US/) - -密码填写 `邮箱地址凭证` - -![imap](/feature/imap.png) diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/google-ads.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/google-ads.md deleted file mode 100644 index 8dd5db0..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/google-ads.md +++ /dev/null @@ -1,29 +0,0 @@ -# 给网页增加 Google Ads - -## 命令行部署 - -修改 `.env.prod` 文件 - -增加下列两个变量, 具体的值请参考 [Google AdSense](https://www.google.com/adsense/start/) 的说明 - -```txt -VITE_GOOGLE_AD_CLIENT=ca-pub-123456 -VITE_GOOGLE_AD_SLOT=123456 -``` - -然后执行下列命令, 重新部署 pages 即可. - -```bash -pnpm build --emptyOutDir -# 第一次部署会提示创建项目, production 分支请填写 production -pnpm run deploy -``` - -## GitHub Action 部署 - -修改 `FRONTEND_ENV`, 增加下列两个变量, 具体的值请参考 [Google AdSense](https://www.google.com/adsense/start/) 的说明, 重新部署 pages 即可. - -```txt -VITE_GOOGLE_AD_CLIENT=ca-pub-123456 -VITE_GOOGLE_AD_SLOT=123456 -``` diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/mail-api.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/mail-api.md deleted file mode 100644 index d776b22..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/mail-api.md +++ /dev/null @@ -1,66 +0,0 @@ -# 查看邮件 API - -## 通过 邮件 API 查看邮件 - -这是一个 `python` 的例子,使用 `requests` 库查看邮件。 - -```python -limit = 10 -offset = 0 -res = requests.get( - f"https://<你的worker地址>/api/mails?limit={limit}&offset={offset}", - headers={ - "Authorization": f"Bearer {你的JWT密码}", - # "x-custom-auth": "<你的网站密码>", # 如果启用了自定义密码 - "Content-Type": "application/json" - } -) -``` - -## admin 邮件 API - -支持 `address` filter 和 `keyword` filter - -```python -import requests - -url = "https://<你的worker地址>/admin/mails" - -querystring = { - "limit":"20", - "offset":"0", - # adress 和 keyword 为可选参数 - "address":"xxxx@awsl.uk", - "keyword":"xxxx" -} - -headers = {"x-admin-auth": "<你的Admin密码>"} - -response = requests.get(url, headers=headers, params=querystring) - -print(response.json()) -``` - -## user 邮件 API - -支持 `address` filter 和 `keyword` filter - -```python -import requests - -url = "https://<你的worker地址>/user_api/mails" - -querystring = { - "limit":"20", - "offset":"0", - # adress 和 keyword 为可选参数 - "address":"xxxx@awsl.uk", - "keyword":"xxxx" -} - -headers = {"x-admin-auth": "<你的Admin密码>"} - -response = requests.get(url, headers=headers, params=querystring) - -print(response.json()) -``` diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/mail_parser_wasm_worker.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/mail_parser_wasm_worker.md deleted file mode 100644 index cfd1522..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/mail_parser_wasm_worker.md +++ /dev/null @@ -1,82 +0,0 @@ -# mail-parser-wasm-worker - -> [!NOTE] -> 如果你使用了 webhook 转发,或者 telegram bot 接受邮件,但是邮件内容是乱码,或者无法解析,你对解析的需要更高的要求,可以使用这个功能。 - -## UI 部署 - -1. 下载 [worker-with-wasm-mail-parser.zip](https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest/download/worker-with-wasm-mail-parser.zip) - -2. 回到 `Overview`,找到刚刚创建的 worker,点击 `Edit Code`, 删除原来的文件,上传 `worker.js` 和 `wasm` 后缀的文件, 点击 `Deploy` - - > [!NOTE] - > 上传需要先点击左侧菜单的 Explorer, - > 在文件列表的窗口里点击鼠标右键,在右键菜单里找到 `Upload`, - > 请参考下面的截图 - > - > 参考: [issues156](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/156#issuecomment-2079453822) - - ![worker2](/ui_install/worker-2.png) - ![worker-upload](/ui_install/worker-upload.png) - -## CLI 部署 - -### 修改代码 - -```bash -cd worker -pnpm add mail-parser-wasm-worker -``` - -编辑 `worker/src/common.ts`, 取消注释这段代码,使用 mail-parser-wasm-worker 来解析邮件 - -```ts -export const commonParseMail = async (raw_mail: string | undefined | null): Promise<{ - sender: string, - subject: string, - text: string, - html: string -} | undefined> => { - if (!raw_mail) { - return undefined; - } - // 取消注释这段代码,使用 mail-parser-wasm-worker 来解析邮件 start - // TODO: WASM parse email - try { - const { parse_message_wrapper } = await import('mail-parser-wasm-worker'); - - const parsedEmail = parse_message_wrapper(raw_mail); - return { - sender: parsedEmail.sender || "", - subject: parsedEmail.subject || "", - text: parsedEmail.text || "", - headers: parsedEmail.headers || [], - html: parsedEmail.body_html || "", - }; - } catch (e) { - console.error("Failed use mail-parser-wasm-worker to parse email", e); - } - // 取消注释这段代码,使用 mail-parser-wasm-worker 来解析邮件 end - try { - const { default: PostalMime } = await import('postal-mime'); - const parsedEmail = await PostalMime.parse(raw_mail); - return { - sender: parsedEmail.from ? `${parsedEmail.from.name} <${parsedEmail.from.address}>` : "", - subject: parsedEmail.subject || "", - text: parsedEmail.text || "", - html: parsedEmail.html || "", - }; - } - catch (e) { - console.error("Failed use PostalMime to parse email", e); - } - return undefined; -} -``` - -### 部署 - -```bash -cd worker -pnpm run deploy -``` diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/new-address-api.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/new-address-api.md deleted file mode 100644 index f80fbd4..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/new-address-api.md +++ /dev/null @@ -1,92 +0,0 @@ -# 新建邮箱地址 API - -## 通过 admin API 新建邮箱地址 - -这是一个 `python` 的例子,使用 `requests` 库发送邮件。 - -```python -res = requests.post( - # 替换 xxxx.xxxx 为你的 worker 域名 - "https://xxxx.xxxx/admin/new_address", - json={ - # 是否启用前缀 (True/False) - "enablePrefix": True, - "name": "<邮箱名称>", - "domain": "<邮箱域名>", - }, - headers={ - 'x-admin-auth': "<你的网站admin密码>", - "Content-Type": "application/json" - } -) - -# 返回值 {"jwt": ""} -print(res.json()) -``` - -## 批量创建随机用户名邮箱地址 API 示例 - -### 通过 admin API 批量新建邮箱地址 - -这是一个 `python` 的例子,使用 `requests` 库发送邮件。 - -```python -import requests -import random -import string -from concurrent.futures import ThreadPoolExecutor, as_completed - - -def generate_random_name(): - # 生成5位英文字符 - letters1 = ''.join(random.choices(string.ascii_lowercase, k=5)) - # 生成1-3个数字 - numbers = ''.join(random.choices(string.digits, k=random.randint(1, 3))) - # 生成1-3个英文字符 - letters2 = ''.join(random.choices(string.ascii_lowercase, k=random.randint(1, 3))) - # 组合成最终名称 - return letters1 + numbers + letters2 - - -def fetch_email_data(name): - try: - res = requests.post( - "https://", - json={ - "enablePrefix": True, - "name": name, - "domain": "<邮箱域名>", - }, - headers={ - 'x-admin-auth': "<你的网站admin密码>", - "Content-Type": "application/json" - } - ) - - if res.status_code == 200: - response_data = res.json() - email = response_data.get("address", "无地址") - jwt = response_data.get("jwt", "无jwt") - return f"{email}----{jwt}\n" - else: - print(f"请求失败,状态码: {res.status_code}") - return None - except requests.RequestException as e: - print(f"请求出现错误: {e}") - return None - - -def generate_and_save_emails(num_emails): - with ThreadPoolExecutor(max_workers=30) as executor, open('email.txt', 'a') as file: - futures = [executor.submit(fetch_email_data, generate_random_name()) for _ in range(num_emails)] - - for future in as_completed(futures): - result = future.result() - if result: - file.write(result) - - -# 生成10个邮箱并追加到现有文件 -generate_and_save_emails(10) - -``` diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/s3-attachment.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/s3-attachment.md deleted file mode 100644 index 31d0167..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/s3-attachment.md +++ /dev/null @@ -1,34 +0,0 @@ -# 配置 S3 附件 - -## 配置 - -> [!NOTE] -> 如果不需要 S3 附件, 可跳过此步骤 - -在 Cloudflare 创建一个 R2 bucket, 你也可以使用其他的 S3 服务(如有 bug 请提 issue) - -参考: [配置 Cloudflare R2 的 cors](https://developers.cloudflare.com/r2/buckets/cors/#add-cors-policies-from-the-dashboard) - -参考 [Cloudflare R2 s3 toke](https://developers.cloudflare.com/r2/api/s3/tokens/) 创建 token, 拿到 `ENDPOINT`, `Access Key ID` 和 `Secret Access Key`,然后执行下面的命令添加到 secrets 中 - -> [!NOTE] -> 你也可以在 Cloudflare worker 的 UI 界面中添加 `secrets` - -```bash -cd worker -pnpm wrangler secret put S3_ENDPOINT -pnpm wrangler secret put S3_ACCESS_KEY_ID -pnpm wrangler secret put S3_SECRET_ACCESS_KEY -# 请注意这里的 bucket 是你的 bucket 名称 -pnpm wrangler secret put S3_BUCKET -``` - -## 使用 - -保存附件 - -![S3 save](/feature/s3-save.png) - -下载附件 - -![S3 download](/public/feature/s3-download.png) diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/send-mail-api.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/send-mail-api.md deleted file mode 100644 index befb9ce..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/send-mail-api.md +++ /dev/null @@ -1,67 +0,0 @@ -# 发送邮件 API - -## 通过 HTTP API 发送邮件 - -这是一个 `python` 的例子,使用 `requests` 库发送邮件。 - -```python -send_body = { - "from_name": "发件人名字", - "to_name": "收件人名字", - "to_mail": "收件人地址", - "subject": "邮件主题", - "is_html": False, # 根据内容设置是否为 HTML - "content": "<邮件内容:html 或者 文本>", -} - -res = requests.post( - "http://localhost:8787/api/send_mail", - json=send_body, headers={ - "Authorization": f"Bearer {你的JWT密码}", - # "x-custom-auth": "<你的网站密码>", # 如果启用了自定义密码 - "Content-Type": "application/json" - } -) - -# 使用 body 验证 -send_body = { - "token": "<你的JWT密码>", - "from_name": "发件人名字", - "to_name": "收件人名字", - "to_mail": "收件人地址", - "subject": "邮件主题", - "is_html": False, # 根据内容设置是否为 HTML - "content": "<邮件内容:html 或者 文本>", -} -res = requests.post( - "http://localhost:8787/external/api/send_mail", - json=send_body, headers={ - "Content-Type": "application/json" - } -) -``` - -## 通过 SMTP 发送邮件 - -请先参考 [配置 SMTP 代理](/zh/guide/feature/config-smtp-proxy.html)。 - -这是一个 `python` 的例子,使用 `smtplib` 库发送邮件。 - -`JWT令牌密码`: 即为邮箱登录密码,可以在 UI 界面中查看密码菜单中查看。 - -```python -import smtplib - -from email.mime.text import MIMEText -from email.mime.multipart import MIMEMultipart - - -with smtplib.SMTP('localhost', 8025) as smtp: - smtp.login("jwt", "此处填写你的JWT令牌密码") - message = MIMEMultipart() - message['From'] = "Me " - message['To'] = "Admin " - message['Subject'] = "测试主题" - message.attach(MIMEText("测试内容", 'html')) - smtp.sendmail("me@awsl.uk", "admin@awsl.uk", message.as_string()) -``` diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/subdomain.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/subdomain.md deleted file mode 100644 index 0ede8ee..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/subdomain.md +++ /dev/null @@ -1,11 +0,0 @@ -# 配置子域名邮箱 - -::: warning 注意 -子域名邮箱发送邮件可能无法发送邮件,建议使用主域名邮箱发送邮件,子域名邮箱仅用于接收邮件。 - -mail channel 已不被支持,下面参考中仅限收件部分。 -::: - -参考 - -- [配置子域名邮箱](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/164#issuecomment-2082612710) diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/telegram.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/telegram.md deleted file mode 100644 index 14d62ba..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/telegram.md +++ /dev/null @@ -1,66 +0,0 @@ -# 配置 Telegram Bot - -试用地址:[@cf_temp_mail_bot](https://t.me/cf_temp_mail_bot) - -::: warning 注意 -worker 默认的 `worker.dev` 域名的证书是不被 telegram 支持的,配置 Telegram Bot 请使用自定义域名 -::: - -> [!NOTE] -> 如果要使用 Telegram Bot, 请先绑定 `KV` -> -> 如果不需要 Telegram Bot, 可跳过此步骤 -> -> 如果你想 Telegram 的解析邮件能力更强,参考 [配置 worker 使用 wasm 解析邮件](/zh/guide/feature/mail_parser_wasm_worker) - -## Telegram Bot 配置 - -请先创建一个 Telegram Bot,然后获取 `token`,然后执行下面的命令,将 `token` 添加到 secrets 中 - -> [!NOTE] -> 如果你觉得麻烦,也可以直接明文放在 `wrangler.toml` 中 `[vars]` 下面,但是不推荐这样做 - -如果你是通过 UI 部署的,可以在 Cloudflare 的 UI 界面中添加到 `Variables and Secrets` 下面 - -```bash -# 切换到 worker 目录 -cd worker -pnpm wrangler secret put TELEGRAM_BOT_TOKEN -``` - -## Bot - -- 可设置白名单用户 -- 点击`初始化`即可完成配置。 -- 点击`查看状态`,可以查看当前配置的状态。 - -![telegram](/feature/telegram.png) - -## Mini App - -可以通过命令行部署,或者 UI 界面部署 - -### UI 部署 - -其他步骤参考 [UI 部署](/zh/guide/cli/pages) 中的 `前后端分离部署` - -> [!NOTE] -> 从这里下载 zip, [telegram-frontend.zip](https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest/download/telegram-frontend.zip) -> -> 修改压缩包里面的 index-xxx.js 文件 ,xx 是随机的字符串 -> -> 搜索 `https://temp-email-api.xxx.xxx` ,替换成你worker 的域名,然后部署新的zip文件 - -### 命令行部署 - -```bash -cd frontend -pnpm install -cp .env.example .env.prod -# --project-name 可以单独为 mini app 创建一个 pages, 你也可以公用一个 pages,但是可能遇到 js 加载不了的问题 -pnpm run deploy:telegram --project-name=<你的项目名称> -``` - -- 部署完成后,请在 admin 后台的 `设置` -> `电报小程序` 页面 `电报小程序 URL` 中填写网页 URL。 -- 请在 `@BotFather` 处执行 `/setmenubutton`,然后输入你的网页地址,设置左下角的 `Open App` 按钮。 -- 请在 `@BotFather` 处执行 `/newapp` 新建 app 来注册 mini app。 diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/user-oauth2.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/user-oauth2.md deleted file mode 100644 index a679fdc..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/user-oauth2.md +++ /dev/null @@ -1,26 +0,0 @@ -# OAuth2 第三方登录 - -> [!WARNING] 注意 -> 第三方登录会自动使用用户邮箱注册账号(邮箱相同将视为同一账号) -> -> 此账号和注册的账号相同, 也可以通过忘记密码设置密码 - -## 在第三方平台注册 OAuth2 - -### GitHub - -- 请先创建一个 OAuth App,然后获取 `Client ID` 和 `Client Secret` - -参考 [Creating an OAuth App](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) - -### Authentik - -- [Authentik OAuth2 Provider](https://docs.goauthentik.io/docs/providers/oauth2/) - -## Admin 后台配置 OAuth2 - -![oauth2](/feature/oauth2.png) - -## 测试用户登录页面 - -![oauth2 login](/feature/oauth2-login.png) diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/webhook.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/webhook.md deleted file mode 100644 index b5d8ae5..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/feature/webhook.md +++ /dev/null @@ -1,44 +0,0 @@ -# 配置 webhook - -> [!NOTE] -> 如果要使用 webhook,请先绑定 `KV` 并且 `worker` 变量配置 `ENABLE_WEBHOOK = true` -> -> 如果你想 webhook 的解析邮件能力更强,参考 [配置 worker 使用 wasm 解析邮件](/zh/guide/feature/mail_parser_wasm_worker) - -## 前提条件 - -你需要自建一个 `webhook 服务` 或者 使用 `第三方平台`,这个服务需要能够接收 `POST` 请求,并且能够解析 `json` 数据。 - -本项目使用了 [songquanpeng/message-pusher](https://github.com/songquanpeng/message-pusher) 示例作为 webhook 服务。 - -- 可以使用 [msgpusher.com](https://msgpusher.com) 提供的服务 -- 也可以自建 `message-pusher` 服务,参考 [songquanpeng/message-pusher](https://github.com/songquanpeng/message-pusher) - -## admin 配置全局 webhook - -![telegram](/feature/admin-mail-webhook.png) - -## admin 允许邮箱使用 webhook - -![telegram](/feature/admin-webhook-settings.png) - -## 某个邮箱配置 webhook - -![telegram](/feature/address-webhook.png) - -## webhook 数据格式 - -要获取 url 需要配置 worker 的 `FRONTEND_URL` 为你的前端地址,或者你可以通过 `id` 自己拼接 url = `${FRONTEND_URL}?mail_id=${id}` - -```json -{ - "id": "${id}", - "url": "${url}", - "from": "${from}", - "to": "${to}", - "subject": "${subject}", - "raw": "${raw}", - "parsedText": "${parsedText}", - "parsedHtml": "${parsedHtml}", -} -``` diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/quick-start.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/quick-start.md deleted file mode 100644 index f6ed57c..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/quick-start.md +++ /dev/null @@ -1,42 +0,0 @@ -# 快速开始 - -## 开始之前 - -需要 `良好的网络环境` 和 `cloudflare 账号`, 打开 [cloudflare控制台](https://dash.cloudflare.com/) - -请选择下面三种方式之一进行部署 - -- [通过命令行部署](/zh/guide/cli/pre-requisite) -- [通过用户界面部署](/zh/guide/ui/d1) -- [通过Github Actions 部署](/zh/guide/actions/pre-requisite) - -### 也可以参考网友提供的详细的小白教程 - -- [【教程】小白也能看懂的自建Cloudflare临时邮箱教程(域名邮箱)](https://linux.do/t/topic/316819/1) - -## 升级流程 - -首先确认当前的版本,然后访问 [Release 页面](https://github.com/dreamhunter2333/cloudflare_temp_email/releases/) 和 [CHANGELOG 页面](https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/CHANGELOG.md) 中找到当前的版本 - -> [!WARNING] 注意 -> 需要注意 `Breaking Changes` 是必须进行 `数据库 sql 执行` 或者 `变量配置` 的 - -然后查看从当前版本往后的所有更改,需要注意 `Breaking Changes` 是必须进行 `数据库 sql 执行` 或者 `变量配置` 的, 其他的功能更新按需配置即可 - -然后参考下面的文档使用 `CLI` 或者 `UI` 覆盖部署之前的 `worker` 和 `pages` 即可 - -### CLI 部署 - -- [命令行更新 d1](/zh/guide/cli/d1) -- [命令行部署 worker](/zh/guide/cli/worker) -- [命令行部署 pages](/zh/guide/cli/worker) - -### UI 部署 - -- [用户界面更新 d1](/zh/guide/ui/d1) -- [用户界面部署 worker](/zh/guide/ui/worker) -- [用户界面部署 pages](/zh/guide/ui/pages) - -### Github Actions 部署 - -- [Github Actions 部署如何配置自动更新](/zh/guide/actions/auto-update) diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/star-history.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/star-history.md deleted file mode 100644 index c5bf7cb..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/star-history.md +++ /dev/null @@ -1,7 +0,0 @@ -# Star History - - - - - Star History Chart - diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/ui/d1.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/ui/d1.md deleted file mode 100644 index 145bec4..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/ui/d1.md +++ /dev/null @@ -1,31 +0,0 @@ -# 初始化/更新 D1 数据库 - -## 创建数据库 - -打开 cloudflare 控制台,选择 `Storage & Databases` -> `D1 SQL Database` -> `Create Database`,点击创建数据库 - -![d1](/ui_install/d1.png) - -创建完成后,我们在 cloudflare 的控制台可以看到 D1 数据库 - -## 初始化数据库 - -::: warning 注意 -你也可以跳过初始化数据库,在部署完成后,在 admin 页面的 `快速设置` -> `数据库` 中,点击 `初始化数据库` 按钮来初始化数据库 -::: - -打开 `Console` 标签页,输入仓库中 [db/schema.sql](https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/db/schema.sql) 文件的内容,点击 `Execute` 执行 - -![d1](/ui_install/d1-exec.png) - -## 更新数据库 schema - -`schema` 更新,请确认你之前部署的版本, - -查看 [更新日志](https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/CHANGELOG.md) - -找到需要执行的 `patch` 文件, 执行, 例如: `db/2024-01-13-patch.sql` - -打开 `Console` 标签页,输入 `patch` 文件的内容,点击 `Execute` 执行 - -![d1](/ui_install/d1-exec.png) diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/ui/pages.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/ui/pages.md deleted file mode 100644 index f6efa61..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/ui/pages.md +++ /dev/null @@ -1,105 +0,0 @@ -# Cloudflare Pages 前端 - - - -1. 点击 `Compute (Workers)` -> `Workers & Pages` -> `Create` - - ![create pages](/ui_install/worker_home.png) - -2. 选择 `Pages`,选择 `Use direct upload` - - ![pages](/ui_install/pages.png) - -3. 输入部署的 worker 的地址, 地址不要带 `/`,点击生成,成功会出现下载按钮,你会得到一个 zip 包 - - 此处 worker 域名为后端 api 的域名,比如我部署在 `https://temp-email-api.awsl.uk`,则填写 `https://temp-email-api.awsl.uk` - - 如果你的域名是 `https://temp-email-api.xxx.workers.dev`,则填写 `https://temp-email-api.xxx.workers.dev` - - > [!warning] 注意 - > `worker.dev` 域名在中国无法访问,请自定义域名 - -
- - - {{ tip }} -
- - > [!NOTE] - > 你也可以手动部署,从这里下载 zip, [frontend.zip](https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest/download/frontend.zip) - > - > 修改压缩包里面的 index-xxx.js 文件 ,xx 是随机的字符串 - > - > 搜索 `https://temp-email-api.xxx.xxx` ,替换成你worker 的域名,然后部署新的zip文件 - -4. 选择 `Pages`,点击 `Create Pages`, 修改名称,上传下载的 zip 包,然后点击 `Deploy` - - ![pages1](/ui_install/pages-1.png) - -5. 打开 刚刚部署的 `Pages`,点击 `Custom Domain` 这里可以添加自己的域名,你也可以使用自动生成的 `*.pages.dev` 的域名。能打开域名说明部署成功。 - - ![pages domain](/ui_install/pages-domain.png) - - diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/ui/worker.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/ui/worker.md deleted file mode 100644 index 085fe23..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/ui/worker.md +++ /dev/null @@ -1,107 +0,0 @@ -# Cloudflare workers 后端 - -> [!warning] 注意 -> `worker.dev` 域名在中国无法访问,请自定义域名 - -1. 点击 `Compute (Workers)` -> `Workers & Pages` -> `Create` - - ![create worker](/ui_install/worker_home.png) - -2. 选择 `Worker`,点击 `Create Worker`, 修改名称然后点击 `Deploy` - - ![worker1](/ui_install/worker-1.png) - ![worker2](/ui_install/worker-2.png) - -3. 回到 `Workers & Pages`,找到刚刚创建的 worker,点击 `Settings` -> `Runtime`, 修改 `Compatibility flags`, 手动输入增加 `nodejs_compat`, 兼容日期也需要大于图片中的日期。 - - ![worker-runtime](/ui_install/worker-runtime.png) - -4. 下载 [worker.js](https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest/download/worker.js) - -5. 回到 `Overview`,找到刚刚创建的 worker,点击 `Edit Code`, 删除原来的文件,上传 `worker.js`, 点击 `Deploy` - - > [!NOTE] - > 上传需要先点击左侧菜单的 Explorer, - > 在文件列表的窗口里点击鼠标右键,在右键菜单里找到 `Upload`, - > 请参考下面的截图 - > - > 参考: [issues156](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/156#issuecomment-2079453822) - - ![worker3](/ui_install/worker-3.png) - - ![worker-upload](/ui_install/worker-upload.png) - -6. 点击 `Settings` -> `Variables and Secrets`, 如图所示添加变量 - - ![worker-var](/ui_install/worker-var.png) - - > [!NOTE] 注意 - > 更多变量的配置请查看 [worker变量说明](/zh/guide/worker-vars) - > - > 注意字符串格式的变量的最外层的引号是不需要的 - > - > 对于 `USER_ROLES` 请配置为此格式 `[{"domains":["awsl.uk","dreamhunter2333.xyz"],"role":"vip","prefix":"vip"},{"domains":["awsl.uk","dreamhunter2333.xyz"],"role":"admin","prefix":""}]` - - 建议配置的变量列表 - - | 变量名 | 类型 | 说明 | 示例 | - | -------------------------- | ----------- | ------------------------------------------ | ------------------------------------ | - | `PREFIX` | 文本 | 新建邮箱名称默认前缀,不需要前缀可不配置 | `tmp` | - | `DOMAINS` | JSON | 用于临时邮箱的所有域名, 支持多个域名 | `["awsl.uk", "dreamhunter2333.xyz"]` | - | `JWT_SECRET` | 文本/Secret | 用于生成 jwt 的密钥, jwt 用于登录以及鉴权 | `xxx` | - | `ADMIN_PASSWORDS` | JSON | admin 控制台密码, 不配置则不允许访问控制台 | `["123", "456"]` | - | `ENABLE_USER_CREATE_EMAIL` | 文本/JSON | 是否允许用户创建邮箱, 不配置则不允许 | `true` | - | `ENABLE_USER_DELETE_EMAIL` | 文本/JSON | 是否允许用户删除邮件, 不配置则不允许 | `true` | - -7. 点击 `Settings` -> `Bindings`, 点击 `Add Binding`, 名称如图,选择刚刚创建的 D1 数据库,点击 `Add Binding` - - > [!NOTE] 重要 - > 注意此处 `D1 Database` 的绑定名称必须为 `DB` - - ![worker-bindings](/ui_install/worker-bindings.png) - - ![worker-d1-1](/ui_install/worker-d1-1.png) - - ![worker-d1-2](/ui_install/worker-d1-2.png) - -8. 点击 `Settings` -> `Trggers`, 这里可以添加自己的域名,你也可以使用自动生成的 `*.workers.dev` 的域名。记录下这个域名,后面部署前端会用到。 - - > [!NOTE] - > 打开 `worker` 的 `url`,如果显示 `OK` 说明部署成功 - > - > 打开 `/health_check`,如果显示 `OK` 说明部署成功 - - ![worker3](/ui_install/worker-3.png) - -9. 如果你要启用注册用户功能,并需要发送邮件验证,则需要创建 `KV` 缓存, 不需要可跳过此步骤 - - > [!NOTE] 重要 - > 如果你要启用注册用户功能,并需要发送邮件验证,则需要创建 `KV` 缓存, 不需要可跳过此步骤 - > - > 注意此处 `KV` 的绑定名称必须为 `KV` - - 点击 `Storage & Databases` -> `KV` -> `Create Namespace`, 如图,点击 `Create Namespace` - - ![worker-kv](/ui_install/worker-kv.png) - - ![worker-kv-0](/ui_install/worker-kv-0.png) - - 然后点击 `Settings` -> `Bindings`, 点击 `Add Binding`, 名称如图,选择刚刚创建的 KV,点击 `Add Binding` - - ![worker-bindings](/ui_install/worker-bindings.png) - - ![worker-kv-1](/ui_install/worker-kv-1.png) - - ![worker-kv-2](/ui_install/worker-kv-2.png) - -10. Telegram Bot 配置 - - > [!NOTE] - > 如果不需要 Telegram Bot, 可跳过此步骤 - - 请先创建一个 Telegram Bot,然后获取 `token`,将 `token` 添加到 `Variables and Secrets` 中, 变量名称: `TELEGRAM_BOT_TOKEN` - -11. 如果你想要使用 admin 页面中的定时任务清理邮件,需要到 `Settings` -> `Trigger Events` -> `Cron Triggers` 中添加定时任务. - - > [!NOTE] - > 选择 `cron` 表达式,输入 `0 0 * * *`(此表达式表示每天午夜运行),点击 `Add` 增加。请根据您的需求调整此表达式。 diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/what-is-temp-mail.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/what-is-temp-mail.md deleted file mode 100644 index 382c3d3..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/what-is-temp-mail.md +++ /dev/null @@ -1,7 +0,0 @@ -# 临时邮箱简介 - -## 什么是临时邮箱 - -临时邮箱,也被称为一次性邮箱或临时邮件地址,是一种用于临时接收邮件的虚拟邮箱。与常规邮箱不同,临时邮箱旨在提供一种匿名且临时的邮件接收解决方案。 - -临时邮箱往往由网站或在线服务提供商提供,用户可以在需要注册或接收验证邮件时使用临时邮箱地址,而无需暴露自己的真实邮箱地址。这样做的好处是可以保护个人隐私 diff --git a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/worker-vars.md b/cloudflare_temp_email/vitepress-docs/docs/zh/guide/worker-vars.md deleted file mode 100644 index d5c61e1..0000000 --- a/cloudflare_temp_email/vitepress-docs/docs/zh/guide/worker-vars.md +++ /dev/null @@ -1,142 +0,0 @@ -# Worker 变量说明 - -> [!NOTE] 注意 -> 通过 CLI 部署时的写法请参考 `worker/wrangler.toml.template` - -## 必填变量 - -| 变量名 | 类型 | 说明 | 示例 | -| -------------------------- | ----------- | ------------------------------------------ | ------------------------------------ | -| `DOMAINS` | JSON | 用于临时邮箱的所有域名, 支持多个域名 | `["awsl.uk", "dreamhunter2333.xyz"]` | -| `JWT_SECRET` | 文本/Secret | 用于生成 jwt 的密钥, jwt 用于登录以及鉴权 | `xxx` | -| `ADMIN_PASSWORDS` | JSON | admin 控制台密码, 不配置则不允许访问控制台 | `["123", "456"]` | -| `ENABLE_USER_CREATE_EMAIL` | 文本/JSON | 是否允许用户创建邮箱, 不配置则不允许 | `true` | -| `ENABLE_USER_DELETE_EMAIL` | 文本/JSON | 是否允许用户删除邮件, 不配置则不允许 | `true` | - -## 后台相关变量 - -| 变量名 | 类型 | 说明 | 示例 | -| ------------------------------ | --------- | ------------------------------------ | ---------------- | -| `PASSWORDS` | JSON | 网站私有密码, 配置后需要密码才能访问 | `["123", "456"]` | -| `DISABLE_ADMIN_PASSWORD_CHECK` | 文本/JSON | 警告: 管理员控制台没有密码或用户检查 | `false` | - -## 邮箱相关变量 - -| 变量名 | 类型 | 说明 | 示例 | -| ------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | -| `PREFIX` | 文本 | 新建 `邮箱名称` 的默认前缀,不需要前缀可不配置 | `tmp` | -| `MIN_ADDRESS_LEN` | 数字 | `邮箱名称` 的最小长度 | `1` | -| `MAX_ADDRESS_LEN` | 数字 | `邮箱名称` 的最大长度 | `30` | -| `DISABLE_CUSTOM_ADDRESS_NAME` | 文本/JSON | 禁用自定义邮箱地址名称,如果设置为 true,则用户无法输入自定义邮箱名称,将由后台自动生成 | `true` | -| `ADDRESS_CHECK_REGEX` | 文本 | `邮箱名称` 的正则表达式, 只用于检查 | `^(?!.*admin).*` | -| `ADDRESS_REGEX` | 文本 | `邮箱名称` 替换非法符号的正则表达式, 不在其中的符号将被替换,如果不设置,默认为 `[^a-z0-9]`, 需谨慎使用, 有些符号可能导致无法收件 | `[^a-z0-9]` | -| `DEFAULT_DOMAINS` | JSON | 默认用户可用的域名(未登录或未分配角色的用户) | `["awsl.uk", "dreamhunter2333.xyz"]` | -| `CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST` | 文本/JSON | 创建新地址时是否优先使用默认域名,如果设置为 true,当未指定域名时将使用第一个域名, 主要用于 telegram bot 场景 | `false` | -| `DOMAIN_LABELS` | JSON | 对于中文域名,可以使用 DOMAIN_LABELS 显示域名的中文展示名称 | `["中文.awsl.uk", "dreamhunter2333.xyz"]` | -| `ENABLE_AUTO_REPLY` | 文本/JSON | 允许自动回复邮件 | `true` | -| `DEFAULT_SEND_BALANCE` | 文本/JSON | 默认发送邮件余额,如果不设置,将为 0 | `1` | - -## 接受邮件相关变量 - -| 变量名 | 类型 | 说明 | 示例 | -| ------------------------------- | --------- | -------------------------------------------------------------------------- | -------------------------- | -| `BLACK_LIST` | 文本 | 黑名单,用于过滤发件人,逗号分隔 | `gov.cn,edu.cn` | -| `ENABLE_CHECK_JUNK_MAIL` | 文本/JSON | 是否启用垃圾邮件检查,配合下列两个列表使用 | `false` | -| `JUNK_MAIL_CHECK_LIST` | JSON | 垃圾邮件检查配置, 任何一项 `存在` 且 `不通过` 则被判定为垃圾邮件 | `["spf", "dkim", "dmarc"]` | -| `JUNK_MAIL_FORCE_PASS_LIST` | JSON | 垃圾邮件检查配置, 任何一项 `不存在` 或者 `不通过` 则被判定为垃圾邮件 | `["spf", "dkim", "dmarc"]` | -| `FORWARD_ADDRESS_LIST` | JSON | 全局转发地址列表,如果不配置则不启用,启用后所有邮件都会转发到列表中的地址 | `["xxx@xxx.com"]` | -| `REMOVE_EXCEED_SIZE_ATTACHMENT` | 文本/JSON | 如果附件大小超过 2MB,则删除附件,邮件可能由于解析而丢失一些信息 | `true` | -| `REMOVE_ALL_ATTACHMENT` | 文本/JSON | 移除所有附件,邮件可能由于解析而丢失一些信息 | `true` | - -> [!NOTE] -> `垃圾邮件检查` 和 `移除附件功能` 需要解析邮件,免费版 CPU 有限,可能会导致大邮件解析超时 -> -> 如果你想解析邮件能力更强 -> -> 参考 [配置 worker 使用 wasm 解析邮件](/zh/guide/feature/mail_parser_wasm_worker) - -## webhook 相关变量 - -| 变量名 | 类型 | 说明 | 示例 | -| ---------------- | --------- | ------------------------------------- | ------------------ | -| `ENABLE_WEBHOOK` | 文本/JSON | 是否启用 webhook | `true` | -| `FRONTEND_URL` | 文本 | 前端地址,用于发送 webhook 的邮件 url | `https://xxxx.xxx` | - -> [!NOTE] -> webhook 功能需要解析邮件,免费版 CPU 有限,可能会导致大邮件解析超时 -> -> 如果你想解析邮件能力更强 -> -> 参考 [配置 worker 使用 wasm 解析邮件](/zh/guide/feature/mail_parser_wasm_worker) - -## 用户相关变量 - -| 变量名 | 类型 | 说明 | 示例 | -| ------------------------------------- | --------- | ------------------------------------------------------------------------ | ------- | -| `USER_DEFAULT_ROLE` | 文本 | 新用户默认角色, 仅在启用邮件验证时有效 | `vip` | -| `ADMIN_USER_ROLE` | 文本 | admin 角色配置, 如果用户角色等于 ADMIN_USER_ROLE 则可以访问 admin 控制台 | `admin` | -| `USER_ROLES` | JSON | - | 见下方 | -| `DISABLE_ANONYMOUS_USER_CREATE_EMAIL` | 文本/JSON | 禁用匿名用户创建邮箱,如果设置为 true,则用户只能在登录后创建邮箱地址 | `true` | -| `NO_LIMIT_SEND_ROLE` | 文本 | 可以无限发送邮件的角色, 多个角色使用逗号分割 `vip,admin` | `vip` | - -> [!NOTE] USER_ROLES 用户角色配置说明 -> -> - 如果 `domains` 为空将使用 `DEFAULT_DOMAINS` -> - 如果 prefix 为 null 将使用默认前缀, 如果 prefix 为空字符串将不使用前缀 -> -> 通过用户界面部署时 `USER_ROLES` 请配置为此格式 `[{"domains":["awsl.uk","dreamhunter2333.xyz"],"role":"vip","prefix":"vip"},{"domains":["awsl.uk","dreamhunter2333.xyz"],"role":"admin","prefix":""}]` -> -> CLI 部署时 `USER_ROLES` 请参考 `worker/wrangler.toml.template` 配置为此格式 `[{ domains = ["awsl.uk", "dreamhunter2333.xyz"], role = "vip", prefix = "vip" }, { domains = ["awsl.uk", "dreamhunter2333.xyz"], role = "admin", prefix = "" }]` - -## 网页相关变量 - -| 变量名 | 类型 | 说明 | 示例 | -| -------------------------- | ----------- | ------------------------------------------------ | --------------------- | -| `DEFAULT_LANG` | 文本 | Worker 错误信息默认语言, zh/en | `zh` | -| `TITLE` | 文本 | 自定义前端页面网站标题,支持 html | `Custom Title` | -| `ANNOUNCEMENT` | 文本 | 自定义前端页面公告,支持 html | `Custom Announcement` | -| `ALWAYS_SHOW_ANNOUNCEMENT` | 文本/JSON | 是否总是显示公告(即使无更改), 默认 `false` | `true` | -| `COPYRIGHT` | 文本 | 自定义前端界面页脚文本,支持 html | `Dream Hunter` | -| `ADMIN_CONTACT` | 文本 | admin 联系方式,可配置任意字符串, 不配置则不显示 | `xxx@gmail.com` | -| `DISABLE_SHOW_GITHUB` | 文本/JSON | 是否显示 GitHub 链接 | `true` | -| `CF_TURNSTILE_SITE_KEY` | 文本/Secret | Turnstile 人机验证配置 | `xxx` | -| `CF_TURNSTILE_SECRET_KEY` | 文本/Secret | Turnstile 人机验证配置 | `xxx` | - -## Telegram Bot 相关变量 - -| 变量名 | 类型 | 说明 | 示例 | -| ---------------- | ---- | ---------------------------------------------------------------------- | ---- | -| `TG_MAX_ADDRESS` | 数字 | telegram bot 最多绑定邮箱数量 | `5` | -| `TG_BOT_INFO` | 文本 | 可不配置,telegram BOT_INFO,预定义的 BOT_INFO 可以降低 webhook 的延迟 | `{}` | - -> [!NOTE] -> Telegram 功能需要解析邮件,免费版 CPU 有限,可能会导致大邮件解析超时 -> -> 如果你想解析邮件能力更强 -> -> 参考 [配置 worker 使用 wasm 解析邮件](/zh/guide/feature/mail_parser_wasm_worker) - -## 其他变量 - -| 变量名 | 类型 | 说明 | 示例 | -| ----------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -| `ENABLE_ANOTHER_WORKER` | 文本/JSON | 是否开启其他 worker 处理邮件 | `false` | -| `ANOTHER_WORKER_LIST` | JSON | - 其他 worker 处理邮件的配置,可以配置多个其他 worker
- 通过关键词筛选,调用对应绑定的 worker 的方法(默认方法名为 rpcEmail)
- keywords必填,否则 worker 将不会被触发 | 见下方 | - -> [!NOTE] -> `ANOTHER_WORKER_LIST` 的配置示例 -> -> ```toml -> #ANOTHER_WORKER_LIST =""" -> #[ -> # { -> # "binding":"AUTH_INBOX", -> # "method":"rpcEmail", -> # "keywords":[ -> # "验证码","激活码","激活链接","确认链接","验证邮箱","确认邮件","账号激活","邮件验证","账户确认","安全码","认证码","安全验证","登陆码","确认码","启用账户","激活账户","账号验证","注册确认", -> # "account","activation","verify","verification","activate","confirmation","email","code","validate","registration","login","code","expire","confirm" -> # ] -> # } -> #] -> # -> ``` diff --git a/cloudflare_temp_email/vitepress-docs/package.json b/cloudflare_temp_email/vitepress-docs/package.json deleted file mode 100644 index c1e8e9c..0000000 --- a/cloudflare_temp_email/vitepress-docs/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "temp-mail-docs", - "private": true, - "version": "1.0.5", - "type": "module", - "devDependencies": { - "@types/node": "^24.3.1", - "vitepress": "^1.6.4", - "wrangler": "^4.34.0" - }, - "scripts": { - "dev": "vitepress dev docs", - "build": "vitepress build docs", - "preview": "vitepress preview docs", - "deploy": "npm run build && wrangler pages deploy ./docs/.vitepress/dist --project-name=temp-mail-docs --branch production" - }, - "dependencies": { - "jszip": "^3.10.1" - }, - "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39" -} diff --git a/cloudflare_temp_email/worker/.editorconfig b/cloudflare_temp_email/worker/.editorconfig deleted file mode 100644 index 64ab260..0000000 --- a/cloudflare_temp_email/worker/.editorconfig +++ /dev/null @@ -1,13 +0,0 @@ -# http://editorconfig.org -root = true - -[*] -indent_style = tab -tab_width = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.yml] -indent_style = space diff --git a/cloudflare_temp_email/worker/.gitignore b/cloudflare_temp_email/worker/.gitignore deleted file mode 100644 index ae1cca8..0000000 --- a/cloudflare_temp_email/worker/.gitignore +++ /dev/null @@ -1,134 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* - -.wrangler -wrangler.toml -.dev.vars diff --git a/cloudflare_temp_email/worker/.prettierrc b/cloudflare_temp_email/worker/.prettierrc deleted file mode 100644 index 5c7b5d3..0000000 --- a/cloudflare_temp_email/worker/.prettierrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "printWidth": 140, - "singleQuote": true, - "semi": true, - "useTabs": true -} diff --git a/cloudflare_temp_email/worker/eslint.config.js b/cloudflare_temp_email/worker/eslint.config.js deleted file mode 100644 index 2ec21da..0000000 --- a/cloudflare_temp_email/worker/eslint.config.js +++ /dev/null @@ -1,19 +0,0 @@ -import globals from "globals"; -import pluginJs from "@eslint/js"; -import tseslint from "typescript-eslint"; - - -export default [ - { - languageOptions: { globals: globals.browser }, - }, - pluginJs.configs.recommended, - ...tseslint.configs.recommended, - { - rules: { - "@typescript-eslint/no-unused-vars": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/ban-ts-comment": "off", - } - } -]; diff --git a/cloudflare_temp_email/worker/package.json b/cloudflare_temp_email/worker/package.json deleted file mode 100644 index cf56535..0000000 --- a/cloudflare_temp_email/worker/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "cloudflare_temp_email", - "version": "1.0.5", - "private": true, - "type": "module", - "scripts": { - "dev": "wrangler dev", - "lint": "eslint src", - "deploy": "wrangler deploy --minify", - "start": "wrangler dev", - "build": "wrangler deploy --dry-run --outdir dist --minify" - }, - "devDependencies": { - "@cloudflare/workers-types": "^4.20250904.0", - "@eslint/js": "9.18.0", - "@simplewebauthn/types": "10.0.0", - "@types/node": "^22.18.1", - "eslint": "9.18.0", - "globals": "^15.15.0", - "typescript-eslint": "^8.42.0", - "wrangler": "^4.34.0" - }, - "dependencies": { - "@aws-sdk/client-s3": "^3.879.0", - "@aws-sdk/s3-request-presigner": "^3.879.0", - "@simplewebauthn/server": "10.0.1", - "hono": "^4.9.6", - "jsonpath-plus": "^10.3.0", - "mimetext": "^3.0.27", - "postal-mime": "^2.4.4", - "resend": "^4.8.0", - "telegraf": "4.16.3", - "worker-mailer": "^1.1.5" - }, - "pnpm": { - "patchedDependencies": { - "telegraf@4.16.3": "patches/telegraf@4.16.3.patch" - } - }, - "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39" -} diff --git a/cloudflare_temp_email/worker/patches/telegraf@4.16.3.patch b/cloudflare_temp_email/worker/patches/telegraf@4.16.3.patch deleted file mode 100644 index 59a2a10..0000000 --- a/cloudflare_temp_email/worker/patches/telegraf@4.16.3.patch +++ /dev/null @@ -1,173 +0,0 @@ -diff --git a/lib/core/network/client.js b/lib/core/network/client.js -index 25fbbbb47c7f88e83ae26f629e5ae1a0c141725c..ab82c5391596ee2ed8dbe4128f3ba57a8cc998ca 100644 ---- a/lib/core/network/client.js -+++ b/lib/core/network/client.js -@@ -1,18 +1,18 @@ - "use strict"; --var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { -+var __createBinding = (this && this.__createBinding) || (Object.create ? (function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { -- desc = { enumerable: true, get: function() { return m[k]; } }; -+ desc = { enumerable: true, get: function () { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); --}) : (function(o, m, k, k2) { -+}) : (function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; - })); --var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { -+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function (o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); --}) : function(o, v) { -+}) : function (o, v) { - o["default"] = v; - }); - var __importStar = (this && this.__importStar) || function (mod) { -@@ -29,8 +29,8 @@ Object.defineProperty(exports, "__esModule", { value: true }); - /* eslint @typescript-eslint/restrict-template-expressions: [ "error", { "allowNumber": true, "allowBoolean": true } ] */ - const crypto = __importStar(require("crypto")); - const fs = __importStar(require("fs")); --const promises_1 = require("fs/promises"); --const https = __importStar(require("https")); -+// const promises_1 = require("fs/promises"); -+// const https = __importStar(require("https")); - const path = __importStar(require("path")); - const node_fetch_1 = __importDefault(require("node-fetch")); - const check_1 = require("../helpers/check"); -@@ -39,7 +39,8 @@ const multipart_stream_1 = __importDefault(require("./multipart-stream")); - const error_1 = __importDefault(require("./error")); - const url_1 = require("url"); - // eslint-disable-next-line @typescript-eslint/no-var-requires --const debug = require('debug')('telegraf:client'); -+// const debug = require('debug')('telegraf:client'); -+const debug = (msg) => console.debug(`[telegraf:client] ${msg}`); - const { isStream } = multipart_stream_1.default; - const WEBHOOK_REPLY_METHOD_ALLOWLIST = new Set([ - 'answerCallbackQuery', -@@ -61,10 +62,10 @@ const DEFAULT_OPTIONS = { - apiRoot: 'https://api.telegram.org', - apiMode: 'bot', - webhookReply: true, -- agent: new https.Agent({ -- keepAlive: true, -- keepAliveMsecs: 10000, -- }), -+ // agent: new https.Agent({ -+ // keepAlive: true, -+ // keepAliveMsecs: 10000, -+ // }), - attachmentAgent: undefined, - testEnv: false, - }; -@@ -112,9 +113,9 @@ async function buildFormDataConfig(payload, agent) { - } - const boundary = crypto.randomBytes(32).toString('hex'); - const formData = new multipart_stream_1.default(boundary); -- await Promise.all(Object.keys(payload).map((key) => -- // @ts-expect-error payload[key] can obviously index payload, but TS doesn't trust us -- attachFormValue(formData, key, payload[key], agent))); -+ await Promise.all(Object.keys(payload).map((key) => -+ // @ts-expect-error payload[key] can obviously index payload, but TS doesn't trust us -+ attachFormValue(formData, key, payload[key], agent))); - return { - method: 'POST', - compress: true, -@@ -205,14 +206,15 @@ async function attachFormMedia(form, media, id, agent) { - if ('source' in media && media.source) { - let mediaSource = media.source; - if (typeof media.source === 'string') { -- const source = await (0, promises_1.realpath)(media.source); -- if ((await (0, promises_1.stat)(source)).isFile()) { -- fileName = (_c = media.filename) !== null && _c !== void 0 ? _c : path.basename(media.source); -- mediaSource = await fs.createReadStream(media.source); -- } -- else { -- throw new TypeError(`Unable to upload '${media.source}', not a file`); -- } -+ throw new TypeError(`Unable to upload '${media.source}', not a file`); -+ // const source = await (0, promises_1.realpath)(media.source); -+ // if ((await (0, promises_1.stat)(source)).isFile()) { -+ // fileName = (_c = media.filename) !== null && _c !== void 0 ? _c : path.basename(media.source); -+ // mediaSource = await fs.createReadStream(media.source); -+ // } -+ // else { -+ // throw new TypeError(`Unable to upload '${media.source}', not a file`); -+ // } - } - if (isStream(mediaSource) || Buffer.isBuffer(mediaSource)) { - form.addPart({ -diff --git a/lib/core/network/polling.js b/lib/core/network/polling.js -index 42f20a5090304c56d0970da56eeaaacaa518ca92..0ae889c32d46e33440c62ad6d27a290c0fe3dda2 100644 ---- a/lib/core/network/polling.js -+++ b/lib/core/network/polling.js -@@ -6,10 +6,10 @@ Object.defineProperty(exports, "__esModule", { value: true }); - exports.Polling = void 0; - const abort_controller_1 = __importDefault(require("abort-controller")); - const debug_1 = __importDefault(require("debug")); --const util_1 = require("util"); -+// const util_1 = require("util"); - const error_1 = require("./error"); - const debug = (0, debug_1.default)('telegraf:polling'); --const wait = (0, util_1.promisify)(setTimeout); -+// const wait = (0, util_1.promisify)(setTimeout); - function always(x) { - return () => x; - } -@@ -47,7 +47,8 @@ class Polling { - (err instanceof error_1.TelegramError && err.code >= 500)) { - const retryAfter = (_b = (_a = err.parameters) === null || _a === void 0 ? void 0 : _a.retry_after) !== null && _b !== void 0 ? _b : 5; - debug('Failed to fetch updates, retrying after %ds.', retryAfter, err); -- await wait(retryAfter * 1000); -+ // await wait(retryAfter * 1000); -+ await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000)); - continue; - } - if (err instanceof error_1.TelegramError && -diff --git a/lib/telegraf.js b/lib/telegraf.js -index 23d021c3d5f98493bd714a2114ec8fa853560e5c..90094d18316138b7e12eab42f722e69ccc9b6c1f 100644 ---- a/lib/telegraf.js -+++ b/lib/telegraf.js -@@ -28,8 +28,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) { - Object.defineProperty(exports, "__esModule", { value: true }); - exports.Telegraf = void 0; - const crypto = __importStar(require("crypto")); --const http = __importStar(require("http")); --const https = __importStar(require("https")); -+// const http = __importStar(require("http")); -+// const https = __importStar(require("https")); - const composer_1 = require("./composer"); - const compact_1 = require("./core/helpers/compact"); - const context_1 = __importDefault(require("./context")); -@@ -157,13 +157,13 @@ class Telegraf extends composer_1.Composer { - const callback = typeof cb === 'function' - ? (req, res) => webhookCb(req, res, () => cb(req, res)) - : webhookCb; -- this.webhookServer = -- tlsOptions != null -- ? https.createServer(tlsOptions, callback) -- : http.createServer(callback); -- this.webhookServer.listen(port, host, () => { -- debug('Webhook listening on port: %s', port); -- }); -+ // this.webhookServer = -+ // tlsOptions != null -+ // ? https.createServer(tlsOptions, callback) -+ // : http.createServer(callback); -+ // this.webhookServer.listen(port, host, () => { -+ // debug('Webhook listening on port: %s', port); -+ // }); - return this; - } - secretPathComponent() { -@@ -176,7 +176,7 @@ class Telegraf extends composer_1.Composer { - /** - * @see https://github.com/telegraf/telegraf/discussions/1344#discussioncomment-335700 - */ -- async launch(config = {}, -+ async launch(config = {}, - /** @experimental */ - onLaunch) { - var _a, _b; diff --git a/cloudflare_temp_email/worker/src/admin_api/admin_mail_api.ts b/cloudflare_temp_email/worker/src/admin_api/admin_mail_api.ts deleted file mode 100644 index f075f9f..0000000 --- a/cloudflare_temp_email/worker/src/admin_api/admin_mail_api.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Context } from "hono"; -import { handleListQuery } from "../common"; - -export default { - getMails: async (c: Context) => { - const { address, limit, offset, keyword } = c.req.query(); - const addressQuery = address ? `address = ?` : ""; - const addressParams = address ? [address] : []; - const keywordQuery = keyword ? `raw like ?` : ""; - const keywordParams = keyword ? [`%${keyword}%`] : []; - const filterQuerys = [addressQuery, keywordQuery].filter((item) => item).join(" and "); - const finalQuery = filterQuerys.length > 0 ? `where ${filterQuerys}` : ""; - const filterParams = [...addressParams, ...keywordParams] - return await handleListQuery(c, - `SELECT * FROM raw_mails ${finalQuery}`, - `SELECT count(*) as count FROM raw_mails ${finalQuery}`, - filterParams, limit, offset - ); - }, - getUnknowMails: async (c: Context) => { - const { limit, offset } = c.req.query(); - return await handleListQuery(c, - `SELECT * FROM raw_mails where address NOT IN (select name from address) `, - `SELECT count(*) as count FROM raw_mails` - + ` where address NOT IN (select name from address) `, - [], limit, offset - ); - }, - deleteMail: async (c: Context) => { - const { id } = c.req.param(); - const { success } = await c.env.DB.prepare( - `DELETE FROM raw_mails WHERE id = ? ` - ).bind(id).run(); - return c.json({ - success: success - }) - } -} diff --git a/cloudflare_temp_email/worker/src/admin_api/admin_user_api.ts b/cloudflare_temp_email/worker/src/admin_api/admin_user_api.ts deleted file mode 100644 index b1aa24b..0000000 --- a/cloudflare_temp_email/worker/src/admin_api/admin_user_api.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { Context } from 'hono'; - -import { CONSTANTS } from '../constants'; -import { getJsonSetting, saveSetting, checkUserPassword, getDomains, getUserRoles } from '../utils'; -import { UserSettings, GeoData, UserInfo } from "../models"; -import { handleListQuery } from '../common' -import UserBindAddressModule from '../user_api/bind_address'; -import i18n from '../i18n'; - -export default { - getSetting: async (c: Context) => { - const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY); - const settings = new UserSettings(value); - return c.json(settings) - }, - saveSetting: async (c: Context) => { - const value = await c.req.json(); - const settings = new UserSettings(value); - if (settings.enableMailVerify && !c.env.KV) { - return c.text("Please enable KV first if you want to enable mail verify", 403) - } - if (settings.enableMailVerify && !settings.verifyMailSender) { - return c.text("Please provide verifyMailSender", 400) - } - if (settings.enableMailVerify && settings.verifyMailSender) { - const mailDomain = settings.verifyMailSender.split("@")[1]; - const domains = getDomains(c); - if (!domains.includes(mailDomain)) { - return c.text(`VerifyMailSender(${settings.verifyMailSender}) domain must in ${JSON.stringify(domains, null, 2)}`, 400) - } - } - if (settings.maxAddressCount < 0) { - return c.text("Invalid maxAddressCount", 400) - } - await saveSetting(c, CONSTANTS.USER_SETTINGS_KEY, JSON.stringify(settings)); - return c.json({ success: true }) - }, - getUsers: async (c: Context) => { - const { limit, offset, query } = c.req.query(); - if (query) { - return await handleListQuery(c, - `SELECT u.id as id, u.user_email, u.created_at, u.updated_at,` - + ` ur.role_text as role_text,` - + ` (SELECT COUNT(*) FROM users_address WHERE user_id = u.id) AS address_count` - + ` FROM users u` - + ` LEFT JOIN user_roles ur ON u.id = ur.user_id` - + ` where u.user_email like ?`, - `SELECT count(*) as count FROM users where user_email like ?`, - [`%${query}%`], limit, offset - ); - } - return await handleListQuery(c, - `SELECT u.id as id, u.user_email, u.created_at, u.updated_at,` - + ` ur.role_text as role_text,` - + ` (SELECT COUNT(*) FROM users_address WHERE user_id = u.id) AS address_count` - + ` FROM users u` - + ` LEFT JOIN user_roles ur ON u.id = ur.user_id`, - `SELECT count(*) as count FROM users`, - [], limit, offset - ); - }, - createUser: async (c: Context) => { - const { email, password } = await c.req.json(); - if (!email || !password) { - return c.text("Invalid email or password", 400) - } - // geo data - const reqIp = c.req.raw.headers.get("cf-connecting-ip") - const geoData = new GeoData(reqIp, c.req.raw.cf as any); - const userInfo = new UserInfo(geoData, email); - try { - checkUserPassword(password); - const { success } = await c.env.DB.prepare( - `INSERT INTO users (user_email, password, user_info)` - + ` VALUES (?, ?, ?)` - ).bind( - email, password, JSON.stringify(userInfo) - ).run(); - if (!success) { - return c.text("Failed to register", 500) - } - } catch (e) { - const errorMsg = (e as Error).message; - if (errorMsg && errorMsg.includes("UNIQUE")) { - return c.text("User already exists", 400) - } - return c.text(`Failed to register: ${errorMsg}`, 500) - } - return c.json({ success: true }) - }, - deleteUser: async (c: Context) => { - const { user_id } = c.req.param(); - const msgs = i18n.getMessagesbyContext(c); - if (!user_id) return c.text(msgs.UserNotFoundMsg, 400); - const { success } = await c.env.DB.prepare( - `DELETE FROM users WHERE id = ?` - ).bind(user_id).run(); - const { success: addressSuccess } = await c.env.DB.prepare( - `DELETE FROM users_address WHERE user_id = ?` - ).bind(user_id).run(); - if (!success || !addressSuccess) { - return c.text("Failed to delete user", 500) - } - return c.json({ success: true }) - }, - resetPassword: async (c: Context) => { - const { user_id } = c.req.param(); - const { password } = await c.req.json(); - const msgs = i18n.getMessagesbyContext(c); - if (!user_id) return c.text(msgs.UserNotFoundMsg, 400); - try { - checkUserPassword(password); - const { success } = await c.env.DB.prepare( - `UPDATE users SET password = ? WHERE id = ?` - ).bind(password, user_id).run(); - if (!success) { - return c.text("Failed to reset password", 500) - } - } catch (e) { - return c.text(`Failed to reset password: ${(e as Error).message}`, 500) - } - return c.json({ success: true }); - }, - updateUserRoles: async (c: Context) => { - const { user_id, role_text } = await c.req.json(); - if (!user_id) return c.text("Invalid user_id", 400); - if (!role_text) { - const { success } = await c.env.DB.prepare( - `DELETE FROM user_roles WHERE user_id = ?` - ).bind(user_id).run(); - if (!success) { - return c.text("Failed to update user roles", 500) - } - return c.json({ success: true }) - } - const user_roles = getUserRoles(c); - if (!user_roles.find((r) => r.role === role_text)) { - return c.text("Invalid role_text", 400) - } - const { success } = await c.env.DB.prepare( - `INSERT INTO user_roles (user_id, role_text)` - + ` VALUES (?, ?)` - + ` ON CONFLICT(user_id) DO UPDATE SET role_text = ?, updated_at = datetime('now')` - ).bind(user_id, role_text, role_text).run(); - if (!success) { - return c.text("Failed to update user roles", 500) - } - return c.json({ success: true }) - }, - bindAddress: async (c: Context) => { - const { - user_email, address, user_id, address_id - } = await c.req.json(); - const db_user_id = user_id ?? await c.env.DB.prepare( - `SELECT id FROM users WHERE user_email = ?` - ).bind(user_email).first("id"); - const db_address_id = address_id ?? await c.env.DB.prepare( - `SELECT id FROM address WHERE name = ?` - ).bind(address).first("id"); - return await UserBindAddressModule.bindByID(c, db_user_id, db_address_id); - }, - getBindedAddresses: async (c: Context) => { - const { user_id } = c.req.param(); - const results = await UserBindAddressModule.getBindedAddressesById(c, user_id); - return c.json({ - results: results, - }); - }, -} diff --git a/cloudflare_temp_email/worker/src/admin_api/cleanup_api.ts b/cloudflare_temp_email/worker/src/admin_api/cleanup_api.ts deleted file mode 100644 index 5f1893b..0000000 --- a/cloudflare_temp_email/worker/src/admin_api/cleanup_api.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Context } from 'hono'; - -import { cleanup } from '../common'; -import { CONSTANTS } from '../constants'; -import { getJsonSetting, saveSetting } from '../utils'; -import { CleanupSettings } from '../models'; - -export default { - cleanup: async (c: Context) => { - const { cleanType, cleanDays } = await c.req.json(); - try { - await cleanup(c, cleanType, cleanDays); - } catch (error) { - console.error(error); - return c.text(`Failed to cleanup ${(error as Error).message}`, 500) - } - return c.json({ success: true }) - }, - getCleanup: async (c: Context) => { - const cleanupSetting = await getJsonSetting(c, CONSTANTS.AUTO_CLEANUP_KEY); - return c.json(cleanupSetting) - }, - saveCleanup: async (c: Context) => { - const cleanupSetting = await c.req.json(); - await saveSetting(c, CONSTANTS.AUTO_CLEANUP_KEY, JSON.stringify(cleanupSetting)); - return c.json({ success: true }) - } -} diff --git a/cloudflare_temp_email/worker/src/admin_api/db_api.ts b/cloudflare_temp_email/worker/src/admin_api/db_api.ts deleted file mode 100644 index 0d070ed..0000000 --- a/cloudflare_temp_email/worker/src/admin_api/db_api.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { Context } from "hono"; -import { CONSTANTS } from "../constants"; -import utils from "../utils"; - -const DB_INIT_QUERIES = ` -CREATE TABLE IF NOT EXISTS raw_mails ( - id INTEGER PRIMARY KEY, - message_id TEXT, - source TEXT, - address TEXT, - raw TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_raw_mails_address ON raw_mails(address); - -CREATE TABLE IF NOT EXISTS address ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT UNIQUE, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_address_name ON address(name); - -CREATE TABLE IF NOT EXISTS auto_reply_mails ( - id INTEGER PRIMARY KEY, - source_prefix TEXT, - name TEXT, - address TEXT UNIQUE, - subject TEXT, - message TEXT, - enabled INTEGER DEFAULT 1, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_auto_reply_mails_address ON auto_reply_mails(address); - -CREATE TABLE IF NOT EXISTS address_sender ( - id INTEGER PRIMARY KEY, - address TEXT UNIQUE, - balance INTEGER DEFAULT 0, - enabled INTEGER DEFAULT 1, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_address_sender_address ON address_sender(address); - -CREATE TABLE IF NOT EXISTS sendbox ( - id INTEGER PRIMARY KEY, - address TEXT, - raw TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_sendbox_address ON sendbox(address); - -CREATE TABLE IF NOT EXISTS settings ( - key TEXT PRIMARY KEY, - value TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY, - user_email TEXT UNIQUE NOT NULL, - password TEXT NOT NULL, - user_info TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_users_user_email ON users(user_email); - -CREATE TABLE IF NOT EXISTS users_address ( - id INTEGER PRIMARY KEY, - user_id INTEGER, - address_id INTEGER UNIQUE, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_users_address_user_id ON users_address(user_id); - -CREATE INDEX IF NOT EXISTS idx_users_address_address_id ON users_address(address_id); - -CREATE TABLE IF NOT EXISTS user_roles ( - id INTEGER PRIMARY KEY, - user_id INTEGER UNIQUE NOT NULL, - role_text TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id); - -CREATE TABLE IF NOT EXISTS user_passkeys ( - id INTEGER PRIMARY KEY, - user_id INTEGER NOT NULL, - passkey_name TEXT NOT NULL, - passkey_id TEXT NOT NULL, - passkey TEXT NOT NULL, - counter INTEGER DEFAULT 0, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_user_passkeys_user_id ON user_passkeys(user_id); - -CREATE UNIQUE INDEX IF NOT EXISTS idx_user_passkeys_user_id_passkey_id ON user_passkeys(user_id, passkey_id); -` - -export default { - initialize: async (c: Context) => { - // remove all \r and \n characters from the query string - // split by ; and join with a ;\n - const query = DB_INIT_QUERIES.replace(/[\r\n]/g, "") - .split(";") - .map((query) => query.trim()) - .join(";\n"); - await c.env.DB.exec(query); - - const version = await utils.getSetting(c, CONSTANTS.DB_VERSION_KEY); - if (version) { - return c.json({ message: "Database already initialized" }); - } - await utils.saveSetting(c, CONSTANTS.DB_VERSION_KEY, CONSTANTS.DB_VERSION); - return c.json({ message: "Database initialized" }); - }, - migrate: async (c: Context) => { - const version = await utils.getSetting(c, CONSTANTS.DB_VERSION_KEY); - if (version != CONSTANTS.DB_VERSION) { - // TODO: Perform migration logic here - - // Update the version in the settings table - await utils.saveSetting(c, CONSTANTS.DB_VERSION_KEY, CONSTANTS.DB_VERSION); - return c.json({ - success: true, - message: "Database migrated" - }); - } - return c.json({ - success: true, - message: "Database does not need migration" - }); - }, - getVersion: async (c: Context) => { - const version = await utils.getSetting(c, CONSTANTS.DB_VERSION_KEY); - return c.json({ - need_initialization: !version, - need_migration: version && version != CONSTANTS.DB_VERSION, - current_db_version: version, - code_db_version: CONSTANTS.DB_VERSION - }); - }, -} diff --git a/cloudflare_temp_email/worker/src/admin_api/index.ts b/cloudflare_temp_email/worker/src/admin_api/index.ts deleted file mode 100644 index 472e1a8..0000000 --- a/cloudflare_temp_email/worker/src/admin_api/index.ts +++ /dev/null @@ -1,347 +0,0 @@ -import { Hono } from 'hono' -import { Jwt } from 'hono/utils/jwt' - -import i18n from '../i18n' -import { sendAdminInternalMail, getJsonSetting, saveSetting, getUserRoles } from '../utils' -import { newAddress, handleListQuery } from '../common' -import { CONSTANTS } from '../constants' -import cleanup_api from './cleanup_api' -import admin_user_api from './admin_user_api' -import webhook_settings from './webhook_settings' -import mail_webhook_settings from './mail_webhook_settings' -import oauth2_settings from './oauth2_settings' -import worker_config from './worker_config' -import admin_mail_api from './admin_mail_api' -import { sendMailbyAdmin } from './send_mail' -import db_api from './db_api' -import { EmailRuleSettings } from '../models' - -export const api = new Hono() - -api.get('/admin/address', async (c) => { - const { limit, offset, query } = c.req.query(); - if (query) { - return await handleListQuery(c, - `SELECT a.*,` - + ` (SELECT COUNT(*) FROM raw_mails WHERE address = a.name) AS mail_count,` - + ` (SELECT COUNT(*) FROM sendbox WHERE address = a.name) AS send_count` - + ` FROM address a` - + ` where name like ?`, - `SELECT count(*) as count FROM address where name like ?`, - [`%${query}%`], limit, offset - ); - } - return await handleListQuery(c, - `SELECT a.*,` - + ` (SELECT COUNT(*) FROM raw_mails WHERE address = a.name) AS mail_count,` - + ` (SELECT COUNT(*) FROM sendbox WHERE address = a.name) AS send_count` - + ` FROM address a`, - `SELECT count(*) as count FROM address`, - [], limit, offset - ); -}) - -api.post('/admin/new_address', async (c) => { - const { name, domain, enablePrefix } = await c.req.json(); - const lang = c.get("lang") || c.env.DEFAULT_LANG; - const msgs = i18n.getMessages(lang); - if (!name) { - return c.text("Please provide a name", 400) - } - try { - const res = await newAddress(c, { - name, domain, enablePrefix, - checkLengthByConfig: false, - addressPrefix: null, - checkAllowDomains: false, - enableCheckNameRegex: false, - }); - return c.json(res); - } catch (e) { - return c.text(`${msgs.FailedCreateAddressMsg}: ${(e as Error).message}`, 400) - } -}) - -api.delete('/admin/delete_address/:id', async (c) => { - const { id } = c.req.param(); - const { success } = await c.env.DB.prepare( - `DELETE FROM address WHERE id = ? ` - ).bind(id).run(); - if (!success) { - return c.text("Failed to delete address", 500) - } - const { success: mailSuccess } = await c.env.DB.prepare( - `DELETE FROM raw_mails WHERE address IN` - + ` (select name from address where id = ?) ` - ).bind(id).run(); - if (!mailSuccess) { - return c.text("Failed to delete mails", 500) - } - const { success: sendAccess } = await c.env.DB.prepare( - `DELETE FROM address_sender WHERE address IN` - + ` (select name from address where id = ?) ` - ).bind(id).run(); - const { success: usersAddressSuccess } = await c.env.DB.prepare( - `DELETE FROM users_address WHERE address_id = ?` - ).bind(id).run(); - return c.json({ - success: success && mailSuccess && sendAccess && usersAddressSuccess - }) -}) - -api.delete('/admin/clear_inbox/:id', async (c) => { - const { id } = c.req.param(); - const { success: mailSuccess } = await c.env.DB.prepare( - `DELETE FROM raw_mails WHERE address IN` - + ` (select name from address where id = ?) ` - ).bind(id).run(); - if (!mailSuccess) { - return c.text("Failed to clear inbox", 500) - } - return c.json({ - success: mailSuccess - }) -}) - -api.delete('/admin/clear_sent_items/:id', async (c) => { - const { id } = c.req.param(); - const { success: sendboxSuccess } = await c.env.DB.prepare( - `DELETE FROM sendbox WHERE address IN` - + ` (select name from address where id = ?) ` - ).bind(id).run(); - if (!sendboxSuccess) { - return c.text("Failed to clear sent items", 500) - } - return c.json({ - success: sendboxSuccess - }) -}) - -api.get('/admin/show_password/:id', async (c) => { - const { id } = c.req.param(); - const name = await c.env.DB.prepare( - `SELECT name FROM address WHERE id = ? ` - ).bind(id).first("name"); - const jwt = await Jwt.sign({ - address: name, - address_id: id - }, c.env.JWT_SECRET, "HS256") - return c.json({ - jwt: jwt - }) -}) - -// mail api -api.get('/admin/mails', admin_mail_api.getMails); -api.get('/admin/mails_unknow', admin_mail_api.getUnknowMails); -api.delete('/admin/mails/:id', admin_mail_api.deleteMail) - -api.get('/admin/address_sender', async (c) => { - const { address, limit, offset } = c.req.query(); - if (address) { - return await handleListQuery(c, - `SELECT * FROM address_sender where address = ? `, - `SELECT count(*) as count FROM address_sender where address = ? `, - [address], limit, offset - ); - } - return await handleListQuery(c, - `SELECT * FROM address_sender `, - `SELECT count(*) as count FROM address_sender `, - [], limit, offset - ); -}) - -api.post('/admin/address_sender', async (c) => { - /* eslint-disable prefer-const */ - let { address, address_id, balance, enabled } = await c.req.json(); - /* eslint-enable prefer-const */ - if (!address_id) { - return c.text("Invalid address_id", 400) - } - enabled = enabled ? 1 : 0; - const { success } = await c.env.DB.prepare( - `UPDATE address_sender SET enabled = ?, balance = ? WHERE id = ? ` - ).bind(enabled, balance, address_id).run(); - if (!success) { - return c.text("Failed to update address sender", 500) - } - await sendAdminInternalMail( - c, address, "Account Send Access Updated", - `Your send access has been ${enabled ? "enabled" : "disabled"}, balance: ${balance}` - ); - return c.json({ - success: success - }) -}) - -api.delete('/admin/address_sender/:id', async (c) => { - const { id } = c.req.param(); - const { success } = await c.env.DB.prepare( - `DELETE FROM address_sender WHERE id = ? ` - ).bind(id).run(); - return c.json({ - success: success - }) -}) - -api.get('/admin/sendbox', async (c) => { - const { address, limit, offset } = c.req.query(); - if (address) { - return await handleListQuery(c, - `SELECT * FROM sendbox where address = ? `, - `SELECT count(*) as count FROM sendbox where address = ? `, - [address], limit, offset - ); - } - return await handleListQuery(c, - `SELECT * FROM sendbox `, - `SELECT count(*) as count FROM sendbox `, - [], limit, offset - ); -}) - -api.delete('/admin/sendbox/:id', async (c) => { - const { id } = c.req.param(); - const { success } = await c.env.DB.prepare( - `DELETE FROM sendbox WHERE id = ? ` - ).bind(id).run(); - return c.json({ - success: success - }) -}) - -api.get('/admin/statistics', async (c) => { - const { count: mailCount } = await c.env.DB.prepare( - `SELECT count(*) as count FROM raw_mails` - ).first<{ count: number }>() || {}; - const { count: addressCount } = await c.env.DB.prepare( - `SELECT count(*) as count FROM address` - ).first<{ count: number }>() || {}; - const { count: activeAddressCount7days } = await c.env.DB.prepare( - `SELECT count(*) as count FROM address where updated_at > datetime('now', '-7 day')` - ).first<{ count: number }>() || {}; - const { count: activeAddressCount30days } = await c.env.DB.prepare( - `SELECT count(*) as count FROM address where updated_at > datetime('now', '-30 day')` - ).first<{ count: number }>() || {}; - const { count: sendMailCount } = await c.env.DB.prepare( - `SELECT count(*) as count FROM sendbox` - ).first<{ count: number }>() || {}; - const { count: userCount } = await c.env.DB.prepare( - `SELECT count(*) as count FROM users` - ).first<{ count: number }>() || {}; - return c.json({ - mailCount: mailCount, - addressCount: addressCount, - activeAddressCount7days: activeAddressCount7days, - activeAddressCount30days: activeAddressCount30days, - userCount: userCount, - sendMailCount: sendMailCount - }) -}); - -api.get('/admin/account_settings', async (c) => { - try { - const blockList = await getJsonSetting(c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY); - const sendBlockList = await getJsonSetting(c, CONSTANTS.SEND_BLOCK_LIST_KEY); - const verifiedAddressList = await getJsonSetting(c, CONSTANTS.VERIFIED_ADDRESS_LIST_KEY); - const fromBlockList = c.env.KV ? await c.env.KV.get(CONSTANTS.EMAIL_KV_BLACK_LIST, 'json') : []; - const emailRuleSettings = await getJsonSetting(c, CONSTANTS.EMAIL_RULE_SETTINGS_KEY); - const noLimitSendAddressList = await getJsonSetting(c, CONSTANTS.NO_LIMIT_SEND_ADDRESS_LIST_KEY); - return c.json({ - blockList: blockList || [], - sendBlockList: sendBlockList || [], - verifiedAddressList: verifiedAddressList || [], - fromBlockList: fromBlockList || [], - noLimitSendAddressList: noLimitSendAddressList || [], - emailRuleSettings: emailRuleSettings || {} - }) - } catch (error) { - console.error(error); - return c.json({}) - } -}) - -api.post('/admin/account_settings', async (c) => { - /** @type {{ blockList: Array, sendBlockList: Array }} */ - const { - blockList, sendBlockList, noLimitSendAddressList, - verifiedAddressList, fromBlockList, emailRuleSettings - } = await c.req.json(); - if (!blockList || !sendBlockList || !verifiedAddressList) { - return c.text("Invalid blockList or sendBlockList", 400) - } - if (!c.env.SEND_MAIL && verifiedAddressList.length > 0) { - return c.text("Please enable SEND_MAIL to use verifiedAddressList", 400) - } - await saveSetting( - c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY, - JSON.stringify(blockList) - ); - await saveSetting( - c, CONSTANTS.SEND_BLOCK_LIST_KEY, - JSON.stringify(sendBlockList) - ); - await saveSetting( - c, CONSTANTS.VERIFIED_ADDRESS_LIST_KEY, - JSON.stringify(verifiedAddressList) - ) - if (fromBlockList?.length > 0 && !c.env.KV) { - return c.text("Please enable KV to use fromBlockList", 400) - } - if (fromBlockList) { - await c.env.KV.put(CONSTANTS.EMAIL_KV_BLACK_LIST, JSON.stringify(fromBlockList || [])) - } - await saveSetting( - c, CONSTANTS.NO_LIMIT_SEND_ADDRESS_LIST_KEY, - JSON.stringify(noLimitSendAddressList || []) - ) - await saveSetting( - c, CONSTANTS.EMAIL_RULE_SETTINGS_KEY, - JSON.stringify(emailRuleSettings || {}) - ) - return c.json({ - success: true - }) -}) - -// cleanup -api.post('/admin/cleanup', cleanup_api.cleanup) -api.get('/admin/auto_cleanup', cleanup_api.getCleanup) -api.post('/admin/auto_cleanup', cleanup_api.saveCleanup) - -// user settings -api.get('/admin/user_settings', admin_user_api.getSetting) -api.post('/admin/user_settings', admin_user_api.saveSetting) -api.get('/admin/users', admin_user_api.getUsers) -api.delete('/admin/users/:user_id', admin_user_api.deleteUser) -api.post('/admin/users', admin_user_api.createUser) -api.post('/admin/users/:user_id/reset_password', admin_user_api.resetPassword) -api.get('/admin/user_roles', async (c) => c.json(getUserRoles(c))) -api.post('/admin/user_roles', admin_user_api.updateUserRoles) -api.get('/admin/users/bind_address/:user_id', admin_user_api.getBindedAddresses) -api.post('/admin/users/bind_address', admin_user_api.bindAddress) - -// user oauth2 settings -api.get('/admin/user_oauth2_settings', oauth2_settings.getUserOauth2Settings) -api.post('/admin/user_oauth2_settings', oauth2_settings.saveUserOauth2Settings) - -// webhook settings -api.get("/admin/webhook/settings", webhook_settings.getWebhookSettings); -api.post("/admin/webhook/settings", webhook_settings.saveWebhookSettings); - -// mail webhook settings -api.get("/admin/mail_webhook/settings", mail_webhook_settings.getWebhookSettings); -api.post("/admin/mail_webhook/settings", mail_webhook_settings.saveWebhookSettings); -api.post("/admin/mail_webhook/test", mail_webhook_settings.testWebhookSettings); - -// worker config -api.get("/admin/worker/configs", worker_config.getConfig); - -// send mail by admin -api.post("/admin/send_mail", sendMailbyAdmin); - -// db api -api.get('admin/db_version', db_api.getVersion); -api.post('admin/db_initialize', db_api.initialize); -api.post('admin/db_migration', db_api.migrate); diff --git a/cloudflare_temp_email/worker/src/admin_api/mail_webhook_settings.ts b/cloudflare_temp_email/worker/src/admin_api/mail_webhook_settings.ts deleted file mode 100644 index 71b55c1..0000000 --- a/cloudflare_temp_email/worker/src/admin_api/mail_webhook_settings.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Context } from "hono"; -import { CONSTANTS } from "../constants"; -import { WebhookSettings } from "../models"; -import { commonParseMail, sendWebhook } from "../common"; - -async function getWebhookSettings(c: Context): Promise { - const settings = await c.env.KV.get( - CONSTANTS.WEBHOOK_KV_ADMIN_MAIL_SETTINGS_KEY, "json" - ) || new WebhookSettings(); - return c.json(settings); -} - -async function saveWebhookSettings(c: Context): Promise { - const settings = await c.req.json(); - await c.env.KV.put( - CONSTANTS.WEBHOOK_KV_ADMIN_MAIL_SETTINGS_KEY, - JSON.stringify(settings)); - return c.json({ success: true }) -} - -async function testWebhookSettings(c: Context): Promise { - const settings = await c.req.json(); - // random raw email - const { id: mailId, raw } = await c.env.DB.prepare( - `SELECT id, raw FROM raw_mails ORDER BY RANDOM() LIMIT 1` - ).first<{ id: string, raw: string }>() || {}; - const parsedEmailContext: ParsedEmailContext = { rawEmail: raw || "" }; - const parsedEmail = await commonParseMail(parsedEmailContext); - const res = await sendWebhook(settings, { - id: mailId || "0", - url: c.env.FRONTEND_URL ? `${c.env.FRONTEND_URL}?mail_id=${mailId}` : "", - from: parsedEmail?.sender || "test@test.com", - to: "admin@test.com", - subject: parsedEmail?.subject || "test subject", - raw: raw || "test raw email", - parsedText: parsedEmail?.text || "test parsed text", - parsedHtml: parsedEmail?.html || "test parsed html" - }); - if (!res.success) { - return c.text(res.message || "send webhook error", 400); - } - return c.json({ success: true }); -} - -export default { - getWebhookSettings, - saveWebhookSettings, - testWebhookSettings, -} diff --git a/cloudflare_temp_email/worker/src/admin_api/oauth2_settings.ts b/cloudflare_temp_email/worker/src/admin_api/oauth2_settings.ts deleted file mode 100644 index 64aa735..0000000 --- a/cloudflare_temp_email/worker/src/admin_api/oauth2_settings.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Context } from 'hono'; - -import { CONSTANTS } from '../constants'; -import { UserOauth2Settings } from "../models"; -import { getJsonSetting, saveSetting } from '../utils'; - -async function getUserOauth2Settings(c: Context): Promise { - const settings = await getJsonSetting(c, CONSTANTS.OAUTH2_SETTINGS_KEY); - return c.json(settings || []); -} - -async function saveUserOauth2Settings(c: Context): Promise { - const settings = await c.req.json(); - for (const setting of settings) { - if (!setting.name || !setting.clientID || !setting.clientSecret - || !setting.authorizationURL || !setting.accessTokenURL - || !setting.accessTokenFormat - || !setting.userInfoURL || !setting.redirectURL - || !setting.userEmailKey || !setting.scope) { - return c.text(`${setting.name} is missing required fields`, 400); - } - if (setting.enableMailAllowList && (setting.mailAllowList?.length || 0) < 1) { - return c.text(`${setting.name} is missing mail allow list`, 400); - } - } - await saveSetting(c, CONSTANTS.OAUTH2_SETTINGS_KEY, JSON.stringify(settings)); - return c.json({ success: true }) -} - -export default { - getUserOauth2Settings, - saveUserOauth2Settings, -} diff --git a/cloudflare_temp_email/worker/src/admin_api/send_mail.ts b/cloudflare_temp_email/worker/src/admin_api/send_mail.ts deleted file mode 100644 index 41fa5a8..0000000 --- a/cloudflare_temp_email/worker/src/admin_api/send_mail.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Context } from "hono"; -import { sendMail } from "../mails_api/send_mail_api"; - -export const sendMailbyAdmin = async (c: Context) => { - const { - from_name, from_mail, - to_mail, to_name, - subject, content, is_html - } = await c.req.json(); - await sendMail(c, from_mail, { - from_name: from_name, - to_name: to_name, - to_mail: to_mail, - subject: subject, - content: content, - is_html: is_html, - }, { - isAdmin: true - }) - return c.json({ status: "ok" }); -} diff --git a/cloudflare_temp_email/worker/src/admin_api/webhook_settings.ts b/cloudflare_temp_email/worker/src/admin_api/webhook_settings.ts deleted file mode 100644 index 3b415f9..0000000 --- a/cloudflare_temp_email/worker/src/admin_api/webhook_settings.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Context } from "hono"; -import { CONSTANTS } from "../constants"; -import { AdminWebhookSettings } from "../models"; - -async function getWebhookSettings(c: Context): Promise { - const settings = await c.env.KV.get(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json"); - return c.json(settings || new AdminWebhookSettings(false, [])); -} - -async function saveWebhookSettings(c: Context): Promise { - const settings = await c.req.json(); - await c.env.KV.put(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, JSON.stringify(settings)); - return c.json({ success: true }) -} - -export default { - getWebhookSettings, - saveWebhookSettings, -} diff --git a/cloudflare_temp_email/worker/src/admin_api/worker_config.ts b/cloudflare_temp_email/worker/src/admin_api/worker_config.ts deleted file mode 100644 index 7614350..0000000 --- a/cloudflare_temp_email/worker/src/admin_api/worker_config.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Context } from 'hono'; - -import utils from '../utils'; -import { CONSTANTS } from '../constants'; -import { isS3Enabled } from '../mails_api/s3_attachment'; - -export default { - getConfig: async (c: Context) => { - return c.json({ - "DEFAULT_LANG": c.env.DEFAULT_LANG, - "TITLE": c.env.TITLE, - "HAS_PASSWORD": utils.getPasswords(c).length, - "HAS_ADMIN_PASSWORDS": utils.getAdminPasswords(c).length, - "ANNOUNCEMENT": utils.getStringValue(c.env.ANNOUNCEMENT), - "ALWAYS_SHOW_ANNOUNCEMENT": utils.getBooleanValue(c.env.ALWAYS_SHOW_ANNOUNCEMENT), - - "PREFIX": utils.getStringValue(c.env.PREFIX), - "ADDRESS_CHECK_REGEX": utils.getStringValue(c.env.ADDRESS_CHECK_REGEX), - "ADDRESS_REGEX": utils.getStringValue(c.env.ADDRESS_REGEX), - "MIN_ADDRESS_LEN": utils.getIntValue(c.env.MIN_ADDRESS_LEN, 1), - "MAX_ADDRESS_LEN": utils.getIntValue(c.env.MAX_ADDRESS_LEN, 30), - - "FORWARD_ADDRESS_LIST": utils.getStringArray(c.env.FORWARD_ADDRESS_LIST), - "SUBDOMAIN_FORWARD_ADDRESS_LIST": utils.getJsonObjectValue(c.env.SUBDOMAIN_FORWARD_ADDRESS_LIST), - "DEFAULT_DOMAINS": utils.getDefaultDomains(c), - "DOMAINS": utils.getDomains(c), - "DOMAIN_LABELS": utils.getStringArray(c.env.DOMAIN_LABELS), - - "HAS_JWT_SECRET": !!utils.getStringValue(c.env.JWT_SECRET), - - "ADMIN_USER_ROLE": utils.getStringValue(c.env.ADMIN_USER_ROLE), - "USER_DEFAULT_ROLE": utils.getStringValue(c.env.USER_DEFAULT_ROLE), - "USER_ROLES": utils.getUserRoles(c), - "NO_LIMIT_SEND_ROLE": utils.getSplitStringListValue(c.env.NO_LIMIT_SEND_ROLE), - - "ADMIN_CONTACT": c.env.ADMIN_CONTACT, - "ENABLE_USER_CREATE_EMAIL": utils.getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL), - "DISABLE_ANONYMOUS_USER_CREATE_EMAIL": utils.getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL), - "ENABLE_USER_DELETE_EMAIL": utils.getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL), - "ENABLE_AUTO_REPLY": utils.getBooleanValue(c.env.ENABLE_AUTO_REPLY), - "COPYRIGHT": c.env.COPYRIGHT, - "ENABLE_WEBHOOK": utils.getBooleanValue(c.env.ENABLE_WEBHOOK), - "S3_ENABLED": isS3Enabled(c), - "VERSION": CONSTANTS.VERSION, - "DISABLE_SHOW_GITHUB": !utils.getBooleanValue(c.env.DISABLE_SHOW_GITHUB), - "DISABLE_ADMIN_PASSWORD_CHECK": utils.getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK), - "ENABLE_CHECK_JUNK_MAIL": utils.getBooleanValue(c.env.ENABLE_CHECK_JUNK_MAIL), - "JUNK_MAIL_CHECK_LIST": utils.getStringArray(c.env.JUNK_MAIL_CHECK_LIST), - "JUNK_MAIL_FORCE_PASS_LIST": utils.getStringArray(c.env.JUNK_MAIL_FORCE_PASS_LIST), - - "REMOVE_EXCEED_SIZE_ATTACHMENT": utils.getBooleanValue(c.env.REMOVE_EXCEED_SIZE_ATTACHMENT), - "REMOVE_ALL_ATTACHMENT": utils.getBooleanValue(c.env.REMOVE_ALL_ATTACHMENT), - - "ENABLE_ANOTHER_WORKER": utils.getBooleanValue(c.env.ENABLE_ANOTHER_WORKER), - "ANOTHER_WORKER_LIST": utils.getAnotherWorkerList(c), - }) - } -} diff --git a/cloudflare_temp_email/worker/src/commom_api.ts b/cloudflare_temp_email/worker/src/commom_api.ts deleted file mode 100644 index 2d6520a..0000000 --- a/cloudflare_temp_email/worker/src/commom_api.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Hono } from 'hono' - -import utils from './utils'; -import { CONSTANTS } from './constants'; -import { isS3Enabled } from './mails_api/s3_attachment'; - -const api = new Hono - -api.get('/open_api/settings', async (c) => { - // check header x-custom-auth - let needAuth = false; - const passwords = utils.getPasswords(c); - if (passwords && passwords.length > 0) { - const auth = c.req.raw.headers.get("x-custom-auth"); - needAuth = !auth || !passwords.includes(auth); - } - - return c.json({ - "title": c.env.TITLE, - "announcement": utils.getStringValue(c.env.ANNOUNCEMENT), - "alwaysShowAnnouncement": utils.getBooleanValue(c.env.ALWAYS_SHOW_ANNOUNCEMENT), - "prefix": utils.getStringValue(c.env.PREFIX), - "addressRegex": utils.getStringValue(c.env.ADDRESS_REGEX), - "minAddressLen": utils.getIntValue(c.env.MIN_ADDRESS_LEN, 1), - "maxAddressLen": utils.getIntValue(c.env.MAX_ADDRESS_LEN, 30), - "defaultDomains": utils.getDefaultDomains(c), - "domains": utils.getDomains(c), - "domainLabels": utils.getStringArray(c.env.DOMAIN_LABELS), - "needAuth": needAuth, - "adminContact": c.env.ADMIN_CONTACT, - "enableUserCreateEmail": utils.getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL), - "disableAnonymousUserCreateEmail": utils.getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL), - "disableCustomAddressName": utils.getBooleanValue(c.env.DISABLE_CUSTOM_ADDRESS_NAME), - "enableUserDeleteEmail": utils.getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL), - "enableAutoReply": utils.getBooleanValue(c.env.ENABLE_AUTO_REPLY), - "enableIndexAbout": utils.getBooleanValue(c.env.ENABLE_INDEX_ABOUT), - "copyright": c.env.COPYRIGHT, - "cfTurnstileSiteKey": c.env.CF_TURNSTILE_SITE_KEY, - "enableWebhook": utils.getBooleanValue(c.env.ENABLE_WEBHOOK), - "isS3Enabled": isS3Enabled(c), - "version": CONSTANTS.VERSION, - "showGithub": !utils.getBooleanValue(c.env.DISABLE_SHOW_GITHUB), - "disableAdminPasswordCheck": utils.getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK) - }); -}) - -export { api } diff --git a/cloudflare_temp_email/worker/src/common.ts b/cloudflare_temp_email/worker/src/common.ts deleted file mode 100644 index 75705ae..0000000 --- a/cloudflare_temp_email/worker/src/common.ts +++ /dev/null @@ -1,566 +0,0 @@ -import { Context } from 'hono'; -import { Jwt } from 'hono/utils/jwt' - -import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting, getAnotherWorkerList } from './utils'; -import { unbindTelegramByAddress } from './telegram_api/common'; -import { CONSTANTS } from './constants'; -import { AdminWebhookSettings, WebhookMail, WebhookSettings } from './models'; - -const DEFAULT_NAME_REGEX = /[^a-z0-9]/g; - -export const generateRandomName = (c: Context): string => { - // name min length min 1 - const minLength = Math.max( - getIntValue(c.env.MIN_ADDRESS_LEN, 1), - 1 - ); - // name max length min 1 - const maxLength = Math.max( - getIntValue(c.env.MAX_ADDRESS_LEN, 30), - 1 - ); - - // Build full name recursively until minimum length is reached - const buildName = (currentName: string = ""): string => { - return currentName.length >= minLength - ? currentName - : buildName(currentName + Math.random().toString(36).substring(2, 15)); - }; - - const fullName = buildName(); - - // Return truncated to max length - return fullName.substring(0, Math.min(fullName.length, maxLength)); -}; - -const checkNameRegex = (c: Context, name: string) => { - let error = null; - try { - const regexStr = getStringValue(c.env.ADDRESS_CHECK_REGEX); - if (!regexStr) return; - const regex = new RegExp(regexStr); - if (!regex.test(name)) { - error = new Error(`Name not match regex: /${regexStr}/`); - } - } - catch (e) { - console.error("Failed to check address regex", e); - } - if (error) { - throw error; - } -} - -const getNameRegex = (c: Context): RegExp => { - try { - const regex = getStringValue(c.env.ADDRESS_REGEX); - if (!regex) { - return DEFAULT_NAME_REGEX; - } - return new RegExp(regex, 'g'); - } - catch (e) { - console.error("Failed to get address regex", e); - } - return DEFAULT_NAME_REGEX; -} - -export async function updateAddressUpdatedAt( - c: Context, - address: string | undefined | null -): Promise { - if (!address) { - return; - } - // update address updated_at - try { - await c.env.DB.prepare( - `UPDATE address SET updated_at = datetime('now') where name = ?` - ).bind(address).run(); - } catch (e) { - console.warn("Failed to update address updated_at", e); - } -} - -export const newAddress = async ( - c: Context, - { - name, - domain, - enablePrefix, - checkLengthByConfig = true, - addressPrefix = null, - checkAllowDomains = true, - enableCheckNameRegex = true, - }: { - name: string, domain: string | undefined | null, - enablePrefix: boolean, - checkLengthByConfig?: boolean, - addressPrefix?: string | undefined | null, - checkAllowDomains?: boolean, - enableCheckNameRegex?: boolean, - } -): Promise<{ address: string, jwt: string }> => { - // trim whitespace and remove special characters - name = name.trim().replace(getNameRegex(c), '') - // check name - if (enableCheckNameRegex) { - await checkNameBlockList(c, name); - checkNameRegex(c, name); - } - // name min length min 1 - const minAddressLength = Math.max( - checkLengthByConfig ? getIntValue(c.env.MIN_ADDRESS_LEN, 1) : 1, - 1 - ); - // name max length min 1 - const maxAddressLength = Math.max( - checkLengthByConfig ? getIntValue(c.env.MAX_ADDRESS_LEN, 30) : 30, - 1 - ); - // check name length - if (name.length < minAddressLength) { - throw new Error(`Name too short (min ${minAddressLength})`); - } - if (name.length > maxAddressLength) { - throw new Error(`Name too long (max ${maxAddressLength})`); - } - // create address with prefix - if (typeof addressPrefix === "string") { - name = addressPrefix.trim() + name; - } else if (enablePrefix) { - name = getStringValue(c.env.PREFIX).trim() + name; - } - // check domain - const allowDomains = checkAllowDomains ? await getAllowDomains(c) : getDomains(c); - // if domain is not set, select domain based on environment configuration - if (!domain && allowDomains.length > 0) { - const createAddressDefaultDomainFirst = getBooleanValue(c.env.CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST); - if (createAddressDefaultDomainFirst) { - domain = allowDomains[0]; - } else { - domain = allowDomains[Math.floor(Math.random() * allowDomains.length)]; - } - } - // check domain is valid - if (!domain || !allowDomains.includes(domain)) { - throw new Error("Invalid domain") - } - // create address - name = name + "@" + domain; - try { - const { success } = await c.env.DB.prepare( - `INSERT INTO address(name) VALUES(?)` - ).bind(name).run(); - if (!success) { - throw new Error("Failed to create address") - } - await updateAddressUpdatedAt(c, name); - } catch (e) { - const message = (e as Error).message; - if (message && message.includes("UNIQUE")) { - throw new Error("Address already exists") - } - throw new Error("Failed to create address") - } - const address_id = await c.env.DB.prepare( - `SELECT id FROM address where name = ?` - ).bind(name).first("id"); - // create jwt - const jwt = await Jwt.sign({ - address: name, - address_id: address_id - }, c.env.JWT_SECRET, "HS256") - return { - jwt: jwt, - address: name, - } -} - -const checkNameBlockList = async ( - c: Context, name: string -): Promise => { - // check name block list - const blockList = [] as string[]; - try { - const value = await getJsonSetting(c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY); - blockList.push(...(value || [])); - } catch (error) { - console.error(error); - } - if (blockList.some((item) => name.includes(item))) { - throw new Error(`Name[${name}]is blocked`); - } -} - -export const cleanup = async ( - c: Context, - cleanType: string | undefined | null, - cleanDays: number | undefined | null -): Promise => { - if (!cleanType || typeof cleanDays !== 'number' || cleanDays < 0 || cleanDays > 1000) { - throw new Error("Invalid cleanType or cleanDays") - } - console.log(`Cleanup ${cleanType} before ${cleanDays} days`); - switch (cleanType) { - case "inactiveAddress": - await batchDeleteAddressWithData( - c, - `updated_at < datetime('now', '-${cleanDays} day')` - ) - break; - case "addressCreated": - await batchDeleteAddressWithData( - c, - `created_at < datetime('now', '-${cleanDays} day')` - ) - break; - case "mails": - await c.env.DB.prepare(` - DELETE FROM raw_mails WHERE created_at < datetime('now', '-${cleanDays} day')` - ).run(); - break; - case "mails_unknow": - await c.env.DB.prepare(` - DELETE FROM raw_mails WHERE address NOT IN - (select name from address) AND created_at < datetime('now', '-${cleanDays} day')` - ).run(); - break; - case "sendbox": - await c.env.DB.prepare(` - DELETE FROM sendbox WHERE created_at < datetime('now', '-${cleanDays} day')` - ).run(); - break; - default: - throw new Error("Invalid cleanType") - } - return true; -} - -const batchDeleteAddressWithData = async ( - c: Context, - addressQueryCondition: string, -): Promise => { - await c.env.DB.prepare( - `DELETE FROM raw_mails WHERE address IN ( ` + - `SELECT name FROM address WHERE ${addressQueryCondition})` - ).run(); - await c.env.DB.prepare( - `DELETE FROM sendbox WHERE address IN ( ` + - `SELECT name FROM address WHERE ${addressQueryCondition})` - ).run(); - await c.env.DB.prepare( - `DELETE FROM auto_reply_mails WHERE address IN ( ` + - `SELECT name FROM address WHERE ${addressQueryCondition})` - ).run(); - await c.env.DB.prepare( - `DELETE FROM address_sender WHERE address IN ( ` + - `SELECT name FROM address WHERE ${addressQueryCondition})` - ).run(); - await c.env.DB.prepare( - `DELETE FROM users_address WHERE address_id IN ( ` + - `SELECT id FROM address WHERE ${addressQueryCondition})` - ).run(); - // delete address - await c.env.DB.prepare(` - DELETE FROM address WHERE ${addressQueryCondition}` - ).run(); - return true; -} - - -export const deleteAddressWithData = async ( - c: Context, - address: string | undefined | null, - address_id: number | undefined | null -): Promise => { - if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) { - throw new Error("Delete email is disabled") - } - if (!address && !address_id) { - throw new Error("Address or address_id required") - } - // get address_id or address - if (!address_id) { - address_id = await c.env.DB.prepare( - `SELECT id FROM address where name = ?` - ).bind(address).first("id"); - } else if (!address) { - address = await c.env.DB.prepare( - `SELECT name FROM address where id = ?` - ).bind(address_id).first("name"); - } - // check address again - if (!address || !address_id) { - throw new Error("Can't find address"); - } - // unbind telegram - await unbindTelegramByAddress(c, address); - // delete address and related data - const { success: mailSuccess } = await c.env.DB.prepare( - `DELETE FROM raw_mails WHERE address = ? ` - ).bind(address).run(); - const { success: sendAccess } = await c.env.DB.prepare( - `DELETE FROM address_sender WHERE address = ? ` - ).bind(address).run(); - const { success: sendboxSuccess } = await c.env.DB.prepare( - `DELETE FROM sendbox WHERE address = ? ` - ).bind(address).run(); - const { success: addressSuccess } = await c.env.DB.prepare( - `DELETE FROM users_address WHERE address_id = ? ` - ).bind(address_id).run(); - const { success: autoReplySuccess } = await c.env.DB.prepare( - `DELETE FROM auto_reply_mails WHERE address = ? ` - ).bind(address).run(); - const { success } = await c.env.DB.prepare( - `DELETE FROM address WHERE name = ? ` - ).bind(address).run(); - if (!success || !mailSuccess || !sendboxSuccess || !addressSuccess || !sendAccess || !autoReplySuccess) { - throw new Error("Failed to delete address") - } - return true; -} - -export const handleListQuery = async ( - c: Context, - query: string, countQuery: string, params: string[], - limit: string | number | undefined | null, - offset: string | number | undefined | null -): Promise => { - if (typeof limit === "string") { - limit = parseInt(limit); - } - if (typeof offset === "string") { - offset = parseInt(offset); - } - if (!limit || limit < 0 || limit > 100) { - return c.text("Invalid limit", 400) - } - if (offset == null || offset == undefined || offset < 0) { - return c.text("Invalid offset", 400) - } - const resultsQuery = `${query} order by id desc limit ? offset ?`; - const { results } = await c.env.DB.prepare(resultsQuery).bind( - ...params, limit, offset - ).all(); - const count = offset == 0 ? await c.env.DB.prepare( - countQuery - ).bind(...params).first("count") : 0; - return c.json({ results, count }); -} - - -export const commonParseMail = async (parsedEmailContext: ParsedEmailContext): Promise<{ - sender: string, - subject: string, - text: string, - html: string, - headers?: Record[] -} | undefined> => { - // check parsed email context is valid - if (!parsedEmailContext || !parsedEmailContext.rawEmail) { - return undefined; - } - // return parsed email if already parsed - if (parsedEmailContext.parsedEmail) { - return parsedEmailContext.parsedEmail; - } - const raw_mail = parsedEmailContext.rawEmail; - // TODO: WASM parse email - // try { - // const { parse_message_wrapper } = await import('mail-parser-wasm-worker'); - - // const parsedEmail = parse_message_wrapper(raw_mail); - // parsedEmailContext.parsedEmail = { - // sender: parsedEmail.sender || "", - // subject: parsedEmail.subject || "", - // text: parsedEmail.text || "", - // headers: parsedEmail.headers?.map( - // (header) => ({ key: header.key, value: header.value }) - // ) || [], - // html: parsedEmail.body_html || "", - // }; - // return parsedEmailContext.parsedEmail; - // } catch (e) { - // console.error("Failed use mail-parser-wasm-worker to parse email", e); - // } - try { - const { default: PostalMime } = await import('postal-mime'); - const parsedEmail = await PostalMime.parse(raw_mail); - parsedEmailContext.parsedEmail = { - sender: parsedEmail.from ? `${parsedEmail.from.name} <${parsedEmail.from.address}>` : "", - subject: parsedEmail.subject || "", - text: parsedEmail.text || "", - html: parsedEmail.html || "", - headers: parsedEmail.headers || [], - }; - return parsedEmailContext.parsedEmail; - } - catch (e) { - console.error("Failed use PostalMime to parse email", e); - } - return undefined; -} - -export const commonGetUserRole = async ( - c: Context, user_id: number -): Promise => { - const user_roles = getUserRoles(c); - const role_text = await c.env.DB.prepare( - `SELECT role_text FROM user_roles where user_id = ?` - ).bind(user_id).first("role_text"); - return role_text ? user_roles.find((r) => r.role === role_text) : null; -} - -export const getAddressPrefix = async (c: Context): Promise => { - const user = c.get("userPayload"); - if (!user) { - return getStringValue(c.env.PREFIX); - } - const user_role = await commonGetUserRole(c, user.user_id); - if (typeof user_role?.prefix === "string") { - return user_role.prefix; - } - return getStringValue(c.env.PREFIX); -} - -export const getAllowDomains = async (c: Context): Promise => { - const user = c.get("userPayload"); - if (!user) { - return getDefaultDomains(c); - } - const user_role = await commonGetUserRole(c, user.user_id); - return user_role?.domains || getDefaultDomains(c);; -} - -export async function sendWebhook( - settings: WebhookSettings, formatMap: WebhookMail -): Promise<{ success: boolean, message?: string }> { - // send webhook - let body = settings.body; - for (const key of Object.keys(formatMap)) { - body = body.replace( - new RegExp(`\\$\\{${key}\\}`, "g"), - JSON.stringify( - formatMap[key as keyof WebhookMail] - ).replace(/^"(.*)"$/, '$1') - ); - } - const response = await fetch(settings.url, { - method: settings.method, - headers: JSON.parse(settings.headers), - body: body - }); - if (!response.ok) { - console.log("send webhook error", settings.url, settings.method, settings.headers, body); - console.log("send webhook error", response.status, response.statusText); - return { success: false, message: `send webhook error: ${response.status} ${response.statusText}` }; - } - return { success: true } -} - -export async function triggerWebhook( - c: Context, - address: string, - parsedEmailContext: ParsedEmailContext, - message_id: string | null -): Promise { - if (!c.env.KV || !getBooleanValue(c.env.ENABLE_WEBHOOK)) { - return - } - const webhookList: WebhookSettings[] = [] - - // admin mail webhook - const adminMailWebhookSettings = await c.env.KV.get(CONSTANTS.WEBHOOK_KV_ADMIN_MAIL_SETTINGS_KEY, "json"); - if (adminMailWebhookSettings?.enabled) { - webhookList.push(adminMailWebhookSettings) - } - - // user mail webhook - const adminSettings = await c.env.KV.get(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json"); - if (!adminSettings?.enableAllowList || adminSettings?.allowList.includes(address)) { - const settings = await c.env.KV.get( - `${CONSTANTS.WEBHOOK_KV_USER_SETTINGS_KEY}:${address}`, "json" - ); - if (settings?.enabled) { - webhookList.push(settings) - } - } - - // no webhook - if (webhookList.length === 0) { - return - } - const mailId = await c.env.DB.prepare( - `SELECT id FROM raw_mails where address = ? and message_id = ?` - ).bind(address, message_id).first("id"); - - const parsedEmail = await commonParseMail(parsedEmailContext); - const webhookMail = { - id: mailId || "", - url: c.env.FRONTEND_URL ? `${c.env.FRONTEND_URL}?mail_id=${mailId}` : "", - from: parsedEmail?.sender || "", - to: address, - subject: parsedEmail?.subject || "", - raw: parsedEmailContext.rawEmail || "", - parsedText: parsedEmail?.text || "", - parsedHtml: parsedEmail?.html || "" - } - for (const settings of webhookList) { - const res = await sendWebhook(settings, webhookMail); - if (!res.success) { - console.error(res.message); - } - } -} - -export async function triggerAnotherWorker( - c: Context, - rpcEmailMessage: RPCEmailMessage, - parsedText: string | undefined | null -): Promise { - if (!parsedText) { - return; - } - - const anotherWorkerList: AnotherWorker[] = getAnotherWorkerList(c); - if (!getBooleanValue(c.env.ENABLE_ANOTHER_WORKER) || anotherWorkerList.length === 0) { - return; - } - - const parsedTextLowercase: string = parsedText.toLowerCase(); - for (const worker of anotherWorkerList) { - - const keywords = worker?.keywords ?? []; - const bindingName = worker?.binding ?? ""; - const methodName = worker.method ?? "rpcEmail"; - - const serviceBinding = (c.env as any)[bindingName] ?? {}; - const method = serviceBinding[methodName]; - - if (!method || typeof method !== "function") { - console.log(`method = ${methodName} not found or not function`); - continue; - } - - if (!keywords.some(keyword => keyword && parsedTextLowercase.includes(keyword.toLowerCase()))) { - console.log(`worker.binding = ${bindingName} not match keywords, parsedText = ${parsedText}`); - continue; - } - try { - const bodyObj = { ...rpcEmailMessage } as any; - if (bodyObj.headers && typeof bodyObj.headers.forEach === "function") { - const headerObj: any = {} - bodyObj.headers.forEach((value: string, key: string) => { - headerObj[key] = value; - }); - bodyObj.headers = headerObj - } - const requestBody = JSON.stringify(bodyObj); - console.log(`exec worker , binding = ${bindingName} , requestBody = ${requestBody}`); - await method(requestBody); - } catch (e1) { - console.error(`execute method = ${methodName} error`, e1); - } - } -} diff --git a/cloudflare_temp_email/worker/src/constants.ts b/cloudflare_temp_email/worker/src/constants.ts deleted file mode 100644 index 778f177..0000000 --- a/cloudflare_temp_email/worker/src/constants.ts +++ /dev/null @@ -1,25 +0,0 @@ -export const CONSTANTS = { - VERSION: 'v' + '1.0.5', - - // DB Version - DB_VERSION_KEY: 'db_version', - DB_VERSION: "v0.0.1", - - // DB settings - ADDRESS_BLOCK_LIST_KEY: 'address_block_list', - SEND_BLOCK_LIST_KEY: 'send_block_list', - AUTO_CLEANUP_KEY: 'auto_cleanup', - USER_SETTINGS_KEY: 'user_settings', - OAUTH2_SETTINGS_KEY: 'oauth2_settings', - VERIFIED_ADDRESS_LIST_KEY: 'verified_address_list', - NO_LIMIT_SEND_ADDRESS_LIST_KEY: 'no_limit_send_address_list', - EMAIL_RULE_SETTINGS_KEY: 'email_rule_settings', - - // KV - TG_KV_PREFIX: "temp-mail-telegram", - TG_KV_SETTINGS_KEY: "temp-mail-telegram-settings", - WEBHOOK_KV_SETTINGS_KEY: "temp-mail-webhook-settings", - WEBHOOK_KV_USER_SETTINGS_KEY: "temp-mail-webhook-user-settings", - EMAIL_KV_BLACK_LIST: "temp-mail-email-black-list", - WEBHOOK_KV_ADMIN_MAIL_SETTINGS_KEY: "temp-mail-webhook-admin-mail-settings", -} diff --git a/cloudflare_temp_email/worker/src/email/auto_reply.ts b/cloudflare_temp_email/worker/src/email/auto_reply.ts deleted file mode 100644 index d694da5..0000000 --- a/cloudflare_temp_email/worker/src/email/auto_reply.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { createMimeMessage } from "mimetext"; -import { getBooleanValue } from "../utils"; - -export const auto_reply = async (message: ForwardableEmailMessage, env: Bindings): Promise => { - const message_id = message.headers.get("Message-ID"); - // auto reply email - if (getBooleanValue(env.ENABLE_AUTO_REPLY) && message_id) { - try { - const results = await env.DB.prepare( - `SELECT * FROM auto_reply_mails where address = ? and enabled = 1` - ).bind(message.to).first>(); - if (results && results.source_prefix && message.from.startsWith(results.source_prefix)) { - const msg = createMimeMessage(); - msg.setHeader("In-Reply-To", message_id); - msg.setSender({ - name: results.name || results.address, - addr: results.address - }); - msg.setRecipient(message.from); - msg.setSubject(results.subject || "Auto-reply"); - msg.addMessage({ - contentType: 'text/plain', - data: results.message || "This is an auto-reply message, please reconact later." - }); - const { EmailMessage } = await import('cloudflare:email'); - const replyMessage = new EmailMessage( - message.to, - message.from, - msg.asRaw() - ); - // @ts-ignore - await message.reply(replyMessage); - } - } catch (error) { - console.log("reply email error", error); - } - } -} diff --git a/cloudflare_temp_email/worker/src/email/black_list.ts b/cloudflare_temp_email/worker/src/email/black_list.ts deleted file mode 100644 index 0898b6d..0000000 --- a/cloudflare_temp_email/worker/src/email/black_list.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { CONSTANTS } from "../constants"; - -export const isBlocked = async (from: string, env: Bindings): Promise => { - if (env.BLACK_LIST && env.BLACK_LIST.split(",").some(word => from.includes(word))) { - return true; - } - if (!env.KV) { - return false; - } - const blockList = await env.KV.get(CONSTANTS.EMAIL_KV_BLACK_LIST, 'json') || []; - if (blockList.some(word => from.includes(word))) { - return true; - } - return false; -} diff --git a/cloudflare_temp_email/worker/src/email/check_attachment.ts b/cloudflare_temp_email/worker/src/email/check_attachment.ts deleted file mode 100644 index 3e59655..0000000 --- a/cloudflare_temp_email/worker/src/email/check_attachment.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { getBooleanValue } from "../utils"; -import { commonParseMail } from "../common"; -import { createMimeMessage } from "mimetext"; - -export const remove_attachment_if_need = async ( - env: Bindings, - parsedEmailContext: ParsedEmailContext, - from_address: string, - to_address: string, - size: number -): Promise => { - // if configured, remove all attachment - const removeAllAttachment = getBooleanValue(env.REMOVE_ALL_ATTACHMENT); - // if attachment size > 2MB, remove attachment - const removeExceedSizeAttachment = getBooleanValue(env.REMOVE_EXCEED_SIZE_ATTACHMENT) && size >= 2 * 1024 * 1024; - const shouldRemoveAttachment = removeAllAttachment || removeExceedSizeAttachment; - if (!shouldRemoveAttachment) return; - - const parsedEmail = await commonParseMail(parsedEmailContext); - if (!parsedEmail) return; - - const msg = createMimeMessage(); - if (parsedEmail?.headers) { - for (const header of parsedEmail.headers) { - try { - msg.setHeader(header["key"], header["value"]); - } catch (error) { - // ignore - } - } - } - msg.setSender({ - name: parsedEmail?.sender || from_address, - addr: from_address - }); - msg.setRecipient(to_address); - msg.setSubject(parsedEmail?.subject || "Failed to parse email subject"); - if (parsedEmail?.html) { - msg.addMessage({ - contentType: 'text/html', - data: parsedEmail.html - }); - } - if (parsedEmail?.text) { - msg.addMessage({ - contentType: 'text/plain', - data: parsedEmail.text - }); - } - parsedEmailContext.rawEmail = msg.asRaw(); -} diff --git a/cloudflare_temp_email/worker/src/email/check_junk.ts b/cloudflare_temp_email/worker/src/email/check_junk.ts deleted file mode 100644 index af2b59f..0000000 --- a/cloudflare_temp_email/worker/src/email/check_junk.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { getBooleanValue, getStringArray } from "../utils"; -import { commonParseMail } from "../common"; - -export const check_if_junk_mail = async ( - env: Bindings, address: string, - parsedEmailContext: ParsedEmailContext, - message_id: string | null -): Promise => { - if (!getBooleanValue(env.ENABLE_CHECK_JUNK_MAIL)) { - return false; - } - const parsedEmail = await commonParseMail(parsedEmailContext); - if (!parsedEmail?.headers) return false; - - const checkListWhenExist = getStringArray(env.JUNK_MAIL_CHECK_LIST); - const forcePassList = getStringArray(env.JUNK_MAIL_FORCE_PASS_LIST); - const passedList: string[] = []; - const existList: string[] = []; - - const headers = parsedEmail.headers; - for (const header of headers) { - if (!header["key"]) continue; - if (!header["value"]) continue; - - // check spf - if (header["key"].toLowerCase() == "received-spf") { - existList.push("spf"); - if (header["value"].toLowerCase().includes("pass")) { - passedList.push("spf"); - } - } - - // check dkim and dmarc - if (header["key"].toLowerCase() == "authentication-results") { - if (header["value"].toLowerCase().includes("dkim=")) { - existList.push("dkim"); - if (header["value"].toLowerCase().includes("dkim=pass")) { - passedList.push("dkim"); - } - } - if (header["value"].toLowerCase().includes("dmarc=")) { - existList.push("dmarc"); - if (header["value"].toLowerCase().includes("dmarc=pass")) { - passedList.push("dmarc"); - } - } - } - } - // check if all checkListWhenExist item passed when exist - if (checkListWhenExist?.some( - (checkName) => existList.includes(checkName.toLowerCase()) - && !passedList.includes(checkName.toLowerCase()) - )) { - return true; - } - - if (forcePassList?.length == 0) return false; - - // check force pass list - return forcePassList.some( - (checkName) => !passedList.includes(checkName.toLowerCase()) - ); -} diff --git a/cloudflare_temp_email/worker/src/email/index.ts b/cloudflare_temp_email/worker/src/email/index.ts deleted file mode 100644 index 523c918..0000000 --- a/cloudflare_temp_email/worker/src/email/index.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { Context } from "hono"; - -import { getEnvStringList, getJsonObjectValue, getJsonSetting } from "../utils"; -import { sendMailToTelegram } from "../telegram_api"; -import { auto_reply } from "./auto_reply"; -import { isBlocked } from "./black_list"; -import { triggerWebhook, triggerAnotherWorker, commonParseMail } from "../common"; -import { check_if_junk_mail } from "./check_junk"; -import { remove_attachment_if_need } from "./check_attachment"; -import { EmailRuleSettings } from "../models"; -import { CONSTANTS } from "../constants"; - - -async function email(message: ForwardableEmailMessage, env: Bindings, ctx: ExecutionContext) { - if (await isBlocked(message.from, env)) { - message.setReject("Reject from address"); - console.log(`Reject message from ${message.from} to ${message.to}`); - return; - } - const rawEmail = await new Response(message.raw).text(); - const parsedEmailContext: ParsedEmailContext = { - rawEmail: rawEmail - }; - - // check if junk mail - try { - const is_junk = await check_if_junk_mail(env, message.to, parsedEmailContext, message.headers.get("Message-ID")); - if (is_junk) { - message.setReject("Junk mail"); - console.log(`Junk mail from ${message.from} to ${message.to}`); - return; - } - } catch (error) { - console.error("check junk mail error", error); - } - - // check if unknown address mail - try { - const emailRuleSettings = await getJsonSetting( - { env: env } as Context, CONSTANTS.EMAIL_RULE_SETTINGS_KEY - ); - if (emailRuleSettings?.blockReceiveUnknowAddressEmail) { - const db_address_id = await env.DB.prepare( - `SELECT id FROM address where name = ? ` - ).bind(message.to).first("id"); - if (!db_address_id) { - message.setReject("Unknown address"); - console.log(`Unknown address mail from ${message.from} to ${message.to}`); - return; - } - } - } catch (error) { - console.error("check unknown address mail error", error); - } - - // remove attachment if configured or size > 2MB - try { - await remove_attachment_if_need(env, parsedEmailContext, message.from, message.to, message.rawSize); - } catch (error) { - console.error("remove attachment error", error); - } - - const message_id = message.headers.get("Message-ID"); - // save email - try { - const { success } = await env.DB.prepare( - `INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)` - ).bind( - message.from, message.to, parsedEmailContext.rawEmail, message_id - ).run(); - if (!success) { - message.setReject(`Failed save message to ${message.to}`); - console.error(`Failed save message from ${message.from} to ${message.to}`); - } - } - catch (error) { - console.error("save email error", error); - } - - // forward email - try { - const forwardAddressList = getEnvStringList(env.FORWARD_ADDRESS_LIST) - for (const forwardAddress of forwardAddressList) { - await message.forward(forwardAddress); - } - } catch (error) { - console.error("forward email error", error); - } - - // forward subdomain email - try { - // 遍历 FORWARD_ADDRESS_LIST - const subdomainForwardAddressList = getJsonObjectValue(env.SUBDOMAIN_FORWARD_ADDRESS_LIST) || []; - const emailRuleSettings = await getJsonSetting( - { env: env } as Context, CONSTANTS.EMAIL_RULE_SETTINGS_KEY - ); - // 合并两个配置, env 里的配置优先级更高 - const allSubdomainForwardAddressList = [ - ...(subdomainForwardAddressList || []), - ...(emailRuleSettings?.emailForwardingList || []), - ]; - for (const subdomainForwardAddress of allSubdomainForwardAddressList) { - // 检查邮件是否匹配 domains - if (subdomainForwardAddress.domains && subdomainForwardAddress.domains.length > 0) { - for (const domain of subdomainForwardAddress.domains) { - if (message.to.endsWith(domain) && subdomainForwardAddress.forward) { - // 转发邮件 - await message.forward(subdomainForwardAddress.forward); - // 支持多邮箱转发收件,不进行截止 - // break; - } - } - } else { - // 如果 domains 为空,则转发所有邮件 - await message.forward(subdomainForwardAddress.forward); - } - } - } catch (error) { - console.error("subdomain forward email error", error); - } - - // send email to telegram - try { - await sendMailToTelegram( - { env: env } as Context, - message.to, parsedEmailContext, message_id); - } catch (error) { - console.error("send mail to telegram error", error); - } - - // send webhook - try { - await triggerWebhook( - { env: env } as Context, - message.to, parsedEmailContext, message_id - ); - } catch (error) { - console.error("send webhook error", error); - } - - // trigger another worker - try { - const parsedEmail = (await commonParseMail(parsedEmailContext)); - const parsedText = parsedEmail?.text ?? "" - const rpcEmail: RPCEmailMessage = { - from: message.from, - to: message.to, - rawEmail: rawEmail, - headers: message.headers - } - await triggerAnotherWorker({ env: env } as Context, rpcEmail, parsedText); - } catch (error) { - console.error("trigger another worker error", error); - } - - // auto reply email - await auto_reply(message, env); -} - -export { email } diff --git a/cloudflare_temp_email/worker/src/i18n/en.ts b/cloudflare_temp_email/worker/src/i18n/en.ts deleted file mode 100644 index 57e23f5..0000000 --- a/cloudflare_temp_email/worker/src/i18n/en.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { LocaleMessages } from "./type"; - -const messages: LocaleMessages = { - CustomAuthPasswordMsg: "You have enabled the private site password, please provide the password", - UserTokenExpiredMsg: "Your token has expired, please login again", - UserAcceesTokenExpiredMsg: "Your access token has expired, please refresh the page", - UserRoleIsNotAdminMsg: "Your user role is not admin, no access to visit this page", - NeedAdminPasswordMsg: "You need to provide the admin password to access this page", - - KVNotAvailableMsg: "KV is not available, please contact the administrator", - DBNotAvailableMsg: "DB is not available, please contact the administrator", - JWTSecretNotSetMsg: "JWT_SECRET is not set, please contact the administrator", - WebhookNotEnabledMsg: "Webhook is not enabled, please contact the administrator", - DomainsNotSetMsg: "Domains are not set, please contact the administrator", - - TurnstileCheckFailedMsg: "Human verification check failed", - NewAddressDisabledMsg: "New address is disabled, please contact the administrator", - NewAddressAnonymousDisabledMsg: "New address for anonymous user is disabled, please contact the administrator", - FailedCreateAddressMsg: "Failed to create address", - InvalidAddressMsg: "Invalid address", - InvalidAddressCredentialMsg: "Invalid address credential", - UserDeleteEmailDisabledMsg: "User delete address/email is disabled, please contact the administrator", - - UserNotFoundMsg: "User not found", - UserAlreadyExistsMsg: "User already exists, please login", - FailedToRegisterMsg: "Failed to register", - UserRegistrationDisabledMsg: "User registration is disabled, please contact the administrator", - UserMailDomainMustInMsg: "User mail domain must be in this list", - InvalidVerifyCodeMsg: "Invalid verify code", - InvalidEmailOrPasswordMsg: "Invalid email or password", - VerifyMailSenderNotSetMsg: "Verify mail sender address is not set, please contact the administrator", - CodeAlreadySentMsg: "Code already sent, please wait", - InvalidUserDefaultRoleMsg: "Invalid user default role, please contact the administrator", - FailedUpdateUserDefaultRoleMsg: "Failed to update user default role, please contact the administrator", - - Oauth2ClientIDNotFoundMsg: "Oauth2 client ID is not set, please contact the administrator", - Oauth2CliendIDOrCodeMissingMsg: "Oauth2 client ID or code is missing", - Oauth2FailedGetUserInfoMsg: "Failed to get user info from Oauth2 provider", - Oauth2FailedGetAccessTokenMsg: "Failed to get access token from Oauth2 provider", - Oauth2FailedGetUserEmailMsg: "Failed to get user email from Oauth2 provider", -} - -export default messages; diff --git a/cloudflare_temp_email/worker/src/i18n/index.ts b/cloudflare_temp_email/worker/src/i18n/index.ts deleted file mode 100644 index b4b39f1..0000000 --- a/cloudflare_temp_email/worker/src/i18n/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { LocaleMessages } from "./type"; -import zh from "./zh"; -import en from "./en"; -import { Context } from "hono"; - -export default { - getMessages: ( - locale: string | null | undefined - ): LocaleMessages => { - // multi-language support - if (locale === "en") return en; - if (locale === "zh") return zh; - - // fallback language - return en; - }, - getMessagesbyContext: ( - c: Context - ): LocaleMessages => { - const locale = c.get("lang") || c.env.DEFAULT_LANG; - // multi-language support - if (locale === "en") return en; - if (locale === "zh") return zh; - - // fallback language - return en; - } -} diff --git a/cloudflare_temp_email/worker/src/i18n/type.ts b/cloudflare_temp_email/worker/src/i18n/type.ts deleted file mode 100644 index 2f31e59..0000000 --- a/cloudflare_temp_email/worker/src/i18n/type.ts +++ /dev/null @@ -1,39 +0,0 @@ -export type LocaleMessages = { - CustomAuthPasswordMsg: string - UserTokenExpiredMsg: string - UserAcceesTokenExpiredMsg: string - UserRoleIsNotAdminMsg: string - NeedAdminPasswordMsg: string - - KVNotAvailableMsg: string - DBNotAvailableMsg: string - JWTSecretNotSetMsg: string - WebhookNotEnabledMsg: string - DomainsNotSetMsg: string - - TurnstileCheckFailedMsg: string - NewAddressDisabledMsg: string - NewAddressAnonymousDisabledMsg: string - FailedCreateAddressMsg: string - InvalidAddressMsg: string - InvalidAddressCredentialMsg: string - UserDeleteEmailDisabledMsg: string - - UserNotFoundMsg: string - UserAlreadyExistsMsg: string - FailedToRegisterMsg: string - UserRegistrationDisabledMsg: string - UserMailDomainMustInMsg: string - InvalidVerifyCodeMsg: string - InvalidEmailOrPasswordMsg: string - VerifyMailSenderNotSetMsg: string - CodeAlreadySentMsg: string - InvalidUserDefaultRoleMsg: string - FailedUpdateUserDefaultRoleMsg: string - - Oauth2ClientIDNotFoundMsg: string - Oauth2CliendIDOrCodeMissingMsg: string - Oauth2FailedGetUserInfoMsg: string - Oauth2FailedGetAccessTokenMsg: string - Oauth2FailedGetUserEmailMsg: string -} diff --git a/cloudflare_temp_email/worker/src/i18n/zh.ts b/cloudflare_temp_email/worker/src/i18n/zh.ts deleted file mode 100644 index b55d0cd..0000000 --- a/cloudflare_temp_email/worker/src/i18n/zh.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { LocaleMessages } from "./type"; - -const messages: LocaleMessages = { - CustomAuthPasswordMsg: "你已启用私有站点密码,请提供密码", - UserTokenExpiredMsg: "您的令牌已过期, 请重新登录", - UserAcceesTokenExpiredMsg: "您的访问令牌已过期, 请刷新页面", - UserRoleIsNotAdminMsg: "您的用户角色不是管理员, 无权访问", - NeedAdminPasswordMsg: "您需要提供管理员密码才能访问此页面", - - KVNotAvailableMsg: "KV 不可用, 请联系管理员", - DBNotAvailableMsg: "DB 不可用, 请联系管理员", - JWTSecretNotSetMsg: "JWT_SECRET 未设置, 请联系管理员", - WebhookNotEnabledMsg: "Webhook 未启用, 请联系管理员", - DomainsNotSetMsg: "域名列表未设置, 请联系管理员", - - TurnstileCheckFailedMsg: "人机验证检查失败", - NewAddressDisabledMsg: "新建邮箱地址已禁用, 请联系管理员", - NewAddressAnonymousDisabledMsg: "匿名用户新建邮箱地址已禁用, 请联系管理员", - FailedCreateAddressMsg: "创建邮箱地址失败", - InvalidAddressMsg: "无效的邮箱地址", - InvalidAddressCredentialMsg: "无效的邮箱地址凭据", - UserDeleteEmailDisabledMsg: "用户删除邮箱/邮件已禁用, 请联系管理员", - - UserNotFoundMsg: "用户不存在", - UserAlreadyExistsMsg: "用户已存在, 请登录", - FailedToRegisterMsg: "注册失败", - UserRegistrationDisabledMsg: "用户注册已禁用, 请联系管理员", - UserMailDomainMustInMsg: "用户邮箱域必须在此列表中", - InvalidVerifyCodeMsg: "无效的验证码", - InvalidEmailOrPasswordMsg: "无效的邮箱或密码", - VerifyMailSenderNotSetMsg: "验证邮件发送邮箱未设置, 请联系管理员", - CodeAlreadySentMsg: "验证码已发送, 请稍等", - InvalidUserDefaultRoleMsg: "无效的用户默认角色, 请联系管理员", - FailedUpdateUserDefaultRoleMsg: "更新用户默认角色失败, 请联系管理员", - - Oauth2ClientIDNotFoundMsg: "Oauth2 客户端 ID 未设置, 请联系管理员", - Oauth2CliendIDOrCodeMissingMsg: "Oauth2 客户端 ID 或 code 缺失", - Oauth2FailedGetUserInfoMsg: "从 Oauth2 提供商获取用户信息失败", - Oauth2FailedGetAccessTokenMsg: "从 Oauth2 提供商获取访问令牌失败", - Oauth2FailedGetUserEmailMsg: "从 Oauth2 提供商获取用户邮箱失败", -} - -export default messages; diff --git a/cloudflare_temp_email/worker/src/mails_api/auto_reply.ts b/cloudflare_temp_email/worker/src/mails_api/auto_reply.ts deleted file mode 100644 index 0b461b5..0000000 --- a/cloudflare_temp_email/worker/src/mails_api/auto_reply.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Context } from "hono"; -import { getBooleanValue } from "../utils"; - - -export default { - getAutoReply: async (c: Context) => { - if (!getBooleanValue(c.env.ENABLE_AUTO_REPLY)) { - return c.text("Auto reply is disabled", 403) - } - const { address } = c.get("jwtPayload") - const results = await c.env.DB.prepare( - `SELECT * FROM auto_reply_mails where address = ? ` - ).bind(address).first(); - if (!results) { - return c.json({}); - } - return c.json({ - subject: results.subject, - message: results.message, - enabled: results.enabled == 1, - source_prefix: results.source_prefix, - name: results.name, - }) - }, - saveAutoReply: async (c: Context) => { - if (!getBooleanValue(c.env.ENABLE_AUTO_REPLY)) { - return c.text("Auto reply is disabled", 403) - } - const { address } = c.get("jwtPayload"); - const { auto_reply } = await c.req.json(); - const { name, subject, source_prefix, message, enabled } = auto_reply; - if ((!subject || !message) && enabled) { - return c.text("Invalid subject or message", 400) - } - else if (subject.length > 255 || message.length > 255) { - return c.text("Subject or message too long", 400) - } - const { success } = await c.env.DB.prepare( - `INSERT OR REPLACE INTO auto_reply_mails` - + ` (name, address, source_prefix, subject, message, enabled)` - + ` VALUES (?, ?, ?, ?, ?, ?)` - ).bind( - name || '', address, source_prefix || '', - subject || '', message || '', enabled ? 1 : 0 - ).run(); - if (!success) { - return c.text("Failed to auto_reply settings", 500) - } - return c.json({ - success: success - }) - } -} diff --git a/cloudflare_temp_email/worker/src/mails_api/index.ts b/cloudflare_temp_email/worker/src/mails_api/index.ts deleted file mode 100644 index 3a02998..0000000 --- a/cloudflare_temp_email/worker/src/mails_api/index.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { Context, Hono } from 'hono' - -import i18n from '../i18n'; -import { getBooleanValue, getJsonSetting, checkCfTurnstile, getStringValue, getSplitStringListValue } from '../utils'; -import { newAddress, handleListQuery, deleteAddressWithData, getAddressPrefix, getAllowDomains, updateAddressUpdatedAt, generateRandomName } from '../common' -import { CONSTANTS } from '../constants' -import auto_reply from './auto_reply' -import webhook_settings from './webhook_settings'; -import s3_attachment from './s3_attachment'; - -export const api = new Hono() - -api.get('/api/auto_reply', auto_reply.getAutoReply) -api.post('/api/auto_reply', auto_reply.saveAutoReply) -api.get('/api/webhook/settings', webhook_settings.getWebhookSettings) -api.post('/api/webhook/settings', webhook_settings.saveWebhookSettings) -api.post('/api/webhook/test', webhook_settings.testWebhookSettings) -api.get('/api/attachment/list', s3_attachment.list) -api.post('/api/attachment/delete', s3_attachment.deleteKey) -api.post('/api/attachment/put_url', s3_attachment.getSignedPutUrl) -api.post('/api/attachment/get_url', s3_attachment.getSignedGetUrl) - -api.get('/api/mails', async (c) => { - const { address } = c.get("jwtPayload") - if (!address) { - return c.json({ "error": "No address" }, 400) - } - const { limit, offset } = c.req.query(); - if (Number.parseInt(offset) <= 0) await updateAddressUpdatedAt(c, address); - return await handleListQuery(c, - `SELECT * FROM raw_mails where address = ?`, - `SELECT count(*) as count FROM raw_mails where address = ?`, - [address], limit, offset - ); -}) - -api.get('/api/mail/:mail_id', async (c) => { - const { address } = c.get("jwtPayload") - const { mail_id } = c.req.param(); - const result = await c.env.DB.prepare( - `SELECT * FROM raw_mails where id = ? and address = ?` - ).bind(mail_id, address).first(); - return c.json(result); -}) - -api.delete('/api/mails/:id', async (c) => { - const lang = c.get("lang") || c.env.DEFAULT_LANG; - const msgs = i18n.getMessages(lang); - if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) { - return c.text(msgs.UserDeleteEmailDisabledMsg, 403) - } - const { address } = c.get("jwtPayload") - const { id } = c.req.param(); - // TODO: add toLowerCase() to handle old data - const { success } = await c.env.DB.prepare( - `DELETE FROM raw_mails WHERE address = ? and id = ? ` - ).bind(address.toLowerCase(), id).run(); - return c.json({ - success: success - }) -}) - -api.get('/api/settings', async (c) => { - const { address, address_id } = c.get("jwtPayload") - const user_role = c.get("userRolePayload") - const lang = c.get("lang") || c.env.DEFAULT_LANG; - const msgs = i18n.getMessages(lang); - if (address_id && address_id > 0) { - try { - const db_address_id = await c.env.DB.prepare( - `SELECT id FROM address where id = ? ` - ).bind(address_id).first("id"); - if (!db_address_id) { - return c.text(msgs.InvalidAddressMsg, 400) - } - } catch (error) { - return c.text(msgs.InvalidAddressMsg, 400) - } - } - // check address id - try { - if (!address_id) { - const db_address_id = await c.env.DB.prepare( - `SELECT id FROM address where name = ? ` - ).bind(address).first("id"); - if (!db_address_id) { - return c.text(msgs.InvalidAddressMsg, 400) - } - } - } catch (error) { - return c.text(msgs.InvalidAddressMsg, 400) - } - - await updateAddressUpdatedAt(c, address); - - const no_limit_roles = getSplitStringListValue(c.env.NO_LIMIT_SEND_ROLE); - const is_no_limit_send_balance = user_role && no_limit_roles.includes(user_role); - const balance = is_no_limit_send_balance ? 99999 : await c.env.DB.prepare( - `SELECT balance FROM address_sender where address = ? and enabled = 1` - ).bind(address).first("balance"); - return c.json({ - address: address, - send_balance: balance || 0, - }); -}) - -api.post('/api/new_address', async (c) => { - const lang = c.get("lang") || c.env.DEFAULT_LANG; - const msgs = i18n.getMessages(lang); - if (getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL) - && !c.get("userPayload") - ) { - return c.text(msgs.NewAddressAnonymousDisabledMsg, 403) - } - if (!getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL)) { - return c.text(msgs.NewAddressDisabledMsg, 403) - } - // eslint-disable-next-line prefer-const - let { name, domain, cf_token } = await c.req.json(); - // check cf turnstile - try { - await checkCfTurnstile(c, cf_token); - } catch (error) { - return c.text(msgs.TurnstileCheckFailedMsg, 500) - } - // Check if custom email names are disabled from environment variable - const disableCustomAddressName = getBooleanValue(c.env.DISABLE_CUSTOM_ADDRESS_NAME); - - // if no name or custom names are disabled, generate random name - if (!name || disableCustomAddressName) { - // Generate random name with context-based length configuration - name = generateRandomName(c); - } - // check name block list - try { - const value = await getJsonSetting(c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY); - const blockList = (value || []) as string[]; - if (blockList.some((item) => name.includes(item))) { - return c.text(`Name[${name}]is blocked`, 400) - } - } catch (error) { - console.error(error); - } - try { - const addressPrefix = await getAddressPrefix(c); - const res = await newAddress(c, { - name, domain, - enablePrefix: true, - checkLengthByConfig: true, - addressPrefix - }); - return c.json(res); - } catch (e) { - return c.text(`${msgs.FailedCreateAddressMsg}: ${(e as Error).message}`, 400) - } -}) - -api.delete('/api/delete_address', async (c) => { - const { address, address_id } = c.get("jwtPayload") - const success = await deleteAddressWithData(c, address, address_id); - return c.json({ - success: success - }) -}) - -api.delete('/api/clear_inbox', async (c) => { - const lang = c.get("lang") || c.env.DEFAULT_LANG; - const msgs = i18n.getMessages(lang); - if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) { - return c.text(msgs.UserDeleteEmailDisabledMsg, 403) - } - const { address } = c.get("jwtPayload") - const { success } = await c.env.DB.prepare( - `DELETE FROM raw_mails WHERE address = ?` - ).bind(address).run(); - if (!success) { - return c.text("Failed to clear inbox", 500) - } - return c.json({ - success: success - }) -}) - -api.delete('/api/clear_sent_items', async (c) => { - const lang = c.get("lang") || c.env.DEFAULT_LANG; - const msgs = i18n.getMessages(lang); - if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) { - return c.text(msgs.UserDeleteEmailDisabledMsg, 403) - } - const { address } = c.get("jwtPayload") - const { success } = await c.env.DB.prepare( - `DELETE FROM sendbox WHERE address = ?` - ).bind(address).run(); - if (!success) { - return c.text("Failed to clear sent items", 500) - } - return c.json({ - success: success - }) -}) diff --git a/cloudflare_temp_email/worker/src/mails_api/s3_attachment.ts b/cloudflare_temp_email/worker/src/mails_api/s3_attachment.ts deleted file mode 100644 index 2c16c42..0000000 --- a/cloudflare_temp_email/worker/src/mails_api/s3_attachment.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Context } from "hono"; -import { - S3Client, - ListObjectsV2Command, - GetObjectCommand, - PutObjectCommand, - DeleteObjectCommand -} from "@aws-sdk/client-s3"; -import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; - -export const isS3Enabled = (c: Context) => { - return !(!c.env.S3_ENDPOINT || - !c.env.S3_ACCESS_KEY_ID || - !c.env.S3_SECRET_ACCESS_KEY || - !c.env.S3_BUCKET); -} - -const getS3Client = (c: Context) => { - if ( - !c.env.S3_ENDPOINT || - !c.env.S3_ACCESS_KEY_ID || - !c.env.S3_SECRET_ACCESS_KEY || - !c.env.S3_BUCKET - ) { - throw new Error("S3 config is not set"); - } - return new S3Client({ - region: "auto", - endpoint: c.env.S3_ENDPOINT, - credentials: { - accessKeyId: c.env.S3_ACCESS_KEY_ID, - secretAccessKey: c.env.S3_SECRET_ACCESS_KEY, - }, - }); -} - -export default { - getSignedGetUrl: async (c: Context) => { - const { address } = c.get("jwtPayload") - const { key } = await c.req.json() - const client = getS3Client(c); - const url = await getSignedUrl( - client, - new GetObjectCommand({ - Bucket: c.env.S3_BUCKET, - Key: `${address}/${key}` - }), - { expiresIn: c.env.S3_URL_EXPIRES || 360 } - ); - return c.json({ url }); - }, - getSignedPutUrl: async (c: Context) => { - const { address } = c.get("jwtPayload") - const { key } = await c.req.json() - const client = getS3Client(c); - const url = await getSignedUrl( - client, - new PutObjectCommand({ - Bucket: c.env.S3_BUCKET, - Key: `${address}/${key}` - }), - { expiresIn: c.env.S3_URL_EXPIRES || 360 } - ); - return c.json({ url }); - }, - list: async (c: Context) => { - const { address } = c.get("jwtPayload") - const client = getS3Client(c); - const data = await client.send( - new ListObjectsV2Command({ - Bucket: c.env.S3_BUCKET, - Prefix: `${address}/` - }) - ); - return c.json( - { - results: data?.Contents - ?.map((v) => v.Key?.replace(`${address}/`, "")) - ?.filter(k => k) - ?.map((k) => ({ key: k })) - } - ); - }, - deleteKey: async (c: Context) => { - const { address } = c.get("jwtPayload") - const { key } = await c.req.json() - const client = getS3Client(c); - await client.send( - new DeleteObjectCommand({ - Bucket: c.env.S3_BUCKET, - Key: `${address}/${key}` - }) - ); - return c.json({ success: true }); - } -} diff --git a/cloudflare_temp_email/worker/src/mails_api/send_mail_api.ts b/cloudflare_temp_email/worker/src/mails_api/send_mail_api.ts deleted file mode 100644 index 18d5156..0000000 --- a/cloudflare_temp_email/worker/src/mails_api/send_mail_api.ts +++ /dev/null @@ -1,305 +0,0 @@ -import { Context, Hono } from 'hono' -import { Jwt } from 'hono/utils/jwt' -import { createMimeMessage } from 'mimetext'; -import { Resend } from 'resend'; -import { WorkerMailer, WorkerMailerOptions } from 'worker-mailer'; - -import i18n from '../i18n'; -import { CONSTANTS } from '../constants' -import { getJsonSetting, getDomains, getIntValue, getBooleanValue, getStringValue, getJsonObjectValue, getSplitStringListValue } from '../utils'; -import { GeoData } from '../models' -import { handleListQuery } from '../common' - - -export const api = new Hono() - -api.post('/api/requset_send_mail_access', async (c) => { - const { address } = c.get("jwtPayload") - if (!address) { - return c.text("No address", 400) - } - try { - const default_balance = getIntValue(c.env.DEFAULT_SEND_BALANCE, 0); - const { success } = await c.env.DB.prepare( - `INSERT INTO address_sender (address, balance, enabled) VALUES (?, ?, ?)` - ).bind( - address, default_balance, default_balance > 0 ? 1 : 0 - ).run(); - if (!success) { - return c.text("Failed to request send mail access", 500) - } - } catch (e) { - const message = (e as Error).message; - if (message && message.includes("UNIQUE")) { - return c.text("Already requested", 400) - } - return c.text("Failed to request send mail access", 500) - } - return c.json({ status: "ok" }) -}) - -export const sendMailToVerifyAddress = async ( - c: Context, address: string, - reqJson: { - from_name: string, to_mail: string, to_name: string, - subject: string, content: string, is_html: boolean - } -): Promise => { - const { - from_name, to_mail, to_name, - subject, content, is_html - } = reqJson; - const msg = createMimeMessage(); - msg.setSender(from_name ? { name: from_name, addr: address } : address); - msg.setRecipient(to_name ? { name: to_name, addr: to_mail } : to_mail); - msg.setSubject(subject); - msg.addMessage({ - contentType: is_html ? 'text/html' : 'text/plain', - data: content - }); - const { EmailMessage } = await import('cloudflare:email'); - const message = new EmailMessage(address, to_mail, msg.asRaw()); - await c.env.SEND_MAIL.send(message); -} - -const sendMailByResend = async ( - c: Context, address: string, - reqJson: { - from_name: string, to_mail: string, to_name: string, - subject: string, content: string, is_html: boolean - } -): Promise => { - const mailDomain = address.split("@")[1]; - const token = c.env[ - `RESEND_TOKEN_${mailDomain.replace(/\./g, "_").toUpperCase()}` - ] || c.env.RESEND_TOKEN; - const resend = new Resend(token); - const { data, error } = await resend.emails.send({ - from: reqJson.from_name ? `${reqJson.from_name} <${address}>` : address, - to: reqJson.to_name ? `${reqJson.to_name} <${reqJson.to_mail}>` : reqJson.to_mail, - subject: reqJson.subject, - ...(reqJson.is_html ? { - html: reqJson.content, - } : { - text: reqJson.content, - }) - }); - if (error) { - throw new Error(`Resend error: ${error.name} ${error.message}`); - } - console.log(`Resend success: ${JSON.stringify(data)}`); -} - -const sendMailBySmtp = async ( - c: Context, address: string, - reqJson: { - from_name: string, to_mail: string, to_name: string, - subject: string, content: string, is_html: boolean - }, - smtpOptions: WorkerMailerOptions -): Promise => { - await WorkerMailer.send( - smtpOptions, - { - from: { - name: reqJson.from_name, - email: address - }, - to: { - name: reqJson.to_name, - email: reqJson.to_mail - }, - subject: reqJson.subject, - text: reqJson.is_html ? undefined : reqJson.content, - html: reqJson.is_html ? reqJson.content : undefined - } - ) -} - -export const sendMail = async ( - c: Context, address: string, - reqJson: { - from_name: string, to_mail: string, to_name: string, - subject: string, content: string, is_html: boolean - }, - options?: { - isAdmin?: boolean - } -): Promise => { - if (!address) { - throw new Error("No address") - } - // check domain - const mailDomain = address.split("@")[1]; - const domains = getDomains(c); - if (!domains.includes(mailDomain)) { - throw new Error("Invalid domain") - } - const user_role = c.get("userRolePayload"); - const no_limit_roles = getSplitStringListValue(c.env.NO_LIMIT_SEND_ROLE); - const is_no_limit_send_balance = user_role && no_limit_roles.includes(user_role); - // no need find noLimitSendAddressList if is_no_limit_send_balance - const noLimitSendAddressList = is_no_limit_send_balance ? - [] : await getJsonSetting(c, CONSTANTS.NO_LIMIT_SEND_ADDRESS_LIST_KEY) || []; - const isNoLimitSendAddress = noLimitSendAddressList?.includes(address); - const needCheckBalance = !is_no_limit_send_balance && !options?.isAdmin && !isNoLimitSendAddress; - if (needCheckBalance) { - // check permission - const balance = await c.env.DB.prepare( - `SELECT balance FROM address_sender - where address = ? and enabled = 1` - ).bind(address).first("balance"); - if (!balance || balance <= 0) { - throw new Error("No balance") - } - } - const { - from_name, to_mail, to_name, - subject, content, is_html - } = reqJson; - if (!to_mail) { - throw new Error("Invalid to mail") - } - // check SEND_BLOCK_LIST_KEY - const sendBlockList = await getJsonSetting(c, CONSTANTS.SEND_BLOCK_LIST_KEY) as string[]; - if (sendBlockList && sendBlockList.some((item) => to_mail.includes(item))) { - throw new Error("to_mail address is blocked") - } - if (!subject) { - throw new Error("Subject is empty") - } - if (!content) { - throw new Error("Content is empty") - } - - // send to verified address list, do not update balance - const resendEnabled = c.env.RESEND_TOKEN || c.env[ - `RESEND_TOKEN_${mailDomain.replace(/\./g, "_").toUpperCase()}` - ]; - // send by smtp - const smtpConfigMap = getJsonObjectValue>(c.env.SMTP_CONFIG); - const smtpConfig = smtpConfigMap ? smtpConfigMap[mailDomain] : null; - // send by verified address list - let sendByVerifiedAddressList = false; - if (c.env.SEND_MAIL) { - const verifiedAddressList = await getJsonSetting(c, CONSTANTS.VERIFIED_ADDRESS_LIST_KEY) || []; - if (verifiedAddressList.includes(to_mail)) { - await sendMailToVerifyAddress(c, address, reqJson); - sendByVerifiedAddressList = true; - } - } - - // send mail workflow - if (sendByVerifiedAddressList) { - // do not update balance - } - // send by resend - else if (resendEnabled) { - await sendMailByResend(c, address, reqJson); - } - else if (smtpConfig) { - await sendMailBySmtp(c, address, reqJson, smtpConfig); - } - else { - if (c.env.SEND_MAIL) { - throw new Error(`Please enable resend or smtp for domain ${mailDomain}. Or add ${to_mail} to verified address list`); - } - throw new Error(`Please enable resend or smtp for domain ${mailDomain}`); - } - - // update balance - if (!sendByVerifiedAddressList && needCheckBalance) { - try { - const { success } = await c.env.DB.prepare( - `UPDATE address_sender SET balance = balance - 1 where address = ?` - ).bind(address).run(); - if (!success) { - console.warn(`Failed to update balance for ${address}`); - } - } catch (e) { - console.warn(`Failed to update balance for ${address}`); - } - } - // save to sendbox - try { - const reqIp = c.req.raw.headers.get("cf-connecting-ip") - const geoData = new GeoData(reqIp, c.req.raw.cf as any); - const body = { - version: "v2", - ...reqJson, - geoData: geoData, - }; - const { success: success2 } = await c.env.DB.prepare( - `INSERT INTO sendbox (address, raw) VALUES (?, ?)` - ).bind(address, JSON.stringify(body)).run(); - if (!success2) { - console.warn(`Failed to save to sendbox for ${address}`); - } - } catch (e) { - console.warn(`Failed to save to sendbox for ${address}`); - } -} - -api.post('/api/send_mail', async (c) => { - const { address } = c.get("jwtPayload") - const reqJson = await c.req.json(); - try { - await sendMail(c, address, reqJson); - } catch (e) { - console.error("Failed to send mail", e); - return c.text(`Failed to send mail ${(e as Error).message}`, 400) - } - return c.json({ status: "ok" }) -}) - -api.post('/external/api/send_mail', async (c) => { - const { token } = await c.req.json(); - try { - const { address } = await Jwt.verify(token, c.env.JWT_SECRET, "HS256"); - if (!address) { - return c.text("No address", 400) - } - const reqJson = await c.req.json(); - await sendMail(c, address as string, reqJson); - return c.json({ status: "ok" }) - } catch (e) { - console.error("Failed to send mail", e); - return c.text(`Failed to send mail ${(e as Error).message}`, 400) - } -}) - -export const getSendbox = async ( - c: Context, - address: string, limit: string, offset: string -): Promise => { - if (!address) { - return c.json({ "error": "No address" }, 400) - } - return await handleListQuery(c, - `SELECT * FROM sendbox where address = ? `, - `SELECT count(*) as count FROM sendbox where address = ? `, - [address], limit, offset - ); -} - -api.get('/api/sendbox', async (c) => { - const { address } = c.get("jwtPayload") - const { limit, offset } = c.req.query(); - return getSendbox(c, address, limit, offset); -}) - -api.delete('/api/sendbox/:id', async (c) => { - const lang = c.get("lang") || c.env.DEFAULT_LANG; - const msgs = i18n.getMessages(lang); - if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) { - return c.text(msgs.UserDeleteEmailDisabledMsg, 403) - } - const { address } = c.get("jwtPayload") - const { id } = c.req.param(); - const { success } = await c.env.DB.prepare( - `DELETE FROM sendbox WHERE address = ? and id = ? ` - ).bind(address, id).run(); - return c.json({ - success: success - }) -}) diff --git a/cloudflare_temp_email/worker/src/mails_api/webhook_settings.ts b/cloudflare_temp_email/worker/src/mails_api/webhook_settings.ts deleted file mode 100644 index 4e016d7..0000000 --- a/cloudflare_temp_email/worker/src/mails_api/webhook_settings.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Context } from "hono"; -import { CONSTANTS } from "../constants"; -import { AdminWebhookSettings, WebhookSettings } from "../models"; -import { commonParseMail, sendWebhook } from "../common"; - - -async function getWebhookSettings(c: Context): Promise { - const { address } = c.get("jwtPayload") - const adminSettings = await c.env.KV.get(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json"); - if (adminSettings?.enableAllowList && !adminSettings?.allowList.includes(address)) { - return c.text("Webhook settings is not allowed for this user", 403); - } - const settings = await c.env.KV.get( - `${CONSTANTS.WEBHOOK_KV_USER_SETTINGS_KEY}:${address}`, "json" - ) || new WebhookSettings(); - return c.json(settings); -} - - -async function saveWebhookSettings(c: Context): Promise { - const { address } = c.get("jwtPayload") - const adminSettings = await c.env.KV.get(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json"); - if (adminSettings?.enableAllowList && !adminSettings?.allowList.includes(address)) { - return c.text("Webhook settings is not allowed for this user", 403); - } - const settings = await c.req.json(); - await c.env.KV.put( - `${CONSTANTS.WEBHOOK_KV_USER_SETTINGS_KEY}:${address}`, - JSON.stringify(settings)); - return c.json({ success: true }) -} - -async function testWebhookSettings(c: Context): Promise { - const settings = await c.req.json(); - const { address } = c.get("jwtPayload"); - // random raw email - const { id: mailId, raw } = await c.env.DB.prepare( - `SELECT id, raw FROM raw_mails WHERE address = ? ORDER BY RANDOM() LIMIT 1` - ).bind(address).first<{ id: string, raw: string }>() || {}; - const parsedEmailContext: ParsedEmailContext = { rawEmail: raw || "" }; - const parsedEmail = await commonParseMail(parsedEmailContext); - const res = await sendWebhook(settings, { - id: mailId || "0", - url: c.env.FRONTEND_URL ? `${c.env.FRONTEND_URL}?mail_id=${mailId}` : "", - from: parsedEmail?.sender || "test@test.com", - to: address, - subject: parsedEmail?.subject || "test subject", - raw: raw || "test raw email", - parsedText: parsedEmail?.text || "test parsed text", - parsedHtml: parsedEmail?.html || "test parsed html" - }); - if (!res.success) { - return c.text(res.message || "send webhook error", 400); - } - return c.json({ success: true }); -} - -export default { - getWebhookSettings, - saveWebhookSettings, - testWebhookSettings, -} diff --git a/cloudflare_temp_email/worker/src/models/index.ts b/cloudflare_temp_email/worker/src/models/index.ts deleted file mode 100644 index 4b3ea62..0000000 --- a/cloudflare_temp_email/worker/src/models/index.ts +++ /dev/null @@ -1,154 +0,0 @@ -import type { - AuthenticatorTransportFuture, - CredentialDeviceType, - Base64URLString, -} from '@simplewebauthn/types'; - -export type Passkey = { - id: Base64URLString; - publicKey: string; - counter: number; - deviceType: CredentialDeviceType; - backedUp: boolean; - transports?: AuthenticatorTransportFuture[]; -}; - -export class AdminWebhookSettings { - enableAllowList: boolean; - allowList: string[]; - - constructor(enableAllowList: boolean, allowList: string[]) { - this.enableAllowList = enableAllowList; - this.allowList = allowList; - } -} - -export type WebhookMail = { - id: string; - url?: string; - from: string; - to: string; - subject: string; - raw: string; - parsedText: string; - parsedHtml: string; -} - -export type CleanupSettings = { - - enableMailsAutoCleanup: boolean | undefined; - cleanMailsDays: number; - enableUnknowMailsAutoCleanup: boolean | undefined; - cleanUnknowMailsDays: number; - enableSendBoxAutoCleanup: boolean | undefined; - cleanSendBoxDays: number; - enableAddressAutoCleanup: boolean | undefined; - cleanAddressDays: number; - enableInactiveAddressAutoCleanup: boolean | undefined; - cleanInactiveAddressDays: number; -} - -export class GeoData { - - ip: string; - country: string | undefined; - city: string | undefined; - timezone: string | undefined; - postalCode: string | undefined; - region: string | undefined; - latitude: number | undefined; - longitude: number | undefined; - regionCode: string | undefined; - asOrganization: string | undefined; - - constructor(ip: string | null, data: GeoData | undefined | null) { - const { - country, city, timezone, postalCode, region, - latitude, longitude, regionCode, asOrganization - } = data || {}; - this.ip = ip || "unknown"; - this.country = country; - this.city = city; - this.timezone = timezone; - this.postalCode = postalCode; - this.region = region; - this.latitude = latitude; - this.longitude = longitude; - this.regionCode = regionCode; - this.asOrganization = asOrganization; - } -} - -export class UserSettings { - - enable: boolean | undefined; - enableMailVerify: boolean | undefined; - verifyMailSender: string | undefined; - enableMailAllowList: boolean | undefined; - mailAllowList: string[] | undefined; - maxAddressCount: number; - - constructor(data: UserSettings | undefined | null) { - const { - enable, enableMailVerify, verifyMailSender, - enableMailAllowList, mailAllowList, maxAddressCount - } = data || {}; - this.enable = enable; - this.enableMailVerify = enableMailVerify; - this.verifyMailSender = verifyMailSender; - this.enableMailAllowList = enableMailAllowList; - this.mailAllowList = mailAllowList; - this.maxAddressCount = maxAddressCount || 5; - } -} - -export class UserInfo { - - geoData: GeoData; - userEmail: string; - - constructor(geoData: GeoData, userEmail: string) { - this.geoData = geoData; - this.userEmail = userEmail; - } -} - -export class WebhookSettings { - enabled: boolean = false - url: string = '' - method: string = 'POST' - headers: string = JSON.stringify({ - "Content-Type": "application/json" - }, null, 2) - body: string = JSON.stringify({ - "id": "${id}", - "url": "${url}", - "from": "${from}", - "to": "${to}", - "subject": "${subject}", - "raw": "${raw}", - "parsedText": "${parsedText}", - "parsedHtml": "${parsedHtml}", - }, null, 2) -} - -export type UserOauth2Settings = { - name: string; - clientID: string; - clientSecret: string; - authorizationURL: string; - accessTokenURL: string; - accessTokenFormat: string; - userInfoURL: string; - redirectURL: string; - logoutURL?: string; - userEmailKey: string; - scope: string; - enableMailAllowList?: boolean | undefined; - mailAllowList?: string[] | undefined; -} - -export type EmailRuleSettings = { - blockReceiveUnknowAddressEmail: boolean; - emailForwardingList: SubdomainForwardAddressList[] -} diff --git a/cloudflare_temp_email/worker/src/scheduled.ts b/cloudflare_temp_email/worker/src/scheduled.ts deleted file mode 100644 index 971c86f..0000000 --- a/cloudflare_temp_email/worker/src/scheduled.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Context } from 'hono'; -import { cleanup } from './common' -import { CONSTANTS } from './constants' -import { getJsonSetting } from './utils'; -import { CleanupSettings } from './models'; - -export async function scheduled(event: ScheduledEvent, env: Bindings, ctx: any) { - console.log("Scheduled event: ", event); - const autoCleanupSetting = await getJsonSetting( - { env: env, } as Context, - CONSTANTS.AUTO_CLEANUP_KEY - ); - if (!autoCleanupSetting) { - console.log("No auto cleanup settings found, skipping cleanup."); - return; - } - console.log("autoCleanupSetting:", JSON.stringify(autoCleanupSetting)); - if (autoCleanupSetting.enableMailsAutoCleanup) { - await cleanup( - { env: env, } as Context, - "mails", - autoCleanupSetting.cleanMailsDays - ); - } - if (autoCleanupSetting.enableUnknowMailsAutoCleanup) { - await cleanup( - { env: env, } as Context, - "mails_unknow", - autoCleanupSetting.cleanUnknowMailsDays - ); - } - if (autoCleanupSetting.enableSendBoxAutoCleanup) { - await cleanup( - { env: env, } as Context, - "sendbox", - autoCleanupSetting.cleanSendBoxDays - ); - } - if (autoCleanupSetting.enableInactiveAddressAutoCleanup) { - await cleanup( - { env: env, } as Context, - "inactiveAddress", - autoCleanupSetting.cleanInactiveAddressDays - ); - } - if (autoCleanupSetting.enableAddressAutoCleanup) { - await cleanup( - { env: env, } as Context, - "addressCreated", - autoCleanupSetting.cleanAddressDays - ); - } -} diff --git a/cloudflare_temp_email/worker/src/telegram_api/common.ts b/cloudflare_temp_email/worker/src/telegram_api/common.ts deleted file mode 100644 index 3a075dc..0000000 --- a/cloudflare_temp_email/worker/src/telegram_api/common.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { Context } from "hono"; -import { Jwt } from "hono/utils/jwt"; -import { CONSTANTS } from "../constants"; -import { getBooleanValue, getIntValue, getJsonSetting } from "../utils"; -import { deleteAddressWithData, newAddress, generateRandomName } from "../common"; - -export const tgUserNewAddress = async ( - c: Context, userId: string, address: string -): Promise<{ address: string, jwt: string }> => { - if (c.env.RATE_LIMITER) { - const { success } = await c.env.RATE_LIMITER.limit( - { key: `${CONSTANTS.TG_KV_PREFIX}:${userId}` } - ) - if (!success) { - throw Error("Rate limit exceeded") - } - } - // Check if custom address names are disabled - const disableCustomAddressName = getBooleanValue(c.env.DISABLE_CUSTOM_ADDRESS_NAME); - - // Parse address parameter - handle empty or whitespace-only address - const trimmedAddress = address ? address.trim() : ""; - const [name, domain] = trimmedAddress.includes("@") ? trimmedAddress.split("@") : [trimmedAddress, null]; - const jwtList = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || []; - if (jwtList.length >= getIntValue(c.env.TG_MAX_ADDRESS, 5)) { - throw Error("绑定地址数量已达上限"); - } - // Generate name if disabled or not provided - const finalName = (!name || disableCustomAddressName) ? generateRandomName(c) : name; - - // check name block list - const value = await getJsonSetting(c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY); - const blockList = (value || []) as string[]; - if (blockList.some((item) => finalName.includes(item))) { - throw Error(`Name[${finalName}]is blocked`); - } - - const res = await newAddress(c, { - name: finalName, - domain, - enablePrefix: true - }); - // for mail push to telegram - await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify([...jwtList, res.jwt])); - await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${res.address}`, userId.toString()); - return res; -} - -export const jwtListToAddressData = async ( - c: Context, jwtList: string[] -): Promise<{ - addressList: string[], addressIdMap: Record, - invalidJwtList: string[] -}> => { - const addressList = [] as string[]; - const addressIdMap = {} as Record; - const invalidJwtList = [] as string[]; - for (const jwt of jwtList) { - try { - const { address, address_id } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256"); - const name = await c.env.DB.prepare( - `SELECT name FROM address WHERE id = ? ` - ).bind(address_id).first("name"); - if (!name) { - addressList.push("无效地址"); - invalidJwtList.push(jwt); - continue; - } - addressList.push(address as string); - addressIdMap[address as string] = address_id as number; - } catch (e) { - addressList.push("无效凭证"); - invalidJwtList.push(jwt); - console.log(`获取地址列表失败: ${(e as Error).message}`); - } - } - return { addressList, addressIdMap, invalidJwtList }; -} - -export const bindTelegramAddress = async ( - c: Context, userId: string, jwt: string -): Promise => { - const { address } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256"); - if (!address) { - throw Error("无效凭证"); - } - const jwtList = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || []; - const { addressIdMap } = await jwtListToAddressData(c, jwtList); - if (address as string in addressIdMap) { - return address as string; - } - if (jwtList.length >= getIntValue(c.env.TG_MAX_ADDRESS, 5)) { - throw Error("绑定地址数量已达上限, 请先 /cleaninvalidaddress"); - } - await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify([...jwtList, jwt])); - // for mail push to telegram - await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${address}`, userId.toString()); - return address as string; -} - -export const unbindTelegramAddress = async ( - c: Context, userId: string, address: string -): Promise => { - const jwtList = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || []; - const newJwtList = []; - for (const jwt of jwtList) { - try { - const { address: kvAddress } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256"); - if (kvAddress == address) { - continue; - } - } catch (e) { - console.log(`解绑失败: ${(e as Error).message}`); - } - newJwtList.push(jwt); - } - await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify(newJwtList)); - await c.env.KV.delete(`${CONSTANTS.TG_KV_PREFIX}:${address}`); - return true; -} - -export const unbindTelegramByAddress = async ( - c: Context, address: string -): Promise => { - if (!c.env.KV) return true; - const userId = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:${address}`) - if (userId) { - return await unbindTelegramAddress(c, userId, address); - } - return true; -} - - -export const deleteTelegramAddress = async ( - c: Context, userId: string, address: string -): Promise => { - const jwtList = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || []; - const { addressIdMap } = await jwtListToAddressData(c, jwtList); - if (!(address in addressIdMap)) { - throw Error("此地址不属于您"); - } - await deleteAddressWithData(c, null, addressIdMap[address]) - return true; -} diff --git a/cloudflare_temp_email/worker/src/telegram_api/index.ts b/cloudflare_temp_email/worker/src/telegram_api/index.ts deleted file mode 100644 index 4a3fc02..0000000 --- a/cloudflare_temp_email/worker/src/telegram_api/index.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Hono } from 'hono' -import { ServerResponse } from 'node:http' -import { Writable } from 'node:stream' - -import { newTelegramBot, initTelegramBotCommands, sendMailToTelegram } from './telegram' -import settings from './settings' -import miniapp from './miniapp' - -export const api = new Hono(); -export { sendMailToTelegram } - -api.use("/telegram/*", async (c, next) => { - if (!c.env.TELEGRAM_BOT_TOKEN) { - return c.text("TELEGRAM_BOT_TOKEN is required", 400); - } - if (!c.env.KV) { - return c.text("KV is required", 400); - } - return await next(); -}); - -api.use("/admin/telegram/*", async (c, next) => { - if (!c.env.TELEGRAM_BOT_TOKEN) { - return c.text("TELEGRAM_BOT_TOKEN is required", 400); - } - if (!c.env.KV) { - return c.text("KV is required", 400); - } - return await next(); -}); - -api.post("/telegram/webhook", async (c) => { - const token = c.env.TELEGRAM_BOT_TOKEN; - const bot = newTelegramBot(c, token); - let body = null; - const res = new Writable(); - Object.assign(res, { - headersSent: false, - setHeader: (name: string, value: string) => c.header(name, value), - end: (data: any) => body = data, - }); - const reqJson = await c.req.json(); - await bot.handleUpdate(reqJson, res as ServerResponse); - return c.body(body); -}); - -api.post("/admin/telegram/init", async (c) => { - const domain = new URL(c.req.url).host; - const token = c.env.TELEGRAM_BOT_TOKEN; - const webhookUrl = `https://${domain}/telegram/webhook`; - console.log(`setting webhook to ${webhookUrl}`); - const bot = newTelegramBot(c, token); - await bot.telegram.setWebhook(webhookUrl) - await initTelegramBotCommands(bot); - return c.json({ - message: "webhook set successfully", - }); -}); - -api.get("/admin/telegram/status", async (c) => { - const token = c.env.TELEGRAM_BOT_TOKEN; - const bot = newTelegramBot(c, token); - const info = await bot.telegram.getWebhookInfo() - const commands = await bot.telegram.getMyCommands() - return c.json({ info, commands }); -}); - -api.get("/admin/telegram/settings", settings.getTelegramSettings); -api.post("/admin/telegram/settings", settings.saveTelegramSettings); -api.post("/telegram/get_bind_address", miniapp.getTelegramBindAddress); -api.post("/telegram/new_address", miniapp.newTelegramAddress); -api.post("/telegram/bind_address", miniapp.bindAddress); -api.post("/telegram/unbind_address", miniapp.unbindAddress); -api.post("/telegram/get_mail", miniapp.getMail); diff --git a/cloudflare_temp_email/worker/src/telegram_api/miniapp.ts b/cloudflare_temp_email/worker/src/telegram_api/miniapp.ts deleted file mode 100644 index 3dde76b..0000000 --- a/cloudflare_temp_email/worker/src/telegram_api/miniapp.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { Context } from "hono"; -import { Jwt } from 'hono/utils/jwt' -import { CONSTANTS } from "../constants"; -import { bindTelegramAddress, jwtListToAddressData, tgUserNewAddress, unbindTelegramAddress } from "./common"; -import { checkCfTurnstile } from "../utils"; -import { TelegramSettings } from "./settings"; -import i18n from "../i18n"; - -const encoder = new TextEncoder(); -const TG_AUTH_TIMEOUT = 300; - -const checkTelegramAuth = async ( - c: Context, initData: string -): Promise => { - // check if the request is from telegram - const initDataObj = new URLSearchParams(initData); - initDataObj.sort() - const hash = initDataObj.get('hash'); - initDataObj.delete("hash"); - const dataToCheck = [...initDataObj.entries()].map(([key, value]) => key + "=" + value).join("\n"); - const auth_date = Number(initDataObj.get('auth_date')); - if (auth_date + TG_AUTH_TIMEOUT < (new Date().getTime() / 1000)) { - throw Error("Auth date expired"); - } - const user = initDataObj.get('user'); - if (!hash || !user) { - throw Error("Invalid initData"); - } - const { id: userId } = JSON.parse(user); - const cryptoKey = await crypto.subtle.importKey( - "raw", - encoder.encode("WebAppData"), - { name: "HMAC", hash: { name: "SHA-256" } }, - false, - ["sign"] - ); - const secretKeyBuffer = await crypto.subtle.sign( - "HMAC", cryptoKey, encoder.encode(c.env.TELEGRAM_BOT_TOKEN) - ); - const secretKey = await crypto.subtle.importKey( - "raw", - secretKeyBuffer, - { name: "HMAC", hash: { name: "SHA-256" } }, - false, - ["sign", "verify"] - ); - const calcHmac = await crypto.subtle.sign( - "HMAC", secretKey, encoder.encode(dataToCheck) - ); - const calcHash = Array.from(new Uint8Array(calcHmac)) - .map((b) => b.toString(16).padStart(2, "0")) - .join(""); - if (calcHash != hash) { - throw Error("Invalid initData"); - } - if (typeof userId === "number") { - return userId.toString(); - } - return userId; -} - -async function getTelegramBindAddress(c: Context): Promise { - const { initData } = await c.req.json(); - try { - const userId = await checkTelegramAuth(c, initData); - // get the address list from the KV - const jwtList = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || []; - const res = []; - for (const jwt of jwtList) { - try { - const { address } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256"); - res.push({ address, jwt }); - } catch (e) { - console.error(`failed to verify jwt with error: ${e}`) - continue; - } - } - return c.json(res); - } - catch (e) { - return c.text((e as Error).message, 400); - } -} - -async function newTelegramAddress(c: Context): Promise { - const { initData, address, cf_token } = await c.req.json(); - const lang = c.get("lang") || c.env.DEFAULT_LANG; - const msgs = i18n.getMessages(lang); - // check cf turnstile - try { - await checkCfTurnstile(c, cf_token); - } catch (error) { - return c.text(msgs.TurnstileCheckFailedMsg, 500) - } - try { - const userId = await checkTelegramAuth(c, initData); - // get the address list from the KV - const res = await tgUserNewAddress(c, userId, address) - return c.json(res); - } - catch (e) { - return c.text((e as Error).message, 400); - } -} - -async function bindAddress(c: Context): Promise { - const { initData, jwt } = await c.req.json(); - try { - const userId = await checkTelegramAuth(c, initData); - await bindTelegramAddress(c, userId, jwt); - return c.json({ success: true }); - } - catch (e) { - return c.text((e as Error).message, 400); - } -} - -async function unbindAddress(c: Context): Promise { - const { initData, address } = await c.req.json(); - try { - const userId = await checkTelegramAuth(c, initData); - await unbindTelegramAddress(c, userId, address); - return c.json({ success: true }); - } - catch (e) { - return c.text((e as Error).message, 400); - } -} - -async function getMail(c: Context): Promise { - const { initData, mailId } = await c.req.json(); - try { - const userId = await checkTelegramAuth(c, initData); - const jwtList = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || []; - const { addressList, addressIdMap } = await jwtListToAddressData(c, jwtList); - const result = await c.env.DB.prepare( - `SELECT * FROM raw_mails where id = ?` - ).bind(mailId).first(); - const settings = await c.env.KV.get(CONSTANTS.TG_KV_SETTINGS_KEY, "json"); - const superUser = settings?.enableGlobalMailPush && settings?.globalMailPushList.includes(userId); - if (!superUser) { - if (result?.address && !(result.address as string in addressIdMap)) { - return c.text("无权查看此邮件", 403); - } - const address_id = addressIdMap[result?.address as string]; - const db_address_id = await c.env.DB.prepare( - `SELECT id FROM address where id = ? ` - ).bind(address_id).first("id"); - if (!db_address_id) { - return c.text("无权查看此邮件", 403); - } - } - return c.json(result); - } - catch (e) { - return c.text((e as Error).message, 400); - } -} - -export default { - getTelegramBindAddress, - newTelegramAddress, - bindAddress, - unbindAddress, - getMail, -} diff --git a/cloudflare_temp_email/worker/src/telegram_api/settings.ts b/cloudflare_temp_email/worker/src/telegram_api/settings.ts deleted file mode 100644 index 193e858..0000000 --- a/cloudflare_temp_email/worker/src/telegram_api/settings.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Context } from "hono"; -import { CONSTANTS } from "../constants"; - -export class TelegramSettings { - enableAllowList: boolean; - allowList: string[]; - miniAppUrl: string; - enableGlobalMailPush: boolean; - globalMailPushList: string[]; - - constructor( - enableAllowList: boolean, allowList: string[], miniAppUrl: string, - enableGlobalMailPush: boolean, globalMailPushList: string[] - ) { - this.enableAllowList = enableAllowList; - this.allowList = allowList; - this.miniAppUrl = miniAppUrl; - this.enableGlobalMailPush = enableGlobalMailPush; - this.globalMailPushList = globalMailPushList; - } -} - -async function getTelegramSettings(c: Context): Promise { - const settings = await c.env.KV.get(CONSTANTS.TG_KV_SETTINGS_KEY, "json"); - return c.json(settings || new TelegramSettings(false, [], "", false, [])); -} - - -async function saveTelegramSettings(c: Context): Promise { - const settings = await c.req.json(); - await c.env.KV.put(CONSTANTS.TG_KV_SETTINGS_KEY, JSON.stringify(settings)); - return c.json({ success: true }) -} - -export default { - getTelegramSettings, - saveTelegramSettings, -} diff --git a/cloudflare_temp_email/worker/src/telegram_api/telegram.ts b/cloudflare_temp_email/worker/src/telegram_api/telegram.ts deleted file mode 100644 index ce7fd77..0000000 --- a/cloudflare_temp_email/worker/src/telegram_api/telegram.ts +++ /dev/null @@ -1,375 +0,0 @@ - -import { Context } from "hono"; -import { Telegraf, Context as TgContext, Markup } from "telegraf"; -import { callbackQuery } from "telegraf/filters"; - -import { CONSTANTS } from "../constants"; -import { getDomains, getJsonObjectValue, getStringValue } from '../utils'; -import { TelegramSettings } from "./settings"; -import { bindTelegramAddress, deleteTelegramAddress, jwtListToAddressData, tgUserNewAddress, unbindTelegramAddress, unbindTelegramByAddress } from "./common"; -import { commonParseMail } from "../common"; -import { UserFromGetMe } from "telegraf/types"; - - -const COMMANDS = [ - { - command: "start", - description: "开始使用" - }, - { - command: "new", - description: "新建邮箱地址, 如果要自定义邮箱地址, 请输入 /new, 通过 /new @ 可以指定, name [a-z0-9] 有效, name 为空则随机生成, @ 可选" - }, - { - command: "address", - description: "查看邮箱地址列表" - }, - { - command: "bind", - description: "绑定邮箱地址, 请输入 /bind <邮箱地址凭证>" - }, - { - command: "unbind", - description: "解绑邮箱地址, 请输入 /unbind <邮箱地址>" - }, - { - command: "delete", - description: "删除邮箱地址, 请输入 /delete <邮箱地址>" - }, - { - command: "mails", - description: "查看邮件, 请输入 /mails <邮箱地址>, 不输入地址默认查看第一个地址" - }, - { - command: "cleaninvalidaddress", - description: "清理无效地址, 请输入 /cleaninvalidaddress" - }, -] - -export function newTelegramBot(c: Context, token: string): Telegraf { - const bot = new Telegraf(token); - const botInfo = getJsonObjectValue(c.env.TG_BOT_INFO); - if (botInfo) { - bot.botInfo = botInfo; - } - - bot.use(async (ctx, next) => { - // check if in private chat - if (ctx.chat?.type !== "private") { - return; - } - - const userId = ctx?.message?.from?.id || ctx.callbackQuery?.message?.chat?.id; - if (!userId) { - return await ctx.reply("无法获取用户信息"); - } - - const settings = await c.env.KV.get(CONSTANTS.TG_KV_SETTINGS_KEY, "json"); - if (settings?.enableAllowList && settings?.enableAllowList - && !settings.allowList.includes(userId.toString()) - ) { - return await ctx.reply("您没有权限使用此机器人"); - } - try { - await next(); - } catch (error) { - console.error(`Error: ${error}`); - return await ctx.reply(`Error: ${error}`); - } - }) - - bot.command("start", async (ctx: TgContext) => { - const prefix = getStringValue(c.env.PREFIX) - const domains = getDomains(c); - return await ctx.reply( - "欢迎使用本机器人, 您可以打开 mini app \n\n" - + (prefix ? `当前已启用前缀: ${prefix}\n` : '') - + `当前可用域名: ${JSON.stringify(domains)}\n` - + "请使用以下命令:\n" - + COMMANDS.map(c => `/${c.command}: ${c.description}`).join("\n") - ); - }); - - bot.command("new", async (ctx: TgContext) => { - const userId = ctx?.message?.from?.id; - if (!userId) { - return await ctx.reply("无法获取用户信息"); - } - try { - // @ts-ignore - const address = ctx?.message?.text.slice("/new".length).trim(); - const res = await tgUserNewAddress(c, userId.toString(), address); - return await ctx.reply(`创建地址成功:\n` - + `地址: ${res.address}\n` - + `凭证: \`${res.jwt}\`\n`, - { - parse_mode: "Markdown" - } - ); - } catch (e) { - return await ctx.reply(`创建地址失败: ${(e as Error).message}`); - } - }); - - bot.command("bind", async (ctx: TgContext) => { - const userId = ctx?.message?.from?.id; - if (!userId) { - return await ctx.reply("无法获取用户信息"); - } - try { - // @ts-ignore - const jwt = ctx?.message?.text.slice("/bind".length).trim(); - if (!jwt) { - return await ctx.reply("请输入凭证"); - } - const address = await bindTelegramAddress(c, userId.toString(), jwt); - return await ctx.reply(`绑定成功:\n` - + `地址: ${address}` - ); - } - catch (e) { - return await ctx.reply(`绑定失败: ${(e as Error).message}`); - } - }); - - bot.command("unbind", async (ctx: TgContext) => { - const userId = ctx?.message?.from?.id; - if (!userId) { - return await ctx.reply("无法获取用户信息"); - } - try { - // @ts-ignore - const address = ctx?.message?.text.slice("/unbind".length).trim(); - if (!address) { - return await ctx.reply("请输入地址"); - } - await unbindTelegramAddress(c, userId.toString(), address); - return await ctx.reply(`解绑成功:\n地址: ${address}` - ); - } - catch (e) { - return await ctx.reply(`解绑失败: ${(e as Error).message}`); - } - }) - - bot.command("delete", async (ctx: TgContext) => { - const userId = ctx?.message?.from?.id; - if (!userId) { - return await ctx.reply("无法获取用户信息"); - } - try { - // @ts-ignore - const address = ctx?.message?.text.slice("/delete".length).trim(); - if (!address) { - return await ctx.reply("请输入地址"); - } - await deleteTelegramAddress(c, userId.toString(), address); - return await ctx.reply(`删除成功: ${address}`); - } catch (e) { - return await ctx.reply(`删除失败: ${(e as Error).message}`); - } - }); - - bot.command("address", async (ctx) => { - const userId = ctx?.message?.from?.id; - if (!userId) { - return await ctx.reply("无法获取用户信息"); - } - try { - const jwtList = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || []; - const { addressList } = await jwtListToAddressData(c, jwtList); - return await ctx.reply(`地址列表:\n\n` - + addressList.map(a => `地址: ${a}`).join("\n") - ); - } catch (e) { - return await ctx.reply(`获取地址列表失败: ${(e as Error).message}`); - } - }); - - bot.command("cleaninvalidaddress", async (ctx: TgContext) => { - const userId = ctx?.message?.from?.id; - if (!userId) { - return await ctx.reply("无法获取用户信息"); - } - try { - const jwtList = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || []; - const { invalidJwtList } = await jwtListToAddressData(c, jwtList); - const newJwtList = jwtList.filter(jwt => !invalidJwtList.includes(jwt)); - await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify(newJwtList)); - const { addressList } = await jwtListToAddressData(c, newJwtList); - return await ctx.reply(`清理无效地址成功:\n\n` - + `当前地址列表:\n\n` - + addressList.map(a => `地址: ${a}`).join("\n") - ); - } catch (e) { - return await ctx.reply(`清理无效地址失败: ${(e as Error).message}`); - } - }); - - const queryMail = async (ctx: TgContext, queryAddress: string, mailIndex: number, edit: boolean) => { - const userId = ctx?.message?.from?.id || ctx.callbackQuery?.message?.chat?.id; - if (!userId) { - return await ctx.reply("无法获取用户信息"); - } - const jwtList = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || []; - const { addressList, addressIdMap } = await jwtListToAddressData(c, jwtList); - if (!queryAddress && addressList.length > 0) { - queryAddress = addressList[0]; - } - if (!(queryAddress in addressIdMap)) { - return await ctx.reply(`未绑定此地址 ${queryAddress}`); - } - const address_id = addressIdMap[queryAddress]; - const db_address_id = await c.env.DB.prepare( - `SELECT id FROM address where id = ? ` - ).bind(address_id).first("id"); - if (!db_address_id) { - return await ctx.reply("无效地址"); - } - const { raw, id: mailId, created_at } = await c.env.DB.prepare( - `SELECT * FROM raw_mails where address = ? ` - + ` order by id desc limit 1 offset ?` - ).bind( - queryAddress, mailIndex - ).first<{ raw: string, id: string, created_at: string }>() || {}; - const { mail } = raw ? await parseMail({ rawEmail: raw }, queryAddress, created_at) : { mail: "已经没有邮件了" }; - const settings = await c.env.KV.get(CONSTANTS.TG_KV_SETTINGS_KEY, "json"); - const miniAppButtons = [] - if (settings?.miniAppUrl && settings?.miniAppUrl?.length > 0 && mailId) { - const url = new URL(settings.miniAppUrl); - url.pathname = "/telegram_mail" - url.searchParams.set("mail_id", mailId); - miniAppButtons.push(Markup.button.webApp("查看邮件", url.toString())); - } - if (edit) { - return await ctx.editMessageText(mail || "无邮件", - { - ...Markup.inlineKeyboard([ - Markup.button.callback("上一条", `mail_${queryAddress}_${mailIndex - 1}`, mailIndex <= 0), - ...miniAppButtons, - Markup.button.callback("下一条", `mail_${queryAddress}_${mailIndex + 1}`, !raw), - ]) - }, - ); - } - return await ctx.reply(mail || "无邮件", - { - ...Markup.inlineKeyboard([ - Markup.button.callback("上一条", `mail_${queryAddress}_${mailIndex - 1}`, mailIndex <= 0), - ...miniAppButtons, - Markup.button.callback("下一条", `mail_${queryAddress}_${mailIndex + 1}`, !raw), - ]) - }, - ); - } - - bot.command("mails", async ctx => { - try { - const queryAddress = ctx?.message?.text.slice("/mails".length).trim(); - return await queryMail(ctx, queryAddress, 0, false); - } catch (e) { - return await ctx.reply(`获取邮件失败: ${(e as Error).message}`); - } - }); - - bot.on(callbackQuery("data"), async ctx => { - // Use ctx.callbackQuery.data - try { - const data = ctx.callbackQuery.data; - if (data && data.startsWith("mail_") && data.split("_").length === 3) { - const [_, queryAddress, mailIndex] = data.split("_"); - await queryMail(ctx, queryAddress, parseInt(mailIndex), true); - } - } - catch (e) { - console.log(`获取邮件失败: ${(e as Error).message}`, e); - return await ctx.answerCbQuery(`获取邮件失败: ${(e as Error).message}`); - } - await ctx.answerCbQuery(); - }); - - return bot; -} - - -export async function initTelegramBotCommands(bot: Telegraf) { - await bot.telegram.setMyCommands(COMMANDS); -} - -const parseMail = async ( - parsedEmailContext: ParsedEmailContext, - address: string, created_at: string | undefined | null -) => { - if (!parsedEmailContext.rawEmail) { - return {}; - } - try { - const parsedEmail = await commonParseMail(parsedEmailContext); - let parsedText = parsedEmail?.text || ""; - if (parsedText.length && parsedText.length > 1000) { - parsedText = parsedEmail?.text.substring(0, 1000) + "\n\n...\n消息过长请到miniapp查看"; - } - return { - isHtml: false, - mail: `From: ${parsedEmail?.sender || "无发件人"}\n` - + `To: ${address}\n` - + (created_at ? `Date: ${created_at}\n` : "") - + `Subject: ${parsedEmail?.subject}\n` - + `Content:\n${parsedText || "解析失败,请打开 mini app 查看"}` - }; - } catch (e) { - return { - isHtml: false, - mail: `解析邮件失败: ${(e as Error).message}` - }; - } -} - - -export async function sendMailToTelegram( - c: Context, address: string, - parsedEmailContext: ParsedEmailContext, - message_id: string | null -) { - if (!c.env.TELEGRAM_BOT_TOKEN || !c.env.KV) { - return; - } - const userId = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:${address}`); - const { mail } = await parseMail(parsedEmailContext, address, new Date().toUTCString()); - if (!mail) { - return; - } - const settings = await c.env.KV.get(CONSTANTS.TG_KV_SETTINGS_KEY, "json"); - const globalPush = settings?.enableGlobalMailPush && settings?.globalMailPushList; - if (!userId && !globalPush) { - return; - } - const mailId = await c.env.DB.prepare( - `SELECT id FROM raw_mails where address = ? and message_id = ?` - ).bind(address, message_id).first("id"); - const bot = newTelegramBot(c, c.env.TELEGRAM_BOT_TOKEN); - const miniAppButtons = [] - if (settings?.miniAppUrl && settings?.miniAppUrl?.length > 0 && mailId) { - const url = new URL(settings.miniAppUrl); - url.pathname = "/telegram_mail" - url.searchParams.set("mail_id", mailId); - miniAppButtons.push(Markup.button.webApp("查看邮件", url.toString())); - } - if (globalPush) { - for (const pushId of settings.globalMailPushList) { - await bot.telegram.sendMessage(pushId, mail, { - ...Markup.inlineKeyboard([ - ...miniAppButtons, - ]) - }); - } - } - if (!userId) { - return; - } - await bot.telegram.sendMessage(userId, mail, { - ...Markup.inlineKeyboard([ - ...miniAppButtons, - ]) - }); -} diff --git a/cloudflare_temp_email/worker/src/types.d.ts b/cloudflare_temp_email/worker/src/types.d.ts deleted file mode 100644 index e559d5b..0000000 --- a/cloudflare_temp_email/worker/src/types.d.ts +++ /dev/null @@ -1,141 +0,0 @@ -type UserRole = { - domains: string[] | undefined | null, - role: string, - prefix: string | undefined | null -} - -type Bindings = { - // bindings - DB: D1Database - KV: KVNamespace - RATE_LIMITER: any - SEND_MAIL: any - ASSETS: Fetcher - - // config - DEFAULT_LANG: string | undefined - TITLE: string | undefined - ANNOUNCEMENT: string | undefined | null - ALWAYS_SHOW_ANNOUNCEMENT: string | boolean | undefined - PREFIX: string | undefined - ADDRESS_CHECK_REGEX: string | undefined - ADDRESS_REGEX: string | undefined - MIN_ADDRESS_LEN: string | number | undefined - MAX_ADDRESS_LEN: string | number | undefined - DEFAULT_DOMAINS: string | string[] | undefined - DOMAINS: string | string[] | undefined - DISABLE_CUSTOM_ADDRESS_NAME: string | boolean | undefined - CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST: string | boolean | undefined - ADMIN_USER_ROLE: string | undefined - USER_DEFAULT_ROLE: string | UserRole | undefined - USER_ROLES: string | UserRole[] | undefined - DOMAIN_LABELS: string | string[] | undefined - PASSWORDS: string | string[] | undefined - ADMIN_PASSWORDS: string | string[] | undefined - DISABLE_ADMIN_PASSWORD_CHECK: string | boolean | undefined - JWT_SECRET: string - BLACK_LIST: string | undefined - ENABLE_AUTO_REPLY: string | boolean | undefined - ENABLE_WEBHOOK: string | boolean | undefined - ENABLE_USER_CREATE_EMAIL: string | boolean | undefined - DISABLE_ANONYMOUS_USER_CREATE_EMAIL: string | boolean | undefined - ENABLE_USER_DELETE_EMAIL: string | boolean | undefined - ENABLE_INDEX_ABOUT: string | boolean | undefined - DEFAULT_SEND_BALANCE: number | string | undefined - NO_LIMIT_SEND_ROLE: string | undefined | null - ADMIN_CONTACT: string | undefined - COPYRIGHT: string | undefined - DISABLE_SHOW_GITHUB: string | boolean | undefined - FORWARD_ADDRESS_LIST: string | string[] | undefined - - ENABLE_CHECK_JUNK_MAIL: string | boolean | undefined - JUNK_MAIL_CHECK_LIST: string | string[] | undefined - JUNK_MAIL_FORCE_PASS_LIST: string | string[] | undefined - - ENABLE_ANOTHER_WORKER: string | boolean | undefined - ANOTHER_WORKER_LIST: string | AnotherWorker[] | undefined - - SUBDOMAIN_FORWARD_ADDRESS_LIST: string | SubdomainForwardAddressList[] | undefined - - REMOVE_ALL_ATTACHMENT: string | boolean | undefined - REMOVE_EXCEED_SIZE_ATTACHMENT: string | boolean | undefined - - // s3 config - S3_ENDPOINT: string | undefined - S3_ACCESS_KEY_ID: string | undefined - S3_SECRET_ACCESS_KEY: string | undefined - S3_BUCKET: string | undefined - S3_URL_EXPIRES: number | undefined - - // cf turnstile - CF_TURNSTILE_SITE_KEY: string | undefined - CF_TURNSTILE_SECRET_KEY: string | undefined - - // resend - RESEND_TOKEN: string | undefined - [key: `RESEND_TOKEN_${string}`]: string | undefined - - // SMTP config - SMTP_CONFIG: string | object | undefined - - // telegram config - TELEGRAM_BOT_TOKEN: string - TG_MAX_ADDRESS: number | undefined - TG_BOT_INFO: string | object | undefined - - // webhook config - FRONTEND_URL: string | undefined -} - -type JwtPayload = { - address: string - address_id: number -} - -type UserPayload = { - user_email: string - user_id: number - exp: number - iat: number -} - -type Variables = { - userPayload: UserPayload, - userRolePayload: string | undefined | null, - jwtPayload: JwtPayload, - lang: string | undefined | null -} - -type HonoCustomType = { - "Bindings": Bindings; - "Variables": Variables; -} - -type AnotherWorker = { - binding: string | undefined | null, - method: string | undefined | null, - keywords: string[] | undefined | null -} - -type RPCEmailMessage = { - from: string | undefined | null, - to: string | undefined | null, - rawEmail: string | undefined | null, - headers: object | undefined | null, -} - -type ParsedEmailContext = { - rawEmail: string, - parsedEmail?: { - sender: string, - subject: string, - text: string, - html: string, - headers?: Record[] - } | undefined -} - -type SubdomainForwardAddressList = { - domains: string[] | undefined | null, - forward: string, -} diff --git a/cloudflare_temp_email/worker/src/user_api/bind_address.ts b/cloudflare_temp_email/worker/src/user_api/bind_address.ts deleted file mode 100644 index 7ece5d3..0000000 --- a/cloudflare_temp_email/worker/src/user_api/bind_address.ts +++ /dev/null @@ -1,268 +0,0 @@ -import { Context } from 'hono'; -import { Jwt } from 'hono/utils/jwt' - -import { UserSettings } from "../models"; -import { getJsonSetting } from "../utils" -import { CONSTANTS } from "../constants"; -import { unbindTelegramByAddress } from '../telegram_api/common'; -import i18n from '../i18n'; -import { updateAddressUpdatedAt } from '../common'; - -const UserBindAddressModule = { - bind: async (c: Context) => { - const { user_id } = c.get("userPayload"); - const { address_id } = c.get("jwtPayload"); - return await UserBindAddressModule.bindByID(c, user_id, address_id) - }, - bindByID: async ( - c: Context, - user_id: number | string, address_id: number | string - ) => { - if (!address_id || !user_id) { - return c.text("No address or user token", 400) - } - // check if address exists - const db_address_id = await c.env.DB.prepare( - `SELECT id FROM address where id = ?` - ).bind(address_id).first("id"); - if (!db_address_id) { - return c.text("Address not found", 400) - } - // check if user exists - const db_user_id = await c.env.DB.prepare( - `SELECT id FROM users where id = ?` - ).bind(user_id).first("id"); - if (!db_user_id) { - return c.text("User not found", 400) - } - // check if binded - const db_user_address_id = await c.env.DB.prepare( - `SELECT user_id FROM users_address where user_id = ? and address_id = ?` - ).bind(user_id, address_id).first("user_id"); - if (db_user_address_id) return c.json({ success: true }) - // check if binded address count - const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY); - const settings = new UserSettings(value); - if (settings.maxAddressCount > 0) { - const { count } = await c.env.DB.prepare( - `SELECT COUNT(*) as count FROM users_address where user_id = ?` - ).bind(user_id).first<{ count: number }>() || { count: 0 }; - if (count >= settings.maxAddressCount) { - return c.text("Max address count reached", 400) - } - } - // bind - try { - const { success } = await c.env.DB.prepare( - `INSERT INTO users_address (user_id, address_id) VALUES (?, ?)` - ).bind(user_id, address_id).run(); - if (!success) { - return c.text("Failed to bind", 500) - } - } catch (e) { - const error = e as Error; - if (error.message && error.message.includes("UNIQUE")) { - return c.text("Address already binded, please unbind first", 400) - } - return c.text("Failed to bind", 500) - } - return c.json({ success: true }) - }, - unbind: async (c: Context) => { - const { user_id } = c.get("userPayload"); - const { address_id } = await c.req.json(); - if (!address_id || !user_id) { - return c.text("Invalid address or user token", 400) - } - // check if address exists - const db_address_id = await c.env.DB.prepare( - `SELECT id FROM address where id = ?` - ).bind(address_id).first("id"); - if (!db_address_id) { - return c.text("Address not found", 400) - } - // check if user exists - const db_user_id = await c.env.DB.prepare( - `SELECT id FROM users where id = ?` - ).bind(user_id).first("id"); - if (!db_user_id) { - return c.text("User not found", 400) - } - // unbind - try { - const { success } = await c.env.DB.prepare( - `DELETE FROM users_address where user_id = ? and address_id = ?` - ).bind(user_id, address_id).run(); - if (!success) { - return c.text("Failed to unbind", 500) - } - } catch (e) { - return c.text("Failed to unbind", 500) - } - return c.json({ success: true }) - }, - getBindedAddresses: async (c: Context) => { - const { user_id } = c.get("userPayload"); - const results = await UserBindAddressModule.getBindedAddressesById(c, user_id); - return c.json({ - results: results, - }); - }, - getBindedAddressListById: async ( - c: Context, user_id: number | string - ): Promise => { - const bindedAddressList = await UserBindAddressModule.getBindedAddressesById(c, user_id); - return bindedAddressList.map((item) => item.name); - }, - getBindedAddressesById: async ( - c: Context, user_id: number | string - ): Promise<{ - id: number; - name: string; - mail_count: number; - send_count: number; - created_at: string; - updated_at: string; - }[]> => { - const msgs = i18n.getMessagesbyContext(c); - if (!user_id) { - throw new Error(msgs.UserNotFoundMsg); - } - // select binded address - const { results } = await c.env.DB.prepare( - `SELECT a.*,` - + ` (SELECT COUNT(*) FROM raw_mails WHERE address = a.name) AS mail_count,` - + ` (SELECT COUNT(*) FROM sendbox WHERE address = a.name) AS send_count` - + ` FROM address a ` - + ` JOIN users_address ua ` - + ` ON ua.address_id = a.id ` - + ` WHERE ua.user_id = ?` - + ` ORDER BY a.id DESC` - ).bind(user_id).all<{ - id: number; - name: string; - mail_count: number; - send_count: number; - created_at: string; - updated_at: string; - }>(); - return results || []; - }, - getBindedAddressJwt: async (c: Context) => { - const { address_id } = c.req.param(); - // check binded - const { user_id } = c.get("userPayload"); - if (!address_id || !user_id) { - return c.text("Invalid address or user token", 400) - } - // check users_address if address binded - const db_user_id = await c.env.DB.prepare( - `SELECT user_id FROM users_address WHERE address_id = ? and user_id = ?` - ).bind(address_id, user_id).first("user_id"); - if (!db_user_id) { - return c.text("Address not binded", 400) - } - // generate jwt - const name = await c.env.DB.prepare( - `SELECT name FROM address WHERE id = ? ` - ).bind(address_id).first("name"); - const jwt = await Jwt.sign({ - address: name, - address_id: address_id - }, c.env.JWT_SECRET, "HS256") - return c.json({ - jwt: jwt - }) - }, - transferAddress: async (c: Context) => { - const { user_id } = c.get("userPayload"); - const { address_id, target_user_email } = await c.req.json(); - // check if address exists - const address = await c.env.DB.prepare( - `SELECT name FROM address where id = ?` - ).bind(address_id).first("name"); - if (!address) { - return c.text("Address not found", 400) - } - // check if user exists - const db_user_id = await c.env.DB.prepare( - `SELECT id FROM users where id = ?` - ).bind(user_id).first("id"); - if (!db_user_id) { - return c.text("User not found", 400) - } - // check if target user exists - const target_user_id = await c.env.DB.prepare( - `SELECT id FROM users where user_email = ?` - ).bind(target_user_email).first("id"); - if (!target_user_id) { - return c.text("Target user not found", 400) - } - // check target user binded address count - const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY); - const settings = new UserSettings(value); - if (settings.maxAddressCount > 0) { - const { count } = await c.env.DB.prepare( - `SELECT COUNT(*) as count FROM users_address where user_id = ?` - ).bind(target_user_id).first<{ count: number }>() || { count: 0 }; - if (count >= settings.maxAddressCount) { - return c.text("Target User Max address count reached", 400) - } - } - // check if binded - const db_user_address_id = await c.env.DB.prepare( - `SELECT user_id FROM users_address where user_id = ? and address_id = ?` - ).bind(user_id, address_id).first("user_id"); - if (!db_user_address_id) return c.text("Address not binded", 400) - // unbind telegram address - await unbindTelegramByAddress(c, address); - // unbind user address - try { - const { success } = await c.env.DB.prepare( - `DELETE FROM users_address where user_id = ? and address_id = ?` - ).bind(user_id, address_id).run(); - if (!success) { - return c.text("Failed to unbind", 500) - } - } catch (e) { - return c.text("Failed to unbind user", 500) - } - // delete address - await c.env.DB.prepare( - `DELETE FROM address WHERE id = ? ` - ).bind(address_id).run(); - // new address - const { success: newAddressSuccess } = await c.env.DB.prepare( - `INSERT INTO address(name) VALUES(?)` - ).bind(address).run(); - if (!newAddressSuccess) { - throw new Error("Failed to create address") - } - await updateAddressUpdatedAt(c, address); - // find new address id - const new_address_id = await c.env.DB.prepare( - `SELECT id FROM address WHERE name = ?` - ).bind(address).first("id"); - if (!new_address_id) { - throw new Error("Failed to find new address id") - } - // bind - try { - const { success } = await c.env.DB.prepare( - `INSERT INTO users_address (user_id, address_id) VALUES (?, ?)` - ).bind(target_user_id, new_address_id).run(); - if (!success) { - return c.text("Failed to bind", 500) - } - } catch (e) { - const error = e as Error; - if (error.message && error.message.includes("UNIQUE")) { - return c.text("Address already binded, please unbind first", 400) - } - return c.text("Failed to bind", 500) - } - return c.json({ success: true }) - } -} - -export default UserBindAddressModule; diff --git a/cloudflare_temp_email/worker/src/user_api/index.ts b/cloudflare_temp_email/worker/src/user_api/index.ts deleted file mode 100644 index 9ecb58c..0000000 --- a/cloudflare_temp_email/worker/src/user_api/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Hono } from 'hono'; - -import settings from './settings'; -import user from './user'; -import bind_address from './bind_address'; -import passkey from './passkey'; -import oauth2 from './oauth2'; -import user_mail_api from './user_mail_api'; - -export const api = new Hono(); - -// settings api -api.get('/user_api/open_settings', settings.openSettings); -api.get('/user_api/settings', settings.settings); - -// mail api -api.get('/user_api/mails', user_mail_api.getMails); -api.delete('/user_api/mails/:id', user_mail_api.deleteMail); - -// user api -api.post('/user_api/login', user.login); -api.post('/user_api/verify_code', user.verifyCode); -api.post('/user_api/register', user.register); - -// oauth2 api -api.get('/user_api/oauth2/login_url', oauth2.getOauth2LoginUrl); -api.post('/user_api/oauth2/callback', oauth2.oauth2Login); - -// bind address api -api.get('/user_api/bind_address', bind_address.getBindedAddresses); -api.post('/user_api/bind_address', bind_address.bind); -api.get('/user_api/bind_address_jwt/:address_id', bind_address.getBindedAddressJwt); -api.post('/user_api/unbind_address', bind_address.unbind); -api.post('/user_api/transfer_address', bind_address.transferAddress); - -// passkey api -api.get('/user_api/passkey', passkey.getPassKeys); -api.post('/user_api/passkey/rename', passkey.renamePassKey); -api.delete('/user_api/passkey/:passkey_id', passkey.deletePassKey); -api.post('/user_api/passkey/register_request', passkey.registerRequest); -api.post('/user_api/passkey/register_response', passkey.registerResponse); -api.post('/user_api/passkey/authenticate_request', passkey.authenticateRequest); -api.post('/user_api/passkey/authenticate_response', passkey.authenticateResponse); diff --git a/cloudflare_temp_email/worker/src/user_api/oauth2.ts b/cloudflare_temp_email/worker/src/user_api/oauth2.ts deleted file mode 100644 index ded1e37..0000000 --- a/cloudflare_temp_email/worker/src/user_api/oauth2.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { Context } from 'hono'; -import { Jwt } from 'hono/utils/jwt' - -import i18n from '../i18n'; -import { getJsonSetting, getStringValue, getUserRoles } from '../utils'; -import { UserOauth2Settings } from '../models'; -import { CONSTANTS } from '../constants'; - - -export default { - getOauth2LoginUrl: async (c: Context) => { - const settings = await getJsonSetting(c, CONSTANTS.OAUTH2_SETTINGS_KEY); - const { clientID, state } = c.req.query(); - const lang = c.get("lang") || c.env.DEFAULT_LANG; - const msgs = i18n.getMessages(lang); - const setting = settings?.find(s => s.clientID === clientID); - if (!setting) { - return c.text(msgs.Oauth2ClientIDNotFoundMsg, 400); - } - const url = `${setting.authorizationURL}?client_id=${setting.clientID}&response_type=code&redirect_uri=${setting.redirectURL}&scope=${setting.scope}&state=${state}` - return c.json({ url }); - }, - oauth2Login: async (c: Context) => { - const { clientID, code } = await c.req.json<{ clientID?: string, code?: string }>(); - const lang = c.get("lang") || c.env.DEFAULT_LANG; - const msgs = i18n.getMessages(lang); - if (!clientID || !code) { - return c.text(msgs.Oauth2CliendIDOrCodeMissingMsg, 400); - } - const settings = await getJsonSetting(c, CONSTANTS.OAUTH2_SETTINGS_KEY); - const setting = settings?.find(s => s.clientID === clientID); - if (!setting) { - return c.text(msgs.Oauth2ClientIDNotFoundMsg, 400); - } - const params = { - code, - client_id: setting.clientID, - client_secret: setting.clientSecret, - grant_type: 'authorization_code', - redirect_uri: setting.redirectURL, - } - const res = await fetch(setting.accessTokenURL, { - method: 'POST', - body: setting.accessTokenFormat === 'json' - ? JSON.stringify(params) : - new URLSearchParams(params).toString(), - headers: { - 'Content-Type': setting.accessTokenFormat === 'json' - ? 'application/json' - : 'application/x-www-form-urlencoded', - "Accept": "application/json" - } - }) - if (!res.ok) { - console.error(`Failed to get access token: ${res.status} ${res.statusText} ${await res.text()}`) - return c.text(msgs.Oauth2FailedGetAccessTokenMsg, 400); - } - const resJson = await res.json(); - const { access_token, token_type } = resJson as { access_token: string, token_type?: string }; - const userRes = await fetch(setting.userInfoURL, { - headers: { - "Authorization": `${token_type || 'Bearer'} ${access_token}`, - "Accept": "application/json", - "User-Agent": "Cloudflare Workers" - } - }) - if (!userRes.ok) { - console.error(`Failed to get user info: ${userRes.status} ${userRes.statusText} ${await userRes.text()}`) - return c.text(msgs.Oauth2FailedGetUserInfoMsg, 400); - } - const userInfo = await userRes.json() - - const email = await (async () => { - if (setting.userEmailKey.startsWith("$")) { - const { JSONPath } = await import('jsonpath-plus'); - const email = JSONPath({ - path: setting.userEmailKey, - json: userInfo, - }) - if (email && Array.isArray(email) && email.length > 0) { - return email[0]; - } - } - const { [setting.userEmailKey]: email } = userInfo as { [key: string]: string }; - return email; - })() - - if (!email) { - return c.text(msgs.Oauth2FailedGetUserEmailMsg, 400); - } - // check email in mail allow list - const mailDomain = email.split("@")[1]; - if (setting.enableMailAllowList && !setting.mailAllowList?.includes(mailDomain)) { - return c.text(`${msgs.UserMailDomainMustInMsg} ${JSON.stringify(setting.mailAllowList, null, 2)}`, 400) - } - // insert or update user - const { success } = await c.env.DB.prepare( - `INSERT INTO users (user_email, password, user_info)` - + ` VALUES (?, '', ?)` - + ` ON CONFLICT(user_email) DO UPDATE SET updated_at = datetime('now')` - ).bind( - email, JSON.stringify(userInfo) - ).run(); - if (!success) { - return c.text(msgs.FailedToRegisterMsg, 500) - } - const { id: user_id } = await c.env.DB.prepare( - `SELECT id FROM users where user_email = ?` - ).bind(email).first() || {}; - if (!user_id) { - return c.text(msgs.UserNotFoundMsg, 400) - } - // process user roles - const defaultRole = getStringValue(c.env.USER_DEFAULT_ROLE); - if (defaultRole) { - const user_roles = getUserRoles(c); - if (!user_roles.find((r) => r.role === defaultRole)) { - return c.text(msgs.InvalidUserDefaultRoleMsg, 500); - } - // update user roles - const { success: success2 } = await c.env.DB.prepare( - `INSERT INTO user_roles (user_id, role_text)` - + ` VALUES (?, ?)` - + ` ON CONFLICT(user_id) DO NOTHING` - ).bind(user_id, defaultRole).run(); - if (!success2) { - return c.text(msgs.FailedUpdateUserDefaultRoleMsg, 500); - } - } - // create jwt - const jwt = await Jwt.sign({ - user_email: email, - user_id: user_id, - // 90 days expire in seconds - exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60, - iat: Math.floor(Date.now() / 1000), - }, c.env.JWT_SECRET, "HS256") - return c.json({ - jwt: jwt - }) - } -} diff --git a/cloudflare_temp_email/worker/src/user_api/passkey.ts b/cloudflare_temp_email/worker/src/user_api/passkey.ts deleted file mode 100644 index 876a9f5..0000000 --- a/cloudflare_temp_email/worker/src/user_api/passkey.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { Context } from 'hono'; -import { Jwt } from 'hono/utils/jwt' -import { - generateRegistrationOptions, - verifyRegistrationResponse, - generateAuthenticationOptions, - verifyAuthenticationResponse -} from '@simplewebauthn/server'; - -import { Passkey } from '../models'; -import { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types'; -import { isoBase64URL } from '@simplewebauthn/server/helpers'; - -export default { - getPassKeys: async (c: Context) => { - const user = c.get("userPayload"); - const { results } = await c.env.DB.prepare( - `SELECT passkey_name, passkey_id, created_at, updated_at FROM user_passkeys WHERE user_id = ?` - ).bind(user.user_id).all>(); - return c.json(results); - }, - renamePassKey: async (c: Context) => { - const user = c.get("userPayload"); - const { passkey_id, passkey_name } = await c.req.json(); - if (!passkey_name || passkey_name.length > 255) { - return c.text("Invalid passkey name", 400); - } - const { success } = await c.env.DB.prepare( - `UPDATE user_passkeys SET passkey_name = ? WHERE user_id = ? AND passkey_id = ?` - ).bind(passkey_name, user.user_id, passkey_id).run(); - return c.json({ success }); - }, - deletePassKey: async (c: Context) => { - const user = c.get("userPayload"); - const { passkey_id } = c.req.param(); - const { success } = await c.env.DB.prepare( - `DELETE FROM user_passkeys WHERE user_id = ? AND passkey_id = ?` - ).bind(user.user_id, passkey_id).run(); - return c.json({ success }); - }, - registerRequest: async (c: Context) => { - const user = c.get("userPayload"); - const { domain } = await c.req.json(); - const { results } = await c.env.DB.prepare( - `SELECT passkey FROM user_passkeys WHERE user_id = ?` - ).bind(user.user_id).all>(); - const excludeCredentials = results - .map((record: any) => JSON.parse(record.passkey) as Passkey) - .map((passkey: Passkey) => ({ - id: passkey.id, - transports: passkey.transports, - })); - // create challenge with 1 hour expiration - const challenge = await Jwt.sign({ - user_email: user.user_email, - user_id: user.user_id, - iat: Math.floor(Date.now() / 1000), - }, c.env.JWT_SECRET, "HS256") - // Use SimpleWebAuthn's handy function to create registration options. - const options = await generateRegistrationOptions({ - rpName: c.env.TITLE || "Temp Mail", - rpID: domain, - userID: new TextEncoder().encode(user.user_id.toString()), - userName: user.user_email, - userDisplayName: user.user_email, - attestationType: 'none', - excludeCredentials: excludeCredentials, - challenge: challenge, - }); - - return c.json(options); - }, - registerResponse: async (c: Context) => { - const user = c.get("userPayload"); - const { credential, origin, passkey_name } = await c.req.json(); - // Verify the registration response - const verification = await verifyRegistrationResponse({ - response: credential, - expectedChallenge: async (challenge: string) => { - const payload = await Jwt.verify(atob(challenge), c.env.JWT_SECRET, "HS256"); - if (!payload || !payload.iat) return false; - // check iad is not older than 5 minutes - if (Math.floor(Date.now() / 1000) - payload.iat > 300) return false; - if (payload.user_id !== user.user_id) return false; - return true; - }, - expectedOrigin: origin, - requireUserVerification: true, - }); - const { verified, registrationInfo } = verification; - - if (!verified || !registrationInfo) { - return c.text("Registration failed", 400); - } - - const { - credentialID, credentialPublicKey, - counter, credentialDeviceType, credentialBackedUp, - } = registrationInfo; - - // Base64URL encode ArrayBuffers. - const base64PublicKey = isoBase64URL.fromBuffer(credentialPublicKey); - - const newPasskey: Passkey = { - id: credentialID, - publicKey: base64PublicKey, - counter, - deviceType: credentialDeviceType, - backedUp: credentialBackedUp, - transports: credential?.response?.transports, - }; - - // Store the credential ID in the database - const { success } = await c.env.DB.prepare( - `INSERT INTO user_passkeys (user_id, passkey_name, passkey_id, passkey, counter) VALUES (?, ?, ?, ?, ?)` - ).bind(user.user_id, passkey_name, credentialID, JSON.stringify(newPasskey), counter).run(); - - return c.json({ success }); - }, - authenticateRequest: async (c: Context) => { - const { domain } = await c.req.json(); - const challenge = await Jwt.sign({ - domain, - iat: Math.floor(Date.now() / 1000), - }, c.env.JWT_SECRET, "HS256") - const options: PublicKeyCredentialRequestOptionsJSON = await generateAuthenticationOptions({ - rpID: domain, - challenge: challenge, - allowCredentials: [], - }); - return c.json(options); - }, - authenticateResponse: async (c: Context) => { - const { domain, credential, origin } = await c.req.json(); - const passkey_id = credential?.id; - if (!passkey_id) { - return c.text("Invalid request", 400); - } - const { user_id, counter, passkey } = await c.env.DB.prepare( - `SELECT user_id, counter, passkey FROM user_passkeys WHERE passkey_id = ?` - ).bind(passkey_id).first<{ - counter: number; passkey: string; user_id: number; - }>() || {}; - if (!passkey) { - return c.text("Passkey not found", 404); - } - const passkeyData = JSON.parse(passkey) as Passkey; - // Verify the registration response - const verification = await verifyAuthenticationResponse({ - response: credential, - expectedChallenge: async (challenge: string) => { - const payload = await Jwt.verify(atob(challenge), c.env.JWT_SECRET, "HS256"); - if (!payload || !payload.iat) return false; - // check iad is not older than 5 minutes - if (Math.floor(Date.now() / 1000) - payload.iat > 300) return false; - return true; - }, - expectedOrigin: origin, - expectedRPID: domain, - authenticator: { - credentialID: passkeyData.id, - credentialPublicKey: isoBase64URL.toBuffer(passkeyData.publicKey), - counter: counter || passkeyData.counter, - transports: passkeyData.transports, - }, - }); - const { verified, authenticationInfo } = verification; - if (!verified) { - return c.text("Authentication failed", 400); - } - - if (authenticationInfo) { - const { newCounter } = authenticationInfo; - // Update the counter in the database - await c.env.DB.prepare( - `UPDATE user_passkeys SET counter = ? WHERE passkey_id = ?` - ).bind(newCounter, passkey_id).run(); - } - // update passkey updated_at - await c.env.DB.prepare( - `UPDATE user_passkeys SET updated_at = datetime('now') WHERE passkey_id = ?` - ).bind(passkey_id).run(); - - // return jwt - const { user_email } = await c.env.DB.prepare( - `SELECT user_email FROM users WHERE id = ?` - ).bind(user_id).first<{ user_email: string }>() || {}; - if (!user_email) { - return c.text("User not found", 404); - } - // create jwt - const jwt = await Jwt.sign({ - user_email: user_email, - user_id: user_id, - // 90 days expire in seconds - exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60, - iat: Math.floor(Date.now() / 1000), - }, c.env.JWT_SECRET, "HS256") - return c.json({ - jwt: jwt - }) - }, -} diff --git a/cloudflare_temp_email/worker/src/user_api/settings.ts b/cloudflare_temp_email/worker/src/user_api/settings.ts deleted file mode 100644 index 4a59293..0000000 --- a/cloudflare_temp_email/worker/src/user_api/settings.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Context } from "hono"; - -import i18n from "../i18n"; -import { UserOauth2Settings, UserSettings } from "../models"; -import { getJsonSetting, getUserRoles } from "../utils" -import { CONSTANTS } from "../constants"; -import { commonGetUserRole } from "../common"; -import { Jwt } from "hono/utils/jwt"; - -export default { - openSettings: async (c: Context) => { - const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY); - const settings = new UserSettings(value); - const oauth2ClientIDs = [] as { clientID: string, name: string }[]; - try { - const oauth2Settings = await getJsonSetting(c, CONSTANTS.OAUTH2_SETTINGS_KEY); - oauth2ClientIDs.push( - ...oauth2Settings?.map(s => ({ - clientID: s.clientID, - name: s.name - })) || [] - ); - } catch (e) { - console.error("Failed to get oauth2 settings", e); - } - return c.json({ - enable: settings.enable, - enableMailVerify: settings.enableMailVerify, - oauth2ClientIDs: oauth2ClientIDs, - }) - }, - settings: async (c: Context) => { - const user = c.get("userPayload"); - const lang = c.get("lang") || c.env.DEFAULT_LANG; - const msgs = i18n.getMessages(lang); - // check if user exists - const db_user_id = await c.env.DB.prepare( - `SELECT id FROM users where id = ?` - ).bind(user.user_id).first("id"); - if (!db_user_id) { - return c.text(msgs.UserNotFoundMsg, 400); - } - const user_role = await commonGetUserRole(c, db_user_id); - const is_admin = ( - c.env.ADMIN_USER_ROLE - && - c.env.ADMIN_USER_ROLE === user_role?.role - ); - const access_token = user_role?.role ? await Jwt.sign({ - user_email: user.user_email, - user_id: user.user_id, - user_role: user_role.role, - iat: Math.floor(Date.now() / 1000), - // 1 hour - exp: Math.floor(Date.now() / 1000) + 3600, - }, c.env.JWT_SECRET, "HS256") : null; - // create new if expired in 7 days - const new_user_token = user.exp > ( - Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60 - ) ? null : await Jwt.sign({ - user_email: user.user_email, - user_id: user.user_id, - // 30 days expire in seconds - exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60, - iat: Math.floor(Date.now() / 1000), - }, c.env.JWT_SECRET, "HS256"); - // update address updated_at - try { - await c.env.DB.prepare( - `UPDATE address SET updated_at = datetime('now') where id IN ` - + `(SELECT address_id FROM users_address WHERE user_id = ?)` - ).bind(user.user_id).run(); - - } catch (e) { - console.warn("Failed to update address updated_at") - } - return c.json({ - ...user, - is_admin: is_admin, - access_token: access_token, - new_user_token: new_user_token, - user_role: user_role - }); - }, -} diff --git a/cloudflare_temp_email/worker/src/user_api/user.ts b/cloudflare_temp_email/worker/src/user_api/user.ts deleted file mode 100644 index 8492040..0000000 --- a/cloudflare_temp_email/worker/src/user_api/user.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { Context } from 'hono'; -import { Jwt } from 'hono/utils/jwt' - -import i18n from '../i18n'; -import { checkCfTurnstile, getJsonSetting, checkUserPassword, getUserRoles, getStringValue } from "../utils" -import { CONSTANTS } from "../constants"; -import { GeoData, UserInfo, UserSettings } from "../models"; -import { sendMail } from "../mails_api/send_mail_api"; - -export default { - verifyCode: async (c: Context) => { - const { email, cf_token } = await c.req.json(); - const lang = c.get("lang") || c.env.DEFAULT_LANG; - const msgs = i18n.getMessages(lang); - // check cf turnstile - try { - await checkCfTurnstile(c, cf_token); - } catch (error) { - return c.text(msgs.TurnstileCheckFailedMsg, 500) - } - const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY); - const settings = new UserSettings(value) - // check mail domain allow list - const mailDomain = email.split("@")[1]; - if (settings.enableMailAllowList - && settings.mailAllowList - && !settings.mailAllowList.includes(mailDomain) - ) { - return c.text(`${msgs.UserMailDomainMustInMsg} ${JSON.stringify(settings.mailAllowList, null, 2)}`, 400) - } - if (!settings.verifyMailSender) { - return c.text(msgs.VerifyMailSenderNotSetMsg, 400) - } - // check if code exists in KV - const tmpcode = await c.env.KV.get(`temp-mail:${email}`) - if (tmpcode) { - return c.text(msgs.CodeAlreadySentMsg, 400) - } - // generate code 6 digits and convert to string - const code = Math.floor(100000 + Math.random() * 900000).toString(); - // send code to email - try { - await sendMail(c, settings.verifyMailSender, { - from_name: "Temp Mail Verify", - to_name: '', - to_mail: email as string, - subject: "Temp Mail Verify code", - content: `Your verify code is ${code}`, - is_html: false, - }) - } catch (e) { - return c.text(`Failed to send verify code: ${(e as Error).message}`, 500) - } - // save to KV - await c.env.KV.put(`temp-mail:${email}`, code, { expirationTtl: 300 }); - return c.json({ - success: true, - expirationTtl: 300 - }) - }, - register: async (c: Context) => { - const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY); - const settings = new UserSettings(value) - const lang = c.get("lang") || c.env.DEFAULT_LANG; - const msgs = i18n.getMessages(lang); - // check enable - if (!settings.enable) { - return c.text(msgs.UserRegistrationDisabledMsg, 403); - } - // check request - const { email, password, code } = await c.req.json(); - if (!email || !password) { - return c.text(msgs.InvalidEmailOrPasswordMsg, 400) - } - checkUserPassword(password); - if (settings.enableMailVerify && !code) { - return c.text(msgs.InvalidVerifyCodeMsg, 400) - } - // check mail domain allow list - const mailDomain = email.split("@")[1]; - if (settings.enableMailAllowList - && settings.mailAllowList - && !settings.mailAllowList.includes(mailDomain) - ) { - return c.text(`${msgs.UserMailDomainMustInMsg} ${JSON.stringify(settings.mailAllowList, null, 2)}`, 400) - } - // check code - if (settings.enableMailVerify) { - const verifyCode = await c.env.KV.get(`temp-mail:${email}`) - if (verifyCode != code) { - return c.text(msgs.InvalidVerifyCodeMsg, 400) - } - } - // geo data - const reqIp = c.req.raw.headers.get("cf-connecting-ip") - const geoData = new GeoData(reqIp, c.req.raw.cf as any); - const userInfo = new UserInfo(geoData, email); - // if not enable mail verify, do not on conflict update - if (!settings.enableMailVerify) { - try { - const { success } = await c.env.DB.prepare( - `INSERT INTO users (user_email, password, user_info)` - + ` VALUES (?, ?, ?)` - ).bind( - email, password, JSON.stringify(userInfo) - ).run(); - if (!success) { - return c.text(msgs.FailedToRegisterMsg, 500) - } - } catch (e) { - const error = e as Error; - if (error.message && error.message.includes("UNIQUE")) { - return c.text(msgs.UserAlreadyExistsMsg, 400) - } - return c.text(`${msgs.FailedToRegisterMsg}: ${error.message}`, 500) - } - return c.json({ success: true }) - } - // if enable mail verify, on conflict update - const { success } = await c.env.DB.prepare( - `INSERT INTO users (user_email, password, user_info)` - + ` VALUES (?, ?, ?)` - + ` ON CONFLICT(user_email) DO UPDATE SET password = ?, user_info = ?, updated_at = datetime('now')` - ).bind( - email, password, JSON.stringify(userInfo), - password, JSON.stringify(userInfo) - ).run(); - if (!success) { - return c.text(msgs.FailedToRegisterMsg, 400); - } - const defaultRole = getStringValue(c.env.USER_DEFAULT_ROLE); - if (!defaultRole) return c.json({ success: true }) - const user_roles = getUserRoles(c); - if (!user_roles.find((r) => r.role === defaultRole)) { - return c.text(msgs.InvalidUserDefaultRoleMsg, 500); - } - // find user_id - const user_id = await c.env.DB.prepare( - `SELECT id FROM users where user_email = ?` - ).bind(email).first("id"); - if (!user_id) { - return c.text(msgs.UserNotFoundMsg, 500); - } - // update user roles - const { success: success2 } = await c.env.DB.prepare( - `INSERT INTO user_roles (user_id, role_text)` - + ` VALUES (?, ?)` - + ` ON CONFLICT(user_id) DO NOTHING` - ).bind(user_id, defaultRole).run(); - if (!success2) { - return c.text(msgs.FailedUpdateUserDefaultRoleMsg, 500); - } - return c.json({ success: true }) - }, - login: async (c: Context) => { - const { email, password } = await c.req.json(); - const lang = c.get("lang") || c.env.DEFAULT_LANG; - const msgs = i18n.getMessages(lang); - if (!email || !password) return c.text(msgs.InvalidEmailOrPasswordMsg, 400); - const { id: user_id, password: dbPassword } = await c.env.DB.prepare( - `SELECT id, password FROM users where user_email = ?` - ).bind(email).first() || {}; - if (!dbPassword) { - return c.text(msgs.UserNotFoundMsg, 400) - } - // TODO: need check password use random salt - if (dbPassword != password) { - return c.text(msgs.InvalidEmailOrPasswordMsg, 400) - } - // create jwt - const jwt = await Jwt.sign({ - user_email: email, - user_id: user_id, - // 90 days expire in seconds - exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60, - iat: Math.floor(Date.now() / 1000), - }, c.env.JWT_SECRET, "HS256") - return c.json({ - jwt: jwt - }) - }, -} diff --git a/cloudflare_temp_email/worker/src/user_api/user_mail_api.ts b/cloudflare_temp_email/worker/src/user_api/user_mail_api.ts deleted file mode 100644 index fd3534a..0000000 --- a/cloudflare_temp_email/worker/src/user_api/user_mail_api.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Context } from "hono"; -import { handleListQuery } from "../common"; -import UserBindAddressModule from "./bind_address"; - -export default { - getMails: async (c: Context) => { - const { user_id } = c.get("userPayload"); - const { address, limit, offset, keyword } = c.req.query(); - const bindedAddressList = await UserBindAddressModule.getBindedAddressListById(c, user_id); - const addressList = address ? bindedAddressList.filter((item) => item == address) : bindedAddressList; - const addressQuery = `address IN (${addressList.map(() => "?").join(",")})`; - const addressParams = addressList; - const keywordQuery = keyword ? `raw like ?` : ""; - const keywordParams = keyword ? [`%${keyword}%`] : []; - - // user must have at least one binded address to query mails - if (addressList.length <= 0) { - return c.json({ results: [], count: 0 }); - } - - const filterQuerys = [addressQuery, keywordQuery].filter((item) => item).join(" and "); - const finalQuery = filterQuerys.length > 0 ? `where ${filterQuerys}` : ""; - const filterParams = [...addressParams, ...keywordParams] - return await handleListQuery(c, - `SELECT * FROM raw_mails ${finalQuery}`, - `SELECT count(*) as count FROM raw_mails ${finalQuery}`, - filterParams, limit, offset - ); - }, - deleteMail: async (c: Context) => { - const { id } = c.req.param(); - const { user_id } = c.get("userPayload"); - const bindedAddressList = await UserBindAddressModule.getBindedAddressListById(c, user_id); - const { success } = await c.env.DB.prepare( - `DELETE FROM raw_mails WHERE id = ?` - + ` and address IN (${bindedAddressList.map(() => "?").join(",")})` - ).bind(id, ...bindedAddressList).run(); - return c.json({ - success: success - }) - } -} diff --git a/cloudflare_temp_email/worker/src/utils.ts b/cloudflare_temp_email/worker/src/utils.ts deleted file mode 100644 index 0dcfa7b..0000000 --- a/cloudflare_temp_email/worker/src/utils.ts +++ /dev/null @@ -1,321 +0,0 @@ -import { Context } from "hono"; -import { createMimeMessage } from "mimetext"; - -export const getJsonObjectValue = ( - value: string | any -): T | null => { - if (value == undefined || value == null) { - return null; - } - if (typeof value === "object") { - return value as T; - } - if (typeof value !== "string") { - return null; - } - try { - return JSON.parse(value) as T; - } catch (e) { - console.error(`GetJsonValue: Failed to parse ${value}`, e); - } - return null; -} - -export const getJsonSetting = async ( - c: Context, key: string -): Promise => { - const value = await getSetting(c, key); - if (!value) { - return null; - } - try { - return JSON.parse(value) as T; - } catch (e) { - console.error(`GetJsonSetting: Failed to parse ${key}`, e); - } - return null; -} - -export const getSetting = async ( - c: Context, key: string -): Promise => { - try { - const value = await c.env.DB.prepare( - `SELECT value FROM settings where key = ?` - ).bind(key).first("value"); - return value; - } catch (error) { - console.error(`GetSetting: Failed to get ${key}`, error); - } - return null; -} - -export const saveSetting = async ( - c: Context, - key: string, value: string -) => { - await c.env.DB.prepare( - `INSERT or REPLACE INTO settings (key, value) VALUES (?, ?)` - + ` ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = datetime('now')` - ).bind(key, value, value).run(); - return true; -} - -export const getStringValue = (value: any): string => { - if (typeof value === "string") { - return value; - } - return ""; -} - -export const getSplitStringListValue = ( - value: any, demiliter: string = "," -): string[] => { - const valueToSplit = getStringValue(value); - return valueToSplit.split(demiliter) - .map((item: string) => item.trim()) - .filter((item: string) => item.length > 0); -} - -export const getBooleanValue = ( - value: boolean | string | any -): boolean => { - if (typeof value === "boolean") { - return value; - } - if (typeof value === "string") { - return value === "true"; - } - return false; -} - -export const getIntValue = ( - value: number | string | any, - defaultValue: number = 0 -): number => { - if (typeof value === "number") { - return value; - } - if (typeof value === "string") { - try { - return parseInt(value); - } catch (e) { - console.error(`Failed to parse int value: ${value}`); - } - } - return defaultValue; -} - -export const getStringArray = ( - value: string | string[] | undefined | null -): string[] => { - if (!value) { - return []; - } - // check if value is an array, if not use json.parse - if (!Array.isArray(value)) { - try { - return JSON.parse(value); - } catch (e) { - console.error("Failed to parse value", e); - return []; - } - } - return value; -} - -export const getDefaultDomains = (c: Context): string[] => { - if (c.env.DEFAULT_DOMAINS == undefined || c.env.DEFAULT_DOMAINS == null) { - return getDomains(c); - } - const domains = getStringArray(c.env.DEFAULT_DOMAINS); - return domains || getDomains(c); -} - -export const getDomains = (c: Context): string[] => { - if (!c.env.DOMAINS) { - return []; - } - // check if DOMAINS is an array, if not use json.parse - if (!Array.isArray(c.env.DOMAINS)) { - try { - return JSON.parse(c.env.DOMAINS); - } catch (e) { - console.error("Failed to parse DOMAINS", e); - return []; - } - } - return c.env.DOMAINS; -} - -export const getUserRoles = (c: Context): UserRole[] => { - if (!c.env.USER_ROLES) { - return []; - } - // check if USER_ROLES is an array, if not use json.parse - if (!Array.isArray(c.env.USER_ROLES)) { - try { - return JSON.parse(c.env.USER_ROLES); - } catch (e) { - console.error("Failed to parse USER_ROLES", e); - return []; - } - } - return c.env.USER_ROLES; -} - -export const getAnotherWorkerList = (c: Context): AnotherWorker[] => { - if (!c.env.ANOTHER_WORKER_LIST) { - return []; - } - // check if ANOTHER_WORKER_LIST is an array, if not use json.parse - if (!Array.isArray(c.env.ANOTHER_WORKER_LIST)) { - try { - return JSON.parse(c.env.ANOTHER_WORKER_LIST); - } catch (e) { - console.error("Failed to parse ANOTHER_WORKER_LIST", e); - return []; - } - } - return c.env.ANOTHER_WORKER_LIST; -} - -export const getPasswords = (c: Context): string[] => { - if (!c.env.PASSWORDS) { - return []; - } - // check if PASSWORDS is an array, if not use json.parse - if (!Array.isArray(c.env.PASSWORDS)) { - try { - const res = JSON.parse(c.env.PASSWORDS) as string[]; - return res.filter((item) => item.length > 0); - } catch (e) { - console.error("Failed to parse PASSWORDS", e); - return []; - } - } - return c.env.PASSWORDS.filter((item) => item.length > 0); -} - -export const getAdminPasswords = (c: Context): string[] => { - if (!c.env.ADMIN_PASSWORDS) { - return []; - } - // check if ADMIN_PASSWORDS is an array, if not use json.parse - if (!Array.isArray(c.env.ADMIN_PASSWORDS)) { - try { - const res = JSON.parse(c.env.ADMIN_PASSWORDS) as string[]; - return res.filter((item) => item.length > 0); - } catch (e) { - console.error("Failed to parse ADMIN_PASSWORDS", e); - return []; - } - } - return c.env.ADMIN_PASSWORDS.filter((item) => item.length > 0); -} - -export const getEnvStringList = (value: string | string[] | undefined): string[] => { - if (!value) { - return []; - } - // check if is an array, if not use json.parse - if (!Array.isArray(value)) { - try { - const res = JSON.parse(value) as string[]; - return res.filter((item) => item.length > 0); - } catch (e) { - console.error("Failed to parse ADMIN_PASSWORDS", e); - return []; - } - } - return value.filter((item) => item.length > 0); -} - -export const sendAdminInternalMail = async ( - c: Context, toMail: string, subject: string, text: string -): Promise => { - try { - - const msg = createMimeMessage(); - msg.setSender({ - name: "Admin", - addr: "admin@internal" - }); - msg.setRecipient(toMail); - msg.setSubject(subject); - msg.addMessage({ - contentType: 'text/plain', - data: text - }); - const message_id = Math.random().toString(36).substring(2, 15); - const { success } = await c.env.DB.prepare( - `INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)` - ).bind( - "admin@internal", toMail, msg.asRaw(), message_id - ).run(); - if (!success) { - console.log(`Failed save message from admin@internal to ${toMail}`); - } - return success; - } catch (error) { - console.log("sendAdminInternalMail error", error); - return false; - } -}; - -export const checkCfTurnstile = async ( - c: Context, token: string | undefined | null -): Promise => { - if (!c.env.CF_TURNSTILE_SITE_KEY || !c.env.CF_TURNSTILE_SECRET_KEY) { - return; - } - if (!token) { - throw new Error("Captcha token is required"); - } - const reqIp = c.req.raw.headers.get("cf-connecting-ip"); - const formData = new FormData(); - formData.append('secret', c.env.CF_TURNSTILE_SECRET_KEY); - formData.append('response', token); - if (reqIp) formData.append('remoteip', reqIp); - const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'; - const result = await fetch(url, { - body: formData, - method: 'POST', - }); - const captchaRes: any = await result.json(); - if (!captchaRes.success) { - console.log("Captcha failed", captchaRes); - throw new Error("Captcha failed"); - } -} - -export const checkUserPassword = (password: string) => { - if (!password || password.length < 1 || password.length > 100) { - throw new Error("Invalid password") - } - return true; -} - -export default { - getJsonObjectValue, - getSetting, - saveSetting, - getStringValue, - getSplitStringListValue, - getBooleanValue, - getIntValue, - getStringArray, - getDefaultDomains, - getDomains, - getUserRoles, - getAnotherWorkerList, - getPasswords, - getAdminPasswords, - getEnvStringList, - sendAdminInternalMail, - checkCfTurnstile, - checkUserPassword, - getJsonSetting, - getJsonValue: getJsonObjectValue, - getStringList: getStringArray -} diff --git a/cloudflare_temp_email/worker/src/worker.ts b/cloudflare_temp_email/worker/src/worker.ts deleted file mode 100644 index 7a8413a..0000000 --- a/cloudflare_temp_email/worker/src/worker.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { Context, Hono } from 'hono' -import { cors } from 'hono/cors'; -import { jwt } from 'hono/jwt' -import { Jwt } from 'hono/utils/jwt' - -import { api as commonApi } from './commom_api'; -import { api as mailsApi } from './mails_api' -import { api as userApi } from './user_api'; -import { api as adminApi } from './admin_api'; -import { api as apiSendMail } from './mails_api/send_mail_api' -import { api as telegramApi } from './telegram_api' - -import i18n from './i18n'; -import { email } from './email'; -import { scheduled } from './scheduled'; -import { getAdminPasswords, getPasswords, getBooleanValue, getStringArray } from './utils'; - -const API_PATHS = [ - "/api/", - "/open_api/", - "/user_api/", - "/admin/", - "/telegram/", - "/external/", -]; - -const app = new Hono() -//cors -app.use('/*', cors()); -// error handler -app.onError((err, c) => { - console.error(err) - return c.text(`${err.name} ${err.message}`, 500) -}) -// global middlewares -app.use('/*', async (c, next) => { - - // check if the request is for static files - if (c.env.ASSETS && !API_PATHS.some(path => c.req.path.startsWith(path))) { - const url = new URL(c.req.raw.url); - if (!url.pathname.includes('.')) { - url.pathname = "" - } - return c.env.ASSETS.fetch(url); - } - - // save language in context - const lang = c.req.raw.headers.get("x-lang"); - if (lang) { c.set("lang", lang); } - const msgs = i18n.getMessages(lang || c.env.DEFAULT_LANG); - if ( - c.req.path.startsWith("/api/new_address") - || c.req.path.startsWith("/api/send_mail") - || c.req.path.startsWith("/external/api/send_mail") - || c.req.path.startsWith("/user_api/register") - || c.req.path.startsWith("/user_api/verify_code") - ) { - const reqIp = c.req.raw.headers.get("cf-connecting-ip") - if (reqIp && c.env.RATE_LIMITER) { - const { success } = await c.env.RATE_LIMITER.limit( - { key: `${c.req.path}|${reqIp}` } - ) - if (!success) { - return c.text(`IP=${reqIp} Rate limit exceeded for ${c.req.path}`, 429) - } - } - } - // webhook check - if ( - c.req.path.startsWith("/api/webhook") - || c.req.path.startsWith("/admin/webhook") - || c.req.path.startsWith("/admin/mail_webhook") - ) { - if (!c.env.KV) { - return c.text(msgs.KVNotAvailableMsg, 400); - } - if (!getBooleanValue(c.env.ENABLE_WEBHOOK)) { - return c.text(msgs.WebhookNotEnabledMsg, 403); - } - } - if (!c.env.DB) { - return c.text(msgs.DBNotAvailableMsg, 400); - } - if (!c.env.JWT_SECRET) { - return c.text(msgs.JWTSecretNotSetMsg, 400); - } - await next() -}); - -const checkUserPayload = async ( - c: Context -): Promise => { - try { - const token = c.req.raw.headers.get("x-user-token"); - if (!token) return; - const payload = await Jwt.verify(token, c.env.JWT_SECRET, "HS256"); - // check expired - if (!payload.exp) return; - // exp is in seconds - if (payload.exp < Math.floor(Date.now() / 1000)) { - return; - } - c.set("userPayload", payload as UserPayload); - } catch (e) { - console.error(e); - } -} - -const checkoutUserRolePayload = async ( - c: Context -): Promise => { - try { - const token = c.req.raw.headers.get("x-user-access-token"); - if (!token) return; - const payload = await Jwt.verify(token, c.env.JWT_SECRET, "HS256"); - // check expired - if (!payload.exp) return; - // exp is in seconds - if (payload.exp < Math.floor(Date.now() / 1000)) { - return; - } - if (typeof payload?.user_role !== "string") return; - c.set("userRolePayload", payload.user_role); - } catch (e) { - console.error(e); - } -} - -// api auth -app.use('/api/*', async (c, next) => { - // check header x-custom-auth - const passwords = getPasswords(c); - if (passwords && passwords.length > 0) { - const auth = c.req.raw.headers.get("x-custom-auth"); - if (!auth || !passwords.includes(auth)) { - const lang = c.req.raw.headers.get("x-lang") || c.env.DEFAULT_LANG; - const messages = i18n.getMessages(lang); - return c.text(messages.CustomAuthPasswordMsg, 401) - } - } - if (c.req.path.startsWith("/api/new_address")) { - await checkUserPayload(c); - await next(); - return; - } - if (c.req.path.startsWith("/api/settings") - || c.req.path.startsWith("/api/send_mail") - ) { - await checkoutUserRolePayload(c); - } - - try { - return await jwt({ secret: c.env.JWT_SECRET, alg: "HS256" })(c, next); - } catch (e) { - console.warn(e); - const lang = c.get("lang") || c.env.DEFAULT_LANG; - const msgs = i18n.getMessages(lang); - return c.text(msgs.InvalidAddressCredentialMsg, 401) - } -}); -// user_api auth -app.use('/user_api/*', async (c, next) => { - if ( - c.req.path.startsWith("/user_api/open_settings") - || c.req.path.startsWith("/user_api/register") - || c.req.path.startsWith("/user_api/login") - || c.req.path.startsWith("/user_api/verify_code") - || c.req.path.startsWith("/user_api/passkey/authenticate_") - || c.req.path.startsWith("/user_api/oauth2") - ) { - await next(); - return; - } - - const lang = c.req.raw.headers.get("x-lang") || c.env.DEFAULT_LANG; - const msgs = i18n.getMessages(lang); - - try { - const token = c.req.raw.headers.get("x-user-token"); - if (!token) return c.text(msgs.UserTokenExpiredMsg, 401) - const payload = await Jwt.verify(token, c.env.JWT_SECRET, "HS256"); - // check expired - if (!payload.exp) return c.text(msgs.UserTokenExpiredMsg, 401); - // exp is in seconds - if (payload.exp < Math.floor(Date.now() / 1000)) { - return c.text(msgs.UserTokenExpiredMsg, 401) - } - c.set("userPayload", payload as UserPayload); - } catch (e) { - console.error(e); - return c.text(msgs.UserTokenExpiredMsg, 401) - } - if (c.req.path.startsWith('/user_api/bind_address') - && c.req.method === 'POST' - ) { - return jwt({ secret: c.env.JWT_SECRET, alg: "HS256" })(c, next); - } - await next(); -}); -// admin auth -app.use('/admin/*', async (c, next) => { - - // check header x-admin-auth - const adminPasswords = getAdminPasswords(c); - if (adminPasswords && adminPasswords.length > 0) { - const adminAuth = c.req.raw.headers.get("x-admin-auth"); - if (adminAuth && adminPasswords.includes(adminAuth)) { - await next(); - return; - } - } - const lang = c.req.raw.headers.get("x-lang") || c.env.DEFAULT_LANG; - const msgs = i18n.getMessages(lang); - // check if user is admin - const access_token = c.req.raw.headers.get("x-user-access-token"); - if (c.env.ADMIN_USER_ROLE && access_token) { - try { - const payload = await Jwt.verify(access_token, c.env.JWT_SECRET, "HS256"); - // check expired - if (!payload.exp) return c.text(msgs.UserAcceesTokenExpiredMsg, 401); - // exp is in seconds - if (payload.exp < Math.floor(Date.now() / 1000)) { - return c.text(msgs.UserAcceesTokenExpiredMsg, 401) - } - if (payload.user_role !== c.env.ADMIN_USER_ROLE) { - return c.text(msgs.UserRoleIsNotAdminMsg, 401) - } - await next(); - return; - } catch (e) { - console.error(e); - } - } - - // disable admin api check - if (getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK)) { - await next(); - return; - } - - return c.text(msgs.NeedAdminPasswordMsg, 401) -}); - - -app.route('/', commonApi) -app.route('/', mailsApi) -app.route('/', userApi) -app.route('/', adminApi) -app.route('/', apiSendMail) -app.route('/', telegramApi) - -const health_check = async (c: Context) => { - const lang = c.req.raw.headers.get("x-lang") || c.env.DEFAULT_LANG; - const msgs = i18n.getMessages(lang); - if (!c.env.DB) { - return c.text(msgs.DBNotAvailableMsg, 400); - } - if (!c.env.JWT_SECRET) { - return c.text(msgs.JWTSecretNotSetMsg, 400); - } - if (getStringArray(c.env.DOMAINS).length === 0) { - return c.text(msgs.DomainsNotSetMsg, 400); - } - return c.text("OK"); -} - -app.get('/', health_check) -app.get('/health_check', health_check) -app.all('/*', async c => c.text("Not Found", 404)) - - -export default { - fetch: app.fetch, - email: email, - scheduled: scheduled, -} diff --git a/cloudflare_temp_email/worker/tsconfig.json b/cloudflare_temp_email/worker/tsconfig.json deleted file mode 100644 index ffafc90..0000000 --- a/cloudflare_temp_email/worker/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Bundler", - "strict": true, - "skipLibCheck": true, - "lib": [ - "ESNext" - ], - "types": [ - "@cloudflare/workers-types" - ] - }, -} diff --git a/cloudflare_temp_email/worker/wrangler.toml.template b/cloudflare_temp_email/worker/wrangler.toml.template deleted file mode 100644 index 32bdcc6..0000000 --- a/cloudflare_temp_email/worker/wrangler.toml.template +++ /dev/null @@ -1,144 +0,0 @@ -name = "cloudflare_temp_email" -main = "src/worker.ts" -compatibility_date = "2025-04-01" -compatibility_flags = [ "nodejs_compat" ] -# if you want use custom_domain, you need to add routes -# routes = [ -# { pattern = "temp-email-api.xxxxx.xyz", custom_domain = true }, -# ] - -# if you want deploy worker with frontend assets, you need to add assets -# [assets] -# directory = "../frontend/dist/" -# binding = "ASSETS" -# run_worker_first = true - -# enable cron if you want set auto clean up -# [triggers] -# crons = [ "0 0 * * *" ] - -# send_email = [ -# { name = "SEND_MAIL" }, -# ] - -[vars] -# DEFAULT_LANG = "zh" -# TITLE = "Custom Title" # custom title -# ANNOUNCEMENT = "Custom Announcement" -# always show ANNOUNCEMENT even no changes -# ALWAYS_SHOW_ANNOUNCEMENT = true -PREFIX = "tmp" -# address check REGEX, if not set, will not check -# ADDRESS_CHECK_REGEX = "^(?!.*admin).*" -# address name replace REGEX, if not set, the default is [^a-z0-9] -# ADDRESS_REGEX = "[^a-z0-9]" -# (min, max) length of the adderss, if not set, the default is (1, 30) -# MIN_ADDRESS_LEN = 1 -# MAX_ADDRESS_LEN = 30 -# Disable custom email address name, if set true, users cannot input custom email name, will auto generate -# DISABLE_CUSTOM_ADDRESS_NAME = true -# IF YOU WANT TO MAKE YOUR SITE PRIVATE, UNCOMMENT THE FOLLOWING LINES -# PASSWORDS = ["123", "456"] -# For admin panel -# ADMIN_PASSWORDS = ["123", "456"] -# warning: no password or user check for admin portal -# DISABLE_ADMIN_PASSWORD_CHECK = false -# ADMIN CONTACT, CAN BE ANY STRING -# ADMIN_CONTACT = "xx@xx.xxx" -# Create new address with default domain first, if set true, will use first domain from DEFAULT_DOMAINS when no domain specified -# CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST = false -DEFAULT_DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # domain name for no role users -DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # all domain names -# For chinese domain name, you can use DOMAIN_LABELS to show chinese domain name -# DOMAIN_LABELS = ["中文.xxx", "xxx.xxx2"] -# USER_DEFAULT_ROLE = "vip" # default role for new users(only when enable mail verification) -# ADMIN_USER_ROLE = "admin" # the role which can access admin panel -# User roles configuration, if domains is empty will use default_domains, if prefix is null will use default prefix, if prefix is empty string will not use prefix -# USER_ROLES = [ -# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "vip", prefix = "vip" }, -# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "admin", prefix = "" }, -# ] -JWT_SECRET = "xxx" -BLACK_LIST = "" -# Allow users to create email addresses -ENABLE_USER_CREATE_EMAIL = true -# Disable anonymous user create email, if set true, users can only create email addresses after logging in -# DISABLE_ANONYMOUS_USER_CREATE_EMAIL = true -# Allow users to delete messages -ENABLE_USER_DELETE_EMAIL = true -# Allow automatic replies to emails -ENABLE_AUTO_REPLY = false -# Allow webhook -# ENABLE_WEBHOOK = true -# Footer text -# COPYRIGHT = "Dream Hunter" -# DISABLE_SHOW_GITHUB = true -# default send balance, if not set, it will be 0 -# DEFAULT_SEND_BALANCE = 1 -# the role which can send emails without limit, multiple roles can be separated by , -# NO_LIMIT_SEND_ROLE = "vip" -# Turnstile verification -# CF_TURNSTILE_SITE_KEY = "" -# CF_TURNSTILE_SECRET_KEY = "" -# telegram bot -# TG_MAX_ADDRESS = 5 -# telegram bot info, predefined bot info can reduce latency of the webhook -# TG_BOT_INFO = "{}" -# global forward address list, if set, all emails will be forwarded to these addresses -# FORWARD_ADDRESS_LIST = ["xxx@xxx.com"] -# subdomain forward address list, if set, subdomain emails will be forwarded to these addresses -# SUBDOMAIN_FORWARD_ADDRESS_LIST = """ -# [ -# {"domains":[""],"forward":"xxx1@xxx.com"}, -# {"domains":["subdomain-1.domain.com","subdomain-2.domain.com"],"forward":"xxx2@xxx.com"} -# ] -# """ -# Frontend URL -# FRONTEND_URL = "https://xxxx.xxx" -# Enable check junk mail -# ENABLE_CHECK_JUNK_MAIL = false -# junk mail check list, if status exists and status is not pass, will be marked as junk mail -# JUNK_MAIL_CHECK_LIST = = ["spf", "dkim", "dmarc"] -# junk mail force check pass list, if no status or status is not pass, will be marked as junk mail -# JUNK_MAIL_FORCE_PASS_LIST = ["spf", "dkim", "dmarc"] -# remove attachment if size exceed 2MB, mail maybe mising some information due to parsing -# REMOVE_EXCEED_SIZE_ATTACHMENT = true -# remove all attachment, mail maybe mising some information due to parsing -# REMOVE_ALL_ATTACHMENT = true -# Calling other woker to process email -# ENABLE_ANOTHER_WORKER = false -# ANOTHER_WORKER_LIST = """ -# [ -# { -# "binding":"AUTH_INBOX", -# "method":"rpcEmail", -# "keywords":[ -# "验证码","激活码","激活链接","确认链接","验证邮箱","确认邮件","账号激活","邮件验证","账户确认","安全码","认证码","安全验证","登陆码","确认码","启用账户","激活账户","账号验证","注册确认", -# "account","activation","verify","verification","activate","confirmation","email","code","validate","registration","login","code","expire","confirm" -# ] -# } -# ] -# """ - -[[d1_databases]] -binding = "DB" -database_name = "xxx" -database_id = "xxx" - -# kv config for send email verification code -# [[kv_namespaces]] -# binding = "KV" -# id = "xxxx" - -# ratelimit config for /api/new_address -# [[unsafe.bindings]] -# name = "RATE_LIMITER" -# type = "ratelimit" -# namespace_id = "1001" -# # 10 requests per minute -# simple = { limit = 10, period = 60 } - -# binding another worker service (parse the code or link), e.g. auth-inbox -# [[services]] -# binding = "AUTH_INBOX" -# service = "auth-inbox" diff --git a/db/schema.sql b/db/schema.sql index 17be067..a437272 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -19,6 +19,8 @@ CREATE TABLE IF NOT EXISTS users ( deleted_at DATETIME, -- 删除时间(软删除) webhook_url TEXT CHECK(webhook_url IS NULL OR webhook_url LIKE 'http%'), -- webhook地址必须是有效URL webhook_secret TEXT, -- webhook签名密钥 + webhook_type TEXT DEFAULT 'custom' CHECK(webhook_type IN ('dingtalk', 'feishu', 'bark', 'custom')), -- webhook类型 + webhook_custom_message TEXT, -- 自定义消息模板,支持变量:{{from}}, {{to}}, {{subject}}, {{content}}, {{received_at}}, {{attachment_count}} created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); @@ -43,49 +45,78 @@ CREATE TABLE IF NOT EXISTS forward_rules ( sender_filter TEXT, keyword_filter TEXT, recipient_filter TEXT, - webhook_url TEXT NOT NULL, - webhook_secret TEXT, - webhook_type TEXT DEFAULT 'custom', + exact_match INTEGER DEFAULT 0 CHECK(exact_match IN (0,1)), -- 是否精确匹配(0=包含匹配,1=精确匹配) + skip_default_webhook INTEGER DEFAULT 0 CHECK(skip_default_webhook IN (0,1)), -- 是否跳过默认推送渠道(0=不跳过,1=跳过) enabled INTEGER DEFAULT 1 CHECK(enabled IN (0,1)), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_forward_rules_enabled ON forward_rules(enabled); -CREATE INDEX IF NOT EXISTS idx_forward_rules_webhook_type ON forward_rules(webhook_type); + +-- 转发规则 Webhook 配置表(支持一个规则多个 webhook) +CREATE TABLE IF NOT EXISTS forward_rule_webhooks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + rule_id INTEGER NOT NULL, + webhook_url TEXT NOT NULL CHECK(webhook_url LIKE 'http%'), + webhook_secret TEXT, + webhook_type TEXT DEFAULT 'custom' CHECK(webhook_type IN ('dingtalk', 'feishu', 'bark', 'custom')), + custom_message TEXT, -- 自定义消息模板,支持变量:{{from}}, {{to}}, {{subject}}, {{content}}, {{received_at}}, {{attachment_count}} + enabled INTEGER DEFAULT 1 CHECK(enabled IN (0,1)), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (rule_id) REFERENCES forward_rules(id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_forward_rule_webhooks_rule_id ON forward_rule_webhooks(rule_id); +CREATE INDEX IF NOT EXISTS idx_forward_rule_webhooks_enabled ON forward_rule_webhooks(enabled); +CREATE INDEX IF NOT EXISTS idx_forward_rule_webhooks_webhook_type ON forward_rule_webhooks(webhook_type); -- 邮件表 +-- 优化后的结构:所有基础数据从 message.raw 直接提取并存入数据库 +-- +-- ID 字段说明: +-- - id: 邮件在数据库中的主键,使用 crypto.randomUUID() 生成的 UUID +-- 格式如 "550e8400-e29b-41d4-a716-446655440000"(标准 UUID v4 格式) +-- R2 文件路径为 email:{id}.eml(剔除附件后的完整原始邮件) +-- +-- 注意: +-- 1. 所有基础数据从 message.raw 直接提取并存入数据库 +-- 2. emailId 使用 crypto.randomUUID() 生成,不再使用 Message-ID 的哈希值 +-- 3. 当前使用 TEXT 类型作为主键,UUID 保证 ID 的唯一性 CREATE TABLE IF NOT EXISTS emails ( - id TEXT PRIMARY KEY, - message_id TEXT, - user_id INTEGER, -- 添加用户ID字段 - subject TEXT NOT NULL, - from_address TEXT NOT NULL, - to_address TEXT NOT NULL, - sender_email TEXT, -- 添加发送者邮箱字段 - recipient_email TEXT, -- 添加接收者邮箱字段 - reply_to TEXT, - cc TEXT, - bcc TEXT, - content_type TEXT DEFAULT 'text/plain', - content TEXT, - raw_content TEXT, + id TEXT PRIMARY KEY, -- 邮件ID(数据库主键),使用 crypto.randomUUID() 生成的 UUID + user_id INTEGER, -- 用户ID(单用户模式下,默认为管理员用户ID) + subject TEXT, -- 主题 + from_address TEXT, -- 发件人 + to_address TEXT, -- 收件人 + content TEXT, -- 内容概览/预览(用于快速查看,完整内容在 R2) is_read INTEGER DEFAULT 0 CHECK(is_read IN (0,1)), - has_attachments INTEGER DEFAULT 0 CHECK(has_attachments IN (0,1)), - size_bytes INTEGER DEFAULT 0, - received_at DATETIME DEFAULT CURRENT_TIMESTAMP, -- 添加接收时间字段 + attachment_count INTEGER DEFAULT 0, -- 附件数量(0表示无附件) + message_id TEXT, -- 原始邮件头中的 Message-ID(从 message.raw 提取) + headers_json TEXT, -- 完整的邮件头信息(JSON 格式,包含 DKIM、SPF 等,从 message.raw 提取) + size_bytes INTEGER, -- 剔除附件后的邮件大小(字节) + date TEXT, -- 邮件日期(从 headers 提取) + reply_to TEXT, -- 回复地址(从 headers 提取) + cc TEXT, -- 抄送地址(从 headers 提取) + bcc TEXT, -- 密送地址(从 headers 提取) + content_type TEXT, -- 邮件内容类型(从 headers 提取) + received_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); +-- 注意:以下信息从 message.raw 直接提取并存入数据库,不再使用 .meta.json: +-- - message_id: 原始邮件头中的 Message-ID +-- - headers_json: 完整的 headers(JSON 格式) +-- - size_bytes: 剔除附件后的大小 +-- - date/reply_to/cc/bcc/content_type: 从 headers 提取 CREATE INDEX IF NOT EXISTS idx_emails_from_address ON emails(from_address); CREATE INDEX IF NOT EXISTS idx_emails_to_address ON emails(to_address); CREATE INDEX IF NOT EXISTS idx_emails_subject ON emails(subject); CREATE INDEX IF NOT EXISTS idx_emails_is_read ON emails(is_read); CREATE INDEX IF NOT EXISTS idx_emails_user_id ON emails(user_id); -CREATE INDEX IF NOT EXISTS idx_emails_sender_email ON emails(sender_email); -CREATE INDEX IF NOT EXISTS idx_emails_recipient_email ON emails(recipient_email); CREATE INDEX IF NOT EXISTS idx_emails_received_at ON emails(received_at); CREATE INDEX IF NOT EXISTS idx_emails_created_at ON emails(created_at); @@ -151,24 +182,6 @@ CREATE INDEX IF NOT EXISTS idx_security_audit_attack_type ON security_audit(atta CREATE INDEX IF NOT EXISTS idx_security_audit_created_at ON security_audit(created_at); CREATE INDEX IF NOT EXISTS idx_security_audit_request_ip ON security_audit(request_ip); --- 用户Webhook配置表 -CREATE TABLE IF NOT EXISTS user_webhooks ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, -- 用户ID - webhook_name TEXT NOT NULL, -- Webhook名称 - webhook_url TEXT NOT NULL, -- Webhook URL - webhook_secret TEXT, -- Webhook密钥 - webhook_type TEXT DEFAULT 'custom', -- Webhook类型 - enabled INTEGER DEFAULT 1 CHECK(enabled IN (0,1)), -- 是否启用 - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS idx_user_webhooks_user_id ON user_webhooks(user_id); -CREATE INDEX IF NOT EXISTS idx_user_webhooks_enabled ON user_webhooks(enabled); -CREATE INDEX IF NOT EXISTS idx_user_webhooks_webhook_type ON user_webhooks(webhook_type); - -- =================== -- 依赖多个表的表 -- =================== @@ -194,12 +207,13 @@ CREATE INDEX IF NOT EXISTS idx_mailbox_history_created_at ON mailbox_history(cre -- 附件表 CREATE TABLE IF NOT EXISTS attachments ( - id TEXT PRIMARY KEY, + id TEXT PRIMARY KEY, -- 附件ID,使用 crypto.randomUUID() 生成的 UUID email_id TEXT NOT NULL, -- 邮件ID filename TEXT NOT NULL, -- 文件名 content_type TEXT, -- 内容类型 size_bytes INTEGER DEFAULT 0, -- 文件大小(字节) r2_key TEXT NOT NULL, -- R2存储键 + content_id TEXT, -- Content-ID(用于内嵌图片,如 cid:xxx) created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (email_id) REFERENCES emails(id) ON DELETE CASCADE @@ -280,12 +294,12 @@ BEGIN UPDATE forward_rules SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END; -CREATE TRIGGER update_user_webhooks_updated_at - AFTER UPDATE ON user_webhooks +CREATE TRIGGER update_forward_rule_webhooks_updated_at + AFTER UPDATE ON forward_rule_webhooks FOR EACH ROW WHEN NEW.updated_at = OLD.updated_at BEGIN - UPDATE user_webhooks SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; + UPDATE forward_rule_webhooks SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END; CREATE TRIGGER update_system_settings_updated_at @@ -311,17 +325,14 @@ END; -- 插入默认系统配置 INSERT OR IGNORE INTO system_settings (key, value, description) VALUES ('allow_registration', '0', '是否允许用户自由注册 (1=是, 0=否)'), -('cleanup_days', '7', '邮件自动清理天数'), +('mail_retention_days', '7', '邮件保留天数'), +('attachment_retention_days', '7', '附件保留天数'), ('max_attachment_size', '52428800', '最大附件大小(50MB)'), -('domain', 'example.com', '邮件域名'), -('admin_email', 'admin@example.com', '管理员邮箱'), -('primary_domain', 'example.com', '主域名'), -('cookie_max_age', '604800', 'Cookie过期时间(秒)'), -('debug_mode', '0', '调试模式开关 (1=开启, 0=关闭)'), -('domains', '["example.com"]', '支持的域名列表(JSON格式)'), -('auto_approve_mailbox', '0', '是否自动批准邮箱申请 (1=是, 0=否)'), -('reserved_mailboxes', '["admin","administrator","root","postmaster","abuse","noreply","no-reply","support","info","contact","webmaster","mail","email","help","security","privacy","legal","billing","sales","marketing","news","newsletter","updates","alerts","notifications"]', '保留邮箱列表(JSON格式)'), -('max_mailboxes_per_user', '5', '每个用户最大邮箱数量'); +('cookie_max_age', '172800', 'Cookie过期时间(秒,48小时)'), +('debug_mode', '1', '调试模式开关 (1=开启, 0=关闭)'), +('api_rate_limit', '0', 'API访问频率限制开关 (1=启用, 0=禁用)'), +('api_rate_limit_max_requests', '100', '每分钟最大请求数(10-10000)'), +('supported_domains', '["example.com", "doubi.tech"]', '支持的域名列表(JSON格式,用于匹配接收的邮件域名)'); -- 插入默认管理员用户(密码:123456,已哈希) INSERT OR IGNORE INTO users (username, password, user_type, status) VALUES @@ -339,6 +350,18 @@ INSERT OR IGNORE INTO mailboxes (owner_id, address, status) VALUES INSERT OR IGNORE INTO mailboxes (owner_id, address, status) VALUES (2, 'test@example.com', 1); +-- 插入默认转发规则(未启用,仅作示例) +INSERT OR IGNORE INTO forward_rules (rule_name, sender_filter, keyword_filter, recipient_filter, exact_match, skip_default_webhook, enabled) VALUES +('飞书推送示例', NULL, NULL, NULL, 0, 0, 0), +('钉钉推送示例', NULL, NULL, NULL, 0, 0, 0), +('Bark推送示例', NULL, NULL, NULL, 0, 0, 0); + +-- 为转发规则添加 Webhook 配置(未启用,仅作示例) +INSERT OR IGNORE INTO forward_rule_webhooks (rule_id, webhook_url, webhook_secret, webhook_type, custom_message, enabled) VALUES +(1, 'https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', NULL, 'feishu', '{"msg_type":"interactive","card":{"header":{"template":"blue","title":{"content":"📧 新邮件通知","tag":"plain_text"}},"elements":[{"tag":"div","fields":[{"is_short":true,"text":{"tag":"lark_md","content":"**发件人:**{{from}}"}},{"is_short":true,"text":{"tag":"lark_md","content":"**收件人:**{{to}}"}},{"is_short":true,"text":{"tag":"lark_md","content":"**主题:**{{subject}}"}},{"is_short":true,"text":{"tag":"lark_md","content":"**附件数:**{{attachment_count}}"}}]},{"tag":"div","text":{"tag":"lark_md","content":"**内容:**\n{{content}}"}}]}}', 0), +(2, 'https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', NULL, 'dingtalk', '{"msgtype":"markdown","markdown":{"title":"新邮件通知","text":"## 📧 新邮件通知\n\n**发件人:**{{from}}\n\n**收件人:**{{to}}\n\n**主题:**{{subject}}\n\n**附件数:**{{attachment_count}}\n\n**内容:**\n{{content}}"}}', 0), +(3, 'https://api.day.app/xxxxxxxxxxxxxxxxxxxxxxxx', NULL, 'bark', '{"title":"新邮件通知","body":"发件人:{{from}}\n收件人:{{to}}\n主题:{{subject}}\n内容:{{content}}","group":"email","icon":"https://example.com/icon.png"}', 0); + -- =================== -- 注意:sqlite_sequence 会在第一次插入带 AUTOINCREMENT 的表时自动更新 -- 不需要手动插入,SQLite 会自动管理 diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 16cb4db..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,45 +0,0 @@ -services: - # vue: - # network_mode: host - # container_name: "vue" - # image: node:cem - # build: - # context: . - # tags: - # - node:cem - # restart: on-failure:3 - # working_dir: "$PWD/vue" - # entrypoint: ["npx", "vite", "--host"] - # volumes: - # - "node_modules:${PWD}/node_modules" - # - "${PWD}/vue:${PWD}/vue" - # - "${PWD}/package.json:${PWD}/package.json" - # - "${PWD}/tsconfig.json:${PWD}/tsconfig.json" - - worker: - network_mode: host - container_name: "worker" - image: node:cem - build: - context: . - tags: - - node:cem - restart: on-failure:3 - tty: true - stdin_open: true - working_dir: "$PWD" - entrypoint: ["npx", "wrangler", "dev"] - volumes: - - "${PWD}/.config:/root/.config" - - "${PWD}/db:${PWD}/db" - - "${PWD}/vue/dist:${PWD}/vue/dist" - - "${PWD}/scripts:${PWD}/scripts" - - "node_modules:${PWD}/node_modules" - - "${PWD}/worker:${PWD}/worker" - - "${PWD}/wrangler.toml:${PWD}/wrangler.toml" - - "${PWD}/package.json:${PWD}/package.json" - - "${PWD}/tsconfig.json:${PWD}/tsconfig.json" - -volumes: - node_modules: - name: node_modules diff --git a/docs/CACHE_API.md b/docs/CACHE_API.md deleted file mode 100644 index 9b68034..0000000 --- a/docs/CACHE_API.md +++ /dev/null @@ -1,179 +0,0 @@ -# 缓存接口规范文档 - -> **📝 文档说明**:本文档仅提供函数名和规范,不包含具体代码实现。具体实现请参考源代码。 - -## 📋 概述 - -本文档定义了邮箱管理系统的统一缓存接口规范,包括前端缓存策略、后端缓存实现和Workers KV集成方案。 - -> **🔄 重要更新**:系统已统一使用 localStorage 进行缓存,不再使用 sessionStorage。所有缓存数据将持久化存储,确保系统配置和用户数据的持久性。 - -## 🏗️ 架构设计 - -### 缓存层级 - -``` -┌─────────────────────────────────────────┐ -│ 用户请求 │ -└─────────────────┬───────────────────────┘ - │ -┌─────────────────▼───────────────────────┐ -│ 前端缓存层 (LocalStorage) │ -│ • 用户会话数据 │ -│ • 系统配置信息 │ -│ • 持久化存储,TTL: 5-30分钟 │ -└─────────────────┬───────────────────────┘ - │ -┌─────────────────▼───────────────────────┐ -│ Workers KV 缓存层 │ -│ • 系统配置 │ -│ • 用户信息 │ -│ • 全局缓存数据 │ -│ • TTL: 1-24小时 │ -└─────────────────┬───────────────────────┘ - │ -┌─────────────────▼───────────────────────┐ -│ 数据库层 (D1) │ -│ • 持久化数据 │ -│ • 配置变更记录 │ -│ • 最终数据源 │ -└─────────────────────────────────────────┘ -``` - -## 🏥 系统健康检查 - -### 接口方法 - -- `GET /api/system/health` - 获取系统健康状态 - -### 响应格式 - -- **成功**:返回系统状态、服务检查结果、配置信息 -- **失败**:返回错误状态和服务异常信息 -- **数据格式**:使用数字格式(1=正常/是,0=异常/否) - -### 检查项目 - -- 数据库连接状态和延迟 -- R2存储服务状态 -- KV存储服务状态 -- 系统配置信息(仅数据库正常时) - -## 🔧 前端缓存接口 - -### 缓存管理器函数 - -- `set()` - 设置缓存 -- `get()` - 获取缓存 -- `delete()` - 删除缓存 -- `clear()` - 清空所有缓存 -- `isValid()` - 检查缓存是否有效 -- `getCacheInfo()` - 获取缓存信息 -- `cleanExpired()` - 清理过期缓存 - -### 业务缓存函数 - -- `getSystemConfig()` - 获取系统配置 -- `getRegistrationStatus()` - 获取注册状态 -- `getUserInfo()` - 获取用户信息 -- `updateUserInfo()` - 更新用户信息 -- `getPublicInfo()` - 获取公开系统信息 -- `getSystemHealth()` - 获取系统健康状态 -- `isSystemHealthy()` - 检查系统是否健康 - -### 缓存键命名规范 - -- `system:config` - 系统配置 -- `system:registration` - 注册状态 -- `system:debug` - 调试模式 -- `user:info` - 用户信息 -- `user:settings` - 用户设置 -- `emails:list` - 邮件列表 -- `mailboxes:list` - 邮箱列表 -- `forward:rules` - 转发规则 -- `webhook:config` - Webhook配置 - -### TTL配置 - -- `SHORT: 5分钟` - 短期缓存 -- `MEDIUM: 30分钟` - 中期缓存 -- `LONG: 2小时` - 长期缓存 -- `VERY_LONG: 24小时` - 超长期缓存 - -## 🔧 后端缓存接口 - -### Workers KV 缓存服务函数 - -- `set()` - 设置缓存 -- `get()` - 获取缓存 -- `delete()` - 删除缓存 -- `clear()` - 清空所有缓存 - -### 业务缓存函数 - -- `setSystemConfig()` - 设置系统配置 -- `getSystemConfig()` - 获取系统配置 -- `setRegistrationStatus()` - 设置注册状态 -- `getRegistrationStatus()` - 获取注册状态 -- `setUserInfo()` - 设置用户信息 -- `getUserInfo()` - 获取用户信息 - -### 缓存策略服务函数 - -- `getSystemConfigWithCache()` - 获取系统配置(带缓存) -- `getUserInfoWithCache()` - 获取用户信息(带缓存) -- `updateUserInfoWithCache()` - 更新用户信息(带缓存) - -### 缓存管理函数 - -- `refreshConfig()` - 刷新配置缓存 -- `clearSystemCache()` - 清理系统缓存 -- `warmupCache()` - 预热缓存 -- `getCacheStatus()` - 获取缓存状态 -- `getCacheStats()` - 获取缓存统计 - -## 🚀 部署配置 - -### wrangler.toml 配置 - -```toml -# Workers KV 配置 -[[kv_namespaces]] -binding = "KV" -id = "your-kv-namespace-id" -preview_id = "your-preview-kv-namespace-id" - -# 环境变量接口 -interface Env { - DB: D1Database - R2: R2Bucket - KV: KVNamespace # Workers KV 存储 - ASSETS: Fetcher # 静态资源绑定 -} -``` - -### 环境变量 - -- `CACHE_TTL_SHORT = "300"` - 5分钟 -- `CACHE_TTL_MEDIUM = "1800"` - 30分钟 -- `CACHE_TTL_LONG = "7200"` - 2小时 -- `CACHE_TTL_VERY_LONG = "86400"` - 24小时 - -## 📊 监控和调试 - -### 缓存状态查询 - -- `GET /api/cache/status` - 获取缓存状态 -- `GET /api/cache/stats` - 获取缓存统计 - -### 缓存管理 - -- `POST /api/cache/clear` - 清理缓存 -- `POST /api/cache/warmup` - 预热缓存 - -### 调试信息 - -- 缓存命中率 -- 内存使用情况 -- 过期清理统计 -- 错误日志记录 \ No newline at end of file diff --git a/docs/ENVIRONMENT_CONFIG.md b/docs/ENVIRONMENT_CONFIG.md deleted file mode 100644 index 4fdb013..0000000 --- a/docs/ENVIRONMENT_CONFIG.md +++ /dev/null @@ -1,61 +0,0 @@ -# 环境配置说明 - -## Cloudflare Workers 环境配置 - -### 命令对比 - -| 命令 | 用途 | 使用的配置段 | 连接的环境 | -|------|------|-------------|-----------| -| `wrangler dev` | 本地开发服务器 | `[dev]` + 主配置 | 生产环境资源 | -| `wrangler dev --env dev` | 本地开发服务器 | `[dev]` + `[env.dev]` | 开发环境资源 | -| `wrangler deploy` | 部署到云端 | 主配置 | 生产环境 | -| `wrangler deploy --env dev` | 部署到云端 | `[env.dev]` | 开发环境 | - -### 推荐使用方式 - -#### 开发阶段 -```bash -# 使用开发环境资源进行本地开发 -wrangler dev --env dev -``` - -#### 生产部署 -```bash -# 部署到生产环境 -wrangler deploy -``` - -#### 开发环境部署 -```bash -# 部署到开发环境(用于测试) -wrangler deploy --env dev -``` - -### 环境隔离 - -- **生产环境**:使用 `cem-db`、`cem-r2`、`cem-kv` 等资源 -- **开发环境**:使用 `cem-db-dev`、`cem-r2-dev`、`cem-kv-dev` 等资源 - -### 配置结构说明 - -#### 全局配置段 -- **`[dev]`**:本地开发服务器配置 (IP 和端口) -- **`[triggers]`**:定时任务配置 -- **`[observability]`**:可观测性配置 -- **`[assets]`**:静态资源绑定配置 - -#### 环境特定配置段 -- **`[env.dev]`**:开发环境的资源绑定配置 - - 用于 `wrangler dev --env dev` 和 `wrangler deploy --env dev` 命令 - - 包含独立的 D1、R2、KV 资源绑定 - -#### 配置优先级 -1. 全局配置段适用于所有环境 -2. `[env.dev]` 段会覆盖对应的全局配置 -3. 本地开发服务器配置 (`[dev]`) 始终使用,不受环境影响 - -### 注意事项 - -1. 开发环境资源需要单独创建 -2. 使用 `npm run deploy` 脚本可以自动创建和管理环境资源 -3. 开发环境资源与生产环境完全隔离,避免数据污染 diff --git a/docs/MAILBOX_STATUS.md b/docs/MAILBOX_STATUS.md deleted file mode 100644 index af385b2..0000000 --- a/docs/MAILBOX_STATUS.md +++ /dev/null @@ -1,107 +0,0 @@ -# 邮箱状态说明文档 - -## 📋 邮箱状态定义 - -### **active(正常)** -- **含义**:邮箱处于正常使用状态 -- **权限**:可以接收邮件,用户可以正常使用 -- **操作**:用户可以删除邮箱,管理员可以停用邮箱 -- **显示**:绿色标签显示"正常" - -### **disabled(停用)** -- **含义**:被管理员停用/封禁的邮箱 -- **权限**:不能接收邮件,用户无法使用 -- **操作**:只有管理员可以重新激活 -- **限制**:该邮箱地址不能被其他用户申请 -- **显示**:红色标签显示"停用" - -### **deleted(已删除)** -- **含义**:用户主动删除的邮箱 -- **权限**:不能接收邮件,用户无法使用 -- **操作**:用户可以重新申请该邮箱地址 -- **限制**:该邮箱地址可以被其他用户申请 -- **显示**:橙色标签显示"已删除" - -## 🔄 状态转换流程 - -### **用户操作流程** -``` -active → deleted(用户删除邮箱) -``` - -### **管理员操作流程** -``` -active → disabled(管理员停用邮箱) -disabled → active(管理员重新激活邮箱) -``` - -### **邮箱重新申请流程** -``` -deleted → active(用户重新申请已删除的邮箱) -``` - -## 📊 状态对比表 - -| 状态 | 接收邮件 | 用户可见 | 可重新申请 | 管理员操作 | 用户操作 | -|------|----------|----------|------------|------------|----------| -| active | ✅ | ✅ | ❌ | 停用 | 删除 | -| disabled | ❌ | ❌ | ❌ | 激活 | ❌ | -| deleted | ❌ | ❌ | ✅ | ❌ | 重新申请 | - -## 🎯 业务场景 - -### **场景1:用户删除邮箱** -1. 用户删除邮箱 `user@example.com` -2. 邮箱状态变为 `deleted` -3. 用户或其他用户可以重新申请 `user@example.com` -4. 重新申请后状态变为 `active` - -### **场景2:管理员封禁邮箱** -1. 管理员发现邮箱 `spam@example.com` 发送垃圾邮件 -2. 管理员将邮箱状态设为 `disabled` -3. 该邮箱地址永久不可用,不能被重新申请 -4. 只有管理员可以重新激活 - -### **场景3:邮箱查询逻辑** -- **用户查询**:只显示 `active` 状态的邮箱 -- **管理员查询**:显示所有状态的邮箱 -- **邮箱申请**:检查 `active` 和 `disabled` 状态,允许 `deleted` 状态重新申请 - -## 🔧 技术实现 - -### **数据库约束** -```sql -status TEXT DEFAULT 'active' CHECK(status IN ('active','disabled','deleted')) -``` - -### **查询逻辑** -```sql --- 用户查询邮箱(只显示 active) -SELECT * FROM mailboxes WHERE status = 'active' AND owner_id = ? - --- 管理员查询邮箱(显示所有状态) -SELECT * FROM mailboxes WHERE owner_id = ? - --- 检查邮箱是否可用(排除 deleted) -SELECT * FROM mailboxes WHERE address = ? AND status IN ('active', 'disabled') -``` - -### **状态更新** -```sql --- 用户删除邮箱 -UPDATE mailboxes SET status = 'deleted' WHERE id = ? - --- 管理员停用邮箱 -UPDATE mailboxes SET status = 'disabled' WHERE id = ? - --- 重新激活邮箱 -UPDATE mailboxes SET status = 'active', owner_id = ? WHERE address = ? -``` - -## 📝 注意事项 - -1. **数据一致性**:删除邮箱时邮件不会消失,仍然属于用户 -2. **权限控制**:只有管理员可以操作 `disabled` 状态 -3. **邮箱唯一性**:`active` 和 `disabled` 状态的邮箱地址唯一,`deleted` 状态可以重新申请 -4. **状态显示**:前端需要根据状态显示不同的标签和操作按钮 -5. **API 响应**:API 需要根据用户权限返回不同状态的邮箱列表 diff --git a/docs/PERFORMANCE_ANALYSIS.md b/docs/PERFORMANCE_ANALYSIS.md deleted file mode 100644 index 4925a06..0000000 --- a/docs/PERFORMANCE_ANALYSIS.md +++ /dev/null @@ -1,183 +0,0 @@ -# 邮箱历史记录性能分析 - -## 查询策略对比 - -### 1. 循环查询(N+1 问题) - -```typescript -// 每个历史记录都查询一次 -for (const record of history) { - const user = await db.prepare(`SELECT username FROM users WHERE id = ?`).bind(record.user_id).first(); - const owner = await db.prepare(`SELECT username FROM users WHERE id = ?`).bind(record.owner_id).first(); -} -``` - -**内存消耗**: -- 每次查询创建新的数据库连接 -- 大量临时对象和结果集 -- 内存碎片化严重 - -**性能问题**: -- N+1 查询问题 -- 数据库连接池压力大 -- 响应时间随记录数线性增长 - -### 2. 批量查询(当前实现) - -```typescript -// 去重后批量查询 -const userIds = new Set(); -for (const record of history) { - userIds.add(record.user_id); - userIds.add(record.owner_id); -} - -const uniqueUserIds = Array.from(userIds); -const placeholders = uniqueUserIds.map(() => '?').join(','); -const userResult = await db.prepare(` - SELECT id, username - FROM users - WHERE id IN (${placeholders}) -`).bind(...uniqueUserIds).all(); -``` - -**内存消耗**: -- `Set`: 每个用户ID 8字节 -- `Map`: 用户ID + 用户名,内存占用小 -- 去重后查询量大幅减少 - -**性能优势**: -- 只有2次数据库查询(历史记录 + 用户名) -- 数据库连接池压力小 -- 响应时间稳定 - -## 内存使用分析 - -### 去重效果 - -假设有100条历史记录,涉及20个不同用户: - -| 方案 | 查询次数 | 内存占用 | 数据库压力 | -|------|----------|----------|------------| -| 循环查询 | 200次 | 高(临时对象多) | 高 | -| 批量查询(无去重) | 2次 | 中(重复查询) | 中 | -| 批量查询(去重) | 2次 | 低(去重优化) | 低 | - -### 内存优化措施 - -1. **使用 Set 去重**:避免重复的用户ID -2. **限制查询数量**:最多查询100个用户,避免SQL参数过多 -3. **使用 for...of 循环**:比 forEach 更节省内存 -4. **及时释放变量**:查询完成后立即使用结果 - -## 错误处理策略 - -### 1. 查询失败不会导致整个接口报错 - -```typescript -try { - const userResult = await db.prepare(`...`).all(); - // 处理结果 -} catch (error) { - debugLog('[邮箱历史] 批量查询用户名失败:', error); - // 查询失败时,所有用户名都设为"未知用户" -} -``` - -**优势**: -- 历史记录仍然可以正常返回 -- 只是用户名显示为"未知用户" -- 不会影响主要功能 - -### 2. 降级处理 - -```typescript -// 添加用户名到历史记录 -const historyWithUsernames = history.map(record => ({ - ...record, - user_username: userMap.get(record.user_id) || '未知用户', - owner_username: userMap.get(record.owner_id) || '未知用户' -})); -``` - -**容错机制**: -- 查询不到的用户名显示"未知用户" -- 保证数据结构完整性 -- 前端可以正常渲染 - -## 性能测试建议 - -### 1. 压力测试 - -```typescript -// 测试不同数量的历史记录 -const testCases = [10, 50, 100, 500, 1000]; -for (const count of testCases) { - const start = Date.now(); - await getAllMailboxHistory(db, 1, count); - const duration = Date.now() - start; - console.log(`${count}条记录耗时: ${duration}ms`); -} -``` - -### 2. 内存监控 - -```typescript -// 监控内存使用 -const memBefore = process.memoryUsage(); -await getAllMailboxHistory(db, 1, 100); -const memAfter = process.memoryUsage(); -console.log('内存使用:', memAfter.heapUsed - memBefore.heapUsed); -``` - -### 3. 数据库查询分析 - -```sql --- 分析查询性能 -EXPLAIN QUERY PLAN -SELECT id, username -FROM users -WHERE id IN (1, 2, 3, 4, 5); -``` - -## 优化建议 - -### 1. 缓存机制 - -```typescript -// 添加用户名缓存 -const usernameCache = new Map(); -const CACHE_DURATION = 5 * 60 * 1000; // 5分钟 - -if (usernameCache.has(userId)) { - return usernameCache.get(userId); -} -``` - -### 2. 分页优化 - -```typescript -// 限制单次查询的历史记录数量 -const MAX_HISTORY_PER_QUERY = 50; -if (limit > MAX_HISTORY_PER_QUERY) { - limit = MAX_HISTORY_PER_QUERY; -} -``` - -### 3. 索引优化 - -```sql --- 确保用户表有合适的索引 -CREATE INDEX idx_users_id_username ON users(id, username); -``` - -## 结论 - -**批量查询 + 去重** 是最优方案: - -1. **内存效率高**:去重后查询量大幅减少 -2. **性能稳定**:只有2次数据库查询 -3. **错误容错**:查询失败不影响主要功能 -4. **可扩展性好**:支持大量历史记录查询 - -相比循环查询,内存使用减少约70%,查询时间减少约80%。 diff --git a/docs/PERMISSION_VALIDATION.md b/docs/PERMISSION_VALIDATION.md deleted file mode 100644 index 453fb22..0000000 --- a/docs/PERMISSION_VALIDATION.md +++ /dev/null @@ -1,271 +0,0 @@ -# 权限验证设计说明 - -## 🔐 权限验证系统 - -### **核心原则** -1. **用户存在性验证**:确保 `user_id` 是真实存在的用户 -2. **权限范围验证**:确保用户只能操作自己有权限的资源 -3. **操作人验证**:确保操作人身份真实,防止伪造 -4. **所有者验证**:确保资源所有者身份真实,防止越权 - -## 🛡️ 权限验证函数 - -### **1. validateUserExists** -```typescript -// 验证用户是否存在且状态正常 -const { exists, user } = await validateUserExists(db, userId); -``` - -**验证内容**: -- 用户ID是否存在于数据库中 -- 用户状态是否为 `active` -- 返回用户基本信息 - -### **2. validateMailboxOwner** -```typescript -// 验证用户是否为邮箱所有者 -const { isOwner, mailbox } = await validateMailboxOwner(db, mailboxId, userId); -``` - -**验证内容**: -- 邮箱是否存在 -- 用户是否为邮箱的所有者 -- 返回邮箱基本信息 - -### **3. validateAdminPermission** -```typescript -// 验证管理员权限 -const { isAdmin, user } = await validateAdminPermission(db, userId); -``` - -**验证内容**: -- 用户是否存在 -- 用户类型是否为 `admin` -- 用户状态是否为 `active` - -### **4. validateMailboxOperationPermission** -```typescript -// 验证邮箱操作权限(综合验证) -const { hasPermission, mailbox, reason } = await validateMailboxOperationPermission( - db, mailboxId, userId, userType -); -``` - -**验证内容**: -- 用户存在性验证 -- 邮箱存在性验证 -- 权限范围验证(管理员 vs 普通用户) -- 所有者权限验证 - -## 🔍 关键操作权限验证 - -### **邮箱删除操作** -```typescript -export async function deleteMailbox( - db: D1Database, - mailboxId: number, - userId: number, - userType: string -): Promise { - // 1. 验证操作权限 - const permissionCheck = await validateMailboxOperationPermission(db, mailboxId, userId, userType); - if (!permissionCheck.hasPermission) { - throw new Error(permissionCheck.reason || '权限验证失败'); - } - - // 2. 验证用户存在 - const userValidation = await validateUserExists(db, userId); - if (!userValidation.exists) { - throw new Error('用户不存在'); - } - - // 3. 执行删除操作 - // 4. 记录操作历史 -} -``` - -### **邮箱状态切换操作** -```typescript -export async function toggleMailboxStatus( - db: D1Database, - mailboxId: number, - status: 'active' | 'disabled', - adminId: number -): Promise { - // 1. 验证管理员权限 - const userValidation = await validateUserExists(db, adminId); - if (!userValidation.exists) { - throw new Error('用户不存在'); - } - - if (userValidation.user!.user_type !== 'admin') { - throw new Error('需要管理员权限'); - } - - // 2. 验证邮箱存在 - const mailbox = await db.prepare(` - SELECT id, owner_id, address, status FROM mailboxes WHERE id = ? - `).bind(mailboxId).first(); - - if (!mailbox) { - throw new Error('邮箱不存在'); - } - - // 3. 执行状态切换 - // 4. 记录操作历史 -} -``` - -## 📋 权限验证检查点 - -### **1. 用户身份验证** -- ✅ 用户ID是否存在于数据库 -- ✅ 用户状态是否为 `active` -- ✅ 用户类型是否正确(admin/user) - -### **2. 资源权限验证** -- ✅ 资源是否存在(邮箱、申请等) -- ✅ 用户是否有权限操作该资源 -- ✅ 权限范围是否正确(自己的 vs 所有的) - -### **3. 操作权限验证** -- ✅ 操作类型是否被允许 -- ✅ 操作时机是否合适 -- ✅ 操作结果是否符合预期 - -## 🚨 安全防护 - -### **1. 防止用户ID伪造** -```typescript -// 每次操作前都验证用户存在性 -const userValidation = await validateUserExists(db, userId); -if (!userValidation.exists) { - throw new Error('用户不存在'); -} -``` - -### **2. 防止越权操作** -```typescript -// 验证用户是否有权限操作特定资源 -const permissionCheck = await validateMailboxOperationPermission(db, resourceId, userId, userType); -if (!permissionCheck.hasPermission) { - throw new Error('无权操作此资源'); -} -``` - -### **3. 防止权限提升** -```typescript -// 验证管理员权限 -if (userType === 'admin') { - const adminValidation = await validateAdminPermission(db, userId); - if (!adminValidation.isAdmin) { - throw new Error('需要管理员权限'); - } -} -``` - -## 📊 权限验证流程 - -### **邮箱操作权限验证流程** -``` -1. 验证用户存在性 - ├─ 用户不存在 → 抛出错误 - └─ 用户存在 → 继续 - -2. 验证资源存在性 - ├─ 资源不存在 → 抛出错误 - └─ 资源存在 → 继续 - -3. 验证权限范围 - ├─ 管理员 → 允许操作所有资源 - └─ 普通用户 → 验证是否为资源所有者 - -4. 执行操作 -5. 记录操作历史 -``` - -### **管理员操作权限验证流程** -``` -1. 验证用户存在性 - ├─ 用户不存在 → 抛出错误 - └─ 用户存在 → 继续 - -2. 验证管理员权限 - ├─ 不是管理员 → 抛出错误 - └─ 是管理员 → 继续 - -3. 验证资源存在性 -4. 执行操作 -5. 记录操作历史 -``` - -## 🔧 实现细节 - -### **数据库查询优化** -```sql --- 用户存在性验证(带状态检查) -SELECT id, username, user_type, created_at -FROM users -WHERE id = ? AND status = 'active' - --- 邮箱所有者验证 -SELECT id, owner_id, address, status -FROM mailboxes -WHERE id = ? AND owner_id = ? - --- 管理员权限验证 -SELECT id, username, user_type -FROM users -WHERE id = ? AND user_type = 'admin' AND status = 'active' -``` - -### **错误处理** -```typescript -// 统一的错误处理 -if (!permissionCheck.hasPermission) { - throw new Error(permissionCheck.reason || '权限验证失败'); -} -``` - -### **日志记录** -```typescript -// 权限验证成功日志 -debugLog('[权限验证] 邮箱所有者权限验证通过:', userId, '邮箱ID:', mailboxId); - -// 权限验证失败日志 -debugLog('[权限验证] 权限验证失败:', userId, '邮箱ID:', mailboxId, '原因:', reason); -``` - -## 📝 最佳实践 - -### **1. 始终验证用户存在性** -- 每个需要用户ID的操作都要验证用户存在 -- 防止伪造用户ID进行攻击 - -### **2. 权限验证前置** -- 在执行任何操作前先验证权限 -- 避免无效操作和资源浪费 - -### **3. 详细的错误信息** -- 提供清晰的错误原因 -- 便于调试和问题排查 - -### **4. 完整的操作记录** -- 记录所有关键操作 -- 包含操作人、资源、时间等信息 - -### **5. 权限验证复用** -- 使用统一的权限验证函数 -- 避免重复代码和逻辑不一致 - -## 🎯 总结 - -通过完善的权限验证系统,我们确保了: - -1. **安全性**:防止用户ID伪造和越权操作 -2. **一致性**:统一的权限验证逻辑 -3. **可维护性**:清晰的权限验证流程 -4. **可扩展性**:支持更多权限验证场景 -5. **可追踪性**:完整的操作记录和日志 - -这样的设计确保了系统的安全性和可靠性!🛡️ diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 04b7457..0000000 --- a/docs/README.md +++ /dev/null @@ -1,216 +0,0 @@ -# Cloudflare 临时邮箱管理系统 - -基于 Cloudflare Workers 的临时邮箱管理系统,提供完整的邮件接收、存储、发送、转发和管理功能。 - -## 🏗️ 项目架构 - -### 一体化部署架构 -``` -cloudflare-email-manager/ -├── vue/ # Vue 3 前端项目 -│ ├── src/ # 前端源码 -│ └── dist/ # 构建产物(自动生成) -├── worker/ # Cloudflare Worker 后端 -│ └── src/ # Worker 源码 -├── db/ # 数据库相关 -├── scripts/ # 构建和开发脚本 -├── package.json # 统一依赖管理(前端 + 后端) -├── wrangler.toml # Cloudflare Worker 配置 -└── README.md # 项目说明 -``` - -### 核心特性 -- **一体化部署架构**:前端通过 Worker 的 ASSETS 绑定提供服务 -- **模块化架构**:清晰的前后端分离,便于维护 -- **TypeScript 支持**:全栈 TypeScript 开发 -- **现代化 UI**:基于 Vue 3 + Naive UI 的响应式界面 -- **完整的邮件管理**:接收、存储、发送、转发、附件处理 -- **用户权限管理**:支持普通用户和管理员角色 -- **Webhook 集成**:支持钉钉、飞书等通知 - -## 🚀 快速开始 - -### 环境要求 -- Node.js >= 20.19.0 -- Docker & Docker Compose -- Cloudflare 账户 - -### 安装依赖 -```bash -# 安装所有依赖(前端 + 后端) -npm install -``` - -### 开发环境 -```bash -# 启动 Docker 容器(前端 + 后端) -docker-compose up -d - -# 查看容器状态 -docker-compose ps - -# 停止容器 -docker-compose down - -# 查看日志 -docker-compose logs vue -docker-compose logs worker - -# 重启服务 -docker-compose restart - -# 进入容器 -docker exec -it vue zsh # 前端容器 -docker exec -it worker zsh # 后端容器 -``` - -### 构建和部署 -```bash -# 构建前端 -npm run build:vue - -# 构建后端 -npm run build:worker - -# 完整部署到 Cloudflare -npm run deploy -``` - -### 数据库操作 -```bash -# 通过 npm 脚本(在宿主机运行) -npm run db:init -npm run db:init:remote -npm run db:migrate -npm run db:import - -# 直接在 Docker 容器内运行 -docker exec -it worker zsh -c "node scripts/db.js init" -docker exec -it worker zsh -c "node scripts/db.js init --remote" -docker exec -it worker zsh -c "node scripts/db.js migrate" -docker exec -it worker zsh -c "node scripts/db.js import" - -# 执行 SQL 命令 -docker exec -it worker zsh -c "node scripts/db.js 'SELECT * FROM users'" -docker exec -it worker zsh -c "node scripts/db.js 'SELECT * FROM users' --remote" - -# 执行 base64 编码的 SQL -docker exec -it worker zsh -c "node scripts/db.js U0VMRUNUICogMSBhcyB0ZXN0" -``` - -## 📁 目录结构 - -### 前端 (vue/) -- `src/` - Vue 3 源码 - - `components/` - 可复用组件 - - `views/` - 页面组件 - - `stores/` - Pinia 状态管理 - - `api/` - API 服务层 - - `router/` - 路由配置 -- `dist/` - 构建产物(自动生成) - -### 后端 (worker/) -- `src/` - Worker 源码 - - `routes/` - API 路由 - - `services/` - 业务逻辑 - - `handlers/` - 事件处理器 - - `middleware/` - 中间件 - - `utils/` - 工具函数 - - `types/` - 类型定义 - -### 根目录 -- `package.json` - 统一依赖管理(前端 + 后端) -- `wrangler.toml` - Cloudflare Worker 配置 -- `scripts/` - JavaScript 脚本 - - `deploy.js` - 部署脚本 - - `db.js` - 数据库操作脚本 - - `env-detector.js` - 环境检测模块 - -## 🔧 配置说明 - -### wrangler.toml -```toml -name = "cem" -main = "worker/src/main.ts" - -# 静态资源绑定 - 前端构建产物 -[assets] -directory = "vue/dist/" -binding = "ASSETS" -run_worker_first = true - -# 环境变量 -[vars] -DOMAIN = "your-domain.com" -JWT_SECRET = "your-jwt-secret" - -# D1 数据库 -[[d1_databases]] -binding = "DB" -database_name = "cem-db" - -# R2 存储桶 -[[r2_buckets]] -binding = "R2" -bucket_name = "cem-r2" -``` - -### 环境变量 -系统现在完全基于数据库配置运行,无需额外的环境变量设置。 - -## 📚 API 文档 - -详细的 API 文档请参考 [API/api-doc.yml](API/api-doc.yml) - -### 主要接口 -- `POST /api/auth/login` - 用户登录 -- `POST /api/auth/register` - 用户注册 -- `GET /api/emails` - 获取邮件列表 -- `POST /api/emails/send` - 发送邮件 -- `GET /api/mailboxes` - 获取邮箱列表 -- `POST /api/mailboxes` - 创建邮箱 - -## 🛠️ 开发指南 - -### 添加新功能 -1. 在 `worker/src/routes/` 中添加 API 路由 -2. 在 `vue/src/views/` 中添加页面组件 -3. 在 `vue/src/api/` 中添加 API 调用 -4. 更新类型定义 - -### 数据库操作 -```bash -# 初始化数据库 -npm run db:init - -# 导入测试数据 -npm run import:emails -``` - -### 调试模式 -通过系统设置界面启用调试模式,可以访问: -- `/api/debug` - 调试信息 -- `/api/debug/simulate-email` - 模拟邮件发送 - -## 📝 更新日志 - -详细的更新日志请参考 [UPDATE_LOG.md](UPDATE_LOG.md) - -## 🤝 贡献指南 - -1. Fork 项目 -2. 创建功能分支 -3. 提交更改 -4. 推送到分支 -5. 创建 Pull Request - -## 📄 许可证 - -MIT License - 详见 [LICENSE](LICENSE) 文件 - -## 🙏 致谢 - -- [Cloudflare Workers](https://workers.cloudflare.com/) -- [Vue.js](https://vuejs.org/) -- [Naive UI](https://www.naiveui.com/) -- [Hono](https://hono.dev/) diff --git a/docs/SCRIPTS.md b/docs/SCRIPTS.md deleted file mode 100644 index 7644de6..0000000 --- a/docs/SCRIPTS.md +++ /dev/null @@ -1,134 +0,0 @@ -# 简化脚本说明 - -## 🎯 设计理念 - -用 JavaScript 脚本替代复杂的 shell 脚本,提供更简洁、易维护的开发体验。 - -## 📁 脚本结构 - -``` -scripts/ -├── env-detector.js # 环境检测模块(可复用) -├── deploy.js # 部署脚本 -└── db.js # 数据库操作脚本(包含初始化、清理、迁移、导入功能) -``` - -## 🚀 使用方法 - -### 开发环境 -```bash -# 启动 Docker 容器(前端 + 后端) -docker-compose up -d - -# 查看容器状态 -docker-compose ps - -# 停止容器 -docker-compose down -``` - -### 部署 -```bash -# 完整部署(包含资源创建、构建、部署) -npm run deploy - -# 初始化部署(同 deploy) -npm run init - -# 清理所有 Cloudflare 资源 -npm run clean -``` - -### 数据库操作 -```bash -# 通过 npm 脚本(在宿主机运行) -npm run db:init -npm run db:init:remote -npm run db:migrate -npm run db:import - -# 直接在 Docker 容器内运行 -docker exec -it worker zsh -c "node scripts/db.js init" -docker exec -it worker zsh -c "node scripts/db.js init --remote" -docker exec -it worker zsh -c "node scripts/db.js migrate" -docker exec -it worker zsh -c "node scripts/db.js import" - -# 执行 SQL 命令 -docker exec -it worker zsh -c "node scripts/db.js 'SELECT * FROM users'" -docker exec -it worker zsh -c "node scripts/db.js 'SELECT * FROM users' --remote" - -# 执行 base64 编码的 SQL -docker exec -it worker zsh -c "node scripts/db.js U0VMRUNUICogRlJPTSB1c2Vycwo=" -``` - -### Docker 管理 -```bash -# 启动所有服务 -docker-compose up -d - -# 查看服务状态 -docker-compose ps - -# 查看日志 -docker-compose logs vue -docker-compose logs worker - -# 停止所有服务 -docker-compose down - -# 重启服务 -docker-compose restart - -# 进入容器 -docker exec -it vue zsh # 前端容器 -docker exec -it worker zsh # 后端容器 -``` - -## 🔧 脚本特性 - -### 1. 自动环境检测 -- 自动检测本地或 Docker 环境 -- 智能选择最佳运行方式 -- 无需手动配置 - -### 2. 统一接口 -- 所有操作通过 npm 脚本调用 -- 支持参数传递(如 `--remote`) -- 清晰的错误处理和日志输出 - -### 3. 易于维护 -- 纯 JavaScript 实现 -- 模块化设计 -- 易于扩展和修改 - -### 4. 环境检测模块 -- `env-detector.js` 提供统一的环境检测功能 -- 可复用的模块,所有脚本共享 -- 提供环境信息、可用性检查等功能 - -### 5. Docker 配置优化 -- 使用 `--net=host` 网络模式,无需端口映射 -- 使用 `-v "${PWD}/:${PWD}" -w ${PWD}` 路径映射 -- 容器内路径与宿主机完全一致,直接使用相对路径 - -## 📋 命令对照表 - -| 功能 | 命令 | 说明 | -|------|------|------| -| 开发 | `npm run dev` | 启动前端和后端 | -| 构建 | `npm run build` | 构建前端和后端 | -| 部署 | `npm run deploy` | 完整部署(资源创建+构建+部署) | -| 初始化 | `npm run init` | 同 deploy 命令 | -| 清理 | `npm run clean` | 删除所有 Cloudflare 资源 | -| 数据库初始化 | `npm run db:init` | 初始化本地数据库 | -| 远程数据库初始化 | `npm run db:init:remote` | 初始化远程数据库 | -| 数据库迁移 | `npm run db:migrate` | 执行数据库迁移 | -| 导入数据 | `npm run db:import` | 导入邮件数据 | - -## 🎉 优势 - -1. **简洁**:只需记住几个简单的命令 -2. **智能**:自动检测环境,无需手动配置 -3. **易维护**:JavaScript 脚本,易于理解和修改 -4. **统一**:所有操作通过 npm 脚本调用 -5. **灵活**:支持参数传递和命令组合 diff --git a/docs/STATUS_CODES.md b/docs/STATUS_CODES.md deleted file mode 100644 index ff6ec74..0000000 --- a/docs/STATUS_CODES.md +++ /dev/null @@ -1,141 +0,0 @@ -# 状态码说明文档 - -## 概述 - -本文档详细说明了系统中各种状态码的含义和使用场景。所有状态都使用数字表示,便于数据库存储和 API 传输。 - -## 用户状态 (User Status) - -| 状态码 | 状态名称 | 描述 | 使用场景 | -|--------|----------|------|----------| -| 1 | 激活 (Active) | 用户账户正常,可以正常使用所有功能 | 默认状态,新注册用户 | -| 2 | 停用 (Disabled) | 用户账户被管理员停用,无法登录 | 管理员封禁用户 | -| 3 | 删除 (Deleted) | 用户账户被软删除,保留数据但不可用 | 用户注销账户 | - -### 状态转换规则 - -- **激活 → 停用**: 管理员操作,可恢复 -- **激活 → 删除**: 用户主动注销,可恢复 -- **停用 → 激活**: 管理员恢复 -- **删除 → 激活**: 管理员恢复(需要重新验证) - -## 邮箱状态 (Mailbox Status) - -| 状态码 | 状态名称 | 描述 | 使用场景 | -|--------|----------|------|----------| -| 1 | 激活 (Active) | 邮箱正常,可以收发邮件 | 默认状态,新创建邮箱 | -| 2 | 停用 (Disabled) | 邮箱被管理员停用,无法收发邮件 | 管理员封禁邮箱 | -| 3 | 删除 (Deleted) | 邮箱被用户删除,可以重新申请 | 用户主动删除邮箱 | - -### 状态转换规则 - -- **激活 → 停用**: 管理员操作,可恢复 -- **激活 → 删除**: 用户主动删除,可重新申请 -- **停用 → 激活**: 管理员恢复 -- **删除 → 激活**: 重新申请后激活 - -## 邮箱申请状态 (Application Status) - -| 状态码 | 状态名称 | 描述 | 使用场景 | -|--------|----------|------|----------| -| 1 | 待审核 (Pending) | 申请已提交,等待管理员审核 | 用户提交申请后 | -| 2 | 已批准 (Approved) | 申请已通过,邮箱已创建 | 管理员批准申请 | -| 3 | 已拒绝 (Rejected) | 申请被拒绝,不能创建邮箱 | 管理员拒绝申请 | - -### 状态转换规则 - -- **待审核 → 已批准**: 管理员批准 -- **待审核 → 已拒绝**: 管理员拒绝 -- **已批准/已拒绝**: 终态,不可转换 - -## 邮箱历史操作类型 (Action Type) - -| 状态码 | 操作类型 | 描述 | 触发条件 | -|--------|----------|------|----------| -| 1 | 创建 (Created) | 邮箱被创建 | 管理员创建或申请通过 | -| 2 | 删除 (Deleted) | 邮箱被删除 | 用户主动删除 | -| 3 | 停用 (Disabled) | 邮箱被停用 | 管理员停用 | - -## 安全审计操作类型 (Security Action Type) - -| 状态码 | 操作类型 | 描述 | 使用场景 | -|--------|----------|------|----------| -| 1 | 权限拒绝 (Permission Denied) | 用户尝试访问无权限的资源 | 越权访问尝试 | -| 2 | 可疑操作 (Suspicious Operation) | 检测到可疑的系统操作 | 异常行为检测 | - -## 安全审计攻击类型 (Attack Type) - -| 状态码 | 攻击类型 | 描述 | 使用场景 | -|--------|----------|------|----------| -| 1 | 权限拒绝 (Permission Denied) | 尝试访问无权限资源 | 越权攻击 | -| 2 | 可疑活动 (Suspicious Activity) | 检测到可疑活动模式 | 行为异常 | -| 3 | 频率限制 (Rate Limit Exceeded) | 超过频率限制 | 暴力攻击 | - -## 数据库字段说明 - -### deleted_at 字段 - -- **类型**: DATETIME -- **可空**: 是 -- **用途**: 记录软删除的时间 -- **使用场景**: - - 用户状态为 3 (删除) 时记录删除时间 - - 邮箱状态为 3 (删除) 时记录删除时间 - - 用于数据恢复和审计 - -## API 使用示例 - -### 查询激活状态的用户 -```sql -SELECT * FROM users WHERE status = 1; -``` - -### 查询被删除的邮箱 -```sql -SELECT * FROM mailboxes WHERE status = 3 AND deleted_at IS NOT NULL; -``` - -### 查询待审核的申请 -```sql -SELECT * FROM mailbox_applications WHERE status = 1; -``` - -## 前端显示映射 - -### 用户状态显示 -```typescript -const getUserStatusText = (status: number) => { - switch (status) { - case 1: return '激活'; - case 2: return '停用'; - case 3: return '已删除'; - default: return '未知'; - } -}; -``` - -### 邮箱状态显示 -```typescript -const getMailboxStatusText = (status: number) => { - switch (status) { - case 1: return '正常'; - case 2: return '已停用'; - case 3: return '已删除'; - default: return '未知'; - } -}; -``` - -## 注意事项 - -1. **状态一致性**: 确保前后端使用相同的状态码定义 -2. **软删除**: 删除操作使用软删除,保留数据用于审计 -3. **权限控制**: 只有管理员可以修改用户和邮箱状态 -4. **数据恢复**: 软删除的数据可以通过管理员操作恢复 -5. **审计追踪**: 所有状态变更都会记录在历史表中 - -## 更新日志 - -- **2025-09-13**: 初始版本,定义所有状态码 -- **2025-09-13**: 添加 deleted_at 字段支持软删除 -- **2025-09-13**: 统一使用数字状态码,提高性能 diff --git a/docs/UPDATE_LOG.md b/docs/UPDATE_LOG.md deleted file mode 100644 index 3dcd544..0000000 --- a/docs/UPDATE_LOG.md +++ /dev/null @@ -1,1248 +0,0 @@ -### 🔄 最近更新 - -#### 组件复用优化和文件夹结构重构 (2025-01-21) - -**优化目标:** -- 创建可复用的通用组件,减少代码重复 -- 重新设计文件夹结构,提高代码组织性 -- 优化API参数支持,添加scope=all/user区分 -- 合并views和layouts文件夹,统一视图管理 -- 修正命名规范,使用View后缀,调整Dashboard为共享视图 - -**优化内容:** - -1. **通用组件创建** - - **页面组件**:`PageHeader`、`DebugInfo`、`PageStates`、`Pagination` - - **业务组件**:`EmailList`、`MailboxList`、`SettingsForm` - - **表单组件**:`FormField`、`CheckboxField`、`Button`、`Modal` - - **状态组件**:`StatusBadge`(支持邮件、邮箱、用户状态) - -2. **文件夹结构重构** - ``` - views/ - ├── auth/ # 认证视图 - ├── admin/ # 管理员视图 - │ ├── emails/ # 邮件管理 - │ ├── mailboxes/ # 邮箱管理 - │ ├── users/ # 用户管理 - │ ├── settings/ # 系统设置 - │ └── security/ # 安全管理 - ├── user/ # 用户视图 - │ ├── emails/ # 我的邮件 - │ ├── mailboxes/ # 我的邮箱 - │ └── settings/ # 个人设置 - └── shared/ # 共享视图和组件 - ├── dashboard/ # 仪表板(登录后共享) - ├── layouts/ # 布局组件 - └── components/ # 通用组件 - ``` - -3. **API参数优化** - - 邮件查询:支持 `scope=all/user` 参数 - - 邮箱查询:支持 `scope=all/user` 参数 - - 转发规则:支持 `scope=all/user` 参数 - - 更新API文档,明确参数说明 - -4. **组件复用效果** - - 页面头部:统一使用 `PageHeader` 组件 - - 调试信息:统一使用 `DebugInfo` 组件 - - 加载状态:统一使用 `PageStates` 组件 - - 分页功能:统一使用 `Pagination` 组件 - - 状态显示:统一使用 `StatusBadge` 组件 - - 表单处理:统一使用 `FormField` 和 `SettingsForm` 组件 - -5. **命名规范统一** - - 视图文件:`[功能]View.vue` - - 组件文件:`[功能]Component.vue` - - 布局文件:`[功能]Layout.vue` - - 视图命名:`[角色][功能]View` - - 共享视图:`[功能]View`(如:`DashboardView`) - -6. **Dashboard共享设计** - - 将Dashboard从admin目录移动到shared目录 - - 作为登录后的共享视图,所有用户都可以访问 - - 符合业务逻辑,提高代码复用性 - -7. **404错误页面处理** - - 创建`NotFoundView.vue`组件,提供友好的404错误页面 - - 添加路由配置`/:pathMatch(.*)*`,捕获所有未匹配的路由 - - 提供"返回首页"和"返回上页"按钮,提升用户体验 - - 支持调试模式,显示详细的错误信息 - - 确保404页面背景为白色,与App.vue样式保持一致 - -8. **404页面美化升级** - - 采用现代化渐变背景设计,提升视觉效果 - - 添加浮动动画装饰元素,增加页面活力 - - 404数字采用弹跳动画效果,增强视觉冲击力 - - 自定义SVG错误图标,支持旋转和脉冲动画 - - 毛玻璃效果卡片设计,提升现代感 - - 渐变文字效果,增强视觉层次 - - 按钮添加悬停动画和光泽效果 - - 新增建议链接区域,提供快速导航 - - 完全响应式设计,适配各种设备尺寸 - - 优化调试信息展示,使用毛玻璃效果 - -**技术改进:** -- 减少代码重复率约70% -- 提高组件复用性 -- 统一样式和交互体验 -- 简化维护工作 -- 提高开发效率 - -#### 页面架构重构 - 拆分独立组件简化系统 (2025-01-21) - -**重构目标:** -- 将复杂的页面拆分为独立的Vue组件,简化路由映射和缓存管理 -- 移除复杂的scope参数处理,每个页面对应独立的API方法 -- 提高代码可维护性和系统性能 - -**重构内容:** - -1. **页面组件拆分** - - **邮件页面**:拆分为 `MyEmailsView.vue`(我的邮件)和 `AllEmailsView.vue`(全部邮件) - - **邮箱页面**:拆分为 `MyMailboxesView.vue`(我的邮箱)和 `MailboxManagementView.vue`(邮箱管理) - - **设置页面**:拆分为 `PersonalSettingsView.vue`(个人设置)和 `SystemSettingsView.vue`(系统设置) - -2. **路由配置更新** - ```typescript - // 用户页面 - 'my-emails' -> MyEmailsView.vue - 'my-mailboxes' -> MyMailboxesView.vue - 'personal-settings' -> PersonalSettingsView.vue - - // 管理员页面 - 'all-emails' -> AllEmailsView.vue - 'mailbox-management' -> MailboxManagementView.vue - 'system-settings' -> SystemSettingsView.vue - ``` - -3. **routeApiCacheMapper简化** - - 移除复杂的scope参数处理 - - 每个页面对应独立的API方法 - - 简化缓存键生成逻辑 - - 优化API服务映射 - -4. **统一页面数据管理** - - 所有新组件使用 `usePaginatedPageData` 或 `usePageData` - - 统一的加载状态、错误处理和调试信息 - - 一致的UI风格和交互体验 - -**重构效果:** -- ✅ **系统架构更清晰**:每个页面职责单一,易于维护 -- ✅ **路由映射更简单**:无需复杂的scope参数处理 -- ✅ **缓存管理更高效**:每个页面独立的缓存键,避免冲突 -- ✅ **代码复用性更高**:统一的页面数据管理逻辑 -- ✅ **开发效率提升**:新页面开发更简单,调试更便捷 -- ✅ **用户体验更好**:页面加载更快,交互更流畅 - -#### 重构API方法类型定义和服务映射系统 (2025-01-21) - -**重构目标:** -- 整理和优化 `ApiMethod` 类型定义 -- 重构 `getApiService` 方法,使其更加清晰和易于维护 -- 添加API服务映射验证和调试工具 - -**重构内容:** - -1. **API方法类型定义重构** - ```typescript - // 按服务分类定义类型 - export type EmailApiMethod = 'getEmails' - export type MailboxApiMethod = 'getMailboxes' | 'getAllMailboxes' - export type UserApiMethod = 'getForwardRules' | 'getUserInfo' - export type AdminApiMethod = 'getUsers' | 'getSystemConfig' | 'getSecurityStats' | 'getMailboxHistory' - - // 联合类型 - export type ApiMethod = EmailApiMethod | MailboxApiMethod | UserApiMethod | AdminApiMethod - ``` - -2. **API服务映射表** - ```typescript - const API_SERVICE_MAPPINGS: ApiServiceMapping[] = [ - { - service: emailApiService, - methods: ['getEmails'], - description: '邮件相关API' - }, - { - service: mailboxApiService, - methods: ['getMailboxes', 'getAllMailboxes'], - description: '邮箱相关API' - }, - // ... 更多映射 - ] - ``` - -3. **重构getApiService方法** - - 使用映射表进行精确匹配 - - 添加启发式规则作为后备方案 - - 增加详细的调试日志 - -4. **添加验证和调试工具** - - `validateApiServiceMappings()`: 验证映射完整性 - - `getApiServiceInfo()`: 获取API服务详细信息 - - 更新测试工具支持新的映射系统 - -**重构效果:** -- ✅ API方法类型定义更加清晰和结构化 -- ✅ 服务选择逻辑更加可靠和易于维护 -- ✅ 添加了完整的验证和调试工具 -- ✅ 提供了详细的日志信息便于问题排查 -- ✅ 支持精确匹配和启发式规则两种选择方式 - -#### 修复邮件页面缓存键冲突问题 (2025-01-21) - -**问题分析:** -- 邮件前端渲染使用了"全部邮件"的缓存,导致用户邮件和管理员全部邮件缓存冲突 -- 缓存键生成逻辑中,`scope` 参数没有被正确包含在缓存键中 -- 用户在 `emails` 页面切换 `showAllEmails` 状态时,缓存键生成不一致 - -**修复方案:** -- 优化缓存键生成逻辑,确保所有参数(包括 `scope`)都被正确包含 -- 添加参数排序确保缓存键的一致性 -- 创建缓存键测试工具验证不同参数组合 - -**技术实现:** -- 修复缓存键生成逻辑: - ```typescript - // 合并默认参数和传递的参数 - const finalParams = { ...config.apiParams, ...params } - - // 生成参数字符串,确保scope参数被正确包含 - const paramString = Object.entries(finalParams) - .filter(([_, value]) => value !== undefined && value !== null) - .sort(([a], [b]) => a.localeCompare(b)) // 排序确保一致性 - .map(([key, value]) => `${key}_${value}`) - .join('_') - ``` -- 添加详细的调试信息: - ```typescript - console.log('🔑 生成缓存键:', { - routeName: config.routeName, - keyPrefix, - userId, - defaultParams: config.apiParams, - passedParams: params, - finalParams, - paramString, - cacheKey - }) - ``` -- 创建缓存键测试工具 (`testCacheKeys.ts`) 验证不同参数组合 - -**修复效果:** -- ✅ 用户邮件和管理员全部邮件使用不同的缓存键 -- ✅ 缓存键生成逻辑更加稳定和一致 -- ✅ 添加了详细的调试信息便于问题排查 -- ✅ 提供了测试工具验证缓存键生成 - -#### 修复统一刷新管理器API服务选择问题 (2025-01-21) - -**问题分析:** -- 统一刷新管理器在调用API时出现 `apiService[config.apiMethod] is not a function` 错误 -- API服务选择逻辑不正确,没有根据具体的API方法选择对应的服务 -- 邮件页面刷新功能失效 - -**修复方案:** -- 修复API服务选择逻辑,根据API方法精确选择对应的服务 -- 添加类型安全检查和错误处理 -- 创建测试工具验证API服务选择 - -**技术实现:** -- 修复 `getApiService` 方法: - ```typescript - switch (config.apiMethod) { - case 'getEmails': - return emailApiService - case 'getAllMailboxes': - case 'getMailboxes': - return mailboxApiService - case 'getUsers': - case 'getSecurityStats': - case 'getSystemConfig': - return adminApiService - // ... 更多映射 - } - ``` -- 添加类型安全检查: - ```typescript - const response = await (apiService as any)[String(config.apiMethod)](finalParams) - ``` -- 创建测试工具 (`testRouteApiMapper.ts`) 验证API服务选择 - -**修复效果:** -- ✅ 邮件页面全局刷新功能恢复正常 -- ✅ API服务选择逻辑更加精确和可靠 -- ✅ 添加了类型安全检查和错误处理 -- ✅ 提供了测试工具便于调试和验证 - -#### 创建统一的路由API缓存映射管理系统 (2025-01-21) - -**系统架构升级:** -- 创建了统一的路由、API和缓存映射管理系统 -- 实现了全局刷新管理器,自动根据当前路由调用对应API -- 提供了多种页面数据管理Hook,简化组件开发 - -**核心组件:** - -1. **路由API缓存映射器** (`routeApiCacheMapper.ts`) - - 统一管理所有页面的路由、API方法和缓存策略 - - 支持权限检查和API服务选择 - - 提供缓存键生成和API调用方法 - -2. **全局刷新管理器** (`globalRefreshManager.ts`) - - 统一处理所有页面的刷新逻辑 - - 支持统一管理器和页面级刷新方法 - - 提供刷新历史记录和统计功能 - -3. **统一页面数据管理Hook** (`useUnifiedPageData.ts`) - - `useUnifiedPageData`: 完整的页面数据管理 - - `useSimplePageData`: 简化的页面数据管理 - - `usePaginatedPageData`: 分页数据管理 - -**技术特性:** -- **自动路由识别**: 根据当前路由自动选择对应的API方法 -- **智能缓存策略**: 不同页面使用不同的缓存时间和策略 -- **权限管理**: 自动检查用户权限,管理员和普通用户使用不同API -- **统一刷新**: 全局刷新按钮自动调用正确的API方法 -- **调试支持**: 提供详细的调试信息和状态监控 -- **向后兼容**: 支持传统页面级刷新方法作为后备方案 - -**配置映射表:** -```typescript -ROUTE_API_CACHE_MAP = { - 'emails': { apiMethod: 'getEmails', scope: 'user', cache: 'user_emails' }, - 'admin-emails': { apiMethod: 'getEmails', scope: 'all', cache: 'admin_emails' }, - 'mailboxes': { apiMethod: 'getMailboxes', cache: 'user_mailboxes' }, - 'admin-mailboxes': { apiMethod: 'getAllMailboxes', scope: 'all', cache: 'admin_mailboxes' }, - // ... 更多页面配置 -} -``` - -**使用示例:** -```typescript -// 在组件中使用统一管理器 -const { data, loading, error, refreshData } = useUnifiedPageData() - -// 分页数据管理 -const { data, pagination, changePage } = usePaginatedPageData() - -// 全局刷新自动处理 -// 点击全局刷新按钮时,系统会自动: -// 1. 识别当前路由 -// 2. 查找对应的API方法 -// 3. 检查用户权限 -// 4. 调用正确的API -// 5. 更新缓存 -``` - -**修复效果:** -- ✅ 全局刷新功能现在完全自动化,无需手动配置 -- ✅ 所有页面使用统一的API调用和缓存策略 -- ✅ 新页面开发更加简单,只需配置映射表 -- ✅ 调试信息更加详细,便于问题排查 -- ✅ 系统架构更加清晰和可维护 - -#### 修复邮件页面全局刷新功能缺少 scope=all 参数问题 (2025-01-21) - -**问题分析:** -- 邮件页面的全局刷新功能没有正确传递 `scope=all` 参数 -- `showAllEmails` 状态初始化逻辑存在问题,导致管理员页面状态不一致 -- 全局刷新时可能获取不到全部邮件数据 - -**修复方案:** -- 优化 `showAllEmails` 状态初始化逻辑 -- 确保管理员页面默认显示全部邮件 -- 增强全局刷新功能的调试信息 - -**技术实现:** -- 修复状态初始化逻辑: - ```typescript - const showAllEmails = ref( - isAdminPage.value ? - (localStorage.getItem('cem_show_all_emails') === 'true' || localStorage.getItem('cem_show_all_emails') === null) : - (localStorage.getItem('cem_show_all_emails') === 'true') - ) - ``` -- 优化页面初始化逻辑: - ```typescript - if (isAdminPage.value) { - const storedValue = localStorage.getItem('cem_show_all_emails') - if (storedValue === null || storedValue === 'false' || !showAllEmails.value) { - showAllEmails.value = true - localStorage.setItem('cem_show_all_emails', 'true') - } - } - ``` -- 增强调试信息: - - 在全局刷新函数中添加详细的状态日志 - - 记录 `showAllEmails`、`isAdminPage`、`routeName` 等关键状态 - -**修复效果:** -- 管理员页面的全局刷新功能正确传递 `scope=all` 参数 -- 确保管理员页面默认显示全部邮件 -- 状态管理更加稳定和可靠 -- 调试信息更加详细,便于问题排查 - -#### 优化邮箱管理页面缓存和过期时间功能 (2025-01-21) - -**问题分析:** -- 前端邮箱管理页面显示过期时间功能,但后端数据库没有 `expires_at` 字段 -- 前端每次点击操作都会重新请求API,没有有效使用缓存机制 -- 全局刷新按钮与页面内刷新功能冲突,导致不必要的强制刷新 - -**修复方案:** -- 移除前端过期时间相关功能(后端不支持) -- 优化缓存使用策略,区分智能刷新和强制刷新 -- 分离全局刷新和页面内刷新功能,避免冲突 - -**技术实现:** -- 移除前端过期时间功能: - - 删除表格中的 `expires_at` 列 - - 移除表单中的有效期选择器 - - 删除 `getExpiryClass` 函数 - - 移除表单中的 `duration` 字段 -- 优化缓存策略: - - 分页切换使用缓存:`loadMailboxes(false)` - - 搜索操作使用缓存:`loadMailboxes(false)` - - 标签页切换使用缓存:`loadMailboxes(false)` - - 页面初始化使用缓存:`loadMailboxes(false)` -- 分离刷新功能: - - 全局刷新按钮:强制刷新(跳过缓存),确保获取最新数据 - - 正常页面操作:使用智能刷新(优先缓存) - - 数据操作后:使用强制刷新(跳过缓存) - - 新增 `forceRefreshData()` 函数用于需要立即更新的操作 - -**修复效果:** -- 前端界面与后端数据结构保持一致 -- 减少不必要的API请求,提升页面响应速度 -- 智能缓存机制:正常操作优先使用缓存,缓存失效时才请求API -- 全局刷新按钮强制刷新,确保获取最新数据 -- 用户体验提升:操作更流畅,响应更快 - -#### 修复缓存冲突和路由名称不一致问题 (2025-01-21) - -**问题分析:** -- 路由名称不一致:`admin-mailboxs` vs `admin-mailboxes` -- 缓存失效策略过于激进:`cacheInvalidation.onNewMailbox()` 会清除所有用户的缓存 -- 多个缓存系统可能相互干扰 - -**修复方案:** -- 统一路由名称:`admin-mailboxs` → `admin-mailboxes` -- 优化缓存失效策略:只清除当前用户相关缓存 -- 精确控制缓存清除范围 - -**技术实现:** -- 修复路由定义: - - `vue/src/composables/routes.ts`:`admin-mailboxs` → `admin-mailboxes` - - `vue/src/layouts/MainLayoutView.vue`:路径和名称统一 -- 优化缓存失效: - - 移除全局缓存失效:`cacheInvalidation.onNewMailbox()` - - 精确清除缓存:只清除当前用户的特定缓存键 - - 避免影响其他用户的缓存数据 - -**修复效果:** -- 路由名称完全一致,避免混淆 -- 缓存策略更加精确,不会误清除其他用户数据 -- 提升缓存命中率,减少不必要的API请求 -- 不同用户之间的缓存完全隔离 - -#### 修复缓存键冲突导致的不必要请求问题 (2025-01-21) - -**问题分析:** -- 同一管理员用户在 `/mailboxes` 和 `/admin-mailboxes` 间切换会触发请求 -- 缓存键不包含路由信息,导致不同路由使用相同的缓存键 -- 转发管理使用不同的缓存系统,所以不会影响邮箱缓存 - -**根本原因:** -- 普通用户访问 `/mailboxes`:缓存键 `mailboxes_1_1_20_user` -- 管理员访问 `/admin-mailboxes`:缓存键 `mailboxes_1_1_20_all` -- 同一用户在不同路由下生成不同缓存键,无法共享缓存 - -**修复方案:** -- 在缓存键中包含路由信息,区分不同页面 -- 优化缓存失效策略,简化依赖关系 -- 保持不同路由的缓存完全独立 - -**技术实现:** -- 修改缓存键生成函数: - - `mailboxList(userId, page, limit, scope, routeName)` - - 包含路由名称:`mailboxes_${userId}_${page}_${limit}_${scope}_${routeName}` -- 更新缓存键使用: - - `/mailboxes`:`mailboxes_1_1_20_user_mailboxes` - - `/admin-mailboxes`:`mailboxes_1_1_20_all_admin-mailboxes` -- 简化缓存失效函数,移除不存在的缓存键引用 - -**修复效果:** -- 不同路由生成独立的缓存键,避免冲突 -- 同一路由的缓存可以正确共享 -- 减少不必要的API请求,提升用户体验 -- 转发管理等其他页面不受影响 - -#### 修复缓存存储问题 - 从内存缓存改为localStorage持久化 (2025-01-21) - -**问题分析:** -- 用户发现localStorage中只有4个缓存项,没有邮箱相关缓存 -- `MailboxesView.vue` 使用的是 `SmartCache`(内存缓存),不会持久化到localStorage -- 其他页面都使用 `CacheService`(localStorage缓存),导致缓存系统不一致 - -**根本原因:** -- `SmartCache` 使用内存中的 `Map`,页面刷新后缓存丢失 -- `CacheService` 使用 localStorage,可以持久化缓存 -- 缓存系统不统一,导致用户体验不一致 - -**修复方案:** -- 将 `MailboxesView.vue` 从 `SmartCache` 改为 `CacheService` -- 统一使用 localStorage 进行缓存持久化 -- 保持缓存键格式的一致性 - -**技术实现:** -- 修改导入:`SmartCache` → `CacheService` -- 更新缓存键生成:使用字符串模板而非函数 -- 修改缓存操作: - - `smartCache.get()` → `cacheService.get()` - - `smartCache.set()` → `cacheService.set()` - - `smartCache.delete()` → `cacheService.delete()` -- 添加TypeScript类型断言:`cacheService.get()` - -**修复效果:** -- 邮箱数据现在会持久化到localStorage -- 页面刷新后缓存不会丢失 -- 缓存系统完全统一,使用 `cem_cache_` 前缀 -- 用户可以在localStorage中看到邮箱相关缓存项 - -#### 修复邮件页面缓存键缺少用户ID导致的数据混乱问题 (2025-01-21) - -**问题分析:** -- 用户发现 `我的邮件` 和 `全部邮件` 之间切换不会重新请求 -- 而 `我的邮箱` 和 `邮箱管理` 之间切换会重新请求 -- 邮件页面的缓存键没有包含用户ID,导致所有用户共享缓存 - -**根本原因:** -- `EmailsView.vue` 的缓存键:`emails_${routeName}_${page}_${limit}_${scope}` -- `MailboxesView.vue` 的缓存键:`mailboxes_${userId}_${page}_${limit}_${scope}_${routeName}` -- 邮件页面缺少 `userId`,导致不同用户共享同一个缓存 - -**安全风险:** -- 不同用户可能看到其他用户的邮件数据 -- 缓存数据混乱,可能导致隐私泄露 - -**修复方案:** -- 在 `EmailsView.vue` 的缓存键中添加用户ID -- 确保所有页面的缓存都包含用户隔离 -- 统一缓存键格式,提高安全性 - -**技术实现:** -- 添加 `useAuthStore` 导入 -- 修改缓存键生成:`emails_${userId}_${routeName}_${page}_${limit}_${scope}` -- 添加用户ID验证,确保缓存键的唯一性 - -**修复效果:** -- 所有页面的缓存都包含用户ID,确保数据隔离 -- 不同用户之间不会共享缓存数据 -- 提高系统安全性,防止数据泄露 -- 缓存行为更加一致和可预测 - -#### 修复邮件处理数据库字段名不匹配问题 (2025-01-21) - -**问题分析:** -- 邮件处理时出现错误:`table emails has no column named user_id: SQLITE_ERROR` -- 数据库表结构正确,但代码中存在字段名不匹配问题 -- 类型定义中使用 `raw_email`,但数据库字段是 `raw_content` - -**根本原因:** -- 数据库 schema 中字段名:`raw_content` -- TypeScript 类型定义中字段名:`raw_email` -- 代码中混用了两种字段名,导致数据库操作失败 - -**修复方案:** -- 统一字段名:将类型定义中的 `raw_email` 改为 `raw_content` -- 更新所有相关代码,使用正确的字段名 -- 确保类型定义与数据库 schema 完全一致 - -**技术实现:** -- 修复 `worker/src/types/index.ts`:`raw_email` → `raw_content` -- 修复 `worker/src/handlers/email.ts`:更新邮件记录创建逻辑 -- 修复 `worker/src/services/email.ts`:更新所有数据库操作 -- 重新初始化数据库,确保表结构正确 - -**修复效果:** -- 邮件处理功能恢复正常 -- 数据库字段名完全一致 -- 类型安全得到保障 -- 系统稳定性提升 - -**后续修复 (2025-01-21 补充):** -- 发现 `createEmail` 和 `getEmailById` 函数中缺少部分数据库字段 -- 修复 `createEmail` 函数:添加缺失的 `reply_to`, `cc`, `bcc`, `is_read` 字段 -- 修复 `getEmailById` 函数:完善 SELECT 语句和返回对象 -- 添加详细的调试日志,便于问题排查 -- 确保所有数据库字段都在代码中正确处理 - -### 🔄 最近更新 - -#### 修复我的邮箱页面没有内容问题 - 数据库状态字段类型错误 (2025-01-21) - -**问题分析:** -- "我的邮箱"页面显示空白,没有任何邮箱数据 -- 后端API查询使用了错误的status字段值(字符串'active'而不是数字1) -- 数据库schema中status字段为INTEGER类型:1=激活, 2=停用, 3=删除 -- 前端缺少debug模式下的原始JSON数据显示 - -**修复方案:** -- 修复后端所有status字段查询,使用正确的数字值 -- 在前端添加debug模式显示原始API响应数据 -- 统一数据库状态字段的使用规范 - -**技术实现:** -- 修复 `worker/src/services/mailbox.ts`: - - `getAllMailboxes`函数:`WHERE m.status = 'active'` → `WHERE m.status = 1` - - `deleteMailbox`函数:`SET status = 'deleted'` → `SET status = 3` -- 修复 `worker/src/services/permission.ts`: - - 管理员权限验证:`WHERE status = 'active'` → `WHERE status = 1` -- 修复 `worker/src/routes/user-info.ts`: - - 用户信息查询:`WHERE status = 'active'` → `WHERE status = 1` - - 修复TypeScript类型错误:`userMap: Record` -- 修复 `worker/src/services/user.ts`: - - 创建用户函数:`VALUES (?, ?, ?, 'active', ...)` → `VALUES (?, ?, ?, 1, ...)` -- 前端 `vue/src/layouts/Admin/MailboxesView.vue`: - - 添加debug模式显示原始JSON响应 - - 显示请求参数和处理后的数据 - - 导入并使用systemStore进行debug模式判断 - -**修复效果:** -- "我的邮箱"页面正常显示用户的邮箱列表 -- 管理员可以正常查看所有邮箱 -- Debug模式下可以查看完整的API响应数据 -- 数据库查询性能提升(使用数字索引而非字符串比较) -- 所有状态字段查询统一使用数字值 -- 全局刷新功能修复:强制刷新时跳过缓存,确保获取最新数据 -- 前端状态渲染修复:支持数字状态值(1=正常, 2=已停用, 3=已删除),兼容字符串状态值 -- 用户信息显示修复:修复前端字段名不匹配问题(row.username → row.owner_username),后端添加owner_usertype字段 -- 前后端字段同步:统一使用owner_usertype字段名,更新API文档schema定义 -- 用户类型字段类型修正:owner_usertype改为数字类型(0=普通用户, 1=管理员),后端使用CASE语句转换 - -#### 修复页面布局滚动条问题 - 确保内容超出时才显示滚动条 (2025-01-21) - -**问题分析:** -- 页面默认状态下出现滚动条,即使内容不多 -- 页面高度设置不合理,使用了 `min-height` 而不是固定 `height` -- 主内容区域没有正确控制溢出行为 - -**修复方案:** -- 修改 `vue/src/layouts/MainLayoutView.vue` 中的页面布局样式 -- 使用固定高度替代最小高度,确保页面占满整个屏幕 -- 正确设置溢出控制,只在内容超出时显示滚动条 - -**技术实现:** -- 修改 `.main-content` 样式: - - 将 `min-height: 100vh` 改为 `height: 100vh` - - 添加 `overflow: hidden` 防止主内容区域出现滚动条 -- 修改 `.page-content` 样式: - - 将 `min-height: calc(100vh - 60px)` 改为 `height: calc(100vh - 60px)` - - 保持 `overflow-y: auto` 确保内容超出时显示滚动条 - -**修复效果:** -- 页面默认状态下占满整个屏幕,无滚动条 -- 只有当内容超出视口高度时才显示滚动条 -- 所有页面(仪表板、邮箱、邮件等)都应用此修复 -- 移动端响应式布局正常工作 - -#### 清理仪表板页面统计信息显示 (2025-01-21) - -**清理内容:** -- 删除仪表板统计卡片中的"总邮件数"、"我的邮箱数"、"待审核申请"显示 -- 删除安全概览部分的统计信息显示 -- 保留管理员快捷操作卡片(用户管理、系统设置) -- 保留最近邮件显示功能 - -**技术实现:** -- 修改 `vue/src/layouts/Admin/DashboardView.vue`: - - 删除统计卡片中的邮件、邮箱、申请统计显示 - - 删除安全概览中的统计信息内容 - - 移除相关的响应式数据:`emailStats`、`mailboxStats`、`applicationStats` - - 删除不再需要的数据加载函数:`loadMailboxesData`、`loadApplicationsData` - - 简化预加载逻辑,只保留邮件数据预加载 - - 删除不再使用的 `goToMailboxes` 方法 - -**优化效果:** -- 简化仪表板界面,减少冗余信息显示 -- 减少不必要的数据加载和API请求 -- 为后续安全概览功能调整预留空间 -- 保持核心功能(最近邮件、快捷操作)完整 - -#### 移除仪表板页面单独刷新按钮,集成全局刷新机制 (2025-01-21) - -**问题分析:** -- 仪表板页面有单独的刷新按钮,与全局刷新功能重复 -- 用户需要记住不同页面的刷新按钮位置 -- 全局刷新机制已经完善,应该统一使用 - -**优化方案:** -- 移除仪表板页面的单独刷新按钮 -- 集成全局刷新机制,支持全局刷新事件监听 -- 统一刷新体验,用户只需使用顶部栏的全局刷新按钮 - -**技术实现:** -- 修改 `vue/src/layouts/Admin/DashboardView.vue`: - - 移除 `PageHeader` 的 `show-refresh` 属性 - - 移除 `refreshData` 方法 - - 添加 `handleGlobalRefresh` 全局刷新事件处理 - - 添加 `refreshDashboardPage` 页面级刷新函数 - - 在 `onMounted` 中注册全局刷新事件监听和页面刷新函数 - - 在 `onUnmounted` 中清理事件监听和全局函数 - -**全局刷新机制:** -``` -MainLayoutView 全局刷新按钮: -1. 触发 'global:refresh' 自定义事件 -2. 调用 window.refreshCurrentPage() 页面级刷新函数 - -仪表板页面响应: -1. 监听 'global:refresh' 事件 -2. 注册 refreshDashboardPage 到 window.refreshCurrentPage -3. 收到刷新事件时调用 loadDashboardData(true) 强制刷新 -``` - -**优势:** -- 统一刷新体验:所有页面使用相同的全局刷新按钮 -- 减少UI冗余:移除重复的刷新按钮 -- 保持功能完整:支持强制刷新和缓存失效 -- 代码简洁:减少重复的刷新逻辑 - -#### 优化仪表板数据加载策略 - 移除冗余API,实现智能预加载 (2025-01-21) - -**问题分析:** -- `dashboard-stats` API 造成数据重复,与现有页面数据重复 -- 仪表板显示的数据实际上就是各个页面第一页的数据 -- 可以预加载页面数据,既满足仪表板显示,又为后续页面访问提供缓存 - -**优化方案:** -- 移除 `dashboard-stats` API,避免数据重复 -- 实现智能预加载系统,在仪表板加载时预加载各页面第一页数据 -- 仪表板统计数据直接从预加载的数据中提取 -- 用户点击进入页面时直接使用缓存数据,无需重新加载 - -**技术实现:** -- 移除 `worker/src/routes/system.ts` 中的 `dashboard-stats` API -- 移除 `vue/src/composables/api.ts` 中的 `getDashboardStats` 方法 -- 重构 `vue/src/layouts/Admin/DashboardView.vue`: - - 实现智能预加载管理器 `preloadPageData` - - 分别实现 `loadEmailsData`、`loadMailboxesData`、`loadApplicationsData` - - 每个数据加载函数都支持缓存机制 - - 仪表板统计数据从预加载数据中提取 - -**预加载策略:** -``` -仪表板预加载: -- 邮件数据:page=1, limit=10(最近10封邮件) -- 邮箱数据:page=1, limit=1(获取总数统计) -- 申请数据:page=1, limit=1(获取待审核数量) - -缓存复用: -- 用户点击"我的邮件"页面:直接使用缓存的 page=1, limit=10 数据 -- 用户点击"我的邮箱"页面:直接使用缓存的 page=1, limit=1 数据 -- 用户点击"申请审核"页面:直接使用缓存的 page=1, limit=1 数据 -``` - -**性能优化:** -- 减少API数量:从4个API减少到3个现有API -- 数据复用:仪表板数据与页面数据完全复用 -- 预加载缓存:用户进入页面时数据已准备就绪 -- 智能缓存:支持依赖关系和自动失效 - -**测试验证:** -- 邮件API:返回正确数据 ✅ -- 邮箱API:返回统计信息和可用域名 ✅ -- 预加载系统:数据正确提取和缓存 ✅ -- 缓存复用:页面访问时直接使用缓存 ✅ - -#### 智能缓存系统重构 - 解决数据重复请求和缓存失效问题 (2025-01-21) - -**问题描述:** -- 仪表板数据重复请求,缓存策略不合理 -- 分页数据无法复用,每次翻页都重新请求 -- 缓存失效机制不完善,数据更新后缓存未及时清理 -- 后端API数据结构不够灵活,缺少聚合统计信息 - -**解决方案:** -- 设计智能缓存管理系统,支持依赖关系和自动失效 -- 创建专门的仪表板统计API,避免重复请求 -- 实现分层缓存策略,统计数据与列表数据分离 -- 优化后端API返回结构,包含统计信息和可用域名 - -**技术实现:** -- 新增 `vue/src/composables/smartCache.ts`:智能缓存管理器 - - 支持缓存依赖关系,自动失效相关缓存 - - 分层缓存策略,不同数据不同TTL - - 缓存键生成器,统一管理缓存键 - - 缓存失效工具函数,事件驱动的缓存清理 -- 新增 `worker/src/routes/system.ts`:仪表板统计API - - `/api/system/dashboard-stats`:获取聚合统计数据 - - 支持管理员和普通用户不同权限 - - 并行查询,提升性能 -- 优化 `worker/src/routes/mailbox.ts`:邮箱API增强 - - 返回统计信息:用户邮箱数、总邮箱数 - - 返回可用域名列表 - - 支持管理员和普通用户不同数据范围 -- 重构前端缓存逻辑: - - 仪表板使用智能缓存,统计数据5分钟,最近邮件2分钟 - - 邮箱列表支持分页缓存,10分钟TTL - - 申请列表支持分页缓存,10分钟TTL - - 自动缓存失效:新邮箱创建时失效相关缓存 - -**缓存策略设计:** -``` -缓存键设计: -- user_stats_{userId}: 用户统计数据 -- recent_emails_{userId}: 最近邮件 -- mailboxes_{userId}_{page}_{limit}_{scope}: 邮箱列表 -- applications_{userId}_{page}_{limit}_{scope}: 申请列表 -- dashboard_stats_{userId}: 仪表板统计 - -依赖关系: -- new_email: 失效邮件相关缓存 -- new_mailbox: 失效邮箱相关缓存 -- application_change: 失效申请相关缓存 -- user_change: 失效用户相关缓存 -- system_config_change: 失效系统配置缓存 -``` - -**性能优化:** -- 仪表板数据:从4个API请求减少到2个 -- 分页数据:支持缓存复用,减少重复请求 -- 统计数据:专门API,避免重复计算 -- 缓存失效:事件驱动,及时清理过期数据 - -**测试验证:** -- 仪表板统计API:返回正确数据 ✅ -- 邮箱API增强:包含统计和域名信息 ✅ -- 智能缓存:依赖关系正确失效 ✅ -- 分页缓存:支持缓存复用 ✅ - -#### 完善仪表板和邮箱申请功能 - 数据填充和用户体验优化 (2025-01-21) - -**问题描述:** -- 仪表板数据填充不完整,最近邮件、安全概览、我的邮箱数量统计不准确 -- 我的邮箱页面使用模拟数据,未连接真实API -- 申请邮箱页面缺少永久选项,申请理由字数限制不合理 -- 邮箱地址下拉选项使用硬编码域名,未使用系统设置 - -**修复内容:** -- 完善仪表板数据填充:最近10封邮件、总邮件数量、我的邮箱数量、待审核申请数量 -- 添加缓存支持:仪表板数据缓存2分钟,提升加载速度 -- 清理我的邮箱页面:移除模拟数据,连接真实API -- 优化申请邮箱页面:添加永久选项、调整字数限制(4-200字)、移除补充说明字段 -- 动态域名选项:从系统配置获取可用域名,在mailboxes API中返回 - -**技术实现:** -- 修复 `vue/src/layouts/Admin/DashboardView.vue`:并行加载数据、添加缓存、完善安全概览 -- 修复 `vue/src/layouts/Admin/MailboxesView.vue`:连接真实API、优化表单验证、动态域名 -- 修复 `worker/src/routes/mailbox.ts`:在API响应中添加可用域名信息 -- 修复 `vue/src/composables/api.ts`:支持对象参数的getMailboxApplications方法 - -**测试验证:** -- 仪表板数据加载:总邮件数、我的邮箱数、待审核申请数 ✅ -- 最近邮件显示:最近10封邮件,支持缓存 ✅ -- 邮箱申请表单:永久选项、字数限制、动态域名 ✅ -- API数据一致性:mailboxes接口返回可用域名 ✅ - -#### 统一前后端status字段类型 - 全面使用数字类型 (2025-01-21) - -**问题描述:** -- 前后端status字段类型不一致,后端部分使用字符串('active'/'disabled'),部分使用数字(1/2/3) -- 数据库schema使用INTEGER类型,但代码中存在字符串比较和赋值 -- 类型转换逻辑复杂,容易出错 - -**修复内容:** -- 统一所有status字段使用数字类型:1=激活, 2=停用, 3=删除 -- 移除所有字符串status的使用和转换逻辑 -- 修复数据库查询中的status条件 -- 简化状态切换逻辑,直接使用数字值 - -**技术实现:** -- 修复 `worker/src/routes/user.ts`:用户状态切换使用数字类型 -- 修复 `worker/src/services/mailbox.ts`:邮箱状态查询和更新使用数字类型 -- 修复 `worker/src/routes/mailbox.ts`:邮箱状态切换使用数字类型 -- 修复 `worker/src/routes/user-info.ts`:用户查询条件使用数字类型 -- 修复 `worker/src/services/permission.ts`:权限验证使用数字类型 -- 移除 `toggleUserStatus` 函数,直接在路由中处理状态更新 -- 修复 `toggleMailboxStatus` 函数参数类型 - -**测试验证:** -- 用户列表查询:`GET /api/admin/users` ✅ -- 用户状态切换:`PUT /api/users/{id}/status` ✅ -- 状态值验证:只接受1(启用)或2(停用) ✅ -- 数据库一致性:所有status字段使用INTEGER类型 ✅ - -#### 修复管理员用户列表接口 - 参数传递和类型转换 (2025-01-21) - -**问题描述:** -- `/api/admin/users?page=1&limit=20` 接口调用失败 -- `getAllUsers` 函数参数传递不正确 -- 用户状态字段类型转换不一致 - -**修复内容:** -- 修复 `worker/src/routes/admin.ts` 中 `getAllUsers` 函数调用参数 -- 统一用户状态字段类型为 `1 | 2 | 3`(数字类型) -- 修复 `toggleUserStatus` 函数中的状态转换逻辑 -- 完善分页参数传递和返回数据结构 - -**技术实现:** -- 修正 `getAllUsers` 函数调用:传递正确的 `page`, `limit`, `searchParams` 参数 -- 统一状态类型:数据库使用 INTEGER,TypeScript 使用 `1 | 2 | 3` -- 添加状态转换:字符串状态('active'/'disabled')转换为数字状态(1/2) -- 完善返回数据:包含 `total`, `items`, `page`, `limit`, `total_pages` - -**测试验证:** -- 基础分页查询:`GET /api/admin/users?page=1&limit=20` ✅ -- 搜索查询:`GET /api/admin/users?page=1&limit=10&query=admin` ✅ -- 返回数据结构完整,包含所有必要字段 - -#### 优化 Worker 静态资源处理 - 移除动态注入 (2025-01-20) - -**变更内容:** -- 移除 Worker 后端直接返回 HTML 字符串的代码 -- 保留 ASSETS 绑定,通过 Cloudflare 原生方式提供静态资源 -- 移除动态配置注入功能,前端通过 API 获取配置 - -**技术实现:** -- 删除 `worker/src/utils/dynamic-config.ts`:动态配置注入功能 -- 删除 `worker/src/static/` 目录:前端代码生成器 -- 删除 `worker/src/templates/` 目录:HTML 模板生成器 -- 删除 `worker/src/utils/template.ts`:模板加载器 -- 保留 `worker/src/main.ts` 中的 ASSETS 绑定处理逻辑 -- 恢复 `wrangler.toml` 中的 ASSETS 绑定配置 - -**架构改进:** -- Worker 后端通过 ASSETS 绑定提供静态资源,不直接返回 HTML 字符串 -- 前端通过 `/api` 接口获取系统配置和动态数据 -- 简化了动态配置逻辑,提升了性能 -- 保持了 Cloudflare 原生的静态资源服务能力 - -#### 统一缓存策略 - 全面使用 localStorage (2025-01-20) - -**变更内容:** -- 统一使用 localStorage 进行缓存,完全移除 sessionStorage 的使用 -- 确保系统配置和用户数据的持久化存储 -- 提升用户体验,避免标签页关闭后数据丢失 - -**技术实现:** -- 更新 `vue/src/layouts/Admin/DebugView.vue`:移除 sessionStorage 相关代码 -- 修改 `worker/src/static/app-config.ts`:将 SessionCache 改为 LocalCache -- 重构 `worker/src/utils/session-cache.ts` → `local-cache.ts`:完全基于 localStorage -- 更新 `docs/CACHE_API.md`:反映缓存策略变更 - -**缓存策略改进:** -- 所有缓存数据使用 `cem_cache_` 前缀存储在 localStorage -- 系统配置持久化存储,不会因标签页关闭而丢失 -- 保持 TTL 机制,自动清理过期缓存 -- 统一缓存管理接口,简化开发维护 - -#### 移除环境变量依赖 - 完全基于数据库配置 (2025-01-20) - -**变更内容:** -- 移除了 `DOMAIN`、`JWT_SECRET`、`cem_debug`、`ALLOW_REGISTRATION`、`CLEANUP_DAYS`、`MAX_ATTACHMENT_SIZE` 环境变量的依赖 -- 系统现在完全基于数据库中的系统设置进行配置 -- 简化了部署配置,减少了环境变量管理的复杂性 - -**技术实现:** -- 修改 `worker/src/types/index.ts`:移除环境变量类型定义 -- 更新 `worker/src/main.ts`:移除环境变量使用 -- 修改 `worker/src/routes/api.ts`:使用 `getPrimaryDomain()` 替代 `c.env.DOMAIN` -- 更新 `worker/src/routes/user.ts`:使用数据库配置替代环境变量 -- 更新 `worker/src/routes/admin.ts`:使用数据库配置替代环境变量 -- 清理 `wrangler.example.toml`:移除所有环境变量示例配置 - -**配置管理改进:** -- 域名配置:通过系统设置中的 `primary_domain` 或 `domains` 配置 -- JWT密钥:通过 `getJWTSecret()` 函数自动管理(环境变量 > 数据库 > 自动生成) -- 调试模式:完全由数据库中的 `debug_mode` 设置控制 -- 用户注册:通过数据库中的 `allow_registration` 设置控制 -- 邮件清理:通过数据库中的 `cleanup_days` 设置控制 -- 附件大小:通过数据库中的 `max_attachment_size` 设置控制 - -**文档更新:** -- 更新 `docs/README.md`:移除所有环境变量说明,更新为完全基于数据库配置 -- 更新 `docs/CACHE_API.md`:移除环境变量接口定义 -- 更新 `wrangler.example.toml`:移除所有环境变量示例配置 - -#### 系统配置参数类型统一 - 统一使用数字格式 (2025-01-20) - -**问题描述:** -- SystemConfig、SystemConfigUpdate 和 health 接口中的配置参数类型不统一 -- health 接口使用数字格式 (1/0),而 SystemConfig 使用布尔格式 (true/false) -- 需要统一所有接口使用数字格式,提高一致性 - -**统一方案:** -- 所有配置参数统一使用数字格式:1=是/开启,0=否/关闭 -- 包括:`debug_mode`、`allow_registration`、`auto_approve_mailbox`、`allow_user_send` - -**API文档更新:** -- `SystemConfig` 接口:所有布尔类型参数改为数字类型 -- `SystemConfigUpdate` 接口:所有布尔类型参数改为数字类型 -- health 接口:保持数字格式不变 - -**后端更新:** -- `worker/src/types/index.ts`:SystemConfig 接口类型定义更新 -- `worker/src/services/settings.ts`:配置读取和更新逻辑适配数字格式 -- `worker/src/routes/system.ts`:健康检查接口配置返回逻辑简化 - -**前端更新:** -- `vue/src/types/index.ts`:相关接口类型定义更新 -- `vue/src/composables/system.ts`:配置读取逻辑适配数字格式 - -**文件变更:** -- `api/api-doc.yml` - 更新 SystemConfig 和 SystemConfigUpdate 类型定义 -- `worker/src/types/index.ts` - 更新 SystemConfig 接口类型 -- `worker/src/services/settings.ts` - 适配数字格式的配置处理 -- `worker/src/routes/system.ts` - 简化健康检查配置返回 -- `vue/src/types/index.ts` - 更新前端类型定义 -- `vue/src/composables/system.ts` - 适配数字格式的配置读取 - -#### API数据格式统一 - 统一health和config接口的数据格式 (2025-01-20) - -**问题描述:** -- 后端响应的数据格式不统一:`/api/system/config` 返回 `"debug_mode": false`,`/api/system/health` 返回 `"debug_mode": 0` -- 需要根据 API 文档统一数据格式 - -**统一方案:** -- `/api/system/config` 接口:返回 `boolean` 类型(true/false) -- `/api/system/health` 接口:返回 `integer` 类型(1/0) -- 前端需要处理两种不同的数据格式 - -**API文档更新:** -- 更新 health 接口的 config 字段定义,只保留 `allow_registration` 和 `debug_mode` 两个字段 -- 移除不需要的敏感字段:`allow_user_send`、`max_mailboxes_per_user`、`storage_provider`、`max_attachment_size`、`mail_retention_days`、`supported_domains` - -**前端处理:** -- health 接口返回 `debug_mode: 0/1`,前端转换为 `boolean` 类型 -- config 接口返回 `debug_mode: true/false`,前端直接使用 -- 统一存储到 `systemStore.systemConfig` 中 - -**文件变更:** -- `api/api-doc.yml` - 更新 health 接口的 config 字段定义 -- `vue/src/composables/system.ts` - 处理不同数据格式的转换 - -#### 安全优化 - 精简health接口配置字段 (2025-01-20) - -**安全考虑:** -- health 接口的 config 字段不应该泄露太多敏感信息 -- 只保留前端真正需要的配置字段 -- 缓存更新应该在配置修改的地方进行,而不是在 health 接口 - -**优化内容:** -- 移除 health 接口中的 `storage_provider` 字段 -- 只保留 `allow_registration` 和 `debug_mode` 两个前端必需的字段 -- 移除 health 接口中的缓存更新逻辑 -- 缓存更新由 `updateSystemConfig` 函数在配置修改时自动处理 - -**保留的配置字段:** -- `allow_registration`: 注册状态(前端需要) -- `debug_mode`: 调试模式(前端需要) - -**缓存更新策略:** -- health 接口:只获取配置,不更新缓存 -- 配置修改:通过 `updateSystemConfig` → `setSystemSetting` → 自动更新缓存 -- 其他接口:通过 `getSystemConfig` 从缓存读取 - -**文件变更:** -- `worker/src/routes/system.ts` - 精简 health 接口配置字段,移除缓存更新 -- `vue/src/types/index.ts` - 更新 SystemHealth 接口类型 - -#### 缓存共享优化 - health接口作为systemConfig同步器 (2025-01-20) - -**优化思路:** -- health 接口负责将 config 字段同步到 `systemConfig` 缓存 -- 其他接口的关键内容都从 `systemConfig` 读取 -- health 接口变成"配置同步器",避免重复数据库查询 - -**技术实现:** -- 新增 `updateSystemSettingsCache()` 函数:批量更新系统设置缓存(不写入数据库) -- 修改 health 接口:获取配置后同步到 `systemConfig` 缓存 -- 前端统一从 `systemConfig` 读取配置,不再依赖 health 接口的 config 字段 -- 更新 `SystemConfig` 接口:添加缺失的配置字段,确保类型安全 - -**工作流程:** -1. **health 接口**:查询数据库 → 同步到 `systemConfig` 缓存 → 返回健康状态 -2. **其他接口**:直接从 `systemConfig` 缓存读取配置 -3. **前端**:统一从 `systemConfig` 获取配置,保证数据一致性 - -**性能改进:** -- 减少重复的数据库查询 -- 提高配置获取的响应速度 -- 统一配置管理,避免数据不一致 -- health 接口承担配置同步职责 - -**文件变更:** -- `worker/src/services/settings.ts` - 新增缓存更新函数,完善类型定义 -- `worker/src/routes/system.ts` - health接口同步配置到缓存 -- `vue/src/composables/system.ts` - 统一从systemConfig读取配置 -- `worker/src/types/index.ts` - 完善SystemConfig接口 - -#### 前端调试模式显示修复 - 优先使用health接口配置 (2025-01-20) - -**问题描述:** -- 前端调试模式在调试模式未开启的情况下依然显示 -- 前端没有正确使用 `/api/system/health` 接口中的 `debug_mode` 字段 -- 多个地方使用硬编码的调试模式检查逻辑 - -**修复内容:** -- 更新 `SystemHealth` 类型定义,匹配实际的 health 接口数据结构 -- 修改 `system.ts` 中的 `isDebugMode` 计算属性,优先使用 health 接口中的 `debug_mode` 配置 -- 添加 `isRegistrationAllowed` 计算属性,同样优先使用 health 接口配置 -- 统一所有调试模式检查逻辑,移除硬编码的环境变量检查 - -**技术实现:** -- 更新 `vue/src/types/index.ts`:修正 `SystemHealth` 接口结构 -- 优化 `vue/src/composables/system.ts`:优先使用 health 接口配置 -- 修复 `vue/src/composables/routes.ts`:使用 systemStore 的调试模式状态 -- 更新 `vue/src/composables/globalState.ts`:统一调试模式获取方式 - -**数据结构对应:** -```json -{ - "success": true, - "data": { - "health": { - "config": { - "debug_mode": 0, // 0=关闭, 1=开启 - "allow_registration": 0 - } - } - } -} -``` - -**文件变更:** -- `vue/src/types/index.ts` - 更新 SystemHealth 接口 -- `vue/src/composables/system.ts` - 优化调试模式检查逻辑 -- `vue/src/composables/routes.ts` - 统一调试模式权限检查 -- `vue/src/composables/globalState.ts` - 移除硬编码调试模式 - -#### 调试模式查询优化 - 避免重复数据库查询 (2025-01-20) - -**变更内容:** -- 创建了专门的调试模式中间件 `debugModeMiddleware` -- 优化了调试模式检查逻辑,避免重复数据库查询 -- 统一了调试模式检查的实现方式 - -**技术实现:** -- 新增 `worker/src/middleware/debug.ts`:专门的调试模式中间件 -- 优化 `database.ts`:移除重复的调试模式查询代码,使用中间件 -- 更新 `main.ts`:调试接口使用新的中间件链 -- 利用 `getSystemConfig()` 的缓存机制,提高性能 - -**性能改进:** -- 减少了重复的数据库查询 -- 统一使用缓存的系统配置 -- 简化了调试模式检查逻辑 - -**文件变更:** -- `worker/src/middleware/debug.ts` - 新增调试模式中间件 -- `worker/src/routes/database.ts` - 使用中间件,移除重复查询 -- `worker/src/main.ts` - 调试接口使用中间件链 - -#### 调试模式配置优化 - 移除环境变量依赖 (2025-01-20) - -**变更内容:** -- 移除了 worker 中通过环境变量 `cem_debug` 检查调试模式的逻辑 -- 调试模式现在完全由数据库中的 `debug_mode` 设置控制 -- 简化了调试模式检查逻辑,提高了配置的一致性 - -**技术实现:** -- 修改 `main.ts`:移除所有 `c.env.cem_debug` 检查,只依赖数据库配置 -- 更新 `database.ts`:简化调试模式校验逻辑,统一从数据库读取 -- 优化 `debug.ts`:移除环境变量依赖,更新初始化逻辑 -- 清理 `types/index.ts`:移除 `cem_debug` 环境变量类型定义 - -**配置管理改进:** -- 调试模式现在完全由管理员通过系统设置界面控制 -- 不再需要修改环境变量来启用/禁用调试功能 -- 提高了配置的透明度和可管理性 - -**文件变更:** -- `worker/src/main.ts` - 移除环境变量检查逻辑 -- `worker/src/routes/database.ts` - 简化调试模式校验 -- `worker/src/utils/debug.ts` - 更新初始化逻辑 -- `worker/src/types/index.ts` - 移除环境变量类型定义 - -#### UI优化 - 移除刷新检测并添加手动刷新按钮 (2025-09-20) - -**变更内容:** -- 删除了所有基于 `performance.navigation` 的自动刷新检测代码 -- 在顶部工具栏(top-bar)添加了手动刷新按钮 -- 实现了清理当前页面缓存并重新加载的功能 - -**技术实现:** -- 修改 `useSimplePageData.ts`:移除刷新检测逻辑,简化初始加载流程 -- 增强 `cache.ts`:添加 `clearAndRefreshCurrentPage` 方法支持页面级缓存清理 -- 更新 `MainLayoutView.vue`:在菜单按钮旁边添加刷新按钮,显示加载状态 - -**用户体验提升:** -- 用户可以主动控制页面刷新,不再依赖自动检测 -- 刷新按钮提供实时反馈(🔄 刷新 / 🔄 刷新中...) -- 更精确的缓存管理,只清理当前页面相关缓存 - -**文件变更:** -- `vue/src/composables/useSimplePageData.ts` - 移除刷新检测代码 -- `vue/src/composables/cache.ts` - 添加页面级缓存清理方法 -- `vue/src/layouts/MainLayoutView.vue` - 添加刷新按钮UI和交互逻辑 - -**Bug修复:** -- 修复 `admin-emails` 页面 "API method getEmails not found" 错误 -- 在 `adminApiService` 中添加 `getEmails` 方法,支持管理员查看所有邮件 -- 修复 `EmailsView.vue` 中缺少 `computed` 导入的问题 -- 确保前后端数据结构一致:后端返回 `{ total: number, items: Email[] }`,前端正确解析 -- 移除调试代码,清理模板中的临时输出 - -#### 开发环境优化 (2024-01-XX) -- **Docker 配置完善**:完善 `docker-compose.yml` 配置,前端和后端独立容器 - - 前端容器:`vue` 服务,运行 Vite 开发服务器 - - 后端容器:`worker` 服务,运行 wrangler dev - - 共享 node_modules 卷,避免依赖冲突 -- **文档清理**:删除过时的文档文件 - - 删除 `docs/FRONTEND_SEPARATION.md` - - 删除 `docs/MIGRATION.md` - - 删除 `docs/ENV_CONFIG.md` -- **数据库操作优化**:直接使用 wrangler d1 execute 命令 - - 通过 `docker exec -it worker zsh -c "node scripts/db.js 'SQL'"` 执行 SQL 命令 - - 支持 base64 编码的 SQL 命令 - - 简化了数据库操作流程,无需复杂的参数传递 -- **文档更新**:更新所有文档反映新的容器化开发环境 -- **脚本整合**:将根目录 `deploy.js` 的功能整合到 `scripts/deploy.js` 中 -- **功能完善**:新的部署脚本包含完整的部署流程 - - 资源创建(D1、KV、R2) - - 用户交互(域名输入、管理员密码等) - - 数据库初始化和管理员账户创建 - - 配置文件更新(wrangler.toml) - - 前后端构建和部署 -- **命令统一**:更新 `package.json` 中的脚本引用 - - `npm run deploy` - 完整部署 - - `npm run init` - 同 deploy 命令 - - `npm run clean` - 清理所有 Cloudflare 资源 -- **文档更新**:更新 `docs/SCRIPTS.md` 反映新的脚本功能 - -#### 后端架构重构 - 前后端分离 (2024-01-XX) -- **静态资源服务**:移除内嵌HTML模板,改为通过 ASSETS 绑定服务前端 -- **API路由重构**:统一API路径设计,符合 OpenAPI 3.0 规范 - - 认证:`/api/auth/*` (login, logout, register) - - 用户:`/api/users/*` (me, 用户管理) - - 邮件:`/api/emails/*` (列表、详情、删除、发送、附件) - - 邮箱:`/api/mailboxes/*` (申请、管理) - - 转发规则:`/api/forward-rules/*` (CRUD操作) - - 系统:`/api/system/*` (配置、健康检查) - - 管理员:`/api/admin/*` (管理功能) -- **类型定义更新**:完善 TypeScript 类型定义,支持新的API结构 -- **服务函数优化**:重构服务层函数,支持分页和权限控制 -- **配置更新**:更新 wrangler.toml 配置,添加 ASSETS 绑定 - -#### API 接口优化 (2024-01-XX) -- **用户搜索功能**:`/api/users` 接口新增 `query` 参数,支持按用户名、邮箱搜索 -- **邮件查询优化**:`/api/emails` 接口调整 `scope` 参数逻辑 - - 默认行为:普通用户获取自己的邮件,管理员获取自己的邮件 - - 全部邮件:仅当 `scope=all` 时管理员可获取全部邮件 - - 移除了 `scope=mine` 的默认值,简化了查询逻辑 -- **登录接口简化**:`/api/auth/login` 只返回 JWT token,不返回用户信息 - - 前端登录后需要调用 `/api/users/me` 获取用户详细信息 - - 避免了接口间的数据重复,职责更加清晰 diff --git a/docs/USERNAME_CHANGE_DESIGN.md b/docs/USERNAME_CHANGE_DESIGN.md deleted file mode 100644 index 125a4cf..0000000 --- a/docs/USERNAME_CHANGE_DESIGN.md +++ /dev/null @@ -1,154 +0,0 @@ -# 用户名修改设计说明 - -## 🚨 当前问题 - -### **MailboxWithUser.owner_username 的问题** -当前设计中,`MailboxWithUser` 接口包含 `owner_username` 字段,这会导致以下问题: - -1. **数据不一致**:如果用户修改用户名,`owner_username` 会过时 -2. **维护困难**:需要同步更新所有相关记录 -3. **查询复杂**:需要额外的 JOIN 操作 - -## 💡 解决方案 - -### **方案1:始终通过 owner_id 查询(推荐)** - -#### **优点** -- 数据一致性:始终获取最新用户名 -- 维护简单:无需同步更新 -- 性能优化:减少数据冗余 - -#### **实现方式** -```typescript -// 查询时动态获取用户名 -const mailboxes = await db.prepare(` - SELECT - m.id, - m.owner_id, - m.address, - m.status, - m.created_at, - m.updated_at, - u.username as owner_username - FROM mailboxes m - JOIN users u ON m.owner_id = u.id - WHERE m.status = 'active' -`).all(); -``` - -### **方案2:缓存用户名(不推荐)** - -#### **缺点** -- 数据不一致风险 -- 需要复杂的同步机制 -- 维护成本高 - -## 🔧 技术实现 - -### **数据库设计** -```sql --- 邮箱表只存储 owner_id -CREATE TABLE mailboxes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - owner_id INTEGER NOT NULL, -- 只存储用户ID - address TEXT UNIQUE NOT NULL, - status TEXT DEFAULT 'active', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE -); - --- 用户表支持用户名修改 -CREATE TABLE users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, -- 用户名可修改 - email TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - user_type TEXT DEFAULT 'user', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); -``` - -### **API 响应设计** -```typescript -// 邮箱列表响应 -interface MailboxListResponse { - total: number; - items: Array<{ - id: number; - owner_id: number; - address: string; - status: string; - created_at: string; - updated_at: string; - owner_username: string; // 动态查询获取 - }>; -} -``` - -### **前端处理** -```typescript -// 前端始终使用 owner_id 作为唯一标识 -const mailbox = { - id: 1, - owner_id: 123, // 唯一标识 - owner_username: "user1", // 显示用,可随时更新 - address: "user1@example.com" -}; - -// 如果用户名修改,重新查询即可 -const updatedMailbox = await fetchMailbox(mailbox.id); -``` - -## 📋 最佳实践 - -### **1. 数据查询** -- 始终通过 `owner_id` 关联查询用户名 -- 避免在数据库中存储冗余的用户名信息 -- 使用 JOIN 查询获取最新用户名 - -### **2. 缓存策略** -- 前端可以缓存用户名,但需要定期更新 -- 后端不缓存用户名,始终查询最新数据 -- 使用 `owner_id` 作为缓存键 - -### **3. 错误处理** -- 如果用户被删除,`owner_id` 关联会失效 -- 需要处理用户不存在的情况 -- 提供友好的错误提示 - -### **4. 性能优化** -- 为 `owner_id` 字段创建索引 -- 使用适当的 JOIN 查询 -- 考虑分页查询的性能 - -## 🎯 实施建议 - -### **立即实施** -1. 修改 `MailboxWithUser` 接口,移除 `owner_username` 字段 -2. 更新所有查询,使用 JOIN 获取用户名 -3. 修改前端代码,使用 `owner_id` 作为唯一标识 - -### **长期规划** -1. 考虑添加用户名修改历史记录 -2. 实现用户名唯一性检查 -3. 添加用户名修改的审计日志 - -## 🔍 相关文件 - -- `worker/src/types/index.ts` - 类型定义 -- `worker/src/services/mailbox.ts` - 邮箱服务 -- `vue/src/api/index.ts` - 前端类型定义 -- `api/api-doc.yml` - API 文档 - -## 📝 总结 - -通过始终使用 `owner_id` 查询用户名,我们可以: - -1. **保证数据一致性**:用户名修改后,所有相关查询都会返回最新值 -2. **简化维护**:无需同步更新多个表 -3. **提高性能**:减少数据冗余,优化查询效率 -4. **增强可扩展性**:支持用户名修改功能 - -这是一个更加健壮和可维护的设计方案。 diff --git a/package-lock.json b/package-lock.json index 2d827e2..a76783a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,19 +16,24 @@ "cloudflare": "^5.1.0", "dayjs": "^1.11.18", "element-plus": "^2.11.3", - "hono": "^4.9.8", + "hono": "^4.10.3", + "html-to-text": "^9.0.5", "naive-ui": "^2.43.1", "pinia": "^3.0.3", + "postal-mime": "^2.6.1", "typescript": "^5.9.2", "unplugin-auto-import": "^20.1.0", "unplugin-vue-components": "^29.0.0", "vue": "^3.5.18", "vue-router": "^4.5.1", - "vue-tsc": "^3.0.7" + "vue-tsc": "^3.1.5", + "wrangler": "^4.51.0" }, "devDependencies": { "@cloudflare/workers-types": "^4.20250920.0", + "@types/html-to-text": "^9.0.4", "@vitejs/plugin-vue": "^6.0.1", + "concurrently": "^9.2.1", "vite": "^7.1.5", "vite-plugin-vue-devtools": "^8.0.0" }, @@ -74,6 +79,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -356,6 +362,7 @@ "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.4.tgz", "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/types": "^7.28.4" }, @@ -528,12 +535,142 @@ "node": ">=6.9.0" } }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.1.tgz", + "integrity": "sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "mime": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.7.11", + "resolved": "https://registry.npmmirror.com/@cloudflare/unenv-preset/-/unenv-preset-2.7.11.tgz", + "integrity": "sha512-se23f1D4PxKrMKOq+Stz+Yn7AJ9ITHcEecXo2Yjb+UgbUDCEBch1FXQC6hx6uT5fNA3kmX3mfzeZiUmpK1W9IQ==", + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": "^1.20251106.1" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20251125.0", + "resolved": "https://registry.npmmirror.com/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20251125.0.tgz", + "integrity": "sha512-xDIVJi8fPxBseRoEIzLiUJb0N+DXnah/ynS+Unzn58HEoKLetUWiV/T1Fhned//lo5krnToG9KRgVRs0SOOTpw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20251125.0", + "resolved": "https://registry.npmmirror.com/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20251125.0.tgz", + "integrity": "sha512-k5FQET5PXnWjeDqZUpl4Ah/Rn0bH6mjfUtTyeAy6ky7QB3AZpwIhgWQD0vOFB3OvJaK4J/K4cUtNChYXB9mY/A==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20251125.0", + "resolved": "https://registry.npmmirror.com/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20251125.0.tgz", + "integrity": "sha512-at6n/FomkftykWx0EqVLUZ0juUFz3ORtEPeBbW9ZZ3BQEyfVUtYfdcz/f1cN8Yyb7TE9ovF071P0mBRkx83ODw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20251125.0", + "resolved": "https://registry.npmmirror.com/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20251125.0.tgz", + "integrity": "sha512-EiRn+jrNaIs1QveabXGHFoyn3s/l02ui6Yp3nssyNhtmtgviddtt8KObBfM1jQKjXTpZlunhwdN4Bxf4jhlOMw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20251125.0", + "resolved": "https://registry.npmmirror.com/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20251125.0.tgz", + "integrity": "sha512-6fdIsSeu65g++k8Y2DKzNKs0BkoU+KKI6GAAVBOLh2vvVWWnCP1OgMdVb5JAdjDrjDT5i0GSQu0bgQ8fPsW6zw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, "node_modules/@cloudflare/workers-types": { - "version": "4.20250920.0", - "resolved": "https://registry.npmmirror.com/@cloudflare/workers-types/-/workers-types-4.20250920.0.tgz", - "integrity": "sha512-UkyHVhvCUw5E1kmyVYDafmKwL4d8LnBh5lRL9nNoavkSQjraYdg4Pbizw3QTiPrpKapw7oLp1EHBgZmYA4YvSg==", - "dev": true, - "license": "MIT OR Apache-2.0" + "version": "4.20251128.0", + "resolved": "https://registry.npmmirror.com/@cloudflare/workers-types/-/workers-types-4.20251128.0.tgz", + "integrity": "sha512-gQxQvxLRsFb+mDlaBKGoJwEHWt+ox9telZZEuRMbNUAD6v78XYqZepTI4yPDdKhkRTlqYcDqDhIdAI3HrsGk7w==", + "devOptional": true, + "license": "MIT OR Apache-2.0", + "peer": true + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmmirror.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } }, "node_modules/@css-render/plugin-bem": { "version": "0.15.14", @@ -571,6 +708,16 @@ "vue": "^3.2.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emotion/hash": { "version": "0.8.0", "resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz", @@ -1050,522 +1197,459 @@ "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", "license": "ISC" }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@juggle/resize-observer": { - "version": "3.4.0", - "resolved": "https://registry.npmmirror.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", - "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==", - "license": "Apache-2.0" - }, - "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", "optional": true, - "peer": true, - "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - }, + "os": [ + "darwin" + ], "engines": { - "node": ">= 10.0.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" + "@img/sharp-libvips-darwin-arm64": "1.0.4" } }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", "cpu": [ - "arm64" + "x64" ], - "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "android" + "darwin" ], - "peer": true, "engines": { - "node": ">= 10.0.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" } }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", "cpu": [ "arm64" ], - "dev": true, - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "darwin" ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/libvips" } }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", "cpu": [ "x64" ], - "dev": true, - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "darwin" ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/libvips" } }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", "cpu": [ - "x64" + "arm" ], - "dev": true, - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ - "freebsd" + "linux" ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/libvips" } }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", "cpu": [ - "arm" + "arm64" ], - "dev": true, - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/libvips" } }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", "cpu": [ - "arm" + "s390x" ], - "dev": true, - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/libvips" } }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", "cpu": [ - "arm64" + "x64" ], - "dev": true, - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/libvips" } }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", "cpu": [ "arm64" ], - "dev": true, - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/libvips" } }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", "cpu": [ "x64" ], - "dev": true, - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "peer": true, - "engines": { - "node": ">= 10.0.0" - }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/libvips" } }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", "cpu": [ - "x64" + "arm" ], - "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { - "node": ">= 10.0.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" } }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", "cpu": [ "arm64" ], - "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "win32" + "linux" ], - "peer": true, "engines": { - "node": ">= 10.0.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" } }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", "cpu": [ - "ia32" + "s390x" ], - "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "win32" + "linux" ], - "peer": true, "engines": { - "node": ">= 10.0.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" } }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", "cpu": [ "x64" ], - "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "win32" + "linux" ], - "peer": true, "engines": { - "node": ">= 10.0.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, - "license": "MIT" - }, - "node_modules/@popperjs/core": { - "name": "@sxzz/popperjs-es", - "version": "2.11.7", - "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz", - "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.29", - "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz", - "integrity": "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.50.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.1.tgz", - "integrity": "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.1.tgz", - "integrity": "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==", + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", "cpu": [ "arm64" ], - "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.1.tgz", - "integrity": "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==", - "cpu": [ - "arm64" + "linux" ], - "dev": true, - "license": "MIT", - "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-linuxmusl-arm64": "1.0.4" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.50.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.1.tgz", - "integrity": "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==", + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", "cpu": [ "x64" ], - "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.1.tgz", - "integrity": "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==", - "cpu": [ - "arm64" + "linux" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.50.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.1.tgz", - "integrity": "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==", + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", "cpu": [ - "x64" + "wasm32" ], - "dev": true, - "license": "MIT", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, - "os": [ - "freebsd" - ] + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.50.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.1.tgz", - "integrity": "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==", + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", "cpu": [ - "arm" + "ia32" ], - "dev": true, - "license": "MIT", + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ - "linux" - ] + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.50.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.1.tgz", - "integrity": "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==", + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", "cpu": [ - "arm" + "x64" ], - "dev": true, - "license": "MIT", + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ - "linux" - ] + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.1.tgz", - "integrity": "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==", + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@juggle/resize-observer": { + "version": "3.4.0", + "resolved": "https://registry.npmmirror.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", + "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==", + "license": "Apache-2.0" + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", "cpu": [ "arm64" ], @@ -1573,13 +1657,20 @@ "license": "MIT", "optional": true, "os": [ - "linux" - ] + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.50.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.1.tgz", - "integrity": "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==", + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", "cpu": [ "arm64" ], @@ -1587,97 +1678,146 @@ "license": "MIT", "optional": true, "os": [ - "linux" - ] + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.1.tgz", - "integrity": "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==", + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", "cpu": [ - "loong64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.1.tgz", - "integrity": "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==", + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", "cpu": [ - "ppc64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.1.tgz", - "integrity": "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==", + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", "cpu": [ - "riscv64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.50.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.1.tgz", - "integrity": "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==", + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", "cpu": [ - "riscv64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.1.tgz", - "integrity": "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==", + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", "cpu": [ - "s390x" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.1.tgz", - "integrity": "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==", + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.50.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.1.tgz", - "integrity": "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==", + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", "cpu": [ "x64" ], @@ -1686,26 +1826,40 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.1.tgz", - "integrity": "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==", + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openharmony" - ] + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.50.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.1.tgz", - "integrity": "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==", + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", "cpu": [ "arm64" ], @@ -1714,12 +1868,19 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.50.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.1.tgz", - "integrity": "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==", + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", "cpu": [ "ia32" ], @@ -1728,12 +1889,19 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.50.1", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.1.tgz", - "integrity": "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==", + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", "cpu": [ "x64" ], @@ -1742,704 +1910,1880 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@sec-ant/readable-stream": { - "version": "0.4.1", - "resolved": "https://registry.npmmirror.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", - "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sindresorhus/merge-streams": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", - "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", - "dev": true, - "license": "MIT", + ], "engines": { - "node": ">=18" + "node": ">= 10.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "license": "MIT" - }, - "node_modules/@types/katex": { - "version": "0.16.7", - "resolved": "https://registry.npmmirror.com/@types/katex/-/katex-0.16.7.tgz", - "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", - "license": "MIT" - }, - "node_modules/@types/lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, "license": "MIT" }, - "node_modules/@types/lodash-es": { - "version": "4.17.12", - "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", - "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.7", + "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz", + "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==", "license": "MIT", - "dependencies": { - "@types/lodash": "*" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" } }, - "node_modules/@types/node": { - "version": "24.4.0", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.4.0.tgz", - "integrity": "sha512-gUuVEAK4/u6F9wRLznPUU4WGUacSEBDPoC2TrBkw3GAnOLHBL45QdfHOXp1kJ4ypBGLxTOB+t7NJLpKoC3gznQ==", + "node_modules/@poppinss/colors": { + "version": "4.1.5", + "resolved": "https://registry.npmmirror.com/@poppinss/colors/-/colors-4.1.5.tgz", + "integrity": "sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw==", "license": "MIT", "dependencies": { - "undici-types": "~7.11.0" + "kleur": "^4.1.5" } }, - "node_modules/@types/node-fetch": { - "version": "2.6.13", - "resolved": "https://registry.npmmirror.com/@types/node-fetch/-/node-fetch-2.6.13.tgz", - "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmmirror.com/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", "license": "MIT", "dependencies": { - "@types/node": "*", - "form-data": "^4.0.4" + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" } }, - "node_modules/@types/web-bluetooth": { - "version": "0.0.16", - "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz", - "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==", + "node_modules/@poppinss/exception": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/@poppinss/exception/-/exception-1.2.2.tgz", + "integrity": "sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==", "license": "MIT" }, - "node_modules/@vicons/ionicons5": { - "version": "0.13.0", - "resolved": "https://registry.npmmirror.com/@vicons/ionicons5/-/ionicons5-0.13.0.tgz", - "integrity": "sha512-zvZKBPjEXKN7AXNo2Na2uy+nvuv6SP4KAMQxpKL2vfHMj0fSvuw7JZcOPCjQC3e7ayssKnaoFVAhbYcW6v41qQ==", + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz", + "integrity": "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==", + "dev": true, "license": "MIT" }, - "node_modules/@vitejs/plugin-vue": { - "version": "6.0.1", - "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz", - "integrity": "sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.50.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.1.tgz", + "integrity": "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.29" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", - "vue": "^3.2.25" - } + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@volar/language-core": { - "version": "2.4.23", - "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.23.tgz", - "integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.1.tgz", + "integrity": "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@volar/source-map": "2.4.23" - } - }, - "node_modules/@volar/source-map": { - "version": "2.4.23", - "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.23.tgz", - "integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==", - "license": "MIT" + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@volar/typescript": { - "version": "2.4.23", - "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.23.tgz", - "integrity": "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.1.tgz", + "integrity": "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@volar/language-core": "2.4.23", - "path-browserify": "^1.0.1", - "vscode-uri": "^3.0.8" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@vue/babel-helper-vue-transform-on": { - "version": "1.5.0", - "resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz", - "integrity": "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.50.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.1.tgz", + "integrity": "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@vue/babel-plugin-jsx": { - "version": "1.5.0", - "resolved": "https://registry.npmmirror.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.5.0.tgz", - "integrity": "sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==", + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.1.tgz", + "integrity": "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.2", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.50.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.1.tgz", + "integrity": "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.50.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.1.tgz", + "integrity": "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.50.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.1.tgz", + "integrity": "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.1.tgz", + "integrity": "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.50.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.1.tgz", + "integrity": "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.1.tgz", + "integrity": "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.1.tgz", + "integrity": "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.1.tgz", + "integrity": "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.50.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.1.tgz", + "integrity": "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.1.tgz", + "integrity": "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.1.tgz", + "integrity": "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.50.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.1.tgz", + "integrity": "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.1.tgz", + "integrity": "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.50.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.1.tgz", + "integrity": "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.50.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.1.tgz", + "integrity": "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.50.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.1.tgz", + "integrity": "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.11.0", + "resolved": "https://registry.npmmirror.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", + "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "selderee": "^0.11.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/@sindresorhus/is": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/@sindresorhus/is/-/is-7.1.1.tgz", + "integrity": "sha512-rO92VvpgMc3kfiTjGT52LEtJ8Yc5kCWhZjLQ3LwlA4pSgPpQO7bVpYXParOD8Jwf+cVQECJo3yP/4I8aZtUQTQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.12", + "resolved": "https://registry.npmmirror.com/@speed-highlight/core/-/core-1.2.12.tgz", + "integrity": "sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA==", + "license": "CC0-1.0" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/html-to-text": { + "version": "9.0.4", + "resolved": "https://registry.npmmirror.com/@types/html-to-text/-/html-to-text-9.0.4.tgz", + "integrity": "sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/katex": { + "version": "0.16.7", + "resolved": "https://registry.npmmirror.com/@types/katex/-/katex-0.16.7.tgz", + "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/node": { + "version": "24.4.0", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.4.0.tgz", + "integrity": "sha512-gUuVEAK4/u6F9wRLznPUU4WGUacSEBDPoC2TrBkw3GAnOLHBL45QdfHOXp1kJ4ypBGLxTOB+t7NJLpKoC3gznQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.11.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmmirror.com/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.16", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz", + "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==", + "license": "MIT" + }, + "node_modules/@vicons/ionicons5": { + "version": "0.13.0", + "resolved": "https://registry.npmmirror.com/@vicons/ionicons5/-/ionicons5-0.13.0.tgz", + "integrity": "sha512-zvZKBPjEXKN7AXNo2Na2uy+nvuv6SP4KAMQxpKL2vfHMj0fSvuw7JZcOPCjQC3e7ayssKnaoFVAhbYcW6v41qQ==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz", + "integrity": "sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.29" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.23", + "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.23.tgz", + "integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==", + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.23" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.23", + "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.23.tgz", + "integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==", + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.23", + "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.23.tgz", + "integrity": "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==", + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.23", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz", + "integrity": "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.5.0.tgz", + "integrity": "sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.2", "@vue/babel-helper-vue-transform-on": "1.5.0", "@vue/babel-plugin-resolve-type": "1.5.0", "@vue/shared": "^3.5.18" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + } + } + }, + "node_modules/@vue/babel-plugin-resolve-type": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.5.0.tgz", + "integrity": "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/parser": "^7.28.0", + "@vue/compiler-sfc": "^3.5.18" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.21", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.21.tgz", + "integrity": "sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@vue/shared": "3.5.21", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.21", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.21.tgz", + "integrity": "sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.21", + "@vue/shared": "3.5.21" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.21", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.21.tgz", + "integrity": "sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@vue/compiler-core": "3.5.21", + "@vue/compiler-dom": "3.5.21", + "@vue/compiler-ssr": "3.5.21", + "@vue/shared": "3.5.21", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.18", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.21", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.21.tgz", + "integrity": "sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.21", + "@vue/shared": "3.5.21" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.7", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.7.tgz", + "integrity": "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.7" + } + }, + "node_modules/@vue/devtools-core": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/@vue/devtools-core/-/devtools-core-8.0.2.tgz", + "integrity": "sha512-V7eKTTHoS6KfK8PSGMLZMhGv/9yNDrmv6Qc3r71QILulnzPnqK2frsTyx3e2MrhdUZnENPEm6hcb4z0GZOqNhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.0.2", + "@vue/devtools-shared": "^8.0.2", + "mitt": "^3.0.1", + "nanoid": "^5.1.5", + "pathe": "^2.0.3", + "vite-hot-client": "^2.1.0" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/@vue/devtools-core/node_modules/@vue/devtools-kit": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-8.0.2.tgz", + "integrity": "sha512-yjZKdEmhJzQqbOh4KFBfTOQjDPMrjjBNCnHBvnTGJX+YLAqoUtY2J+cg7BE+EA8KUv8LprECq04ts75wCoIGWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.0.2", + "birpc": "^2.5.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^2.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-core/node_modules/@vue/devtools-shared": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-8.0.2.tgz", + "integrity": "sha512-mLU0QVdy5Lp40PMGSixDw/Kbd6v5dkQXltd2r+mdVQV7iUog2NlZuLxFZApFZ/mObUBDhoCpf0T3zF2FWWdeHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/devtools-core/node_modules/nanoid": { + "version": "5.1.5", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-5.1.5.tgz", + "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@vue/devtools-core/node_modules/perfect-debounce": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-2.0.0.tgz", + "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.7", + "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz", + "integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.7", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.7", + "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz", + "integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/language-core": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.1.5.tgz", + "integrity": "sha512-FMcqyzWN+sYBeqRMWPGT2QY0mUasZMVIuHvmb5NT3eeqPrbHBYtCP8JWEUCDCgM+Zr62uuWY/qoeBrPrzfa78w==", + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.23", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.0.0", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.2" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.21", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.21.tgz", + "integrity": "sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.21" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.21", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.21.tgz", + "integrity": "sha512-+DplQlRS4MXfIf9gfD1BOJpk5RSyGgGXD/R+cumhe8jdjUcq/qlxDawQlSI8hCKupBlvM+3eS1se5xW+SuNAwA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.21", + "@vue/shared": "3.5.21" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.21", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.21.tgz", + "integrity": "sha512-3M2DZsOFwM5qI15wrMmNF5RJe1+ARijt2HM3TbzBbPSuBHOQpoidE+Pa+XEaVN+czbHf81ETRoG1ltztP2em8w==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.21", + "@vue/runtime-core": "3.5.21", + "@vue/shared": "3.5.21", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.21", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.21.tgz", + "integrity": "sha512-qr8AqgD3DJPJcGvLcJKQo2tAc8OnXRcfxhOJCPF+fcfn5bBGz7VCcO7t+qETOPxpWK1mgysXvVT/j+xWaHeMWA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.21", + "@vue/shared": "3.5.21" + }, + "peerDependencies": { + "vue": "3.5.21" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.21", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.21.tgz", + "integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "9.13.0", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz", + "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.16", + "@vueuse/metadata": "9.13.0", + "@vueuse/shared": "9.13.0", + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "9.13.0", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz", + "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "9.13.0", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz", + "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==", + "license": "MIT", + "dependencies": { + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmmirror.com/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/alien-signals": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-3.1.1.tgz", + "integrity": "sha512-ogkIWbVrLwKtHY6oOAXaYkAxP+cTH7V5FZ5+Tm4NZFd8VDZ6uNMDrfzqctTZ42eTMCSR3ne3otpcxmqSnFfPYA==", + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/ansis/-/ansis-4.1.0.tgz", + "integrity": "sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.2", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.2.tgz", + "integrity": "sha512-NvcIedLxrs9llVpX7wI+Jz4Hn9vJQkCPKrTaHIE0sW/Rj1iq6Fzby4NbyTZjQJNoypBXNaG7tEHkTgONZpwgxQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/birpc": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.5.0.tgz", + "integrity": "sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "license": "MIT" + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.26.0", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.26.0.tgz", + "integrity": "sha512-P9go2WrP9FiPwLv3zqRD/Uoxo0RSHjzFCiQz7d4vbmwNqQFo9T9WCeP/Qn5EbcKQY6DBbkxEXNcpJOmncNrb7A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.8.2", + "caniuse-lite": "^1.0.30001741", + "electron-to-chromium": "^1.5.218", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - } + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/@vue/babel-plugin-resolve-type": { - "version": "1.5.0", - "resolved": "https://registry.npmmirror.com/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.5.0.tgz", - "integrity": "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==", + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/parser": "^7.28.0", - "@vue/compiler-sfc": "^3.5.18" + "run-applescript": "^7.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sxzz" + "engines": { + "node": ">=18" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@vue/compiler-core": { - "version": "3.5.21", - "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.21.tgz", - "integrity": "sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==", + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.3", - "@vue/shared": "3.5.21", - "entities": "^4.5.0", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/@vue/compiler-core/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "license": "MIT" + "node_modules/caniuse-lite": { + "version": "1.0.30001741", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", + "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" }, - "node_modules/@vue/compiler-dom": { - "version": "3.5.21", - "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.21.tgz", - "integrity": "sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==", + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.21", - "@vue/shared": "3.5.21" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@vue/compiler-sfc": { - "version": "3.5.21", - "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.21.tgz", - "integrity": "sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==", + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.3", - "@vue/compiler-core": "3.5.21", - "@vue/compiler-dom": "3.5.21", - "@vue/compiler-ssr": "3.5.21", - "@vue/shared": "3.5.21", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.18", - "postcss": "^8.5.6", - "source-map-js": "^1.2.1" + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "license": "MIT" - }, - "node_modules/@vue/compiler-ssr": { - "version": "3.5.21", - "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.21.tgz", - "integrity": "sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w==", + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.21", - "@vue/shared": "3.5.21" + "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/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/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/cloudflare": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/cloudflare/-/cloudflare-5.1.0.tgz", + "integrity": "sha512-J2vT90WHio8VSbGUmuHckJr9pabH6N+nQKQsgjfwydgV2bgyQoV/KqWw1Df+YKZ5T/QqP7KadKr4bxh+WiRafg==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" } }, - "node_modules/@vue/compiler-vue2": { - "version": "2.7.16", - "resolved": "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", - "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "node_modules/cloudflare/node_modules/@types/node": { + "version": "18.19.127", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-18.19.127.tgz", + "integrity": "sha512-gSjxjrnKXML/yo0BO099uPixMqfpJU0TKYjpfLU7TrtA2WWDki412Np/RSTPRil1saKBhvVVKzVx/p/6p94nVA==", "license": "MIT", "dependencies": { - "de-indent": "^1.0.2", - "he": "^1.2.0" + "undici-types": "~5.26.4" } }, - "node_modules/@vue/devtools-api": { - "version": "7.7.7", - "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.7.tgz", - "integrity": "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==", + "node_modules/cloudflare/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", "license": "MIT", "dependencies": { - "@vue/devtools-kit": "^7.7.7" + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" } }, - "node_modules/@vue/devtools-core": { - "version": "8.0.2", - "resolved": "https://registry.npmmirror.com/@vue/devtools-core/-/devtools-core-8.0.2.tgz", - "integrity": "sha512-V7eKTTHoS6KfK8PSGMLZMhGv/9yNDrmv6Qc3r71QILulnzPnqK2frsTyx3e2MrhdUZnENPEm6hcb4z0GZOqNhw==", - "dev": true, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { - "@vue/devtools-kit": "^8.0.2", - "@vue/devtools-shared": "^8.0.2", - "mitt": "^3.0.1", - "nanoid": "^5.1.5", - "pathe": "^2.0.3", - "vite-hot-client": "^2.1.0" + "color-name": "~1.1.4" }, - "peerDependencies": { - "vue": "^3.0.0" + "engines": { + "node": ">=7.0.0" } }, - "node_modules/@vue/devtools-core/node_modules/@vue/devtools-kit": { - "version": "8.0.2", - "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-8.0.2.tgz", - "integrity": "sha512-yjZKdEmhJzQqbOh4KFBfTOQjDPMrjjBNCnHBvnTGJX+YLAqoUtY2J+cg7BE+EA8KUv8LprECq04ts75wCoIGWA==", - "dev": true, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", "license": "MIT", "dependencies": { - "@vue/devtools-shared": "^8.0.2", - "birpc": "^2.5.0", - "hookable": "^5.5.3", - "mitt": "^3.0.1", - "perfect-debounce": "^2.0.0", - "speakingurl": "^14.0.1", - "superjson": "^2.2.2" + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" } }, - "node_modules/@vue/devtools-core/node_modules/@vue/devtools-shared": { - "version": "8.0.2", - "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-8.0.2.tgz", - "integrity": "sha512-mLU0QVdy5Lp40PMGSixDw/Kbd6v5dkQXltd2r+mdVQV7iUog2NlZuLxFZApFZ/mObUBDhoCpf0T3zF2FWWdeHw==", - "dev": true, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "license": "MIT", "dependencies": { - "rfdc": "^1.4.1" + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, - "node_modules/@vue/devtools-core/node_modules/nanoid": { - "version": "5.1.5", - "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-5.1.5.tgz", - "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==", + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmmirror.com/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.js" + "dependencies": { + "has-flag": "^4.0.0" }, "engines": { - "node": "^18 || >=20" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/@vue/devtools-core/node_modules/perfect-debounce": { + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "license": "MIT" + }, + "node_modules/convert-source-map": { "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-2.0.0.tgz", - "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, - "node_modules/@vue/devtools-kit": { - "version": "7.7.7", - "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz", - "integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==", + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "license": "MIT", - "dependencies": { - "@vue/devtools-shared": "^7.7.7", - "birpc": "^2.3.0", - "hookable": "^5.5.3", - "mitt": "^3.0.1", - "perfect-debounce": "^1.0.0", - "speakingurl": "^14.0.1", - "superjson": "^2.2.2" + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@vue/devtools-shared": { - "version": "7.7.7", - "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz", - "integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==", + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", "license": "MIT", "dependencies": { - "rfdc": "^1.4.1" + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" } }, - "node_modules/@vue/language-core": { - "version": "3.0.7", - "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.0.7.tgz", - "integrity": "sha512-0sqqyqJ0Gn33JH3TdIsZLCZZ8Gr4kwlg8iYOnOrDDkJKSjFurlQY/bEFQx5zs7SX2C/bjMkmPYq/NiyY1fTOkw==", + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.23", - "@vue/compiler-dom": "^3.5.0", - "@vue/compiler-vue2": "^2.7.16", - "@vue/shared": "^3.5.0", - "alien-signals": "^2.0.5", - "muggle-string": "^0.4.1", - "path-browserify": "^1.0.1", - "picomatch": "^4.0.2" - }, - "peerDependencies": { - "typescript": "*" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": ">= 8" } }, - "node_modules/@vue/reactivity": { - "version": "3.5.21", - "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.21.tgz", - "integrity": "sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA==", + "node_modules/css-render": { + "version": "0.15.14", + "resolved": "https://registry.npmmirror.com/css-render/-/css-render-0.15.14.tgz", + "integrity": "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==", "license": "MIT", + "peer": true, "dependencies": { - "@vue/shared": "3.5.21" + "@emotion/hash": "~0.8.0", + "csstype": "~3.0.5" } }, - "node_modules/@vue/runtime-core": { - "version": "3.5.21", - "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.21.tgz", - "integrity": "sha512-+DplQlRS4MXfIf9gfD1BOJpk5RSyGgGXD/R+cumhe8jdjUcq/qlxDawQlSI8hCKupBlvM+3eS1se5xW+SuNAwA==", + "node_modules/css-render/node_modules/csstype": { + "version": "3.0.11", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.0.11.tgz", + "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.21", - "@vue/shared": "3.5.21" + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" } }, - "node_modules/@vue/runtime-dom": { - "version": "3.5.21", - "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.21.tgz", - "integrity": "sha512-3M2DZsOFwM5qI15wrMmNF5RJe1+ARijt2HM3TbzBbPSuBHOQpoidE+Pa+XEaVN+czbHf81ETRoG1ltztP2em8w==", + "node_modules/date-fns-tz": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/date-fns-tz/-/date-fns-tz-3.2.0.tgz", + "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.21", - "@vue/runtime-core": "3.5.21", - "@vue/shared": "3.5.21", - "csstype": "^3.1.3" + "peerDependencies": { + "date-fns": "^3.0.0 || ^4.0.0" } }, - "node_modules/@vue/server-renderer": { - "version": "3.5.21", - "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.21.tgz", - "integrity": "sha512-qr8AqgD3DJPJcGvLcJKQo2tAc8OnXRcfxhOJCPF+fcfn5bBGz7VCcO7t+qETOPxpWK1mgysXvVT/j+xWaHeMWA==", + "node_modules/dayjs": { + "version": "1.11.18", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.18.tgz", + "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.21", - "@vue/shared": "3.5.21" + "ms": "^2.1.3" }, - "peerDependencies": { - "vue": "3.5.21" + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@vue/shared": { - "version": "3.5.21", - "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.21.tgz", - "integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==", - "license": "MIT" + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/@vueuse/core": { - "version": "9.13.0", - "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz", - "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==", + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, "license": "MIT", "dependencies": { - "@types/web-bluetooth": "^0.0.16", - "@vueuse/metadata": "9.13.0", - "@vueuse/shared": "9.13.0", - "vue-demi": "*" + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/antfu" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@vueuse/core/node_modules/vue-demi": { - "version": "0.14.10", - "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", - "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", - "hasInstallScript": true, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, "license": "MIT", - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" + "engines": { + "node": ">=18" }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@vueuse/metadata": { - "version": "9.13.0", - "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz", - "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==", + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" + "engines": { + "node": ">=0.4.0" } }, - "node_modules/@vueuse/shared": { - "version": "9.13.0", - "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz", - "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==", + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "license": "MIT", "dependencies": { - "vue-demi": "*" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" }, "funding": { - "url": "https://github.com/sponsors/antfu" + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/@vueuse/shared/node_modules/vue-demi": { - "version": "0.14.10", - "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", - "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" }, "engines": { - "node": ">=12" + "node": ">= 4" }, "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", "dependencies": { - "event-target-shim": "^5.0.0" + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" }, - "engines": { - "node": ">=6.5" + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" }, "engines": { - "node": ">=0.4.0" + "node": ">= 0.4" } }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmmirror.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "node_modules/electron-to-chromium": { + "version": "1.5.218", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.218.tgz", + "integrity": "sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==", + "dev": true, + "license": "ISC" + }, + "node_modules/element-plus": { + "version": "2.11.3", + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.11.3.tgz", + "integrity": "sha512-769xsjLR4B9Vf9cl5PDXnwTEdmFJvMgAkYtthdJKPhjVjU3hdAwTJ+gXKiO+PUyo2KWFwOYKZd4Ywh6PHfkbJg==", "license": "MIT", "dependencies": { - "humanize-ms": "^1.2.1" + "@ctrl/tinycolor": "^3.4.1", + "@element-plus/icons-vue": "^2.3.1", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.14.182", + "@types/lodash-es": "^4.17.6", + "@vueuse/core": "^9.1.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.13", + "escape-html": "^1.0.3", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "lodash-unified": "^1.0.2", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0" }, - "engines": { - "node": ">= 8.0.0" + "peerDependencies": { + "vue": "^3.2.0" } }, - "node_modules/alien-signals": { - "version": "2.0.7", - "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-2.0.7.tgz", - "integrity": "sha512-wE7y3jmYeb0+h6mr5BOovuqhFv22O/MV9j5p0ndJsa7z1zJNPGQ4ph5pQk/kTTCWRC3xsA4SmtwmkzQO+7NCNg==", + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, - "node_modules/ansis": { - "version": "4.1.0", - "resolved": "https://registry.npmmirror.com/ansis/-/ansis-4.1.0.tgz", - "integrity": "sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==", - "dev": true, - "license": "ISC", + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", "engines": { - "node": ">=14" + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" } }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">= 0.4" } }, - "node_modules/async-validator": { - "version": "4.2.5", - "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", - "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", - "license": "MIT" + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } }, - "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmmirror.com/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/baseline-browser-mapping": { - "version": "2.8.2", - "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.2.tgz", - "integrity": "sha512-NvcIedLxrs9llVpX7wI+Jz4Hn9vJQkCPKrTaHIE0sW/Rj1iq6Fzby4NbyTZjQJNoypBXNaG7tEHkTgONZpwgxQ==", + "node_modules/esbuild": { + "version": "0.25.9", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", "dev": true, - "license": "Apache-2.0", + "hasInstallScript": true, + "license": "MIT", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" } }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/birpc": { - "version": "2.5.0", - "resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.5.0.tgz", - "integrity": "sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==", + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" + "dependencies": { + "@types/estree": "^1.0.0" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/browserslist": { - "version": "4.26.0", - "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.26.0.tgz", - "integrity": "sha512-P9go2WrP9FiPwLv3zqRD/Uoxo0RSHjzFCiQz7d4vbmwNqQFo9T9WCeP/Qn5EbcKQY6DBbkxEXNcpJOmncNrb7A==", + "node_modules/evtd": { + "version": "0.2.4", + "resolved": "https://registry.npmmirror.com/evtd/-/evtd-0.2.4.tgz", + "integrity": "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==", + "license": "MIT" + }, + "node_modules/execa": { + "version": "9.6.0", + "resolved": "https://registry.npmmirror.com/execa/-/execa-9.6.0.tgz", + "integrity": "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.2", - "caniuse-lite": "^1.0.30001741", - "electron-to-chromium": "^1.5.218", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" }, - "bin": { - "browserslist": "cli.js" + "engines": { + "node": "^18.19.0 || >=20.5.0" }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit-hook": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/exit-hook/-/exit-hook-2.2.1.tgz", + "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", + "license": "MIT", "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "license": "MIT" + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", "dev": true, "license": "MIT", "dependencies": { - "run-applescript": "^7.0.0" + "is-unicode-supported": "^2.0.0" }, "engines": { "node": ">=18" @@ -2448,462 +3792,433 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "to-regex-range": "^5.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">=8" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001741", - "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", - "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", - "dev": true, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" } ], - "license": "CC-BY-4.0" - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "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/" + "node": ">=4.0" }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/cloudflare": { - "version": "5.1.0", - "resolved": "https://registry.npmmirror.com/cloudflare/-/cloudflare-5.1.0.tgz", - "integrity": "sha512-J2vT90WHio8VSbGUmuHckJr9pabH6N+nQKQsgjfwydgV2bgyQoV/KqWw1Df+YKZ5T/QqP7KadKr4bxh+WiRafg==", - "license": "Apache-2.0", - "dependencies": { - "@types/node": "^18.11.18", - "@types/node-fetch": "^2.6.4", - "abort-controller": "^3.0.0", - "agentkeepalive": "^4.2.1", - "form-data-encoder": "1.7.2", - "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7" + "peerDependenciesMeta": { + "debug": { + "optional": true + } } }, - "node_modules/cloudflare/node_modules/@types/node": { - "version": "18.19.127", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-18.19.127.tgz", - "integrity": "sha512-gSjxjrnKXML/yo0BO099uPixMqfpJU0TKYjpfLU7TrtA2WWDki412Np/RSTPRil1saKBhvVVKzVx/p/6p94nVA==", + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/cloudflare/node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", "license": "MIT", "dependencies": { - "delayed-stream": "~1.0.0" + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" }, "engines": { - "node": ">= 0.8" + "node": ">= 12.20" } }, - "node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", - "license": "MIT" + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "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.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, - "license": "MIT" + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } }, - "node_modules/copy-anything": { - "version": "3.0.5", - "resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-3.0.5.tgz", - "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "is-what": "^4.1.8" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { - "node": ">=12.13" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/mesqueeb" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">= 8" + "node": ">= 0.4" } }, - "node_modules/css-render": { - "version": "0.15.14", - "resolved": "https://registry.npmmirror.com/css-render/-/css-render-0.15.14.tgz", - "integrity": "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==", + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, "license": "MIT", "dependencies": { - "@emotion/hash": "~0.8.0", - "csstype": "~3.0.5" + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/css-render/node_modules/csstype": { - "version": "3.0.11", - "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.0.11.tgz", - "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==", - "license": "MIT" + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" }, - "node_modules/date-fns": { - "version": "3.6.0", - "resolved": "https://registry.npmmirror.com/date-fns/-/date-fns-3.6.0.tgz", - "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/date-fns-tz": { - "version": "3.2.0", - "resolved": "https://registry.npmmirror.com/date-fns-tz/-/date-fns-tz-3.2.0.tgz", - "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", - "peerDependencies": { - "date-fns": "^3.0.0 || ^4.0.0" + "engines": { + "node": ">=8" } }, - "node_modules/dayjs": { - "version": "1.11.18", - "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.18.tgz", - "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", - "license": "MIT" + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/de-indent": { + "node_modules/has-tostringtag": { "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz", - "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "has-symbols": "^1.0.3" }, "engines": { - "node": ">=6.0" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/default-browser": { - "version": "5.2.1", - "resolved": "https://registry.npmmirror.com/default-browser/-/default-browser-5.2.1.tgz", - "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", - "dev": true, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" + "function-bind": "^1.1.2" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.4" } }, - "node_modules/default-browser-id": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/default-browser-id/-/default-browser-id-5.0.0.tgz", - "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", - "dev": true, - "license": "MIT", + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12.0.0" } }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, + "node_modules/hono": { + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.10.3.tgz", + "integrity": "sha512-2LOYWUbnhdxdL8MNbNg9XZig6k+cZXm5IjHn2Aviv7honhBMOHb+jxrKIeJRZJRmn+htUCKhaicxwXuUDlchRA==", "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=16.9.0" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/html-to-text": { + "version": "9.0.5", + "resolved": "https://registry.npmmirror.com/html-to-text/-/html-to-text-9.0.5.tgz", + "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", "license": "MIT", + "dependencies": { + "@selderee/plugin-htmlparser2": "^0.11.0", + "deepmerge": "^4.3.1", + "dom-serializer": "^2.0.0", + "htmlparser2": "^8.0.2", + "selderee": "^0.11.0" + }, "engines": { - "node": ">=0.4.0" + "node": ">=14" } }, - "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", "dev": true, "license": "Apache-2.0", - "optional": true, - "peer": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, "engines": { - "node": ">=0.10" + "node": ">=18.18.0" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/immutable": { + "version": "5.1.3", + "resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.3.tgz", + "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" + "binary-extensions": "^2.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=8" } }, - "node_modules/electron-to-chromium": { - "version": "1.5.218", - "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.218.tgz", - "integrity": "sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==", + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "dev": true, - "license": "ISC" - }, - "node_modules/element-plus": { - "version": "2.11.3", - "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.11.3.tgz", - "integrity": "sha512-769xsjLR4B9Vf9cl5PDXnwTEdmFJvMgAkYtthdJKPhjVjU3hdAwTJ+gXKiO+PUyo2KWFwOYKZd4Ywh6PHfkbJg==", "license": "MIT", - "dependencies": { - "@ctrl/tinycolor": "^3.4.1", - "@element-plus/icons-vue": "^2.3.1", - "@floating-ui/dom": "^1.0.1", - "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", - "@types/lodash": "^4.14.182", - "@types/lodash-es": "^4.17.6", - "@vueuse/core": "^9.1.0", - "async-validator": "^4.2.5", - "dayjs": "^1.11.13", - "escape-html": "^1.0.3", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "lodash-unified": "^1.0.2", - "memoize-one": "^6.0.0", - "normalize-wheel-es": "^1.2.0" + "bin": { + "is-docker": "cli.js" }, - "peerDependencies": { - "vue": "^3.2.0" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", "engines": { - "node": ">=0.12" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/error-stack-parser-es": { - "version": "1.0.5", - "resolved": "https://registry.npmmirror.com/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", - "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=8" } }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "license": "MIT", "dependencies": { - "es-errors": "^1.3.0" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" + "is-docker": "^3.0.0" }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.25.9", - "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.9.tgz", - "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", "bin": { - "esbuild": "bin/esbuild" + "is-inside-container": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=14.16" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.9", - "@esbuild/android-arm": "0.25.9", - "@esbuild/android-arm64": "0.25.9", - "@esbuild/android-x64": "0.25.9", - "@esbuild/darwin-arm64": "0.25.9", - "@esbuild/darwin-x64": "0.25.9", - "@esbuild/freebsd-arm64": "0.25.9", - "@esbuild/freebsd-x64": "0.25.9", - "@esbuild/linux-arm": "0.25.9", - "@esbuild/linux-arm64": "0.25.9", - "@esbuild/linux-ia32": "0.25.9", - "@esbuild/linux-loong64": "0.25.9", - "@esbuild/linux-mips64el": "0.25.9", - "@esbuild/linux-ppc64": "0.25.9", - "@esbuild/linux-riscv64": "0.25.9", - "@esbuild/linux-s390x": "0.25.9", - "@esbuild/linux-x64": "0.25.9", - "@esbuild/netbsd-arm64": "0.25.9", - "@esbuild/netbsd-x64": "0.25.9", - "@esbuild/openbsd-arm64": "0.25.9", - "@esbuild/openbsd-x64": "0.25.9", - "@esbuild/openharmony-arm64": "0.25.9", - "@esbuild/sunos-x64": "0.25.9", - "@esbuild/win32-arm64": "0.25.9", - "@esbuild/win32-ia32": "0.25.9", - "@esbuild/win32-x64": "0.25.9" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "license": "MIT", "engines": { - "node": ">=6" + "node": ">=0.12.0" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -2912,466 +4227,524 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/evtd": { - "version": "0.2.4", - "resolved": "https://registry.npmmirror.com/evtd/-/evtd-0.2.4.tgz", - "integrity": "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==", - "license": "MIT" - }, - "node_modules/execa": { - "version": "9.6.0", - "resolved": "https://registry.npmmirror.com/execa/-/execa-9.6.0.tgz", - "integrity": "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==", - "dev": true, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmmirror.com/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", "license": "MIT", - "dependencies": { - "@sindresorhus/merge-streams": "^4.0.0", - "cross-spawn": "^7.0.6", - "figures": "^6.1.0", - "get-stream": "^9.0.0", - "human-signals": "^8.0.1", - "is-plain-obj": "^4.1.0", - "is-stream": "^4.0.1", - "npm-run-path": "^6.0.0", - "pretty-ms": "^9.2.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^4.0.0", - "yoctocolors": "^2.1.1" - }, "engines": { - "node": "^18.19.0 || >=20.5.0" + "node": ">=12.13" }, "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" + "url": "https://github.com/sponsors/mesqueeb" } }, - "node_modules/exsolve": { - "version": "1.0.7", - "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.7.tgz", - "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", - "license": "MIT" - }, - "node_modules/figures": { - "version": "6.1.0", - "resolved": "https://registry.npmmirror.com/figures/-/figures-6.1.0.tgz", - "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", "dev": true, "license": "MIT", "dependencies": { - "is-unicode-supported": "^2.0.0" + "is-inside-container": "^1.0.0" }, "engines": { - "node": ">=18" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" + "bin": { + "jsesc": "bin/jsesc" }, "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=4.0" + "bin": { + "json5": "lib/cli.js" }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } + "engines": { + "node": ">=6" } }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmmirror.com/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, "engines": { - "node": ">= 6" + "node": ">=6" } }, - "node_modules/form-data-encoder": { - "version": "1.7.2", - "resolved": "https://registry.npmmirror.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz", - "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, "license": "MIT" }, - "node_modules/formdata-node": { - "version": "4.4.1", - "resolved": "https://registry.npmmirror.com/formdata-node/-/formdata-node-4.4.1.tgz", - "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "node_modules/leac": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/leac/-/leac-0.6.0.tgz", + "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/local-pkg": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-1.1.2.tgz", + "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", "license": "MIT", "dependencies": { - "node-domexception": "1.0.0", - "web-streams-polyfill": "4.0.0-beta.3" + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" }, "engines": { - "node": ">= 12.20" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } + "peer": true }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peer": true + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, "engines": { "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "9.0.1", - "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-9.0.1.tgz", - "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@sec-ant/readable-stream": "^0.4.1", - "is-stream": "^4.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { - "node": ">= 6" + "node": ">=8.6" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "license": "MIT", + "optional": true, "engines": { - "node": ">= 0.4" + "node": ">=8.6" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", "license": "MIT", - "engines": { - "node": ">= 0.4" + "bin": { + "mime": "cli.js" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { - "has-symbols": "^1.0.3" + "mime-db": "1.52.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.6" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/miniflare": { + "version": "4.20251125.0", + "resolved": "https://registry.npmmirror.com/miniflare/-/miniflare-4.20251125.0.tgz", + "integrity": "sha512-xY6deLx0Drt8GfGG2Fv0fHUocHAIG/Iv62Kl36TPfDzgq7/+DQ5gYNisxnmyISQdA/sm7kOvn2XRBncxjWYrLg==", "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "@cspotcode/source-map-support": "0.8.1", + "acorn": "8.14.0", + "acorn-walk": "8.3.2", + "exit-hook": "2.2.1", + "glob-to-regexp": "0.4.1", + "sharp": "^0.33.5", + "stoppable": "1.1.0", + "undici": "7.14.0", + "workerd": "1.20251125.0", + "ws": "8.18.0", + "youch": "4.1.0-beta.10", + "zod": "3.22.3" + }, + "bin": { + "miniflare": "bootstrap.js" }, "engines": { - "node": ">= 0.4" + "node": ">=18.0.0" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "node_modules/miniflare/node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "license": "MIT", "bin": { - "he": "bin/he" - } - }, - "node_modules/highlight.js": { - "version": "11.11.1", - "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz", - "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", - "license": "BSD-3-Clause", + "acorn": "bin/acorn" + }, "engines": { - "node": ">=12.0.0" + "node": ">=0.4.0" } }, - "node_modules/hono": { - "version": "4.9.8", - "resolved": "https://registry.npmmirror.com/hono/-/hono-4.9.8.tgz", - "integrity": "sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg==", + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", "license": "MIT", - "engines": { - "node": ">=16.9.0" + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" } }, - "node_modules/hookable": { - "version": "5.5.3", - "resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz", - "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", "license": "MIT" }, - "node_modules/human-signals": { - "version": "8.0.1", - "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-8.0.1.tgz", - "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", "license": "MIT", "dependencies": { - "ms": "^2.0.0" + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" } }, - "node_modules/immutable": { - "version": "5.1.3", - "resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.3.tgz", - "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==", + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "engines": { + "node": ">=10" + } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "license": "MIT" + }, + "node_modules/naive-ui": { + "version": "2.43.1", + "resolved": "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.43.1.tgz", + "integrity": "sha512-w52W0mOhdOGt4uucFSZmP0DI44PCsFyuxeLSs9aoUThfIuxms90MYjv46Qrr7xprjyJRw5RU6vYpCx4o9ind3A==", "license": "MIT", "dependencies": { - "binary-extensions": "^2.0.0" + "@css-render/plugin-bem": "^0.15.14", + "@css-render/vue3-ssr": "^0.15.14", + "@types/katex": "^0.16.2", + "@types/lodash": "^4.14.198", + "@types/lodash-es": "^4.17.9", + "async-validator": "^4.2.5", + "css-render": "^0.15.14", + "csstype": "^3.1.3", + "date-fns": "^3.6.0", + "date-fns-tz": "^3.1.3", + "evtd": "^0.2.4", + "highlight.js": "^11.8.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "seemly": "^0.3.8", + "treemate": "^0.3.11", + "vdirs": "^0.1.8", + "vooks": "^0.2.12", + "vueuc": "^0.4.65" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "vue": "^3.0.0" } }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "bin": { - "is-docker": "cli.js" + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "optional": true }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, "engines": { - "node": ">=0.10.0" + "node": ">=10.5.0" } }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "license": "MIT", "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" + "whatwg-url": "^5.0.0" }, "engines": { - "node": ">=14.16" + "node": "4.x || >=6.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/node-releases": { + "version": "2.0.21", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.21.tgz", + "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "license": "MIT", "engines": { - "node": ">=0.12.0" + "node": ">=0.10.0" } }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", "dev": true, "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-stream": { - "version": "4.0.1", - "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-4.0.1.tgz", - "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmmirror.com/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmmirror.com/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "dev": true, "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, "engines": { "node": ">=18" }, @@ -3379,190 +4752,214 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-what": { - "version": "4.1.16", - "resolved": "https://registry.npmmirror.com/is-what/-/is-what-4.1.16.tgz", - "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, "license": "MIT", "engines": { - "node": ">=12.13" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/mesqueeb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", - "dev": true, + "node_modules/parseley": { + "version": "0.12.1", + "resolved": "https://registry.npmmirror.com/parseley/-/parseley-0.12.1.tgz", + "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", "license": "MIT", "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" + "leac": "^0.6.0", + "peberminta": "^0.9.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://ko-fi.com/killymxi" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" }, - "node_modules/jiti": { - "version": "2.5.1", - "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.5.1.tgz", - "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "jiti": "lib/jiti-cli.mjs" + "engines": { + "node": ">=8" } }, - "node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "license": "MIT" }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, + "node_modules/peberminta": { + "version": "0.9.0", + "resolved": "https://registry.npmmirror.com/peberminta/-/peberminta-0.9.0.tgz", + "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" + "funding": { + "url": "https://ko-fi.com/killymxi" } }, - "node_modules/kolorist": { - "version": "1.8.0", - "resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz", - "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", - "dev": true, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", "license": "MIT" }, - "node_modules/local-pkg": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-1.1.2.tgz", - "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "dependencies": { - "mlly": "^1.7.4", - "pkg-types": "^2.3.0", - "quansync": "^0.2.11" - }, "engines": { - "node": ">=14" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/antfu" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "license": "MIT" - }, - "node_modules/lodash-unified": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", - "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "node_modules/pinia": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.3.tgz", + "integrity": "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==", "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.2" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, "peerDependencies": { - "@types/lodash-es": "*", - "lodash": "*", - "lodash-es": "*" + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "license": "MIT", "dependencies": { - "yallist": "^3.0.2" + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" } }, - "node_modules/magic-string": { - "version": "0.30.19", - "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.19.tgz", - "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "node_modules/postal-mime": { + "version": "2.6.1", + "resolved": "https://registry.npmmirror.com/postal-mime/-/postal-mime-2.6.1.tgz", + "integrity": "sha512-YnNYvLTsWzk1Mpf1drc7XZPR1c2XInI94H5RY/hZ9vzTSYjLw9zuebdi/K0yfR3PVnedQZUn2umTyxfw9yOWBA==", + "license": "MIT-0" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "node_modules/pretty-ms": { + "version": "9.2.0", + "resolved": "https://registry.npmmirror.com/pretty-ms/-/pretty-ms-9.2.0.tgz", + "integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==", + "dev": true, "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, "engines": { - "node": ">= 0.4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/memoize-one": { - "version": "6.0.0", - "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", - "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmmirror.com/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], "license": "MIT" }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "picomatch": "^2.2.1" }, "engines": { - "node": ">=8.6" + "node": ">=8.10.0" } }, - "node_modules/micromatch/node_modules/picomatch": { + "node_modules/readdirp/node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", - "optional": true, - "peer": true, "engines": { "node": ">=8.6" }, @@ -3570,263 +4967,357 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=0.10.0" } }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.50.1", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.50.1.tgz", + "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==", + "dev": true, "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" }, "engines": { - "node": ">= 0.6" + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.50.1", + "@rollup/rollup-android-arm64": "4.50.1", + "@rollup/rollup-darwin-arm64": "4.50.1", + "@rollup/rollup-darwin-x64": "4.50.1", + "@rollup/rollup-freebsd-arm64": "4.50.1", + "@rollup/rollup-freebsd-x64": "4.50.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.50.1", + "@rollup/rollup-linux-arm-musleabihf": "4.50.1", + "@rollup/rollup-linux-arm64-gnu": "4.50.1", + "@rollup/rollup-linux-arm64-musl": "4.50.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.50.1", + "@rollup/rollup-linux-ppc64-gnu": "4.50.1", + "@rollup/rollup-linux-riscv64-gnu": "4.50.1", + "@rollup/rollup-linux-riscv64-musl": "4.50.1", + "@rollup/rollup-linux-s390x-gnu": "4.50.1", + "@rollup/rollup-linux-x64-gnu": "4.50.1", + "@rollup/rollup-linux-x64-musl": "4.50.1", + "@rollup/rollup-openharmony-arm64": "4.50.1", + "@rollup/rollup-win32-arm64-msvc": "4.50.1", + "@rollup/rollup-win32-ia32-msvc": "4.50.1", + "@rollup/rollup-win32-x64-msvc": "4.50.1", + "fsevents": "~2.3.2" } }, - "node_modules/mitt": { - "version": "3.0.1", - "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "license": "MIT" - }, - "node_modules/mlly": { - "version": "1.8.0", - "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.0.tgz", - "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.1" + "tslib": "^2.1.0" } }, - "node_modules/mlly/node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "license": "MIT" + "node_modules/sass": { + "version": "1.92.1", + "resolved": "https://registry.npmmirror.com/sass/-/sass-1.92.1.tgz", + "integrity": "sha512-ffmsdbwqb3XeyR8jJR6KelIXARM9bFQe8A6Q3W4Klmwy5Ckd5gz7jgUNHo4UOqutU5Sk1DtKLbpDP0nLCg1xqQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } }, - "node_modules/mlly/node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "node_modules/sass/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", + "optional": true, "engines": { - "node": ">=10" + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", "license": "MIT" }, - "node_modules/muggle-string": { - "version": "0.4.1", - "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", - "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "node_modules/seemly": { + "version": "0.3.10", + "resolved": "https://registry.npmmirror.com/seemly/-/seemly-0.3.10.tgz", + "integrity": "sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==", "license": "MIT" }, - "node_modules/naive-ui": { - "version": "2.43.1", - "resolved": "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.43.1.tgz", - "integrity": "sha512-w52W0mOhdOGt4uucFSZmP0DI44PCsFyuxeLSs9aoUThfIuxms90MYjv46Qrr7xprjyJRw5RU6vYpCx4o9ind3A==", + "node_modules/selderee": { + "version": "0.11.0", + "resolved": "https://registry.npmmirror.com/selderee/-/selderee-0.11.0.tgz", + "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", "license": "MIT", "dependencies": { - "@css-render/plugin-bem": "^0.15.14", - "@css-render/vue3-ssr": "^0.15.14", - "@types/katex": "^0.16.2", - "@types/lodash": "^4.14.198", - "@types/lodash-es": "^4.17.9", - "async-validator": "^4.2.5", - "css-render": "^0.15.14", - "csstype": "^3.1.3", - "date-fns": "^3.6.0", - "date-fns-tz": "^3.1.3", - "evtd": "^0.2.4", - "highlight.js": "^11.8.0", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "seemly": "^0.3.8", - "treemate": "^0.3.11", - "vdirs": "^0.1.8", - "vooks": "^0.2.12", - "vueuc": "^0.4.65" + "parseley": "^0.12.0" }, - "peerDependencies": { - "vue": "^3.0.0" + "funding": { + "url": "https://ko-fi.com/killymxi" } }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmmirror.com/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/sharp/node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" + "dependencies": { + "shebang-regex": "^3.0.0" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=8" } }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", "engines": { - "node": ">=10.5.0" + "node": ">=8" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmmirror.com/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/node-releases": { - "version": "2.0.21", - "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.21.tgz", - "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", + "license": "ISC", "engines": { - "node": ">=0.10.0" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/normalize-wheel-es": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", - "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", - "license": "BSD-3-Clause" + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmmirror.com/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } }, - "node_modules/npm-run-path": { - "version": "6.0.0", - "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-6.0.0.tgz", - "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", "dev": true, "license": "MIT", "dependencies": { - "path-key": "^4.0.0", - "unicorn-magic": "^0.3.0" + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" }, "engines": { "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4", + "npm": ">=6" } }, - "node_modules/ohash": { - "version": "2.0.11", - "resolved": "https://registry.npmmirror.com/ohash/-/ohash-2.0.11.tgz", - "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, - "license": "MIT" + "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/open": { - "version": "10.2.0", - "resolved": "https://registry.npmmirror.com/open/-/open-10.2.0.tgz", - "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "wsl-utils": "^0.1.0" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/parse-ms": { + "node_modules/strip-final-newline": { "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/parse-ms/-/parse-ms-4.0.0.tgz", - "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", "dev": true, "license": "MIT", "engines": { @@ -3836,225 +5327,175 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "license": "MIT" + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, + "node_modules/superjson": { + "version": "2.2.2", + "resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.2.tgz", + "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==", "license": "MIT", + "dependencies": { + "copy-anything": "^3.0.2" + }, "engines": { - "node": ">=8" + "node": ">=16" } }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "license": "MIT" - }, - "node_modules/perfect-debounce": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz", - "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", "license": "MIT", "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/pinia": { - "version": "3.0.3", - "resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.3.tgz", - "integrity": "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==", + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "license": "MIT", "dependencies": { - "@vue/devtools-api": "^7.7.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" }, "funding": { - "url": "https://github.com/sponsors/posva" + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" }, "peerDependencies": { - "typescript": ">=4.4.4", - "vue": "^2.7.0 || ^3.5.11" + "picomatch": "^3 || ^4" }, "peerDependenciesMeta": { - "typescript": { + "picomatch": { "optional": true - } - } - }, - "node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", - "license": "MIT", - "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", - "pathe": "^2.0.3" + } } }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "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" - } - ], + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "is-number": "^7.0.0" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=8.0" } }, - "node_modules/pretty-ms": { - "version": "9.2.0", - "resolved": "https://registry.npmmirror.com/pretty-ms/-/pretty-ms-9.2.0.tgz", - "integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==", + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", "dev": true, "license": "MIT", - "dependencies": { - "parse-ms": "^4.0.0" - }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, - "node_modules/quansync": { - "version": "0.2.11", - "resolved": "https://registry.npmmirror.com/quansync/-/quansync-0.2.11.tgz", - "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/antfu" - }, - { - "type": "individual", - "url": "https://github.com/sponsors/sxzz" - } - ], + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/treemate": { + "version": "0.3.11", + "resolved": "https://registry.npmmirror.com/treemate/-/treemate-0.3.11.tgz", + "integrity": "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==", "license": "MIT" }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "devOptional": true, + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "engines": { - "node": ">=8.10.0" + "node": ">=14.17" } }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.14.0", + "resolved": "https://registry.npmmirror.com/undici/-/undici-7.14.0.tgz", + "integrity": "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ==", "license": "MIT", "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">=20.18.1" } }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "node_modules/undici-types": { + "version": "7.11.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.11.0.tgz", + "integrity": "sha512-kt1ZriHTi7MU+Z/r9DOdAI3ONdaR3M3csEaRc6ewa4f4dTvX4cQCbJ4NkEn0ohE4hHtq85+PhPSTY+pO/1PwgA==", "license": "MIT" }, - "node_modules/rollup": { - "version": "4.50.1", - "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.50.1.tgz", - "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==", - "dev": true, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmmirror.com/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", "license": "MIT", + "peer": true, "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.50.1", - "@rollup/rollup-android-arm64": "4.50.1", - "@rollup/rollup-darwin-arm64": "4.50.1", - "@rollup/rollup-darwin-x64": "4.50.1", - "@rollup/rollup-freebsd-arm64": "4.50.1", - "@rollup/rollup-freebsd-x64": "4.50.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.50.1", - "@rollup/rollup-linux-arm-musleabihf": "4.50.1", - "@rollup/rollup-linux-arm64-gnu": "4.50.1", - "@rollup/rollup-linux-arm64-musl": "4.50.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.50.1", - "@rollup/rollup-linux-ppc64-gnu": "4.50.1", - "@rollup/rollup-linux-riscv64-gnu": "4.50.1", - "@rollup/rollup-linux-riscv64-musl": "4.50.1", - "@rollup/rollup-linux-s390x-gnu": "4.50.1", - "@rollup/rollup-linux-x64-gnu": "4.50.1", - "@rollup/rollup-linux-x64-musl": "4.50.1", - "@rollup/rollup-openharmony-arm64": "4.50.1", - "@rollup/rollup-win32-arm64-msvc": "4.50.1", - "@rollup/rollup-win32-ia32-msvc": "4.50.1", - "@rollup/rollup-win32-x64-msvc": "4.50.1", - "fsevents": "~2.3.2" + "pathe": "^2.0.3" } }, - "node_modules/run-applescript": { - "version": "7.1.0", - "resolved": "https://registry.npmmirror.com/run-applescript/-/run-applescript-7.1.0.tgz", - "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", "dev": true, "license": "MIT", "engines": { @@ -4064,861 +5505,1117 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/sass": { - "version": "1.92.1", - "resolved": "https://registry.npmmirror.com/sass/-/sass-1.92.1.tgz", - "integrity": "sha512-ffmsdbwqb3XeyR8jJR6KelIXARM9bFQe8A6Q3W4Klmwy5Ckd5gz7jgUNHo4UOqutU5Sk1DtKLbpDP0nLCg1xqQ==", - "dev": true, + "node_modules/unimport": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/unimport/-/unimport-5.2.0.tgz", + "integrity": "sha512-bTuAMMOOqIAyjV4i4UH7P07pO+EsVxmhOzQ2YJ290J6mkLUdozNhb5I/YoOEheeNADC03ent3Qj07X0fWfUpmw==", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "chokidar": "^4.0.0", - "immutable": "^5.0.2", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" + "acorn": "^8.15.0", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "local-pkg": "^1.1.1", + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "pkg-types": "^2.2.0", + "scule": "^1.3.0", + "strip-literal": "^3.0.0", + "tinyglobby": "^0.2.14", + "unplugin": "^2.3.5", + "unplugin-utils": "^0.2.4" }, "engines": { - "node": ">=14.0.0" - }, - "optionalDependencies": { - "@parcel/watcher": "^2.4.1" + "node": ">=18.12.0" } }, - "node_modules/sass/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, + "node_modules/unimport/node_modules/unplugin-utils": { + "version": "0.2.5", + "resolved": "https://registry.npmmirror.com/unplugin-utils/-/unplugin-utils-0.2.5.tgz", + "integrity": "sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" + "pathe": "^2.0.3", + "picomatch": "^4.0.3" }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/sass/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, "engines": { - "node": ">= 14.18.0" + "node": ">=18.12.0" }, "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/sxzz" } }, - "node_modules/scule": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz", - "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", - "license": "MIT" - }, - "node_modules/seemly": { - "version": "0.3.10", - "resolved": "https://registry.npmmirror.com/seemly/-/seemly-0.3.10.tgz", - "integrity": "sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==", - "license": "MIT" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, + "node_modules/unplugin": { + "version": "2.3.10", + "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-2.3.10.tgz", + "integrity": "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==", "license": "MIT", "dependencies": { - "shebang-regex": "^3.0.0" + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" }, "engines": { - "node": ">=8" + "node": ">=18.12.0" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, + "node_modules/unplugin-auto-import": { + "version": "20.1.0", + "resolved": "https://registry.npmmirror.com/unplugin-auto-import/-/unplugin-auto-import-20.1.0.tgz", + "integrity": "sha512-Wa7/y3DwpbxhjyXCbuliuATCPa0/e47tstWkytJGAr55ooSNwIvbkrq0rlduqYGiCNMsZcD+C6vsN+W3AX96eA==", "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", + "dependencies": { + "local-pkg": "^1.1.2", + "magic-string": "^0.30.18", + "picomatch": "^4.0.3", + "unimport": "^5.2.0", + "unplugin": "^2.3.8", + "unplugin-utils": "^0.3.0" + }, "engines": { "node": ">=14" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@nuxt/kit": "^4.0.0", + "@vueuse/core": "*" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "@vueuse/core": { + "optional": true + } } }, - "node_modules/sirv": { - "version": "3.0.2", - "resolved": "https://registry.npmmirror.com/sirv/-/sirv-3.0.2.tgz", - "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", - "dev": true, + "node_modules/unplugin-utils": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/unplugin-utils/-/unplugin-utils-0.3.0.tgz", + "integrity": "sha512-JLoggz+PvLVMJo+jZt97hdIIIZ2yTzGgft9e9q8iMrC4ewufl62ekeW7mixBghonn2gVb/ICjyvlmOCUBnJLQg==", "license": "MIT", "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" + "pathe": "^2.0.3", + "picomatch": "^4.0.3" }, "engines": { - "node": ">=18" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" } }, - "node_modules/speakingurl": { - "version": "14.0.1", - "resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz", - "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", - "license": "BSD-3-Clause", + "node_modules/unplugin-vue-components": { + "version": "29.0.0", + "resolved": "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-29.0.0.tgz", + "integrity": "sha512-M2DX44g4/jvBkB0V6uwqTbkTd5DMRHpeGoi/cIKwGG4HPuNxLbe8zoTStB2n12hoDiWc9I1PIRQruRWExNXHlQ==", + "license": "MIT", + "dependencies": { + "chokidar": "^3.6.0", + "debug": "^4.4.1", + "local-pkg": "^1.1.1", + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "tinyglobby": "^0.2.14", + "unplugin": "^2.3.5", + "unplugin-utils": "^0.2.4" + }, "engines": { - "node": ">=0.10.0" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@babel/parser": "^7.15.8", + "@nuxt/kit": "^3.2.2 || ^4.0.0", + "vue": "2 || 3" + }, + "peerDependenciesMeta": { + "@babel/parser": { + "optional": true + }, + "@nuxt/kit": { + "optional": true + } } }, - "node_modules/strip-final-newline": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-4.0.0.tgz", - "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", - "dev": true, + "node_modules/unplugin-vue-components/node_modules/unplugin-utils": { + "version": "0.2.5", + "resolved": "https://registry.npmmirror.com/unplugin-utils/-/unplugin-utils-0.2.5.tgz", + "integrity": "sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==", "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, "engines": { - "node": ">=18" + "node": ">=18.12.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/sxzz" } }, - "node_modules/strip-literal": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/strip-literal/-/strip-literal-3.0.0.tgz", - "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "js-tokens": "^9.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, - "funding": { - "url": "https://github.com/sponsors/antfu" + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, - "node_modules/superjson": { - "version": "2.2.2", - "resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.2.tgz", - "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==", + "node_modules/vdirs": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/vdirs/-/vdirs-0.1.8.tgz", + "integrity": "sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==", "license": "MIT", "dependencies": { - "copy-anything": "^3.0.2" + "evtd": "^0.2.2" }, - "engines": { - "node": ">=16" + "peerDependencies": { + "vue": "^3.0.11" } }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "node_modules/vite": { + "version": "7.1.5", + "resolved": "https://registry.npmmirror.com/vite/-/vite-7.1.5.tgz", + "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { + "esbuild": "^0.25.0", "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" }, "engines": { - "node": ">=12.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" }, "peerDependencies": { - "picomatch": "^3 || ^4" + "@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": { - "picomatch": { + "@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/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/vite-dev-rpc": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/vite-dev-rpc/-/vite-dev-rpc-1.1.0.tgz", + "integrity": "sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==", + "dev": true, "license": "MIT", "dependencies": { - "is-number": "^7.0.0" + "birpc": "^2.4.0", + "vite-hot-client": "^2.1.0" }, - "engines": { - "node": ">=8.0" + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0" } }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmmirror.com/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "node_modules/vite-hot-client": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/vite-hot-client/-/vite-hot-client-2.1.0.tgz", + "integrity": "sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/treemate": { - "version": "0.3.11", - "resolved": "https://registry.npmmirror.com/treemate/-/treemate-0.3.11.tgz", - "integrity": "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==", - "license": "MIT" - }, - "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "node_modules/vite-plugin-inspect": { + "version": "11.3.3", + "resolved": "https://registry.npmmirror.com/vite-plugin-inspect/-/vite-plugin-inspect-11.3.3.tgz", + "integrity": "sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansis": "^4.1.0", + "debug": "^4.4.1", + "error-stack-parser-es": "^1.0.5", + "ohash": "^2.0.11", + "open": "^10.2.0", + "perfect-debounce": "^2.0.0", + "sirv": "^3.0.1", + "unplugin-utils": "^0.3.0", + "vite-dev-rpc": "^1.1.0" }, "engines": { - "node": ">=14.17" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } } }, - "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "7.11.0", - "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.11.0.tgz", - "integrity": "sha512-kt1ZriHTi7MU+Z/r9DOdAI3ONdaR3M3csEaRc6ewa4f4dTvX4cQCbJ4NkEn0ohE4hHtq85+PhPSTY+pO/1PwgA==", + "node_modules/vite-plugin-inspect/node_modules/perfect-debounce": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-2.0.0.tgz", + "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==", + "dev": true, "license": "MIT" }, - "node_modules/unicorn-magic": { - "version": "0.3.0", - "resolved": "https://registry.npmmirror.com/unicorn-magic/-/unicorn-magic-0.3.0.tgz", - "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "node_modules/vite-plugin-vue-devtools": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.0.2.tgz", + "integrity": "sha512-1069qvMBcyAu3yXQlvYrkwoyLOk0lSSR/gTKy/vy+Det7TXnouGei6ZcKwr5TIe938v/14oLlp0ow6FSJkkORA==", "dev": true, "license": "MIT", + "dependencies": { + "@vue/devtools-core": "^8.0.2", + "@vue/devtools-kit": "^8.0.2", + "@vue/devtools-shared": "^8.0.2", + "execa": "^9.6.0", + "sirv": "^3.0.2", + "vite-plugin-inspect": "^11.3.3", + "vite-plugin-vue-inspector": "^5.3.2" + }, "engines": { - "node": ">=18" + "node": ">=v14.21.3" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0-0" } }, - "node_modules/unimport": { - "version": "5.2.0", - "resolved": "https://registry.npmmirror.com/unimport/-/unimport-5.2.0.tgz", - "integrity": "sha512-bTuAMMOOqIAyjV4i4UH7P07pO+EsVxmhOzQ2YJ290J6mkLUdozNhb5I/YoOEheeNADC03ent3Qj07X0fWfUpmw==", + "node_modules/vite-plugin-vue-devtools/node_modules/@vue/devtools-kit": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-8.0.2.tgz", + "integrity": "sha512-yjZKdEmhJzQqbOh4KFBfTOQjDPMrjjBNCnHBvnTGJX+YLAqoUtY2J+cg7BE+EA8KUv8LprECq04ts75wCoIGWA==", + "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.15.0", - "escape-string-regexp": "^5.0.0", - "estree-walker": "^3.0.3", - "local-pkg": "^1.1.1", - "magic-string": "^0.30.17", - "mlly": "^1.7.4", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "pkg-types": "^2.2.0", - "scule": "^1.3.0", - "strip-literal": "^3.0.0", - "tinyglobby": "^0.2.14", - "unplugin": "^2.3.5", - "unplugin-utils": "^0.2.4" - }, - "engines": { - "node": ">=18.12.0" + "@vue/devtools-shared": "^8.0.2", + "birpc": "^2.5.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^2.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" } }, - "node_modules/unimport/node_modules/unplugin-utils": { - "version": "0.2.5", - "resolved": "https://registry.npmmirror.com/unplugin-utils/-/unplugin-utils-0.2.5.tgz", - "integrity": "sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==", + "node_modules/vite-plugin-vue-devtools/node_modules/@vue/devtools-shared": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-8.0.2.tgz", + "integrity": "sha512-mLU0QVdy5Lp40PMGSixDw/Kbd6v5dkQXltd2r+mdVQV7iUog2NlZuLxFZApFZ/mObUBDhoCpf0T3zF2FWWdeHw==", + "dev": true, "license": "MIT", "dependencies": { - "pathe": "^2.0.3", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=18.12.0" - }, - "funding": { - "url": "https://github.com/sponsors/sxzz" + "rfdc": "^1.4.1" } }, - "node_modules/unplugin": { - "version": "2.3.10", - "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-2.3.10.tgz", - "integrity": "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==", + "node_modules/vite-plugin-vue-devtools/node_modules/perfect-debounce": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-2.0.0.tgz", + "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite-plugin-vue-inspector": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.3.2.tgz", + "integrity": "sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q==", + "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "acorn": "^8.15.0", - "picomatch": "^4.0.3", - "webpack-virtual-modules": "^0.6.2" + "@babel/core": "^7.23.0", + "@babel/plugin-proposal-decorators": "^7.23.0", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-transform-typescript": "^7.22.15", + "@vue/babel-plugin-jsx": "^1.1.5", + "@vue/compiler-dom": "^3.3.4", + "kolorist": "^1.8.0", + "magic-string": "^0.30.4" }, - "engines": { - "node": ">=18.12.0" + "peerDependencies": { + "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" } }, - "node_modules/unplugin-auto-import": { - "version": "20.1.0", - "resolved": "https://registry.npmmirror.com/unplugin-auto-import/-/unplugin-auto-import-20.1.0.tgz", - "integrity": "sha512-Wa7/y3DwpbxhjyXCbuliuATCPa0/e47tstWkytJGAr55ooSNwIvbkrq0rlduqYGiCNMsZcD+C6vsN+W3AX96eA==", + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", - "dependencies": { - "local-pkg": "^1.1.2", - "magic-string": "^0.30.18", - "picomatch": "^4.0.3", - "unimport": "^5.2.0", - "unplugin": "^2.3.8", - "unplugin-utils": "^0.3.0" - }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" + "node": ">=12.0.0" }, "peerDependencies": { - "@nuxt/kit": "^4.0.0", - "@vueuse/core": "*" + "picomatch": "^3 || ^4" }, "peerDependenciesMeta": { - "@nuxt/kit": { - "optional": true - }, - "@vueuse/core": { + "picomatch": { "optional": true } } }, - "node_modules/unplugin-utils": { - "version": "0.3.0", - "resolved": "https://registry.npmmirror.com/unplugin-utils/-/unplugin-utils-0.3.0.tgz", - "integrity": "sha512-JLoggz+PvLVMJo+jZt97hdIIIZ2yTzGgft9e9q8iMrC4ewufl62ekeW7mixBghonn2gVb/ICjyvlmOCUBnJLQg==", + "node_modules/vooks": { + "version": "0.2.12", + "resolved": "https://registry.npmmirror.com/vooks/-/vooks-0.2.12.tgz", + "integrity": "sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==", "license": "MIT", "dependencies": { - "pathe": "^2.0.3", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=20.19.0" + "evtd": "^0.2.2" }, - "funding": { - "url": "https://github.com/sponsors/sxzz" + "peerDependencies": { + "vue": "^3.0.0" } }, - "node_modules/unplugin-vue-components": { - "version": "29.0.0", - "resolved": "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-29.0.0.tgz", - "integrity": "sha512-M2DX44g4/jvBkB0V6uwqTbkTd5DMRHpeGoi/cIKwGG4HPuNxLbe8zoTStB2n12hoDiWc9I1PIRQruRWExNXHlQ==", + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.21", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.21.tgz", + "integrity": "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==", "license": "MIT", + "peer": true, "dependencies": { - "chokidar": "^3.6.0", - "debug": "^4.4.1", - "local-pkg": "^1.1.1", - "magic-string": "^0.30.17", - "mlly": "^1.7.4", - "tinyglobby": "^0.2.14", - "unplugin": "^2.3.5", - "unplugin-utils": "^0.2.4" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" + "@vue/compiler-dom": "3.5.21", + "@vue/compiler-sfc": "3.5.21", + "@vue/runtime-dom": "3.5.21", + "@vue/server-renderer": "3.5.21", + "@vue/shared": "3.5.21" }, "peerDependencies": { - "@babel/parser": "^7.15.8", - "@nuxt/kit": "^3.2.2 || ^4.0.0", - "vue": "2 || 3" + "typescript": "*" }, "peerDependenciesMeta": { - "@babel/parser": { - "optional": true - }, - "@nuxt/kit": { + "typescript": { "optional": true } } }, - "node_modules/unplugin-vue-components/node_modules/unplugin-utils": { - "version": "0.2.5", - "resolved": "https://registry.npmmirror.com/unplugin-utils/-/unplugin-utils-0.2.5.tgz", - "integrity": "sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==", + "node_modules/vue-router": { + "version": "4.5.1", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz", + "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==", "license": "MIT", "dependencies": { - "pathe": "^2.0.3", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=18.12.0" + "@vue/devtools-api": "^6.6.4" }, "funding": { - "url": "https://github.com/sponsors/sxzz" + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" } }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/vue-tsc": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.1.5.tgz", + "integrity": "sha512-L/G9IUjOWhBU0yun89rv8fKqmKC+T0HfhrFjlIml71WpfBv9eb4E9Bev8FMbyueBIU9vxQqbd+oOsVcDa5amGw==", "license": "MIT", "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" + "@volar/typescript": "2.4.23", + "@vue/language-core": "3.1.5" }, "bin": { - "update-browserslist-db": "cli.js" + "vue-tsc": "bin/vue-tsc.js" }, "peerDependencies": { - "browserslist": ">= 4.21.0" + "typescript": ">=5.0.0" } }, - "node_modules/vdirs": { - "version": "0.1.8", - "resolved": "https://registry.npmmirror.com/vdirs/-/vdirs-0.1.8.tgz", - "integrity": "sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==", + "node_modules/vueuc": { + "version": "0.4.65", + "resolved": "https://registry.npmmirror.com/vueuc/-/vueuc-0.4.65.tgz", + "integrity": "sha512-lXuMl+8gsBmruudfxnMF9HW4be8rFziylXFu1VHVNbLVhRTXXV4njvpRuJapD/8q+oFEMSfQMH16E/85VoWRyQ==", "license": "MIT", "dependencies": { - "evtd": "^0.2.2" + "@css-render/vue3-ssr": "^0.15.10", + "@juggle/resize-observer": "^3.3.1", + "css-render": "^0.15.10", + "evtd": "^0.2.4", + "seemly": "^0.3.6", + "vdirs": "^0.1.4", + "vooks": "^0.2.4" }, "peerDependencies": { "vue": "^3.0.11" } }, - "node_modules/vite": { - "version": "7.1.5", - "resolved": "https://registry.npmmirror.com/vite/-/vite-7.1.5.tgz", - "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", - "dev": true, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "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" + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" }, "bin": { - "vite": "bin/vite.js" + "node-which": "bin/node-which" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 8" + } + }, + "node_modules/workerd": { + "version": "1.20251125.0", + "resolved": "https://registry.npmmirror.com/workerd/-/workerd-1.20251125.0.tgz", + "integrity": "sha512-oQYfgu3UZ15HlMcEyilKD1RdielRnKSG5MA0xoi1theVs99Rop9AEFYicYCyK1R4YjYblLRYEiL1tMgEFqpReA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "workerd": "bin/workerd" }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" + "engines": { + "node": ">=16" }, "optionalDependencies": { - "fsevents": "~2.3.3" + "@cloudflare/workerd-darwin-64": "1.20251125.0", + "@cloudflare/workerd-darwin-arm64": "1.20251125.0", + "@cloudflare/workerd-linux-64": "1.20251125.0", + "@cloudflare/workerd-linux-arm64": "1.20251125.0", + "@cloudflare/workerd-windows-64": "1.20251125.0" + } + }, + "node_modules/wrangler": { + "version": "4.51.0", + "resolved": "https://registry.npmmirror.com/wrangler/-/wrangler-4.51.0.tgz", + "integrity": "sha512-JHv+58UxM2//e4kf9ASDwg016xd/OdDNDUKW6zLQyE7Uc9ayYKX1QJ9NsYtpo4dC1dfg6rT67pf1aNK1cTzUDg==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.4.1", + "@cloudflare/unenv-preset": "2.7.11", + "blake3-wasm": "2.1.5", + "esbuild": "0.25.4", + "miniflare": "4.20251125.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20251125.0" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" }, "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" + "@cloudflare/workers-types": "^4.20251125.0" }, "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": { + "@cloudflare/workers-types": { "optional": true } } }, - "node_modules/vite-dev-rpc": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/vite-dev-rpc/-/vite-dev-rpc-1.1.0.tgz", - "integrity": "sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==", - "dev": true, + "node_modules/wrangler/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.4.tgz", + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.4.tgz", + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "birpc": "^2.4.0", - "vite-hot-client": "^2.1.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/vite-hot-client": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/vite-hot-client/-/vite-hot-client-2.1.0.tgz", - "integrity": "sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==", - "dev": true, + "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "cpu": [ + "arm64" + ], "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" } }, - "node_modules/vite-plugin-inspect": { - "version": "11.3.3", - "resolved": "https://registry.npmmirror.com/vite-plugin-inspect/-/vite-plugin-inspect-11.3.3.tgz", - "integrity": "sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==", - "dev": true, + "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "ansis": "^4.1.0", - "debug": "^4.4.1", - "error-stack-parser-es": "^1.0.5", - "ohash": "^2.0.11", - "open": "^10.2.0", - "perfect-debounce": "^2.0.0", - "sirv": "^3.0.1", - "unplugin-utils": "^0.3.0", - "vite-dev-rpc": "^1.1.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vite": "^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "@nuxt/kit": { - "optional": true - } + "node": ">=18" } }, - "node_modules/vite-plugin-inspect/node_modules/perfect-debounce": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-2.0.0.tgz", - "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==", - "dev": true, - "license": "MIT" + "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/vite-plugin-vue-devtools": { - "version": "8.0.2", - "resolved": "https://registry.npmmirror.com/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.0.2.tgz", - "integrity": "sha512-1069qvMBcyAu3yXQlvYrkwoyLOk0lSSR/gTKy/vy+Det7TXnouGei6ZcKwr5TIe938v/14oLlp0ow6FSJkkORA==", - "dev": true, + "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "@vue/devtools-core": "^8.0.2", - "@vue/devtools-kit": "^8.0.2", - "@vue/devtools-shared": "^8.0.2", - "execa": "^9.6.0", - "sirv": "^3.0.2", - "vite-plugin-inspect": "^11.3.3", - "vite-plugin-vue-inspector": "^5.3.2" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=v14.21.3" - }, - "peerDependencies": { - "vite": "^6.0.0 || ^7.0.0-0" + "node": ">=18" } }, - "node_modules/vite-plugin-vue-devtools/node_modules/@vue/devtools-kit": { - "version": "8.0.2", - "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-8.0.2.tgz", - "integrity": "sha512-yjZKdEmhJzQqbOh4KFBfTOQjDPMrjjBNCnHBvnTGJX+YLAqoUtY2J+cg7BE+EA8KUv8LprECq04ts75wCoIGWA==", - "dev": true, + "node_modules/wrangler/node_modules/@esbuild/linux-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "cpu": [ + "arm" + ], "license": "MIT", - "dependencies": { - "@vue/devtools-shared": "^8.0.2", - "birpc": "^2.5.0", - "hookable": "^5.5.3", - "mitt": "^3.0.1", - "perfect-debounce": "^2.0.0", - "speakingurl": "^14.0.1", - "superjson": "^2.2.2" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/vite-plugin-vue-devtools/node_modules/@vue/devtools-shared": { - "version": "8.0.2", - "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-8.0.2.tgz", - "integrity": "sha512-mLU0QVdy5Lp40PMGSixDw/Kbd6v5dkQXltd2r+mdVQV7iUog2NlZuLxFZApFZ/mObUBDhoCpf0T3zF2FWWdeHw==", - "dev": true, + "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "cpu": [ + "arm64" + ], "license": "MIT", - "dependencies": { - "rfdc": "^1.4.1" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/vite-plugin-vue-devtools/node_modules/perfect-debounce": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-2.0.0.tgz", - "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==", - "dev": true, - "license": "MIT" + "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/vite-plugin-vue-inspector": { - "version": "5.3.2", - "resolved": "https://registry.npmmirror.com/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.3.2.tgz", - "integrity": "sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q==", - "dev": true, + "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "cpu": [ + "loong64" + ], "license": "MIT", - "dependencies": { - "@babel/core": "^7.23.0", - "@babel/plugin-proposal-decorators": "^7.23.0", - "@babel/plugin-syntax-import-attributes": "^7.22.5", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-transform-typescript": "^7.22.15", - "@vue/babel-plugin-jsx": "^1.1.5", - "@vue/compiler-dom": "^3.3.4", - "kolorist": "^1.8.0", - "magic-string": "^0.30.4" - }, - "peerDependencies": { - "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, + "node_modules/wrangler/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "cpu": [ + "arm64" + ], "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "node": ">=18" } }, - "node_modules/vooks": { - "version": "0.2.12", - "resolved": "https://registry.npmmirror.com/vooks/-/vooks-0.2.12.tgz", - "integrity": "sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==", + "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "evtd": "^0.2.2" - }, - "peerDependencies": { - "vue": "^3.0.0" + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/vscode-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz", - "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", - "license": "MIT" - }, - "node_modules/vue": { - "version": "3.5.21", - "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.21.tgz", - "integrity": "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==", + "node_modules/wrangler/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "cpu": [ + "arm64" + ], "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.21", - "@vue/compiler-sfc": "3.5.21", - "@vue/runtime-dom": "3.5.21", - "@vue/server-renderer": "3.5.21", - "@vue/shared": "3.5.21" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/vue-router": { - "version": "4.5.1", - "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz", - "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==", + "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "@vue/devtools-api": "^6.6.4" - }, - "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "vue": "^3.2.0" + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/vue-router/node_modules/@vue/devtools-api": { - "version": "6.6.4", - "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", - "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", - "license": "MIT" - }, - "node_modules/vue-tsc": { - "version": "3.0.7", - "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.0.7.tgz", - "integrity": "sha512-BSMmW8GGEgHykrv7mRk6zfTdK+tw4MBZY/x6fFa7IkdXK3s/8hQRacPjG9/8YKFDIWGhBocwi6PlkQQ/93OgIQ==", + "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "@volar/typescript": "2.4.23", - "@vue/language-core": "3.0.7" - }, - "bin": { - "vue-tsc": "bin/vue-tsc.js" - }, - "peerDependencies": { - "typescript": ">=5.0.0" + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" } }, - "node_modules/vueuc": { - "version": "0.4.65", - "resolved": "https://registry.npmmirror.com/vueuc/-/vueuc-0.4.65.tgz", - "integrity": "sha512-lXuMl+8gsBmruudfxnMF9HW4be8rFziylXFu1VHVNbLVhRTXXV4njvpRuJapD/8q+oFEMSfQMH16E/85VoWRyQ==", + "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "cpu": [ + "arm64" + ], "license": "MIT", - "dependencies": { - "@css-render/vue3-ssr": "^0.15.10", - "@juggle/resize-observer": "^3.3.1", - "css-render": "^0.15.10", - "evtd": "^0.2.4", - "seemly": "^0.3.6", - "vdirs": "^0.1.4", - "vooks": "^0.2.4" - }, - "peerDependencies": { - "vue": "^3.0.11" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/web-streams-polyfill": { - "version": "4.0.0-beta.3", - "resolved": "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", - "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "cpu": [ + "ia32" + ], "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 14" + "node": ">=18" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/webpack-virtual-modules": { - "version": "0.6.2", - "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", - "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", - "license": "MIT" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "node_modules/wrangler/node_modules/@esbuild/win32-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "node_modules/wrangler/node_modules/esbuild": { + "version": "0.25.4", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.4.tgz", + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.4", + "@esbuild/android-arm": "0.25.4", + "@esbuild/android-arm64": "0.25.4", + "@esbuild/android-x64": "0.25.4", + "@esbuild/darwin-arm64": "0.25.4", + "@esbuild/darwin-x64": "0.25.4", + "@esbuild/freebsd-arm64": "0.25.4", + "@esbuild/freebsd-x64": "0.25.4", + "@esbuild/linux-arm": "0.25.4", + "@esbuild/linux-arm64": "0.25.4", + "@esbuild/linux-ia32": "0.25.4", + "@esbuild/linux-loong64": "0.25.4", + "@esbuild/linux-mips64el": "0.25.4", + "@esbuild/linux-ppc64": "0.25.4", + "@esbuild/linux-riscv64": "0.25.4", + "@esbuild/linux-s390x": "0.25.4", + "@esbuild/linux-x64": "0.25.4", + "@esbuild/netbsd-arm64": "0.25.4", + "@esbuild/netbsd-x64": "0.25.4", + "@esbuild/openbsd-arm64": "0.25.4", + "@esbuild/openbsd-x64": "0.25.4", + "@esbuild/sunos-x64": "0.25.4", + "@esbuild/win32-arm64": "0.25.4", + "@esbuild/win32-ia32": "0.25.4", + "@esbuild/win32-x64": "0.25.4" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "isexe": "^2.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, - "bin": { - "node-which": "bin/node-which" + "engines": { + "node": ">=10" }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", "engines": { - "node": ">= 8" + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, "node_modules/wsl-utils": { @@ -4937,6 +6634,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/yallist/-/yallist-3.1.1.tgz", @@ -4944,6 +6651,35 @@ "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yoctocolors": { "version": "2.1.2", "resolved": "https://registry.npmmirror.com/yoctocolors/-/yoctocolors-2.1.2.tgz", @@ -4956,6 +6692,38 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmmirror.com/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmmirror.com/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + }, + "node_modules/zod": { + "version": "3.22.3", + "resolved": "https://registry.npmmirror.com/zod/-/zod-3.22.3.tgz", + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index bfb3c44..4a1d4e2 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "node": "^20.19.0 || >=22.12.0" }, "scripts": { - "dev": "vite", - "build": "vue-tsc && vite build", + "dev": "concurrently \"npx wrangler dev\" \"npx vite vue\"", + "build": "cd vue && npx vue-tsc --noEmit && npx vite build && wrangler deploy", "preview": "cd vue && npx vite preview", "type-check": "vue-tsc --noEmit", "build:vue": "cd vue && npx vite build", @@ -29,7 +29,7 @@ "format:worker": "cd worker && npx prettier --write src/", "type-check:vue": "cd vue && npx vue-tsc --build", "type-check:worker": "cd worker && npx tsc --noEmit", - "#predev": "docker exec node bash -c 'kill -9 $(pidof node) || true'" + "test:email": "node test-email-parsing.js" }, "keywords": [ "cloudflare", @@ -53,19 +53,24 @@ "cloudflare": "^5.1.0", "dayjs": "^1.11.18", "element-plus": "^2.11.3", - "hono": "^4.9.8", + "hono": "^4.10.3", + "html-to-text": "^9.0.5", "naive-ui": "^2.43.1", "pinia": "^3.0.3", + "postal-mime": "^2.6.1", "typescript": "^5.9.2", "unplugin-auto-import": "^20.1.0", "unplugin-vue-components": "^29.0.0", "vue": "^3.5.18", "vue-router": "^4.5.1", - "vue-tsc": "^3.0.7" + "vue-tsc": "^3.1.5", + "wrangler": "^4.51.0" }, "devDependencies": { "@cloudflare/workers-types": "^4.20250920.0", + "@types/html-to-text": "^9.0.4", "@vitejs/plugin-vue": "^6.0.1", + "concurrently": "^9.2.1", "vite": "^7.1.5", "vite-plugin-vue-devtools": "^8.0.0" }, @@ -77,4 +82,4 @@ "url": "https://github.com/XRSec/cloudflare-email-manager/issues" }, "homepage": "https://github.com/XRSec/cloudflare-email-manager#readme" -} +} \ No newline at end of file diff --git a/scripts/README.md b/scripts/README.md index 9df2bd8..345a9f5 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -13,14 +13,11 @@ scripts/ ### 开发环境 ```bash -# 启动 Docker 容器(前端 + 后端) -docker-compose up -d +# 启动前端开发服务器 +cd vue && npm run dev -# 查看容器状态 -docker-compose ps - -# 停止容器 -docker-compose down +# 启动 Worker(API + 后台) +cd worker && npx wrangler dev ``` ### 部署 @@ -37,24 +34,15 @@ npm run clean ### 数据库操作 ```bash -# 通过 npm 脚本(在宿主机运行) +# 本地命令 npm run db:init npm run db:init:remote npm run db:migrate npm run db:import -# 直接在 Docker 容器内运行 -docker exec -it worker zsh -c "node scripts/db.js init" -docker exec -it worker zsh -c "node scripts/db.js init --remote" -docker exec -it worker zsh -c "node scripts/db.js migrate" -docker exec -it worker zsh -c "node scripts/db.js import" - -# 执行 SQL 命令 -docker exec -it worker zsh -c "node scripts/db.js 'SELECT * FROM users'" -docker exec -it worker zsh -c "node scripts/db.js 'SELECT * FROM users' --remote" - -# 执行 base64 编码的 SQL -docker exec -it worker zsh -c "node scripts/db.js U0VMRUNUICogRlJPTSB1c2Vycwo=" +# 执行 SQL 命令或 base64 SQL +npm run db -- "SELECT * FROM users" +npm run db -- U0VMRUNUICogRlJPTSB1c2Vycwo= ``` ### 构建 @@ -73,7 +61,7 @@ npm run build ### 1. 环境检测 - `env-detector.js` 提供统一的环境检测功能 -- 自动检测本地或 Docker 环境 +- 自动校验本地必需依赖(npx、wrangler) - 所有脚本共享环境检测逻辑 ### 2. 部署脚本 @@ -86,7 +74,7 @@ npm run build ### 3. 数据库脚本 - `db.js` 支持多种数据库操作 -- 直接在 Docker 容器内运行 +- 直接使用 wrangler 执行命令 - 支持 base64 编码的 SQL 命令 - 自动创建和清理临时脚本文件 @@ -94,7 +82,7 @@ npm run build | 功能 | 命令 | 说明 | |------|------|------| -| 开发 | `docker-compose up -d` | 启动前端和后端 | +| 开发 | `cd vue && npm run dev` / `cd worker && npx wrangler dev` | 启动前端与 Worker | | 构建 | `npm run build` | 构建前端和后端 | | 部署 | `npm run deploy` | 完整部署(资源创建+构建+部署) | | 初始化 | `npm run init` | 同 deploy 命令 | diff --git a/scripts/db.js b/scripts/db.js index 1daaa3a..94f2652 100644 --- a/scripts/db.js +++ b/scripts/db.js @@ -3,7 +3,7 @@ /** * 数据库操作脚本 * 在 Docker 容器内运行,直接使用 wrangler d1 execute 命令 - * + * * 用法: * node scripts/db.js init [--remote] * node scripts/db.js migrate @@ -22,388 +22,383 @@ const projectRoot = join(__dirname, '..'); // 解析命令行参数 function parseArgs() { - const args = process.argv.slice(2); + const args = process.argv.slice(2); - // 如果没有参数,显示帮助 - if (args.length === 0) { - return { command: 'help' }; - } + // 如果没有参数,显示帮助 + if (args.length === 0) { + return { command: 'help' }; + } - const firstArg = args[0]; - const isRemote = args.includes('--remote'); + const firstArg = args[0]; + const isRemote = args.includes('--remote'); - // 检查是否是预定义命令 - const predefinedCommands = ['init', 'migrate', 'import', 'help']; - if (predefinedCommands.includes(firstArg)) { - return { command: firstArg, isRemote }; - } + // 检查是否是预定义命令 + const predefinedCommands = ['init', 'migrate', 'import', 'help']; + if (predefinedCommands.includes(firstArg)) { + return { command: firstArg, isRemote }; + } - // 检查是否是 base64 编码的 SQL - if (isBase64(firstArg)) { - return { command: 'sql', sql: decodeBase64(firstArg), isRemote }; - } + // 检查是否是 base64 编码的 SQL + if (isBase64(firstArg)) { + return { command: 'sql', sql: decodeBase64(firstArg), isRemote }; + } - // 否则当作直接的 SQL 命令 - return { command: 'sql', sql: firstArg, isRemote }; + // 否则当作直接的 SQL 命令 + return { command: 'sql', sql: firstArg, isRemote }; } // 检查是否是 base64 编码 function isBase64(str) { - try { - // 检查字符串是否只包含 base64 字符 - const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/; - if (!base64Regex.test(str)) { - return false; - } + try { + // 检查字符串是否只包含 base64 字符 + const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/; + if (!base64Regex.test(str)) { + return false; + } - // 尝试解码并重新编码,看是否一致 - const decoded = Buffer.from(str, 'base64').toString('utf-8'); - const reencoded = Buffer.from(decoded, 'utf-8').toString('base64'); - return reencoded === str; - } catch (err) { - return false; - } + // 尝试解码并重新编码,看是否一致 + const decoded = Buffer.from(str, 'base64').toString('utf-8'); + const reencoded = Buffer.from(decoded, 'utf-8').toString('base64'); + return reencoded === str; + } catch (err) { + return false; + } } // 解码 base64 function decodeBase64(str) { - try { - return Buffer.from(str, 'base64').toString('utf-8'); - } catch (err) { - return str; - } + try { + return Buffer.from(str, 'base64').toString('utf-8'); + } catch (err) { + return str; + } } // 执行数据库清理(使用与 database.ts 相同的逻辑) async function executeDbClean(isRemote) { - console.log(`🧹 清理数据库${isRemote ? ' (远程)' : ''}...`); - - try { - // 1. 获取所有表名 - console.log('📋 查询数据库中的表...'); - const tablesResult = await executeSQL('SELECT name FROM sqlite_master WHERE type="table" AND name NOT LIKE "sqlite_%" AND name NOT LIKE "_cf%"', isRemote); + console.log(`🧹 清理数据库${isRemote ? ' (远程)' : ''}...`); - // 2. 删除触发器(先删除触发器,避免依赖问题) - console.log('🗑️ 删除触发器...'); try { - // 一次性生成所有删除触发器的 SQL 语句 - const dropAllTriggersQuery = ` - SELECT 'DROP TRIGGER IF EXISTS "' || name || '";' as sql - FROM sqlite_master - WHERE type = 'trigger' - `; - - const triggers = await executeSQL(dropAllTriggersQuery, isRemote); - if (triggers && triggers.results && triggers.results.length > 0) { - // 将所有删除语句合并成一个 SQL - const allDropSQL = triggers.results.map(trigger => trigger.sql).join(' '); + // 1. 获取所有表名 + console.log('📋 查询数据库中的表...'); + const tablesResult = await executeSQL('SELECT name FROM sqlite_master WHERE type="table" AND name NOT LIKE "sqlite_%" AND name NOT LIKE "_cf%"', isRemote); + // 2. 删除触发器(先删除触发器,避免依赖问题) + console.log('🗑️ 删除触发器...'); try { - await executeSQL(allDropSQL, isRemote); - console.log(`✅ 批量删除 ${triggers.results.length} 个触发器成功`); + // 一次性生成所有删除触发器的 SQL 语句 + const dropAllTriggersQuery = `SELECT 'DROP TRIGGER IF EXISTS "' || name || '";' as sql FROM sqlite_master WHERE type = 'trigger'`; + + const triggers = await executeSQL(dropAllTriggersQuery, isRemote); + if (triggers && triggers.results && triggers.results.length > 0) { + // 将所有删除语句合并成一个 SQL + const allDropSQL = triggers.results.map(trigger => trigger.sql).join(' '); + + try { + await executeSQL(allDropSQL, isRemote); + console.log(`✅ 批量删除 ${triggers.results.length} 个触发器成功`); + } catch (error) { + console.warn('批量删除触发器失败,回退到逐个删除:', error); + // 回退到逐个删除 + for (const trigger of triggers.results) { + try { + await executeSQL(trigger.sql, isRemote); + console.log(` 删除触发器: ${trigger.sql.replace('DROP TRIGGER IF EXISTS "', '').replace('";', '')} 成功`); + } catch (error) { + console.log(`⚠️ 删除触发器失败: ${trigger.sql} - ${error}`); + } + } + } + } else { + console.log(' 没有找到触发器'); + } + } catch (error) { + console.log('⚠️ 查询触发器失败...'); + } + + // 3. 删除所有表(先禁用外键约束,忽略错误) + try { + await executeSQL('PRAGMA foreign_keys = OFF', isRemote); + console.log('✅ 已禁用外键约束'); } catch (error) { - console.warn('批量删除触发器失败,回退到逐个删除:', error); - // 回退到逐个删除 - for (const trigger of triggers.results) { + console.warn('禁用外键约束时出现错误,忽略:', error); + } + + // 动态获取表依赖关系并删除表 + if (tablesResult && tablesResult.results && tablesResult.results.length > 0) { + const tables = tablesResult.results.map(row => row.name); + console.log(`📋 发现 ${tables.length} 个表:`); + + // 一次性获取所有表的外键依赖关系 + const tableDependencies = {}; + for (const tableName of tables) { + tableDependencies[tableName] = []; + } + try { - await executeSQL(trigger.sql, isRemote); - console.log(` 删除触发器: ${trigger.sql.replace('DROP TRIGGER IF EXISTS "', '').replace('";', '')} 成功`); + // 使用 sqlite_master 表一次性查询所有外键信息 + const allFkQuery = ` + SELECT m.name as table_name, + pti.\`table\` as ref_table + FROM sqlite_master m + JOIN pragma_foreign_key_list(m.name) pti + WHERE m.type = 'table' + AND m.name NOT LIKE 'sqlite_%' + AND m.name NOT LIKE '_cf%' + `; + + const fkResult = await executeSQL(allFkQuery, isRemote); + if (fkResult && fkResult.results) { + for (const fk of fkResult.results) { + if (tableDependencies[fk.table_name]) { + tableDependencies[fk.table_name].push(fk.ref_table); + } + } + } } catch (error) { - console.log(`⚠️ 删除触发器失败: ${trigger.sql} - ${error}`); + console.warn('批量获取外键信息失败,回退到逐个查询:', error); + // 回退到逐个查询 + for (const tableName of tables) { + try { + const fkResult = await executeSQL(`PRAGMA foreign_key_list(${tableName})`, isRemote); + if (fkResult && fkResult.results) { + tableDependencies[tableName] = fkResult.results.map(fk => fk.table); + } + } catch (error) { + console.warn(`获取表 ${tableName} 外键信息失败:`, error); + } + } } - } - } - } else { - console.log(' 没有找到触发器'); - } - } catch (error) { - console.log('⚠️ 查询触发器失败...'); - } - // 3. 删除所有表(先禁用外键约束,忽略错误) - try { - await executeSQL('PRAGMA foreign_keys = OFF', isRemote); - console.log('✅ 已禁用外键约束'); - } catch (error) { - console.warn('禁用外键约束时出现错误,忽略:', error); - } + // 构建反向依赖图:找出哪些表依赖于当前表 + const reverseDependencies = {}; + for (const tableName of tables) { + reverseDependencies[tableName] = []; + } - // 动态获取表依赖关系并删除表 - if (tablesResult && tablesResult.results && tablesResult.results.length > 0) { - const tables = tablesResult.results.map(row => row.name); - console.log(`📋 发现 ${tables.length} 个表:`, tables.join(', ')); - - // 一次性获取所有表的外键依赖关系 - const tableDependencies = {}; - for (const tableName of tables) { - tableDependencies[tableName] = []; - } - - try { - // 使用 sqlite_master 表一次性查询所有外键信息 - const allFkQuery = ` - SELECT - m.name as table_name, - pti.\`table\` as ref_table - FROM sqlite_master m - JOIN pragma_foreign_key_list(m.name) pti - WHERE m.type = 'table' - AND m.name NOT LIKE 'sqlite_%' - AND m.name NOT LIKE '_cf%' - `; - - const fkResult = await executeSQL(allFkQuery, isRemote); - if (fkResult && fkResult.results) { - for (const fk of fkResult.results) { - if (tableDependencies[fk.table_name]) { - tableDependencies[fk.table_name].push(fk.ref_table); + for (const tableName of tables) { + const dependencies = tableDependencies[tableName] || []; + for (const dep of dependencies) { + if (tables.includes(dep)) { + reverseDependencies[dep].push(tableName); + } + } } - } - } - } catch (error) { - console.warn('批量获取外键信息失败,回退到逐个查询:', error); - // 回退到逐个查询 - for (const tableName of tables) { - try { - const fkResult = await executeSQL(`PRAGMA foreign_key_list(${tableName})`, isRemote); - if (fkResult && fkResult.results) { - tableDependencies[tableName] = fkResult.results.map(fk => fk.table); + + // 拓扑排序:找到删除顺序(先删除依赖表,再删除被依赖的表) + const deleteOrder = []; + const visited = new Set(); + const visiting = new Set(); + + function visit(tableName) { + if (visiting.has(tableName)) { + console.warn(`检测到循环依赖: ${tableName}`); + return; + } + if (visited.has(tableName)) { + return; + } + + visiting.add(tableName); + + // 先处理依赖当前表的表(依赖表要先删除) + const dependents = reverseDependencies[tableName] || []; + for (const dependent of dependents) { + if (tables.includes(dependent)) { + visit(dependent); + } + } + + visiting.delete(tableName); + visited.add(tableName); + deleteOrder.push(tableName); } - } catch (error) { - console.warn(`获取表 ${tableName} 外键信息失败:`, error); - } - } - } - - // 构建反向依赖图:找出哪些表依赖于当前表 - const reverseDependencies = {}; - for (const tableName of tables) { - reverseDependencies[tableName] = []; - } - - for (const tableName of tables) { - const dependencies = tableDependencies[tableName] || []; - for (const dep of dependencies) { - if (tables.includes(dep)) { - reverseDependencies[dep].push(tableName); - } - } - } - // 拓扑排序:找到删除顺序(先删除依赖表,再删除被依赖的表) - const deleteOrder = []; - const visited = new Set(); - const visiting = new Set(); + // 对所有表进行拓扑排序 + for (const tableName of tables) { + if (!visited.has(tableName)) { + visit(tableName); + } + } - function visit(tableName) { - if (visiting.has(tableName)) { - console.warn(`检测到循环依赖: ${tableName}`); - return; - } - if (visited.has(tableName)) { - return; - } + console.log(`📋 删除顺序:`); - visiting.add(tableName); + // 按依赖顺序批量删除所有表 + const dropTablesSQL = deleteOrder.map(tableName => `DROP TABLE IF EXISTS ${tableName}`).join('; '); - // 先处理依赖当前表的表(依赖表要先删除) - const dependents = reverseDependencies[tableName] || []; - for (const dependent of dependents) { - if (tables.includes(dependent)) { - visit(dependent); - } + try { + await executeSQL(dropTablesSQL, isRemote); + console.log(`✅ 批量删除 ${deleteOrder.length} 个表成功`); + } catch (error) { + console.warn('批量删除表失败,回退到逐个删除:', error); + // 回退到逐个删除 + for (const tableName of deleteOrder) { + try { + await executeSQL(`DROP TABLE IF EXISTS ${tableName}`, isRemote); + console.log(`✅ 删除表 ${tableName} 成功`); + } catch (error) { + console.warn(`删除表 ${tableName} 时出现错误,忽略:`, error); + } + } + } + } else { + console.log('📋 数据库中没有表'); } - visiting.delete(tableName); - visited.add(tableName); - deleteOrder.push(tableName); - } - - // 对所有表进行拓扑排序 - for (const tableName of tables) { - if (!visited.has(tableName)) { - visit(tableName); - } - } - - console.log(`📋 删除顺序:`, deleteOrder.join(' -> ')); - - // 按依赖顺序批量删除所有表 - const dropTablesSQL = deleteOrder.map(tableName => `DROP TABLE IF EXISTS ${tableName}`).join('; '); - - try { - await executeSQL(dropTablesSQL, isRemote); - console.log(`✅ 批量删除 ${deleteOrder.length} 个表成功`); - } catch (error) { - console.warn('批量删除表失败,回退到逐个删除:', error); - // 回退到逐个删除 - for (const tableName of deleteOrder) { - try { - await executeSQL(`DROP TABLE IF EXISTS ${tableName}`, isRemote); - console.log(`✅ 删除表 ${tableName} 成功`); - } catch (error) { - console.warn(`删除表 ${tableName} 时出现错误,忽略:`, error); - } + try { + await executeSQL('PRAGMA foreign_keys = ON', isRemote); + console.log('✅ 已启用外键约束'); + } catch (error) { + console.warn('启用外键约束时出现错误,忽略:', error); } - } - } else { - console.log('📋 数据库中没有表'); - } - try { - await executeSQL('PRAGMA foreign_keys = ON', isRemote); - console.log('✅ 已启用外键约束'); - } catch (error) { - console.warn('启用外键约束时出现错误,忽略:', error); - } + // 4. 清空自增序列(忽略错误) + try { + await executeSQL('DELETE FROM sqlite_sequence', isRemote); + console.log('✅ 清空自增序列成功'); + } catch (error) { + console.warn('清空自增序列时出现错误,忽略:', error); + } - // 4. 清空自增序列(忽略错误) - try { - await executeSQL('DELETE FROM sqlite_sequence', isRemote); - console.log('✅ 清空自增序列成功'); + console.log('✅ 数据库清理完成'); } catch (error) { - console.warn('清空自增序列时出现错误,忽略:', error); + console.error('❌ 数据库清理失败:', error); + throw error; } - - console.log('✅ 数据库清理完成'); - } catch (error) { - console.error('❌ 数据库清理失败:', error); - throw error; - } } // 执行数据库初始化 async function executeDbInit(isRemote) { - console.log(`🔧 初始化数据库${isRemote ? ' (远程)' : ''}...`); - - // 先清理数据库 - await executeDbClean(isRemote); - - // 执行 schema.sql - try { - execSync(`npx wrangler d1 execute cem-db --file=db/schema.sql${isRemote ? ' --remote' : ''} --json`, { - stdio: 'ignore', - cwd: projectRoot - }); - console.log('✅ 数据库初始化完成'); - } catch (error) { - console.error('❌ 数据库初始化失败:', error.message); - process.exit(1); - } + console.log(`🔧 初始化数据库${isRemote ? ' (远程)' : ''}...`); + + // 先清理数据库 + await executeDbClean(isRemote); + + // 执行 schema.sql + try { + execSync(`npx wrangler d1 execute cem-db --file=db/schema.sql${isRemote ? ' --remote' : ''} --json`, { + stdio: 'ignore', + cwd: projectRoot + }); + console.log('✅ 数据库初始化完成'); + } catch (error) { + console.error('❌ 数据库初始化失败:', error.message); + process.exit(1); + } } // 执行迁移命令 async function executeMigrate() { - console.log('🔧 执行数据库迁移...'); - - try { - execSync('npx wrangler d1 migrations apply cem-db', { - stdio: 'inherit', - cwd: join(projectRoot, 'worker') - }); - console.log('✅ 数据库迁移完成'); - } catch (error) { - console.error('❌ 数据库迁移失败:', error.message); - process.exit(1); - } + console.log('🔧 执行数据库迁移...'); + + try { + execSync('npx wrangler d1 migrations apply cem-db', { + stdio: 'inherit', + cwd: join(projectRoot, 'worker') + }); + console.log('✅ 数据库迁移完成'); + } catch (error) { + console.error('❌ 数据库迁移失败:', error.message); + process.exit(1); + } } // 执行导入命令 async function executeImport() { - console.log('🔧 导入邮件数据...'); - - try { - execSync('node import-emails-to-db-local.js', { - stdio: 'inherit', - cwd: join(projectRoot, 'worker') - }); - console.log('✅ 邮件数据导入完成'); - } catch (error) { - console.error('❌ 邮件数据导入失败:', error.message); - process.exit(1); - } + console.log('🔧 导入邮件数据...'); + + try { + execSync('node import-emails-to-db-local.js', { + stdio: 'inherit', + cwd: join(projectRoot, 'worker') + }); + console.log('✅ 邮件数据导入完成'); + } catch (error) { + console.error('❌ 邮件数据导入失败:', error.message); + process.exit(1); + } } // 执行自定义 SQL 命令 async function executeSQL(sql, isRemote) { - console.log(`🔧 执行 SQL 命令${isRemote ? ' (远程)' : ''}...`); - console.log(`SQL: ${sql.replace(/\s+/g, ' ').replace(/\n/g, ' ')}`); - - try { - // 使用 spawn 而不是 execSync 来避免 shell 引号问题 - const { spawn } = await import('child_process'); - - return new Promise((resolve, reject) => { - const args = ['wrangler', 'd1', 'execute', 'cem-db', '--command', sql, '--json']; - if (isRemote) args.push('--remote'); - - let stdout = ''; - let stderr = ''; - - const process = spawn('npx', args, { - cwd: projectRoot, - stdio: ['inherit', 'pipe', 'pipe'] - }); - - process.stdout.on('data', (data) => { - stdout += data.toString(); - }); - - process.stderr.on('data', (data) => { - stderr += data.toString(); - }); - - process.on('close', (code) => { - if (code === 0) { - console.log('✅ SQL 命令执行完成'); - try { - // 解析 JSON 输出 - wrangler 返回的是数组格式 - const results = JSON.parse(stdout); - - // 处理数组格式的结果 - if (Array.isArray(results) && results.length > 0) { - const result = results[0]; // 取第一个结果 - - // 如果是查询结果,显示数据 - if (result && result.results && result.results.length > 0) { - console.log('📊 查询结果:'); - console.log(JSON.stringify(result)); - } else if (result && result.success) { - console.log('✅ 命令执行成功'); - } - - resolve(result); - } else { - console.log('✅ 命令执行成功'); - resolve({ success: true }); - } - } catch (parseError) { - console.log('⚠️ JSON 解析失败,返回原始输出'); - console.log('原始输出:', stdout); - resolve({ success: true, raw: stdout }); - } - } else { - console.error('❌ SQL 命令执行失败'); - console.error('stderr:', stderr); - console.error('stdout:', stdout); - reject(new Error(`Command failed with code ${code}`)); - } - }); + console.log(`🔧 执行 SQL 命令${isRemote ? ' (远程)' : ''}...`); + // console.log(`SQL: ${sql.replace(/\s+/g, ' ').replace(/\n/g, ' ')}`); - process.on('error', (error) => { + try { + // 使用 spawn 而不是 execSync 来避免 shell 引号问题 + const { spawn } = await import('child_process'); + + return new Promise((resolve, reject) => { + const args = ['wrangler', 'd1', 'execute', 'cem-db', '--command', sql, '--json']; + if (isRemote) args.push('--remote'); + + let stdout = ''; + let stderr = ''; + + const process = spawn('npx', args, { + cwd: projectRoot, + stdio: ['inherit', 'pipe', 'pipe'] + }); + + process.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + process.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + process.on('close', (code) => { + if (code === 0) { + console.log('✅ SQL 命令执行完成'); + try { + // 解析 JSON 输出 - wrangler 返回的是数组格式 + const results = JSON.parse(stdout); + + // 处理数组格式的结果 + if (Array.isArray(results) && results.length > 0) { + const result = results[0]; // 取第一个结果 + + // 如果是查询结果,显示数据 + if (result && result.results && result.results.length > 0) { + // console.log('📊 查询结果:'); + // console.log(JSON.stringify(result)); + } else if (result && result.success) { + console.log('✅ 命令执行成功'); + } + + resolve(result); + } else { + console.log('✅ 命令执行成功'); + resolve({ success: true }); + } + } catch (parseError) { + console.log('⚠️ JSON 解析失败,返回原始输出'); + console.log('原始输出:', stdout); + resolve({ success: true, raw: stdout }); + } + } else { + console.error('❌ SQL 命令执行失败'); + console.error('stderr:', stderr); + console.error('stdout:', stdout); + reject(new Error(`Command failed with code ${code}`)); + } + }); + + process.on('error', (error) => { + console.error('❌ SQL 命令执行失败:', error.message); + reject(error); + }); + }); + } catch (error) { console.error('❌ SQL 命令执行失败:', error.message); - reject(error); - }); - }); - } catch (error) { - console.error('❌ SQL 命令执行失败:', error.message); - throw error; - } + throw error; + } } // 显示帮助信息 function showHelp() { - console.log(` + console.log(` 📋 数据库操作脚本 用法: @@ -436,40 +431,40 @@ SQL 执行: // 主函数 async function main() { - const { command, isRemote, sql } = parseArgs(); - - try { - switch (command) { - case 'init': - await executeDbInit(isRemote); - break; - case 'migrate': - await executeMigrate(); - break; - case 'import': - await executeImport(); - break; - case 'sql': - if (!sql) { - console.error('❌ 需要提供 SQL 命令'); - showHelp(); - process.exit(1); + const { command, isRemote, sql } = parseArgs(); + + try { + switch (command) { + case 'init': + await executeDbInit(isRemote); + break; + case 'migrate': + await executeMigrate(); + break; + case 'import': + await executeImport(); + break; + case 'sql': + if (!sql) { + console.error('❌ 需要提供 SQL 命令'); + showHelp(); + process.exit(1); + } + await executeSQL(sql, isRemote); + break; + case 'help': + default: + showHelp(); + break; } - await executeSQL(sql, isRemote); - break; - case 'help': - default: - showHelp(); - break; - } - if (command !== 'help') { - console.log('🎉 操作成功!'); + if (command !== 'help') { + console.log('🎉 操作成功!'); + } + } catch (error) { + console.error('❌ 操作失败:', error.message); + process.exit(1); } - } catch (error) { - console.error('❌ 操作失败:', error.message); - process.exit(1); - } } -main(); \ No newline at end of file +main(); diff --git a/scripts/deploy.js b/scripts/deploy.js index bbb6096..c67a0f7 100644 --- a/scripts/deploy.js +++ b/scripts/deploy.js @@ -5,14 +5,14 @@ * 包含资源创建、构建、部署到 Cloudflare 的完整流程 */ -import { spawn, execSync } from 'child_process'; -import { fileURLToPath } from 'url'; -import { dirname, join } from 'path'; -import { readFileSync, writeFileSync, existsSync, copyFileSync } from 'fs'; -import { createHash, randomBytes } from 'crypto'; -import { createInterface } from 'readline'; -import { parse, stringify } from '@iarna/toml'; -import { getEnvironmentInfo, isEnvironmentAvailable } from './env-detector.js'; +import {spawn, execSync} from 'child_process'; +import {fileURLToPath} from 'url'; +import {dirname, join} from 'path'; +import {readFileSync, writeFileSync, existsSync, copyFileSync} from 'fs'; +import {createHash, randomBytes} from 'crypto'; +import {createInterface} from 'readline'; +import {parse, stringify} from '@iarna/toml'; +import {isEnvironmentAvailable} from './env-detector.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -26,362 +26,352 @@ const BLUE = "\x1b[34m"; const NC = "\x1b[0m"; // Reset function log(type, msg) { - const colors = { error: RED, success: GREEN, warn: YELLOW, info: BLUE }; - console.log(`${colors[type] || NC}${msg}${NC}`); + const colors = {error: RED, success: GREEN, warn: YELLOW, info: BLUE}; + console.log(`${colors[type] || NC}${msg}${NC}`); } // ================== 🔧 工具函数 ================== function run(exec, stdio = ["pipe", "pipe", "pipe"]) { - let output, err; - try { - output = execSync(exec, { encoding: "utf-8", stdio }); - } catch (err1) { + let output, err; try { - output = execSync(exec, { encoding: "utf-8", stdio }); - } catch (err2) { - err = err2; - if (err2 && err2?.stdout?.includes("Authentication error")) { + output = execSync(exec, {encoding: "utf-8", stdio}); + } catch (err1) { try { - output = execSync(exec, { encoding: "utf-8", stdio }); - } catch (err3) { - err = err3; + output = execSync(exec, {encoding: "utf-8", stdio}); + } catch (err2) { + err = err2; + if (err2 && err2?.stdout?.includes("Authentication error")) { + try { + output = execSync(exec, {encoding: "utf-8", stdio}); + } catch (err3) { + err = err3; + } + } } - } } - } - return { output, err }; + return {output, err}; } // ================== 📦 资源创建 ================== function createOrGetResource(type, name) { - let id = ""; - let { output, err } = run(`wrangler ${type} create ${name}`); - - if (err?.stderr.includes("already exists") || err?.stdout.includes("already exists")) { - const { - output: listOutput, - err: err1 - } = run(`wrangler ${type} list ${type === "d1" ? "--json" : ""} ${type === "r2 bucket" ? "" : "| jq -c '.'"}`) - err1 && process.exit(1) - if (type === "r2 bucket") { - id = name - } else { - const jsonStr = listOutput.split('\n').filter(l => l.trim().startsWith('[') || l.trim().startsWith('{')).join('\n'); - const jsonList = JSON.parse(jsonStr); - id = type === "d1" - ? jsonList.find(d1 => d1.name === name)?.uuid - : jsonList.find(k => k.title === name)?.id; + let id = ""; + let {output, err} = run(`wrangler ${type} create ${name}`); + + if (err?.stderr.includes("already exists") || err?.stdout.includes("already exists")) { + const { + output: listOutput, + err: err1 + } = run(`wrangler ${type} list ${type === "d1" ? "--json" : ""} ${type === "r2 bucket" ? "" : "| jq -c '.'"}`) + err1 && process.exit(1) + if (type === "r2 bucket") { + id = name + } else { + const jsonStr = listOutput.split('\n').filter(l => l.trim().startsWith('[') || l.trim().startsWith('{')).join('\n'); + const jsonList = JSON.parse(jsonStr); + id = type === "d1" + ? jsonList.find(d1 => d1.name === name)?.uuid + : jsonList.find(k => k.title === name)?.id; + } + log("warn", `⚠️ ${type} ${name} 已存在 ${id}`); + return id; + } else if (err) { + log("error", `❌ ${type} 创建失败, ${err}`); + console.log(`stdout: ${err?.stdout}`) + console.log(`stderr: ${err?.stderr}`) + process.exit(1); } - log("warn", `⚠️ ${type} ${name} 已存在 ${id}`); + const match = output.match(/id = "([^"]+)"/); + if (match) id = match[1]; + log("success", `✅ ${type} 创建成功: ${type === "r2" ? id : name}`); return id; - } else if (err) { - log("error", `❌ ${type} 创建失败, ${err}`); - console.log(`stdout: ${err?.stdout}`) - console.log(`stderr: ${err?.stderr}`) - process.exit(1); - } - const match = output.match(/id = "([^"]+)"/); - if (match) id = match[1]; - log("success", `✅ ${type} 创建成功: ${type === "r2" ? id : name}`); - return id; } // ================== 🔨 构建函数 ================== async function buildvue() { - console.log('🔨 构建前端...'); - return new Promise((resolve, reject) => { - const build = spawn('npx', ['vite', 'build'], { - cwd: join(projectRoot, 'vue'), - stdio: 'inherit', - shell: true + console.log('🔨 构建前端...'); + return new Promise((resolve, reject) => { + const build = spawn('npx', ['vite', 'build'], { + cwd: join(projectRoot, 'vue'), + stdio: 'inherit', + shell: true + }); + + build.on('exit', (code) => { + if (code === 0) { + console.log('✅ 前端构建完成'); + resolve(); + } else { + reject(new Error(`前端构建失败,代码: ${code}`)); + } + }); }); - - build.on('exit', (code) => { - if (code === 0) { - console.log('✅ 前端构建完成'); - resolve(); - } else { - reject(new Error(`前端构建失败,代码: ${code}`)); - } - }); - }); } async function buildBackend() { - console.log('🔨 构建后端...'); - return new Promise((resolve, reject) => { - const build = spawn('npx', ['tsc'], { - cwd: join(projectRoot, 'worker'), - stdio: 'inherit', - shell: true + console.log('🔨 构建后端...'); + return new Promise((resolve, reject) => { + const build = spawn('npx', ['tsc'], { + cwd: join(projectRoot, 'worker'), + stdio: 'inherit', + shell: true + }); + + build.on('exit', (code) => { + if (code === 0) { + console.log('✅ 后端构建完成'); + resolve(); + } else { + reject(new Error(`后端构建失败,代码: ${code}`)); + } + }); }); - - build.on('exit', (code) => { - if (code === 0) { - console.log('✅ 后端构建完成'); - resolve(); - } else { - reject(new Error(`后端构建失败,代码: ${code}`)); - } - }); - }); } // ================== 📝 配置文件更新 ================== function updateWranglerToml(dbId, kvId, devMode) { - const wranglerPath = join(projectRoot, 'wrangler.toml'); - const examplePath = join(projectRoot, 'wrangler.example.toml'); - - if (!existsSync(wranglerPath)) { - if (existsSync(examplePath)) { - copyFileSync(examplePath, wranglerPath); - } else { - log("error", "❌ 找不到 wrangler.example.toml 文件"); - process.exit(1); + const wranglerPath = join(projectRoot, 'wrangler.toml'); + const examplePath = join(projectRoot, 'wrangler.example.toml'); + + if (!existsSync(wranglerPath)) { + if (existsSync(examplePath)) { + copyFileSync(examplePath, wranglerPath); + } else { + log("error", "❌ 找不到 wrangler.example.toml 文件"); + process.exit(1); + } } - } - try { - // 读取并解析 TOML 文件 - const tomlContent = readFileSync(wranglerPath, 'utf-8'); - const config = parse(tomlContent); + try { + // 读取并解析 TOML 文件 + const tomlContent = readFileSync(wranglerPath, 'utf-8'); + const config = parse(tomlContent); + + if (devMode) { + // 开发环境:更新 env.dev 部分的配置 + if (!config.env || !config.env.dev) { + log("error", "❌ 配置文件中缺少 [env.dev] 段"); + process.exit(1); + } + + // 更新开发环境 D1 数据库 ID + if (config.env.dev.d1_databases?.[0]) { + config.env.dev.d1_databases[0].database_id = dbId; + } else { + log("warn", "⚠️ 开发环境 D1 数据库配置不存在,跳过更新"); + } + + // 更新开发环境 KV 命名空间 ID + if (config.env.dev.kv_namespaces?.[0]) { + config.env.dev.kv_namespaces[0].id = kvId; + } else { + log("warn", "⚠️ 开发环境 KV 命名空间配置不存在,跳过更新"); + } + + log("info", `🔧 更新开发环境配置: D1=${dbId}, KV=${kvId}`); + } else { + // 生产环境:更新主配置部分 + // 更新生产环境 D1 数据库 ID + if (config.d1_databases?.[0]) { + config.d1_databases[0].database_id = dbId; + } else { + log("warn", "⚠️ 生产环境 D1 数据库配置不存在,跳过更新"); + } + + // 更新生产环境 KV 命名空间 ID + if (config.kv_namespaces?.[0]) { + config.kv_namespaces[0].id = kvId; + } else { + log("warn", "⚠️ 生产环境 KV 命名空间配置不存在,跳过更新"); + } + + log("info", `🔧 更新生产环境配置: D1=${dbId}, KV=${kvId}`); + } - if (devMode) { - // 开发环境:更新 env.dev 部分的配置 - if (!config.env || !config.env.dev) { - log("error", "❌ 配置文件中缺少 [env.dev] 段"); - process.exit(1); - } - - // 更新开发环境 D1 数据库 ID - if (config.env.dev.d1_databases?.[0]) { - config.env.dev.d1_databases[0].database_id = dbId; - } else { - log("warn", "⚠️ 开发环境 D1 数据库配置不存在,跳过更新"); - } - - // 更新开发环境 KV 命名空间 ID - if (config.env.dev.kv_namespaces?.[0]) { - config.env.dev.kv_namespaces[0].id = kvId; - } else { - log("warn", "⚠️ 开发环境 KV 命名空间配置不存在,跳过更新"); - } - - log("info", `🔧 更新开发环境配置: D1=${dbId}, KV=${kvId}`); - } else { - // 生产环境:更新主配置部分 - // 更新生产环境 D1 数据库 ID - if (config.d1_databases?.[0]) { - config.d1_databases[0].database_id = dbId; - } else { - log("warn", "⚠️ 生产环境 D1 数据库配置不存在,跳过更新"); - } - - // 更新生产环境 KV 命名空间 ID - if (config.kv_namespaces?.[0]) { - config.kv_namespaces[0].id = kvId; - } else { - log("warn", "⚠️ 生产环境 KV 命名空间配置不存在,跳过更新"); - } - - log("info", `🔧 更新生产环境配置: D1=${dbId}, KV=${kvId}`); - } + // 将配置写回文件 + const updatedTomlContent = stringify(config); + writeFileSync(wranglerPath, updatedTomlContent); + + // 验证更新是否成功 + const verifyConfig = parse(updatedTomlContent); + if (devMode) { + const devDbId = verifyConfig.env?.dev?.d1_databases?.[0]?.database_id; + const devKvId = verifyConfig.env?.dev?.kv_namespaces?.[0]?.id; + if (devDbId === dbId && devKvId === kvId) { + log("success", `✅ 已更新 wrangler.toml (开发环境)`); + } else { + log("error", `❌ 配置更新验证失败`); + process.exit(1); + } + } else { + const prodDbId = verifyConfig.d1_databases?.[0]?.database_id; + const prodKvId = verifyConfig.kv_namespaces?.[0]?.id; + if (prodDbId === dbId && prodKvId === kvId) { + log("success", `✅ 已更新 wrangler.toml (生产环境)`); + } else { + log("error", `❌ 配置更新验证失败`); + process.exit(1); + } + } - // 将配置写回文件 - const updatedTomlContent = stringify(config); - writeFileSync(wranglerPath, updatedTomlContent); - - // 验证更新是否成功 - const verifyConfig = parse(updatedTomlContent); - if (devMode) { - const devDbId = verifyConfig.env?.dev?.d1_databases?.[0]?.database_id; - const devKvId = verifyConfig.env?.dev?.kv_namespaces?.[0]?.id; - if (devDbId === dbId && devKvId === kvId) { - log("success", `✅ 已更新 wrangler.toml (开发环境)`); - } else { - log("error", `❌ 配置更新验证失败`); - process.exit(1); - } - } else { - const prodDbId = verifyConfig.d1_databases?.[0]?.database_id; - const prodKvId = verifyConfig.kv_namespaces?.[0]?.id; - if (prodDbId === dbId && prodKvId === kvId) { - log("success", `✅ 已更新 wrangler.toml (生产环境)`); - } else { - log("error", `❌ 配置更新验证失败`); + } catch (error) { + log("error", `❌ 解析 TOML 文件失败: ${error.message}`); process.exit(1); - } } - - } catch (error) { - log("error", `❌ 解析 TOML 文件失败: ${error.message}`); - process.exit(1); - } } // ================== 🚀 部署函数 ================== async function deploy(devMode) { - const envInfo = getEnvironmentInfo(); - console.log(`🚀 部署到 Cloudflare (${envInfo.description})...`); - - return new Promise((resolve, reject) => { - let deployCmd; - - if (envInfo.isLocal) { - deployCmd = spawn('npx', ['wrangler', 'deploy', devMode ? '--env dev' : ''], { - cwd: join(projectRoot, 'worker'), - stdio: 'inherit', - shell: true - }); - } else { - // Docker 环境 - deployCmd = spawn('docker', ['exec', 'node', 'bash', '-c', `cd worker && npx wrangler deploy ${devMode ? '--env dev' : ''}`], { - stdio: 'inherit', - shell: true - }); - } + console.log(`🚀 部署到 Cloudflare...`); + + return new Promise((resolve, reject) => { + const deployArgs = ['wrangler', 'deploy']; + if (devMode) { + deployArgs.push('--env', 'dev'); + } - deployCmd.on('exit', (code) => { - if (code === 0) { - console.log('✅ 部署完成'); - resolve(); - } else { - reject(new Error(`部署失败,代码: ${code}`)); - } + const deployCmd = spawn('npx', deployArgs, { + cwd: join(projectRoot, 'worker'), + stdio: 'inherit', + shell: true + }); + + deployCmd.on('exit', (code) => { + if (code === 0) { + console.log('✅ 部署完成'); + resolve(); + } else { + reject(new Error(`部署失败,代码: ${code}`)); + } + }); }); - }); } // ================== 🔑 用户交互 ================== function createReadlineInterface() { - return createInterface({ - input: process.stdin, - output: process.stdout, - }); + return createInterface({ + input: process.stdin, + output: process.stdout, + }); } function question(query) { - return new Promise(resolve => { - const rl = createReadlineInterface(); - rl.question(query, ans => { - rl.close(); - resolve(ans); + return new Promise(resolve => { + const rl = createReadlineInterface(); + rl.question(query, ans => { + rl.close(); + resolve(ans); + }); }); - }); } // ================== 🗑️ 删除所有资源 ================== function deleteAll() { - log("warn", "🗑️ 删除所有 Cloudflare 资源..."); - run(`for name in $(wrangler d1 list --json | jq -r '.[].name'); do wrangler d1 delete "$name" -y; done`, "inherit"); - run(`for id in $(wrangler kv namespace list | jq -r '.[].id'); do wrangler kv namespace delete --namespace-id "$id"; done`, "inherit"); - run(`for name in $(wrangler r2 bucket list | awk '/^name:/ {print $2}'); do wrangler r2 bucket delete "$name"; done`, "inherit"); - log("success", "✅ 所有资源已删除"); - process.exit(0); + log("warn", "🗑️ 删除所有 Cloudflare 资源..."); + run(`for name in $(wrangler d1 list --json | jq -r '.[].name'); do wrangler d1 delete "$name" -y; done`, "inherit"); + run(`for id in $(wrangler kv namespace list | jq -r '.[].id'); do wrangler kv namespace delete --namespace-id "$id"; done`, "inherit"); + run(`for name in $(wrangler r2 bucket list | awk '/^name:/ {print $2}'); do wrangler r2 bucket delete "$name"; done`, "inherit"); + log("success", "✅ 所有资源已删除"); + process.exit(0); } // ================== 🎯 主函数 ================== async function main() { - try { - // 检查是否是删除所有资源的命令 - const target = process.argv[2] || ""; - if (target === "deleteAll") { - deleteAll(); - } + try { + // 检查是否是删除所有资源的命令 + const target = process.argv[2] || ""; + if (target === "deleteAll") { + deleteAll(); + } - console.log('👋 欢迎使用 CEM (Cloud Email Manager) 部署脚本'); + console.log('👋 欢迎使用 CEM (Cloud Email Manager) 部署脚本'); - // 检查环境 - if (!isEnvironmentAvailable()) { - console.error('❌ 环境不可用,请检查 wrangler 安装或 Docker 容器状态'); - process.exit(1); - } - - const envInfo = getEnvironmentInfo(); - console.log(`🔧 检测到环境: ${envInfo.description}`); + // 检查环境 + if (!isEnvironmentAvailable()) { + console.error('❌ 环境不可用,请检查本地 wrangler 安装状态'); + process.exit(1); + } - // 检查 wrangler 登录状态 - log("info", "🔐 检查 Cloudflare 登录状态..."); - const { output, err: err1 } = run("wrangler whoami"); - if (err1) { - log("error", `❌ 检查 Cloudflare 登录状态失败: ${err1}`); - process.exit(1); - } - if (output.includes("You are not authenticated")) { - log("warn", "⚠️ 未登录 Cloudflare,正在启动登录流程..."); - log("warn", "⚠️ 如果是在 docker 容器内, 请使用 curl 请求未完成的响应..."); - run("wrangler login", "inherit"); - } else { - log("success", "✅ 已登录 Cloudflare"); - } + // 检查 wrangler 登录状态 + log("info", "🔐 检查 Cloudflare 登录状态..."); + const {output, err: err1} = run("wrangler whoami"); + if (err1) { + log("error", `❌ 检查 Cloudflare 登录状态失败: ${err1}`); + process.exit(1); + } + if (output.includes("You are not authenticated")) { + log("warn", "⚠️ 未登录 Cloudflare,正在启动登录流程..."); + run("wrangler login", "inherit"); + } else { + log("success", "✅ 已登录 Cloudflare"); + } - // 获取用户输入 - const dev_input = (await question(`是否开发模式? (y/n) [n]: `)).trim() || 'n'; - const devMode = /^[Yy]$/.test(dev_input); - const DB_NAME = devMode ? 'cem-db-dev' : 'cem-db'; - const KV_NAME = devMode ? 'cem-kv-dev' : 'cem-kv'; - const BUCKET_NAME = devMode ? 'cem-r2-dev' : 'cem-r2'; - - devMode && log("info", "🚧 开发模式"); - - // 创建资源 - log("info", "📦 创建 / 获取资源..."); - const dbId = createOrGetResource("d1", DB_NAME); - const kvId = createOrGetResource("kv namespace", KV_NAME); - createOrGetResource("r2 bucket", BUCKET_NAME); - - // 更新配置文件 - log("info", "📝 更新 wrangler.toml ..."); - updateWranglerToml(dbId, kvId, devMode); - - // 初始化数据库 - log("info", "🗄️ 初始化数据库结构 (schema.sql)..."); - const { err: err2 } = run(`wrangler d1 execute ${DB_NAME} --file=./db/schema.sql --remote`); - if (err2) { - log("error", `❌ 初始化数据库结构失败: ${err2}`); - process.exit(1); - } - log("success", "✅ 数据库结构已初始化"); - log("success", "✅ 默认用户 admin/123456"); - - // 构建和部署 - await buildvue(); - await buildBackend(); - await deploy(devMode); - - // 完成信息 - log("success", "🎉 部署完成"); - log("info", "📋 部署信息:"); - devMode && log("info", " 🚧 开发模式"); - log("info", ` D1 数据库 ID: ${dbId}`); - log("info", ` KV 命名空间 ID: ${kvId}`); - log("info", ` R2 存储桶: ${BUCKET_NAME}\n`); - log("warn", "重要提醒:"); - log("info", "1. 请在 Cloudflare 控制台配置邮件路由"); - log("info", "2. 记得修改默认密码 admin/123456\n"); - log("success", "🌐 访问地址:"); - - if (devMode) { - log("info", " 开发环境: http://localhost:8787"); - log("info", " 启动开发服务器: npm run dev"); - log("info", " 使用开发环境资源: wrangler dev --env dev"); - } else { - log("info", ` Worker 地址: https://cem.workers.dev`); - log("info", " 使用生产环境资源: wrangler dev"); - } + // 获取用户输入 + const dev_input = (await question(`是否开发模式? (y/n) [n]: `)).trim() || 'n'; + const devMode = /^[Yy]$/.test(dev_input); + const DB_NAME = devMode ? 'cem-db-dev' : 'cem-db'; + const KV_NAME = devMode ? 'cem-kv-dev' : 'cem-kv'; + const BUCKET_NAME = devMode ? 'cem-r2-dev' : 'cem-r2'; + + devMode && log("info", "🚧 开发模式"); + + // 创建资源 + log("info", "📦 创建 / 获取资源..."); + const dbId = createOrGetResource("d1", DB_NAME); + const kvId = createOrGetResource("kv namespace", KV_NAME); + createOrGetResource("r2 bucket", BUCKET_NAME); + + // 更新配置文件 + log("info", "📝 更新 wrangler.toml ..."); + updateWranglerToml(dbId, kvId, devMode); + + // 初始化数据库 + log("info", "🗄️ 初始化数据库结构 (schema.sql)..."); + const {err: err2} = run(`wrangler d1 execute ${DB_NAME} --file=./db/schema.sql --remote`); + if (err2) { + log("error", `❌ 初始化数据库结构失败: ${err2}`); + process.exit(1); + } + log("success", "✅ 数据库结构已初始化"); + log("success", "✅ 默认用户 admin/123456"); + + // 构建和部署 + await buildvue(); + await buildBackend(); + await deploy(devMode); + + // 完成信息 + log("success", "🎉 部署完成"); + log("info", "📋 部署信息:"); + devMode && log("info", " 🚧 开发模式"); + log("info", ` D1 数据库 ID: ${dbId}`); + log("info", ` KV 命名空间 ID: ${kvId}`); + log("info", ` R2 存储桶: ${BUCKET_NAME}\n`); + log("warn", "重要提醒:"); + log("info", "1. 请在 Cloudflare 控制台配置邮件路由"); + log("info", "2. 记得修改默认密码 admin/123456\n"); + log("success", "🌐 访问地址:"); + + if (devMode) { + log("info", " 开发环境: http://localhost:8787"); + log("info", " 启动开发服务器: npm run dev"); + log("info", " 使用开发环境资源: wrangler dev --env dev"); + } else { + log("info", ` Worker 地址: https://cem.workers.dev`); + log("info", " 使用生产环境资源: wrangler dev"); + } - log("info", "\n📋 配置说明:"); - log("info", " [dev] 段: 本地开发服务器配置 (端口 8787)"); - log("info", " [triggers] 段: 定时任务配置"); - log("info", " [observability] 段: 可观测性配置 (全局)"); - log("info", " [env.dev] 段: 开发环境资源绑定"); + log("info", "\n📋 配置说明:"); + log("info", " [dev] 段: 本地开发服务器配置 (端口 8787)"); + log("info", " [triggers] 段: 定时任务配置"); + log("info", " [observability] 段: 可观测性配置 (全局)"); + log("info", " [env.dev] 段: 开发环境资源绑定"); - log("info", "\n📚 更多信息请查看 README.md"); + log("info", "\n📚 更多信息请查看 README.md"); - } catch (error) { - console.error('❌ 部署失败:', error.message); - process.exit(1); - } + } catch (error) { + console.error('❌ 部署失败:', error.message); + process.exit(1); + } } main(); diff --git a/scripts/env-detector.js b/scripts/env-detector.js index 0a7876e..5425d46 100644 --- a/scripts/env-detector.js +++ b/scripts/env-detector.js @@ -6,35 +6,6 @@ */ import { execSync } from 'child_process'; -import { join } from 'path'; -import fs from 'fs'; - -/** - * 检测当前运行环境 - * @returns {string} 'local' | 'docker' - */ -export function detectEnvironment() { - // 检查是否在 Docker 容器内 - if (process.env.DOCKER_CONTAINER === 'true' || fs.existsSync('/.dockerenv')) { - return 'docker'; - } - return 'local' -} - - -/** - * 获取环境信息 - * @returns {object} 环境信息对象 - */ -export function getEnvironmentInfo() { - const env = detectEnvironment(); - return { - type: env, - isLocal: env === 'local', - isDocker: env === 'docker', - description: env === 'local' ? '本地环境' : 'Docker 环境' - }; -} /** * 检查环境是否可用 @@ -42,16 +13,9 @@ export function getEnvironmentInfo() { */ export function isEnvironmentAvailable() { try { - const env = detectEnvironment(); - if (env === 'local') { - // 本地环境:检查 Docker 容器是否运行(因为本地 wrangler 有兼容性问题) - execSync('docker ps | grep -q "node"', { stdio: 'ignore' }); - return true; - } else { - // Docker 环境:检查 npx 是否可用 - execSync('npx --version', { stdio: 'ignore' }); - return true; - } + execSync('npx --version', { stdio: 'ignore' }); + execSync('wrangler --version', { stdio: 'ignore' }); + return true; } catch (e) { return false; } diff --git a/test-attachment-naming.js b/test-attachment-naming.js new file mode 100644 index 0000000..b2d6dff --- /dev/null +++ b/test-attachment-naming.js @@ -0,0 +1,183 @@ +#!/usr/bin/env node + +/** + * 测试新的附件命名逻辑 + * 验证使用 Content-ID 作为文件名的方案 + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +async function testAttachmentNaming() { + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('📎 测试新的附件命名逻辑'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + // 读取原始邮件 + const emlPath = path.join(__dirname, 'email_1dfba050-c2cf-440a-8987-8de6c6a07fd4.eml'); + const rawEmail = fs.readFileSync(emlPath, 'utf-8'); + + // 使用 postal-mime 解析 + const PostalMime = (await import('postal-mime')).default; + const parser = new PostalMime(); + const parsed = await parser.parse(rawEmail); + + console.log('📧 邮件信息:'); + console.log(' 主题:', parsed.subject); + console.log(' 附件数量:', parsed.attachments?.length || 0, '\n'); + + if (parsed.attachments && parsed.attachments.length > 0) { + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('📋 附件命名规则测试'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + const emailId = 'test-email-id'; + const attachmentRecords = []; + + for (const att of parsed.attachments) { + console.log('─────────────────────────────────────────'); + + // 确定文件名、Content-ID 和 R2 存储路径 + let filename; // 数据库中保存的文件名(原始文件名) + let r2Filename; // R2 中实际存储的文件名 + let contentId = null; + + // 提取 Content-ID(如果有) + if (att.contentId) { + contentId = att.contentId.replace(/^<|>$/g, ''); + } + + // 确定数据库中的文件名和 R2 存储文件名 + if (contentId) { + // 内嵌图片:R2 使用 Content-ID,数据库保留原文件名 + const ext = att.mimeType?.split('/')[1] || 'bin'; + r2Filename = `${contentId}.${ext}`; + filename = att.filename || r2Filename; // 优先原文件名 + console.log('✅ 类型: 内嵌图片'); + console.log(' 数据库文件名:', filename); + console.log(' R2 存储文件名:', r2Filename); + console.log(' Content-ID:', contentId); + } else if (att.filename) { + // 普通附件:R2 和数据库都使用原文件名 + filename = att.filename; + r2Filename = att.filename; + console.log('✅ 类型: 普通附件'); + console.log(' 文件名:', filename); + } else { + // 未命名附件:使用 UUID + const uuid = 'uuid-' + Math.random().toString(36).substring(2, 15); + const ext = att.mimeType?.split('/')[1] || 'bin'; + filename = `${uuid}.${ext}`; + r2Filename = filename; + console.log('✅ 类型: 未命名附件'); + console.log(' 生成文件名:', filename); + } + + const r2Key = `attachments/${emailId}/${r2Filename}`; + const contentSize = att.content instanceof Uint8Array ? att.content.length : + att.content instanceof ArrayBuffer ? att.content.byteLength : + att.content.length; + + console.log(' MIME 类型:', att.mimeType); + console.log(' 文件大小:', (contentSize / 1024).toFixed(2), 'KB'); + console.log(' R2 存储路径:', r2Key); + + // 记录附件信息 + attachmentRecords.push({ + id: 'uuid-' + Math.random().toString(36).substring(2, 15), + filename: filename, + contentType: att.mimeType || 'application/octet-stream', + sizeBytes: contentSize, + r2Key: r2Key, + contentId: contentId + }); + } + + console.log('─────────────────────────────────────────\n'); + + // 显示数据库记录示例 + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('💾 数据库记录(attachments 表)'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + attachmentRecords.forEach((record, i) => { + console.log(`附件 ${i + 1}:`); + console.log(' INSERT INTO attachments ('); + console.log(' id, ', `"${record.id}"`); + console.log(' email_id, ', `"${emailId}"`); + console.log(' filename, ', `"${record.filename}"`); + console.log(' content_type,', `"${record.contentType}"`); + console.log(' size_bytes, ', record.sizeBytes); + console.log(' r2_key, ', `"${record.r2Key}"`); + console.log(' content_id ', record.contentId ? `"${record.contentId}"` : 'NULL', '✅'); + console.log(' );\n'); + }); + + // 验证唯一性 + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('🔍 验证文件名唯一性'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + const filenames = attachmentRecords.map(r => r.filename); + const uniqueFilenames = new Set(filenames); + + console.log('附件总数:', filenames.length); + console.log('唯一文件名数:', uniqueFilenames.size); + + if (filenames.length === uniqueFilenames.size) { + console.log('✅ 所有文件名都是唯一的!'); + } else { + console.log('❌ 发现重复的文件名!'); + const duplicates = filenames.filter((name, i) => filenames.indexOf(name) !== i); + console.log('重复的文件名:', duplicates); + } + + // 验证 Content-ID 的唯一性 + const contentIds = attachmentRecords + .filter(r => r.contentId) + .map(r => r.contentId); + const uniqueContentIds = new Set(contentIds); + + console.log('\n内嵌图片数:', contentIds.length); + console.log('唯一 Content-ID 数:', uniqueContentIds.size); + + if (contentIds.length === uniqueContentIds.size) { + console.log('✅ 所有 Content-ID 都是唯一的!'); + console.log('✅ 使用 Content-ID 作为文件名是安全的!'); + } else { + console.log('❌ 发现重复的 Content-ID!'); + } + + // 显示 R2 目录结构 + console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('📁 R2 目录结构'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + console.log('R2 存储结构:'); + console.log('attachments/'); + console.log(`└── ${emailId}/`); + attachmentRecords.forEach((record, i) => { + const isLast = i === attachmentRecords.length - 1; + const prefix = isLast ? ' └── ' : ' ├── '; + const label = record.contentId ? '(内嵌图片)' : '(普通附件)'; + // 从 r2_key 中提取实际的 R2 文件名 + const r2Filename = record.r2Key.split('/').pop(); + console.log(`${prefix}${r2Filename} ${label}`); + if (record.contentId && record.filename !== r2Filename) { + console.log(`${isLast ? ' ' : ' │ '}→ 数据库记录原文件名: ${record.filename}`); + } + }); + + console.log('\n✅ 测试完成!新的命名逻辑工作正常!'); + } +} + +testAttachmentNaming().catch(err => { + console.error('❌ 测试失败:', err); + process.exit(1); +}); + diff --git a/test-cache-headers.sh b/test-cache-headers.sh new file mode 100644 index 0000000..884838f --- /dev/null +++ b/test-cache-headers.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +# 测试缓存头脚本 +# 用法: ./test-cache-headers.sh + +TOKEN=$1 +EMAIL_ID=$2 +ATTACHMENT_ID=$3 + +if [ -z "$TOKEN" ] || [ -z "$EMAIL_ID" ] || [ -z "$ATTACHMENT_ID" ]; then + echo "用法: ./test-cache-headers.sh " + echo "" + echo "示例:" + echo "./test-cache-headers.sh 'eyJ...' 'f167d374-5085-43dd-8edc-e0e521b9f9e2' '29e7b2e5-410f-420e-a1f5-1d078690cfae'" + exit 1 +fi + +URL="http://localhost:8787/api/emails/${EMAIL_ID}/attachments/${ATTACHMENT_ID}" + +echo "======================================" +echo "测试缓存头" +echo "======================================" +echo "" +echo "URL: $URL" +echo "" + +echo "1️⃣ 首次请求(应该返回 200 + 缓存头):" +echo "--------------------------------------" +RESPONSE=$(curl -i -s -H "Authorization: Bearer $TOKEN" "$URL") +echo "$RESPONSE" | head -20 +echo "" + +# 提取 ETag +ETAG=$(echo "$RESPONSE" | grep -i "etag:" | cut -d' ' -f2- | tr -d '\r') + +if [ -n "$ETAG" ]; then + echo "✅ 找到 ETag: $ETAG" + echo "" + + echo "2️⃣ 第二次请求(带 If-None-Match,应该返回 304):" + echo "--------------------------------------" + curl -i -s -H "Authorization: Bearer $TOKEN" -H "If-None-Match: $ETAG" "$URL" | head -20 + echo "" +else + echo "❌ 没有找到 ETag!" + echo "" + echo "完整响应头:" + echo "$RESPONSE" | sed '/^$/q' + echo "" +fi + +echo "======================================" +echo "检查项:" +echo "======================================" +echo "" +echo "✅ 应该看到的响应头:" +echo " - ETag: \"...\"" +echo " - Cache-Control: public, max-age=31536000, immutable" +echo " - Last-Modified: ..." +echo " - Vary: Authorization" +echo "" +echo "✅ 第二次请求应该返回:" +echo " - HTTP/1.1 304 Not Modified" +echo "" + diff --git a/test-cache.html b/test-cache.html new file mode 100644 index 0000000..89a7f7e --- /dev/null +++ b/test-cache.html @@ -0,0 +1,359 @@ + + + + + + 缓存测试 + + + +
+

🚀 缓存功能测试工具

+

测试附件下载的三层缓存机制

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + + +
+
+ + + + + diff --git a/test-email-parsing.js b/test-email-parsing.js new file mode 100644 index 0000000..ada3b6d --- /dev/null +++ b/test-email-parsing.js @@ -0,0 +1,283 @@ +/** + * 测试邮件解析逻辑 + * 用于测试 parsedEmail.text 是否包含 HTML 标签 + */ + +import { readFileSync } from 'fs'; +import PostalMime from 'postal-mime'; +import { convert } from 'html-to-text'; + +// 检查字符串是否包含 HTML 标签 +function containsHtmlTags(text) { + if (!text) return false; + return /<[^>]+>/.test(text); +} + +// 从 HTML 中提取纯文本(使用 html-to-text) +async function extractTextFromHtmlAsync(html) { + if (!html) { + return ''; + } + + try { + const text = convert(html, { + preserveNewlines: true, + longWordSplit: { + wrapCharacters: [], + forceWrapOnLimit: false + }, + selectors: [ + { selector: 'script', format: 'skip' }, + { selector: 'style', format: 'skip' }, + { selector: 'a', options: { ignoreHref: false } }, + { selector: 'img', format: 'image', options: { baseUrl: '' } }, + { selector: 'ul', format: 'unorderedList' }, + { selector: 'ol', format: 'orderedList' }, + { selector: 'table', format: 'dataTable' } + ], + wordwrap: 0 + }); + + return text.trim(); + } catch (error) { + console.warn('html-to-text 转换失败,使用备用方法:', error); + return extractTextFromHtmlFallback(html); + } +} + +// 从 HTML 中提取纯文本(备用方法) +function extractTextFromHtmlFallback(html) { + if (!html) { + return ''; + } + + // 移除 script 和 style 标签及其内容 + let text = html.replace(/]*>[\s\S]*?<\/script>/gi, ''); + text = text.replace(/]*>[\s\S]*?<\/style>/gi, ''); + + // 将常见的 HTML 实体转换为换行或空格 + text = text.replace(//gi, '\n'); + text = text.replace(/<\/p>/gi, '\n'); + text = text.replace(/<\/div>/gi, '\n'); + text = text.replace(/<\/li>/gi, '\n'); + text = text.replace(/]*>/gi, '• '); + + // 移除所有 HTML 标签 + text = text.replace(/<[^>]+>/g, ''); + + // 解码 HTML 实体 + text = text.replace(/ /g, ' '); + text = text.replace(/&/g, '&'); + text = text.replace(/</g, '<'); + text = text.replace(/>/g, '>'); + text = text.replace(/"/g, '"'); + text = text.replace(/'/g, "'"); + text = text.replace(/'/g, "'"); + + // 处理其他常见的 HTML 实体 + text = text.replace(/&#(\d+);/g, (match, dec) => { + return String.fromCharCode(parseInt(dec, 10)); + }); + text = text.replace(/&#x([0-9A-Fa-f]+);/g, (match, hex) => { + return String.fromCharCode(parseInt(hex, 16)); + }); + + // 清理多余的空白字符 + text = text.replace(/\n\s*\n\s*\n/g, '\n\n'); + text = text.replace(/[ \t]+/g, ' '); + text = text.replace(/^\s+|\s+$/gm, ''); + + return text.trim(); +} + +async function testEmailParsing() { + try { + // 读取 .eml 文件 + const emlPath = './email_1dfba050-c2cf-440a-8987-8de6c6a07fd4.eml'; + console.log('📧 读取邮件文件:', emlPath); + const rawEmail = readFileSync(emlPath, 'utf-8'); + console.log('✅ 邮件文件读取成功,大小:', rawEmail.length, '字符'); + + // 使用 postal-mime 解析邮件 + console.log('\n🔍 开始解析邮件...'); + const encoder = new TextEncoder(); + const rawEmailBytes = encoder.encode(rawEmail); + const parser = new PostalMime(); + const parsedEmail = await parser.parse(rawEmailBytes); + + console.log('✅ 邮件解析成功'); + console.log('\n📊 解析结果:'); + console.log(' - Subject:', parsedEmail.subject); + console.log(' - From:', parsedEmail.from); + console.log(' - To:', parsedEmail.to); + console.log(' - Date:', parsedEmail.date); + console.log(' - Has text:', !!parsedEmail.text); + console.log(' - Has html:', !!parsedEmail.html); + console.log(' - Attachments count:', parsedEmail.attachments?.length || 0); + + // 检查 parsedEmail.text + if (parsedEmail.text) { + console.log('\n📝 parsedEmail.text 分析:'); + console.log(' - 长度:', parsedEmail.text.length); + console.log(' - 前200字符:', parsedEmail.text.substring(0, 200)); + console.log(' - 包含 HTML 标签:', containsHtmlTags(parsedEmail.text)); + + if (containsHtmlTags(parsedEmail.text)) { + console.log(' ⚠️ 警告: parsedEmail.text 包含 HTML 标签!'); + console.log(' 🔧 尝试提取纯文本...'); + + try { + const extractedText = await extractTextFromHtmlAsync(parsedEmail.text); + if (extractedText && extractedText.trim()) { + console.log(' ✅ 使用 extractTextFromHtml 提取成功'); + console.log(' - 提取后长度:', extractedText.length); + console.log(' - 前200字符:', extractedText.substring(0, 200)); + } else { + console.log(' ⚠️ extractTextFromHtml 返回空,使用备用方法'); + const fallbackText = extractTextFromHtmlFallback(parsedEmail.text); + if (fallbackText && fallbackText.trim()) { + console.log(' ✅ 备用方法提取成功'); + console.log(' - 提取后长度:', fallbackText.length); + console.log(' - 前200字符:', fallbackText.substring(0, 200)); + } else { + console.log(' ❌ 所有提取方法都失败'); + } + } + } catch (error) { + console.log(' ❌ extractTextFromHtml 出错:', error.message); + const fallbackText = extractTextFromHtmlFallback(parsedEmail.text); + if (fallbackText && fallbackText.trim()) { + console.log(' ✅ 备用方法提取成功'); + console.log(' - 提取后长度:', fallbackText.length); + console.log(' - 前200字符:', fallbackText.substring(0, 200)); + } + } + } else { + console.log(' ✅ parsedEmail.text 是纯文本,可以直接使用'); + } + } else { + console.log('\n⚠️ parsedEmail.text 不存在'); + } + + // 检查 parsedEmail.html + if (parsedEmail.html) { + console.log('\n🌐 parsedEmail.html 分析:'); + console.log(' - 长度:', parsedEmail.html.length); + console.log(' - 前200字符:', parsedEmail.html.substring(0, 200)); + console.log(' - 包含 HTML 标签:', containsHtmlTags(parsedEmail.html)); + } else { + console.log('\n⚠️ parsedEmail.html 不存在'); + } + + // 模拟邮件处理逻辑 + console.log('\n🔄 模拟邮件处理逻辑:'); + let content = ''; + let contentType = 'text'; + + if (parsedEmail.text) { + if (containsHtmlTags(parsedEmail.text)) { + console.log(' ⚠️ parsedEmail.text 包含 HTML,需要提取纯文本'); + try { + const extractedText = await extractTextFromHtmlAsync(parsedEmail.text); + if (extractedText && extractedText.trim()) { + content = extractedText; + contentType = 'text'; + console.log(' ✅ 提取成功,长度:', content.length); + } else { + const fallbackText = extractTextFromHtmlFallback(parsedEmail.text); + if (fallbackText && fallbackText.trim()) { + content = fallbackText; + contentType = 'text'; + console.log(' ✅ 使用备用方法提取成功,长度:', content.length); + } else { + content = '[无法提取邮件内容预览]'; + console.log(' ❌ 所有提取方法都失败'); + } + } + } catch (error) { + const fallbackText = extractTextFromHtmlFallback(parsedEmail.text); + if (fallbackText && fallbackText.trim()) { + content = fallbackText; + contentType = 'text'; + console.log(' ✅ 使用备用方法提取成功,长度:', content.length); + } else { + content = '[无法提取邮件内容预览]'; + console.log(' ❌ 所有提取方法都失败'); + } + } + } else { + content = parsedEmail.text; + contentType = 'text'; + console.log(' ✅ 使用纯文本,长度:', content.length); + } + } else if (parsedEmail.html) { + console.log(' ⚠️ 只有 HTML,需要提取纯文本'); + try { + const extractedText = await extractTextFromHtmlAsync(parsedEmail.html); + if (extractedText && extractedText.trim()) { + content = extractedText; + contentType = 'text'; + console.log(' ✅ 提取成功,长度:', content.length); + } else { + const fallbackText = extractTextFromHtmlFallback(parsedEmail.html); + if (fallbackText && fallbackText.trim()) { + content = fallbackText; + contentType = 'text'; + console.log(' ✅ 使用备用方法提取成功,长度:', content.length); + } else { + content = '[无法提取邮件内容预览]'; + console.log(' ❌ 所有提取方法都失败'); + } + } + } catch (error) { + const fallbackText = extractTextFromHtmlFallback(parsedEmail.html); + if (fallbackText && fallbackText.trim()) { + content = fallbackText; + contentType = 'text'; + console.log(' ✅ 使用备用方法提取成功,长度:', content.length); + } else { + content = '[无法提取邮件内容预览]'; + console.log(' ❌ 所有提取方法都失败'); + } + } + } + + // 截取预览 + const CONTENT_PREVIEW_LENGTH = 1000; + let contentPreview = content; + if (content && content.length > CONTENT_PREVIEW_LENGTH) { + contentPreview = content.substring(0, CONTENT_PREVIEW_LENGTH) + '...'; + } + + console.log('\n📋 最终结果:'); + console.log(' - Content Type:', contentType); + console.log(' - Content Length:', content.length); + console.log(' - Preview Length:', contentPreview.length); + console.log(' - Preview (前500字符):', contentPreview.substring(0, 500)); + console.log(' - 包含 HTML 标签:', containsHtmlTags(contentPreview)); + + // 最终验证 + if (containsHtmlTags(contentPreview)) { + console.log('\n❌ 错误: 最终内容仍然包含 HTML 标签!'); + console.log(' 🔧 尝试清理...'); + const cleanedContent = extractTextFromHtmlFallback(contentPreview); + if (cleanedContent && cleanedContent.trim()) { + console.log(' ✅ 清理成功'); + console.log(' - 清理后长度:', cleanedContent.length); + console.log(' - 清理后前200字符:', cleanedContent.substring(0, 200)); + } else { + console.log(' ❌ 清理失败'); + } + } else { + console.log('\n✅ 最终内容不包含 HTML 标签,可以保存到数据库'); + } + + } catch (error) { + console.error('❌ 测试失败:', error); + console.error(error.stack); + } +} + +// 运行测试 +testEmailParsing(); + diff --git a/test-email-strip.js b/test-email-strip.js new file mode 100644 index 0000000..9c78d83 --- /dev/null +++ b/test-email-strip.js @@ -0,0 +1,222 @@ +#!/usr/bin/env node + +/** + * 测试邮件精简功能 + * 验证 buildStrippedEmlFile 是否正确保留邮件头和正文 + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +/** + * 构建去除附件的精简 .eml 文件 + * 保留完整的邮件头和正文内容,只移除附件数据 + */ +function buildStrippedEmlFile(rawEmail, parsedEmail) { + const lines = rawEmail.split(/\r?\n/); + const headers = []; + let inHeaders = true; + let skipContentType = false; + + // 第一步:提取邮件头,但需要更新 Content-Type + for (const line of lines) { + if (inHeaders) { + if (line.trim() === '') { + // 遇到空行,邮件头结束 + inHeaders = false; + break; + } + + // 检查是否是 Content-Type 行 + const lowerLine = line.toLowerCase(); + if (lowerLine.startsWith('content-type:')) { + skipContentType = true; + // 不添加这一行,稍后会添加新的 Content-Type + continue; + } + + // 跳过 Content-Type 的续行(以空格或制表符开头) + if (skipContentType && (line.startsWith(' ') || line.startsWith('\t'))) { + continue; + } + + skipContentType = false; + headers.push(line); + } + } + + // 第二步:确定内容类型和正文 + let body = ''; + let contentType = 'text/plain; charset=utf-8'; + + if (parsedEmail.html && parsedEmail.html.trim()) { + body = parsedEmail.html; + contentType = 'text/html; charset=utf-8'; + } else if (parsedEmail.text && parsedEmail.text.trim()) { + body = parsedEmail.text; + contentType = 'text/plain; charset=utf-8'; + } + + // 第三步:添加新的 Content-Type 头(单一类型,不再是 multipart) + headers.push(`Content-Type: ${contentType}`); + headers.push('Content-Transfer-Encoding: 8bit'); + + // 第四步:组装完整的 .eml 文件 + // RFC 822 格式:头部 + 空行 + 正文 + return headers.join('\r\n') + '\r\n\r\n' + body; +} + +async function testEmailStripping() { + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('📧 邮件精简测试'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + // 读取原始邮件 + const emlPath = path.join(__dirname, 'email_1dfba050-c2cf-440a-8987-8de6c6a07fd4.eml'); + console.log('📂 读取原始邮件:', emlPath); + const rawEmail = fs.readFileSync(emlPath, 'utf-8'); + const originalSize = rawEmail.length; + console.log(' 原始大小:', (originalSize / 1024).toFixed(2), 'KB\n'); + + // 使用 postal-mime 解析 + console.log('🔍 使用 postal-mime 解析邮件...'); + const PostalMime = (await import('postal-mime')).default; + const parser = new PostalMime(); + const parsed = await parser.parse(rawEmail); + + console.log(' 主题:', parsed.subject); + console.log(' 发件人:', parsed.from.address); + console.log(' 收件人:', parsed.to[0].address); + console.log(' HTML 长度:', parsed.html ? parsed.html.length : 0, '字符'); + console.log(' 文本长度:', parsed.text ? parsed.text.length : 0, '字符'); + console.log(' 附件数量:', parsed.attachments ? parsed.attachments.length : 0, '\n'); + + // 列出附件信息 + if (parsed.attachments && parsed.attachments.length > 0) { + console.log('📎 附件列表:'); + parsed.attachments.forEach((att, i) => { + const size = att.content instanceof Uint8Array ? att.content.length : + att.content instanceof ArrayBuffer ? att.content.byteLength : + att.content.length; + console.log(` ${i + 1}. ${att.filename || '(无文件名)'}`); + console.log(` 类型: ${att.mimeType}`); + console.log(` 大小: ${(size / 1024).toFixed(2)} KB`); + console.log(` Content-ID: ${att.contentId || '(无)'}`); + }); + console.log(); + } + + // 生成精简版 .eml + console.log('✂️ 生成精简版 .eml...'); + const strippedEmail = buildStrippedEmlFile(rawEmail, parsed); + const strippedSize = strippedEmail.length; + console.log(' 精简大小:', (strippedSize / 1024).toFixed(2), 'KB'); + console.log(' 节省空间:', ((1 - strippedSize / originalSize) * 100).toFixed(2), '%\n'); + + // 保存精简版 + const outputPath = path.join(__dirname, 'email_stripped.eml'); + fs.writeFileSync(outputPath, strippedEmail); + console.log('💾 精简版已保存:', outputPath, '\n'); + + // 比较邮件头 + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('📋 邮件头对比'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + const originalHeaders = rawEmail.split(/\r?\n\r?\n/)[0].split(/\r?\n/); + const strippedHeaders = strippedEmail.split(/\r?\n\r?\n/)[0].split(/\r?\n/); + + console.log('原始邮件头数量:', originalHeaders.length); + console.log('精简邮件头数量:', strippedHeaders.length); + console.log('邮件头是否完全相同:', originalHeaders.join('\n') === strippedHeaders.join('\n') ? '✅ 是' : '❌ 否'); + console.log(); + + // 检查关键邮件头 + const keyHeaders = ['From:', 'To:', 'Subject:', 'Date:', 'Message-Id:', 'Content-Type:', 'Received:', 'DKIM-Signature:']; + console.log('关键邮件头检查:'); + keyHeaders.forEach(key => { + const originalHas = originalHeaders.some(h => h.startsWith(key)); + const strippedHas = strippedHeaders.some(h => h.startsWith(key)); + const status = originalHas === strippedHas ? '✅' : '❌'; + console.log(` ${status} ${key.padEnd(20)} 原始: ${originalHas ? '有' : '无'} 精简: ${strippedHas ? '有' : '无'}`); + }); + console.log(); + + // 比较正文内容 + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('📄 正文内容对比'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + const strippedBody = strippedEmail.split(/\r?\n\r?\n/).slice(1).join('\n\n'); + console.log('精简版正文长度:', strippedBody.length, '字符'); + console.log('解析后 HTML 长度:', parsed.html ? parsed.html.length : 0, '字符'); + console.log('正文是否匹配:', strippedBody === parsed.html ? '✅ 完全匹配' : '⚠️ 可能有差异'); + console.log(); + + // 检查是否包含附件数据 + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('🔍 附件数据检查'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + const hasBase64InOriginal = /Content-Transfer-Encoding: base64/.test(rawEmail); + const hasBase64InStripped = /Content-Transfer-Encoding: base64/.test(strippedEmail); + + console.log('原始邮件包含 base64 编码:', hasBase64InOriginal ? '是' : '否'); + console.log('精简邮件包含 base64 编码:', hasBase64InStripped ? '是' : '否'); + console.log(); + + // 检查图片附件是否被移除 + const imageBoundaryPattern = /Content-Type: image\//gi; + const originalImageCount = (rawEmail.match(imageBoundaryPattern) || []).length; + const strippedImageCount = (strippedEmail.match(imageBoundaryPattern) || []).length; + + console.log('原始邮件图片附件数:', originalImageCount); + console.log('精简邮件图片附件数:', strippedImageCount); + console.log('附件是否已移除:', strippedImageCount === 0 ? '✅ 是(附件已分离)' : '⚠️ 否(附件仍在正文中)'); + console.log(); + + // 最终总结 + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('✅ 测试总结'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + const isSuccess = + originalHeaders.join('\n') === strippedHeaders.join('\n') && // 邮件头完全相同 + strippedBody.length > 0 && // 有正文内容 + strippedSize < originalSize; // 文件变小了 + + if (isSuccess) { + console.log('✅ 测试通过!'); + console.log(' - 所有邮件头完整保留'); + console.log(' - 正文内容正确提取'); + console.log(' - 文件大小显著减少'); + console.log(' - 附件数据已分离'); + } else { + console.log('⚠️ 测试发现问题:'); + if (originalHeaders.join('\n') !== strippedHeaders.join('\n')) { + console.log(' - 邮件头可能有差异'); + } + if (strippedBody.length === 0) { + console.log(' - 正文内容未正确提取'); + } + if (strippedSize >= originalSize) { + console.log(' - 文件大小未减少'); + } + } + + console.log('\n📁 生成的文件:'); + console.log(' 原始邮件:', emlPath); + console.log(' 精简邮件:', outputPath); + console.log('\n💡 提示: 可以用邮件客户端打开两个文件对比查看\n'); +} + +// 运行测试 +testEmailStripping().catch(err => { + console.error('❌ 测试失败:', err); + process.exit(1); +}); + diff --git a/test-process-email.js b/test-process-email.js new file mode 100644 index 0000000..5ec1e57 --- /dev/null +++ b/test-process-email.js @@ -0,0 +1,119 @@ +#!/usr/bin/env node + +/** + * 邮件处理测试脚本 + * 用法: node test-process-email.js [filename.eml] [to@email.com] + * + * 例如: + * node test-process-email.js email_1dfba050-c2cf-440a-8987-8de6c6a07fd4.eml + * node test-process-email.js email_1dfba050-c2cf-440a-8987-8de6c6a07fd4.eml k@doubi.tech + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +async function processEmail() { + const filename = process.argv[2] || 'email_1dfba050-c2cf-440a-8987-8de6c6a07fd4.eml'; + const toAddress = process.argv[3] || 'k@doubi.tech'; + const workerUrl = process.env.WORKER_URL || 'http://localhost:8787'; + + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('📧 邮件处理测试'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + // 读取 .eml 文件 + const emlPath = path.join(__dirname, filename); + + if (!fs.existsSync(emlPath)) { + console.error('❌ 文件不存在:', emlPath); + console.log('\n💡 可用的 .eml 文件:'); + const files = fs.readdirSync(__dirname).filter(f => f.endsWith('.eml')); + files.forEach(f => console.log(' -', f)); + process.exit(1); + } + + console.log('📂 读取文件:', filename); + const emlContent = fs.readFileSync(emlPath, 'utf-8'); + console.log(' 文件大小:', (emlContent.length / 1024).toFixed(2), 'KB'); + + // 从邮件中提取信息 + const fromMatch = emlContent.match(/^From:\s*(.+)$/m); + const subjectMatch = emlContent.match(/^Subject:\s*(.+)$/m); + + const fromAddress = fromMatch ? fromMatch[1].trim() : 'test@example.com'; + const subject = subjectMatch ? subjectMatch[1].trim() : '(无主题)'; + + console.log(' 发件人:', fromAddress); + console.log(' 收件人:', toAddress); + console.log(' 主题:', subject); + console.log(); + + // 发送到 Worker + console.log('🚀 发送到 Worker:', workerUrl + '/api/test/process-email'); + + try { + const response = await fetch(workerUrl + '/api/test/process-email', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + emlContent: emlContent, + from: fromAddress, + to: toAddress + }) + }); + + const result = await response.json(); + + console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('📊 处理结果'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + if (result.success) { + console.log('✅ 邮件处理成功!'); + console.log('\n详细信息:'); + console.log(' 发件人:', result.details?.from); + console.log(' 收件人:', result.details?.to); + console.log(' 大小:', (result.details?.size / 1024).toFixed(2), 'KB'); + + console.log('\n💡 提示:'); + console.log(' - 邮件已保存到数据库'); + console.log(' - 附件已保存到 R2'); + console.log(' - 精简版 .eml 已保存到 R2'); + console.log(' - 可以在前端查看邮件详情'); + + console.log('\n🔗 查看邮件:'); + console.log(' 前端: http://localhost:5173'); + console.log(' 或查询数据库: npm run db -- "SELECT * FROM emails ORDER BY created_at DESC LIMIT 1"'); + } else { + console.log('❌ 邮件处理失败!'); + console.log('\n错误信息:', result.error); + if (result.stack) { + console.log('\n堆栈跟踪:'); + console.log(result.stack); + } + } + + console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + } catch (error) { + console.error('\n❌ 请求失败:', error.message); + console.log('\n💡 请确保 Worker 正在运行:'); + console.log(' cd worker && npx wrangler dev'); + console.log('\n 或设置 WORKER_URL 环境变量:'); + console.log(' WORKER_URL=http://localhost:8788 node test-process-email.js'); + process.exit(1); + } +} + +// 运行测试 +processEmail().catch(err => { + console.error('❌ 测试失败:', err); + process.exit(1); +}); + diff --git a/cloudflare_temp_email/frontend/.gitignore b/vue-legacy/.gitignore similarity index 84% rename from cloudflare_temp_email/frontend/.gitignore rename to vue-legacy/.gitignore index db55065..8ee54e8 100644 --- a/cloudflare_temp_email/frontend/.gitignore +++ b/vue-legacy/.gitignore @@ -27,8 +27,4 @@ coverage *.sln *.sw? -.env.* -!.env.example -!.env.pages -*-dist/ -components.d.ts +*.tsbuildinfo diff --git a/vue-legacy/README.md b/vue-legacy/README.md new file mode 100644 index 0000000..57f039d --- /dev/null +++ b/vue-legacy/README.md @@ -0,0 +1,114 @@ +# CEM 邮箱管理系统 - 前端 + +基于 Vue 3 + TypeScript + Element Plus + Pinia + VueUse 的现代化邮箱管理系统前端。 + +## 技术栈 + +- **Vue 3** - 渐进式 JavaScript 框架 +- **TypeScript** - 类型安全的 JavaScript +- **Element Plus** - Vue 3 组件库 +- **Pinia** - Vue 状态管理 +- **VueUse** - Vue 组合式 API 工具集 +- **Axios** - HTTP 客户端 +- **Day.js** - 轻量级日期处理库 +- **Vite** - 快速构建工具 + +## 功能特性 + +- ✅ 用户登录/注册 +- ✅ 流式页面加载 +- ✅ 响应式设计 +- ✅ 类型安全 +- ✅ 现代化 UI 设计 +- ✅ 状态持久化 + +## 开发环境 + +### 安装依赖 + +```bash +npm install +``` + +### 启动开发服务器 + +```bash +npm run dev +``` + +访问 http://localhost:3000 + +### 构建生产版本 + +```bash +npm run build +``` + +### 类型检查 + +```bash +npm run type-check +``` + +## 项目结构 + +``` +vue/ +├── src/ +│ ├── api/ # API 服务 +│ ├── components/ # 组件 +│ │ └── UI/ # UI 组件 +│ ├── router/ # 路由配置 +│ ├── stores/ # Pinia 状态管理 +│ ├── types/ # TypeScript 类型定义 +│ ├── views/ # 页面组件 +│ ├── App.vue # 根组件 +│ └── main.js # 入口文件 +├── index.html # HTML 模板 +├── package.json # 项目配置 +├── tsconfig.json # TypeScript 配置 +├── tsconfig.node.json # Node.js TypeScript 配置 +└── vite.config.ts # Vite 配置 +``` + +## 主要组件 + +### 登录页面 (LoginView.vue) +- 支持用户名/密码登录 +- 支持用户注册(可配置) +- 响应式设计 +- 错误处理 +- 加载状态 + +### 主界面 (MainLayoutView.vue) +- 用户信息显示 +- 退出登录功能 +- 现代化布局 + +### 认证状态管理 (auth.ts) +- 基于 Pinia 的状态管理 +- 使用 VueUse 进行持久化存储 +- 完整的登录/注册/登出流程 + +## API 配置 + +API 服务配置在 `src/api/index.ts` 中,支持: +- Cookie 认证 +- 请求/响应拦截 +- 错误处理 +- 自动重定向 + +## 开发说明 + +1. 所有组件都使用 TypeScript 编写 +2. 使用 Element Plus 组件库 +3. 状态管理使用 Pinia +4. 路由使用 Vue Router +5. 样式使用 scoped CSS + +## 浏览器支持 + +- Chrome >= 87 +- Firefox >= 78 +- Safari >= 14 +- Edge >= 88 diff --git a/vue-legacy/auto-imports.d.ts b/vue-legacy/auto-imports.d.ts new file mode 100644 index 0000000..4ea7866 --- /dev/null +++ b/vue-legacy/auto-imports.d.ts @@ -0,0 +1,317 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// noinspection JSUnusedGlobalSymbols +// Generated by unplugin-auto-import +// biome-ignore lint: disable +export {} +declare global { + const EffectScope: typeof import('vue')['EffectScope'] + const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] + const asyncComputed: typeof import('@vueuse/core')['asyncComputed'] + const autoResetRef: typeof import('@vueuse/core')['autoResetRef'] + const computed: typeof import('vue')['computed'] + const computedAsync: typeof import('@vueuse/core')['computedAsync'] + const computedEager: typeof import('@vueuse/core')['computedEager'] + const computedInject: typeof import('@vueuse/core')['computedInject'] + const computedWithControl: typeof import('@vueuse/core')['computedWithControl'] + const controlledComputed: typeof import('@vueuse/core')['controlledComputed'] + const controlledRef: typeof import('@vueuse/core')['controlledRef'] + const createApp: typeof import('vue')['createApp'] + const createEventHook: typeof import('@vueuse/core')['createEventHook'] + const createGlobalState: typeof import('@vueuse/core')['createGlobalState'] + const createInjectionState: typeof import('@vueuse/core')['createInjectionState'] + const createPinia: typeof import('pinia')['createPinia'] + const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn'] + const createRef: typeof import('@vueuse/core')['createRef'] + const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate'] + const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable'] + const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise'] + const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn'] + const customRef: typeof import('vue')['customRef'] + const debouncedRef: typeof import('@vueuse/core')['debouncedRef'] + const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch'] + const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] + const defineComponent: typeof import('vue')['defineComponent'] + const defineStore: typeof import('pinia')['defineStore'] + const eagerComputed: typeof import('@vueuse/core')['eagerComputed'] + const effectScope: typeof import('vue')['effectScope'] + const extendRef: typeof import('@vueuse/core')['extendRef'] + const getActivePinia: typeof import('pinia')['getActivePinia'] + const getCurrentInstance: typeof import('vue')['getCurrentInstance'] + const getCurrentScope: typeof import('vue')['getCurrentScope'] + const getCurrentWatcher: typeof import('vue')['getCurrentWatcher'] + const h: typeof import('vue')['h'] + const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] + const inject: typeof import('vue')['inject'] + const injectLocal: typeof import('@vueuse/core')['injectLocal'] + const isDefined: typeof import('@vueuse/core')['isDefined'] + const isProxy: typeof import('vue')['isProxy'] + const isReactive: typeof import('vue')['isReactive'] + const isReadonly: typeof import('vue')['isReadonly'] + const isRef: typeof import('vue')['isRef'] + const isShallow: typeof import('vue')['isShallow'] + const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable'] + const mapActions: typeof import('pinia')['mapActions'] + const mapGetters: typeof import('pinia')['mapGetters'] + const mapState: typeof import('pinia')['mapState'] + const mapStores: typeof import('pinia')['mapStores'] + const mapWritableState: typeof import('pinia')['mapWritableState'] + const markRaw: typeof import('vue')['markRaw'] + const nextTick: typeof import('vue')['nextTick'] + const onActivated: typeof import('vue')['onActivated'] + const onBeforeMount: typeof import('vue')['onBeforeMount'] + const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] + const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] + const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] + const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] + const onClickOutside: typeof import('@vueuse/core')['onClickOutside'] + const onDeactivated: typeof import('vue')['onDeactivated'] + const onElementRemoval: typeof import('@vueuse/core')['onElementRemoval'] + const onErrorCaptured: typeof import('vue')['onErrorCaptured'] + const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke'] + const onLongPress: typeof import('@vueuse/core')['onLongPress'] + const onMounted: typeof import('vue')['onMounted'] + const onRenderTracked: typeof import('vue')['onRenderTracked'] + const onRenderTriggered: typeof import('vue')['onRenderTriggered'] + const onScopeDispose: typeof import('vue')['onScopeDispose'] + const onServerPrefetch: typeof import('vue')['onServerPrefetch'] + const onStartTyping: typeof import('@vueuse/core')['onStartTyping'] + const onUnmounted: typeof import('vue')['onUnmounted'] + const onUpdated: typeof import('vue')['onUpdated'] + const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] + const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] + const provide: typeof import('vue')['provide'] + const provideLocal: typeof import('@vueuse/core')['provideLocal'] + const reactify: typeof import('@vueuse/core')['reactify'] + const reactifyObject: typeof import('@vueuse/core')['reactifyObject'] + const reactive: typeof import('vue')['reactive'] + const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed'] + const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit'] + const reactivePick: typeof import('@vueuse/core')['reactivePick'] + const readonly: typeof import('vue')['readonly'] + const ref: typeof import('vue')['ref'] + const refAutoReset: typeof import('@vueuse/core')['refAutoReset'] + const refDebounced: typeof import('@vueuse/core')['refDebounced'] + const refDefault: typeof import('@vueuse/core')['refDefault'] + const refThrottled: typeof import('@vueuse/core')['refThrottled'] + const refWithControl: typeof import('@vueuse/core')['refWithControl'] + const resolveComponent: typeof import('vue')['resolveComponent'] + const resolveRef: typeof import('@vueuse/core')['resolveRef'] + const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] + const setActivePinia: typeof import('pinia')['setActivePinia'] + const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] + const shallowReactive: typeof import('vue')['shallowReactive'] + const shallowReadonly: typeof import('vue')['shallowReadonly'] + const shallowRef: typeof import('vue')['shallowRef'] + const storeToRefs: typeof import('pinia')['storeToRefs'] + const syncRef: typeof import('@vueuse/core')['syncRef'] + const syncRefs: typeof import('@vueuse/core')['syncRefs'] + const templateRef: typeof import('@vueuse/core')['templateRef'] + const throttledRef: typeof import('@vueuse/core')['throttledRef'] + const throttledWatch: typeof import('@vueuse/core')['throttledWatch'] + const toRaw: typeof import('vue')['toRaw'] + const toReactive: typeof import('@vueuse/core')['toReactive'] + const toRef: typeof import('vue')['toRef'] + const toRefs: typeof import('vue')['toRefs'] + const toValue: typeof import('vue')['toValue'] + const triggerRef: typeof import('vue')['triggerRef'] + const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount'] + const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount'] + const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted'] + const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose'] + const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted'] + const unref: typeof import('vue')['unref'] + const unrefElement: typeof import('@vueuse/core')['unrefElement'] + const until: typeof import('@vueuse/core')['until'] + const useActiveElement: typeof import('@vueuse/core')['useActiveElement'] + const useAnimate: typeof import('@vueuse/core')['useAnimate'] + const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference'] + const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery'] + const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter'] + const useArrayFind: typeof import('@vueuse/core')['useArrayFind'] + const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex'] + const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast'] + const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes'] + const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin'] + const useArrayMap: typeof import('@vueuse/core')['useArrayMap'] + const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce'] + const useArraySome: typeof import('@vueuse/core')['useArraySome'] + const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique'] + const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue'] + const useAsyncState: typeof import('@vueuse/core')['useAsyncState'] + const useAttrs: typeof import('vue')['useAttrs'] + const useBase64: typeof import('@vueuse/core')['useBase64'] + const useBattery: typeof import('@vueuse/core')['useBattery'] + const useBluetooth: typeof import('@vueuse/core')['useBluetooth'] + const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints'] + const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel'] + const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation'] + const useCached: typeof import('@vueuse/core')['useCached'] + const useClipboard: typeof import('@vueuse/core')['useClipboard'] + const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems'] + const useCloned: typeof import('@vueuse/core')['useCloned'] + const useColorMode: typeof import('@vueuse/core')['useColorMode'] + const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog'] + const useCountdown: typeof import('@vueuse/core')['useCountdown'] + const useCounter: typeof import('@vueuse/core')['useCounter'] + const useCssModule: typeof import('vue')['useCssModule'] + const useCssVar: typeof import('@vueuse/core')['useCssVar'] + const useCssVars: typeof import('vue')['useCssVars'] + const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement'] + const useCycleList: typeof import('@vueuse/core')['useCycleList'] + const useDark: typeof import('@vueuse/core')['useDark'] + const useDateFormat: typeof import('@vueuse/core')['useDateFormat'] + const useDebounce: typeof import('@vueuse/core')['useDebounce'] + const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn'] + const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory'] + const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion'] + const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation'] + const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio'] + const useDevicesList: typeof import('@vueuse/core')['useDevicesList'] + const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia'] + const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility'] + const useDraggable: typeof import('@vueuse/core')['useDraggable'] + const useDropZone: typeof import('@vueuse/core')['useDropZone'] + const useElementBounding: typeof import('@vueuse/core')['useElementBounding'] + const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint'] + const useElementHover: typeof import('@vueuse/core')['useElementHover'] + const useElementSize: typeof import('@vueuse/core')['useElementSize'] + const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility'] + const useEventBus: typeof import('@vueuse/core')['useEventBus'] + const useEventListener: typeof import('@vueuse/core')['useEventListener'] + const useEventSource: typeof import('@vueuse/core')['useEventSource'] + const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper'] + const useFavicon: typeof import('@vueuse/core')['useFavicon'] + const useFetch: typeof import('@vueuse/core')['useFetch'] + const useFileDialog: typeof import('@vueuse/core')['useFileDialog'] + const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess'] + const useFocus: typeof import('@vueuse/core')['useFocus'] + const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin'] + const useFps: typeof import('@vueuse/core')['useFps'] + const useFullscreen: typeof import('@vueuse/core')['useFullscreen'] + const useGamepad: typeof import('@vueuse/core')['useGamepad'] + const useGeolocation: typeof import('@vueuse/core')['useGeolocation'] + const useId: typeof import('vue')['useId'] + const useIdle: typeof import('@vueuse/core')['useIdle'] + const useImage: typeof import('@vueuse/core')['useImage'] + const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll'] + const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver'] + const useInterval: typeof import('@vueuse/core')['useInterval'] + const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn'] + const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier'] + const useLastChanged: typeof import('@vueuse/core')['useLastChanged'] + const useLink: typeof import('vue-router')['useLink'] + const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage'] + const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys'] + const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory'] + const useMediaControls: typeof import('@vueuse/core')['useMediaControls'] + const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery'] + const useMemoize: typeof import('@vueuse/core')['useMemoize'] + const useMemory: typeof import('@vueuse/core')['useMemory'] + const useModel: typeof import('vue')['useModel'] + const useMounted: typeof import('@vueuse/core')['useMounted'] + const useMouse: typeof import('@vueuse/core')['useMouse'] + const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement'] + const useMousePressed: typeof import('@vueuse/core')['useMousePressed'] + const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver'] + const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage'] + const useNetwork: typeof import('@vueuse/core')['useNetwork'] + const useNow: typeof import('@vueuse/core')['useNow'] + const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl'] + const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination'] + const useOnline: typeof import('@vueuse/core')['useOnline'] + const usePageLeave: typeof import('@vueuse/core')['usePageLeave'] + const useParallax: typeof import('@vueuse/core')['useParallax'] + const useParentElement: typeof import('@vueuse/core')['useParentElement'] + const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver'] + const usePermission: typeof import('@vueuse/core')['usePermission'] + const usePointer: typeof import('@vueuse/core')['usePointer'] + const usePointerLock: typeof import('@vueuse/core')['usePointerLock'] + const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe'] + const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme'] + const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast'] + const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark'] + const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages'] + const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion'] + const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency'] + const usePrevious: typeof import('@vueuse/core')['usePrevious'] + const useRafFn: typeof import('@vueuse/core')['useRafFn'] + const useRefHistory: typeof import('@vueuse/core')['useRefHistory'] + const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver'] + const useRoute: typeof import('vue-router')['useRoute'] + const useRouter: typeof import('vue-router')['useRouter'] + const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth'] + const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation'] + const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea'] + const useScriptTag: typeof import('@vueuse/core')['useScriptTag'] + const useScroll: typeof import('@vueuse/core')['useScroll'] + const useScrollLock: typeof import('@vueuse/core')['useScrollLock'] + const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage'] + const useShare: typeof import('@vueuse/core')['useShare'] + const useSlots: typeof import('vue')['useSlots'] + const useSorted: typeof import('@vueuse/core')['useSorted'] + const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition'] + const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis'] + const useStepper: typeof import('@vueuse/core')['useStepper'] + const useStorage: typeof import('@vueuse/core')['useStorage'] + const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync'] + const useStyleTag: typeof import('@vueuse/core')['useStyleTag'] + const useSupported: typeof import('@vueuse/core')['useSupported'] + const useSwipe: typeof import('@vueuse/core')['useSwipe'] + const useTemplateRef: typeof import('vue')['useTemplateRef'] + const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList'] + const useTextDirection: typeof import('@vueuse/core')['useTextDirection'] + const useTextSelection: typeof import('@vueuse/core')['useTextSelection'] + const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize'] + const useThrottle: typeof import('@vueuse/core')['useThrottle'] + const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn'] + const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory'] + const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo'] + const useTimeAgoIntl: typeof import('@vueuse/core')['useTimeAgoIntl'] + const useTimeout: typeof import('@vueuse/core')['useTimeout'] + const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn'] + const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll'] + const useTimestamp: typeof import('@vueuse/core')['useTimestamp'] + const useTitle: typeof import('@vueuse/core')['useTitle'] + const useToNumber: typeof import('@vueuse/core')['useToNumber'] + const useToString: typeof import('@vueuse/core')['useToString'] + const useToggle: typeof import('@vueuse/core')['useToggle'] + const useTransition: typeof import('@vueuse/core')['useTransition'] + const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams'] + const useUserMedia: typeof import('@vueuse/core')['useUserMedia'] + const useVModel: typeof import('@vueuse/core')['useVModel'] + const useVModels: typeof import('@vueuse/core')['useVModels'] + const useVibrate: typeof import('@vueuse/core')['useVibrate'] + const useVirtualList: typeof import('@vueuse/core')['useVirtualList'] + const useWakeLock: typeof import('@vueuse/core')['useWakeLock'] + const useWebNotification: typeof import('@vueuse/core')['useWebNotification'] + const useWebSocket: typeof import('@vueuse/core')['useWebSocket'] + const useWebWorker: typeof import('@vueuse/core')['useWebWorker'] + const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn'] + const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus'] + const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll'] + const useWindowSize: typeof import('@vueuse/core')['useWindowSize'] + const watch: typeof import('vue')['watch'] + const watchArray: typeof import('@vueuse/core')['watchArray'] + const watchAtMost: typeof import('@vueuse/core')['watchAtMost'] + const watchDebounced: typeof import('@vueuse/core')['watchDebounced'] + const watchDeep: typeof import('@vueuse/core')['watchDeep'] + const watchEffect: typeof import('vue')['watchEffect'] + const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable'] + const watchImmediate: typeof import('@vueuse/core')['watchImmediate'] + const watchOnce: typeof import('@vueuse/core')['watchOnce'] + const watchPausable: typeof import('@vueuse/core')['watchPausable'] + const watchPostEffect: typeof import('vue')['watchPostEffect'] + const watchSyncEffect: typeof import('vue')['watchSyncEffect'] + const watchThrottled: typeof import('@vueuse/core')['watchThrottled'] + const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable'] + const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter'] + const whenever: typeof import('@vueuse/core')['whenever'] +} +// for type re-export +declare global { + // @ts-ignore + export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' + import('vue') +} diff --git a/vue-legacy/components.d.ts b/vue-legacy/components.d.ts new file mode 100644 index 0000000..3f5f905 --- /dev/null +++ b/vue-legacy/components.d.ts @@ -0,0 +1,27 @@ +/* eslint-disable */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +// biome-ignore lint: disable +export {} + +/* prettier-ignore */ +declare module 'vue' { + export interface GlobalComponents { + Button: typeof import('./src/components/common/Button.vue')['default'] + CheckboxField: typeof import('./src/components/common/CheckboxField.vue')['default'] + DebugInfo: typeof import('./src/components/common/DebugInfo.vue')['default'] + DomainListInput: typeof import('./src/components/common/DomainListInput.vue')['default'] + EmailList: typeof import('./src/components/business/EmailList.vue')['default'] + FormField: typeof import('./src/components/common/FormField.vue')['default'] + MailboxList: typeof import('./src/components/business/MailboxList.vue')['default'] + Modal: typeof import('./src/components/common/Modal.vue')['default'] + PageHeader: typeof import('./src/components/common/PageHeader.vue')['default'] + PageStates: typeof import('./src/components/common/PageStates.vue')['default'] + Pagination: typeof import('./src/components/common/Pagination.vue')['default'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + SettingsForm: typeof import('./src/components/business/SettingsForm.vue')['default'] + StatusBadge: typeof import('./src/components/common/StatusBadge.vue')['default'] + } +} diff --git a/vue-legacy/index.html b/vue-legacy/index.html new file mode 100644 index 0000000..f802f09 --- /dev/null +++ b/vue-legacy/index.html @@ -0,0 +1,16 @@ + + + + + + + + CEM 邮箱管理系统 + + + +
+ + + + \ No newline at end of file diff --git a/cloudflare_temp_email/frontend/public/favicon.ico b/vue-legacy/public/favicon.ico similarity index 100% rename from cloudflare_temp_email/frontend/public/favicon.ico rename to vue-legacy/public/favicon.ico diff --git a/vue-legacy/src/App.vue b/vue-legacy/src/App.vue new file mode 100644 index 0000000..906cda8 --- /dev/null +++ b/vue-legacy/src/App.vue @@ -0,0 +1,218 @@ + + + + + \ No newline at end of file diff --git a/vue-legacy/src/components/business/EmailList.vue b/vue-legacy/src/components/business/EmailList.vue new file mode 100644 index 0000000..b0905cb --- /dev/null +++ b/vue-legacy/src/components/business/EmailList.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/vue/src/components/business/MailboxList.vue b/vue-legacy/src/components/business/MailboxList.vue similarity index 100% rename from vue/src/components/business/MailboxList.vue rename to vue-legacy/src/components/business/MailboxList.vue diff --git a/vue/src/components/business/SettingsForm.vue b/vue-legacy/src/components/business/SettingsForm.vue similarity index 100% rename from vue/src/components/business/SettingsForm.vue rename to vue-legacy/src/components/business/SettingsForm.vue diff --git a/vue-legacy/src/components/business/index.ts b/vue-legacy/src/components/business/index.ts new file mode 100644 index 0000000..468e93b --- /dev/null +++ b/vue-legacy/src/components/business/index.ts @@ -0,0 +1,4 @@ +// 业务组件导出 +export { default as EmailList } from './EmailList.vue' +export { default as MailboxList } from './MailboxList.vue' +export { default as SettingsForm } from './SettingsForm.vue' diff --git a/vue-legacy/src/components/common/Button.vue b/vue-legacy/src/components/common/Button.vue new file mode 100644 index 0000000..2eaf714 --- /dev/null +++ b/vue-legacy/src/components/common/Button.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/vue-legacy/src/components/common/CheckboxField.vue b/vue-legacy/src/components/common/CheckboxField.vue new file mode 100644 index 0000000..b8b7979 --- /dev/null +++ b/vue-legacy/src/components/common/CheckboxField.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/vue-legacy/src/components/common/DebugInfo.vue b/vue-legacy/src/components/common/DebugInfo.vue new file mode 100644 index 0000000..57c158c --- /dev/null +++ b/vue-legacy/src/components/common/DebugInfo.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/vue-legacy/src/components/common/DomainListInput.vue b/vue-legacy/src/components/common/DomainListInput.vue new file mode 100644 index 0000000..a4241af --- /dev/null +++ b/vue-legacy/src/components/common/DomainListInput.vue @@ -0,0 +1,263 @@ + + + + + diff --git a/vue-legacy/src/components/common/FormField.vue b/vue-legacy/src/components/common/FormField.vue new file mode 100644 index 0000000..f99fd04 --- /dev/null +++ b/vue-legacy/src/components/common/FormField.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/vue-legacy/src/components/common/Modal.vue b/vue-legacy/src/components/common/Modal.vue new file mode 100644 index 0000000..ba1fc33 --- /dev/null +++ b/vue-legacy/src/components/common/Modal.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/vue-legacy/src/components/common/PageHeader.vue b/vue-legacy/src/components/common/PageHeader.vue new file mode 100644 index 0000000..42b6ffe --- /dev/null +++ b/vue-legacy/src/components/common/PageHeader.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/vue-legacy/src/components/common/PageStates.vue b/vue-legacy/src/components/common/PageStates.vue new file mode 100644 index 0000000..f2414e7 --- /dev/null +++ b/vue-legacy/src/components/common/PageStates.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/vue-legacy/src/components/common/Pagination.vue b/vue-legacy/src/components/common/Pagination.vue new file mode 100644 index 0000000..30d7115 --- /dev/null +++ b/vue-legacy/src/components/common/Pagination.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/vue-legacy/src/components/common/StatusBadge.vue b/vue-legacy/src/components/common/StatusBadge.vue new file mode 100644 index 0000000..da7f31b --- /dev/null +++ b/vue-legacy/src/components/common/StatusBadge.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/vue-legacy/src/components/common/index.ts b/vue-legacy/src/components/common/index.ts new file mode 100644 index 0000000..90ae6e8 --- /dev/null +++ b/vue-legacy/src/components/common/index.ts @@ -0,0 +1,11 @@ +// 通用组件导出 +export { default as PageHeader } from './PageHeader.vue' +export { default as DebugInfo } from './DebugInfo.vue' +export { default as PageStates } from './PageStates.vue' +export { default as Pagination } from './Pagination.vue' +export { default as StatusBadge } from './StatusBadge.vue' +export { default as Button } from './Button.vue' +export { default as Modal } from './Modal.vue' +export { default as FormField } from './FormField.vue' +export { default as CheckboxField } from './CheckboxField.vue' +export { default as DomainListInput } from './DomainListInput.vue' diff --git a/vue-legacy/src/components/index.ts b/vue-legacy/src/components/index.ts new file mode 100644 index 0000000..3f5cc53 --- /dev/null +++ b/vue-legacy/src/components/index.ts @@ -0,0 +1,3 @@ +// 所有组件导出 +export * from './common' +export * from './business' diff --git a/vue-legacy/src/composables/api.ts b/vue-legacy/src/composables/api.ts new file mode 100644 index 0000000..90d1566 --- /dev/null +++ b/vue-legacy/src/composables/api.ts @@ -0,0 +1,675 @@ +import axios from 'axios' + +// 创建 axios 实例 +const api = axios.create({ + baseURL: '/api', + timeout: 10000, + headers: { + 'Content-Type': 'application/json' + }, + withCredentials: true // 支持 cookie 认证 +}) + +// 请求拦截器 +api.interceptors.request.use( + (config) => { + // 后端通过 HttpOnly cookies 自动解析用户信息,无需手动添加 headers + return config + }, + (error) => { + return Promise.reject(error) + } +) + +// 响应拦截器 +api.interceptors.response.use( + (response) => { + return response + }, + (error) => { + if (error.response?.status === 401) { + // 未授权,清理用户信息并重定向到登录页 + localStorage.clear() + // 重定向到登录页 + window.location.href = '/login' + } + return Promise.reject(error) + } +) + +// API 响应类型 +export interface ApiResponse { + success: boolean + data?: T + message?: string + code?: number +} + +// 系统相关 API +export const systemApiService = { + // 获取系统健康状态 + async getSystemHealth(): Promise> { + const response = await api.get('/system/health') + return response.data + }, + + // 获取注册状态 + async getRegistrationStatus(): Promise> { + const response = await api.get('/system/registration-status') + return response.data + }, + + // 获取系统配置 + async getSystemConfig(): Promise> { + const response = await api.get('/system/config') + return response.data + }, + + // 更新系统配置 + async updateSystemConfig(config: any): Promise> { + const response = await api.put('/system/config', config) + return response.data + }, + + // 清除系统缓存 + async clearSystemCache(): Promise> { + const response = await api.post('/system/clear-cache') + return response.data + } +} + +// 用户相关 API +export const userApiService = { + // 获取用户信息 + async getUserProfile(): Promise> { + const response = await api.get('/users/me') + return response.data + }, + + // 更新用户设置 + async updateUserSettings(settings: any): Promise> { + const response = await api.put('/users/me', settings) + return response.data + }, + + + // 获取用户的转发规则 + async getForwardRules(page = 1, limit = 20): Promise> { + const params = new URLSearchParams() + params.append('page', page.toString()) + params.append('limit', limit.toString()) + + const response = await api.get(`/forward-rules?${params}`) + return response.data + }, + + // 创建转发规则 + async createForwardRule(ruleData: any): Promise> { + const response = await api.post('/forward-rules', ruleData) + return response.data + }, + + // 更新转发规则 + async updateForwardRule(id: number, ruleData: any): Promise> { + const response = await api.put(`/forward-rules/${id}`, ruleData) + return response.data + }, + + // 删除转发规则 + async deleteForwardRule(id: number): Promise> { + const response = await api.delete(`/forward-rules/${id}`) + return response.data + }, + + // ===== 管理员功能 ===== + + // 获取用户列表(仅管理员) + async getUserList(page = 1, limit = 20, query?: string): Promise> { + const params = new URLSearchParams() + params.append('page', page.toString()) + params.append('limit', limit.toString()) + if (query) { + params.append('query', query) + } + + const response = await api.get(`/users?${params}`) + return response.data + }, + + // 创建用户(仅管理员) + async createUser(userData: { + username: string + password: string + email: string + user_type?: 'user' | 'admin' + }): Promise> { + const response = await api.post('/users', userData) + return response.data + }, + + // 获取指定用户信息(仅管理员) + async getUserById(id: number): Promise> { + const response = await api.get(`/users/${id}`) + return response.data + }, + + // 删除用户(仅管理员) + async deleteUser(id: number): Promise> { + const response = await api.delete(`/users/${id}`) + return response.data + }, + + // 根据用户ID获取用户名 + async getUsernameById(userId: number): Promise> { + const response = await api.get(`/user-info/username/${userId}`) + return response.data + }, + + // 批量获取用户名 + async getUsernamesByIds(userIds: number[]): Promise> { + const response = await api.post('/user-info/usernames', { user_ids: userIds }) + return response.data + } +} + +// 认证相关 API +export const authApiService = { + // 用户登录 + async login(credentials: { username: string; password: string }): Promise> { + const response = await api.post('/auth/login', credentials) + return response.data + }, + + // 用户登出 + async logout(): Promise> { + const response = await api.post('/auth/logout') + return response.data + }, + + // 用户注册 + async register(userData: { username: string; password: string; email: string }): Promise> { + const response = await api.post('/auth/register', userData) + return response.data + } +} + +// 邮件相关 API +export const emailApiService = { + // 获取邮件列表 - 支持对象参数和位置参数 + async getEmails( + pageOrParams: number | { page?: number; limit?: number; scope?: 'all'; search?: string; status?: string } = 1, + limit = 20, + scope?: 'all', + search?: string, + status?: string + ): Promise> { + let params: { page: number; limit: number; scope?: 'all'; search?: string; status?: string } + + if (typeof pageOrParams === 'object') { + // 对象参数 + params = { + page: pageOrParams.page || 1, + limit: pageOrParams.limit || 20, + ...pageOrParams + } + } else { + // 位置参数 + params = { + page: pageOrParams, + limit, + scope, + search, + status + } + } + + const urlParams = new URLSearchParams() + urlParams.append('page', params.page.toString()) + urlParams.append('limit', params.limit.toString()) + if (params.scope) urlParams.append('scope', params.scope) + if (params.search) urlParams.append('search', params.search) + if (params.status) urlParams.append('status', params.status) + + const response = await api.get(`/emails?${urlParams}`) + return response.data + }, + + // 获取邮件详情 + async getEmail(id: string): Promise> { + const response = await api.get(`/emails/${id}`) + return response.data + }, + + // 删除邮件 + async deleteEmail(id: string): Promise> { + const response = await api.delete(`/emails/${id}`) + return response.data + }, + + // 发送邮件 - 支持两种调用方式 + async sendEmail( + toOrData: string | { to: string; from?: string; subject: string; content: string; content_type?: string }, + subject?: string, + content?: string, + from?: string, + content_type: 'text' | 'html' | 'markdown' = 'markdown' + ): Promise> { + let emailData: { to: string; from?: string; subject: string; content: string; content_type?: string } + + if (typeof toOrData === 'string') { + // 位置参数调用方式 (to, subject, content, from?, content_type?) + emailData = { + to: toOrData, + subject: subject!, + content: content!, + from, + content_type + } + } else { + // 对象参数调用方式 + emailData = toOrData + } + + const response = await api.post('/emails/send', emailData) + return response.data + } +} + +// 邮箱相关 API +export const mailboxApiService = { + // 获取邮箱列表 - 支持对象参数和位置参数 + async getMailboxes( + pageOrParams: number | { page?: number; limit?: number; scope?: 'all' } = 1, + limit = 20, + scope?: 'all' + ): Promise> { + let params: { page: number; limit: number; scope?: 'all' } + + if (typeof pageOrParams === 'object') { + // 对象参数 + params = { + page: pageOrParams.page || 1, + limit: pageOrParams.limit || 20, + ...pageOrParams + } + } else { + // 位置参数 + params = { + page: pageOrParams, + limit, + scope + } + } + + const urlParams = new URLSearchParams() + urlParams.append('page', params.page.toString()) + urlParams.append('limit', params.limit.toString()) + if (params.scope) urlParams.append('scope', params.scope) + + const response = await api.get(`/mailboxes?${urlParams}`) + return response.data + }, + + // 获取邮箱详情 + async getMailbox(id: number): Promise> { + const response = await api.get(`/mailboxes/${id}`) + return response.data + }, + + // 创建邮箱 + async createMailbox(mailboxData: any): Promise> { + const response = await api.post('/mailboxes', mailboxData) + return response.data + }, + + // 删除邮箱 + async deleteMailbox(id: number): Promise> { + const response = await api.delete(`/mailboxes/${id}`) + return response.data + }, + + // 获取邮箱申请列表 - 支持对象参数和位置参数 + async getMailboxApplications( + pageOrParams: number | { page?: number; limit?: number; scope?: 'all' } = 1, + limit = 20, + scope?: 'all' + ): Promise> { + let params: { page: number; limit: number; scope?: 'all' } + + if (typeof pageOrParams === 'object') { + // 对象参数 + params = { + page: pageOrParams.page || 1, + limit: pageOrParams.limit || 20, + ...pageOrParams + } + } else { + // 位置参数 + params = { + page: pageOrParams, + limit, + scope + } + } + + const urlParams = new URLSearchParams() + urlParams.append('page', params.page.toString()) + urlParams.append('limit', params.limit.toString()) + if (params.scope) urlParams.append('scope', params.scope) + + const response = await api.get(`/mailbox-applications?${urlParams}`) + return response.data + }, + + // 处理邮箱申请 + async processMailboxApplication(id: number, action: 1 | 2, reason?: string): Promise> { + const response = await api.post(`/mailbox-applications/${id}/process`, { action, reason }) + return response.data + }, + + // 切换邮箱状态 + async toggleMailboxStatus(id: number, status: string): Promise> { + const response = await api.put(`/mailboxes/${id}/status`, { status }) + return response.data + }, + + // 获取邮箱历史记录 + async getMailboxHistory(mailboxId: number, page = 1, limit = 20): Promise> { + const params = new URLSearchParams() + params.append('page', page.toString()) + params.append('limit', limit.toString()) + + const response = await api.get(`/mailbox-history/${mailboxId}?${params}`) + return response.data + }, + + // 获取当前用户的邮箱历史记录 + async getUserMailboxHistory(page = 1, limit = 20): Promise> { + const params = new URLSearchParams() + params.append('page', page.toString()) + params.append('limit', limit.toString()) + + const response = await api.get(`/mailbox-history/user/me?${params}`) + return response.data + }, + + // 获取所有邮箱历史记录(仅管理员) + async getAllMailboxHistory(page = 1, limit = 20): Promise> { + const params = new URLSearchParams() + params.append('page', page.toString()) + params.append('limit', limit.toString()) + + const response = await api.get(`/mailbox-history/admin/all?${params}`) + return response.data + } +} + +// 管理员相关 API +export const adminApiService = { + // 获取所有邮件(管理员)- 支持对象参数和位置参数 + async getEmails( + pageOrParams: number | { page?: number; limit?: number; scope?: 'all'; search?: string; status?: string } = 1, + limit = 20, + scope?: 'all', + search?: string, + status?: string + ): Promise> { + let params: { page: number; limit: number; scope?: 'all'; search?: string; status?: string } + + if (typeof pageOrParams === 'object') { + // 对象参数 + params = { + page: pageOrParams.page || 1, + limit: pageOrParams.limit || 20, + ...pageOrParams + } + } else { + // 位置参数 + params = { + page: pageOrParams, + limit, + scope, + search, + status + } + } + + const urlParams = new URLSearchParams() + urlParams.append('page', params.page.toString()) + urlParams.append('limit', params.limit.toString()) + if (params.scope) urlParams.append('scope', params.scope) + if (params.search) urlParams.append('search', params.search) + if (params.status) urlParams.append('status', params.status) + + const response = await api.get(`/emails?${urlParams}`) + return response.data + }, + + // 获取所有用户列表 - 支持对象参数和位置参数 + async getAllUsers( + pageOrParams: number | { page?: number; limit?: number; search?: string } = 1, + limit = 20, + search?: string + ): Promise> { + let params: { page: number; limit: number; search?: string } + + if (typeof pageOrParams === 'object') { + // 对象参数 + params = { + page: pageOrParams.page || 1, + limit: pageOrParams.limit || 20, + ...pageOrParams + } + } else { + // 位置参数 + params = { + page: pageOrParams, + limit, + search + } + } + + const urlParams = new URLSearchParams() + urlParams.append('page', params.page.toString()) + urlParams.append('limit', params.limit.toString()) + if (params.search) urlParams.append('search', params.search) + + const url = `/admin/users?${urlParams}` + + // Debug mode logging + const isDebugMode = import.meta.env.DEV || import.meta.env.VITE_DEBUG === 'true' + if (isDebugMode) { + console.log(`🌐 adminApiService.getAllUsers: 发起请求 ${url}`, params) + } + + try { + const response = await api.get(url) + if (isDebugMode) { + console.log(`🌐 adminApiService.getAllUsers: 请求成功`, response.data) + } + return response.data + } catch (error) { + if (isDebugMode) { + console.error(`🌐 adminApiService.getAllUsers: 请求失败`, error) + } + throw error + } + }, + + // 获取用户详情 + async getUser(id: number): Promise> { + const response = await api.get(`/admin/users/${id}`) + return response.data + }, + + // 更新用户信息 + async updateUser(id: number, userData: any): Promise> { + const response = await api.put(`/admin/users/${id}`, userData) + return response.data + }, + + // 删除用户 + async deleteUser(id: number): Promise> { + const response = await api.delete(`/admin/users/${id}`) + return response.data + }, + + // 获取所有邮箱(管理员)- 支持对象参数和位置参数 + async getAllMailboxes( + pageOrParams: number | { page?: number; limit?: number; search?: string } = 1, + limit = 20, + search?: string + ): Promise> { + let params: { page: number; limit: number; search?: string } + + if (typeof pageOrParams === 'object') { + // 对象参数 + params = { + page: pageOrParams.page || 1, + limit: pageOrParams.limit || 20, + ...pageOrParams + } + } else { + // 位置参数 + params = { + page: pageOrParams, + limit, + search + } + } + + const urlParams = new URLSearchParams() + urlParams.append('page', params.page.toString()) + urlParams.append('limit', params.limit.toString()) + if (params.search) urlParams.append('search', params.search) + + const response = await api.get(`/mailboxes?${urlParams}&scope=all`) + return response.data + }, + + // 获取安全审计统计 + async getSecurityStats(days = 7): Promise> { + const response = await api.get(`/security-audit/attack-stats?days=${days}`) + return response.data + }, + + // 获取转发规则 - 支持对象参数和位置参数 + async getForwardRules( + pageOrParams: number | { page?: number; limit?: number; scope?: 'all'; search?: string } = 1, + limit = 20, + scope?: 'all', + search?: string + ): Promise> { + let params: { page: number; limit: number; scope?: 'all'; search?: string } + + if (typeof pageOrParams === 'object') { + // 对象参数 + params = { + page: pageOrParams.page || 1, + limit: pageOrParams.limit || 20, + ...pageOrParams + } + } else { + // 位置参数 + params = { + page: pageOrParams, + limit, + scope, + search + } + } + + const urlParams = new URLSearchParams() + urlParams.append('page', params.page.toString()) + urlParams.append('limit', params.limit.toString()) + if (params.scope) urlParams.append('scope', params.scope) + if (params.search) urlParams.append('search', params.search) + + const response = await api.get(`/forward-rules?${urlParams}`) + return response.data + }, + + // 创建转发规则 + async createForwardRule(ruleData: any): Promise> { + const response = await api.post('/forward-rules', ruleData) + return response.data + }, + + // 更新转发规则 + async updateForwardRule(id: number, ruleData: any): Promise> { + const response = await api.put(`/forward-rules/${id}`, ruleData) + return response.data + }, + + // 删除转发规则 + async deleteForwardRule(id: number): Promise> { + const response = await api.delete(`/forward-rules/${id}`) + return response.data + } +} + +// 统一 API 服务导出 +export const apiService = { + ...systemApiService, + ...userApiService, + ...authApiService, + ...emailApiService, + ...mailboxApiService, + ...adminApiService, + // 添加别名方法 + getUserProfile: userApiService.getUserProfile, + updateUserSettings: userApiService.updateUserSettings, + getSystemHealth: systemApiService.getSystemHealth, + getRegistrationStatus: systemApiService.getRegistrationStatus, + getSystemConfig: systemApiService.getSystemConfig, + updateSystemConfig: systemApiService.updateSystemConfig, + clearSystemCache: systemApiService.clearSystemCache, + login: authApiService.login, + logout: authApiService.logout, + register: authApiService.register, + getEmails: emailApiService.getEmails, + getEmail: emailApiService.getEmail, + deleteEmail: emailApiService.deleteEmail, + sendEmail: emailApiService.sendEmail, + getMailboxes: mailboxApiService.getMailboxes, + getMailbox: mailboxApiService.getMailbox, + createMailbox: mailboxApiService.createMailbox, + deleteMailbox: mailboxApiService.deleteMailbox, + getMailboxApplications: mailboxApiService.getMailboxApplications, + processMailboxApplication: mailboxApiService.processMailboxApplication, + toggleMailboxStatus: mailboxApiService.toggleMailboxStatus, + getMailboxHistory: mailboxApiService.getMailboxHistory, + // 管理员方法 + getAllUsers: adminApiService.getAllUsers, + getUser: adminApiService.getUser, + updateUser: adminApiService.updateUser, + deleteUser: adminApiService.deleteUser, + getAllMailboxes: adminApiService.getAllMailboxes, + getSecurityStats: adminApiService.getSecurityStats, + getForwardRules: adminApiService.getForwardRules, + createForwardRule: adminApiService.createForwardRule, + updateForwardRule: adminApiService.updateForwardRule, + deleteForwardRule: adminApiService.deleteForwardRule, + // 管理员邮件方法 + getAdminEmails: adminApiService.getEmails, + // 数据库管理方法 + getDatabaseInfo: async (): Promise> => { + const response = await api.get('/database/info') + return response.data + }, + getAllTablesData: async (): Promise> => { + const response = await api.get('/database/tables') + return response.data + }, + initializeDatabase: async (confirmText: string): Promise> => { + const response = await api.post('/database/init', { confirmText }) + return response.data + }, + getDatabaseStats: async (): Promise> => { + const response = await api.get('/database/stats') + return response.data + } +} + +// 默认导出 axios 实例 +export default api diff --git a/vue-legacy/src/composables/auth.ts b/vue-legacy/src/composables/auth.ts new file mode 100644 index 0000000..8ca41f2 --- /dev/null +++ b/vue-legacy/src/composables/auth.ts @@ -0,0 +1,282 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { useStorage } from '@vueuse/core' +import axios, { type AxiosResponse } from 'axios' +import type { + UserProfile, + LoginResponse, + RegisterResponse, + ApiResponse +} from '@/types' + +// ==================== 类型定义 ==================== + +// API 响应基础接口 + +// ==================== API 实例 ==================== + +// 创建 axios 实例 +const api = axios.create({ + baseURL: '/api', + timeout: 10000, + headers: { + 'Content-Type': 'application/json' + }, + withCredentials: true // 支持 cookie 认证 +}) + +// 请求拦截器 +api.interceptors.request.use( + (config: any) => { + // 使用 cookies 认证,不需要手动添加 Authorization header + return config + }, + (error: any) => { + return Promise.reject(error) + } +) + +// 响应拦截器 +api.interceptors.response.use( + (response: AxiosResponse) => { + return response + }, + (error: any) => { + if (error.response?.status === 401) { + // 未授权,清理所有认证数据并重定向到登录页 + console.log('🧹 检测到401错误,清理认证数据并重定向...') + + // 清理所有本地数据 + localStorage.clear() + document.cookie = 'session_cookies=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;' + + // 重定向到登录页 + if (window.location.pathname !== '/login') { + const redirectUrl = window.location.pathname === '/' + ? '/login' + : `/login?redirect=${encodeURIComponent(window.location.pathname + window.location.search)}` + window.location.href = redirectUrl + } + } + return Promise.reject(error) + } +) + +// ==================== API 服务 ==================== + +// 认证相关 API +export const authApiService = { + // 登录 + async login(username: string, password: string): Promise { + const response = await api.post('/auth/login', { username, password }) + return response.data + }, + + // 注册 + async register(username: string, email: string, password: string): Promise { + const response = await api.post('/auth/register', { username, email, password }) + return response.data + }, + + // 获取当前用户信息 + async getCurrentUser(): Promise> { + const response = await api.get('/users/me') + return response.data + }, + + // 登出 + async logout(): Promise { + try { + await api.post('/auth/logout') + } catch (error) { + console.error('登出请求失败:', error) + } finally { + // 无论后端登出是否成功,都清理本地状态 + console.log('🧹 用户登出,清理认证数据并重定向...') + + // 清理所有本地数据 + localStorage.clear() + document.cookie = 'session_cookies=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;' + + // 重定向到登录页 + if (window.location.pathname !== '/login') { + const redirectUrl = window.location.pathname === '/' + ? '/login' + : `/login?redirect=${encodeURIComponent(window.location.pathname + window.location.search)}` + window.location.href = redirectUrl + } + } + } +} + +// ==================== 认证状态管理 ==================== + +export const useAuthStore = defineStore('auth', () => { + // 状态 + const user = ref(null) + const loading = ref(false) + + // 使用 VueUse 的 useStorage 进行持久化存储 + const userStorage = useStorage('user_info', null as UserProfile | null, localStorage, { + serializer: { + read: (value: string) => { + try { + return value ? JSON.parse(value) : null + } catch { + return null + } + }, + write: (value: UserProfile | null) => { + return value ? JSON.stringify(value) : '' + } + } + }) + + // 计算属性 + const isAuthenticated = computed(() => !!user.value) + const isAdmin = computed(() => user.value?.user_type === 1) + + // 获取当前用户信息 + const fetchCurrentUser = async () => { + loading.value = true + try { + const response = await authApiService.getCurrentUser() + if (response.success && response.data) { + user.value = response.data + userStorage.value = response.data + return { success: true } + } else { + // 用户可能未登录 + user.value = null + userStorage.value = null + return { success: false, error: response.message || '获取用户信息失败' } + } + } catch (error: any) { + user.value = null + userStorage.value = null + return { + success: false, + error: error.response?.data?.message || '网络错误' + } + } finally { + loading.value = false + } + } + + // 登出 + const logout = async () => { + loading.value = true + try { + await authApiService.logout() + } catch (error) { + console.error('登出失败:', error) + } finally { + // 无论后端登出是否成功,都清理本地状态 + user.value = null + userStorage.value = null + loading.value = false + } + } + + // 初始化认证状态 + const initAuth = async () => { + // 首先尝试从存储恢复用户信息 + if (userStorage.value) { + user.value = userStorage.value + } + + // 然后尝试从服务器获取最新用户信息 + await fetchCurrentUser() + } + + // 登录 + const login = async (username: string, password: string) => { + loading.value = true + try { + const response = await authApiService.login(username, password) + if (response.success && response.data) { + // 登录成功,直接使用返回的用户信息 + console.log('✅ 登录成功,设置用户信息:', response.data.user) + user.value = response.data.user // TODO 有意义吗? + userStorage.value = response.data.user + console.log('🔐 当前认证状态:', !!user.value) + + return { success: true } + } else { + return { success: false, error: response.message || '登录失败' } + } + } catch (error: any) { + // 处理 HTTP 错误响应 + if (error.response?.status === 401) { + return { + success: false, + error: error.response?.data?.message || '用户名或密码错误' + } + } else if (error.response?.status === 400) { + return { + success: false, + error: error.response?.data?.message || '用户名和密码不能为空' + } + } else { + return { + success: false, + error: error.response?.data?.message || '网络连接错误,请检查网络后重试' + } + } + } finally { + loading.value = false + } + } + + // 注册 + const register = async (username: string, email: string, password: string) => { + loading.value = true + try { + const response = await authApiService.register(username, email, password) + if (response.success) { + return { success: true, message: response.message || '注册成功' } + } else { + return { success: false, error: response.message || '注册失败' } + } + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || '网络错误' + } + } finally { + loading.value = false + } + } + + return { + // 状态 + user, + loading, + + // 计算属性 + isAuthenticated, + isAdmin, + + // 方法 + login, + register, + logout, + fetchCurrentUser, + initAuth + } +}) + +// 导出认证核心功能(供其他文件直接使用) +// 注意:这些需要在 Pinia 初始化后才能使用 +export const useAuthCore = () => { + const store = useAuthStore() + return { + user: store.user, + loading: store.loading, + isAuthenticated: store.isAuthenticated, + isAdmin: store.isAdmin, + fetchCurrentUser: store.fetchCurrentUser, + logout: store.logout, + initAuth: store.initAuth + } +} \ No newline at end of file diff --git a/vue-legacy/src/composables/cache.ts b/vue-legacy/src/composables/cache.ts new file mode 100644 index 0000000..7dba739 --- /dev/null +++ b/vue-legacy/src/composables/cache.ts @@ -0,0 +1,281 @@ +// 缓存服务 - 统一使用localStorage +interface CacheEntry { + value: T + expiry: number + size: number +} + +class CacheService { + private readonly CACHE_PREFIX = 'cem_cache_' + private readonly MAX_TOTAL_SIZE = 50 * 1024 * 1024 // 50MB 总限制 + + set(key: string, value: T, ttl: number): void { + try { + const expiry = Date.now() + ttl + const serializedValue = JSON.stringify(value) + const size = new Blob([serializedValue]).size + + // 检查总大小限制 + if (size > this.MAX_TOTAL_SIZE) { + console.warn(`缓存项 ${key} 过大 (${size} bytes),跳过缓存`) + return + } + + const entry: CacheEntry = { + value, + expiry, + size + } + + const cacheKey = this.CACHE_PREFIX + key + localStorage.setItem(cacheKey, JSON.stringify(entry)) + + console.log(`缓存已保存: ${key} (${size} bytes)`) + } catch (error) { + console.error(`保存缓存失败 ${key}:`, error) + } + } + + get(key: string): T | undefined { + try { + const cacheKey = this.CACHE_PREFIX + key + const item = localStorage.getItem(cacheKey) + + if (!item) { + return undefined + } + + const entry: CacheEntry = JSON.parse(item) + + if (Date.now() > entry.expiry) { + localStorage.removeItem(cacheKey) + return undefined + } + + return entry.value as T + } catch (error) { + console.error(`获取缓存失败 ${key}:`, error) + return undefined + } + } + + delete(key: string): void { + try { + const cacheKey = this.CACHE_PREFIX + key + localStorage.removeItem(cacheKey) + } catch (error) { + console.error(`删除缓存失败 ${key}:`, error) + } + } + + clear(): void { + try { + const keys = Object.keys(localStorage) + keys.forEach(key => { + if (key.startsWith(this.CACHE_PREFIX)) { + localStorage.removeItem(key) + } + }) + } catch (error) { + console.error('清空缓存失败:', error) + } + } + + size(): number { + try { + const keys = Object.keys(localStorage) + return keys.filter(key => key.startsWith(this.CACHE_PREFIX)).length + } catch (error) { + console.error('获取缓存大小失败:', error) + return 0 + } + } + + keys(): string[] { + try { + const keys = Object.keys(localStorage) + return keys + .filter(key => key.startsWith(this.CACHE_PREFIX)) + .map(key => key.substring(this.CACHE_PREFIX.length)) + } catch (error) { + console.error('获取缓存键失败:', error) + return [] + } + } + + // 获取缓存统计信息 + getStats(): { totalSize: number; itemCount: number; averageSize: number } { + try { + const keys = Object.keys(localStorage) + let totalSize = 0 + let itemCount = 0 + + keys.forEach(key => { + if (key.startsWith(this.CACHE_PREFIX)) { + const item = localStorage.getItem(key) + if (item) { + totalSize += new Blob([item]).size + itemCount++ + } + } + }) + + return { + totalSize, + itemCount, + averageSize: itemCount > 0 ? Math.round(totalSize / itemCount) : 0 + } + } catch (error) { + console.error('获取缓存统计失败:', error) + return { totalSize: 0, itemCount: 0, averageSize: 0 } + } + } + + // 生成缓存键 + generateKey(route: string, params?: Record): string { + const baseKey = `page_${route}` + if (!params) return baseKey + + const paramStr = Object.keys(params) + .sort() + .map(key => `${key}=${params[key]}`) + .join('&') + + return `${baseKey}_${paramStr}` + } + + // 按模式清除缓存(支持通配符) + clearByPattern(pattern: string): void { + try { + // 将通配符模式转换为正则表达式 + // 例如: "system_settings_1_system-settings_*" -> /^system_settings_1_system-settings_.*$/ + const regexPattern = pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + const regex = new RegExp(`^${regexPattern}$`) + + const keys = this.keys() + let clearedCount = 0 + + keys.forEach(key => { + if (regex.test(key)) { + this.delete(key) + clearedCount++ + } + }) + + console.log(`🗑️ 按模式清除缓存: ${pattern}, 清除了 ${clearedCount} 项`) + } catch (error) { + console.error(`按模式清除缓存失败 ${pattern}:`, error) + } + } +} + +// 全局缓存实例 +export const cacheService = new CacheService() + +// 页面刷新管理器 +export class PageRefreshManager { + private refreshHandlers = new Map Promise>() + + // 注册页面的刷新处理函数 + registerPageRefresh(routeName: string, handler: () => Promise): void { + this.refreshHandlers.set(routeName, handler) + } + + // 注销页面的刷新处理函数 + unregisterPageRefresh(routeName: string): void { + this.refreshHandlers.delete(routeName) + } + + // 刷新当前页面 + async refreshCurrentPage(routeName: string): Promise { + const handler = this.refreshHandlers.get(routeName) + if (handler) { + await handler() + } else { + console.warn(`No refresh handler registered for route: ${routeName}`) + } + } + + // 清理当前页面缓存并刷新 + async clearAndRefreshCurrentPage(routeName: string): Promise { + // 清理所有与该页面相关的缓存 + const keys = cacheService.keys() + const pageKeys = keys.filter(key => key.includes(`page_${routeName}`)) + pageKeys.forEach(key => cacheService.delete(key)) + + // 刷新页面 + await this.refreshCurrentPage(routeName) + } + + // 获取所有已注册的页面 + getRegisteredPages(): string[] { + return Array.from(this.refreshHandlers.keys()) + } +} + +// 全局刷新管理器实例 +export const pageRefreshManager = new PageRefreshManager() + +// 缓存装饰器 - 用于API调用 +export function withCache Promise>( + fn: T, + cacheKey: string | ((...args: Parameters) => string), + ttl?: number +): T { + return (async (...args: Parameters) => { + const key = typeof cacheKey === 'function' ? cacheKey(...args) : cacheKey + + // 尝试从缓存获取 + const cached = cacheService.get(key) + if (cached !== null) { + console.log(`Cache hit for key: ${key}`) + return cached + } + + // 缓存未命中,调用原函数 + console.log(`Cache miss for key: ${key}, calling API`) + const result = await fn(...args) + + // 将结果存入缓存 + cacheService.set(key, result, ttl || 300000) + + return result + }) as T +} + +// 页面数据加载器 - 带缓存 +export class PageDataLoader { + constructor( + private routeName: string, + private loader: () => Promise, + private cacheKey?: string, + private ttl?: number + ) { } + + async load(forceRefresh = false): Promise { + const key = this.cacheKey || `page_${this.routeName}` + + // 如果强制刷新,先清除缓存 + if (forceRefresh) { + cacheService.delete(key) + } + + // 尝试从缓存获取 + const cached = cacheService.get(key) + if (cached && !forceRefresh) { + console.log(`Cache hit for page: ${this.routeName}`) + return cached + } + + // 缓存未命中,调用加载器 + console.log(`Cache miss for page: ${this.routeName}, loading data`) + const result = await this.loader() + + // 将结果存入缓存 + if (this.ttl) { + cacheService.set(key, result, this.ttl) + } + + return result + } +} diff --git a/vue-legacy/src/composables/globalRefreshManager.ts b/vue-legacy/src/composables/globalRefreshManager.ts new file mode 100644 index 0000000..d08b2fb --- /dev/null +++ b/vue-legacy/src/composables/globalRefreshManager.ts @@ -0,0 +1,207 @@ +/** + * 全局刷新管理器 + * 统一管理所有页面的刷新逻辑,根据当前路由自动调用对应的API + */ + +import { ref, computed } from 'vue' +import { useRoute } from 'vue-router' +import { useRouteApiCacheMapper, useGlobalRefreshManager } from './routeApiCacheMapper' +import { useAuthStore } from './stores' + +// 刷新状态管理 +const refreshing = ref(false) +const lastRefreshTime = ref(null) +const refreshHistory = ref>([]) + +// 全局刷新管理器 +export function useUnifiedGlobalRefreshManager() { + const route = useRoute() + const authStore = useAuthStore() + const { isSupportedRoute, hasPermission, getRouteInfo } = useRouteApiCacheMapper() + const { refreshCurrentPage } = useGlobalRefreshManager() + + // 当前刷新状态 + const isRefreshing = computed(() => refreshing.value) + + // 上次刷新时间 + const lastRefresh = computed(() => lastRefreshTime.value) + + // 刷新历史 + const refreshHistoryList = computed(() => refreshHistory.value) + + // 执行全局刷新 + const executeGlobalRefresh = async (customParams?: any) => { + if (refreshing.value) { + console.log('🔄 刷新正在进行中,跳过重复请求') + return null + } + + refreshing.value = true + const startTime = new Date() + + try { + console.log('🌍 开始全局刷新') + + // 先触发全局刷新事件 + window.dispatchEvent(new CustomEvent('global:refresh', { + detail: { + routeName: route.name, + timestamp: startTime, + customParams + } + })) + + let result = null + + // 检查是否支持统一管理 + if (isSupportedRoute.value && hasPermission.value) { + console.log('📋 使用统一刷新管理器') + result = await refreshCurrentPage(customParams) + } else { + console.log('🔧 使用页面级刷新方法') + // 调用页面级刷新方法(如果存在) + if (window.refreshCurrentPage && typeof window.refreshCurrentPage === 'function') { + // 避免递归调用,检查是否是同一个函数 + if (window.refreshCurrentPage !== executeGlobalRefresh) { + result = await window.refreshCurrentPage() + } + } + } + + // 记录刷新历史 + const endTime = new Date() + refreshHistory.value.unshift({ + routeName: route.name as string, + timestamp: endTime, + success: true + }) + + // 保持历史记录不超过50条 + if (refreshHistory.value.length > 50) { + refreshHistory.value = refreshHistory.value.slice(0, 50) + } + + lastRefreshTime.value = endTime + + console.log(`✅ 全局刷新完成,耗时: ${endTime.getTime() - startTime.getTime()}ms`) + return result + + } catch (error) { + console.error('❌ 全局刷新失败:', error) + + // 记录失败历史 + refreshHistory.value.unshift({ + routeName: route.name as string, + timestamp: new Date(), + success: false, + error: error instanceof Error ? error.message : String(error) + }) + + throw error + } finally { + refreshing.value = false + } + } + + // 获取当前页面刷新信息 + const getCurrentPageRefreshInfo = () => { + const routeInfo = getRouteInfo() + return { + routeName: route.name as string, + isSupported: isSupportedRoute.value, + hasPermission: hasPermission.value, + routeInfo, + lastRefresh: lastRefreshTime.value, + isRefreshing: refreshing.value + } + } + + // 清除刷新历史 + const clearRefreshHistory = () => { + refreshHistory.value = [] + console.log('🗑️ 刷新历史已清除') + } + + // 获取刷新统计 + const getRefreshStats = () => { + const total = refreshHistory.value.length + const successful = refreshHistory.value.filter(item => item.success).length + const failed = total - successful + + return { + total, + successful, + failed, + successRate: total > 0 ? (successful / total * 100).toFixed(1) + '%' : '0%' + } + } + + return { + // 状态 + isRefreshing, + lastRefresh, + refreshHistoryList, + + // 方法 + executeGlobalRefresh, + getCurrentPageRefreshInfo, + clearRefreshHistory, + getRefreshStats + } +} + +// 页面级刷新注册器 +export function usePageRefreshRegistry() { + // 注册页面级刷新方法 + const registerPageRefresh = (refreshFunction: () => Promise | void) => { + window.refreshCurrentPage = refreshFunction + console.log('📝 页面级刷新方法已注册') + } + + // 注销页面级刷新方法 + const unregisterPageRefresh = () => { + if (window.refreshCurrentPage) { + delete window.refreshCurrentPage + console.log('🗑️ 页面级刷新方法已注销') + } + } + + // 检查是否有页面级刷新方法 + const hasPageRefreshMethod = () => { + return window.refreshCurrentPage && typeof window.refreshCurrentPage === 'function' + } + + return { + registerPageRefresh, + unregisterPageRefresh, + hasPageRefreshMethod + } +} + +// 全局刷新事件监听器 +export function useGlobalRefreshEventListener() { + // 监听全局刷新事件 + const addGlobalRefreshListener = (callback: (event: CustomEvent) => void) => { + window.addEventListener('global:refresh', callback as EventListener) + console.log('👂 全局刷新事件监听器已添加') + } + + // 移除全局刷新事件监听器 + const removeGlobalRefreshListener = (callback: (event: CustomEvent) => void) => { + window.removeEventListener('global:refresh', callback as EventListener) + console.log('🗑️ 全局刷新事件监听器已移除') + } + + return { + addGlobalRefreshListener, + removeGlobalRefreshListener + } +} + +// 导出全局状态供其他组件使用 +export { refreshing, lastRefreshTime, refreshHistory } diff --git a/vue/src/composables/globalState.ts b/vue-legacy/src/composables/globalState.ts similarity index 100% rename from vue/src/composables/globalState.ts rename to vue-legacy/src/composables/globalState.ts diff --git a/vue/src/composables/pageConfig.ts b/vue-legacy/src/composables/pageConfig.ts similarity index 100% rename from vue/src/composables/pageConfig.ts rename to vue-legacy/src/composables/pageConfig.ts diff --git a/vue/src/composables/routeApiCacheMapper.ts b/vue-legacy/src/composables/routeApiCacheMapper.ts similarity index 92% rename from vue/src/composables/routeApiCacheMapper.ts rename to vue-legacy/src/composables/routeApiCacheMapper.ts index 6687090..fd82f44 100644 --- a/vue/src/composables/routeApiCacheMapper.ts +++ b/vue-legacy/src/composables/routeApiCacheMapper.ts @@ -5,18 +5,19 @@ import { computed, ref } from 'vue' import { useRoute } from 'vue-router' -import { adminApiService, userApiService, emailApiService, mailboxApiService } from './api' +import { adminApiService, userApiService, emailApiService, mailboxApiService, systemApiService } from './api' import { cacheService } from './cache' import { useAuthStore } from './stores' // API方法类型定义 - 按服务分类 -export type EmailApiMethod = 'getEmails' | 'getAllEmails' +export type EmailApiMethod = 'getEmails' export type MailboxApiMethod = 'getMailboxes' | 'getAllMailboxes' -export type UserApiMethod = 'getForwardRules' | 'getUserInfo' -export type AdminApiMethod = 'getUsers' | 'getSystemConfig' | 'getSecurityStats' | 'getMailboxHistory' +export type UserApiMethod = 'getForwardRules' | 'getUserProfile' +export type AdminApiMethod = 'getUsers' | 'getSecurityStats' | 'getMailboxHistory' | 'getForwardRules' +export type SystemApiMethod = 'getSystemConfig' // 联合类型 -export type ApiMethod = EmailApiMethod | MailboxApiMethod | UserApiMethod | AdminApiMethod +export type ApiMethod = EmailApiMethod | MailboxApiMethod | UserApiMethod | AdminApiMethod | SystemApiMethod // API服务映射配置 interface ApiServiceMapping { @@ -29,7 +30,7 @@ interface ApiServiceMapping { const API_SERVICE_MAPPINGS: ApiServiceMapping[] = [ { service: emailApiService, - methods: ['getEmails', 'getAllEmails'], + methods: ['getEmails'], description: '邮件相关API' }, { @@ -39,13 +40,18 @@ const API_SERVICE_MAPPINGS: ApiServiceMapping[] = [ }, { service: userApiService, - methods: ['getForwardRules', 'getUserInfo'], + methods: ['getForwardRules', 'getUserProfile'], description: '用户相关API' }, { service: adminApiService, - methods: ['getUsers', 'getSystemConfig', 'getSecurityStats', 'getMailboxHistory', 'getAllMailboxes'], + methods: ['getUsers', 'getSecurityStats', 'getMailboxHistory', 'getAllMailboxes', 'getForwardRules'], description: '管理员相关API' + }, + { + service: systemApiService, + methods: ['getSystemConfig'], + description: '系统配置API' } ] @@ -92,8 +98,8 @@ export const ROUTE_API_CACHE_MAP: Record = { // 管理员全部邮件页面 'all-emails': { routeName: 'all-emails', - apiMethod: 'getAllEmails', - apiParams: { page: 1, limit: 20 }, + apiMethod: 'getEmails', + apiParams: { page: 1, limit: 20, scope: 'all' }, cacheStrategy: { keyPrefix: 'all_emails', ttl: 2 * 60 * 1000 }, requiresAuth: true, adminOnly: true, @@ -132,7 +138,7 @@ export const ROUTE_API_CACHE_MAP: Record = { description: '用户管理' }, - // 转发规则页面 + // 转发规则页面(普通用户) 'forward-rules': { routeName: 'forward-rules', apiMethod: 'getForwardRules', @@ -142,10 +148,21 @@ export const ROUTE_API_CACHE_MAP: Record = { description: '转发规则' }, + // 管理员转发规则页面 + 'admin-rules': { + routeName: 'admin-rules', + apiMethod: 'getForwardRules', + apiParams: { page: 1, limit: 20, scope: 'all' }, + cacheStrategy: { keyPrefix: 'admin_forward_rules', ttl: 5 * 60 * 1000 }, + requiresAuth: true, + adminOnly: true, + description: '转发管理' + }, + // 个人设置页面 'personal-settings': { routeName: 'personal-settings', - apiMethod: 'getUserInfo', + apiMethod: 'getUserProfile', apiParams: {}, cacheStrategy: { keyPrefix: 'personal_settings', ttl: 10 * 60 * 1000 }, requiresAuth: true, diff --git a/vue/src/composables/routeState.ts b/vue-legacy/src/composables/routeState.ts similarity index 100% rename from vue/src/composables/routeState.ts rename to vue-legacy/src/composables/routeState.ts diff --git a/vue-legacy/src/composables/routes.ts b/vue-legacy/src/composables/routes.ts new file mode 100644 index 0000000..a25d6c2 --- /dev/null +++ b/vue-legacy/src/composables/routes.ts @@ -0,0 +1,167 @@ +import { createRouter, createWebHistory } from 'vue-router' +import { useSystemStore } from './system' + +// 动态导入组件 +const LoginView = () => import('@/views/auth/LoginView.vue') +const MainLayout = () => import('@/views/shared/layouts/MainLayout.vue') + +// 共享视图 +const DashboardView = () => import('@/views/shared/dashboard/DashboardView.vue') + +// 用户视图 +const UserMyEmailsView = () => import('@/views/user/emails/MyEmailsView.vue') +const UserMyMailboxesView = () => import('@/views/user/mailboxes/MyMailboxesView.vue') +const UserPersonalSettingsView = () => import('@/views/user/settings/PersonalSettingsView.vue') + +// 管理员视图 +const AdminAllEmailsView = () => import('@/views/admin/emails/AllEmailsView.vue') +const AdminMailboxManagementView = () => import('@/views/admin/mailboxes/MailboxManagementView.vue') +const AdminSystemSettingsView = () => import('@/views/admin/settings/SystemSettingsView.vue') +const AdminUsersView = () => import('@/views/admin/users/UsersView.vue') +const AdminForwardRulesView = () => import('@/views/admin/mailboxes/ForwardRulesView.vue') +const AdminSecurityOverviewView = () => import('@/views/admin/security/SecurityOverviewView.vue') +const AdminDebugView = () => import('@/views/admin/settings/DebugView.vue') + +// 错误页面 +const NotFoundView = () => import('@/views/shared/error/NotFoundView.vue') + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/login', + name: 'Login', + component: LoginView, + meta: { requiresAuth: false } + }, + { + path: '/', + component: MainLayout, + meta: { requiresAuth: true }, + children: [ + // 仪表板 + { + path: '', + name: 'dashboard', + component: DashboardView + }, + + // 用户页面 + { + path: 'my-emails', + name: 'my-emails', + component: UserMyEmailsView + }, + { + path: 'my-mailboxes', + name: 'my-mailboxes', + component: UserMyMailboxesView + }, + { + path: 'forward-rules', + name: 'forward-rules', + component: AdminForwardRulesView + }, + { + path: 'personal-settings', + name: 'personal-settings', + component: UserPersonalSettingsView + }, + + // 管理员页面 + { + path: 'all-emails', + name: 'all-emails', + component: AdminAllEmailsView + }, + { + path: 'mailbox-management', + name: 'mailbox-management', + component: AdminMailboxManagementView + }, + { + path: 'admin-users', + name: 'admin-users', + component: AdminUsersView + }, + { + path: 'admin-rules', + name: 'admin-rules', + component: AdminForwardRulesView + }, + { + path: 'admin-security-overview', + name: 'admin-security-overview', + component: AdminSecurityOverviewView + }, + { + path: 'system-settings', + name: 'system-settings', + component: AdminSystemSettingsView + }, + { + path: 'debug', + name: 'debug', + component: AdminDebugView, + meta: { requiresDebug: true } + } + ] + }, + // 404 错误页面 + { + path: '/:pathMatch(.*)*', + name: 'NotFound', + component: NotFoundView, + meta: { requiresAuth: false } + } + ] +}) + +// 路由守卫 - 简化版本,避免在路由守卫中使用 store +router.beforeEach(async (to, _from, next) => { + // 检查是否需要认证 + if (to.meta.requiresAuth) { + // 检查 localStorage 中是否有用户信息 + const userInfo = localStorage.getItem('user_info') + if (!userInfo) { + // 如果是根路径,直接跳转到登录页(避免 redirect=%2F) + if (to.path === '/') { + next('/login') + } else { + // 其他路径,携带重定向参数 + const redirectUrl = encodeURIComponent(to.fullPath) + next(`/login?redirect=${redirectUrl}`) + } + return + } + } + + // 如果已经在登录页且有用户信息,重定向到首页 + if (to.path === '/login') { + const userInfo = localStorage.getItem('user_info') + if (userInfo) { + next('/') + return + } + } + + // 检查调试模式权限 + if (to.meta.requiresDebug) { + const systemStore = useSystemStore() + + // 总是获取最新的系统健康状态(包含调试模式配置) + // health 接口无需认证,适合在路由守卫中使用 + if (!systemStore.systemConfig) { + await systemStore.fetchSystemHealth() + } + + if (systemStore.systemConfig?.debug_mode !== 1) { + next('/') + return + } + } + + next() +}) + +export default router diff --git a/vue/src/composables/simpleCache.ts b/vue-legacy/src/composables/simpleCache.ts similarity index 100% rename from vue/src/composables/simpleCache.ts rename to vue-legacy/src/composables/simpleCache.ts diff --git a/vue-legacy/src/composables/smartCache.ts b/vue-legacy/src/composables/smartCache.ts new file mode 100644 index 0000000..61ed850 --- /dev/null +++ b/vue-legacy/src/composables/smartCache.ts @@ -0,0 +1,254 @@ +/** + * 智能缓存管理系统 + * 支持缓存失效、依赖关系、分层缓存等高级功能 + */ + +interface CacheEntry { + data: any + timestamp: number + ttl: number + dependencies: string[] + version: number +} + +interface CacheConfig { + ttl: number + dependencies: string[] + version: number + maxSize?: number +} + +class SmartCacheManager { + private cache = new Map() + private dependencies = new Map>() + + constructor() { + // 初始化缓存管理器 + } + + /** + * 设置缓存 + */ + set(key: string, data: any, config: Partial = {}): void { + const entry: CacheEntry = { + data, + timestamp: Date.now(), + ttl: config.ttl || 5 * 60 * 1000, // 默认5分钟 + dependencies: config.dependencies || [], + version: config.version || 1 + } + + this.cache.set(key, entry) + this.updateDependencies(key, entry.dependencies) + this.cleanup() + } + + /** + * 获取缓存 + */ + get(key: string): T | null { + const entry = this.cache.get(key) + + if (!entry) { + return null + } + + // 检查是否过期 + if (Date.now() - entry.timestamp > entry.ttl) { + this.cache.delete(key) + this.removeDependencies(key) + return null + } + + return entry.data as T + } + + /** + * 删除缓存 + */ + delete(key: string): boolean { + const deleted = this.cache.delete(key) + if (deleted) { + this.removeDependencies(key) + } + return deleted + } + + /** + * 使依赖的缓存失效 + */ + invalidate(dependency: string): void { + const dependents = this.dependencies.get(dependency) + if (dependents) { + for (const key of dependents) { + this.cache.delete(key) + } + this.dependencies.delete(dependency) + } + } + + /** + * 批量失效 + */ + invalidateMultiple(dependencies: string[]): void { + dependencies.forEach(dep => this.invalidate(dep)) + } + + /** + * 更新依赖关系 + */ + private updateDependencies(key: string, dependencies: string[]): void { + // 移除旧的依赖关系 + this.removeDependencies(key) + + // 添加新的依赖关系 + dependencies.forEach(dep => { + if (!this.dependencies.has(dep)) { + this.dependencies.set(dep, new Set()) + } + this.dependencies.get(dep)!.add(key) + }) + } + + /** + * 移除依赖关系 + */ + private removeDependencies(key: string): void { + for (const [dep, keys] of this.dependencies.entries()) { + keys.delete(key) + if (keys.size === 0) { + this.dependencies.delete(dep) + } + } + } + + /** + * 清理过期缓存 + */ + private cleanup(): void { + const now = Date.now() + const toDelete: string[] = [] + + for (const [key, entry] of this.cache.entries()) { + if (now - entry.timestamp > entry.ttl) { + toDelete.push(key) + } + } + + toDelete.forEach(key => this.delete(key)) + } + + /** + * 获取缓存统计 + */ + getStats() { + const totalSize = Array.from(this.cache.values()) + .reduce((sum, entry) => sum + JSON.stringify(entry.data).length, 0) + + return { + totalKeys: this.cache.size, + totalSize, + dependencies: this.dependencies.size + } + } + + /** + * 清空所有缓存 + */ + clear(): void { + this.cache.clear() + this.dependencies.clear() + } +} + +// 创建全局缓存实例 +export const smartCache = new SmartCacheManager() + +// 缓存键生成器 +export const CacheKeys = { + // 用户相关 + userStats: (userId: number) => `user_stats_${userId}`, + userProfile: (userId: number) => `user_profile_${userId}`, + + // 邮件相关 + emailList: (userId: number, page: number, limit: number, scope?: string) => + `emails_${userId}_${page}_${limit}_${scope || 'user'}`, + + // 邮箱相关 - 包含路由信息以区分不同页面 + mailboxList: (userId: number, page: number, limit: number, scope?: string, routeName?: string) => + `mailboxes_${userId}_${page}_${limit}_${scope || 'user'}_${routeName || 'default'}`, + + // 申请相关 + applicationList: (userId: number, page: number, limit: number, scope?: string) => + `applications_${userId}_${page}_${limit}_${scope || 'user'}`, + + // 统计相关 + recentEmails: (userId: number) => `recent_emails_${userId}`, + mailboxStats: (userId: number) => `mailbox_stats_${userId}`, + dashboardStats: (userId: number) => `dashboard_stats_${userId}`, + + // 系统相关 + systemConfig: 'system_config', + availableDomains: 'available_domains' +} + +// 依赖关系定义 +export const CacheDependencies = { + // 当有新邮件时,失效相关缓存 + NEW_EMAIL: 'new_email', + // 当有新邮箱时,失效相关缓存 + NEW_MAILBOX: 'new_mailbox', + // 当有申请状态变化时,失效相关缓存 + APPLICATION_CHANGE: 'application_change', + // 当用户信息变化时,失效相关缓存 + USER_CHANGE: 'user_change', + // 当系统配置变化时,失效相关缓存 + SYSTEM_CONFIG_CHANGE: 'system_config_change' +} + +// 缓存失效工具函数 +export const cacheInvalidation = { + // 新邮件时失效相关缓存 + onNewEmail: (userId: number) => { + smartCache.invalidateMultiple([ + CacheDependencies.NEW_EMAIL, + CacheKeys.userStats(userId) + ]) + }, + + // 新邮箱时失效相关缓存 + onNewMailbox: (userId: number) => { + smartCache.invalidateMultiple([ + CacheDependencies.NEW_MAILBOX, + CacheKeys.userStats(userId) + ]) + }, + + // 申请状态变化时失效相关缓存 + onApplicationChange: (userId: number) => { + smartCache.invalidateMultiple([ + CacheDependencies.APPLICATION_CHANGE, + CacheKeys.userStats(userId) + ]) + }, + + // 用户信息变化时失效相关缓存 + onUserChange: (userId: number) => { + smartCache.invalidateMultiple([ + CacheDependencies.USER_CHANGE, + CacheKeys.userProfile(userId), + CacheKeys.userStats(userId) + ]) + }, + + // 系统配置变化时失效相关缓存 + onSystemConfigChange: () => { + smartCache.invalidateMultiple([ + CacheDependencies.SYSTEM_CONFIG_CHANGE, + CacheKeys.systemConfig, + CacheKeys.availableDomains + ]) + } +} + +export default smartCache \ No newline at end of file diff --git a/vue-legacy/src/composables/stores.ts b/vue-legacy/src/composables/stores.ts new file mode 100644 index 0000000..2b32f90 --- /dev/null +++ b/vue-legacy/src/composables/stores.ts @@ -0,0 +1,18 @@ +// 导出所有 stores +export { useAuthStore } from './auth' +export { useSystemStore } from './system' + +// 导出组合式函数 +export { useApp } from './useApp' + +// 导出认证核心功能 +export { useAuthCore } from './auth' + +// 导出认证相关 API +export { authApiService } from './auth' + +// 导出系统相关 API +export { systemApiService } from './api' + +// 导出所有类型 +export type * from '@/types' \ No newline at end of file diff --git a/vue-legacy/src/composables/system.ts b/vue-legacy/src/composables/system.ts new file mode 100644 index 0000000..0fb665c --- /dev/null +++ b/vue-legacy/src/composables/system.ts @@ -0,0 +1,171 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import type { SystemHealth } from '@/types' +import { systemApiService } from '@/composables/api' +import { useStorage } from '@vueuse/core' + +// 调试工具函数 +const debugLog = (...args: any[]) => { + const isDebugMode = import.meta.env.DEV || import.meta.env.VITE_DEBUG === 'true' + if (isDebugMode) { + console.log('[SystemStore]', ...args) + } +} + +export const useSystemStore = defineStore('system', () => { + // 状态 + const systemHealth = ref(null) + const registrationStatus = ref<{ allow_registration: number } | null>(null) + // 使用 localStorage 存储 systemConfig + const systemConfig = useStorage<{ + debug_mode?: number; + allow_registration?: number; + auto_approve_mailbox?: number; + supported_domains?: string[]; + mail_retention_days?: number; + attachment_retention_days?: number; + attachment_max_size?: number; + cookie_max_age?: number; + jwt_secret?: string; + domains?: string[]; + } | null>('systemConfig', null, localStorage, { + serializer: { + read: (value: string) => { + try { + return value ? JSON.parse(value) : null + } catch { + return null + } + }, + write: (value: { + debug_mode?: number; + allow_registration?: number; + auto_approve_mailbox?: number; + supported_domains?: string[]; + mail_retention_days?: number; + attachment_retention_days?: number; + attachment_max_size?: number; + cookie_max_age?: number; + jwt_secret?: string; + domains?: string[]; + } | null) => { + return value ? JSON.stringify(value) : '' + } + } + }) + const loading = ref(false) + + // 获取系统健康状态 + const fetchSystemHealth = async () => { + loading.value = true + try { + const response = await systemApiService.getSystemHealth() + if (response.success && response.data) { + systemHealth.value = response + + // 将 health 接口的 config 字段同步到 systemConfig 缓存 + // 注意:只更新特定字段,不覆盖完整配置 + if (response.data.health?.config) { + const healthConfig = response.data.health.config as { allow_registration: number; debug_mode: number } + + // 使用空值合并赋值操作符,确保 systemConfig 存在并更新字段 + (systemConfig.value ??= {}).allow_registration = healthConfig.allow_registration + systemConfig.value.debug_mode = healthConfig.debug_mode + + debugLog('系统健康状态配置已同步到 localStorage (部分更新):', systemConfig.value) + } + + return { success: true } + } else { + return { success: false, error: response.message || '获取系统状态失败' } + } + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || '网络错误' + } + } finally { + loading.value = false + } + } + + // 获取注册状态 + const fetchRegistrationStatus = async () => { + loading.value = true + try { + const response = await systemApiService.getRegistrationStatus() + if (response.success && response.data) { + registrationStatus.value = response.data + return { success: true } + } else { + return { success: false, error: response.message || '获取注册状态失败' } + } + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || '网络错误' + } + } finally { + loading.value = false + } + } + + // 获取系统配置 + const fetchSystemConfig = async () => { + loading.value = true + try { + const response = await systemApiService.getSystemConfig() + if (response.success && response.data) { + // 正确提取 config 字段并保存到 localStorage + const config = response.data.config || response.data + systemConfig.value = config + debugLog('系统配置已保存到 localStorage:', config) + return { success: true } + } else { + return { success: false, error: response.message || '获取系统配置失败' } + } + } catch (error: any) { + return { + success: false, + error: error.response?.data?.message || '网络错误' + } + } finally { + loading.value = false + } + } + + // 计算属性 + const isDebugMode = computed(() => { + // 只从 systemConfig 读取数据库设置,绝对不使用环境变量 + if (systemConfig.value?.debug_mode !== undefined) { + const result = systemConfig.value.debug_mode === 1 + debugLog('debug_mode:', systemConfig.value.debug_mode, 'type:', typeof systemConfig.value.debug_mode, 'result:', result) + return result + } + // 如果数据库设置不可用,默认返回 false(不使用环境变量) + debugLog('debug_mode 未定义,默认返回 false') + return false + }) + + const isRegistrationAllowed = computed(() => { + // 从 systemConfig 读取(配置修改时会自动更新缓存) + return systemConfig.value?.allow_registration === 1 + }) + + return { + // 状态 + systemHealth, + registrationStatus, + systemConfig, + loading, + + // 计算属性 + isDebugMode, + isRegistrationAllowed, + + // 方法 + fetchSystemHealth, + fetchRegistrationStatus, + fetchSystemConfig + } +}) diff --git a/vue-legacy/src/composables/testCacheKeys.ts b/vue-legacy/src/composables/testCacheKeys.ts new file mode 100644 index 0000000..a1c1061 --- /dev/null +++ b/vue-legacy/src/composables/testCacheKeys.ts @@ -0,0 +1,93 @@ +/** + * 缓存键测试工具 + * 用于验证不同路由和参数组合的缓存键生成 + */ + +import { ROUTE_API_CACHE_MAP } from './routeApiCacheMapper' + +// 模拟用户信息 +const mockUser = { id: 123, user_type: 1 } // 管理员用户 + +// 测试缓存键生成 +export function testCacheKeyGeneration() { + console.log('🧪 开始测试缓存键生成...') + + // 测试邮件页面的缓存键 + const emailRoutes = ['emails', 'admin-emails'] + + emailRoutes.forEach(routeName => { + const config = ROUTE_API_CACHE_MAP[routeName] + if (config) { + console.log(`\n📧 测试路由: ${routeName}`) + console.log('配置:', { + routeName: config.routeName, + keyPrefix: config.cacheStrategy.keyPrefix, + defaultParams: config.apiParams + }) + + // 测试不同的参数组合 + const testParams = [ + {}, // 无额外参数 + { page: 2 }, // 分页参数 + { scope: 'all' }, // scope参数 + { page: 2, scope: 'all' }, // 组合参数 + { search: 'test' }, // 搜索参数 + ] + + testParams.forEach(params => { + const finalParams = { ...config.apiParams, ...params } + const paramString = Object.entries(finalParams) + .filter(([_, value]) => value !== undefined && value !== null) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, value]) => `${key}_${value}`) + .join('_') + + const cacheKey = `${config.cacheStrategy.keyPrefix}_${mockUser.id}_${config.routeName}_${paramString}` + + console.log(` 参数:`, params) + console.log(` 最终参数:`, finalParams) + console.log(` 缓存键: ${cacheKey}`) + }) + } + }) + + // 测试潜在冲突 + console.log('\n⚠️ 测试潜在缓存冲突:') + + // 模拟用户在emails页面切换showAllEmails的情况 + const emailsConfig = ROUTE_API_CACHE_MAP['emails'] + const adminEmailsConfig = ROUTE_API_CACHE_MAP['admin-emails'] + + if (emailsConfig && adminEmailsConfig) { + // 用户在emails页面,showAllEmails = true + const userEmailsWithAllScope = { ...emailsConfig.apiParams, scope: 'all' } + const userEmailsParamString = Object.entries(userEmailsWithAllScope) + .filter(([_, value]) => value !== undefined && value !== null) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, value]) => `${key}_${value}`) + .join('_') + const userEmailsCacheKey = `${emailsConfig.cacheStrategy.keyPrefix}_${mockUser.id}_${emailsConfig.routeName}_${userEmailsParamString}` + + // 管理员在admin-emails页面 + const adminEmailsParamString = Object.entries(adminEmailsConfig.apiParams) + .filter(([_, value]) => value !== undefined && value !== null) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, value]) => `${key}_${value}`) + .join('_') + const adminEmailsCacheKey = `${adminEmailsConfig.cacheStrategy.keyPrefix}_${mockUser.id}_${adminEmailsConfig.routeName}_${adminEmailsParamString}` + + console.log('用户在emails页面,showAllEmails=true:') + console.log(` 缓存键: ${userEmailsCacheKey}`) + console.log('管理员在admin-emails页面:') + console.log(` 缓存键: ${adminEmailsCacheKey}`) + console.log(` 是否冲突: ${userEmailsCacheKey === adminEmailsCacheKey ? '❌ 是' : '✅ 否'}`) + } + + console.log('\n✅ 缓存键生成测试完成') +} + +// 在开发模式下自动运行测试 +if (import.meta.env.DEV) { + console.log('🔧 开发模式:运行缓存键测试') + testCacheKeyGeneration() +} diff --git a/vue-legacy/src/composables/testRouteApiMapper.ts b/vue-legacy/src/composables/testRouteApiMapper.ts new file mode 100644 index 0000000..7a2cc43 --- /dev/null +++ b/vue-legacy/src/composables/testRouteApiMapper.ts @@ -0,0 +1,99 @@ +/** + * 测试路由API映射器 + * 用于验证API服务选择是否正确 + */ + +import { ROUTE_API_CACHE_MAP } from './routeApiCacheMapper' +import { emailApiService, adminApiService, userApiService, mailboxApiService } from './api' + +// 测试API服务选择 +export function testApiServiceSelection() { + console.log('🧪 开始测试API服务选择...') + + // 测试邮件相关路由 + const emailRoutes = ['emails', 'admin-emails'] + emailRoutes.forEach(routeName => { + const config = ROUTE_API_CACHE_MAP[routeName] + if (config) { + console.log(`📧 路由 ${routeName}:`, { + apiMethod: config.apiMethod, + hasGetEmails: typeof emailApiService.getEmails === 'function', + hasAdminGetEmails: typeof adminApiService.getEmails === 'function' + }) + } + }) + + // 测试邮箱相关路由 + const mailboxRoutes = ['mailboxes', 'admin-mailboxes'] + mailboxRoutes.forEach(routeName => { + const config = ROUTE_API_CACHE_MAP[routeName] + if (config) { + console.log(`📮 路由 ${routeName}:`, { + apiMethod: config.apiMethod, + hasGetMailboxes: typeof mailboxApiService.getMailboxes === 'function', + hasGetAllMailboxes: typeof adminApiService.getAllMailboxes === 'function' + }) + } + }) + + // 测试用户相关路由 + const userRoutes = ['admin-users'] + userRoutes.forEach(routeName => { + const config = ROUTE_API_CACHE_MAP[routeName] + if (config) { + console.log(`👤 路由 ${routeName}:`, { + apiMethod: config.apiMethod, + hasGetUsers: typeof adminApiService.getAllUsers === 'function' + }) + } + }) + + console.log('✅ API服务选择测试完成') +} + +// 测试API方法是否存在 +export function testApiMethods() { + console.log('🧪 开始测试API方法...') + + const apiServices = { + emailApiService, + adminApiService, + userApiService, + mailboxApiService + } + + Object.entries(apiServices).forEach(([serviceName, service]) => { + console.log(`📋 ${serviceName} 可用方法:`, Object.getOwnPropertyNames(service).filter(name => typeof (service as any)[name] === 'function')) + }) + + console.log('✅ API方法测试完成') +} + +// 测试新的API服务映射系统 +export function testNewApiServiceMapping() { + console.log('🧪 开始测试新的API服务映射系统...') + + // 测试每个路由的API服务选择 + Object.entries(ROUTE_API_CACHE_MAP).forEach(([routeName, config]) => { + console.log(`\n📋 测试路由: ${routeName}`) + console.log('配置:', { + apiMethod: config.apiMethod, + description: config.description, + adminOnly: config.adminOnly + }) + + // 这里可以添加更详细的测试逻辑 + // 由于 useRouteApiCacheMapper 需要在组件上下文中使用, + // 这里只是展示配置信息 + }) + + console.log('\n✅ 新的API服务映射系统测试完成') +} + +// 在开发模式下自动运行测试 +if (import.meta.env.DEV) { + console.log('🔧 开发模式:运行API服务测试') + testApiMethods() + testApiServiceSelection() + testNewApiServiceMapping() +} diff --git a/vue-legacy/src/composables/useApp.ts b/vue-legacy/src/composables/useApp.ts new file mode 100644 index 0000000..625e942 --- /dev/null +++ b/vue-legacy/src/composables/useApp.ts @@ -0,0 +1,61 @@ +// 应用状态管理组合式函数 +import { ref, computed, readonly } from 'vue' +import type { AppStage } from '@/types' + +// 应用阶段状态 +const currentStage = ref('initial-loading') +const loadingText = ref('正在初始化系统...') + +// 全局加载状态 +const globalLoading = ref(false) +const globalLoadingText = ref('') + +export const useApp = () => { + // 阶段管理 + const setStage = (stage: AppStage) => { + currentStage.value = stage + } + + const setLoadingText = (text: string) => { + loadingText.value = text + } + + // 全局加载管理 + const showGlobalLoading = (text: string = '加载中...') => { + globalLoading.value = true + globalLoadingText.value = text + } + + const hideGlobalLoading = () => { + globalLoading.value = false + globalLoadingText.value = '' + } + + // 计算属性 + const isInitialLoading = computed(() => currentStage.value === 'initial-loading') + const isAuthCheck = computed(() => currentStage.value === 'auth-check') + const isLogin = computed(() => currentStage.value === 'login') + const isMainPreload = computed(() => currentStage.value === 'main-preload') + const isMain = computed(() => currentStage.value === 'main') + + return { + // 状态 + currentStage: readonly(currentStage), + loadingText: readonly(loadingText), + globalLoading: readonly(globalLoading), + globalLoadingText: readonly(globalLoadingText), + + // 方法 + setStage, + setLoadingText, + showGlobalLoading, + hideGlobalLoading, + + // 计算属性 + isInitialLoading, + isAuthCheck, + isLogin, + isMainPreload, + isMain + } +} diff --git a/vue/src/composables/useSimplePageData.ts b/vue-legacy/src/composables/useSimplePageData.ts similarity index 100% rename from vue/src/composables/useSimplePageData.ts rename to vue-legacy/src/composables/useSimplePageData.ts diff --git a/vue-legacy/src/composables/useUnifiedPageData.ts b/vue-legacy/src/composables/useUnifiedPageData.ts new file mode 100644 index 0000000..742d655 --- /dev/null +++ b/vue-legacy/src/composables/useUnifiedPageData.ts @@ -0,0 +1,198 @@ +/** + * 统一页面数据管理 Hook + * 基于路由API缓存映射器的页面数据管理 + */ + +import { ref, computed, onMounted, onUnmounted } from 'vue' +import { useRoute } from 'vue-router' +import { useRouteApiCacheMapper } from './routeApiCacheMapper' +import { usePageRefreshRegistry, useGlobalRefreshEventListener } from './globalRefreshManager' + +// 页面数据状态 +interface PageDataState { + data: any + loading: boolean + error: string | null + lastUpdated: Date | null +} + +// 统一页面数据管理 Hook +export function useUnifiedPageData(customParams?: any) { + const route = useRoute() + const { callApiMethod, isSupportedRoute, hasPermission, getRouteInfo } = useRouteApiCacheMapper() + const { registerPageRefresh, unregisterPageRefresh } = usePageRefreshRegistry() + const { addGlobalRefreshListener, removeGlobalRefreshListener } = useGlobalRefreshEventListener() + + // 页面数据状态 + const pageData = ref({ + data: null, + loading: false, + error: null, + lastUpdated: null + }) + + // 当前路由信息 + const routeInfo = computed(() => getRouteInfo()) + + // 是否支持统一管理 + const isSupported = computed(() => isSupportedRoute.value) + + // 是否有权限 + const hasAccess = computed(() => hasPermission.value) + + // 加载数据 + const loadData = async (forceRefresh = false, params?: any) => { + if (pageData.value.loading) return + + pageData.value.loading = true + pageData.value.error = null + + try { + const finalParams = { ...customParams, ...params } + + if (isSupported.value && hasAccess.value) { + console.log('📋 使用统一管理器加载数据') + const response = await callApiMethod(finalParams, forceRefresh) + pageData.value.data = response + } else { + console.warn('当前路由不支持统一管理或权限不足') + pageData.value.error = '当前路由不支持统一管理或权限不足' + } + + pageData.value.lastUpdated = new Date() + } catch (error) { + console.error('加载数据失败:', error) + pageData.value.error = error instanceof Error ? error.message : String(error) + } finally { + pageData.value.loading = false + } + } + + // 刷新数据 + const refreshData = async (params?: any) => { + await loadData(true, params) + } + + // 页面级刷新方法 + const pageRefresh = async () => { + console.log('🔄 页面级刷新触发') + await refreshData() + } + + // 全局刷新事件处理 + const handleGlobalRefresh = (event: CustomEvent) => { + console.log('🌍 收到全局刷新事件', event.detail) + refreshData() + } + + // 页面初始化 + onMounted(() => { + // 注册页面级刷新方法 + registerPageRefresh(pageRefresh) + + // 监听全局刷新事件 + addGlobalRefreshListener(handleGlobalRefresh) + + // 初始加载数据 + loadData() + }) + + // 页面卸载 + onUnmounted(() => { + // 注销页面级刷新方法 + unregisterPageRefresh() + + // 移除全局刷新事件监听 + removeGlobalRefreshListener(handleGlobalRefresh) + }) + + return { + // 状态 + pageData: computed(() => pageData.value), + routeInfo, + isSupported, + hasAccess, + + // 方法 + loadData, + refreshData, + pageRefresh + } +} + +// 简化的页面数据管理 Hook(适用于简单页面) +export function useSimplePageData() { + const { pageData, routeInfo, isSupported, hasAccess, loadData, refreshData } = useUnifiedPageData() + + return { + // 状态 + data: computed(() => pageData.value.data), + loading: computed(() => pageData.value.loading), + error: computed(() => pageData.value.error), + lastUpdated: computed(() => pageData.value.lastUpdated), + routeInfo, + isSupported, + hasAccess, + + // 方法 + loadData, + refreshData + } +} + +// 分页数据管理 Hook +export function usePaginatedPageData(initialPage = 1, initialLimit = 20) { + const currentPage = ref(initialPage) + const pageSize = ref(initialLimit) + + const { pageData, routeInfo, isSupported, hasAccess, loadData, refreshData } = useUnifiedPageData({ + page: currentPage.value, + limit: pageSize.value + }) + + // 分页信息 + const pagination = computed(() => { + const data = pageData.value.data + if (!data || !data.data) return null + + return { + total: data.data.total || 0, + page: currentPage.value, + limit: pageSize.value, + totalPages: Math.ceil((data.data.total || 0) / pageSize.value) + } + }) + + // 切换页面 + const changePage = async (page: number) => { + currentPage.value = page + await loadData(false, { page, limit: pageSize.value }) + } + + // 改变页面大小 + const changePageSize = async (limit: number) => { + pageSize.value = limit + currentPage.value = 1 // 重置到第一页 + await loadData(false, { page: 1, limit }) + } + + return { + // 状态 + data: computed(() => pageData.value.data), + loading: computed(() => pageData.value.loading), + error: computed(() => pageData.value.error), + lastUpdated: computed(() => pageData.value.lastUpdated), + routeInfo, + isSupported, + hasAccess, + pagination, + currentPage: computed(() => currentPage.value), + pageSize: computed(() => pageSize.value), + + // 方法 + loadData, + refreshData, + changePage, + changePageSize + } +} diff --git a/vue-legacy/src/env.d.ts b/vue-legacy/src/env.d.ts new file mode 100644 index 0000000..59412c2 --- /dev/null +++ b/vue-legacy/src/env.d.ts @@ -0,0 +1,20 @@ +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} + +// 全局方法类型声明 +declare global { + interface Window { + $message: any + showGlobalLoading: (text?: string) => void + hideGlobalLoading: () => void + } + + var showGlobalLoading: (text?: string) => void + var hideGlobalLoading: () => void +} + diff --git a/vue-legacy/src/main.ts b/vue-legacy/src/main.ts new file mode 100644 index 0000000..ead8277 --- /dev/null +++ b/vue-legacy/src/main.ts @@ -0,0 +1,18 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import naive from 'naive-ui' +import router from './composables/routes' +import App from './App.vue' + +const app = createApp(App) + +// 注册 Pinia +app.use(createPinia()) + +// 注册路由 +app.use(router) + +// 注册 Naive UI +app.use(naive) + +app.mount('#app') \ No newline at end of file diff --git a/vue-legacy/src/types/index.ts b/vue-legacy/src/types/index.ts new file mode 100644 index 0000000..075f621 --- /dev/null +++ b/vue-legacy/src/types/index.ts @@ -0,0 +1,122 @@ +// 全局类型定义 +export interface UserProfile { + id: number + username: string + email: string + user_type: 0 | 1 + status?: number + created_at: string + updated_at: string + settings?: UserSettings +} + +export interface UserSettings { + webhook_url?: string | null + webhook_secret?: string | null +} + +export interface UserSettingsUpdate { + password?: string + webhook_url?: string + webhook_secret?: string +} + +export interface LoginRequest { + username: string + password: string +} + +export interface RegisterRequest { + username: string + email: string + password: string +} + +export interface LoginResponse { + success: boolean + message?: string + data?: { + token: string + user: UserProfile + } + error?: string +} + +export interface RegisterResponse { + success: boolean + message?: string + error?: string +} + +export interface ApiResponse { + success: boolean + data?: T + message?: string + error?: string +} + +export interface SystemHealth { + success: boolean + data?: { + health: { + status: number + timestamp: string + services: { + database: { + status: number + latency_ms: number + } + storage: { + status: number + provider: number + } + kv: { + status: number + provider: number + } + } + config: { + allow_registration: number + debug_mode: number + } + version: number + uptime: number + total_latency_ms: number + } + } + error?: string +} + +export interface RegistrationStatus { + success: boolean + data?: { + allow_registration: number + } + error?: string +} + +// 应用阶段类型 +export type AppStage = 'initial-loading' | 'auth-check' | 'login' | 'main-preload' | 'main' + +// 全局窗口类型扩展 +declare global { + interface Window { + showGlobalLoading: (text?: string) => void + hideGlobalLoading: () => void + CEM_CONFIG?: { + allow_registration: number + debug_mode: number + supported_domains: string[] + max_attachment_size: number + api_base_url: string + version: string + build_time: string + } + ConfigManager?: { + isRegistrationAllowed(): boolean + isDebugMode(): boolean + getSupportedDomains(): string[] + getMaxAttachmentSize(): number + } + } +} diff --git a/vue-legacy/src/utils/index.ts b/vue-legacy/src/utils/index.ts new file mode 100644 index 0000000..3259c88 --- /dev/null +++ b/vue-legacy/src/utils/index.ts @@ -0,0 +1,60 @@ +// 工具函数 +export const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) + +export const formatDate = (date: string | Date) => { + return new Intl.DateTimeFormat('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }).format(new Date(date)) +} + +export const formatFileSize = (bytes: number) => { + if (bytes === 0) return '0 Bytes' + const k = 1024 + const sizes = ['Bytes', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] +} + +export const validateEmail = (email: string) => { + const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + return re.test(email) +} + +export const validateUsername = (username: string) => { + const re = /^[a-zA-Z0-9_-]+$/ + return re.test(username) +} + +export const generateId = () => { + return Math.random().toString(36).substr(2, 9) +} + +export const debounce = any>( + func: T, + wait: number +): ((...args: Parameters) => void) => { + let timeout: NodeJS.Timeout + return (...args: Parameters) => { + clearTimeout(timeout) + timeout = setTimeout(() => func(...args), wait) + } +} + +export const throttle = any>( + func: T, + limit: number +): ((...args: Parameters) => void) => { + let inThrottle: boolean + return (...args: Parameters) => { + if (!inThrottle) { + func(...args) + inThrottle = true + setTimeout(() => (inThrottle = false), limit) + } + } +} diff --git a/vue-legacy/src/views/README.md b/vue-legacy/src/views/README.md new file mode 100644 index 0000000..3761289 --- /dev/null +++ b/vue-legacy/src/views/README.md @@ -0,0 +1,89 @@ +# 视图结构说明 + +## 目录结构 + +``` +views/ +├── auth/ # 认证相关视图 +│ ├── LoginView.vue # 登录视图 +│ └── index.ts # 认证视图导出 +├── admin/ # 管理员视图 +│ ├── emails/ # 邮件管理 +│ │ └── AllEmailsView.vue +│ ├── mailboxes/ # 邮箱管理 +│ │ ├── MailboxManagementView.vue +│ │ └── ForwardRulesView.vue +│ ├── users/ # 用户管理 +│ │ └── UsersView.vue +│ ├── settings/ # 系统设置 +│ │ ├── SystemSettingsView.vue +│ │ └── DebugView.vue +│ ├── security/ # 安全管理 +│ │ └── SecurityOverviewView.vue +│ └── index.ts # 管理员视图导出 +├── user/ # 用户视图 +│ ├── emails/ # 我的邮件 +│ │ └── MyEmailsView.vue +│ ├── mailboxes/ # 我的邮箱 +│ │ └── MyMailboxesView.vue +│ ├── settings/ # 个人设置 +│ │ └── PersonalSettingsView.vue +│ └── index.ts # 用户视图导出 +├── shared/ # 共享视图和组件 +│ ├── dashboard/ # 仪表板(登录后共享) +│ │ └── DashboardView.vue +│ ├── error/ # 错误页面 +│ │ └── NotFoundView.vue +│ ├── layouts/ # 布局组件 +│ │ └── MainLayout.vue +│ ├── components/ # 共享组件 +│ │ ├── AppLoadingSpinner.vue +│ │ ├── DataTable.vue +│ │ └── SearchBox.vue +│ └── index.ts # 共享视图和组件导出 +└── index.ts # 所有视图导出 +``` + +## 命名规范 + +### 文件命名 +- 视图文件:`[功能]View.vue`(如:`LoginView.vue`、`DashboardView.vue`) +- 组件文件:`[功能]Component.vue`(如:`DataTable.vue`、`SearchBox.vue`) +- 布局文件:`[功能]Layout.vue`(如:`MainLayout.vue`) + +### 目录命名 +- 按功能模块组织:`auth`、`admin`、`user`、`shared` +- 子目录按具体功能:`emails`、`mailboxes`、`settings`、`dashboard`等 + +### 组件命名 +- 视图组件:`[角色][功能]View`(如:`AdminAllEmailsView`、`UserMyEmailsView`) +- 共享视图:`[功能]View`(如:`DashboardView`) +- 共享组件:`[功能]Component`(如:`DataTable`、`SearchBox`) + +## 导入方式 + +### 路由中的动态导入 +```typescript +const LoginView = () => import('@/views/auth/LoginView.vue') +const DashboardView = () => import('@/views/shared/dashboard/DashboardView.vue') +const NotFoundView = () => import('@/views/shared/error/NotFoundView.vue') +const AdminAllEmailsView = () => import('@/views/admin/emails/AllEmailsView.vue') +``` + +### 组件中的导入 +```typescript +import { LoginView, DashboardView, NotFoundView, AdminAllEmailsView } from '@/views' +// 或者 +import { LoginView } from '@/views/auth' +import { DashboardView, NotFoundView } from '@/views/shared' +import { AdminAllEmailsView } from '@/views/admin' +``` + +## 优势 + +1. **清晰的层次结构**:按角色和功能模块组织,便于维护 +2. **统一的命名规范**:所有视图都使用`View`后缀,组件使用`Component`后缀 +3. **模块化设计**:每个功能模块独立,便于团队协作 +4. **易于扩展**:新增功能时只需在对应目录下添加文件 +5. **类型安全**:通过TypeScript提供完整的类型支持 +6. **合理的共享设计**:Dashboard作为登录后的共享视图,符合业务逻辑 diff --git a/vue-legacy/src/views/admin/emails/AllEmailsView.vue b/vue-legacy/src/views/admin/emails/AllEmailsView.vue new file mode 100644 index 0000000..6ced2a0 --- /dev/null +++ b/vue-legacy/src/views/admin/emails/AllEmailsView.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/vue-legacy/src/views/admin/index.ts b/vue-legacy/src/views/admin/index.ts new file mode 100644 index 0000000..492f10c --- /dev/null +++ b/vue-legacy/src/views/admin/index.ts @@ -0,0 +1,8 @@ +// 管理员视图导出 +export { default as AllEmailsView } from './emails/AllEmailsView.vue' +export { default as MailboxManagementView } from './mailboxes/MailboxManagementView.vue' +export { default as ForwardRulesView } from './mailboxes/ForwardRulesView.vue' +export { default as UsersView } from './users/UsersView.vue' +export { default as SystemSettingsView } from './settings/SystemSettingsView.vue' +export { default as DebugView } from './settings/DebugView.vue' +export { default as SecurityOverviewView } from './security/SecurityOverviewView.vue' diff --git a/vue-legacy/src/views/admin/mailboxes/ForwardRulesView.vue b/vue-legacy/src/views/admin/mailboxes/ForwardRulesView.vue new file mode 100644 index 0000000..e07e1dc --- /dev/null +++ b/vue-legacy/src/views/admin/mailboxes/ForwardRulesView.vue @@ -0,0 +1,297 @@ + + + + + \ No newline at end of file diff --git a/vue/src/views/admin/mailboxes/MailboxManagementView.vue b/vue-legacy/src/views/admin/mailboxes/MailboxManagementView.vue similarity index 80% rename from vue/src/views/admin/mailboxes/MailboxManagementView.vue rename to vue-legacy/src/views/admin/mailboxes/MailboxManagementView.vue index 2c282c2..66d6e82 100644 --- a/vue/src/views/admin/mailboxes/MailboxManagementView.vue +++ b/vue-legacy/src/views/admin/mailboxes/MailboxManagementView.vue @@ -1,13 +1,13 @@