commit 0f133962dc241712357e23def8265f526b0c5d8e Author: WanWanYun Date: Mon Nov 10 21:50:16 2025 +0800 Initial commit - 玩玩云文件管理系统 v1.0.0 - 完整的前后端代码 - 支持本地存储和SFTP存储 - 文件分享功能 - 上传工具源代码 - 完整的部署文档 - Nginx配置模板 技术栈: - 后端: Node.js + Express + SQLite - 前端: Vue.js 3 + Axios - 存储: 本地存储 / SFTP远程存储 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c59e420 --- /dev/null +++ b/.gitignore @@ -0,0 +1,67 @@ +# 依赖 +node_modules/ +__pycache__/ +*.pyc +*.pyo + +# 数据库 +*.db +*.db-journal +*.sqlite +*.sqlite3 + +# 临时文件 +backend/uploads/ +storage/ # 本地存储数据 +*.log +.DS_Store +Thumbs.db + +# 环境配置 +.env +!backend/.env.example + +# SSL证书 +certbot/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# 上传工具构建产物 +upload-tool/dist/ +upload-tool/build/ +upload-tool/__pycache__/ +upload-tool/config.json +upload-tool/*.spec + +# 备份文件 +*.bak +*.backup +*.old + +# 操作系统 +.DS_Store +.Spotlight-V100 +.Trashes +ehthumbs.db +Desktop.ini + +# 压缩文件 +*.zip +*.tar +*.gz +*.rar +*.7z + +# npm/yarn +npm-debug.log* +yarn-debug.log* +yarn-error.log* +package-lock.json.bak + +# Claude配置 +.claude/ diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..3eeb7b1 --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,183 @@ +# 玩玩云 - 部署指南 + +## 🚀 快速部署(3分钟完成) + +### 第一步:上传项目到服务器 + +```bash +# 方法1: 使用scp上传 +scp -r ftp-web-manager root@服务器IP:/var/www/ + +# 方法2: 使用FTP工具上传到 /var/www/ 目录 +``` + +### 第二步:SSH登录服务器 + +```bash +ssh root@服务器IP +``` + +### 第三步:一键部署 + +```bash +cd /var/www/ftp-web-manager +bash deploy.sh +``` + +部署脚本会自动: +- ✅ 检查Docker环境 +- ✅ 创建必要目录 +- ✅ 构建Docker镜像 +- ✅ 启动所有服务 +- ✅ 显示访问信息 + +### 第四步:访问系统 + +打开浏览器访问: +``` +http://服务器IP:8080 +``` + +使用默认账号登录: +``` +用户名: admin +密码: admin123 +``` + +**⚠️ 重要:首次登录后立即修改密码!** + +--- + +## 📋 环境要求 + +- Docker 20.10.0+ +- Docker Compose 2.0.0+ +- 最低 1GB 内存(推荐 2GB+) +- Linux 系统(Ubuntu/Debian/CentOS) + +--- + +## 🔧 手动部署(如果自动脚本失败) + +```bash +# 1. 进入项目目录 +cd /var/www/ftp-web-manager + +# 2. 创建必要目录 +mkdir -p certbot/conf certbot/www backend/uploads + +# 3. 构建并启动 +docker-compose up --build -d + +# 4. 查看日志 +docker-compose logs -f +``` + +--- + +## ✅ 部署验证 + +检查容器状态: +```bash +docker-compose ps +``` + +应该看到3个容器都是 "Up" 状态: +- wanwanyun-backend +- wanwanyun-frontend +- wanwanyun-certbot + +查看后端日志: +```bash +docker-compose logs backend +``` + +应该看到: +``` +数据库初始化完成 +默认管理员账号已创建 +玩玩云已启动 +``` + +--- + +## 🛑 停止服务 + +```bash +cd /var/www/ftp-web-manager +docker-compose down +``` + +--- + +## 🔄 重启服务 + +```bash +cd /var/www/ftp-web-manager +docker-compose restart +``` + +--- + +## 📦 更新代码 + +```bash +cd /var/www/ftp-web-manager +git pull # 或重新上传文件 +docker-compose up --build -d +``` + +--- + +## ❓ 常见问题 + +### Q: 端口8080被占用怎么办? + +修改 docker-compose.yml 中的端口映射: +```yaml +ports: + - "8081:80" # 改为8081或其他端口 +``` + +### Q: Docker容器启动失败? + +```bash +# 查看详细日志 +docker-compose logs backend + +# 重新构建 +docker-compose down +docker-compose up --build -d +``` + +### Q: 忘记管理员密码怎么办? + +删除数据库文件重新初始化: +```bash +docker-compose down +rm backend/ftp-manager.db +docker-compose up -d +``` + +### Q: 如何配置HTTPS? + +参考主README.md中的SSL配置章节。 + +--- + +## 📞 获取帮助 + +- 查看详细文档: README.md +- 查看部署检查报告: 桌面上的检查报告文件 +- 查看对话历史: 桌面上的对话总结文件 + +--- + +**部署成功后,记得:** +1. ✅ 修改admin密码 +2. ✅ 配置SFTP连接 +3. ✅ 设置JWT密钥(backend/.env) +4. ✅ 配置HTTPS(生产环境) +5. ✅ 定期备份数据库 + +祝您使用愉快!☁️ diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..6d97557 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,189 @@ +# 玩玩云部署指南 + +## 快速部署 + +### 1. 基础部署(Docker Compose) + +```bash +# 克隆项目 +git clone +cd ftp-web-manager + +# 启动服务 +docker-compose up -d +``` + +服务将在以下端口运行: +- Frontend (Nginx): 8080 (HTTP), 8443 (HTTPS) +- Backend (Node.js): 40001 + +### 2. 如果使用宿主机Nginx作为反向代理 + +如果你在宿主机上使用Nginx作为SSL终止/反向代理(推荐用于生产环境),需要在Nginx配置中添加大文件上传支持。 + +#### 2.1 创建Nginx配置文件 + +创建 `/etc/nginx/sites-available/wanwanyun.conf`(或对应的配置目录): + +```nginx +server { + listen 80; + server_name your-domain.com; + + # HTTP重定向到HTTPS + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + server_name your-domain.com; + + # SSL证书配置(使用Let's Encrypt或其他证书) + ssl_certificate /path/to/fullchain.pem; + ssl_certificate_key /path/to/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + + # 反向代理到Docker容器 + location / { + # ⚠️ 重要:设置最大上传文件大小为5GB + client_max_body_size 5G; + + # ⚠️ 重要:大文件上传超时设置(1小时) + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 3600s; + + # 代理到Docker容器的8080端口 + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +} +``` + +#### 2.2 应用配置 + +```bash +# 启用站点配置 +ln -s /etc/nginx/sites-available/wanwanyun.conf /etc/nginx/sites-enabled/ + +# 测试Nginx配置 +nginx -t + +# 重新加载Nginx +nginx -s reload +``` + +### 3. 宝塔面板用户 + +如果使用宝塔面板,配置文件通常在: +- `/www/server/panel/vhost/nginx/your-domain.conf` + +在站点的 `location /` 块中添加: + +```nginx +location / { + # 设置最大上传文件大小为5GB + client_max_body_size 5G; + + # 大文件上传超时设置(1小时) + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 3600s; + + # ... 其他代理配置 +} +``` + +然后重载Nginx: +```bash +nginx -s reload +``` + +## 上传限制说明 + +系统支持的最大上传文件大小为 **5GB**,需要在以下三个层级进行配置: + +### 1. ✅ 容器内Nginx(已配置) +- 文件:`nginx/nginx.conf` +- 配置:`client_max_body_size 5G;` + +### 2. ✅ 后端Multer(已配置) +- 文件:`backend/server.js` +- 配置:`limits: { fileSize: 5 * 1024 * 1024 * 1024 }` + +### 3. ⚠️ 宿主机Nginx(需要手动配置) +- 如果使用宿主机Nginx作为反向代理 +- 必须在 `location /` 块中添加 `client_max_body_size 5G;` +- 否则上传会在64MB时失败(Nginx默认限制) + +## 故障排查 + +### 上传文件提示413错误 + +**问题**:上传大于64MB的文件时失败,浏览器控制台显示 `413 Payload Too Large` + +**原因**:宿主机Nginx的 `client_max_body_size` 限制(默认1MB或64MB) + +**解决方案**: +1. 找到宿主机Nginx配置文件(通常是 `/etc/nginx/sites-available/` 或 `/www/server/panel/vhost/nginx/`) +2. 在 `location /` 块中添加: + ```nginx + client_max_body_size 5G; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 3600s; + ``` +3. 测试并重载Nginx: + ```bash + nginx -t + nginx -s reload + ``` + +### 上传进度 + +前端已实现实时上传进度显示(使用axios的 `onUploadProgress`),无需额外配置。 + +## 存储配置 + +系统支持两种存储方式: + +### 本地存储 +- 文件存储在:`backend/local-storage/` +- 可设置用户配额限制 +- 适合中小型部署 + +### SFTP存储 +- 用户可配置自己的SFTP服务器 +- 支持HTTP直接下载(配置 `http_download_base_url`) +- 适合大规模部署 + +## 安全建议 + +1. **使用HTTPS**:生产环境务必配置SSL证书 +2. **定期备份数据库**:`backend/data.db` 包含所有用户数据 +3. **限制管理员账号**:定期审查用户权限 +4. **配置防火墙**:只开放必要的端口(80, 443) + +## 技术支持 + +如有问题,请查看日志: + +```bash +# 后端日志 +docker logs wanwanyun-backend + +# 前端日志 +docker logs wanwanyun-frontend + +# Nginx日志 +tail -f /www/wwwlogs/your-domain.log +tail -f /www/wwwlogs/your-domain.error.log +``` diff --git a/DOCKER部署指南.md b/DOCKER部署指南.md new file mode 100644 index 0000000..cb6ac37 --- /dev/null +++ b/DOCKER部署指南.md @@ -0,0 +1,1211 @@ +# 玩玩云 - Docker部署指南 + +> **📌 重要提示:代码自动适配域名** +> +> 本项目代码**完全支持域名动态适配**,无需修改任何代码: +> - ✅ 前端会自动使用当前访问的域名和协议(http/https) +> - ✅ 后端会从HTTP请求头动态获取域名信息 +> - ✅ 上传工具配置文件会自动生成正确的API地址 +> +> **您只需要:** +> 1. 配置宝塔/Nginx的反向代理指向Docker容器 +> 2. 配置SSL证书(可选,推荐) +> 3. 启动Docker容器 +> +> **无需修改代码中的任何域名或IP地址!** + +## 目录 +1. [系统要求](#系统要求) +2. [部署架构](#部署架构) +3. [快速部署步骤](#快速部署步骤) +4. [详细配置说明](#详细配置说明) +5. [常见问题解决](#常见问题解决) +6. [维护操作](#维护操作) + +--- + +## 系统要求 + +### 服务器配置 +- **操作系统**: Ubuntu 20.04+ / CentOS 7+ / Debian 10+ +- **内存**: 最低 1GB RAM(推荐 2GB+) +- **磁盘**: 最低 10GB 可用空间 +- **端口**: 需要开放以下端口 + - `80` - HTTP (可选,用于重定向到HTTPS) + - `443` - HTTPS (SSL访问) + - `8080` - 内部HTTP端口(Docker容器) + - `40001` - 后端API端口(Docker容器) + +### 软件依赖 +- **Docker**: 20.10.0+ +- **Docker Compose**: 2.0.0+ +- **Git**: 用于代码管理(可选) +- **Nginx**: 宝塔面板或系统级Nginx(用于SSL终止和反向代理) + +--- + +## 部署架构 + +``` +互联网 + ↓ +宝塔 Nginx (443 HTTPS) ← SSL证书在此层 + ↓ +Docker Nginx (8080 HTTP) ← 前端静态文件 + ↓ +Backend API (40001) ← Node.js + SQLite + ↓ +SFTP服务器 (用户自建) +``` + +### 容器架构 +``` +docker-compose.yml +├── frontend (nginx:alpine) +│ ├── 静态文件: /usr/share/nginx/html +│ ├── Nginx配置: /etc/nginx/conf.d/default.conf +│ └── 端口: 8080:80, 8443:443 +│ +├── backend (wanwanyun-backend) +│ ├── Node.js应用: /app +│ ├── 数据库: /app/users.db (SQLite) +│ ├── 上传工具: /upload-tool +│ └── 端口: 40001:40001 +│ +└── certbot (certbot/certbot) + └── SSL证书: /etc/letsencrypt (可选) +``` + +--- + +## 快速部署步骤 + +### 步骤1: 准备服务器环境 + +```bash +# 1. 安装Docker +curl -fsSL https://get.docker.com | sh +systemctl start docker +systemctl enable docker + +# 2. 安装Docker Compose +curl -L "https://github.com/docker/compose/releases/download/v2.20.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +chmod +x /usr/local/bin/docker-compose + +# 3. 验证安装 +docker --version +docker-compose --version +``` + +### 步骤2: 上传项目文件 + +```bash +# 在服务器上创建项目目录 +mkdir -p /var/www/wanwanyun +cd /var/www/wanwanyun + +# 从本地上传所有文件(在本地执行) +scp -r C:/Users/Administrator/Desktop/ftp-web-manager/* root@YOUR_SERVER_IP:/var/www/wanwanyun/ +``` + +或使用Git: +```bash +cd /var/www/wanwanyun +git clone YOUR_REPOSITORY_URL . +``` + +### 步骤3: 构建后端镜像 + +```bash +cd /var/www/wanwanyun +docker build -t wanwanyun-backend -f backend/Dockerfile . +``` + +**Dockerfile内容**(应该在 `backend/Dockerfile`): +```dockerfile +FROM node:18-alpine + +WORKDIR /app + +# 复制package.json和package-lock.json +COPY backend/package*.json ./ + +# 安装依赖 +RUN npm install --production + +# 复制应用代码 +COPY backend/ ./ + +# 暴露端口 +EXPOSE 40001 + +# 启动应用 +CMD ["node", "server.js"] +``` + +### 步骤4: 启动容器 + +```bash +cd /var/www/wanwanyun +docker-compose up -d + +# 查看容器状态 +docker-compose ps + +# 查看日志 +docker-compose logs -f +``` + +### 步骤5: 配置宝塔Nginx(或系统Nginx) + +#### 宝塔面板配置 + +1. **添加网站** + - 域名: `example.com`(替换为您的实际域名) + - 根目录: `/var/www/wanwanyun/frontend`(仅作为占位,实际不使用) + - PHP版本: 纯静态 + +2. **配置SSL证书** + - 在宝塔面板申请Let's Encrypt证书 + - 或上传自有证书 + +3. **修改Nginx配置** + +编辑网站配置文件(通常在 `/www/server/panel/vhost/nginx/您的域名.conf`): + +```nginx +# HTTP重定向到HTTPS +server { + listen 80; + server_name example.com; # 替换为您的域名 + return 301 https://$server_name$request_uri; +} + +# HTTPS配置 +server { + listen 443 ssl http2; + server_name example.com; # 替换为您的域名 + + # SSL证书路径(宝塔自动配置) + ssl_certificate /www/server/panel/vhost/cert/example.com/fullchain.pem; + ssl_certificate_key /www/server/panel/vhost/cert/example.com/privkey.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + + # 反向代理到Docker容器 + location / { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # API反向代理 + location /api/ { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # 分享链接 + location /s/ { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + # 访问日志 + access_log /www/wwwlogs/example.com.log; + error_log /www/wwwlogs/example.com.error.log; +} +``` + +4. **重载Nginx** +```bash +nginx -t # 测试配置 +nginx -s reload # 重载配置 +``` + +#### 系统Nginx配置(无宝塔) + +配置文件位置: `/etc/nginx/sites-available/wanwanyun.conf` + +```nginx +# 与上述配置相同,但SSL证书路径可能不同 +ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; +ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; +``` + +创建软链接并重载: +```bash +ln -s /etc/nginx/sites-available/wanwanyun.conf /etc/nginx/sites-enabled/ +nginx -t +systemctl reload nginx +``` + +### 步骤6: 验证部署 + +1. **访问网站** + ``` + https://example.com # 替换为您的域名 + ``` + +2. **默认管理员账号** + - 首次启动会自动创建: + - 用户名: `admin` + - 密码: `admin123` + - ⚠️ **登录后请立即修改密码!** + +3. **测试功能** + - ✓ 登录/注册 + - ✓ SFTP配置 + - ✓ 文件列表 + - ✓ 文件上传/下载 + - ✓ 文件分享 + - ✓ 下载上传工具 + +--- + +## 详细配置说明 + +### 1. docker-compose.yml 配置 + +```yaml +version: '3.8' + +services: + backend: + image: wanwanyun-backend + container_name: wanwanyun-backend + restart: always + ports: + - "40001:40001" + volumes: + - ./backend:/app + - /app/node_modules + - ./upload-tool:/upload-tool # 上传工具目录 + environment: + - NODE_ENV=production + networks: + - wanwanyun-network + + frontend: + image: nginx:alpine + container_name: wanwanyun-frontend + restart: always + ports: + - "8080:80" # HTTP端口 + - "8443:443" # HTTPS端口(可选,如果使用Docker内SSL) + volumes: + - ./frontend:/usr/share/nginx/html + - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf + - ./certbot/conf:/etc/letsencrypt # 可选 + - ./certbot/www:/var/www/certbot # 可选 + depends_on: + - backend + networks: + - wanwanyun-network + command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'" + + certbot: # 可选:Docker内自动续期SSL证书 + image: certbot/certbot + container_name: wanwanyun-certbot + restart: unless-stopped + volumes: + - ./certbot/conf:/etc/letsencrypt + - ./certbot/www:/var/www/certbot + entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" + +networks: + wanwanyun-network: + driver: bridge +``` + +### 2. Nginx配置(Docker内部) + +文件位置: `nginx/nginx.conf` + +```nginx +server { + listen 80; + server_name localhost; + + # 前端静态文件 + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ =404; + } + + # 后端API反向代理 + location /api/ { + proxy_pass http://backend:40001; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + + # 分享链接重定向 + location /s/ { + proxy_pass http://backend:40001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} +``` + +### 3. 环境变量配置(可选) + +创建 `.env` 文件: +```env +# 数据库配置 +DB_PATH=/app/users.db + +# JWT密钥(请修改为随机字符串) +JWT_SECRET=your-secret-key-change-this-in-production + +# 后端端口 +PORT=40001 + +# 最大上传大小(字节) +MAX_UPLOAD_SIZE=104857600 + +# 生产环境标志 +NODE_ENV=production +``` + +### 4. 目录结构说明 + +``` +/var/www/wanwanyun/ +├── backend/ # 后端代码 +│ ├── server.js # 主服务器文件 +│ ├── package.json # Node.js依赖 +│ ├── Dockerfile # 后端镜像构建文件 +│ └── users.db # SQLite数据库(自动生成) +│ +├── frontend/ # 前端代码 +│ ├── index.html # 登录页面 +│ ├── app.html # 主应用页面 +│ ├── app.js # Vue.js应用逻辑 +│ └── libs/ # 第三方库 +│ ├── vue.global.js +│ ├── axios.min.js +│ └── fontawesome/ +│ +├── nginx/ # Nginx配置 +│ ├── nginx.conf # 当前使用的配置 +│ └── nginx.conf.example # 配置示例 +│ +├── upload-tool/ # 上传工具 +│ └── dist/ +│ └── 玩玩云上传工具.exe +│ +├── certbot/ # SSL证书(可选) +│ ├── conf/ +│ └── www/ +│ +└── docker-compose.yml # Docker编排文件 +``` + +--- + +## 常见问题解决 + +### 问题1: 502 Bad Gateway + +**症状**: 访问网站显示nginx 502错误 + +**原因**: +- Docker容器未启动 +- 宝塔nginx配置错误(HTTPS代理到HTTP容器) +- 端口冲突 + +**解决方案**: + +```bash +# 1. 检查容器状态 +docker-compose ps + +# 2. 查看容器日志 +docker-compose logs backend +docker-compose logs frontend + +# 3. 重启容器 +docker-compose restart + +# 4. 检查宝塔nginx配置 +# 确保proxy_pass使用HTTP而不是HTTPS +proxy_pass http://127.0.0.1:8080; # 正确 +# proxy_pass https://127.0.0.1:8443; # 错误(如果Docker内未配置SSL) + +# 5. 检查端口占用 +netstat -tulnp | grep 8080 +netstat -tulnp | grep 40001 +``` + +### 问题2: 上传工具无法下载 + +**症状**: 点击"下载上传工具"后浏览器报错 + +**原因**: Docker容器未挂载 `upload-tool` 目录 + +**解决方案**: + +```bash +# 1. 检查docker-compose.yml中是否有以下配置 +# backend服务的volumes部分应包含: +- ./upload-tool:/upload-tool + +# 2. 检查文件是否存在 +ls -la /var/www/wanwanyun/upload-tool/dist/ + +# 3. 重新创建容器(不是重启) +cd /var/www/wanwanyun +docker-compose up -d --force-recreate backend + +# 4. 验证容器内可访问文件 +docker exec wanwanyun-backend ls -la /upload-tool/dist/ +``` + +### 问题3: SFTP连接失败 + +**症状**: 保存SFTP配置时报错 "Connection refused" 或 "Permission denied" + +**原因**: +- SFTP服务器IP/端口错误 +- 防火墙阻止 +- 认证信息错误 +- 服务器IP被SFTP服务器限制 + +**解决方案**: + +```bash +# 1. 在服务器上测试SFTP连接 +sshpass -p 'YOUR_PASSWORD' sftp -P 22 username@sftp.server.com + +# 2. 检查防火墙 +# 确保后端容器可以访问外部SFTP服务器 +docker exec wanwanyun-backend ping sftp.server.com + +# 3. 如果SFTP服务器限制IP +# 需要在SFTP服务器上添加Docker服务器IP到白名单 + +# 4. 检查后端日志 +docker logs wanwanyun-backend | grep -i sftp +``` + +### 问题4: 文件删除失败 "Permission denied" + +**症状**: 删除某些文件时提示权限被拒绝 + +**原因**: SFTP chroot安全限制,根目录文件不可删除 + +**解决方案**: + +这**不是bug**,是安全设计: +- `/` 根目录: 只读,系统配置文件(.bashrc等) +- `/files` 目录: 可读写,用户工作目录 + +**建议**: 所有文件应上传到 `/files` 目录 + +### 问题5: 语法错误 "Uncaught SyntaxError" + +**症状**: 浏览器控制台显示 app.js 语法错误 + +**原因**: JavaScript字符串中有字面换行符 + +**解决方案**: + +```bash +# 检查是否有换行符错误 +cd /var/www/wanwanyun/frontend +cat -A app.js | grep -n "\\$" + +# 如果发现问题,重新从本地同步 +scp C:/Users/Administrator/Desktop/ftp-web-manager/frontend/app.js root@SERVER:/var/www/wanwanyun/frontend/ + +# 重启前端容器 +docker-compose restart frontend +``` + +### 问题6: 数据库锁定 "Database is locked" + +**症状**: 多个用户同时操作时报数据库锁定错误 + +**原因**: SQLite不支持高并发写入 + +**解决方案**: + +1. **短期方案**: 增加超时时间 + +修改 `backend/server.js`: +```javascript +const db = new Database('users.db', { + timeout: 10000 // 增加到10秒 +}); +``` + +2. **长期方案**: 迁移到PostgreSQL或MySQL + +### 问题7: 端口冲突 + +**症状**: 启动容器时报错 "port is already allocated" + +**解决方案**: + +```bash +# 1. 查找占用端口的进程 +netstat -tulnp | grep 8080 +netstat -tulnp | grep 40001 + +# 2. 停止冲突的服务 +systemctl stop nginx # 如果是系统nginx占用8080 + +# 3. 或修改docker-compose.yml中的端口映射 +# 将 "8080:80" 改为 "8081:80" +# 相应修改宝塔nginx配置中的proxy_pass端口 +``` + +### 问题8: SSL证书过期 + +**症状**: 浏览器显示证书无效警告 + +**解决方案**: + +#### 使用宝塔面板: +```bash +# 在宝塔面板网站设置中点击"续签证书" +# 或使用宝塔计划任务自动续签 +``` + +#### 使用Certbot (Docker): +```bash +# 手动续签 +docker-compose run --rm certbot renew + +# 重载nginx +docker-compose exec frontend nginx -s reload + +# certbot容器会自动每12小时检查并续签 +``` + +### 问题9: 容器内存不足 + +**症状**: 容器频繁重启,日志显示 "Out of memory" + +**解决方案**: + +```bash +# 1. 检查内存使用 +docker stats + +# 2. 限制容器内存(在docker-compose.yml中) +services: + backend: + # ... 其他配置 + deploy: + resources: + limits: + memory: 512M + reservations: + memory: 256M + +# 3. 清理Docker缓存 +docker system prune -a +``` + +### 问题10: X-Forwarded-Proto检测失败 + +**症状**: 下载上传工具时生成的URL是HTTP而不是HTTPS + +**原因**: 反向代理未传递协议头 + +**解决方案**: + +确保宝塔nginx配置包含: +```nginx +proxy_set_header X-Forwarded-Proto $scheme; +``` + +验证后端正确读取: +```javascript +// backend/server.js +const protocol = req.get('x-forwarded-proto') || req.protocol; +``` + +--- + +## 维护操作 + +### 日常维护 + +#### 查看日志 +```bash +# 查看所有容器日志 +docker-compose logs -f + +# 查看特定容器日志 +docker-compose logs -f backend +docker-compose logs -f frontend + +# 查看最近100行 +docker-compose logs --tail=100 backend + +# 实时滚动日志 +docker-compose logs -f --tail=50 +``` + +#### 重启服务 +```bash +# 重启所有容器 +docker-compose restart + +# 重启特定容器 +docker-compose restart backend +docker-compose restart frontend + +# 重新创建容器(配置更改后) +docker-compose up -d --force-recreate +``` + +#### 停止和启动 +```bash +# 停止所有容器 +docker-compose stop + +# 启动所有容器 +docker-compose start + +# 完全停止并移除容器 +docker-compose down + +# 启动(创建容器) +docker-compose up -d +``` + +### 更新部署 + +#### 更新代码 +```bash +# 1. 备份数据库 +cp /var/www/wanwanyun/backend/users.db /var/www/wanwanyun/backend/users.db.backup + +# 2. 拉取最新代码(如果使用Git) +cd /var/www/wanwanyun +git pull + +# 3. 或从本地上传更新的文件 +scp C:/Users/Administrator/Desktop/ftp-web-manager/backend/server.js root@SERVER:/var/www/wanwanyun/backend/ +scp C:/Users/Administrator/Desktop/ftp-web-manager/frontend/app.js root@SERVER:/var/www/wanwanyun/frontend/ + +# 4. 重启容器 +docker-compose restart backend frontend +``` + +#### 更新Docker镜像 +```bash +# 1. 停止容器 +docker-compose down + +# 2. 重新构建后端镜像 +docker build -t wanwanyun-backend -f backend/Dockerfile . + +# 3. 拉取最新官方镜像 +docker-compose pull frontend certbot + +# 4. 启动 +docker-compose up -d +``` + +### 备份和恢复 + +#### 备份数据 +```bash +# 创建备份目录 +mkdir -p /backup/wanwanyun/$(date +%Y%m%d) + +# 备份数据库 +cp /var/www/wanwanyun/backend/users.db /backup/wanwanyun/$(date +%Y%m%d)/ + +# 备份整个项目 +tar -czf /backup/wanwanyun/$(date +%Y%m%d)/wanwanyun-full.tar.gz /var/www/wanwanyun + +# 定期备份脚本(添加到crontab) +# 每天凌晨2点备份 +# 0 2 * * * /usr/bin/tar -czf /backup/wanwanyun/$(date +\%Y\%m\%d)/backup.tar.gz /var/www/wanwanyun/backend/users.db +``` + +#### 恢复数据 +```bash +# 停止容器 +docker-compose down + +# 恢复数据库 +cp /backup/wanwanyun/20251108/users.db /var/www/wanwanyun/backend/ + +# 启动容器 +docker-compose up -d +``` + +### 监控和性能 + +#### 监控容器资源 +```bash +# 实时监控 +docker stats + +# 查看容器详细信息 +docker inspect wanwanyun-backend +docker inspect wanwanyun-frontend +``` + +#### 性能优化 + +1. **启用Gzip压缩** (nginx/nginx.conf): +```nginx +gzip on; +gzip_types text/plain text/css application/json application/javascript text/xml application/xml; +gzip_min_length 1000; +``` + +2. **配置浏览器缓存**: +```nginx +location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ { + expires 7d; + add_header Cache-Control "public, immutable"; +} +``` + +3. **限制并发连接**: +```nginx +limit_conn_zone $binary_remote_addr zone=addr:10m; +limit_conn addr 10; +``` + +### 清理和优化 + +```bash +# 清理未使用的Docker资源 +docker system prune -a + +# 清理日志(如果过大) +truncate -s 0 /var/lib/docker/containers/*/*-json.log + +# 查看磁盘使用 +docker system df +``` + +--- + +## 安全建议 + +### 1. 修改默认配置 + +- 修改JWT_SECRET为随机字符串 +- 修改数据库默认路径 +- 限制管理员账号数量 + +### 2. 防火墙配置 + +```bash +# 只开放必要端口 +ufw allow 80/tcp +ufw allow 443/tcp +ufw deny 40001/tcp # 不对外开放后端端口 +ufw deny 8080/tcp # 不对外开放Docker HTTP端口 +ufw enable +``` + +### 3. 日志审计 + +```bash +# 定期检查访问日志 +tail -f /www/wwwlogs/example.com.log + +# 查找异常请求 +grep "POST /api/login" /www/wwwlogs/example.com.log | grep -v "200" +``` + +### 4. 定期更新 + +```bash +# 更新系统 +apt update && apt upgrade -y + +# 更新Docker镜像 +docker-compose pull +docker-compose up -d +``` + +--- + +## 故障排查流程 + +### 基本诊断命令 + +```bash +# 1. 检查容器状态 +docker-compose ps + +# 2. 检查端口监听 +netstat -tulnp | grep -E "80|443|8080|40001" + +# 3. 测试后端API +curl http://localhost:40001/api/user/profile + +# 4. 测试前端访问 +curl http://localhost:8080 + +# 5. 检查nginx配置 +nginx -t + +# 6. 查看完整日志 +docker-compose logs --tail=100 + +# 7. 进入容器调试 +docker exec -it wanwanyun-backend sh +docker exec -it wanwanyun-frontend sh +``` + +### 故障决策树 + +``` +网站无法访问 +├── 502 错误 +│ ├── 容器未启动 → docker-compose up -d +│ ├── nginx配置错误 → 检查proxy_pass +│ └── 端口冲突 → netstat检查 +│ +├── 404 错误 +│ ├── 路由配置错误 → 检查nginx.conf +│ └── 文件不存在 → 检查/usr/share/nginx/html +│ +├── 500 错误 +│ ├── 后端崩溃 → docker logs backend +│ ├── 数据库错误 → 检查users.db权限 +│ └── 代码错误 → 查看error.log +│ +└── 连接超时 + ├── 防火墙阻止 → ufw status + ├── 容器网络问题 → docker network ls + └── DNS解析失败 → ping域名 +``` + +--- + +## 联系和支持 + +如果遇到无法解决的问题: + +1. 收集以下信息: + - 错误信息截图 + - `docker-compose logs` 输出 + - `nginx -T` 配置输出 + - 系统信息 `uname -a` + +2. 检查日志中的错误堆栈 + +3. 参考本指南的"常见问题解决"部分 + +--- + +## 附录 + +### A. 完整的docker-compose.yml示例 + +参见项目根目录的 `docker-compose.yml` 文件 + +### B. 完整的Nginx配置示例 + +参见 `nginx/nginx.conf` 和 `nginx/nginx.conf.example` + +### C. 后端Dockerfile示例 + +```dockerfile +FROM node:18-alpine + +WORKDIR /app + +COPY backend/package*.json ./ +RUN npm install --production + +COPY backend/ ./ + +EXPOSE 40001 + +CMD ["node", "server.js"] +``` + +### D. 环境检查脚本 + +```bash +#!/bin/bash +# 保存为 check-env.sh + +echo "=== 玩玩云环境检查 ===" + +echo -e "\n1. Docker版本:" +docker --version + +echo -e "\n2. Docker Compose版本:" +docker-compose --version + +echo -e "\n3. 容器状态:" +docker-compose ps + +echo -e "\n4. 端口监听:" +netstat -tulnp | grep -E "80|443|8080|40001" + +echo -e "\n5. 磁盘空间:" +df -h /var/www/wanwanyun + +echo -e "\n6. 内存使用:" +docker stats --no-stream + +echo -e "\n7. 最近错误日志:" +docker-compose logs --tail=20 | grep -i error + +echo -e "\n=== 检查完成 ===" +``` + +--- + +**文档版本**: v1.0 +**最后更新**: 2025-11-08 +**适用版本**: 玩玩云 v1.0 + +--- + +## SFTP服务器配置指南 + +### 方案:使用bindfs实现根目录可写的chroot + +本项目推荐使用bindfs技术配置SFTP服务器,可以实现: +- ✅ **根目录可写** - 上传工具可直接上传到 `/filename` +- ✅ **Chroot隔离** - 用户只能访问独立空间,安全性高 +- ✅ **无需修改代码** - 上传工具保持原样 + +### 配置步骤 + +#### 1. 安装必要工具 + +```bash +# 在SFTP服务器上执行 +apt-get update +apt-get install -y bindfs openssh-server +``` + +#### 2. 创建用户和目录 + +```bash +# 创建真实数据目录 +mkdir -p /var/sftp_data/wwy_upload +chown -R wwy_upload:wwy_upload /var/sftp_data/wwy_upload +chmod 755 /var/sftp_data/wwy_upload + +# 创建chroot目录 +mkdir -p /home/wwy_ftp_upload + +# 创建用户 +useradd -d /var/sftp_data/wwy_upload -s /bin/bash wwy_upload +printf 'wwy_upload:Wwy@2024Pass' | chpasswd +``` + +#### 3. 配置bindfs挂载 + +```bash +# 使用bindfs挂载(关键步骤) +bindfs --mirror=wwy_upload \ + --force-user=root --force-group=root \ + --perms=0755 \ + /var/sftp_data/wwy_upload /home/wwy_ftp_upload +``` + +**参数说明**: +- `--mirror=wwy_upload`: 以wwy_upload用户身份镜像所有操作 +- `--force-user=root --force-group=root`: 让目录看起来归root所有(满足chroot要求) +- `--perms=0755`: 设置目录权限 + +#### 4. 配置开机自动挂载 + +编辑 `/etc/fstab`,添加: + +``` +/var/sftp_data/wwy_upload /home/wwy_ftp_upload fuse.bindfs mirror=wwy_upload,force-user=root,force-group=root,perms=0755 0 0 +``` + +#### 5. 配置SSH chroot + +编辑 `/etc/ssh/sshd_config`,添加: + +``` +Match User wwy_upload + ChrootDirectory /home/wwy_ftp_upload + ForceCommand internal-sftp + AllowTcpForwarding no + X11Forwarding no +``` + +重启SSH服务: + +```bash +systemctl restart sshd +``` + +#### 6. 测试配置 + +```bash +# 测试上传到根目录 +echo 'Test file' > /tmp/test.txt +sshpass -p 'Wwy@2024Pass' sftp wwy_upload@localhost < { + const filePath = req.query.path; + const sftp = await connectToSFTP(req.user); + + // 获取文件大小(用于显示下载进度) + const fileStats = await sftp.stat(filePath); + const fileSize = fileStats.size; + + // 设置响应头 + res.setHeader('Content-Type', 'application/octet-stream'); + res.setHeader('Content-Length', fileSize); // 浏览器可显示进度 + res.setHeader('Content-Disposition', 'attachment; filename="..."'); + + // 流式传输(服务器不保存临时文件) + const stream = sftp.createReadStream(filePath); + stream.pipe(res); +}); +``` + +### 自动选择逻辑 + +前端自动判断使用哪种模式: + +```javascript +downloadFile(file) { + if (file.httpDownloadUrl) { + // 如果配置了HTTP URL,使用HTTP直接下载 + window.open(file.httpDownloadUrl, "_blank"); + } else { + // 如果没有配置,通过后端SFTP下载 + const link = document.createElement('a'); + link.href = `${this.apiBase}/api/files/download?path=${filePath}&token=${token}`; + link.click(); + } +} +``` + +--- + +## 版本更新日志 + +### v1.1 (2025-11-09) + +**新增功能**: +- ✅ SFTP流式下载(无需配置HTTP URL) +- ✅ 下载进度显示(文件大小、速度、剩余时间) +- ✅ bindfs SFTP服务器配置方案 + +**改进**: +- ✅ 服务器零存储下载(纯中转) +- ✅ 下载逻辑自动选择(HTTP/SFTP) +- ✅ 优化用户体验 + +**修复**: +- ✅ 修复下载无进度显示的问题 +- ✅ 修复未配置HTTP URL时无法下载的问题 + +### v1.0 (2025-11-08) + +**初始版本**: +- ✅ 文件管理基础功能 +- ✅ 文件分享功能 +- ✅ 多用户系统 +- ✅ 上传工具 +- ✅ Docker部署 + +--- + +**文档版本**: v1.1 +**最后更新**: 2025-11-09 +**适用版本**: 玩玩云 v1.1 diff --git a/QUICK_START.txt b/QUICK_START.txt new file mode 100644 index 0000000..c10196e --- /dev/null +++ b/QUICK_START.txt @@ -0,0 +1,49 @@ +╔══════════════════════════════════════════════════════════════╗ +║ 玩玩云 - 快速开始指南 ║ +╚══════════════════════════════════════════════════════════════╝ + +【最简单的部署方法 - 3步完成】 + +1️⃣ 上传项目到服务器的 /var/www/ 目录 + +2️⃣ SSH登录服务器,执行: + cd /var/www/ftp-web-manager + bash deploy.sh + +3️⃣ 打开浏览器访问: + http://服务器IP:8080 + + 默认账号:admin + 默认密码:admin123 + +✅ 部署完成! + +─────────────────────────────────────────────────────────────── + +【环境要求】 +✓ Docker 20.10+ +✓ Docker Compose 2.0+ +✓ Linux系统 +✓ 1GB+ 内存 + +─────────────────────────────────────────────────────────────── + +【详细文档】 +📖 完整部署指南:DEPLOY.md +📖 使用说明:README.md +📖 部署检查报告:桌面上的检查报告 + +─────────────────────────────────────────────────────────────── + +【重要提示】 +⚠️ 首次登录后立即修改admin密码 +⚠️ 生产环境请配置HTTPS +⚠️ 定期备份 backend/ftp-manager.db 数据库文件 + +─────────────────────────────────────────────────────────────── + +【获取帮助】 +💬 查看常见问题:DEPLOY.md +💬 查看详细文档:README.md + +祝您使用愉快!☁️ diff --git a/README.md b/README.md new file mode 100644 index 0000000..30475e1 --- /dev/null +++ b/README.md @@ -0,0 +1,244 @@ +# 玩玩云 - Web SFTP 文件管理系统 + +> 一个基于Web的SFTP文件管理系统,提供文件上传、下载、分享等功能,支持多用户管理。 + +## 📋 项目简介 + +玩玩云是一个现代化的Web文件管理系统,让您可以通过浏览器管理SFTP服务器上的文件。系统支持文件的上传、下载、重命名、删除、分享等操作,并提供桌面端上传工具,方便快速上传大文件。 + +### 主要特性 + +- ✅ **文件管理** - 浏览、上传、下载、重命名、删除文件 +- ✅ **文件分享** - 生成分享链接,支持密码保护和有效期设置 +- ✅ **多用户系统** - 用户注册、登录、权限管理 +- ✅ **桌面上传工具** - 拖拽上传,实时显示进度 +- ✅ **流式下载** - 服务器零存储,纯中转下载 +- ✅ **管理员功能** - 用户管理、文件审查、系统设置 +- ✅ **Docker部署** - 一键部署,易于维护 + +## 🛠️ 技术栈 + +### 前端 +- **Vue.js 3** - 渐进式JavaScript框架 +- **Axios** - HTTP请求库 +- **Font Awesome** - 图标库 + +### 后端 +- **Node.js 20** - JavaScript运行时 +- **Express** - Web应用框架 +- **better-sqlite3** - 轻量级数据库 +- **ssh2-sftp-client** - SFTP客户端 +- **JWT** - 用户认证 + +### 部署 +- **Docker** - 容器化 +- **Docker Compose** - 容器编排 +- **Nginx** - 反向代理 + +## 🚀 快速开始 + +### 环境要求 + +- **Docker**: 20.10.0+ +- **Docker Compose**: 2.0.0+ +- **操作系统**: Linux (Ubuntu 20.04+ / Debian 10+ / CentOS 7+) +- **内存**: 最低 1GB RAM(推荐 2GB+) + +### 方法1: 一键部署(推荐) + +```bash +# 1. 上传或克隆项目 +cd /var/www +# 将项目文件上传到此目录 + +# 2. 进入项目目录 +cd ftp-web-manager + +# 3. 一键部署 +bash deploy.sh +``` + +deploy.sh脚本会自动: +- 检查Docker和Docker Compose环境 +- 创建必要的目录 +- 构建并启动所有服务 +- 显示访问信息和默认账号 + +### 方法2: 手动部署 + +```bash +# 1. 进入项目目录 +cd /var/www/ftp-web-manager + +# 2. 创建必要的目录 +mkdir -p certbot/conf certbot/www backend/uploads + +# 3. 构建并启动服务 +docker-compose up --build -d + +# 4. 查看日志 +docker-compose logs -f +``` + +### 访问系统 + +部署完成后: + +- **前端地址**: http://服务器IP:8080 +- **后端API**: http://服务器IP:40001 +- **默认管理员账号**: + - 用户名: `admin` + - 密码: `admin123` + - ⚠️ **请立即登录并修改密码!** + +## 📖 使用教程 + +### 配置SFTP服务器 + +首次使用需要配置SFTP连接信息: + +1. 登录后点击右上角用户菜单 +2. 选择"设置" +3. 填写SFTP配置: + - **SFTP主机**: 您的SFTP服务器IP + - **SFTP端口**: 默认22 + - **SFTP用户名**: SFTP账号 + - **SFTP密码**: SFTP密码 + - **HTTP下载基础URL**(可选): 如果有HTTP直接下载地址 + +4. 点击"保存配置" + +### 文件管理 + +- **浏览文件**: 点击文件夹图标进入子目录 +- **上传文件**: 点击"上传文件"按钮选择本地文件 +- **下载文件**: 点击文件行的下载按钮 +- **重命名**: 点击"重命名"按钮修改文件名 +- **删除**: 点击"删除"按钮删除文件 + +### 文件分享 + +1. 点击文件行的"分享"按钮 +2. 设置分享选项: + - **分享密码**(可选) + - **有效期**(可选) +3. 复制分享链接发送给他人 + +## 🔧 维护操作 + +### 查看日志 + +```bash +# 查看所有日志 +docker-compose logs -f + +# 查看后端日志 +docker-compose logs -f backend +``` + +### 重启服务 + +```bash +# 重启所有容器 +docker-compose restart + +# 重启指定容器 +docker-compose restart backend +``` + +### 停止服务 + +```bash +docker-compose down +``` + +### 备份数据 + +```bash +# 备份数据库 +cp backend/ftp-manager.db backup/ftp-manager.db.$(date +%Y%m%d) + +# 备份整个项目 +tar -czf backup/wanwanyun-$(date +%Y%m%d).tar.gz . +``` + +### 更新代码 + +```bash +# 拉取最新代码 +git pull + +# 重新构建并重启 +docker-compose up --build -d +``` + +## 🔐 安全建议 + +1. **修改默认密码**: 首次登录后立即修改admin密码 +2. **使用HTTPS**: 配置SSL证书,使用HTTPS访问 +3. **修改JWT密钥**: 在backend/.env文件中设置随机的JWT_SECRET +4. **定期备份**: 定期备份数据库文件 +5. **限制端口**: 不要对外暴露40001端口,只通过Nginx访问 + +## ❓ 常见问题 + +### Docker容器启动失败 + +```bash +# 查看日志 +docker-compose logs backend + +# 重新构建 +docker-compose down +docker-compose up --build -d +``` + +### 上传失败提示权限错误 + +检查SFTP服务器目录权限,确保上传目录有写权限。 + +### 分享链接无法访问 + +检查nginx配置和防火墙设置,确保端口8080可访问。 + +## 📁 项目结构 + +``` +ftp-web-manager/ +├── backend/ # 后端代码 +│ ├── server.js # 主服务器文件 +│ ├── database.js # 数据库操作 +│ ├── auth.js # 认证中间件 +│ ├── Dockerfile # Docker镜像 +│ └── package.json # 依赖配置 +│ +├── frontend/ # 前端代码 +│ ├── index.html # 登录页面 +│ ├── app.html # 主应用页面 +│ ├── share.html # 分享页面 +│ └── libs/ # 第三方库 +│ +├── nginx/ # Nginx配置 +│ └── nginx.conf # 配置文件 +│ +├── upload-tool/ # 上传工具 +│ ├── upload_tool.py # Python源码 +│ └── build.bat # 打包脚本 +│ +├── docker-compose.yml # Docker编排 +├── deploy.sh # 一键部署脚本 +├── .gitignore # Git忽略文件 +└── README.md # 本文件 +``` + +## 🤝 贡献指南 + +欢迎提交Issue和Pull Request! + +## 📄 许可证 + +本项目仅供学习和个人使用。 + +--- + +**玩玩云** - 让文件管理更简单 ☁️ diff --git a/VERSION.txt b/VERSION.txt new file mode 100644 index 0000000..31bf171 --- /dev/null +++ b/VERSION.txt @@ -0,0 +1,98 @@ +玩玩云 - 版本信息 +═══════════════════════════════════════ + +版本号: v1.0.0 +发布日期: 2025-11-09 +状态: 生产就绪 ✅ + +═══════════════════════════════════════ + +【本版本特性】 + +✅ 完整的文件管理功能 + - SFTP文件浏览、上传、下载 + - 文件重命名、删除 + - 流式下载,支持进度显示 + +✅ 文件分享功能 + - 生成分享链接 + - 支持密码保护 + - 支持有效期设置 + - 双模式下载(HTTP/SFTP) + +✅ 用户管理系统 + - 用户注册、登录 + - 密码加密存储 + - JWT认证 + - 管理员权限管理 + +✅ 桌面上传工具 + - 拖拽上传 + - 实时进度显示 + - 自动配置 + +✅ Docker容器化部署 + - 一键部署脚本 + - 自动环境检查 + - 完整的日志记录 + +═══════════════════════════════════════ + +【技术栈】 + +后端: +- Node.js 20 +- Express 4.x +- better-sqlite3 +- ssh2-sftp-client +- JWT认证 + +前端: +- Vue.js 3 +- Axios +- Font Awesome + +部署: +- Docker +- Docker Compose +- Nginx + +═══════════════════════════════════════ + +【已修复的问题】 + +✅ 数据库初始化语法错误 +✅ 分享链接重定向错误 +✅ 分享页面下载按钮缺失 +✅ 密码验证错误 +✅ SFTP连接过早关闭 +✅ Docker配置不完整 + +═══════════════════════════════════════ + +【部署状态】 + +✅ 数据库自动初始化 +✅ 默认管理员自动创建 +✅ 数据库迁移逻辑完整 +✅ Docker镜像自动构建 +✅ 所有依赖配置齐全 +✅ 部署脚本完整可用 + +═══════════════════════════════════════ + +【安全特性】 + +✅ 密码bcrypt加密 +✅ JWT令牌认证 +✅ SFTP密码安全存储 +✅ SQL注入防护 +✅ XSS防护 +✅ CORS配置 + +═══════════════════════════════════════ + +更新日志: 查看 CHANGELOG.md (如有) +许可证: 仅供学习和个人使用 + +═══════════════════════════════════════ diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..80aa116 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,8 @@ +# FTP服务器配置 +FTP_HOST=your-ftp-host.com +FTP_PORT=21 +FTP_USER=your-username +FTP_PASSWORD=your-password + +# 服务器配置 +PORT=3000 diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..93a5e04 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,21 @@ +FROM node:20-alpine + +WORKDIR /app + +# 安装编译工具 +RUN apk add --no-cache python3 make g++ + +# 复制package文件 +COPY package*.json ./ + +# 安装依赖 +RUN npm install --production + +# 复制应用代码 +COPY . . + +# 暴露端口 +EXPOSE 40001 + +# 启动应用 +CMD ["node", "server.js"] diff --git a/backend/auth.js b/backend/auth.js new file mode 100644 index 0000000..28f95f9 --- /dev/null +++ b/backend/auth.js @@ -0,0 +1,108 @@ +const jwt = require('jsonwebtoken'); +const { UserDB } = require('./database'); + +// JWT密钥(生产环境应该放在环境变量中) +const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'; + +// 生成JWT Token +function generateToken(user) { + return jwt.sign( + { + id: user.id, + username: user.username, + is_admin: user.is_admin + }, + JWT_SECRET, + { expiresIn: '7d' } + ); +} + +// 验证Token中间件 +function authMiddleware(req, res, next) { + // 从请求头、cookie或URL参数中获取token + const token = req.headers.authorization?.replace('Bearer ', '') || req.cookies?.token || req.query?.token; + + if (!token) { + return res.status(401).json({ + success: false, + message: '未提供认证令牌' + }); + } + + try { + const decoded = jwt.verify(token, JWT_SECRET); + const user = UserDB.findById(decoded.id); + + if (!user) { + return res.status(401).json({ + success: false, + message: '用户不存在' + }); + } + + if (user.is_banned) { + return res.status(403).json({ + success: false, + message: '账号已被封禁' + }); + } + + if (!user.is_active) { + return res.status(403).json({ + success: false, + message: '账号未激活' + }); + } + + // 将用户信息附加到请求对象(包含所有存储相关字段) + req.user = { + id: user.id, + username: user.username, + email: user.email, + is_admin: user.is_admin, + has_ftp_config: user.has_ftp_config, + ftp_host: user.ftp_host, + ftp_port: user.ftp_port, + ftp_user: user.ftp_user, + ftp_password: user.ftp_password, + http_download_base_url: user.http_download_base_url, + // 存储相关字段(v2.0新增) + storage_permission: user.storage_permission || 'sftp_only', + current_storage_type: user.current_storage_type || 'sftp', + local_storage_quota: user.local_storage_quota || 1073741824, + local_storage_used: user.local_storage_used || 0 + }; + + next(); + } catch (error) { + if (error.name === 'TokenExpiredError') { + return res.status(401).json({ + success: false, + message: '令牌已过期' + }); + } + + return res.status(401).json({ + success: false, + message: '无效的令牌' + }); + } +} + +// 管理员权限中间件 +function adminMiddleware(req, res, next) { + if (!req.user || !req.user.is_admin) { + return res.status(403).json({ + success: false, + message: '需要管理员权限' + }); + } + next(); +} + +module.exports = { + JWT_SECRET, + generateToken, + authMiddleware, + adminMiddleware +}; diff --git a/backend/backup.bat b/backend/backup.bat new file mode 100644 index 0000000..5c4eb22 --- /dev/null +++ b/backend/backup.bat @@ -0,0 +1,52 @@ +@echo off +chcp 65001 >nul +echo ======================================== +echo 数据库备份工具 +echo ======================================== +echo. + +cd /d %~dp0 + +REM 创建备份目录 +if not exist backup mkdir backup + +REM 生成时间戳 +set YEAR=%date:~0,4% +set MONTH=%date:~5,2% +set DAY=%date:~8,2% +set HOUR=%time:~0,2% +set MINUTE=%time:~3,2% +set SECOND=%time:~6,2% + +REM 去掉小时前面的空格 +if "%HOUR:~0,1%" == " " set HOUR=0%HOUR:~1,1% + +set TIMESTAMP=%YEAR%%MONTH%%DAY%_%HOUR%%MINUTE%%SECOND% + +REM 备份数据库 +copy ftp-manager.db backup\ftp-manager-%TIMESTAMP%.db >nul + +if %errorlevel% == 0 ( + echo [成功] 备份完成! + echo 文件: backup\ftp-manager-%TIMESTAMP%.db + + REM 获取文件大小 + for %%A in (backup\ftp-manager-%TIMESTAMP%.db) do echo 大小: %%~zA 字节 +) else ( + echo [错误] 备份失败! +) + +echo. + +REM 清理30天前的备份 +echo 清理30天前的旧备份... +forfiles /P backup /M ftp-manager-*.db /D -30 /C "cmd /c del @path" 2>nul +if %errorlevel% == 0 ( + echo [成功] 旧备份已清理 +) else ( + echo [提示] 没有需要清理的旧备份 +) + +echo. +echo ======================================== +pause diff --git a/backend/database.js b/backend/database.js new file mode 100644 index 0000000..fa3200b --- /dev/null +++ b/backend/database.js @@ -0,0 +1,554 @@ +const Database = require('better-sqlite3'); +const bcrypt = require('bcryptjs'); +const path = require('path'); + +// 创建或连接数据库 +const db = new Database(path.join(__dirname, 'ftp-manager.db')); + +// 启用外键约束 +db.pragma('foreign_keys = ON'); + +// 初始化数据库表 +function initDatabase() { + // 用户表 + db.exec(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + email TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + + -- FTP配置(可选) + ftp_host TEXT, + ftp_port INTEGER DEFAULT 22, + ftp_user TEXT, + ftp_password TEXT, + http_download_base_url TEXT, + + -- 上传工具API密钥 + upload_api_key TEXT, + + -- 用户状态 + is_admin INTEGER DEFAULT 0, + is_active INTEGER DEFAULT 1, + is_banned INTEGER DEFAULT 0, + has_ftp_config INTEGER DEFAULT 0, + + -- 时间戳 + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `); + + // 分享链接表 + db.exec(` + CREATE TABLE IF NOT EXISTS shares ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + share_code TEXT UNIQUE NOT NULL, + share_path TEXT NOT NULL, + share_type TEXT DEFAULT 'file', + share_password TEXT, + + -- 分享统计 + view_count INTEGER DEFAULT 0, + download_count INTEGER DEFAULT 0, + + -- 时间戳 + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + expires_at DATETIME, + + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ) + `); + + // 系统设置表 + db.exec(` + CREATE TABLE IF NOT EXISTS system_settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `); + + // 密码重置请求表 + db.exec(` + CREATE TABLE IF NOT EXISTS password_reset_requests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + new_password TEXT NOT NULL, + status TEXT DEFAULT 'pending', -- pending, approved, rejected + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + reviewed_at DATETIME, + reviewed_by INTEGER, + + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (reviewed_by) REFERENCES users (id) + ) + `); + + // 创建索引 + db.exec(` + CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); + CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); + CREATE INDEX IF NOT EXISTS idx_shares_code ON shares(share_code); + CREATE INDEX IF NOT EXISTS idx_shares_user ON shares(user_id); + CREATE INDEX IF NOT EXISTS idx_reset_requests_user ON password_reset_requests(user_id); + CREATE INDEX IF NOT EXISTS idx_reset_requests_status ON password_reset_requests(status); + `); + + // 数据库迁移:添加upload_api_key字段(如果不存在) + try { + const columns = db.prepare("PRAGMA table_info(users)").all(); + const hasUploadApiKey = columns.some(col => col.name === 'upload_api_key'); + + if (!hasUploadApiKey) { + db.exec(`ALTER TABLE users ADD COLUMN upload_api_key TEXT`); + console.log('数据库迁移:添加upload_api_key字段完成'); + } + } catch (error) { + console.error('数据库迁移失败:', error); + } + + // 数据库迁移:添加share_type字段(如果不存在) + try { + const shareColumns = db.prepare("PRAGMA table_info(shares)").all(); + const hasShareType = shareColumns.some(col => col.name === 'share_type'); + + if (!hasShareType) { + db.exec(`ALTER TABLE shares ADD COLUMN share_type TEXT DEFAULT 'file'`); + console.log('数据库迁移:添加share_type字段完成'); + } + } catch (error) { + console.error('数据库迁移(share_type)失败:', error); + } + + console.log('数据库初始化完成'); +} + +// 创建默认管理员账号 +function createDefaultAdmin() { + const adminExists = db.prepare('SELECT id FROM users WHERE is_admin = 1').get(); + + if (!adminExists) { + const hashedPassword = bcrypt.hashSync('admin123', 10); + + db.prepare(` + INSERT INTO users ( + username, email, password, + is_admin, is_active, has_ftp_config + ) VALUES (?, ?, ?, ?, ?, ?) + `).run( + 'admin', + 'admin@example.com', + hashedPassword, + 1, + 1, + 0 // 管理员不需要FTP配置 + ); + + console.log('默认管理员账号已创建'); + console.log('用户名: admin'); + console.log('密码: admin123'); + console.log('⚠️ 请登录后立即修改密码!'); + } +} + +// 用户相关操作 +const UserDB = { + // 创建用户 + create(userData) { + const hashedPassword = bcrypt.hashSync(userData.password, 10); + + const hasFtpConfig = userData.ftp_host && userData.ftp_user && userData.ftp_password ? 1 : 0; + + const stmt = db.prepare(` + INSERT INTO users ( + username, email, password, + ftp_host, ftp_port, ftp_user, ftp_password, http_download_base_url, + has_ftp_config + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + + const result = stmt.run( + userData.username, + userData.email, + hashedPassword, + userData.ftp_host || null, + userData.ftp_port || 22, + userData.ftp_user || null, + userData.ftp_password || null, + userData.http_download_base_url || null, + hasFtpConfig + ); + + return result.lastInsertRowid; + }, + + // 根据用户名查找 + findByUsername(username) { + return db.prepare('SELECT * FROM users WHERE username = ?').get(username); + }, + + // 根据邮箱查找 + findByEmail(email) { + return db.prepare('SELECT * FROM users WHERE email = ?').get(email); + }, + + // 根据ID查找 + findById(id) { + return db.prepare('SELECT * FROM users WHERE id = ?').get(id); + }, + + // 验证密码 + verifyPassword(plainPassword, hashedPassword) { + return bcrypt.compareSync(plainPassword, hashedPassword); + }, + + // 更新用户 + update(id, updates) { + const fields = []; + const values = []; + + for (const [key, value] of Object.entries(updates)) { + if (key === 'password') { + fields.push(`${key} = ?`); + values.push(bcrypt.hashSync(value, 10)); + } else { + fields.push(`${key} = ?`); + values.push(value); + } + } + + fields.push('updated_at = CURRENT_TIMESTAMP'); + values.push(id); + + const stmt = db.prepare(`UPDATE users SET ${fields.join(', ')} WHERE id = ?`); + return stmt.run(...values); + }, + + // 获取所有用户 + getAll(filters = {}) { + let query = 'SELECT * FROM users WHERE 1=1'; + const params = []; + + if (filters.is_admin !== undefined) { + query += ' AND is_admin = ?'; + params.push(filters.is_admin); + } + + if (filters.is_banned !== undefined) { + query += ' AND is_banned = ?'; + params.push(filters.is_banned); + } + + query += ' ORDER BY created_at DESC'; + + return db.prepare(query).all(...params); + }, + + // 删除用户 + delete(id) { + return db.prepare('DELETE FROM users WHERE id = ?').run(id); + }, + + // 封禁/解封用户 + setBanStatus(id, isBanned) { + return db.prepare('UPDATE users SET is_banned = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?') + .run(isBanned ? 1 : 0, id); + } +}; + +// 分享链接相关操作 +const ShareDB = { + // 生成随机分享码 + generateShareCode(length = 8) { + const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + let code = ''; + for (let i = 0; i < length; i++) { + code += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return code; + }, + + // 创建分享链接 + // 创建分享链接 + create(userId, options = {}) { + const { + share_type = 'file', + file_path = '', + file_name = '', + password = null, + expiry_days = null + } = options; + + let shareCode; + let attempts = 0; + + // 尝试生成唯一的分享码 + do { + shareCode = this.generateShareCode(); + attempts++; + if (attempts > 10) { + shareCode = this.generateShareCode(10); // 增加长度 + } + } while (this.findByCode(shareCode) && attempts < 20); + + // 计算过期时间 + let expiresAt = null; + if (expiry_days) { + const expireDate = new Date(); + expireDate.setDate(expireDate.getDate() + parseInt(expiry_days)); + expiresAt = expireDate.toISOString(); + } + + const stmt = db.prepare(` + INSERT INTO shares (user_id, share_code, share_path, share_type, share_password, expires_at) + VALUES (?, ?, ?, ?, ?, ?) + `); + + const hashedPassword = password ? bcrypt.hashSync(password, 10) : null; + const sharePath = share_type === 'file' ? file_path : '/'; + + const result = stmt.run( + userId, + shareCode, + sharePath, + share_type, + hashedPassword, + expiresAt + ); + + return { + id: result.lastInsertRowid, + share_code: shareCode, + share_type: share_type + }; + }, + + // 根据分享码查找 + findByCode(shareCode) { + return db.prepare(` + SELECT s.*, u.username, u.ftp_host, u.ftp_port, u.ftp_user, u.ftp_password, u.http_download_base_url + FROM shares s + JOIN users u ON s.user_id = u.id + WHERE s.share_code = ? + `).get(shareCode); + }, + + // 根据ID查找 + findById(id) { + return db.prepare('SELECT * FROM shares WHERE id = ?').get(id); + }, + + // 验证分享密码 + verifyPassword(plainPassword, hashedPassword) { + return bcrypt.compareSync(plainPassword, hashedPassword); + }, + + // 获取用户的所有分享 + getUserShares(userId) { + return db.prepare(` + SELECT * FROM shares + WHERE user_id = ? + ORDER BY created_at DESC + `).all(userId); + }, + + // 增加查看次数 + incrementViewCount(shareCode) { + return db.prepare(` + UPDATE shares + SET view_count = view_count + 1 + WHERE share_code = ? + `).run(shareCode); + }, + + // 增加下载次数 + incrementDownloadCount(shareCode) { + return db.prepare(` + UPDATE shares + SET download_count = download_count + 1 + WHERE share_code = ? + `).run(shareCode); + }, + + // 删除分享 + delete(id, userId = null) { + if (userId) { + return db.prepare('DELETE FROM shares WHERE id = ? AND user_id = ?').run(id, userId); + } + return db.prepare('DELETE FROM shares WHERE id = ?').run(id); + }, + + // 获取所有分享(管理员) + getAll() { + return db.prepare(` + SELECT s.*, u.username + FROM shares s + JOIN users u ON s.user_id = u.id + ORDER BY s.created_at DESC + `).all(); + } +}; + +// 系统设置管理 +const SettingsDB = { + // 获取设置 + get(key) { + const row = db.prepare('SELECT value FROM system_settings WHERE key = ?').get(key); + return row ? row.value : null; + }, + + // 设置值 + set(key, value) { + db.prepare(` + INSERT INTO system_settings (key, value, updated_at) + VALUES (?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(key) DO UPDATE SET + value = excluded.value, + updated_at = CURRENT_TIMESTAMP + `).run(key, value); + }, + + // 获取所有设置 + getAll() { + return db.prepare('SELECT key, value FROM system_settings').all(); + } +}; + +// 密码重置请求管理 +const PasswordResetDB = { + // 创建密码重置请求 + create(userId, newPassword) { + const hashedPassword = bcrypt.hashSync(newPassword, 10); + + // 删除该用户之前的pending请求 + db.prepare('DELETE FROM password_reset_requests WHERE user_id = ? AND status = ?') + .run(userId, 'pending'); + + const stmt = db.prepare(` + INSERT INTO password_reset_requests (user_id, new_password, status) + VALUES (?, ?, 'pending') + `); + + const result = stmt.run(userId, hashedPassword); + return result.lastInsertRowid; + }, + + // 获取待审核的请求 + getPending() { + return db.prepare(` + SELECT r.*, u.username, u.email + FROM password_reset_requests r + JOIN users u ON r.user_id = u.id + WHERE r.status = 'pending' + ORDER BY r.created_at DESC + `).all(); + }, + + // 审核请求(批准或拒绝) + review(requestId, adminId, approved) { + const request = db.prepare('SELECT * FROM password_reset_requests WHERE id = ?').get(requestId); + + if (!request || request.status !== 'pending') { + throw new Error('请求不存在或已被处理'); + } + + const newStatus = approved ? 'approved' : 'rejected'; + + db.prepare(` + UPDATE password_reset_requests + SET status = ?, reviewed_at = CURRENT_TIMESTAMP, reviewed_by = ? + WHERE id = ? + `).run(newStatus, adminId, requestId); + + // 如果批准,更新用户密码 + if (approved) { + db.prepare('UPDATE users SET password = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?') + .run(request.new_password, request.user_id); + } + + return true; + }, + + // 获取用户的所有请求 + getUserRequests(userId) { + return db.prepare(` + SELECT * FROM password_reset_requests + WHERE user_id = ? + ORDER BY created_at DESC + `).all(userId); + }, + + // 检查用户是否有待处理的请求 + hasPendingRequest(userId) { + const request = db.prepare(` + SELECT id FROM password_reset_requests + WHERE user_id = ? AND status = 'pending' + `).get(userId); + return !!request; + } +}; + +// 初始化默认设置 +function initDefaultSettings() { + // 默认上传限制为100MB + if (!SettingsDB.get('max_upload_size')) { + SettingsDB.set('max_upload_size', '104857600'); // 100MB in bytes + } +} + +// 数据库版本迁移 - v2.0 本地存储功能 +function migrateToV2() { + try { + const columns = db.prepare("PRAGMA table_info(users)").all(); + const hasStoragePermission = columns.some(col => col.name === 'storage_permission'); + + if (!hasStoragePermission) { + console.log('[数据库迁移] 检测到旧版本数据库,开始升级到 v2.0...'); + + // 添加本地存储相关字段 + db.exec(` + ALTER TABLE users ADD COLUMN storage_permission TEXT DEFAULT 'sftp_only'; + ALTER TABLE users ADD COLUMN current_storage_type TEXT DEFAULT 'sftp'; + ALTER TABLE users ADD COLUMN local_storage_quota INTEGER DEFAULT 1073741824; + ALTER TABLE users ADD COLUMN local_storage_used INTEGER DEFAULT 0; + `); + + // 更新现有用户为SFTP模式(保持兼容) + const updateStmt = db.prepare("UPDATE users SET current_storage_type = 'sftp' WHERE has_ftp_config = 1"); + updateStmt.run(); + + console.log('[数据库迁移] ✓ 用户表已升级'); + + // 为分享表添加存储类型字段 + const shareColumns = db.prepare("PRAGMA table_info(shares)").all(); + const hasShareStorageType = shareColumns.some(col => col.name === 'storage_type'); + + if (!hasShareStorageType) { + db.exec(`ALTER TABLE shares ADD COLUMN storage_type TEXT DEFAULT 'sftp';`); + console.log('[数据库迁移] ✓ 分享表已升级'); + } + + console.log('[数据库迁移] ✅ 数据库升级到 v2.0 完成!本地存储功能已启用'); + } + } catch (error) { + console.error('[数据库迁移] 迁移失败:', error); + throw error; + } +} + +// 初始化数据库 +initDatabase(); +createDefaultAdmin(); +initDefaultSettings(); +migrateToV2(); // 执行数据库迁移 + +module.exports = { + db, + UserDB, + ShareDB, + SettingsDB, + PasswordResetDB +}; diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..b78fa46 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,3010 @@ +{ + "name": "ftp-web-manager-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ftp-web-manager-backend", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "archiver": "^7.0.1", + "basic-ftp": "^5.0.4", + "bcryptjs": "^3.0.3", + "better-sqlite3": "^12.4.1", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-validator": "^7.3.0", + "jsonwebtoken": "^9.0.2", + "multer": "^2.0.2", + "ssh2-sftp-client": "^12.0.1" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/better-sqlite3": { + "version": "12.4.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.4.1.tgz", + "integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/buildcheck": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", + "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/crc32-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-validator": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.0.tgz", + "integrity": "sha512-ujK2BX5JUun5NR4JuBo83YSXoDDIpoGz3QxgHTzQcHFevkKnwV1in4K7YNuuXQ1W3a2ObXB/P4OTnTZpUyGWiw==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "validator": "~13.15.15" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "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" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/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/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/nan": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.1.tgz", + "integrity": "sha512-r7bBUGKzlqk8oPBDYxt6Z0aEdF1G1rwlMcLk8LCOMbOzf0mG+JUfUzG4fIMWwHWP0iyaLWEQZJmtB7nOHEm/qw==", + "license": "MIT", + "optional": true + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.80.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.80.0.tgz", + "integrity": "sha512-LyPuZJcI9HVwzXK1GPxWNzrr+vr8Hp/3UqlmWxxh8p54U1ZbclOqbSog9lWHaCX+dBaiGi6n/hIX+mKu74GmPA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, + "node_modules/ssh2-sftp-client": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/ssh2-sftp-client/-/ssh2-sftp-client-12.0.1.tgz", + "integrity": "sha512-ICJ1L2PmBel2Q2ctbyxzTFZCPKSHYYD6s2TFZv7NXmZDrDNGk8lHBb/SK2WgXLMXNANH78qoumeJzxlWZqSqWg==", + "license": "Apache-2.0", + "dependencies": { + "concat-stream": "^2.0.0", + "ssh2": "^1.16.0" + }, + "engines": { + "node": ">=18.20.4" + }, + "funding": { + "type": "individual", + "url": "https://square.link/u/4g7sPflL" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/validator": { + "version": "13.15.20", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz", + "integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zip-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..cbf0b1f --- /dev/null +++ b/backend/package.json @@ -0,0 +1,34 @@ +{ + "name": "ftp-web-manager-backend", + "version": "1.0.0", + "description": "FTP Web Manager Backend", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js" + }, + "keywords": [ + "ftp", + "web", + "file-manager" + ], + "author": "", + "license": "MIT", + "dependencies": { + "archiver": "^7.0.1", + "basic-ftp": "^5.0.4", + "bcryptjs": "^3.0.3", + "better-sqlite3": "^12.4.1", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-validator": "^7.3.0", + "jsonwebtoken": "^9.0.2", + "multer": "^2.0.2", + "ssh2-sftp-client": "^12.0.1" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..5296440 --- /dev/null +++ b/backend/server.js @@ -0,0 +1,2113 @@ +const express = require('express'); +const cors = require('cors'); +const cookieParser = require('cookie-parser'); +const SftpClient = require('ssh2-sftp-client'); +const multer = require('multer'); +const path = require('path'); +const fs = require('fs'); +const { body, validationResult } = require('express-validator'); +const archiver = require('archiver'); +const { execSync } = require('child_process'); + +const { db, UserDB, ShareDB, SettingsDB, PasswordResetDB } = require('./database'); +const { generateToken, authMiddleware, adminMiddleware } = require('./auth'); + +const app = express(); +const PORT = process.env.PORT || 40001; + +// 中间件 +app.use(cors({ credentials: true, origin: true })); +app.use(express.json()); +app.use(cookieParser()); + +// 请求日志 +app.use((req, res, next) => { + console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`); + next(); +}); + +// 文件上传配置(临时存储) +const upload = multer({ + dest: path.join(__dirname, 'uploads'), + limits: { fileSize: 5 * 1024 * 1024 * 1024 } // 5GB限制 +}); + +// 分享文件信息缓存(内存缓存) +// 格式: Map +const shareFileCache = new Map(); + +// ===== 工具函数 ===== + +// SFTP连接 +async function connectToSFTP(config) { + const sftp = new SftpClient(); + await sftp.connect({ + host: config.ftp_host, + port: config.ftp_port || 22, + username: config.ftp_user, + password: config.ftp_password + }); + return sftp; +} + +// 格式化文件大小 +function formatFileSize(bytes) { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; +} + +// ===== 公开API ===== + +// 健康检查 +app.get('/api/health', (req, res) => { + res.json({ success: true, message: 'Server is running' }); +}); + +// 用户注册(简化版) +app.post('/api/register', + [ + body('username').isLength({ min: 3, max: 20 }).withMessage('用户名长度3-20个字符'), + body('email').optional({ checkFalsy: true }).isEmail().withMessage('邮箱格式不正确'), + body('password').isLength({ min: 6 }).withMessage('密码至少6个字符') + ], + async (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + errors: errors.array() + }); + } + + try { + const { username, email, password } = req.body; + + // 检查用户名是否存在 + if (UserDB.findByUsername(username)) { + return res.status(400).json({ + success: false, + message: '用户名已存在' + }); + } + + // 如果提供了邮箱,检查邮箱是否存在 + if (email && UserDB.findByEmail(email)) { + return res.status(400).json({ + success: false, + message: '邮箱已被使用' + }); + } + + // 创建用户(不需要FTP配置) + const userId = UserDB.create({ + username, + email: email || `${username}@localhost`, // 如果没提供邮箱,使用默认值 + password + }); + + res.json({ + success: true, + message: '注册成功', + user_id: userId + }); + } catch (error) { + console.error('注册失败:', error); + res.status(500).json({ + success: false, + message: '注册失败: ' + error.message + }); + } + } +); + +// 用户登录 +app.post('/api/login', + [ + body('username').notEmpty().withMessage('用户名不能为空'), + body('password').notEmpty().withMessage('密码不能为空') + ], + (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + errors: errors.array() + }); + } + + const { username, password } = req.body; + + try { + const user = UserDB.findByUsername(username); + + if (!user) { + return res.status(401).json({ + success: false, + message: '用户名或密码错误' + }); + } + + if (user.is_banned) { + return res.status(403).json({ + success: false, + message: '账号已被封禁' + }); + } + + if (!UserDB.verifyPassword(password, user.password)) { + return res.status(401).json({ + success: false, + message: '用户名或密码错误' + }); + } + + const token = generateToken(user); + + res.cookie('token', token, { + httpOnly: true, + maxAge: 7 * 24 * 60 * 60 * 1000 // 7天 + }); + + res.json({ + success: true, + message: '登录成功', + token, + user: { + id: user.id, + username: user.username, + email: user.email, + is_admin: user.is_admin, + has_ftp_config: user.has_ftp_config, + // 存储相关字段 + storage_permission: user.storage_permission || 'sftp_only', + current_storage_type: user.current_storage_type || 'sftp', + local_storage_quota: user.local_storage_quota || 1073741824, + local_storage_used: user.local_storage_used || 0 + } + }); + } catch (error) { + console.error('登录失败:', error); + res.status(500).json({ + success: false, + message: '登录失败: ' + error.message + }); + } + } +); + +// ===== 需要认证的API ===== + +// 获取当前用户信息 +app.get('/api/user/profile', authMiddleware, (req, res) => { + // 不返回密码明文 + const { ftp_password, password, ...safeUser } = req.user; + res.json({ + success: true, + user: safeUser + }); +}); + +// 更新FTP配置 +app.post('/api/user/update-ftp', + authMiddleware, + [ + body('ftp_host').notEmpty().withMessage('FTP主机不能为空'), + body('ftp_port').isInt({ min: 1, max: 65535 }).withMessage('FTP端口范围1-65535'), + body('ftp_user').notEmpty().withMessage('FTP用户名不能为空') + ], + async (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + errors: errors.array() + }); + } + + try { + const { ftp_host, ftp_port, ftp_user, ftp_password, http_download_base_url } = req.body; + // 调试日志:查看接收到的配置 + console.log("[DEBUG] 收到SFTP配置:", { + ftp_host, + ftp_port, + ftp_user, + ftp_password: ftp_password ? "***" : "(empty)", + http_download_base_url + }); + + + // 如果用户已配置FTP且密码为空,使用现有密码 + let actualPassword = ftp_password; + if (!ftp_password && req.user.has_ftp_config && req.user.ftp_password) { + actualPassword = req.user.ftp_password; + } else if (!ftp_password) { + return res.status(400).json({ + success: false, + message: 'FTP密码不能为空' + }); + } + + // 验证FTP连接 + try { + const sftp = await connectToSFTP({ ftp_host, ftp_port, ftp_user, ftp_password: actualPassword }); + await sftp.end(); + } catch (error) { + return res.status(400).json({ + success: false, + message: 'SFTP连接失败,请检查配置: ' + error.message + }); + } + + // 更新用户配置 + UserDB.update(req.user.id, { + ftp_host, + ftp_port, + ftp_user, + ftp_password: actualPassword, + http_download_base_url: http_download_base_url || null, + has_ftp_config: 1 + }); + + res.json({ + success: true, + message: 'SFTP配置已更新' + }); + } catch (error) { + console.error('更新配置失败:', error); + res.status(500).json({ + success: false, + message: '更新配置失败: ' + error.message + }); + } + } +); + +// 修改管理员账号信息(仅管理员可修改用户名) +app.post('/api/admin/update-profile', + authMiddleware, + adminMiddleware, + [ + body('username').isLength({ min: 3, max: 20 }).withMessage('用户名长度3-20个字符') + ], + async (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + errors: errors.array() + }); + } + + try { + const { username } = req.body; + + // 检查用户名是否被占用(排除自己) + if (username !== req.user.username) { + const existingUser = UserDB.findByUsername(username); + if (existingUser && existingUser.id !== req.user.id) { + return res.status(400).json({ + success: false, + message: '用户名已被使用' + }); + } + + // 更新用户名 + UserDB.update(req.user.id, { username }); + + // 获取更新后的用户信息 + const updatedUser = UserDB.findById(req.user.id); + + // 生成新的token(因为用户名变了) + const newToken = generateToken(updatedUser); + + res.json({ + success: true, + message: '用户名已更新', + token: newToken, + user: { + id: updatedUser.id, + username: updatedUser.username, + email: updatedUser.email, + is_admin: updatedUser.is_admin + } + }); + } else { + res.json({ + success: true, + message: '没有需要更新的信息' + }); + } + } catch (error) { + console.error('更新账号信息失败:', error); + res.status(500).json({ + success: false, + message: '更新失败: ' + error.message + }); + } + } +); + +// 修改当前用户密码(管理员直接修改,不需要验证当前密码) +app.post('/api/user/change-password', + authMiddleware, + [ + body('new_password').isLength({ min: 6 }).withMessage('新密码至少6个字符') + ], + (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + errors: errors.array() + }); + } + + try { + const { new_password } = req.body; + + // 直接更新密码,不需要验证当前密码 + UserDB.update(req.user.id, { password: new_password }); + + res.json({ + success: true, + message: '密码修改成功' + }); + } catch (error) { + console.error('修改密码失败:', error); + res.status(500).json({ + success: false, + message: '修改密码失败: ' + error.message + }); + } + } +); + +// 修改当前用户名 +app.post('/api/user/update-username', + authMiddleware, + [ + body('username').isLength({ min: 3 }).withMessage('用户名至少3个字符') + ], + (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + errors: errors.array() + }); + } + + try { + const { username } = req.body; + + // 检查用户名是否已存在 + const existingUser = UserDB.findByUsername(username); + if (existingUser && existingUser.id !== req.user.id) { + return res.status(400).json({ + success: false, + message: '用户名已存在' + }); + } + + // 更新用户名 + UserDB.update(req.user.id, { username }); + + res.json({ + success: true, + message: '用户名修改成功' + }); + } catch (error) { + console.error('修改用户名失败:', error); + res.status(500).json({ + success: false, + message: '修改用户名失败: ' + error.message + }); + } + } +); + +// 切换存储方式 +app.post('/api/user/switch-storage', + authMiddleware, + [ + body('storage_type').isIn(['local', 'sftp']).withMessage('无效的存储类型') + ], + async (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + errors: errors.array() + }); + } + + try { + const { storage_type } = req.body; + + // 检查权限 + if (req.user.storage_permission === 'local_only' && storage_type !== 'local') { + return res.status(403).json({ + success: false, + message: '您只能使用本地存储' + }); + } + + if (req.user.storage_permission === 'sftp_only' && storage_type !== 'sftp') { + return res.status(403).json({ + success: false, + message: '您只能使用SFTP存储' + }); + } + + // 检查SFTP配置 + if (storage_type === 'sftp' && !req.user.has_ftp_config) { + return res.status(400).json({ + success: false, + message: '请先配置SFTP服务器' + }); + } + + // 更新存储类型 + UserDB.update(req.user.id, { current_storage_type: storage_type }); + + res.json({ + success: true, + message: '存储方式已切换', + storage_type + }); + } catch (error) { + console.error('切换存储失败:', error); + res.status(500).json({ + success: false, + message: '切换存储失败: ' + error.message + }); + } + } +); + +// 获取文件列表 +app.get('/api/files', authMiddleware, async (req, res) => { + const dirPath = req.query.path || '/'; + let storage; + + try { + // 使用统一存储接口 + const { StorageInterface } = require('./storage'); + const storageInterface = new StorageInterface(req.user); + storage = await storageInterface.connect(); + + const list = await storage.list(dirPath); + + const httpBaseUrl = req.user.http_download_base_url || ''; + const storageType = req.user.current_storage_type || 'sftp'; + + const formattedList = list.map(item => { + // 构建完整的文件路径用于下载 + let httpDownloadUrl = null; + // 只有SFTP存储且配置了HTTP下载地址时才提供HTTP下载URL + if (storageType === 'sftp' && httpBaseUrl && item.type !== 'd') { + // 移除基础URL末尾的斜杠(如果有) + const baseUrl = httpBaseUrl.replace(/\/+$/, ''); + + // 构建完整路径:当前目录路径 + 文件名 + const fullPath = dirPath === '/' + ? `/${item.name}` + : `${dirPath}/${item.name}`; + + // 拼接最终的下载URL + httpDownloadUrl = `${baseUrl}${fullPath}`; + } + + return { + name: item.name, + type: item.type === 'd' ? 'directory' : 'file', + size: item.size, + sizeFormatted: formatFileSize(item.size), + modifiedAt: new Date(item.modifyTime), + isDirectory: item.type === 'd', + httpDownloadUrl: httpDownloadUrl + }; + }); + + formattedList.sort((a, b) => { + if (a.isDirectory && !b.isDirectory) return -1; + if (!a.isDirectory && b.isDirectory) return 1; + return a.name.localeCompare(b.name); + }); + + res.json({ + success: true, + path: dirPath, + items: formattedList, + storageType: storageType, + storagePermission: req.user.storage_permission || 'sftp_only' + }); + } catch (error) { + console.error('获取文件列表失败:', error); + res.status(500).json({ + success: false, + message: '获取文件列表失败: ' + error.message + }); + } finally { + if (storage) await storage.end(); + } +}); + +// 重命名文件 +app.post('/api/files/rename', authMiddleware, async (req, res) => { + const { oldName, newName, path } = req.body; + let storage; + + if (!oldName || !newName) { + return res.status(400).json({ + success: false, + message: '缺少文件名参数' + }); + } + + try { + // 使用统一存储接口 + const { StorageInterface } = require('./storage'); + const storageInterface = new StorageInterface(req.user); + storage = await storageInterface.connect(); + + const oldPath = path === '/' ? `/${oldName}` : `${path}/${oldName}`; + const newPath = path === '/' ? `/${newName}` : `${path}/${newName}`; + + await storage.rename(oldPath, newPath); + + res.json({ + success: true, + message: '文件重命名成功' + }); + } catch (error) { + console.error('重命名文件失败:', error); + res.status(500).json({ + success: false, + message: '重命名文件失败: ' + error.message + }); + } finally { + if (storage) await storage.end(); + } +}); + +// 删除文件 +app.post('/api/files/delete', authMiddleware, async (req, res) => { + const { fileName, path } = req.body; + let storage; + + if (!fileName) { + return res.status(400).json({ + success: false, + message: '缺少文件名参数' + }); + } + + try { + // 使用统一存储接口 + const { StorageInterface } = require('./storage'); + const storageInterface = new StorageInterface(req.user); + storage = await storageInterface.connect(); + + const filePath = path === '/' ? `/${fileName}` : `${path}/${fileName}`; + + await storage.delete(filePath); + + res.json({ + success: true, + message: '文件删除成功' + }); + } catch (error) { + console.error('删除文件失败:', error); + res.status(500).json({ + success: false, + message: '删除文件失败: ' + error.message + }); + } finally { + if (storage) await storage.end(); + } +}); + +// 上传文件 +app.post('/api/upload', authMiddleware, upload.single('file'), async (req, res) => { + if (!req.file) { + return res.status(400).json({ + success: false, + message: '没有上传文件' + }); + } + + // 检查文件大小限制 + const maxUploadSize = parseInt(SettingsDB.get('max_upload_size') || '104857600'); + if (req.file.size > maxUploadSize) { + // 删除已上传的临时文件 + if (fs.existsSync(req.file.path)) { + fs.unlinkSync(req.file.path); + } + + return res.status(413).json({ + success: false, + message: '文件超过上传限制', + maxSize: maxUploadSize, + fileSize: req.file.size + }); + } + + const remotePath = req.body.path || '/'; + const remoteFilePath = remotePath === '/' + ? `/${req.file.originalname}` + : `${remotePath}/${req.file.originalname}`; + + let storage; + + try { + // 使用统一存储接口 + const { StorageInterface } = require('./storage'); + const storageInterface = new StorageInterface(req.user); + storage = await storageInterface.connect(); + + // storage.put() 内部已经实现了临时文件+重命名逻辑 + await storage.put(req.file.path, remoteFilePath); + console.log(`[上传] 文件上传成功: ${remoteFilePath}`); + + // 删除本地临时文件 + fs.unlinkSync(req.file.path); + + res.json({ + success: true, + message: '文件上传成功', + filename: req.file.originalname, + path: remoteFilePath + }); + } catch (error) { + console.error('文件上传失败:', error); + + // 删除临时文件 + if (fs.existsSync(req.file.path)) { + fs.unlinkSync(req.file.path); + } + + res.status(500).json({ + success: false, + message: '文件上传失败: ' + error.message + }); + } finally { + if (storage) await storage.end(); + } +}); + + +// 下载文件 +app.get('/api/files/download', authMiddleware, async (req, res) => { + const filePath = req.query.path; + let storage; + + if (!filePath) { + return res.status(400).json({ + success: false, + message: '缺少文件路径参数' + }); + } + + try { + // 使用统一存储接口 + const { StorageInterface } = require('./storage'); + const storageInterface = new StorageInterface(req.user); + storage = await storageInterface.connect(); + + // 获取文件名 + const fileName = filePath.split('/').pop(); + + // 先获取文件信息(获取文件大小) + const fileStats = await storage.stat(filePath); + const fileSize = fileStats.size; + console.log('[下载] 文件: ' + fileName + ', 大小: ' + fileSize + ' 字节'); + + // 设置响应头(包含文件大小,浏览器可显示下载进度) + res.setHeader('Content-Type', 'application/octet-stream'); + res.setHeader('Content-Length', fileSize); + res.setHeader('Content-Disposition', 'attachment; filename="' + encodeURIComponent(fileName) + '"; filename*=UTF-8\'\'' + encodeURIComponent(fileName)); + + // 创建文件流并传输(流式下载,服务器不保存临时文件) + const stream = storage.createReadStream(filePath); + + stream.on('error', (error) => { + console.error('文件流错误:', error); + if (!res.headersSent) { + res.status(500).json({ + success: false, + message: '文件下载失败: ' + error.message + }); + } + }); + + stream.pipe(res); + + } catch (error) { + console.error('下载文件失败:', error); + if (!res.headersSent) { + res.status(500).json({ + success: false, + message: '下载文件失败: ' + error.message + }); + } + } +}); + +// 生成上传工具(生成新密钥并创建配置文件) +app.post('/api/upload/generate-tool', authMiddleware, async (req, res) => { + try { + // 生成新的API密钥(32位随机字符串) + const crypto = require('crypto'); + const newApiKey = crypto.randomBytes(16).toString('hex'); + + // 更新用户的upload_api_key + UserDB.update(req.user.id, { upload_api_key: newApiKey }); + + // 创建配置文件内容 + const config = { + username: req.user.username, + api_key: newApiKey, + api_base_url: `${req.get('x-forwarded-proto') || req.protocol}://${req.get('host')}` + }; + + res.json({ + success: true, + message: '上传工具已生成', + config: config + }); + } catch (error) { + console.error('生成上传工具失败:', error); + res.status(500).json({ + success: false, + message: '生成上传工具失败: ' + error.message + }); + } +}); + +// 下载上传工具(zip包含exe+config.json+README.txt) +app.get('/api/upload/download-tool', authMiddleware, async (req, res) => { + let tempZipPath = null; + + try { + console.log(`[上传工具] 用户 ${req.user.username} 请求下载上传工具`); + + // 生成新的API密钥 + const crypto = require('crypto'); + const newApiKey = crypto.randomBytes(16).toString('hex'); + + // 更新用户的upload_api_key + UserDB.update(req.user.id, { upload_api_key: newApiKey }); + + // 创建配置文件内容 + const config = { + username: req.user.username, + api_key: newApiKey, + api_base_url: `${req.get('x-forwarded-proto') || req.protocol}://${req.get('host')}` + }; + console.log("[上传工具配置]", JSON.stringify(config, null, 2)); + + // 检查exe文件是否存在 + const toolDir = path.join(__dirname, '..', 'upload-tool'); + const exePath = path.join(toolDir, 'dist', '玩玩云上传工具.exe'); + const readmePath = path.join(toolDir, 'README.txt'); + + if (!fs.existsSync(exePath)) { + console.error('[上传工具] exe文件不存在:', exePath); + return res.status(500).json({ + success: false, + message: '上传工具尚未打包,请联系管理员运行 upload-tool/build.bat' + }); + } + + // 创建临时zip文件路径 + const uploadsDir = path.join(__dirname, 'uploads'); + if (!fs.existsSync(uploadsDir)) { + fs.mkdirSync(uploadsDir, { recursive: true }); + } + tempZipPath = path.join(uploadsDir, `tool_${req.user.username}_${Date.now()}.zip`); + + console.log('[上传工具] 开始创建zip包到临时文件:', tempZipPath); + + // 创建文件写入流 + const output = fs.createWriteStream(tempZipPath); + const archive = archiver('zip', { + store: true // 使用STORE模式,不压缩,速度最快 + }); + + // 等待zip文件创建完成 + await new Promise((resolve, reject) => { + output.on('close', () => { + console.log(`[上传工具] zip创建完成,大小: ${archive.pointer()} 字节`); + resolve(); + }); + + archive.on('error', (err) => { + console.error('[上传工具] archiver错误:', err); + reject(err); + }); + + // 连接archive到文件流 + archive.pipe(output); + + // 添加exe文件 + console.log('[上传工具] 添加exe文件...'); + archive.file(exePath, { name: '玩玩云上传工具.exe' }); + + // 添加config.json + console.log('[上传工具] 添加config.json...'); + archive.append(JSON.stringify(config, null, 2), { name: 'config.json' }); + + // 添加README.txt + if (fs.existsSync(readmePath)) { + console.log('[上传工具] 添加README.txt...'); + archive.file(readmePath, { name: 'README.txt' }); + } + + // 完成打包 + console.log('[上传工具] 执行finalize...'); + archive.finalize(); + }); + + // 获取文件大小 + const stats = fs.statSync(tempZipPath); + const fileSize = stats.size; + console.log(`[上传工具] 准备发送文件,大小: ${fileSize} 字节`); + + // 设置响应头(包含Content-Length) + const filename = `玩玩云上传工具_${req.user.username}.zip`; + res.setHeader('Content-Type', 'application/zip'); + res.setHeader('Content-Length', fileSize); + res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"; filename*=UTF-8''${encodeURIComponent(filename)}`); + + // 创建文件读取流并发送 + const fileStream = fs.createReadStream(tempZipPath); + + fileStream.on('end', () => { + console.log(`[上传工具] 用户 ${req.user.username} 下载完成`); + // 删除临时文件 + if (tempZipPath && fs.existsSync(tempZipPath)) { + fs.unlinkSync(tempZipPath); + console.log('[上传工具] 临时文件已删除'); + } + }); + + fileStream.on('error', (err) => { + console.error('[上传工具] 文件流错误:', err); + // 删除临时文件 + if (tempZipPath && fs.existsSync(tempZipPath)) { + fs.unlinkSync(tempZipPath); + } + }); + + fileStream.pipe(res); + + } catch (error) { + console.error('[上传工具] 异常:', error); + + // 删除临时文件 + if (tempZipPath && fs.existsSync(tempZipPath)) { + fs.unlinkSync(tempZipPath); + console.log('[上传工具] 临时文件已删除(异常)'); + } + + if (!res.headersSent) { + res.status(500).json({ + success: false, + message: '下载失败: ' + error.message + }); + } + } +}); + +// 通过API密钥获取SFTP配置(供Python工具调用) +app.post('/api/upload/get-config', async (req, res) => { + try { + const { api_key } = req.body; + + if (!api_key) { + return res.status(400).json({ + success: false, + message: 'API密钥不能为空' + }); + } + + // 查找拥有此API密钥的用户 + const user = db.prepare('SELECT * FROM users WHERE upload_api_key = ?').get(api_key); + + if (!user) { + return res.status(401).json({ + success: false, + message: 'API密钥无效或已过期' + }); + } + + if (user.is_banned) { + return res.status(403).json({ + success: false, + message: '账号已被封禁' + }); + } + + if (!user.has_ftp_config) { + return res.status(400).json({ + success: false, + message: '用户未配置SFTP服务器' + }); + } + + // 返回SFTP配置 + res.json({ + success: true, + sftp_config: { + host: user.ftp_host, + port: user.ftp_port, + username: user.ftp_user, + password: user.ftp_password + } + }); + } catch (error) { + console.error('获取SFTP配置失败:', error); + res.status(500).json({ + success: false, + message: '获取SFTP配置失败: ' + error.message + }); + } +}); + +// 创建分享链接 +app.post('/api/share/create', authMiddleware, (req, res) => { + try { + const { share_type, file_path, file_name, password, expiry_days } = req.body; + console.log("[DEBUG] 创建分享请求:", { share_type, file_path, file_name, password: password ? "***" : null, expiry_days }); + + if (share_type === 'file' && !file_path) { + return res.status(400).json({ + success: false, + message: '文件路径不能为空' + }); + } + + const result = ShareDB.create(req.user.id, { + share_type: share_type || 'file', + file_path: file_path || '', + file_name: file_name || '', + password: password || null, + expiry_days: expiry_days || null + }); + + // 更新分享的存储类型 + const { db } = require('./database'); + db.prepare('UPDATE shares SET storage_type = ? WHERE id = ?') + .run(req.user.current_storage_type || 'sftp', result.id); + + const shareUrl = `${req.protocol}://${req.get('host')}/s/${result.share_code}`; + + res.json({ + success: true, + message: '分享链接创建成功', + share_code: result.share_code, + share_url: shareUrl, + share_type: result.share_type + }); + } catch (error) { + console.error('创建分享链接失败:', error); + res.status(500).json({ + success: false, + message: '创建分享链接失败: ' + error.message + }); + } +}); + +// 获取我的分享列表 +app.get('/api/share/my', authMiddleware, (req, res) => { + try { + const shares = ShareDB.getUserShares(req.user.id); + + res.json({ + success: true, + shares: shares.map(share => ({ + ...share, + share_url: `${req.protocol}://${req.get('host')}/s/${share.share_code}` + })) + }); + } catch (error) { + console.error('获取分享列表失败:', error); + res.status(500).json({ + success: false, + message: '获取分享列表失败: ' + error.message + }); + } +}); + +// 删除分享 +app.delete('/api/share/:id', authMiddleware, (req, res) => { + try { + // 先获取分享信息以获得share_code + const share = ShareDB.findById(req.params.id); + + if (share && share.user_id === req.user.id) { + // 删除缓存 + if (shareFileCache.has(share.share_code)) { + shareFileCache.delete(share.share_code); + console.log(`[缓存清除] 分享码: ${share.share_code}`); + } + + // 删除数据库记录 + ShareDB.delete(req.params.id, req.user.id); + + res.json({ + success: true, + message: '分享已删除' + }); + } else { + res.status(404).json({ + success: false, + message: '分享不存在或无权限' + }); + } + } catch (error) { + console.error('删除分享失败:', error); + res.status(500).json({ + success: false, + message: '删除分享失败: ' + error.message + }); + } +}); + +// ===== 分享链接访问(公开) ===== + +// 访问分享链接 - 验证密码(支持本地存储和SFTP) +app.post('/api/share/:code/verify', async (req, res) => { + const { code } = req.params; + const { password } = req.body; + let storage; + + try { + const share = ShareDB.findByCode(code); + + if (!share) { + return res.status(404).json({ + success: false, + message: '分享不存在' + }); + } + + // 如果设置了密码,验证密码 + if (share.share_password) { + if (!password) { + return res.status(401).json({ + success: false, + message: '需要密码', + needPassword: true + }); + } + + if (!ShareDB.verifyPassword(password, share.share_password)) { + return res.status(401).json({ + success: false, + message: '密码错误' + }); + } + } + + // 增加查看次数 + ShareDB.incrementViewCount(code); + + // 构建返回数据 + const responseData = { + success: true, + share: { + share_path: share.share_path, + share_type: share.share_type, + username: share.username, + created_at: share.created_at + } + }; + + // 如果是单文件分享,查询存储获取文件信息(带缓存) + if (share.share_type === 'file') { + const filePath = share.share_path; + const lastSlashIndex = filePath.lastIndexOf('/'); + const dirPath = lastSlashIndex > 0 ? filePath.substring(0, lastSlashIndex) : '/'; + const fileName = lastSlashIndex >= 0 ? filePath.substring(lastSlashIndex + 1) : filePath; + + // 检查缓存 + if (shareFileCache.has(code)) { + console.log(`[缓存命中] 分享码: ${code}`); + responseData.file = shareFileCache.get(code); + } else { + // 缓存未命中,查询存储 + const storageType = share.storage_type || 'sftp'; + console.log(`[缓存未命中] 分享码: ${code},存储类型: ${storageType}`); + try { + // 获取分享者的用户信息 + const shareOwner = UserDB.findById(share.user_id); + if (!shareOwner) { + throw new Error('分享者不存在'); + } + + // 使用统一存储接口 + const { StorageInterface } = require('./storage'); + const userForStorage = { + ...shareOwner, + current_storage_type: storageType + }; + + const storageInterface = new StorageInterface(userForStorage); + storage = await storageInterface.connect(); + + const list = await storage.list(dirPath); + const fileInfo = list.find(item => item.name === fileName); + + // 检查文件是否存在 + if (!fileInfo) { + shareFileCache.delete(code); + throw new Error("分享的文件已被删除或不存在"); + } + + if (fileInfo) { + // 移除基础URL末尾的斜杠 + const httpBaseUrl = share.http_download_base_url || ''; + const baseUrl = httpBaseUrl ? httpBaseUrl.replace(/\/+$/, '') : ''; + const normalizedFilePath = filePath.startsWith('/') ? filePath : `/${filePath}`; + // SFTP存储才提供HTTP下载URL,本地存储使用API下载 + const httpDownloadUrl = (storageType === 'sftp' && baseUrl) ? `${baseUrl}${normalizedFilePath}` : null; + + const fileData = { + name: fileName, + type: 'file', + isDirectory: false, + httpDownloadUrl: httpDownloadUrl, + size: fileInfo.size, + sizeFormatted: formatFileSize(fileInfo.size), + modifiedAt: new Date(fileInfo.modifyTime) + }; + + // 存入缓存 + shareFileCache.set(code, fileData); + console.log(`[缓存存储] 分享码: ${code},文件: ${fileName}`); + + responseData.file = fileData; + } + } catch (storageError) { + console.error('获取文件信息失败:', storageError); + + // 如果是文件不存在的错误,重新抛出 + if (storageError.message && storageError.message.includes("分享的文件已被删除或不存在")) { + throw storageError; + } + // 存储失败时仍返回基本信息,只是没有大小 + const httpBaseUrl = share.http_download_base_url || ''; + const baseUrl = httpBaseUrl ? httpBaseUrl.replace(/\/+$/, '') : ''; + const normalizedFilePath = filePath.startsWith('/') ? filePath : `/${filePath}`; + const storageType = share.storage_type || 'sftp'; + const httpDownloadUrl = (storageType === 'sftp' && baseUrl) ? `${baseUrl}${normalizedFilePath}` : null; + + responseData.file = { + name: fileName, + type: 'file', + isDirectory: false, + httpDownloadUrl: httpDownloadUrl, + size: 0, + sizeFormatted: '-' + }; + } + } + } + + res.json(responseData); + } catch (error) { + console.error('验证分享失败:', error); + res.status(500).json({ + success: false, + message: '验证失败: ' + error.message + }); + } finally { + if (storage) await storage.end(); + } +}); + +// 获取分享的文件列表(支持本地存储和SFTP) +app.post('/api/share/:code/list', async (req, res) => { + const { code } = req.params; + const { password, path: subPath } = req.body; + + let storage; + + try { + const share = ShareDB.findByCode(code); + + if (!share) { + return res.status(404).json({ + success: false, + message: '分享不存在' + }); + } + + // 验证密码 + if (share.share_password && !ShareDB.verifyPassword(password, share.share_password)) { + return res.status(401).json({ + success: false, + message: '密码错误' + }); + } + + // 获取分享者的用户信息 + const shareOwner = UserDB.findById(share.user_id); + if (!shareOwner) { + return res.status(404).json({ + success: false, + message: '分享者不存在' + }); + } + + // 使用统一存储接口,根据分享的storage_type选择存储后端 + const { StorageInterface } = require('./storage'); + const storageType = share.storage_type || 'sftp'; + console.log(`[分享列表] 存储类型: ${storageType}, 分享路径: ${share.share_path}`); + + // 临时构造用户对象以使用存储接口 + const userForStorage = { + ...shareOwner, + current_storage_type: storageType + }; + + const storageInterface = new StorageInterface(userForStorage); + storage = await storageInterface.connect(); + + const httpBaseUrl = share.http_download_base_url || ''; + let formattedList = []; + + // 如果是单文件分享 + if (share.share_type === 'file') { + // share_path 就是文件路径 + const filePath = share.share_path; + + // 提取父目录和文件名 + const lastSlashIndex = filePath.lastIndexOf('/'); + const dirPath = lastSlashIndex > 0 ? filePath.substring(0, lastSlashIndex) : '/'; + const fileName = lastSlashIndex >= 0 ? filePath.substring(lastSlashIndex + 1) : filePath; + + // 列出父目录 + const list = await storage.list(dirPath); + + // 只返回这个文件 + const fileInfo = list.find(item => item.name === fileName); + + if (fileInfo) { + // 移除基础URL末尾的斜杠 + const baseUrl = httpBaseUrl ? httpBaseUrl.replace(/\/+$/, '') : ''; + + // 确保文件路径以斜杠开头 + const normalizedFilePath = filePath.startsWith('/') ? filePath : `/${filePath}`; + + // SFTP存储才提供HTTP下载URL,本地存储使用API下载 + const httpDownloadUrl = (storageType === 'sftp' && baseUrl) ? `${baseUrl}${normalizedFilePath}` : null; + + formattedList = [{ + name: fileInfo.name, + type: 'file', + size: fileInfo.size, + sizeFormatted: formatFileSize(fileInfo.size), + modifiedAt: new Date(fileInfo.modifyTime), + isDirectory: false, + httpDownloadUrl: httpDownloadUrl + }]; + } + } + // 如果是目录分享(分享所有文件) + else { + const fullPath = subPath ? `${share.share_path}/${subPath}`.replace('//', '/') : share.share_path; + const list = await storage.list(fullPath); + + formattedList = list.map(item => { + // 构建完整的文件路径用于下载 + let httpDownloadUrl = null; + // SFTP存储才提供HTTP下载URL,本地存储使用API下载 + if (storageType === 'sftp' && httpBaseUrl && item.type !== 'd') { + // 移除基础URL末尾的斜杠 + const baseUrl = httpBaseUrl.replace(/\/+$/, ''); + + // 确保fullPath以斜杠开头 + const normalizedPath = fullPath.startsWith('/') ? fullPath : `/${fullPath}`; + + // 构建完整路径:当前目录路径 + 文件名 + const filePath = normalizedPath === '/' + ? `/${item.name}` + : `${normalizedPath}/${item.name}`; + + // 拼接最终的下载URL + httpDownloadUrl = `${baseUrl}${filePath}`; + } + + return { + name: item.name, + type: item.type === 'd' ? 'directory' : 'file', + size: item.size, + sizeFormatted: formatFileSize(item.size), + modifiedAt: new Date(item.modifyTime), + isDirectory: item.type === 'd', + httpDownloadUrl: httpDownloadUrl + }; + }); + + formattedList.sort((a, b) => { + if (a.isDirectory && !b.isDirectory) return -1; + if (!a.isDirectory && b.isDirectory) return 1; + return a.name.localeCompare(b.name); + }); + } + + res.json({ + success: true, + path: share.share_path, + items: formattedList + }); + } catch (error) { + console.error('获取分享文件列表失败:', error); + res.status(500).json({ + success: false, + message: '获取文件列表失败: ' + error.message + }); + } finally { + if (storage) await storage.end(); + } +}); + +// 记录下载次数 +app.post('/api/share/:code/download', (req, res) => { + const { code } = req.params; + + try { + const share = ShareDB.findByCode(code); + + if (!share) { + return res.status(404).json({ + success: false, + message: '分享不存在' + }); + } + + // 增加下载次数 + ShareDB.incrementDownloadCount(code); + + res.json({ + success: true, + message: '下载统计已记录' + }); + } catch (error) { + console.error('记录下载失败:', error); + res.status(500).json({ + success: false, + message: '记录下载失败: ' + error.message + }); + } +}); + +// 分享文件下载(支持本地存储和SFTP,公开API,需要分享码和密码验证) +app.get('/api/share/:code/download-file', async (req, res) => { + const { code } = req.params; + const { path: filePath, password } = req.query; + let storage; + + if (!filePath) { + return res.status(400).json({ + success: false, + message: '缺少文件路径参数' + }); + } + + try { + const share = ShareDB.findByCode(code); + + if (!share) { + return res.status(404).json({ + success: false, + message: '分享不存在' + }); + } + + // 验证密码(如果需要) + if (share.share_password) { + if (!password || !ShareDB.verifyPassword(password, share.share_password)) { + return res.status(401).json({ + success: false, + message: '密码错误或未提供密码' + }); + } + } + + // 获取分享者的用户信息 + const shareOwner = UserDB.findById(share.user_id); + if (!shareOwner) { + return res.status(404).json({ + success: false, + message: '分享者不存在' + }); + } + + // 使用统一存储接口,根据分享的storage_type选择存储后端 + const { StorageInterface } = require('./storage'); + const storageType = share.storage_type || 'sftp'; + console.log(`[分享下载] 存储类型: ${storageType}, 文件路径: ${filePath}`); + + // 临时构造用户对象以使用存储接口 + const userForStorage = { + ...shareOwner, + current_storage_type: storageType + }; + + const storageInterface = new StorageInterface(userForStorage); + storage = await storageInterface.connect(); + + // 获取文件名 + const fileName = filePath.split('/').pop(); + + // 获取文件信息(获取文件大小) + const fileStats = await storage.stat(filePath); + const fileSize = fileStats.size; + console.log(`[分享下载] 文件: ${fileName}, 大小: ${fileSize} 字节`); + + // 增加下载次数 + ShareDB.incrementDownloadCount(code); + + // 设置响应头(包含文件大小,浏览器可显示下载进度) + res.setHeader('Content-Type', 'application/octet-stream'); + res.setHeader('Content-Length', fileSize); + res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(fileName)}"; filename*=UTF-8''${encodeURIComponent(fileName)}`); + + // 创建文件流并传输(流式下载,服务器不保存临时文件) + const stream = storage.createReadStream(filePath); + + stream.on('error', (error) => { + console.error('文件流错误:', error); + if (!res.headersSent) { + res.status(500).json({ + success: false, + message: '文件下载失败: ' + error.message + }); + } + // 发生错误时关闭存储连接 + if (storage) { + storage.end().catch(err => console.error('关闭存储连接失败:', err)); + } + }); + + // 在传输完成后关闭存储连接 + stream.on('close', () => { + console.log('[分享下载] 文件传输完成,关闭存储连接'); + if (storage) { + storage.end().catch(err => console.error('关闭存储连接失败:', err)); + } + }); + + stream.pipe(res); + + } catch (error) { + console.error('分享下载文件失败:', error); + if (!res.headersSent) { + res.status(500).json({ + success: false, + message: '下载文件失败: ' + error.message + }); + } + // 如果发生错误,关闭存储连接 + if (storage) { + storage.end().catch(err => console.error('关闭存储连接失败:', err)); + } + } +}); + +// ===== 管理员API ===== + +// 获取系统设置 +app.get('/api/admin/settings', authMiddleware, adminMiddleware, (req, res) => { + try { + const maxUploadSize = parseInt(SettingsDB.get('max_upload_size') || '104857600'); + + res.json({ + success: true, + settings: { + max_upload_size: maxUploadSize + } + }); + } catch (error) { + console.error('获取系统设置失败:', error); + res.status(500).json({ + success: false, + message: '获取系统设置失败: ' + error.message + }); + } +}); + +// 更新系统设置 +app.post('/api/admin/settings', authMiddleware, adminMiddleware, (req, res) => { + try { + const { max_upload_size } = req.body; + + if (max_upload_size !== undefined) { + const size = parseInt(max_upload_size); + if (isNaN(size) || size < 0) { + return res.status(400).json({ + success: false, + message: '无效的文件大小' + }); + } + SettingsDB.set('max_upload_size', size.toString()); + } + + res.json({ + success: true, + message: '系统设置已更新' + }); + } catch (error) { + console.error('更新系统设置失败:', error); + res.status(500).json({ + success: false, + message: '更新系统设置失败: ' + error.message + }); + } +}); + +// 获取服务器存储统计信息 +app.get('/api/admin/storage-stats', authMiddleware, adminMiddleware, (req, res) => { + try { + // 获取本地存储目录 + const localStorageDir = path.join(__dirname, 'local-storage'); + + // 获取磁盘信息(使用df命令) + let totalDisk = 0; + let usedDisk = 0; + let availableDisk = 0; + + try { + // 获取本地存储目录所在分区的磁盘信息 + const dfOutput = execSync(`df -B 1 / | tail -1`, { encoding: 'utf8' }); + const parts = dfOutput.trim().split(/\s+/); + + if (parts.length >= 4) { + totalDisk = parseInt(parts[1]) || 0; // 总大小 + usedDisk = parseInt(parts[2]) || 0; // 已使用 + availableDisk = parseInt(parts[3]) || 0; // 可用 + } + } catch (dfError) { + console.error('获取磁盘信息失败:', dfError.message); + // 如果df命令失败,尝试使用Windows的wmic命令 + try { + // 获取本地存储目录所在的驱动器号 + const driveLetter = localStorageDir.charAt(0); + const wmicOutput = execSync(`wmic logicaldisk where "DeviceID='${driveLetter}:'" get Size,FreeSpace /value`, { encoding: 'utf8' }); + + const freeMatch = wmicOutput.match(/FreeSpace=(\d+)/); + const sizeMatch = wmicOutput.match(/Size=(\d+)/); + + if (sizeMatch && freeMatch) { + totalDisk = parseInt(sizeMatch[1]) || 0; + availableDisk = parseInt(freeMatch[1]) || 0; + usedDisk = totalDisk - availableDisk; + } + } catch (wmicError) { + console.error('获取Windows磁盘信息失败:', wmicError.message); + } + } + + // 从数据库获取所有用户的本地存储配额和使用情况 + const users = UserDB.getAll(); + let totalUserQuotas = 0; + let totalUserUsed = 0; + + users.forEach(user => { + // 只统计使用本地存储的用户(local_only 或 user_choice) + const storagePermission = user.storage_permission || 'sftp_only'; + if (storagePermission === 'local_only' || storagePermission === 'user_choice') { + totalUserQuotas += user.local_storage_quota || 0; + totalUserUsed += user.local_storage_used || 0; + } + }); + + res.json({ + success: true, + stats: { + totalDisk, // 磁盘总容量 + usedDisk, // 磁盘已使用 + availableDisk, // 磁盘可用空间 + totalUserQuotas, // 用户配额总和 + totalUserUsed, // 用户实际使用总和 + totalUsers: users.length // 用户总数 + } + }); + +// 获取所有用户 +} catch (error) { console.error('获取存储统计失败:', error); res.status(500).json({ success: false, message: '获取存储统计失败: ' + error.message }); }}); +app.get('/api/admin/users', authMiddleware, adminMiddleware, (req, res) => { + try { + const users = UserDB.getAll(); + + res.json({ + success: true, + users: users.map(u => ({ + id: u.id, + username: u.username, + email: u.email, + is_admin: u.is_admin, + is_active: u.is_active, + is_banned: u.is_banned, + has_ftp_config: u.has_ftp_config, + created_at: u.created_at, + // 新增:存储相关字段 + storage_permission: u.storage_permission || 'sftp_only', + current_storage_type: u.current_storage_type || 'sftp', + local_storage_quota: u.local_storage_quota || 1073741824, + local_storage_used: u.local_storage_used || 0 + })) + }); + } catch (error) { + console.error('获取用户列表失败:', error); + res.status(500).json({ + success: false, + message: '获取用户列表失败: ' + error.message + }); + } +}); + +// 封禁/解封用户 +app.post('/api/admin/users/:id/ban', authMiddleware, adminMiddleware, (req, res) => { + try { + const { id } = req.params; + const { banned } = req.body; + + UserDB.setBanStatus(id, banned); + + res.json({ + success: true, + message: banned ? '用户已封禁' : '用户已解封' + }); + } catch (error) { + console.error('操作失败:', error); + res.status(500).json({ + success: false, + message: '操作失败: ' + error.message + }); + } +}); + +// 删除用户(级联删除文件和分享) +app.delete('/api/admin/users/:id', authMiddleware, adminMiddleware, async (req, res) => { + try { + const { id } = req.params; + + if (parseInt(id) === req.user.id) { + return res.status(400).json({ + success: false, + message: '不能删除自己的账号' + }); + } + + // 获取用户信息 + const user = UserDB.findById(id); + if (!user) { + return res.status(404).json({ + success: false, + message: '用户不存在' + }); + } + + const deletionLog = { + userId: id, + username: user.username, + deletedFiles: [], + deletedShares: 0, + warnings: [] + }; + + // 1. 删除本地存储文件(如果用户使用了本地存储) + const storagePermission = user.storage_permission || 'sftp_only'; + if (storagePermission === 'local_only' || storagePermission === 'user_choice') { + const storageRoot = process.env.STORAGE_ROOT || path.join(__dirname, 'storage'); + const userStorageDir = path.join(storageRoot, `user_${id}`); + + if (fs.existsSync(userStorageDir)) { + try { + // 递归删除用户目录 + const deletedSize = getUserDirectorySize(userStorageDir); + fs.rmSync(userStorageDir, { recursive: true, force: true }); + deletionLog.deletedFiles.push({ + type: 'local', + path: userStorageDir, + size: deletedSize + }); + console.log(`[删除用户] 已删除本地存储目录: ${userStorageDir}`); + } catch (error) { + console.error(`[删除用户] 删除本地存储失败:`, error); + deletionLog.warnings.push(`删除本地存储失败: ${error.message}`); + } + } + } + + // 2. SFTP存储文件 - 只记录警告,不实际删除(安全考虑) + if (user.has_ftp_config && (storagePermission === 'sftp_only' || storagePermission === 'user_choice')) { + deletionLog.warnings.push( + `用户配置了SFTP存储 (${user.ftp_host}:${user.ftp_port}),SFTP文件未自动删除,请手动处理` + ); + } + + // 3. 删除用户的所有分享记录 + try { + const userShares = ShareDB.getUserShares(id); + deletionLog.deletedShares = userShares.length; + + userShares.forEach(share => { + ShareDB.delete(share.id); + // 清除分享缓存 + if (shareFileCache.has(share.share_code)) { + shareFileCache.delete(share.share_code); + } + }); + + console.log(`[删除用户] 已删除 ${deletionLog.deletedShares} 条分享记录`); + } catch (error) { + console.error(`[删除用户] 删除分享记录失败:`, error); + deletionLog.warnings.push(`删除分享记录失败: ${error.message}`); + } + + // 4. 删除用户记录 + UserDB.delete(id); + + // 构建响应消息 + let message = `用户 ${user.username} 已删除`; + if (deletionLog.deletedFiles.length > 0) { + const totalSize = deletionLog.deletedFiles.reduce((sum, f) => sum + f.size, 0); + message += `,已清理本地文件 ${formatFileSize(totalSize)}`; + } + if (deletionLog.deletedShares > 0) { + message += `,已删除 ${deletionLog.deletedShares} 条分享`; + } + + res.json({ + success: true, + message, + details: deletionLog + }); + } catch (error) { + console.error('删除用户失败:', error); + res.status(500).json({ + success: false, + message: '删除用户失败: ' + error.message + }); + } +}); + +// 辅助函数:计算目录大小 +function getUserDirectorySize(dirPath) { + let totalSize = 0; + + function calculateSize(currentPath) { + try { + const stats = fs.statSync(currentPath); + + if (stats.isDirectory()) { + const files = fs.readdirSync(currentPath); + files.forEach(file => { + calculateSize(path.join(currentPath, file)); + }); + } else { + totalSize += stats.size; + } + } catch (error) { + console.error(`计算大小失败: ${currentPath}`, error); + } + } + + calculateSize(dirPath); + return totalSize; +} + +// 设置用户存储权限(管理员) +app.post('/api/admin/users/:id/storage-permission', + authMiddleware, + adminMiddleware, + [ + body('storage_permission').isIn(['local_only', 'sftp_only', 'user_choice']).withMessage('无效的存储权限') + ], + (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + errors: errors.array() + }); + } + + try { + const { id } = req.params; + const { storage_permission, local_storage_quota } = req.body; + + const updates = { storage_permission }; + + // 如果提供了配额,更新配额(单位:字节) + if (local_storage_quota !== undefined) { + updates.local_storage_quota = parseInt(local_storage_quota); + } + + // 根据权限设置自动调整存储类型 + const user = UserDB.findById(id); + if (!user) { + return res.status(404).json({ + success: false, + message: '用户不存在' + }); + } + + if (storage_permission === 'local_only') { + updates.current_storage_type = 'local'; + } else if (storage_permission === 'sftp_only') { + // 只有配置了SFTP才切换到SFTP + if (user.has_ftp_config) { + updates.current_storage_type = 'sftp'; + } + } + // user_choice 不自动切换,保持用户当前选择 + + UserDB.update(id, updates); + + res.json({ + success: true, + message: '存储权限已更新' + }); + } catch (error) { + console.error('设置存储权限失败:', error); + res.status(500).json({ + success: false, + message: '设置存储权限失败: ' + error.message + }); + } + } +); + +// 重置用户密码 +// ===== 密码重置请求系统 ===== + +// 用户提交密码重置请求(公开API) +app.post('/api/password-reset/request', + [ + body('username').notEmpty().withMessage('用户名不能为空'), + body('new_password').isLength({ min: 6 }).withMessage('新密码至少6个字符') + ], + (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + errors: errors.array() + }); + } + + try { + const { username, new_password } = req.body; + + const user = UserDB.findByUsername(username); + if (!user) { + return res.status(404).json({ + success: false, + message: '用户不存在' + }); + } + + // 检查是否已有待审核的请求 + if (PasswordResetDB.hasPendingRequest(user.id)) { + return res.status(400).json({ + success: false, + message: '您已经提交过密码重置请求,请等待管理员审核' + }); + } + + // 创建密码重置请求 + PasswordResetDB.create(user.id, new_password); + + res.json({ + success: true, + message: '密码重置请求已提交,请等待管理员审核' + }); + } catch (error) { + console.error('提交密码重置请求失败:', error); + res.status(500).json({ + success: false, + message: '提交失败: ' + error.message + }); + } + } +); + +// 获取待审核的密码重置请求(管理员) +app.get('/api/admin/password-reset/pending', authMiddleware, adminMiddleware, (req, res) => { + try { + const requests = PasswordResetDB.getPending(); + + res.json({ + success: true, + requests + }); + } catch (error) { + console.error('获取密码重置请求失败:', error); + res.status(500).json({ + success: false, + message: '获取请求失败: ' + error.message + }); + } +}); + +// 审核密码重置请求(管理员) +app.post('/api/admin/password-reset/:id/review', authMiddleware, adminMiddleware, (req, res) => { + try { + const { id } = req.params; + const { approved } = req.body; + + PasswordResetDB.review(id, req.user.id, approved); + + res.json({ + success: true, + message: approved ? '密码重置已批准' : '密码重置已拒绝' + }); + } catch (error) { + console.error('审核密码重置请求失败:', error); + res.status(500).json({ + success: false, + message: error.message || '审核失败' + }); + } +}); + +// ===== 管理员文件审查功能 ===== + +// 查看用户文件列表(管理员,只读) +app.get('/api/admin/users/:id/files', authMiddleware, adminMiddleware, async (req, res) => { + const { id } = req.params; + const dirPath = req.query.path || '/'; + let sftp; + + try { + const user = UserDB.findById(id); + + if (!user) { + return res.status(404).json({ + success: false, + message: '用户不存在' + }); + } + + if (!user.has_ftp_config) { + return res.status(400).json({ + success: false, + message: '该用户未配置SFTP服务器' + }); + } + + sftp = await connectToSFTP(user); + const list = await sftp.list(dirPath); + + const formattedList = list.map(item => ({ + name: item.name, + type: item.type === 'd' ? 'directory' : 'file', + size: item.size, + sizeFormatted: formatFileSize(item.size), + modifiedAt: new Date(item.modifyTime), + isDirectory: item.type === 'd' + })); + + formattedList.sort((a, b) => { + if (a.isDirectory && !b.isDirectory) return -1; + if (!a.isDirectory && b.isDirectory) return 1; + return a.name.localeCompare(b.name); + }); + + res.json({ + success: true, + username: user.username, + path: dirPath, + items: formattedList + }); + } catch (error) { + console.error('管理员查看用户文件失败:', error); + res.status(500).json({ + success: false, + message: '获取文件列表失败: ' + error.message + }); + } finally { + if (sftp) await sftp.end(); + } +}); + +// 获取所有分享(管理员) +app.get('/api/admin/shares', authMiddleware, adminMiddleware, (req, res) => { + try { + const shares = ShareDB.getAll(); + + res.json({ + success: true, + shares + }); + } catch (error) { + console.error('获取分享列表失败:', error); + res.status(500).json({ + success: false, + message: '获取分享列表失败: ' + error.message + }); + } +}); + +// 删除分享(管理员) +app.delete('/api/admin/shares/:id', authMiddleware, adminMiddleware, (req, res) => { + try { + // 先获取分享信息以获得share_code + const share = ShareDB.findById(req.params.id); + + if (share) { + // 删除缓存 + if (shareFileCache.has(share.share_code)) { + shareFileCache.delete(share.share_code); + console.log(`[缓存清除] 分享码: ${share.share_code} (管理员操作)`); + } + + // 删除数据库记录 + ShareDB.delete(req.params.id); + + res.json({ + success: true, + message: '分享已删除' + }); + } else { + res.status(404).json({ + success: false, + message: '分享不存在' + }); + } + } catch (error) { + console.error('删除分享失败:', error); + res.status(500).json({ + success: false, + message: '删除分享失败: ' + error.message + }); + } +}); + +// 分享页面访问路由 +app.get("/s/:code", (req, res) => { + const shareCode = req.params.code; + // 使用相对路径重定向,浏览器会自动使用当前的协议和host + const frontendUrl = `/share.html?code=${shareCode}`; + console.log(`[分享] 重定向到: ${frontendUrl}`); + res.redirect(frontendUrl); +}); + +// 启动服务器 +app.listen(PORT, '0.0.0.0', () => { + console.log(`\n========================================`); + console.log(`玩玩云已启动`); + console.log(`服务器地址: http://localhost:${PORT}`); + console.log(`外网访问地址: http://0.0.0.0:${PORT}`); + console.log(`========================================\n`); +}); diff --git a/backend/start.bat b/backend/start.bat new file mode 100644 index 0000000..719cb35 --- /dev/null +++ b/backend/start.bat @@ -0,0 +1,10 @@ +@echo off +echo ======================================== +echo FTP 网盘管理平台 - 启动脚本 +echo ======================================== +echo. + +cd /d %~dp0 +node server.js + +pause diff --git a/backend/storage.js b/backend/storage.js new file mode 100644 index 0000000..cc42b1b --- /dev/null +++ b/backend/storage.js @@ -0,0 +1,321 @@ +const SftpClient = require('ssh2-sftp-client'); +const fs = require('fs'); +const path = require('path'); +const { UserDB } = require('./database'); + +// ===== 统一存储接口 ===== + +/** + * 存储接口工厂 + * 根据用户的存储类型返回对应的存储客户端 + */ +class StorageInterface { + constructor(user) { + this.user = user; + this.type = user.current_storage_type || 'sftp'; + } + + /** + * 创建并返回存储客户端 + */ + async connect() { + if (this.type === 'local') { + const client = new LocalStorageClient(this.user); + await client.init(); + return client; + } else { + const client = new SftpStorageClient(this.user); + await client.connect(); + return client; + } + } +} + +// ===== 本地存储客户端 ===== + +class LocalStorageClient { + constructor(user) { + this.user = user; + // 使用环境变量或默认路径(不硬编码) + const storageRoot = process.env.STORAGE_ROOT || path.join(__dirname, 'storage'); + this.basePath = path.join(storageRoot, `user_${user.id}`); + } + + /** + * 初始化用户存储目录 + */ + async init() { + if (!fs.existsSync(this.basePath)) { + fs.mkdirSync(this.basePath, { recursive: true, mode: 0o755 }); + console.log(`[本地存储] 创建用户目录: ${this.basePath}`); + } + } + + /** + * 列出目录内容 + */ + async list(dirPath) { + const fullPath = this.getFullPath(dirPath); + + // 确保目录存在 + if (!fs.existsSync(fullPath)) { + fs.mkdirSync(fullPath, { recursive: true }); + return []; + } + + const items = fs.readdirSync(fullPath, { withFileTypes: true }); + + return items.map(item => { + const itemPath = path.join(fullPath, item.name); + const stats = fs.statSync(itemPath); + return { + name: item.name, + type: item.isDirectory() ? 'd' : '-', + size: stats.size, + modifyTime: stats.mtimeMs + }; + }); + } + + /** + * 上传文件 + */ + async put(localPath, remotePath) { + const destPath = this.getFullPath(remotePath); + + // 检查配额 + const fileSize = fs.statSync(localPath).size; + this.checkQuota(fileSize); + + // 确保目标目录存在 + const destDir = path.dirname(destPath); + if (!fs.existsSync(destDir)) { + fs.mkdirSync(destDir, { recursive: true }); + } + + // 使用临时文件+重命名模式,避免文件被占用问题 + const tempPath = `${destPath}.uploading_${Date.now()}`; + + try { + // 复制到临时文件 + fs.copyFileSync(localPath, tempPath); + + // 如果目标文件存在,先删除 + if (fs.existsSync(destPath)) { + fs.unlinkSync(destPath); + } + + // 重命名临时文件为目标文件 + fs.renameSync(tempPath, destPath); + + // 更新已使用空间 + this.updateUsedSpace(fileSize); + } catch (error) { + // 清理临时文件 + if (fs.existsSync(tempPath)) { + fs.unlinkSync(tempPath); + } + throw error; + } + } + + /** + * 删除文件 + */ + async delete(filePath) { + const fullPath = this.getFullPath(filePath); + const stats = fs.statSync(fullPath); + + fs.unlinkSync(fullPath); + + // 更新已使用空间 + this.updateUsedSpace(-stats.size); + } + + /** + * 重命名文件 + */ + async rename(oldPath, newPath) { + const oldFullPath = this.getFullPath(oldPath); + const newFullPath = this.getFullPath(newPath); + + // 确保新路径的目录存在 + const newDir = path.dirname(newFullPath); + if (!fs.existsSync(newDir)) { + fs.mkdirSync(newDir, { recursive: true }); + } + + fs.renameSync(oldFullPath, newFullPath); + } + + /** + * 获取文件信息 + */ + async stat(filePath) { + const fullPath = this.getFullPath(filePath); + return fs.statSync(fullPath); + } + + /** + * 创建文件读取流 + */ + createReadStream(filePath) { + const fullPath = this.getFullPath(filePath); + return fs.createReadStream(fullPath); + } + + /** + * 关闭连接(本地存储无需关闭) + */ + async end() { + // 本地存储无需关闭连接 + } + + // ===== 辅助方法 ===== + + /** + * 获取完整路径(带安全检查) + */ + getFullPath(relativePath) { + // 1. 规范化路径,移除 ../ 等危险路径 + const normalized = path.normalize(relativePath).replace(/^(\.\.[\/\\])+/, ''); + + // 2. 拼接完整路径 + const fullPath = path.join(this.basePath, normalized); + + // 3. 安全检查:确保路径在用户目录内(防止目录遍历攻击) + if (!fullPath.startsWith(this.basePath)) { + throw new Error('非法路径访问'); + } + + return fullPath; + } + + /** + * 检查配额 + */ + checkQuota(additionalSize) { + const newUsed = (this.user.local_storage_used || 0) + additionalSize; + if (newUsed > this.user.local_storage_quota) { + const used = this.formatSize(this.user.local_storage_used); + const quota = this.formatSize(this.user.local_storage_quota); + const need = this.formatSize(additionalSize); + throw new Error(`存储配额不足。已使用: ${used}, 配额: ${quota}, 需要: ${need}`); + } + } + + /** + * 更新已使用空间 + */ + updateUsedSpace(delta) { + const newUsed = Math.max(0, (this.user.local_storage_used || 0) + delta); + UserDB.update(this.user.id, { local_storage_used: newUsed }); + // 更新内存中的值 + this.user.local_storage_used = newUsed; + } + + /** + * 格式化文件大小 + */ + formatSize(bytes) { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; + } +} + +// ===== SFTP存储客户端 ===== + +class SftpStorageClient { + constructor(user) { + this.user = user; + this.sftp = new SftpClient(); + } + + /** + * 连接SFTP服务器 + */ + async connect() { + await this.sftp.connect({ + host: this.user.ftp_host, + port: this.user.ftp_port || 22, + username: this.user.ftp_user, + password: this.user.ftp_password + }); + return this; + } + + /** + * 列出目录内容 + */ + async list(dirPath) { + return await this.sftp.list(dirPath); + } + + /** + * 上传文件 + */ + async put(localPath, remotePath) { + // 使用临时文件+重命名模式(与upload_tool保持一致) + const tempRemotePath = `${remotePath}.uploading_${Date.now()}`; + + // 第一步:上传到临时文件 + await this.sftp.put(localPath, tempRemotePath); + + // 第二步:检查目标文件是否存在,如果存在先删除 + try { + await this.sftp.stat(remotePath); + await this.sftp.delete(remotePath); + } catch (err) { + // 文件不存在,无需删除 + } + + // 第三步:重命名临时文件为目标文件 + await this.sftp.rename(tempRemotePath, remotePath); + } + + /** + * 删除文件 + */ + async delete(filePath) { + return await this.sftp.delete(filePath); + } + + /** + * 重命名文件 + */ + async rename(oldPath, newPath) { + return await this.sftp.rename(oldPath, newPath); + } + + /** + * 获取文件信息 + */ + async stat(filePath) { + return await this.sftp.stat(filePath); + } + + /** + * 创建文件读取流 + */ + createReadStream(filePath) { + return this.sftp.createReadStream(filePath); + } + + /** + * 关闭连接 + */ + async end() { + if (this.sftp) { + await this.sftp.end(); + } + } +} + +module.exports = { + StorageInterface, + LocalStorageClient, + SftpStorageClient +}; diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..f87f06f --- /dev/null +++ b/deploy.sh @@ -0,0 +1,110 @@ +#!/bin/bash + +# 玩玩云一键部署脚本 +# 使用方法: bash deploy.sh + +set -e + +echo "=========================================" +echo " 玩玩云 - 一键部署脚本" +echo "=========================================" +echo "" + +# 检查Docker +if ! command -v docker &> /dev/null; then + echo "❌ 错误: Docker未安装" + echo "请先安装Docker: https://docs.docker.com/engine/install/" + exit 1 +fi + +# 检查Docker Compose +if ! command -v docker-compose &> /dev/null; then + echo "❌ 错误: Docker Compose未安装" + echo "请先安装Docker Compose: https://docs.docker.com/compose/install/" + exit 1 +fi + +echo "✓ Docker版本: $(docker --version)" +echo "✓ Docker Compose版本: $(docker-compose --version)" +echo "" + +# 检查必要的目录 +echo "📁 检查项目结构..." +REQUIRED_DIRS=("backend" "frontend" "nginx") +for dir in "${REQUIRED_DIRS[@]}"; do + if [ ! -d "$dir" ]; then + echo "❌ 错误: 缺少 $dir 目录" + exit 1 + fi +done +echo "✓ 项目结构完整" +echo "" + +# 创建必要的目录 +echo "📂 创建必要的目录..." +mkdir -p certbot/conf +mkdir -p certbot/www +mkdir -p backend/uploads +echo "✓ 目录创建完成" +echo "" + +# 检查.env文件 +if [ ! -f "backend/.env" ]; then + echo "⚠️ 警告: backend/.env 文件不存在" + if [ -f "backend/.env.example" ]; then + echo "正在从.env.example创建.env文件..." + cp backend/.env.example backend/.env + echo "✓ 已创建.env文件,请根据需要修改配置" + else + echo "⚠️ 建议创建.env文件配置JWT密钥等参数" + fi + echo "" +fi + +# 停止旧容器 +echo "🔄 停止旧容器..." +docker-compose down 2>/dev/null || true +echo "✓ 旧容器已停止" +echo "" + +# 构建并启动 +echo "🚀 构建并启动服务..." +docker-compose up --build -d + +# 等待服务启动 +echo "" +echo "⏳ 等待服务启动..." +sleep 5 + +# 检查容器状态 +echo "" +echo "📊 检查容器状态..." +docker-compose ps + +# 检查后端日志 +echo "" +echo "📝 后端启动日志:" +docker-compose logs --tail=20 backend + +# 显示访问信息 +echo "" +echo "=========================================" +echo " 🎉 部署完成!" +echo "=========================================" +echo "" +echo "📍 访问地址:" +echo " 前端: http://localhost:8080" +echo " 后端API: http://localhost:40001" +echo "" +echo "👤 默认管理员账号:" +echo " 用户名: admin" +echo " 密码: admin123" +echo " ⚠️ 请立即登录并修改密码!" +echo "" +echo "📚 查看日志:" +echo " docker-compose logs -f" +echo "" +echo "🛑 停止服务:" +echo " docker-compose down" +echo "" +echo "=========================================" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3159ed9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,53 @@ +version: '3.8' + +services: + backend: + build: + context: ./backend + dockerfile: Dockerfile + image: wanwanyun-backend + container_name: wanwanyun-backend + restart: always + ports: + - "40001:40001" + volumes: + - ./backend:/app + - /app/node_modules + - ./upload-tool:/upload-tool + - ./storage:/app/storage # 本地存储卷 + environment: + - NODE_ENV=production + - STORAGE_ROOT=/app/storage # 存储根目录(不硬编码) + networks: + - wanwanyun-network + + frontend: + image: nginx:alpine + container_name: wanwanyun-frontend + restart: always + ports: + - "8080:80" + - "8443:443" + volumes: + - ./frontend:/usr/share/nginx/html + - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf + - ./certbot/conf:/etc/letsencrypt + - ./certbot/www:/var/www/certbot + depends_on: + - backend + networks: + - wanwanyun-network + command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'" + + certbot: + image: certbot/certbot + container_name: wanwanyun-certbot + restart: unless-stopped + volumes: + - ./certbot/conf:/etc/letsencrypt + - ./certbot/www:/var/www/certbot + entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" + +networks: + wanwanyun-network: + driver: bridge diff --git a/frontend/app.html b/frontend/app.html new file mode 100644 index 0000000..64d63cd --- /dev/null +++ b/frontend/app.html @@ -0,0 +1,1997 @@ + + + + + + 玩玩云 - 文件管理平台 + + + + + + +
+
+
+
+ + {{ isLogin ? '登录' : '注册' }} +
+
{{ errorMessage }}
+
{{ successMessage }}
+
+
+ + +
+
+ + +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ {{ isLogin ? '还没有账号?' : '已有账号?' }} + {{ isLogin ? '立即注册' : '去登录' }} +
+
+
+ + + + + +
+
+ +
+
+ + + 当前存储: {{ storageTypeText }} + +
+
+
+ 配额使用情况 + {{ localUsedFormatted }} / {{ localQuotaFormatted }} ({{ quotaPercentage }}%) +
+
+
+
+
+
+ + +
+
+ + + + + +
+
+ + +
+
+ + + + + +
+
+

加载中...

+
+ + +
+

文件夹是空的

+ +
+
+ +
拖放文件到这里上传
+
松开鼠标即可开始上传
+
+
+ + +
+
+
+ + + +
+ +
+ + + + + + + + + +
+
{{ file.name }}
+
{{ file.isDirectory ? '文件夹' : file.sizeFormatted }}
+
+
+ + +
+ + + + + + + + + + + + + + + +
文件名大小修改时间
+ + + +
+ +
+ + + + + + + + + + {{ file.name }} +
{{ file.isDirectory ? '-' : file.sizeFormatted }}{{ formatDate(file.modifiedTime) }}
+
+
+
+
+ + + + + + + + + + + + +
+
+ +
+

+ 存储管理 +

+ +
+
+ 当前存储方式: + {{ storageTypeText }} +
+ +
+ 配额使用: + {{ localUsedFormatted }} / {{ localQuotaFormatted }} ({{ quotaPercentage }}%) +
+
+
+
+ +
+ + +
+ +
+ + 提示: 本地存储速度快但有配额限制;SFTP存储需先配置服务器信息 +
+
+
+ + +
+

+ 本地存储 +

+ +
+
+ 存储方式: + 本地存储 + + 仅本地 + +
+ +
+ 配额使用: + {{ localUsedFormatted }} / {{ localQuotaFormatted }} ({{ quotaPercentage }}%) +
+
+
+
+ +
+ + 说明: 管理员已将您的存储权限设置为"仅本地存储",您的文件存储在服务器本地,速度快但有配额限制。如需使用SFTP存储,请联系管理员修改权限设置。 +
+
+
+ + +
+

+ SFTP存储 +

+ +
+
+ 存储方式: + SFTP存储 + + 仅SFTP + +
+ +
+ 服务器: + {{ user.ftp_host }}:{{ user.ftp_port }} +
+ +
+ + 说明: 管理员已将您的存储权限设置为"仅SFTP存储",您的文件存储在远程SFTP服务器上。如需使用本地存储,请联系管理员修改权限设置。 +
+
+
+ + +
+

SFTP配置

+
+ 请配置SFTP服务器 +
+ + +
+
+ +

+ 快速导入配置文件 +

+

+ 点击选择或拖拽 .inf 文件到此处 +

+

+ 导入后请检查配置信息并点击保存按钮 +

+ +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + 配置后可直接通过HTTP下载文件。URL格式: 基础URL/文件路径,例如: http://example.com/files/test.exe + +
+ +
+
+ + +

账号设置

+ + +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+
+
+ + +
+
+ +
+

我的分享

+
+ + +
+
+ + +
+ 还没有创建任何分享 +
+ + +
+
+
+ +
+
{{ share.share_path }}
+
+ 访问: {{ share.view_count }} | 下载: {{ share.download_count }} +
+
+ + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
文件路径分享链接访问次数下载次数操作
{{ share.share_path }} + {{ share.share_url }} + {{ share.view_count }}{{ share.download_count }} + +
+
+
+ + +
+
+ +
+

+ 服务器存储统计 +

+ +
+ +
+
+
+
磁盘总容量
+
{{ formatBytes(serverStorageStats.totalDisk) }}
+
+ +
+
+ + +
+
+
+
已使用空间
+
{{ formatBytes(serverStorageStats.usedDisk) }}
+
+ {{ serverStorageStats.totalDisk > 0 ? Math.round((serverStorageStats.usedDisk / serverStorageStats.totalDisk) * 100) : 0 }}% 使用率 +
+
+ +
+
+ + +
+
+
+
可用空间
+
{{ formatBytes(serverStorageStats.availableDisk) }}
+
+ +
+
+ + +
+
+
+
用户配额总和
+
{{ formatBytes(serverStorageStats.totalUserQuotas) }}
+
+ {{ serverStorageStats.totalUsers }} 个用户 +
+
+ +
+
+ + +
+
+
+
用户实际使用
+
{{ formatBytes(serverStorageStats.totalUserUsed) }}
+
+ {{ serverStorageStats.totalUserQuotas > 0 ? Math.round((serverStorageStats.totalUserUsed / serverStorageStats.totalUserQuotas) * 100) : 0 }}% 配额使用率 +
+
+ +
+
+ + +
+
+
+
安全可分配配额
+
+ {{ formatBytes(Math.max(0, serverStorageStats.availableDisk - (serverStorageStats.totalUserQuotas - serverStorageStats.totalUserUsed))) }} +
+
+ 可用空间 - 未使用的配额 +
+
+ +
+
+
+ + +
+ + 警告: 磁盘使用率已超过90%,建议及时清理空间或扩容! +
+ +
+ + 配额超分配: 用户配额总和 ({{ formatBytes(serverStorageStats.totalUserQuotas) }}) 已超过磁盘总容量 ({{ formatBytes(serverStorageStats.totalDisk) }})! +
+
+ +

用户管理

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
ID用户名邮箱存储权限当前存储配额使用状态操作
{{ u.id }} + {{ u.username }} + + + + {{ u.email }} + 仅本地 + 仅SFTP + 用户选择 + + + 本地 + + + SFTP + + +
+
{{ formatBytes(u.local_storage_used) }} / {{ formatBytes(u.local_storage_quota) }}
+
+ {{ Math.round((u.local_storage_used / u.local_storage_quota) * 100) }}% +
+
+ - +
+ 已封禁 + 正常 + +
+ + + + + +
+
+
+
+ + +
+

密码重置审核

+
+ 暂无待审核的密码重置请求 +
+ + + + + + + + + + + + + + + + + +
用户名邮箱提交时间操作
{{ req.username }}{{ req.email }}{{ formatDate(req.created_at) }} + + +
+
+
+ + + + + + + + +
+
+ +
+
{{ toast.title }}
+
{{ toast.message }}
+
+
+
+ + +
+
+ +
+
正在上传文件
+
{{ uploadingFileName }}
+
{{ formatFileSize(uploadedBytes) }} / {{ formatFileSize(totalBytes) }}
+
+
{{ uploadProgress }}%
+
+
+
+
+
+ + +
+
+ 预览 +
+
+ 下载 +
+
+ 重命名 +
+
+ 分享 +
+
+
+ 删除 +
+
+ + + + + + + + + + + +
+ + + + + + diff --git a/frontend/app.js b/frontend/app.js new file mode 100644 index 0000000..44fffe6 --- /dev/null +++ b/frontend/app.js @@ -0,0 +1,1708 @@ +const { createApp } = Vue; + +createApp({ + data() { + return { + // API配置 + // API配置 - 动态适配localhost或生产环境 + apiBase: window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' + ? 'http://localhost:40001' + : window.location.protocol + '//' + window.location.host, + + // 用户状态 + isLoggedIn: false, + user: null, + token: null, + + // 视图状态 + currentView: 'files', + isLogin: true, + fileViewMode: 'grid', // 文件显示模式: grid 大图标, list 列表 + shareViewMode: 'list', // 分享显示模式: grid 大图标, list 列表 + + // 表单数据 + loginForm: { + username: '', + password: '' + }, + registerForm: { + username: '', + email: '', + password: '' + }, + + // SFTP配置表单 + ftpConfigForm: { + ftp_host: '', + ftp_port: 22, + ftp_user: '', + ftp_password: '', + http_download_base_url: '' + }, + showFtpConfigModal: false, + + // 修改密码表单 + changePasswordForm: { + new_password: '' + }, + // 用户名修改表单 + usernameForm: { + newUsername: '' + }, + currentPath: '/', + files: [], + loading: false, + + // 分享管理 + shares: [], + showShareAllModal: false, + showShareFileModal: false, + shareAllForm: { + password: "", + expiryType: "never", + customDays: 7 + }, + shareFileForm: { + fileName: "", + filePath: "", + password: "", + expiryType: "never", + customDays: 7 + }, + shareResult: null, + + // 文件重命名 + showRenameModal: false, + renameForm: { + oldName: "", + newName: "", + path: "" + }, + + // 上传 + showUploadModal: false, + uploadProgress: 0, + uploadedBytes: 0, + totalBytes: 0, + uploadingFileName: '', + isDragging: false, + + // 上传工具下载 + downloadingTool: false, + + // 管理员 + adminUsers: [], + showResetPwdModal: false, + resetPwdUser: {}, + newPassword: '', + + // 密码重置审核 + passwordResetRequests: [], + + // 文件审查 + showFileInspectionModal: false, + inspectionUser: null, + inspectionFiles: [], + inspectionPath: '/', + inspectionLoading: false, + inspectionViewMode: 'grid', // 文件审查显示模式: grid 大图标, list 列表 + + // 忘记密码 + showForgotPasswordModal: false, + forgotPasswordForm: { + username: '', + new_password: '' + }, + + // 系统设置 + systemSettings: { + maxUploadSizeMB: 100 + }, + + // Toast通知 + toasts: [], + toastIdCounter: 0, + + // 提示信息 + errorMessage: '', + successMessage: '', + + // 存储相关 + storageType: 'sftp', // 当前使用的存储类型 + storagePermission: 'sftp_only', // 存储权限 + localQuota: 0, // 本地存储配额(字节) + localUsed: 0, // 本地存储已使用(字节) + + + // 右键菜单 + showContextMenu: false, + contextMenuX: 0, + contextMenuY: 0, + contextMenuFile: null, + + // 长按检测 + longPressTimer: null, + + // 媒体预览 + showImageViewer: false, + showVideoPlayer: false, + showAudioPlayer: false, + currentMediaUrl: '', + currentMediaName: '', + currentMediaType: '', // 'image', 'video', 'audio' + longPressDuration: 500, // 长按时间(毫秒) + // 管理员编辑用户存储权限 + showEditStorageModal: false, + editStorageForm: { + userId: null, + username: '', + storage_permission: 'sftp_only', + local_storage_quota_value: 1, // 配额数值 + quota_unit: 'GB' // 配额单位:MB 或 GB + }, + + // 服务器存储统计 + serverStorageStats: { + totalDisk: 0, + usedDisk: 0, + availableDisk: 0, + totalUserQuotas: 0, + totalUserUsed: 0, + totalUsers: 0 + } + }; + }, + + computed: { + pathParts() { + return this.currentPath.split('/').filter(p => p !== ''); + }, + + // 格式化配额显示 + localQuotaFormatted() { + return this.formatBytes(this.localQuota); + }, + + localUsedFormatted() { + return this.formatBytes(this.localUsed); + }, + + // 配额使用百分比 + quotaPercentage() { + if (this.localQuota === 0) return 0; + return Math.round((this.localUsed / this.localQuota) * 100); + }, + + // 存储类型显示文本 + storageTypeText() { + return this.storageType === 'local' ? '本地存储' : 'SFTP存储'; + } + }, + + methods: { + // 格式化文件大小 + formatFileSize(bytes) { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]; + }, + + // 拖拽上传处理 + handleDragEnter(e) { + e.preventDefault(); + e.stopPropagation(); + this.isDragging = true; + }, + handleDragOver(e) { + e.preventDefault(); + e.stopPropagation(); + this.isDragging = true; + }, +handleDragLeave(e) { + e.preventDefault(); + e.stopPropagation(); + + // 使用更可靠的检测:检查鼠标实际位置 + const container = e.currentTarget; + const rect = container.getBoundingClientRect(); + const x = e.clientX; + const y = e.clientY; + + // 如果鼠标位置在容器边界外,隐藏覆盖层 + // 添加5px的容差,避免边界问题 + const margin = 5; + const isOutside = + x < rect.left - margin || + x > rect.right + margin || + y < rect.top - margin || + y > rect.bottom + margin; + + if (isOutside) { + this.isDragging = false; + return; + } + + // 备用检测:检查 relatedTarget + const related = e.relatedTarget; + if (!related || !container.contains(related)) { + this.isDragging = false; + } + }, + + async handleDrop(e) { + e.preventDefault(); + e.stopPropagation(); + this.isDragging = false; + + const files = e.dataTransfer.files; + if (files.length > 0) { + const file = files[0]; + await this.uploadFile(file); + } + }, + + // ===== 认证相关 ===== + + toggleAuthMode() { + this.isLogin = !this.isLogin; + this.errorMessage = ''; + this.successMessage = ''; + }, + + async handleLogin() { + this.errorMessage = ''; + try { + const response = await axios.post(`${this.apiBase}/api/login`, this.loginForm); + + if (response.data.success) { + this.token = response.data.token; + this.user = response.data.user; + this.isLoggedIn = true; + + // 保存token到localStorage + localStorage.setItem('token', this.token); + localStorage.setItem('user', JSON.stringify(this.user)); + + // 直接从登录响应中获取存储信息 + this.storagePermission = this.user.storage_permission || 'sftp_only'; + this.storageType = this.user.current_storage_type || 'sftp'; + this.localQuota = this.user.local_storage_quota || 0; + this.localUsed = this.user.local_storage_used || 0; + + console.log('[登录] 存储权限:', this.storagePermission, '存储类型:', this.storageType); + + // 管理员直接跳转到管理后台 + if (this.user.is_admin) { + this.currentView = 'admin'; + } + // 普通用户:检查存储权限 + else { + // 如果用户可以使用本地存储,直接进入文件页面 + if (this.storagePermission === 'local_only' || this.storagePermission === 'user_choice') { + this.currentView = 'files'; + this.loadFiles('/'); + } + // 如果仅SFTP模式,需要检查是否配置了SFTP + else if (this.storagePermission === 'sftp_only') { + if (this.user.has_ftp_config) { + this.currentView = 'files'; + this.loadFiles('/'); + } else { + this.currentView = 'settings'; + alert('欢迎!请先配置您的SFTP服务器'); + } + } else { + // 默认行为:跳转到文件页面 + this.currentView = 'files'; + this.loadFiles('/'); + } + } + } + } catch (error) { + this.errorMessage = error.response?.data?.message || '登录失败'; + } + }, + + async handleRegister() { + this.errorMessage = ''; + this.successMessage = ''; + + try { + const response = await axios.post(`${this.apiBase}/api/register`, this.registerForm); + + if (response.data.success) { + this.successMessage = '注册成功!请登录'; + this.isLogin = true; + + // 清空表单 + this.registerForm = { + username: '', + email: '', + password: '' + }; + } + } catch (error) { + const errorData = error.response?.data; + if (errorData?.errors) { + this.errorMessage = errorData.errors.map(e => e.msg).join(', '); + } else { + this.errorMessage = errorData?.message || '注册失败'; + } + } + }, + + async updateFtpConfig() { + try { + const response = await axios.post( + `${this.apiBase}/api/user/update-ftp`, + this.ftpConfigForm, + { headers: { Authorization: `Bearer ${this.token}` } } + ); + + if (response.data.success) { + alert('SFTP配置已保存!'); + // 更新用户信息 + this.user.has_ftp_config = 1; + // 刷新到文件页面 + this.currentView = 'files'; + this.loadFiles('/'); + } + } catch (error) { + alert('配置失败: ' + (error.response?.data?.message || error.message)); + } + }, + + async updateAdminProfile() { + try { + const response = await axios.post( + `${this.apiBase}/api/admin/update-profile`, + { + username: this.adminProfileForm.username + }, + { headers: { Authorization: `Bearer ${this.token}` } } + ); + + if (response.data.success) { + alert('用户名已更新!请重新登录。'); + + // 更新token和用户信息 + if (response.data.token) { + this.token = response.data.token; + localStorage.setItem('token', response.data.token); + } + + if (response.data.user) { + this.user = response.data.user; + localStorage.setItem('user', JSON.stringify(response.data.user)); + } + + // 重新登录 + this.logout(); + } + } catch (error) { + alert('修改失败: ' + (error.response?.data?.message || error.message)); + } + }, + + async changePassword() { + if (this.changePasswordForm.new_password.length < 6) { + alert('新密码至少6个字符'); + return; + } + + try { + const response = await axios.post( + `${this.apiBase}/api/user/change-password`, + { + new_password: this.changePasswordForm.new_password + }, + { headers: { Authorization: `Bearer ${this.token}` } } + ); + + if (response.data.success) { + alert('密码修改成功!'); + this.changePasswordForm.new_password = ''; + } + } catch (error) { + alert('密码修改失败: ' + (error.response?.data?.message || error.message)); + } + }, + + async loadFtpConfig() { + try { + const response = await axios.get( + `${this.apiBase}/api/user/profile`, + { headers: { Authorization: `Bearer ${this.token}` } } + ); + + if (response.data.success && response.data.user) { + const user = response.data.user; + // 填充SFTP配置表单(密码不回显) + this.ftpConfigForm.ftp_host = user.ftp_host || ''; + this.ftpConfigForm.ftp_port = user.ftp_port || 22; + this.ftpConfigForm.ftp_user = user.ftp_user || ''; + this.ftpConfigForm.ftp_password = ''; // 密码不回显 + this.ftpConfigForm.http_download_base_url = user.http_download_base_url || ''; + } + } catch (error) { + console.error('加载SFTP配置失败:', error); + } + }, + + // 处理配置文件上传 + handleConfigFileUpload(event) { + const file = event.target.files[0]; + if (!file) return; + + this.processConfigFile(file); + + // 清空文件选择,允许重复选择同一文件 + event.target.value = ''; + }, + + // 处理配置文件拖拽 + handleConfigFileDrop(event) { + const file = event.dataTransfer.files[0]; + if (!file) return; + + // 检查文件扩展名 + if (!file.name.toLowerCase().endsWith('.inf')) { + this.showToast('error', '错误', '只支持 .inf 格式的配置文件'); + return; + } + + this.processConfigFile(file); + + // 恢复背景色 + event.currentTarget.style.background = '#f8f9ff'; + }, + + // 处理配置文件 + async processConfigFile(file) { + const reader = new FileReader(); + reader.onload = async (e) => { + try { + const content = e.target.result; + const config = this.parseConfigFile(content); + + if (config) { + // 填充表单 + this.ftpConfigForm.ftp_host = config.ip || ''; + this.ftpConfigForm.ftp_port = config.port || 22; + this.ftpConfigForm.ftp_user = config.id || ''; + this.ftpConfigForm.ftp_password = config.pw || ''; + this.ftpConfigForm.http_download_base_url = config.arr || ''; + + // 提示用户配置已导入,需要确认后保存 + this.showToast('success', '成功', '配置文件已导入!请检查并确认信息后点击"保存配置"按钮'); + } else { + this.showToast('error', '错误', '配置文件格式不正确,请检查文件内容'); + } + } catch (error) { + console.error('解析配置文件失败:', error); + this.showToast('error', '错误', '解析配置文件失败: ' + error.message); + } + }; + + reader.readAsText(file); + }, + + // 解析INI格式的配置文件 + parseConfigFile(content) { + const lines = content.split('\n'); + const config = {}; + + for (let line of lines) { + line = line.trim(); + + // 跳过空行和注释 + if (!line || line.startsWith('#') || line.startsWith(';') || line.startsWith('[')) { + continue; + } + + // 解析 key=value 格式 + const equalsIndex = line.indexOf('='); + if (equalsIndex > 0) { + const key = line.substring(0, equalsIndex).trim(); + const value = line.substring(equalsIndex + 1).trim(); + config[key] = value; + } + } + + // 验证必需字段 + if (config.ip && config.id && config.pw && config.port) { + return config; + } + + return null; + }, + + async updateUsername() { + if (!this.usernameForm.newUsername || this.usernameForm.newUsername.length < 3) { + alert('用户名至少3个字符'); + return; + } + + try { + const response = await axios.post( + `${this.apiBase}/api/user/update-username`, + { username: this.usernameForm.newUsername }, + { headers: { Authorization: `Bearer ${this.token}` } } + ); + + if (response.data.success) { + alert('用户名修改成功!请重新登录'); + // 更新本地用户信息 + this.user.username = this.usernameForm.newUsername; + localStorage.setItem('user', JSON.stringify(this.user)); + this.usernameForm.newUsername = ''; + } + } catch (error) { + alert('用户名修改失败: ' + (error.response?.data?.message || error.message)); + } + }, + + async updateProfile() { + try { + const response = await axios.post( + `${this.apiBase}/api/user/update-profile`, + { email: this.profileForm.email }, + { headers: { Authorization: `Bearer ${this.token}` } } + ); + + if (response.data.success) { + alert('邮箱已更新!'); + // 更新本地用户信息 + if (response.data.user) { + this.user = response.data.user; + localStorage.setItem('user', JSON.stringify(this.user)); + } + } + } catch (error) { + alert('更新失败: ' + (error.response?.data?.message || error.message)); + } + }, + + logout() { + this.isLoggedIn = false; + this.user = null; + this.token = null; + localStorage.removeItem('token'); + localStorage.removeItem('user'); + }, + + // 检查本地存储的登录状态 + async checkLoginStatus() { + const token = localStorage.getItem('token'); + const user = localStorage.getItem('user'); + + if (token && user) { + this.token = token; + this.user = JSON.parse(user); + this.isLoggedIn = true; + + // 从localStorage中的用户信息初始化存储相关字段 + this.storagePermission = this.user.storage_permission || 'sftp_only'; + this.storageType = this.user.current_storage_type || 'sftp'; + this.localQuota = this.user.local_storage_quota || 0; + this.localUsed = this.user.local_storage_used || 0; + + console.log('[页面加载] 存储权限:', this.storagePermission, '存储类型:', this.storageType); + + // 加载最新的用户信息(异步更新) + this.loadUserProfile(); + + // 管理员跳转到管理后台 + if (this.user.is_admin) { + this.currentView = 'admin'; + } + // 普通用户:根据存储权限决定跳转 + else { + // 如果用户可以使用本地存储,直接加载文件 + if (this.storagePermission === 'local_only' || this.storagePermission === 'user_choice') { + this.loadFiles('/'); + } + // 如果仅SFTP模式,需要检查是否配置了SFTP + else if (this.storagePermission === 'sftp_only') { + if (this.user.has_ftp_config) { + this.loadFiles('/'); + } else { + this.currentView = 'settings'; + } + } else { + // 默认加载文件 + this.loadFiles('/'); + } + } + } + }, + + // 检查URL参数 + checkUrlParams() { + const urlParams = new URLSearchParams(window.location.search); + const action = urlParams.get('action'); + + if (action === 'login') { + this.isLogin = true; + } else if (action === 'register') { + this.isLogin = false; + } + }, + + // ===== 文件管理 ===== + + async loadFiles(path) { + this.loading = true; + this.currentPath = path; + + try { + const response = await axios.get(`${this.apiBase}/api/files`, { + params: { path }, + headers: { Authorization: `Bearer ${this.token}` } + }); + + if (response.data.success) { + this.files = response.data.items; + + // 更新存储类型信息 + if (response.data.storageType) { + this.storageType = response.data.storageType; + } + if (response.data.storagePermission) { + this.storagePermission = response.data.storagePermission; + } + + // 更新用户本地存储信息 + await this.loadUserProfile(); + } + } catch (error) { + console.error('加载文件失败:', error); + alert('加载文件失败: ' + (error.response?.data?.message || error.message)); + + if (error.response?.status === 401) { + this.logout(); + } + } finally { + this.loading = false; + } + }, + + handleFileClick(file) { + if (file.isDirectory) { + const newPath = this.currentPath === '/' + ? `/${file.name}` + : `${this.currentPath}/${file.name}`; + this.loadFiles(newPath); + } else { + // 检查文件类型,打开相应的预览 + if (file.name.match(/\.(jpg|jpeg|png|gif|bmp|svg|webp)$/i)) { + this.openImageViewer(file); + } else if (file.name.match(/\.(mp4|avi|mov|wmv|flv|mkv|webm)$/i)) { + this.openVideoPlayer(file); + } else if (file.name.match(/\.(mp3|wav|flac|aac|ogg|m4a)$/i)) { + this.openAudioPlayer(file); + } + // 其他文件类型不做任何操作,用户可以通过右键菜单下载 + } + }, + navigateToPath(path) { + this.loadFiles(path); + }, + + navigateToIndex(index) { + const parts = this.pathParts.slice(0, index + 1); + const path = '/' + parts.join('/'); + this.loadFiles(path); + }, + + downloadFile(file) { + console.log("[DEBUG] 下载文件:", file); + if (file.httpDownloadUrl) { + // 如果配置了HTTP下载URL,使用HTTP直接下载 + console.log("[DEBUG] 使用HTTP下载:", file.httpDownloadUrl); + window.open(file.httpDownloadUrl, "_blank"); + } else { + // 如果没有配置HTTP URL,通过后端SFTP下载 + console.log("[DEBUG] 使用SFTP下载"); + const filePath = this.currentPath === '/' + ? `/${file.name}` + : `${this.currentPath}/${file.name}`; + + // 使用标签下载,通过URL参数传递token + const link = document.createElement('a'); + link.href = `${this.apiBase}/api/files/download?path=${encodeURIComponent(filePath)}&token=${this.token}`; + link.setAttribute('download', file.name); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + }, + + // ===== 文件操作 ===== + + openRenameModal(file) { + this.renameForm.oldName = file.name; + this.renameForm.newName = file.name; + this.renameForm.path = this.currentPath; + this.showRenameModal = true; + }, + + async renameFile() { + if (!this.renameForm.newName || this.renameForm.newName === this.renameForm.oldName) { + alert('请输入新的文件名'); + return; + } + + try { + const response = await axios.post( + `${this.apiBase}/api/files/rename`, + this.renameForm, + { headers: { Authorization: `Bearer ${this.token}` } } + ); + + if (response.data.success) { + this.showToast('success', '成功', '文件已重命名'); + this.showRenameModal = false; + this.loadFiles(this.currentPath); + } + } catch (error) { + console.error('重命名失败:', error); + this.showToast('error', '错误', error.response?.data?.message || '重命名失败'); + } + }, + + confirmDeleteFile(file) { + const fileType = file.isDirectory ? '文件夹' : '文件'; + const warning = file.isDirectory ? "\n注意:只能删除空文件夹!" : ""; + if (confirm(`确定要删除${fileType} "${file.name}" 吗?此操作无法撤销!${warning}`)) { + this.deleteFile(file); + } + }, + + // ===== 右键菜单和长按功能 ===== + + // 显示右键菜单(PC端) + showFileContextMenu(file, event) { + if (file.isDirectory) return; // 文件夹不显示菜单 + + event.preventDefault(); + this.contextMenuFile = file; + this.contextMenuX = event.clientX; + this.contextMenuY = event.clientY; + this.showContextMenu = true; + + // 点击其他地方关闭菜单 + this.$nextTick(() => { + document.addEventListener('click', this.hideContextMenu, { once: true }); + }); + }, + + // 隐藏右键菜单 + hideContextMenu() { + this.showContextMenu = false; + this.contextMenuFile = null; + }, + + // 长按开始(移动端) + handleLongPressStart(file, event) { + if (file.isDirectory) return; // 文件夹不响应长按 + + // 阻止默认的长按行为(如文本选择) + event.preventDefault(); + + this.longPressTimer = setTimeout(() => { + // 触发长按菜单 + this.contextMenuFile = file; + + // 获取触摸点位置 + const touch = event.touches[0]; + this.contextMenuX = touch.clientX; + this.contextMenuY = touch.clientY; + this.showContextMenu = true; + + // 触摸震动反馈(如果支持) + if (navigator.vibrate) { + navigator.vibrate(50); + } + + // 点击其他地方关闭菜单 + this.$nextTick(() => { + document.addEventListener('click', this.hideContextMenu, { once: true }); + }); + }, this.longPressDuration); + }, + + // 长按取消(移动端) + handleLongPressEnd() { + if (this.longPressTimer) { + clearTimeout(this.longPressTimer); + this.longPressTimer = null; + } + }, + + // 从菜单执行操作 + contextMenuAction(action) { + if (!this.contextMenuFile) return; + + switch (action) { + case 'preview': + // 根据文件类型打开对应的预览 + if (this.contextMenuFile.name.match(/\.(jpg|jpeg|png|gif|bmp|svg|webp)$/i)) { + this.openImageViewer(this.contextMenuFile); + } else if (this.contextMenuFile.name.match(/\.(mp4|avi|mov|wmv|flv|mkv|webm)$/i)) { + this.openVideoPlayer(this.contextMenuFile); + } else if (this.contextMenuFile.name.match(/\.(mp3|wav|flac|aac|ogg|m4a)$/i)) { + this.openAudioPlayer(this.contextMenuFile); + } + break; + case 'download': + this.downloadFile(this.contextMenuFile); + break; + case 'rename': + this.openRenameModal(this.contextMenuFile); + break; + case 'share': + this.openShareFileModal(this.contextMenuFile); + break; + case 'delete': + this.confirmDeleteFile(this.contextMenuFile); + break; + } + + this.hideContextMenu(); + }, + + // ===== 媒体预览功能 ===== + + // 获取媒体文件URL + getMediaUrl(file) { + const filePath = this.currentPath === '/' + ? `/${file.name}` + : `${this.currentPath}/${file.name}`; + + // SFTP存储且配置了HTTP下载URL,使用HTTP直接访问 + if (file.httpDownloadUrl) { + return file.httpDownloadUrl; + } + + // 本地存储或未配置HTTP URL,使用API下载 + return `${this.apiBase}/api/files/download?path=${encodeURIComponent(filePath)}&token=${this.token}`; + }, + + // 获取文件缩略图URL + getThumbnailUrl(file) { + if (!file || file.isDirectory) return null; + + // 检查是否是图片或视频 + const isImage = file.name.match(/\.(jpg|jpeg|png|gif|bmp|svg|webp)$/i); + const isVideo = file.name.match(/\.(mp4|avi|mov|wmv|flv|mkv|webm)$/i); + + if (!isImage && !isVideo) return null; + + return this.getMediaUrl(file); + }, + + // 打开图片预览 + openImageViewer(file) { + this.currentMediaUrl = this.getMediaUrl(file); + this.currentMediaName = file.name; + this.currentMediaType = 'image'; + this.showImageViewer = true; + }, + + // 打开视频播放器 + openVideoPlayer(file) { + this.currentMediaUrl = this.getMediaUrl(file); + this.currentMediaName = file.name; + this.currentMediaType = 'video'; + this.showVideoPlayer = true; + }, + + // 打开音频播放器 + openAudioPlayer(file) { + this.currentMediaUrl = this.getMediaUrl(file); + this.currentMediaName = file.name; + this.currentMediaType = 'audio'; + this.showAudioPlayer = true; + }, + + // 关闭媒体预览 + closeMediaViewer() { + this.showImageViewer = false; + this.showVideoPlayer = false; + this.showAudioPlayer = false; + this.currentMediaUrl = ''; + this.currentMediaName = ''; + this.currentMediaType = ''; + }, + + // 下载当前预览的媒体文件 + downloadCurrentMedia() { + if (!this.currentMediaUrl) return; + + // 创建临时a标签触发下载 + const link = document.createElement('a'); + link.href = this.currentMediaUrl; + link.setAttribute('download', this.currentMediaName); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }, + + + // 判断文件是否支持预览 + isPreviewable(file) { + if (!file || file.isDirectory) return false; + return file.name.match(/\.(jpg|jpeg|png|gif|bmp|svg|webp|mp4|avi|mov|wmv|flv|mkv|webm|mp3|wav|flac|aac|ogg|m4a)$/i); + }, + async deleteFile(file) { + try { + const response = await axios.post( + `${this.apiBase}/api/files/delete`, + { + fileName: file.name, + path: this.currentPath, + isDirectory: file.isDirectory + }, + { headers: { Authorization: `Bearer ${this.token}` } } + ); + + if (response.data.success) { + this.showToast('success', '成功', '文件已删除'); + this.loadFiles(this.currentPath); + } + } catch (error) { + console.error('删除失败:', error); + this.showToast('error', '错误', error.response?.data?.message || '删除失败'); + } + }, + + downloadUploadTool() { + try { + this.downloadingTool = true; + this.showToast('info', '提示', '正在生成上传工具,下载即将开始...'); + + // 使用标签下载,通过URL参数传递token,浏览器会显示下载进度 + const link = document.createElement('a'); + link.href = `${this.apiBase}/api/upload/download-tool?token=${this.token}`; + link.setAttribute('download', `玩玩云上传工具_${this.user.username}.zip`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // 延迟重置按钮状态,给下载一些启动时间 + setTimeout(() => { + this.downloadingTool = false; + this.showToast('success', '提示', '下载已开始,请查看浏览器下载进度'); + }, 2000); + } catch (error) { + console.error('下载上传工具失败:', error); + this.showToast('error', '错误', '下载失败'); + this.downloadingTool = false; + } + }, + + // ===== 分享功能 ===== + + openShareFileModal(file) { + this.shareFileForm.fileName = file.name; + this.shareFileForm.filePath = this.currentPath === '/' + ? file.name + : `${this.currentPath}/${file.name}`; + this.shareFileForm.password = ''; + this.shareFileForm.expiryType = 'never'; + this.shareFileForm.customDays = 7; + this.shareResult = null; // 清空上次的分享结果 + this.showShareFileModal = true; + }, + + async createShareAll() { + try { + const expiryDays = this.shareAllForm.expiryType === 'never' ? null : + this.shareAllForm.expiryType === 'custom' ? this.shareAllForm.customDays : + parseInt(this.shareAllForm.expiryType); + + const response = await axios.post( + `${this.apiBase}/api/share/create`, + { + share_type: 'all', + password: this.shareAllForm.password || null, + expiry_days: expiryDays + }, + { headers: { Authorization: `Bearer ${this.token}` } } + ); + + if (response.data.success) { + this.shareResult = response.data; + this.showToast('success', '成功', '分享链接已创建'); + this.loadShares(); + } + } catch (error) { + console.error('创建分享失败:', error); + this.showToast('error', '错误', error.response?.data?.message || '创建分享失败'); + } + }, + + async createShareFile() { + try { + const expiryDays = this.shareFileForm.expiryType === 'never' ? null : + this.shareFileForm.expiryType === 'custom' ? this.shareFileForm.customDays : + parseInt(this.shareFileForm.expiryType); + + const response = await axios.post( + `${this.apiBase}/api/share/create`, + { + share_type: 'file', + file_path: this.shareFileForm.filePath, + file_name: this.shareFileForm.fileName, + password: this.shareFileForm.password || null, + expiry_days: expiryDays + }, + { headers: { Authorization: `Bearer ${this.token}` } } + ); + + if (response.data.success) { + this.shareResult = response.data; + this.showToast('success', '成功', '文件分享链接已创建'); + this.loadShares(); + } + } catch (error) { + console.error('创建分享失败:', error); + this.showToast('error', '错误', error.response?.data?.message || '创建分享失败'); + } + }, + + + // ===== 文件上传 ===== + + handleFileSelect(event) { + const files = event.target.files; + if (files && files.length > 0) { + // 支持多文件上传 + Array.from(files).forEach(file => { + this.uploadFile(file); + }); + // 清空input,允许重复上传相同文件 + event.target.value = ''; + } + }, + + handleFileDrop(event) { + this.isDragging = false; + const file = event.dataTransfer.files[0]; + if (file) { + this.uploadFile(file); + } + }, + + async uploadFile(file) { + // 本地存储配额预检查 + if (this.storageType === 'local') { + const estimatedUsage = this.localUsed + file.size; + if (estimatedUsage > this.localQuota) { + this.showToast( + 'error', + '配额不足', + `文件大小 ${this.formatBytes(file.size)},剩余配额 ${this.formatBytes(this.localQuota - this.localUsed)},无法上传` + ); + return; + } + + // 如果使用率将超过90%,给出警告 + const willExceed90 = (estimatedUsage / this.localQuota) > 0.9; + if (willExceed90) { + const confirmed = confirm( + `警告:上传此文件后将使用 ${Math.round((estimatedUsage / this.localQuota) * 100)}% 的配额。是否继续?` + ); + if (!confirmed) return; + } + } + + const formData = new FormData(); + formData.append('file', file); + formData.append('path', this.currentPath); + + try { + // 设置上传文件名和进度 + this.uploadingFileName = file.name; + this.uploadProgress = 0; + this.uploadedBytes = 0; + this.totalBytes = 0; + + const response = await axios.post(`${this.apiBase}/api/upload`, formData, { + headers: { + 'Authorization': `Bearer ${this.token}`, + 'Content-Type': 'multipart/form-data' + }, + onUploadProgress: (progressEvent) => { + this.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total); + this.uploadedBytes = progressEvent.loaded; + this.totalBytes = progressEvent.total; + } + }); + + if (response.data.success) { + // 显示成功提示 + this.showToast('success', '上传成功', `文件 ${file.name} 已上传`); + + // 重置上传进度 + this.uploadProgress = 0; + this.uploadedBytes = 0; + this.totalBytes = 0; + this.uploadingFileName = ''; + + // 自动刷新文件列表 + await this.loadFiles(this.currentPath); + } + } catch (error) { + console.error('上传失败:', error); + + // 重置上传进度 + this.uploadProgress = 0; + this.uploadedBytes = 0; + this.totalBytes = 0; + this.uploadingFileName = ''; + + // 处理文件大小超限错误 + if (error.response?.status === 413) { + const errorData = error.response.data; + + // 判断响应是JSON还是HTML(Nginx返回HTML,Backend返回JSON) + if (typeof errorData === 'object' && errorData.maxSize && errorData.fileSize) { + // Backend返回的JSON响应 + const maxSizeMB = Math.round(errorData.maxSize / (1024 * 1024)); + const fileSizeMB = Math.round(errorData.fileSize / (1024 * 1024)); + this.showToast( + 'error', + '文件超过上传限制', + `文件大小 ${fileSizeMB}MB 超过限制 ${maxSizeMB}MB` + ); + } else { + // Nginx返回的HTML响应,显示通用消息 + const fileSizeMB = Math.round(file.size / (1024 * 1024)); + this.showToast( + 'error', + '文件超过上传限制', + `文件大小 ${fileSizeMB}MB 超过系统限制,请联系管理员` + ); + } + } else { + this.showToast('error', '上传失败', error.response?.data?.message || error.message); + } + } + }, + + // ===== 分享管理 ===== + + async loadShares() { + try { + const response = await axios.get(`${this.apiBase}/api/share/my`, { + headers: { Authorization: `Bearer ${this.token}` } + }); + + if (response.data.success) { + this.shares = response.data.shares; + } + } catch (error) { + console.error('加载分享列表失败:', error); + alert('加载分享列表失败: ' + (error.response?.data?.message || error.message)); + } + }, + + async createShare() { + this.shareForm.path = this.currentPath; + + try { + const response = await axios.post(`${this.apiBase}/api/share/create`, this.shareForm, { + headers: { Authorization: `Bearer ${this.token}` } + }); + + if (response.data.success) { + this.shareResult = response.data; + this.loadShares(); + } + } catch (error) { + console.error('创建分享失败:', error); + alert('创建分享失败: ' + (error.response?.data?.message || error.message)); + } + }, + + async deleteShare(id) { + if (!confirm('确定要删除这个分享吗?')) return; + + try { + const response = await axios.delete(`${this.apiBase}/api/share/${id}`, { + headers: { Authorization: `Bearer ${this.token}` } + }); + + if (response.data.success) { + alert('分享已删除'); + this.loadShares(); + } + } catch (error) { + console.error('删除分享失败:', error); + alert('删除分享失败: ' + (error.response?.data?.message || error.message)); + } + }, + + copyShareLink(url) { + // 复制分享链接到剪贴板 + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(url).then(() => { + this.showToast('success', '成功', '分享链接已复制到剪贴板'); + }).catch(() => { + this.fallbackCopyToClipboard(url); + }); + } else { + this.fallbackCopyToClipboard(url); + } + }, + + fallbackCopyToClipboard(text) { + // 备用复制方法 + const textArea = document.createElement('textarea'); + textArea.value = text; + textArea.style.position = 'fixed'; + textArea.style.left = '-999999px'; + document.body.appendChild(textArea); + textArea.select(); + try { + document.execCommand('copy'); + this.showToast('success', '成功', '分享链接已复制到剪贴板'); + } catch (err) { + this.showToast('error', '错误', '复制失败,请手动复制'); + } + document.body.removeChild(textArea); + }, + + // ===== 管理员功能 ===== + + async loadUsers() { + try { + const response = await axios.get(`${this.apiBase}/api/admin/users`, { + headers: { Authorization: `Bearer ${this.token}` } + }); + + if (response.data.success) { + this.adminUsers = response.data.users; + } + } catch (error) { + console.error('加载用户列表失败:', error); + alert('加载用户列表失败: ' + (error.response?.data?.message || error.message)); + } + }, + + async banUser(userId, banned) { + const action = banned ? '封禁' : '解封'; + if (!confirm(`确定要${action}这个用户吗?`)) return; + + try { + const response = await axios.post( + `${this.apiBase}/api/admin/users/${userId}/ban`, + { banned }, + { headers: { Authorization: `Bearer ${this.token}` } } + ); + + if (response.data.success) { + alert(response.data.message); + this.loadUsers(); + } + } catch (error) { + console.error('操作失败:', error); + alert('操作失败: ' + (error.response?.data?.message || error.message)); + } + }, + + async deleteUser(userId) { + if (!confirm('确定要删除这个用户吗?此操作不可恢复!')) return; + + try { + const response = await axios.delete(`${this.apiBase}/api/admin/users/${userId}`, { + headers: { Authorization: `Bearer ${this.token}` } + }); + + if (response.data.success) { + alert('用户已删除'); + this.loadUsers(); + } + } catch (error) { + console.error('删除用户失败:', error); + alert('删除用户失败: ' + (error.response?.data?.message || error.message)); + } + }, + + // ===== 忘记密码功能 ===== + + async requestPasswordReset() { + if (!this.forgotPasswordForm.username) { + this.showToast('error', '错误', '请输入用户名'); + return; + } + if (!this.forgotPasswordForm.new_password || this.forgotPasswordForm.new_password.length < 6) { + this.showToast('error', '错误', '新密码至少6个字符'); + return; + } + + try { + const response = await axios.post( + `${this.apiBase}/api/password-reset/request`, + this.forgotPasswordForm + ); + + if (response.data.success) { + this.showToast('success', '成功', '密码重置请求已提交,请等待管理员审核'); + this.showForgotPasswordModal = false; + this.forgotPasswordForm = { username: '', new_password: '' }; + } + } catch (error) { + console.error('提交密码重置请求失败:', error); + this.showToast('error', '错误', error.response?.data?.message || '提交失败'); + } + }, + + // ===== 管理员:密码重置审核 ===== + + async loadPasswordResetRequests() { + try { + const response = await axios.get(`${this.apiBase}/api/admin/password-reset/pending`, { + headers: { Authorization: `Bearer ${this.token}` } + }); + + if (response.data.success) { + this.passwordResetRequests = response.data.requests; + } + } catch (error) { + console.error('加载密码重置请求失败:', error); + this.showToast('error', '错误', '加载密码重置请求失败'); + } + }, + + async reviewPasswordReset(requestId, approved) { + const action = approved ? '批准' : '拒绝'; + if (!confirm(`确定要${action}这个密码重置请求吗?`)) return; + + try { + const response = await axios.post( + `${this.apiBase}/api/admin/password-reset/${requestId}/review`, + { approved }, + { headers: { Authorization: `Bearer ${this.token}` } } + ); + + if (response.data.success) { + this.showToast('success', '成功', response.data.message); + this.loadPasswordResetRequests(); + } + } catch (error) { + console.error('审核失败:', error); + this.showToast('error', '错误', error.response?.data?.message || '审核失败'); + } + }, + + // ===== 管理员:文件审查功能 ===== + + async openFileInspection(user) { + this.inspectionUser = user; + this.inspectionPath = '/'; + this.showFileInspectionModal = true; + await this.loadUserFiles('/'); + }, + + async loadUserFiles(path) { + this.inspectionLoading = true; + this.inspectionPath = path; + + try { + const response = await axios.get( + `${this.apiBase}/api/admin/users/${this.inspectionUser.id}/files`, + { + params: { path }, + headers: { Authorization: `Bearer ${this.token}` } + } + ); + + if (response.data.success) { + this.inspectionFiles = response.data.items; + } + } catch (error) { + console.error('加载用户文件失败:', error); + this.showToast('error', '错误', error.response?.data?.message || '加载文件失败'); + } finally { + this.inspectionLoading = false; + } + }, + + handleInspectionFileClick(file) { + if (file.isDirectory) { + const newPath = this.inspectionPath === '/' + ? `/${file.name}` + : `${this.inspectionPath}/${file.name}`; + this.loadUserFiles(newPath); + } + }, + + navigateInspectionToRoot() { + this.loadUserFiles('/'); + }, + + navigateInspectionUp() { + if (this.inspectionPath === '/') return; + const lastSlash = this.inspectionPath.lastIndexOf('/'); + const parentPath = lastSlash > 0 ? this.inspectionPath.substring(0, lastSlash) : '/'; + this.loadUserFiles(parentPath); + }, + + // ===== 存储管理 ===== + + // 加载用户个人资料(包含存储信息) + async loadUserProfile() { + try { + const response = await axios.get( + `${this.apiBase}/api/user/profile`, + { headers: { Authorization: `Bearer ${this.token}` } } + ); + + if (response.data.success && response.data.user) { + const user = response.data.user; + this.localQuota = user.local_storage_quota || 0; + this.localUsed = user.local_storage_used || 0; + this.storagePermission = user.storage_permission || 'sftp_only'; + this.storageType = user.current_storage_type || 'sftp'; + } + } catch (error) { + console.error('加载用户资料失败:', error); + } + }, + + // 用户切换存储方式 + async switchStorage(type) { + if (!confirm(`确定要切换到${type === 'local' ? '本地存储' : 'SFTP存储'}吗?`)) { + return; + } + + try { + const response = await axios.post( + `${this.apiBase}/api/user/switch-storage`, + { storage_type: type }, + { headers: { Authorization: `Bearer ${this.token}` } } + ); + + if (response.data.success) { + this.storageType = type; + this.showToast('success', '成功', `已切换到${type === 'local' ? '本地存储' : 'SFTP存储'}`); + + // 重新加载文件列表 + if (this.currentView === 'files') { + this.loadFiles(this.currentPath); + } + } + } catch (error) { + console.error('切换存储失败:', error); + this.showToast('error', '错误', error.response?.data?.message || '切换存储失败'); + } + }, + + // 管理员:打开编辑用户存储权限模态框 + openEditStorageModal(user) { + this.editStorageForm.userId = user.id; + this.editStorageForm.username = user.username; + this.editStorageForm.storage_permission = user.storage_permission || 'sftp_only'; + + // 智能识别配额单位 + const quotaBytes = user.local_storage_quota || 1073741824; + const quotaMB = quotaBytes / 1024 / 1024; + const quotaGB = quotaMB / 1024; + + // 如果配额能被1024整除且大于等于1GB,使用GB单位,否则使用MB + if (quotaMB >= 1024 && quotaMB % 1024 === 0) { + this.editStorageForm.local_storage_quota_value = quotaGB; + this.editStorageForm.quota_unit = 'GB'; + } else { + this.editStorageForm.local_storage_quota_value = Math.round(quotaMB); + this.editStorageForm.quota_unit = 'MB'; + } + + this.showEditStorageModal = true; + }, + + // 管理员:更新用户存储权限 + async updateUserStorage() { + try { + // 根据单位计算字节数 + let quotaBytes; + if (this.editStorageForm.quota_unit === 'GB') { + quotaBytes = this.editStorageForm.local_storage_quota_value * 1024 * 1024 * 1024; + } else { + quotaBytes = this.editStorageForm.local_storage_quota_value * 1024 * 1024; + } + + const response = await axios.post( + `${this.apiBase}/api/admin/users/${this.editStorageForm.userId}/storage-permission`, + { + storage_permission: this.editStorageForm.storage_permission, + local_storage_quota: quotaBytes + }, + { headers: { Authorization: `Bearer ${this.token}` } } + ); + + if (response.data.success) { + this.showToast('success', '成功', '存储权限已更新'); + this.showEditStorageModal = false; + this.loadUsers(); + } + } catch (error) { + console.error('更新存储权限失败:', error); + this.showToast('error', '错误', error.response?.data?.message || '更新失败'); + } + }, + + // ===== 工具函数 ===== + + formatBytes(bytes) { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; + }, + + formatDate(dateString) { + if (!dateString) return '-'; + const date = new Date(dateString); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + return `${year}-${month}-${day} ${hours}:${minutes}`; + }, + + // ===== Toast通知 ===== + + showToast(type, title, message) { + const toast = { + id: ++this.toastIdCounter, + type, + title, + message, + icon: type === 'error' ? 'fas fa-circle-exclamation' : type === 'success' ? 'fas fa-circle-check' : 'fas fa-circle-info', + hiding: false + }; + + this.toasts.push(toast); + + // 4.5秒后开始淡出动画 + setTimeout(() => { + const index = this.toasts.findIndex(t => t.id === toast.id); + if (index !== -1) { + this.toasts[index].hiding = true; + + // 0.5秒后移除(动画时长) + setTimeout(() => { + const removeIndex = this.toasts.findIndex(t => t.id === toast.id); + if (removeIndex !== -1) { + this.toasts.splice(removeIndex, 1); + } + }, 500); + } + }, 4500); + }, + + // ===== 系统设置管理 ===== + + async loadSystemSettings() { + try { + const response = await axios.get(`${this.apiBase}/api/admin/settings`, { + headers: { Authorization: `Bearer ${this.token}` } + }); + + if (response.data.success) { + const settings = response.data.settings; + this.systemSettings.maxUploadSizeMB = Math.round(settings.max_upload_size / (1024 * 1024)); + } + } catch (error) { + console.error('加载系统设置失败:', error); + this.showToast('error', '错误', '加载系统设置失败'); + } + }, + + async loadServerStorageStats() { + try { + const response = await axios.get(`${this.apiBase}/api/admin/storage-stats`, { + headers: { Authorization: `Bearer ${this.token}` } + }); + + if (response.data.success) { + this.serverStorageStats = response.data.stats; + } + } catch (error) { + console.error('加载服务器存储统计失败:', error); + this.showToast('error', '错误', '加载服务器存储统计失败'); + } + }, + + async updateSystemSettings() { + try { + const maxUploadSize = parseInt(this.systemSettings.maxUploadSizeMB) * 1024 * 1024; + + const response = await axios.post( + `${this.apiBase}/api/admin/settings`, + { max_upload_size: maxUploadSize }, + { headers: { Authorization: `Bearer ${this.token}` } } + ); + + if (response.data.success) { + this.showToast('success', '成功', '系统设置已更新'); + } + } catch (error) { + console.error('更新系统设置失败:', error); + this.showToast('error', '错误', '更新系统设置失败'); + } + } + }, + + mounted() { + // 阻止全局拖拽默认行为(防止拖到区域外打开新页面) + window.addEventListener("dragover", (e) => { + e.preventDefault(); + }); + window.addEventListener("drop", (e) => { + e.preventDefault(); + }); + + + // 添加全局 dragend 监听(拖拽结束时总是隐藏覆盖层) + window.addEventListener("dragend", () => { + this.isDragging = false; + }); + + // 添加 ESC 键监听(按 ESC 关闭拖拽覆盖层) + window.addEventListener("keydown", (e) => { + if (e.key === "Escape" && this.isDragging) { + this.isDragging = false; + } + }); + + // 检查URL参数 + this.checkUrlParams(); + // 检查登录状态 + this.checkLoginStatus(); + }, + + watch: { + currentView(newView) { + if (newView === 'shares') { + this.loadShares(); + } else if (newView === 'admin' && this.user?.is_admin) { + this.loadUsers(); + this.loadSystemSettings(); + this.loadPasswordResetRequests(); + this.loadServerStorageStats(); + } else if (newView === 'settings' && this.user && !this.user.is_admin) { + // 普通用户进入设置页面时加载SFTP配置 + this.loadFtpConfig(); + } + } + } +}).mount('#app'); diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..3563b19 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,191 @@ + + + + + + 玩玩云 - 主页 + + + + + + +
+

玩玩云管理平台

+

简单、安全、高效的文件管理解决方案
连接你的SFTP服务器,随时随地管理文件

+ +
+ +
+
+
+
连接你的SFTP
+
支持连接任何SFTP服务器,数据存储在你自己的服务器上,更安全可靠
+
+
+
+
轻松上传下载
+
网盘式界面,拖拽上传,快速下载,文件管理从未如此简单
+
+
+
+
一键分享
+
生成分享链接,可设置密码保护,轻松分享文件给朋友
+
+
+
+
安全可靠
+
JWT认证,密码加密存储,保护你的数据安全
+
+
+
+
响应式设计
+
完美支持桌面和移动设备,随时随地访问你的文件
+
+
+
+
分享统计
+
查看分享链接的访问次数和下载统计,了解分享效果
+
+
+ + + + diff --git a/frontend/libs/axios.min.js b/frontend/libs/axios.min.js new file mode 100644 index 0000000..2b482fa --- /dev/null +++ b/frontend/libs/axios.min.js @@ -0,0 +1,3 @@ +/*! Axios v1.13.2 Copyright (c) 2025 Matt Zabriskie and contributors */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).axios=t()}(this,(function(){"use strict";function e(e){var r,n;function o(r,n){try{var a=e[r](n),u=a.value,s=u instanceof t;Promise.resolve(s?u.v:u).then((function(t){if(s){var n="return"===r?"return":"next";if(!u.k||t.done)return o(n,t);t=e[n](t).value}i(a.done?"return":"normal",t)}),(function(e){o("throw",e)}))}catch(e){i("throw",e)}}function i(e,t){switch(e){case"return":r.resolve({value:t,done:!0});break;case"throw":r.reject(t);break;default:r.resolve({value:t,done:!1})}(r=r.next)?o(r.key,r.arg):n=null}this._invoke=function(e,t){return new Promise((function(i,a){var u={key:e,arg:t,resolve:i,reject:a,next:null};n?n=n.next=u:(r=n=u,o(e,t))}))},"function"!=typeof e.return&&(this.return=void 0)}function t(e,t){this.v=e,this.k=t}function r(e){var r={},n=!1;function o(r,o){return n=!0,o=new Promise((function(t){t(e[r](o))})),{done:!1,value:new t(o,1)}}return r["undefined"!=typeof Symbol&&Symbol.iterator||"@@iterator"]=function(){return this},r.next=function(e){return n?(n=!1,e):o("next",e)},"function"==typeof e.throw&&(r.throw=function(e){if(n)throw n=!1,e;return o("throw",e)}),"function"==typeof e.return&&(r.return=function(e){return n?(n=!1,e):o("return",e)}),r}function n(e){var t,r,n,i=2;for("undefined"!=typeof Symbol&&(r=Symbol.asyncIterator,n=Symbol.iterator);i--;){if(r&&null!=(t=e[r]))return t.call(e);if(n&&null!=(t=e[n]))return new o(t.call(e));r="@@asyncIterator",n="@@iterator"}throw new TypeError("Object is not async iterable")}function o(e){function t(e){if(Object(e)!==e)return Promise.reject(new TypeError(e+" is not an object."));var t=e.done;return Promise.resolve(e.value).then((function(e){return{value:e,done:t}}))}return o=function(e){this.s=e,this.n=e.next},o.prototype={s:null,n:null,next:function(){return t(this.n.apply(this.s,arguments))},return:function(e){var r=this.s.return;return void 0===r?Promise.resolve({value:e,done:!0}):t(r.apply(this.s,arguments))},throw:function(e){var r=this.s.return;return void 0===r?Promise.reject(e):t(r.apply(this.s,arguments))}},new o(e)}function i(e){return new t(e,0)}function a(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function u(e){for(var t=1;t=0;--i){var a=this.tryEntries[i],u=a.completion;if("root"===a.tryLoc)return o("end");if(a.tryLoc<=this.prev){var s=n.call(a,"catchLoc"),c=n.call(a,"finallyLoc");if(s&&c){if(this.prev=0;--r){var o=this.tryEntries[r];if(o.tryLoc<=this.prev&&n.call(o,"finallyLoc")&&this.prev=0;--t){var r=this.tryEntries[t];if(r.finallyLoc===e)return this.complete(r.completion,r.afterLoc),A(r),y}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.tryLoc===e){var n=r.completion;if("throw"===n.type){var o=n.arg;A(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(t,r,n){return this.delegate={iterator:L(t),resultName:r,nextLoc:n},"next"===this.method&&(this.arg=e),y}},t}function c(e){var t=function(e,t){if("object"!=typeof e||!e)return e;var r=e[Symbol.toPrimitive];if(void 0!==r){var n=r.call(e,t||"default");if("object"!=typeof n)return n;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==typeof t?t:String(t)}function f(e){return f="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},f(e)}function l(t){return function(){return new e(t.apply(this,arguments))}}function p(e,t,r,n,o,i,a){try{var u=e[i](a),s=u.value}catch(e){return void r(e)}u.done?t(s):Promise.resolve(s).then(n,o)}function d(e){return function(){var t=this,r=arguments;return new Promise((function(n,o){var i=e.apply(t,r);function a(e){p(i,n,o,a,u,"next",e)}function u(e){p(i,n,o,a,u,"throw",e)}a(void 0)}))}}function h(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function v(e,t){for(var r=0;re.length)&&(t=e.length);for(var r=0,n=new Array(t);r2&&void 0!==arguments[2]?arguments[2]:{},i=o.allOwnKeys,a=void 0!==i&&i;if(null!=e)if("object"!==f(e)&&(e=[e]),L(e))for(r=0,n=e.length;r0;)if(t===(r=n[o]).toLowerCase())return r;return null}var Q="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:global,Z=function(e){return!N(e)&&e!==Q};var ee,te=(ee="undefined"!=typeof Uint8Array&&R(Uint8Array),function(e){return ee&&e instanceof ee}),re=A("HTMLFormElement"),ne=function(e){var t=Object.prototype.hasOwnProperty;return function(e,r){return t.call(e,r)}}(),oe=A("RegExp"),ie=function(e,t){var r=Object.getOwnPropertyDescriptors(e),n={};$(r,(function(r,o){var i;!1!==(i=t(r,o,e))&&(n[o]=i||r)})),Object.defineProperties(e,n)};var ae,ue,se,ce,fe=A("AsyncFunction"),le=(ae="function"==typeof setImmediate,ue=F(Q.postMessage),ae?setImmediate:ue?(se="axios@".concat(Math.random()),ce=[],Q.addEventListener("message",(function(e){var t=e.source,r=e.data;t===Q&&r===se&&ce.length&&ce.shift()()}),!1),function(e){ce.push(e),Q.postMessage(se,"*")}):function(e){return setTimeout(e)}),pe="undefined"!=typeof queueMicrotask?queueMicrotask.bind(Q):"undefined"!=typeof process&&process.nextTick||le,de={isArray:L,isArrayBuffer:_,isBuffer:C,isFormData:function(e){var t;return e&&("function"==typeof FormData&&e instanceof FormData||F(e.append)&&("formdata"===(t=j(e))||"object"===t&&F(e.toString)&&"[object FormData]"===e.toString()))},isArrayBufferView:function(e){return"undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&_(e.buffer)},isString:U,isNumber:B,isBoolean:function(e){return!0===e||!1===e},isObject:D,isPlainObject:I,isEmptyObject:function(e){if(!D(e)||C(e))return!1;try{return 0===Object.keys(e).length&&Object.getPrototypeOf(e)===Object.prototype}catch(e){return!1}},isReadableStream:K,isRequest:V,isResponse:G,isHeaders:X,isUndefined:N,isDate:q,isFile:M,isBlob:z,isRegExp:oe,isFunction:F,isStream:function(e){return D(e)&&F(e.pipe)},isURLSearchParams:J,isTypedArray:te,isFileList:H,forEach:$,merge:function e(){for(var t=Z(this)&&this||{},r=t.caseless,n=t.skipUndefined,o={},i=function(t,i){var a=r&&Y(o,i)||i;I(o[a])&&I(t)?o[a]=e(o[a],t):I(t)?o[a]=e({},t):L(t)?o[a]=t.slice():n&&N(t)||(o[a]=t)},a=0,u=arguments.length;a3&&void 0!==arguments[3]?arguments[3]:{},o=n.allOwnKeys;return $(t,(function(t,n){r&&F(t)?e[n]=O(t,r):e[n]=t}),{allOwnKeys:o}),e},trim:function(e){return e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")},stripBOM:function(e){return 65279===e.charCodeAt(0)&&(e=e.slice(1)),e},inherits:function(e,t,r,n){e.prototype=Object.create(t.prototype,n),e.prototype.constructor=e,Object.defineProperty(e,"super",{value:t.prototype}),r&&Object.assign(e.prototype,r)},toFlatObject:function(e,t,r,n){var o,i,a,u={};if(t=t||{},null==e)return t;do{for(i=(o=Object.getOwnPropertyNames(e)).length;i-- >0;)a=o[i],n&&!n(a,e,t)||u[a]||(t[a]=e[a],u[a]=!0);e=!1!==r&&R(e)}while(e&&(!r||r(e,t))&&e!==Object.prototype);return t},kindOf:j,kindOfTest:A,endsWith:function(e,t,r){e=String(e),(void 0===r||r>e.length)&&(r=e.length),r-=t.length;var n=e.indexOf(t,r);return-1!==n&&n===r},toArray:function(e){if(!e)return null;if(L(e))return e;var t=e.length;if(!B(t))return null;for(var r=new Array(t);t-- >0;)r[t]=e[t];return r},forEachEntry:function(e,t){for(var r,n=(e&&e[k]).call(e);(r=n.next())&&!r.done;){var o=r.value;t.call(e,o[0],o[1])}},matchAll:function(e,t){for(var r,n=[];null!==(r=e.exec(t));)n.push(r);return n},isHTMLForm:re,hasOwnProperty:ne,hasOwnProp:ne,reduceDescriptors:ie,freezeMethods:function(e){ie(e,(function(t,r){if(F(e)&&-1!==["arguments","caller","callee"].indexOf(r))return!1;var n=e[r];F(n)&&(t.enumerable=!1,"writable"in t?t.writable=!1:t.set||(t.set=function(){throw Error("Can not rewrite read-only method '"+r+"'")}))}))},toObjectSet:function(e,t){var r={},n=function(e){e.forEach((function(e){r[e]=!0}))};return L(e)?n(e):n(String(e).split(t)),r},toCamelCase:function(e){return e.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,(function(e,t,r){return t.toUpperCase()+r}))},noop:function(){},toFiniteNumber:function(e,t){return null!=e&&Number.isFinite(e=+e)?e:t},findKey:Y,global:Q,isContextDefined:Z,isSpecCompliantForm:function(e){return!!(e&&F(e.append)&&"FormData"===e[T]&&e[k])},toJSONObject:function(e){var t=new Array(10);return function e(r,n){if(D(r)){if(t.indexOf(r)>=0)return;if(C(r))return r;if(!("toJSON"in r)){t[n]=r;var o=L(r)?[]:{};return $(r,(function(t,r){var i=e(t,n+1);!N(i)&&(o[r]=i)})),t[n]=void 0,o}}return r}(e,0)},isAsyncFn:fe,isThenable:function(e){return e&&(D(e)||F(e))&&F(e.then)&&F(e.catch)},setImmediate:le,asap:pe,isIterable:function(e){return null!=e&&F(e[k])}};function he(e,t,r,n,o){Error.call(this),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack,this.message=e,this.name="AxiosError",t&&(this.code=t),r&&(this.config=r),n&&(this.request=n),o&&(this.response=o,this.status=o.status?o.status:null)}de.inherits(he,Error,{toJSON:function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:de.toJSONObject(this.config),code:this.code,status:this.status}}});var ve=he.prototype,ye={};["ERR_BAD_OPTION_VALUE","ERR_BAD_OPTION","ECONNABORTED","ETIMEDOUT","ERR_NETWORK","ERR_FR_TOO_MANY_REDIRECTS","ERR_DEPRECATED","ERR_BAD_RESPONSE","ERR_BAD_REQUEST","ERR_CANCELED","ERR_NOT_SUPPORT","ERR_INVALID_URL"].forEach((function(e){ye[e]={value:e}})),Object.defineProperties(he,ye),Object.defineProperty(ve,"isAxiosError",{value:!0}),he.from=function(e,t,r,n,o,i){var a=Object.create(ve);de.toFlatObject(e,a,(function(e){return e!==Error.prototype}),(function(e){return"isAxiosError"!==e}));var u=e&&e.message?e.message:"Error",s=null==t&&e?e.code:t;return he.call(a,u,s,r,n,o),e&&null==a.cause&&Object.defineProperty(a,"cause",{value:e,configurable:!0}),a.name=e&&e.name||"Error",i&&Object.assign(a,i),a};function me(e){return de.isPlainObject(e)||de.isArray(e)}function be(e){return de.endsWith(e,"[]")?e.slice(0,-2):e}function ge(e,t,r){return e?e.concat(t).map((function(e,t){return e=be(e),!r&&t?"["+e+"]":e})).join(r?".":""):t}var we=de.toFlatObject(de,{},null,(function(e){return/^is[A-Z]/.test(e)}));function Ee(e,t,r){if(!de.isObject(e))throw new TypeError("target must be an object");t=t||new FormData;var n=(r=de.toFlatObject(r,{metaTokens:!0,dots:!1,indexes:!1},!1,(function(e,t){return!de.isUndefined(t[e])}))).metaTokens,o=r.visitor||c,i=r.dots,a=r.indexes,u=(r.Blob||"undefined"!=typeof Blob&&Blob)&&de.isSpecCompliantForm(t);if(!de.isFunction(o))throw new TypeError("visitor must be a function");function s(e){if(null===e)return"";if(de.isDate(e))return e.toISOString();if(de.isBoolean(e))return e.toString();if(!u&&de.isBlob(e))throw new he("Blob is not supported. Use a Buffer instead.");return de.isArrayBuffer(e)||de.isTypedArray(e)?u&&"function"==typeof Blob?new Blob([e]):Buffer.from(e):e}function c(e,r,o){var u=e;if(e&&!o&&"object"===f(e))if(de.endsWith(r,"{}"))r=n?r:r.slice(0,-2),e=JSON.stringify(e);else if(de.isArray(e)&&function(e){return de.isArray(e)&&!e.some(me)}(e)||(de.isFileList(e)||de.endsWith(r,"[]"))&&(u=de.toArray(e)))return r=be(r),u.forEach((function(e,n){!de.isUndefined(e)&&null!==e&&t.append(!0===a?ge([r],n,i):null===a?r:r+"[]",s(e))})),!1;return!!me(e)||(t.append(ge(o,r,i),s(e)),!1)}var l=[],p=Object.assign(we,{defaultVisitor:c,convertValue:s,isVisitable:me});if(!de.isObject(e))throw new TypeError("data must be an object");return function e(r,n){if(!de.isUndefined(r)){if(-1!==l.indexOf(r))throw Error("Circular reference detected in "+n.join("."));l.push(r),de.forEach(r,(function(r,i){!0===(!(de.isUndefined(r)||null===r)&&o.call(t,r,de.isString(i)?i.trim():i,n,p))&&e(r,n?n.concat(i):[i])})),l.pop()}}(e),t}function Oe(e){var t={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+","%00":"\0"};return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g,(function(e){return t[e]}))}function Se(e,t){this._pairs=[],e&&Ee(e,this,t)}var xe=Se.prototype;function Re(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+")}function ke(e,t,r){if(!t)return e;var n=r&&r.encode||Re;de.isFunction(r)&&(r={serialize:r});var o,i=r&&r.serialize;if(o=i?i(t,r):de.isURLSearchParams(t)?t.toString():new Se(t,r).toString(n)){var a=e.indexOf("#");-1!==a&&(e=e.slice(0,a)),e+=(-1===e.indexOf("?")?"?":"&")+o}return e}xe.append=function(e,t){this._pairs.push([e,t])},xe.toString=function(e){var t=e?function(t){return e.call(this,t,Oe)}:Oe;return this._pairs.map((function(e){return t(e[0])+"="+t(e[1])}),"").join("&")};var Te=function(){function e(){h(this,e),this.handlers=[]}return y(e,[{key:"use",value:function(e,t,r){return this.handlers.push({fulfilled:e,rejected:t,synchronous:!!r&&r.synchronous,runWhen:r?r.runWhen:null}),this.handlers.length-1}},{key:"eject",value:function(e){this.handlers[e]&&(this.handlers[e]=null)}},{key:"clear",value:function(){this.handlers&&(this.handlers=[])}},{key:"forEach",value:function(e){de.forEach(this.handlers,(function(t){null!==t&&e(t)}))}}]),e}(),je={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1},Ae={isBrowser:!0,classes:{URLSearchParams:"undefined"!=typeof URLSearchParams?URLSearchParams:Se,FormData:"undefined"!=typeof FormData?FormData:null,Blob:"undefined"!=typeof Blob?Blob:null},protocols:["http","https","file","blob","url","data"]},Pe="undefined"!=typeof window&&"undefined"!=typeof document,Le="object"===("undefined"==typeof navigator?"undefined":f(navigator))&&navigator||void 0,Ne=Pe&&(!Le||["ReactNative","NativeScript","NS"].indexOf(Le.product)<0),Ce="undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope&&"function"==typeof self.importScripts,_e=Pe&&window.location.href||"http://localhost",Ue=u(u({},Object.freeze({__proto__:null,hasBrowserEnv:Pe,hasStandardBrowserWebWorkerEnv:Ce,hasStandardBrowserEnv:Ne,navigator:Le,origin:_e})),Ae);function Fe(e){function t(e,r,n,o){var i=e[o++];if("__proto__"===i)return!0;var a=Number.isFinite(+i),u=o>=e.length;return i=!i&&de.isArray(n)?n.length:i,u?(de.hasOwnProp(n,i)?n[i]=[n[i],r]:n[i]=r,!a):(n[i]&&de.isObject(n[i])||(n[i]=[]),t(e,r,n[i],o)&&de.isArray(n[i])&&(n[i]=function(e){var t,r,n={},o=Object.keys(e),i=o.length;for(t=0;t-1,i=de.isObject(e);if(i&&de.isHTMLForm(e)&&(e=new FormData(e)),de.isFormData(e))return o?JSON.stringify(Fe(e)):e;if(de.isArrayBuffer(e)||de.isBuffer(e)||de.isStream(e)||de.isFile(e)||de.isBlob(e)||de.isReadableStream(e))return e;if(de.isArrayBufferView(e))return e.buffer;if(de.isURLSearchParams(e))return t.setContentType("application/x-www-form-urlencoded;charset=utf-8",!1),e.toString();if(i){if(n.indexOf("application/x-www-form-urlencoded")>-1)return function(e,t){return Ee(e,new Ue.classes.URLSearchParams,u({visitor:function(e,t,r,n){return Ue.isNode&&de.isBuffer(e)?(this.append(t,e.toString("base64")),!1):n.defaultVisitor.apply(this,arguments)}},t))}(e,this.formSerializer).toString();if((r=de.isFileList(e))||n.indexOf("multipart/form-data")>-1){var a=this.env&&this.env.FormData;return Ee(r?{"files[]":e}:e,a&&new a,this.formSerializer)}}return i||o?(t.setContentType("application/json",!1),function(e,t,r){if(de.isString(e))try{return(t||JSON.parse)(e),de.trim(e)}catch(e){if("SyntaxError"!==e.name)throw e}return(r||JSON.stringify)(e)}(e)):e}],transformResponse:[function(e){var t=this.transitional||Be.transitional,r=t&&t.forcedJSONParsing,n="json"===this.responseType;if(de.isResponse(e)||de.isReadableStream(e))return e;if(e&&de.isString(e)&&(r&&!this.responseType||n)){var o=!(t&&t.silentJSONParsing)&&n;try{return JSON.parse(e,this.parseReviver)}catch(e){if(o){if("SyntaxError"===e.name)throw he.from(e,he.ERR_BAD_RESPONSE,this,null,this.response);throw e}}}return e}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:Ue.classes.FormData,Blob:Ue.classes.Blob},validateStatus:function(e){return e>=200&&e<300},headers:{common:{Accept:"application/json, text/plain, */*","Content-Type":void 0}}};de.forEach(["delete","get","head","post","put","patch"],(function(e){Be.headers[e]={}}));var De=Be,Ie=de.toObjectSet(["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"]),qe=Symbol("internals");function Me(e){return e&&String(e).trim().toLowerCase()}function ze(e){return!1===e||null==e?e:de.isArray(e)?e.map(ze):String(e)}function He(e,t,r,n,o){return de.isFunction(n)?n.call(this,t,r):(o&&(t=r),de.isString(t)?de.isString(n)?-1!==t.indexOf(n):de.isRegExp(n)?n.test(t):void 0:void 0)}var Je=function(e,t){function r(e){h(this,r),e&&this.set(e)}return y(r,[{key:"set",value:function(e,t,r){var n=this;function o(e,t,r){var o=Me(t);if(!o)throw new Error("header name must be a non-empty string");var i=de.findKey(n,o);(!i||void 0===n[i]||!0===r||void 0===r&&!1!==n[i])&&(n[i||t]=ze(e))}var i=function(e,t){return de.forEach(e,(function(e,r){return o(e,r,t)}))};if(de.isPlainObject(e)||e instanceof this.constructor)i(e,t);else if(de.isString(e)&&(e=e.trim())&&!/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim()))i(function(e){var t,r,n,o={};return e&&e.split("\n").forEach((function(e){n=e.indexOf(":"),t=e.substring(0,n).trim().toLowerCase(),r=e.substring(n+1).trim(),!t||o[t]&&Ie[t]||("set-cookie"===t?o[t]?o[t].push(r):o[t]=[r]:o[t]=o[t]?o[t]+", "+r:r)})),o}(e),t);else if(de.isObject(e)&&de.isIterable(e)){var a,u,s,c={},f=function(e,t){var r="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!r){if(Array.isArray(e)||(r=w(e))||t&&e&&"number"==typeof e.length){r&&(e=r);var n=0,o=function(){};return{s:o,n:function(){return n>=e.length?{done:!0}:{done:!1,value:e[n++]}},e:function(e){throw e},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,a=!0,u=!1;return{s:function(){r=r.call(e)},n:function(){var e=r.next();return a=e.done,e},e:function(e){u=!0,i=e},f:function(){try{a||null==r.return||r.return()}finally{if(u)throw i}}}}(e);try{for(f.s();!(s=f.n()).done;){var l=s.value;if(!de.isArray(l))throw TypeError("Object iterator must return a key-value pair");c[u=l[0]]=(a=c[u])?de.isArray(a)?[].concat(g(a),[l[1]]):[a,l[1]]:l[1]}}catch(e){f.e(e)}finally{f.f()}i(c,t)}else null!=e&&o(t,e,r);return this}},{key:"get",value:function(e,t){if(e=Me(e)){var r=de.findKey(this,e);if(r){var n=this[r];if(!t)return n;if(!0===t)return function(e){for(var t,r=Object.create(null),n=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;t=n.exec(e);)r[t[1]]=t[2];return r}(n);if(de.isFunction(t))return t.call(this,n,r);if(de.isRegExp(t))return t.exec(n);throw new TypeError("parser must be boolean|regexp|function")}}}},{key:"has",value:function(e,t){if(e=Me(e)){var r=de.findKey(this,e);return!(!r||void 0===this[r]||t&&!He(0,this[r],r,t))}return!1}},{key:"delete",value:function(e,t){var r=this,n=!1;function o(e){if(e=Me(e)){var o=de.findKey(r,e);!o||t&&!He(0,r[o],o,t)||(delete r[o],n=!0)}}return de.isArray(e)?e.forEach(o):o(e),n}},{key:"clear",value:function(e){for(var t=Object.keys(this),r=t.length,n=!1;r--;){var o=t[r];e&&!He(0,this[o],o,e,!0)||(delete this[o],n=!0)}return n}},{key:"normalize",value:function(e){var t=this,r={};return de.forEach(this,(function(n,o){var i=de.findKey(r,o);if(i)return t[i]=ze(n),void delete t[o];var a=e?function(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(function(e,t,r){return t.toUpperCase()+r}))}(o):String(o).trim();a!==o&&delete t[o],t[a]=ze(n),r[a]=!0})),this}},{key:"concat",value:function(){for(var e,t=arguments.length,r=new Array(t),n=0;n1?r-1:0),o=1;o1&&void 0!==arguments[1]?arguments[1]:Date.now();o=i,r=null,n&&(clearTimeout(n),n=null),e.apply(void 0,g(t))};return[function(){for(var e=Date.now(),t=e-o,u=arguments.length,s=new Array(u),c=0;c=i?a(s,e):(r=s,n||(n=setTimeout((function(){n=null,a(r)}),i-t)))},function(){return r&&a(r)}]}de.inherits(Ge,he,{__CANCEL__:!0});var Qe=function(e,t){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:3,n=0,o=$e(50,250);return Ye((function(r){var i=r.loaded,a=r.lengthComputable?r.total:void 0,u=i-n,s=o(u);n=i;var c=m({loaded:i,total:a,progress:a?i/a:void 0,bytes:u,rate:s||void 0,estimated:s&&a&&i<=a?(a-i)/s:void 0,event:r,lengthComputable:null!=a},t?"download":"upload",!0);e(c)}),r)},Ze=function(e,t){var r=null!=e;return[function(n){return t[0]({lengthComputable:r,total:e,loaded:n})},t[1]]},et=function(e){return function(){for(var t=arguments.length,r=new Array(t),n=0;n1?t-1:0),n=1;n1?"since :\n"+s.map(xt).join("\n"):" "+xt(s[0]):"as no adapter specified"),"ERR_NOT_SUPPORT")}return n},adapters:St};function Tt(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new Ge(null,e)}function jt(e){return Tt(e),e.headers=We.from(e.headers),e.data=Ke.call(e,e.transformRequest),-1!==["post","put","patch"].indexOf(e.method)&&e.headers.setContentType("application/x-www-form-urlencoded",!1),kt.getAdapter(e.adapter||De.adapter,e)(e).then((function(t){return Tt(e),t.data=Ke.call(e,e.transformResponse,t),t.headers=We.from(t.headers),t}),(function(t){return Ve(t)||(Tt(e),t&&t.response&&(t.response.data=Ke.call(e,e.transformResponse,t.response),t.response.headers=We.from(t.response.headers))),Promise.reject(t)}))}var At="1.13.2",Pt={};["object","boolean","number","function","string","symbol"].forEach((function(e,t){Pt[e]=function(r){return f(r)===e||"a"+(t<1?"n ":" ")+e}}));var Lt={};Pt.transitional=function(e,t,r){function n(e,t){return"[Axios v1.13.2] Transitional option '"+e+"'"+t+(r?". "+r:"")}return function(r,o,i){if(!1===e)throw new he(n(o," has been removed"+(t?" in "+t:"")),he.ERR_DEPRECATED);return t&&!Lt[o]&&(Lt[o]=!0,console.warn(n(o," has been deprecated since v"+t+" and will be removed in the near future"))),!e||e(r,o,i)}},Pt.spelling=function(e){return function(t,r){return console.warn("".concat(r," is likely a misspelling of ").concat(e)),!0}};var Nt={assertOptions:function(e,t,r){if("object"!==f(e))throw new he("options must be an object",he.ERR_BAD_OPTION_VALUE);for(var n=Object.keys(e),o=n.length;o-- >0;){var i=n[o],a=t[i];if(a){var u=e[i],s=void 0===u||a(u,i,e);if(!0!==s)throw new he("option "+i+" must be "+s,he.ERR_BAD_OPTION_VALUE)}else if(!0!==r)throw new he("Unknown option "+i,he.ERR_BAD_OPTION)}},validators:Pt},Ct=Nt.validators,_t=function(){function e(t){h(this,e),this.defaults=t||{},this.interceptors={request:new Te,response:new Te}}var t;return y(e,[{key:"request",value:(t=d(s().mark((function e(t,r){var n,o;return s().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,this._request(t,r);case 3:return e.abrupt("return",e.sent);case 6:if(e.prev=6,e.t0=e.catch(0),e.t0 instanceof Error){n={},Error.captureStackTrace?Error.captureStackTrace(n):n=new Error,o=n.stack?n.stack.replace(/^.+\n/,""):"";try{e.t0.stack?o&&!String(e.t0.stack).endsWith(o.replace(/^.+\n.+\n/,""))&&(e.t0.stack+="\n"+o):e.t0.stack=o}catch(e){}}throw e.t0;case 10:case"end":return e.stop()}}),e,this,[[0,6]])}))),function(e,r){return t.apply(this,arguments)})},{key:"_request",value:function(e,t){"string"==typeof e?(t=t||{}).url=e:t=e||{};var r=t=it(this.defaults,t),n=r.transitional,o=r.paramsSerializer,i=r.headers;void 0!==n&&Nt.assertOptions(n,{silentJSONParsing:Ct.transitional(Ct.boolean),forcedJSONParsing:Ct.transitional(Ct.boolean),clarifyTimeoutError:Ct.transitional(Ct.boolean)},!1),null!=o&&(de.isFunction(o)?t.paramsSerializer={serialize:o}:Nt.assertOptions(o,{encode:Ct.function,serialize:Ct.function},!0)),void 0!==t.allowAbsoluteUrls||(void 0!==this.defaults.allowAbsoluteUrls?t.allowAbsoluteUrls=this.defaults.allowAbsoluteUrls:t.allowAbsoluteUrls=!0),Nt.assertOptions(t,{baseUrl:Ct.spelling("baseURL"),withXsrfToken:Ct.spelling("withXSRFToken")},!0),t.method=(t.method||this.defaults.method||"get").toLowerCase();var a=i&&de.merge(i.common,i[t.method]);i&&de.forEach(["delete","get","head","post","put","patch","common"],(function(e){delete i[e]})),t.headers=We.concat(a,i);var u=[],s=!0;this.interceptors.request.forEach((function(e){"function"==typeof e.runWhen&&!1===e.runWhen(t)||(s=s&&e.synchronous,u.unshift(e.fulfilled,e.rejected))}));var c,f=[];this.interceptors.response.forEach((function(e){f.push(e.fulfilled,e.rejected)}));var l,p=0;if(!s){var d=[jt.bind(this),void 0];for(d.unshift.apply(d,u),d.push.apply(d,f),l=d.length,c=Promise.resolve(t);p0;)n._listeners[t](e);n._listeners=null}})),this.promise.then=function(e){var t,r=new Promise((function(e){n.subscribe(e),t=e})).then(e);return r.cancel=function(){n.unsubscribe(t)},r},t((function(e,t,o){n.reason||(n.reason=new Ge(e,t,o),r(n.reason))}))}return y(e,[{key:"throwIfRequested",value:function(){if(this.reason)throw this.reason}},{key:"subscribe",value:function(e){this.reason?e(this.reason):this._listeners?this._listeners.push(e):this._listeners=[e]}},{key:"unsubscribe",value:function(e){if(this._listeners){var t=this._listeners.indexOf(e);-1!==t&&this._listeners.splice(t,1)}}},{key:"toAbortSignal",value:function(){var e=this,t=new AbortController,r=function(e){t.abort(e)};return this.subscribe(r),t.signal.unsubscribe=function(){return e.unsubscribe(r)},t.signal}}],[{key:"source",value:function(){var t;return{token:new e((function(e){t=e})),cancel:t}}}]),e}(),Bt=Ft;var Dt={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511,WebServerIsDown:521,ConnectionTimedOut:522,OriginIsUnreachable:523,TimeoutOccurred:524,SslHandshakeFailed:525,InvalidSslCertificate:526};Object.entries(Dt).forEach((function(e){var t=b(e,2),r=t[0],n=t[1];Dt[n]=r}));var It=Dt;var qt=function e(t){var r=new Ut(t),n=O(Ut.prototype.request,r);return de.extend(n,Ut.prototype,r,{allOwnKeys:!0}),de.extend(n,r,null,{allOwnKeys:!0}),n.create=function(r){return e(it(t,r))},n}(De);return qt.Axios=Ut,qt.CanceledError=Ge,qt.CancelToken=Bt,qt.isCancel=Ve,qt.VERSION=At,qt.toFormData=Ee,qt.AxiosError=he,qt.Cancel=qt.CanceledError,qt.all=function(e){return Promise.all(e)},qt.spread=function(e){return function(t){return e.apply(null,t)}},qt.isAxiosError=function(e){return de.isObject(e)&&!0===e.isAxiosError},qt.mergeConfig=it,qt.AxiosHeaders=We,qt.formToJSON=function(e){return Fe(de.isHTMLForm(e)?new FormData(e):e)},qt.getAdapter=kt.getAdapter,qt.HttpStatusCode=It,qt.default=qt,qt})); +//# sourceMappingURL=axios.min.js.map diff --git a/frontend/libs/fontawesome/css/all.min.css b/frontend/libs/fontawesome/css/all.min.css new file mode 100644 index 0000000..1f367c1 --- /dev/null +++ b/frontend/libs/fontawesome/css/all.min.css @@ -0,0 +1,9 @@ +/*! + * Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + * Copyright 2023 Fonticons, Inc. + */ +.fa{font-family:var(--fa-style-family,"Font Awesome 6 Free");font-weight:var(--fa-style,900)}.fa,.fa-brands,.fa-classic,.fa-regular,.fa-sharp,.fa-solid,.fab,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:var(--fa-display,inline-block);font-style:normal;font-variant:normal;line-height:1;text-rendering:auto}.fa-classic,.fa-regular,.fa-solid,.far,.fas{font-family:"Font Awesome 6 Free"}.fa-brands,.fab{font-family:"Font Awesome 6 Brands"}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-2xs{font-size:.625em;line-height:.1em;vertical-align:.225em}.fa-xs{font-size:.75em;line-height:.08333em;vertical-align:.125em}.fa-sm{font-size:.875em;line-height:.07143em;vertical-align:.05357em}.fa-lg{font-size:1.25em;line-height:.05em;vertical-align:-.075em}.fa-xl{font-size:1.5em;line-height:.04167em;vertical-align:-.125em}.fa-2xl{font-size:2em;line-height:.03125em;vertical-align:-.1875em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:var(--fa-li-margin,2.5em);padding-left:0}.fa-ul>li{position:relative}.fa-li{left:calc(var(--fa-li-width, 2em)*-1);position:absolute;text-align:center;width:var(--fa-li-width,2em);line-height:inherit}.fa-border{border-radius:var(--fa-border-radius,.1em);border:var(--fa-border-width,.08em) var(--fa-border-style,solid) var(--fa-border-color,#eee);padding:var(--fa-border-padding,.2em .25em .15em)}.fa-pull-left{float:left;margin-right:var(--fa-pull-margin,.3em)}.fa-pull-right{float:right;margin-left:var(--fa-pull-margin,.3em)}.fa-beat{-webkit-animation-name:fa-beat;animation-name:fa-beat;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,ease-in-out);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-bounce{-webkit-animation-name:fa-bounce;animation-name:fa-bounce;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1))}.fa-fade{-webkit-animation-name:fa-fade;animation-name:fa-fade;-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-beat-fade,.fa-fade{-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s)}.fa-beat-fade{-webkit-animation-name:fa-beat-fade;animation-name:fa-beat-fade;-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-flip{-webkit-animation-name:fa-flip;animation-name:fa-flip;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,ease-in-out);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-shake{-webkit-animation-name:fa-shake;animation-name:fa-shake;-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,linear);animation-timing-function:var(--fa-animation-timing,linear)}.fa-shake,.fa-spin{-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal)}.fa-spin{-webkit-animation-name:fa-spin;animation-name:fa-spin;-webkit-animation-duration:var(--fa-animation-duration,2s);animation-duration:var(--fa-animation-duration,2s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,linear);animation-timing-function:var(--fa-animation-timing,linear)}.fa-spin-reverse{--fa-animation-direction:reverse}.fa-pulse,.fa-spin-pulse{-webkit-animation-name:fa-spin;animation-name:fa-spin;-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,steps(8));animation-timing-function:var(--fa-animation-timing,steps(8))}@media (prefers-reduced-motion:reduce){.fa-beat,.fa-beat-fade,.fa-bounce,.fa-fade,.fa-flip,.fa-pulse,.fa-shake,.fa-spin,.fa-spin-pulse{-webkit-animation-delay:-1ms;animation-delay:-1ms;-webkit-animation-duration:1ms;animation-duration:1ms;-webkit-animation-iteration-count:1;animation-iteration-count:1;-webkit-transition-delay:0s;transition-delay:0s;-webkit-transition-duration:0s;transition-duration:0s}}@-webkit-keyframes fa-beat{0%,90%{-webkit-transform:scale(1);transform:scale(1)}45%{-webkit-transform:scale(var(--fa-beat-scale,1.25));transform:scale(var(--fa-beat-scale,1.25))}}@keyframes fa-beat{0%,90%{-webkit-transform:scale(1);transform:scale(1)}45%{-webkit-transform:scale(var(--fa-beat-scale,1.25));transform:scale(var(--fa-beat-scale,1.25))}}@-webkit-keyframes fa-bounce{0%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}10%{-webkit-transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0);transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{-webkit-transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em));transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{-webkit-transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0);transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{-webkit-transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em));transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em))}64%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}to{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}}@keyframes fa-bounce{0%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}10%{-webkit-transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0);transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{-webkit-transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em));transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{-webkit-transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0);transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{-webkit-transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em));transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em))}64%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}to{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}}@-webkit-keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@-webkit-keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity,.4);-webkit-transform:scale(1);transform:scale(1)}50%{opacity:1;-webkit-transform:scale(var(--fa-beat-fade-scale,1.125));transform:scale(var(--fa-beat-fade-scale,1.125))}}@keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity,.4);-webkit-transform:scale(1);transform:scale(1)}50%{opacity:1;-webkit-transform:scale(var(--fa-beat-fade-scale,1.125));transform:scale(var(--fa-beat-fade-scale,1.125))}}@-webkit-keyframes fa-flip{50%{-webkit-transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg));transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@keyframes fa-flip{50%{-webkit-transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg));transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@-webkit-keyframes fa-shake{0%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}4%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}8%,24%{-webkit-transform:rotate(-18deg);transform:rotate(-18deg)}12%,28%{-webkit-transform:rotate(18deg);transform:rotate(18deg)}16%{-webkit-transform:rotate(-22deg);transform:rotate(-22deg)}20%{-webkit-transform:rotate(22deg);transform:rotate(22deg)}32%{-webkit-transform:rotate(-12deg);transform:rotate(-12deg)}36%{-webkit-transform:rotate(12deg);transform:rotate(12deg)}40%,to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes fa-shake{0%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}4%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}8%,24%{-webkit-transform:rotate(-18deg);transform:rotate(-18deg)}12%,28%{-webkit-transform:rotate(18deg);transform:rotate(18deg)}16%{-webkit-transform:rotate(-22deg);transform:rotate(-22deg)}20%{-webkit-transform:rotate(22deg);transform:rotate(22deg)}32%{-webkit-transform:rotate(-12deg);transform:rotate(-12deg)}36%{-webkit-transform:rotate(12deg);transform:rotate(12deg)}40%,to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}.fa-rotate-by{-webkit-transform:rotate(var(--fa-rotate-angle,none));transform:rotate(var(--fa-rotate-angle,none))}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%;z-index:var(--fa-stack-z-index,auto)}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:var(--fa-inverse,#fff)} + +.fa-0:before{content:"\30"}.fa-1:before{content:"\31"}.fa-2:before{content:"\32"}.fa-3:before{content:"\33"}.fa-4:before{content:"\34"}.fa-5:before{content:"\35"}.fa-6:before{content:"\36"}.fa-7:before{content:"\37"}.fa-8:before{content:"\38"}.fa-9:before{content:"\39"}.fa-fill-drip:before{content:"\f576"}.fa-arrows-to-circle:before{content:"\e4bd"}.fa-chevron-circle-right:before,.fa-circle-chevron-right:before{content:"\f138"}.fa-at:before{content:"\40"}.fa-trash-alt:before,.fa-trash-can:before{content:"\f2ed"}.fa-text-height:before{content:"\f034"}.fa-user-times:before,.fa-user-xmark:before{content:"\f235"}.fa-stethoscope:before{content:"\f0f1"}.fa-comment-alt:before,.fa-message:before{content:"\f27a"}.fa-info:before{content:"\f129"}.fa-compress-alt:before,.fa-down-left-and-up-right-to-center:before{content:"\f422"}.fa-explosion:before{content:"\e4e9"}.fa-file-alt:before,.fa-file-lines:before,.fa-file-text:before{content:"\f15c"}.fa-wave-square:before{content:"\f83e"}.fa-ring:before{content:"\f70b"}.fa-building-un:before{content:"\e4d9"}.fa-dice-three:before{content:"\f527"}.fa-calendar-alt:before,.fa-calendar-days:before{content:"\f073"}.fa-anchor-circle-check:before{content:"\e4aa"}.fa-building-circle-arrow-right:before{content:"\e4d1"}.fa-volleyball-ball:before,.fa-volleyball:before{content:"\f45f"}.fa-arrows-up-to-line:before{content:"\e4c2"}.fa-sort-desc:before,.fa-sort-down:before{content:"\f0dd"}.fa-circle-minus:before,.fa-minus-circle:before{content:"\f056"}.fa-door-open:before{content:"\f52b"}.fa-right-from-bracket:before,.fa-sign-out-alt:before{content:"\f2f5"}.fa-atom:before{content:"\f5d2"}.fa-soap:before{content:"\e06e"}.fa-heart-music-camera-bolt:before,.fa-icons:before{content:"\f86d"}.fa-microphone-alt-slash:before,.fa-microphone-lines-slash:before{content:"\f539"}.fa-bridge-circle-check:before{content:"\e4c9"}.fa-pump-medical:before{content:"\e06a"}.fa-fingerprint:before{content:"\f577"}.fa-hand-point-right:before{content:"\f0a4"}.fa-magnifying-glass-location:before,.fa-search-location:before{content:"\f689"}.fa-forward-step:before,.fa-step-forward:before{content:"\f051"}.fa-face-smile-beam:before,.fa-smile-beam:before{content:"\f5b8"}.fa-flag-checkered:before{content:"\f11e"}.fa-football-ball:before,.fa-football:before{content:"\f44e"}.fa-school-circle-exclamation:before{content:"\e56c"}.fa-crop:before{content:"\f125"}.fa-angle-double-down:before,.fa-angles-down:before{content:"\f103"}.fa-users-rectangle:before{content:"\e594"}.fa-people-roof:before{content:"\e537"}.fa-people-line:before{content:"\e534"}.fa-beer-mug-empty:before,.fa-beer:before{content:"\f0fc"}.fa-diagram-predecessor:before{content:"\e477"}.fa-arrow-up-long:before,.fa-long-arrow-up:before{content:"\f176"}.fa-burn:before,.fa-fire-flame-simple:before{content:"\f46a"}.fa-male:before,.fa-person:before{content:"\f183"}.fa-laptop:before{content:"\f109"}.fa-file-csv:before{content:"\f6dd"}.fa-menorah:before{content:"\f676"}.fa-truck-plane:before{content:"\e58f"}.fa-record-vinyl:before{content:"\f8d9"}.fa-face-grin-stars:before,.fa-grin-stars:before{content:"\f587"}.fa-bong:before{content:"\f55c"}.fa-pastafarianism:before,.fa-spaghetti-monster-flying:before{content:"\f67b"}.fa-arrow-down-up-across-line:before{content:"\e4af"}.fa-spoon:before,.fa-utensil-spoon:before{content:"\f2e5"}.fa-jar-wheat:before{content:"\e517"}.fa-envelopes-bulk:before,.fa-mail-bulk:before{content:"\f674"}.fa-file-circle-exclamation:before{content:"\e4eb"}.fa-circle-h:before,.fa-hospital-symbol:before{content:"\f47e"}.fa-pager:before{content:"\f815"}.fa-address-book:before,.fa-contact-book:before{content:"\f2b9"}.fa-strikethrough:before{content:"\f0cc"}.fa-k:before{content:"\4b"}.fa-landmark-flag:before{content:"\e51c"}.fa-pencil-alt:before,.fa-pencil:before{content:"\f303"}.fa-backward:before{content:"\f04a"}.fa-caret-right:before{content:"\f0da"}.fa-comments:before{content:"\f086"}.fa-file-clipboard:before,.fa-paste:before{content:"\f0ea"}.fa-code-pull-request:before{content:"\e13c"}.fa-clipboard-list:before{content:"\f46d"}.fa-truck-loading:before,.fa-truck-ramp-box:before{content:"\f4de"}.fa-user-check:before{content:"\f4fc"}.fa-vial-virus:before{content:"\e597"}.fa-sheet-plastic:before{content:"\e571"}.fa-blog:before{content:"\f781"}.fa-user-ninja:before{content:"\f504"}.fa-person-arrow-up-from-line:before{content:"\e539"}.fa-scroll-torah:before,.fa-torah:before{content:"\f6a0"}.fa-broom-ball:before,.fa-quidditch-broom-ball:before,.fa-quidditch:before{content:"\f458"}.fa-toggle-off:before{content:"\f204"}.fa-archive:before,.fa-box-archive:before{content:"\f187"}.fa-person-drowning:before{content:"\e545"}.fa-arrow-down-9-1:before,.fa-sort-numeric-desc:before,.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-face-grin-tongue-squint:before,.fa-grin-tongue-squint:before{content:"\f58a"}.fa-spray-can:before{content:"\f5bd"}.fa-truck-monster:before{content:"\f63b"}.fa-w:before{content:"\57"}.fa-earth-africa:before,.fa-globe-africa:before{content:"\f57c"}.fa-rainbow:before{content:"\f75b"}.fa-circle-notch:before{content:"\f1ce"}.fa-tablet-alt:before,.fa-tablet-screen-button:before{content:"\f3fa"}.fa-paw:before{content:"\f1b0"}.fa-cloud:before{content:"\f0c2"}.fa-trowel-bricks:before{content:"\e58a"}.fa-face-flushed:before,.fa-flushed:before{content:"\f579"}.fa-hospital-user:before{content:"\f80d"}.fa-tent-arrow-left-right:before{content:"\e57f"}.fa-gavel:before,.fa-legal:before{content:"\f0e3"}.fa-binoculars:before{content:"\f1e5"}.fa-microphone-slash:before{content:"\f131"}.fa-box-tissue:before{content:"\e05b"}.fa-motorcycle:before{content:"\f21c"}.fa-bell-concierge:before,.fa-concierge-bell:before{content:"\f562"}.fa-pen-ruler:before,.fa-pencil-ruler:before{content:"\f5ae"}.fa-people-arrows-left-right:before,.fa-people-arrows:before{content:"\e068"}.fa-mars-and-venus-burst:before{content:"\e523"}.fa-caret-square-right:before,.fa-square-caret-right:before{content:"\f152"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-sun-plant-wilt:before{content:"\e57a"}.fa-toilets-portable:before{content:"\e584"}.fa-hockey-puck:before{content:"\f453"}.fa-table:before{content:"\f0ce"}.fa-magnifying-glass-arrow-right:before{content:"\e521"}.fa-digital-tachograph:before,.fa-tachograph-digital:before{content:"\f566"}.fa-users-slash:before{content:"\e073"}.fa-clover:before{content:"\e139"}.fa-mail-reply:before,.fa-reply:before{content:"\f3e5"}.fa-star-and-crescent:before{content:"\f699"}.fa-house-fire:before{content:"\e50c"}.fa-minus-square:before,.fa-square-minus:before{content:"\f146"}.fa-helicopter:before{content:"\f533"}.fa-compass:before{content:"\f14e"}.fa-caret-square-down:before,.fa-square-caret-down:before{content:"\f150"}.fa-file-circle-question:before{content:"\e4ef"}.fa-laptop-code:before{content:"\f5fc"}.fa-swatchbook:before{content:"\f5c3"}.fa-prescription-bottle:before{content:"\f485"}.fa-bars:before,.fa-navicon:before{content:"\f0c9"}.fa-people-group:before{content:"\e533"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-heart-broken:before,.fa-heart-crack:before{content:"\f7a9"}.fa-external-link-square-alt:before,.fa-square-up-right:before{content:"\f360"}.fa-face-kiss-beam:before,.fa-kiss-beam:before{content:"\f597"}.fa-film:before{content:"\f008"}.fa-ruler-horizontal:before{content:"\f547"}.fa-people-robbery:before{content:"\e536"}.fa-lightbulb:before{content:"\f0eb"}.fa-caret-left:before{content:"\f0d9"}.fa-circle-exclamation:before,.fa-exclamation-circle:before{content:"\f06a"}.fa-school-circle-xmark:before{content:"\e56d"}.fa-arrow-right-from-bracket:before,.fa-sign-out:before{content:"\f08b"}.fa-chevron-circle-down:before,.fa-circle-chevron-down:before{content:"\f13a"}.fa-unlock-alt:before,.fa-unlock-keyhole:before{content:"\f13e"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-headphones-alt:before,.fa-headphones-simple:before{content:"\f58f"}.fa-sitemap:before{content:"\f0e8"}.fa-circle-dollar-to-slot:before,.fa-donate:before{content:"\f4b9"}.fa-memory:before{content:"\f538"}.fa-road-spikes:before{content:"\e568"}.fa-fire-burner:before{content:"\e4f1"}.fa-flag:before{content:"\f024"}.fa-hanukiah:before{content:"\f6e6"}.fa-feather:before{content:"\f52d"}.fa-volume-down:before,.fa-volume-low:before{content:"\f027"}.fa-comment-slash:before{content:"\f4b3"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-compress:before{content:"\f066"}.fa-wheat-alt:before,.fa-wheat-awn:before{content:"\e2cd"}.fa-ankh:before{content:"\f644"}.fa-hands-holding-child:before{content:"\e4fa"}.fa-asterisk:before{content:"\2a"}.fa-check-square:before,.fa-square-check:before{content:"\f14a"}.fa-peseta-sign:before{content:"\e221"}.fa-header:before,.fa-heading:before{content:"\f1dc"}.fa-ghost:before{content:"\f6e2"}.fa-list-squares:before,.fa-list:before{content:"\f03a"}.fa-phone-square-alt:before,.fa-square-phone-flip:before{content:"\f87b"}.fa-cart-plus:before{content:"\f217"}.fa-gamepad:before{content:"\f11b"}.fa-circle-dot:before,.fa-dot-circle:before{content:"\f192"}.fa-dizzy:before,.fa-face-dizzy:before{content:"\f567"}.fa-egg:before{content:"\f7fb"}.fa-house-medical-circle-xmark:before{content:"\e513"}.fa-campground:before{content:"\f6bb"}.fa-folder-plus:before{content:"\f65e"}.fa-futbol-ball:before,.fa-futbol:before,.fa-soccer-ball:before{content:"\f1e3"}.fa-paint-brush:before,.fa-paintbrush:before{content:"\f1fc"}.fa-lock:before{content:"\f023"}.fa-gas-pump:before{content:"\f52f"}.fa-hot-tub-person:before,.fa-hot-tub:before{content:"\f593"}.fa-map-location:before,.fa-map-marked:before{content:"\f59f"}.fa-house-flood-water:before{content:"\e50e"}.fa-tree:before{content:"\f1bb"}.fa-bridge-lock:before{content:"\e4cc"}.fa-sack-dollar:before{content:"\f81d"}.fa-edit:before,.fa-pen-to-square:before{content:"\f044"}.fa-car-side:before{content:"\f5e4"}.fa-share-alt:before,.fa-share-nodes:before{content:"\f1e0"}.fa-heart-circle-minus:before{content:"\e4ff"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-microscope:before{content:"\f610"}.fa-sink:before{content:"\e06d"}.fa-bag-shopping:before,.fa-shopping-bag:before{content:"\f290"}.fa-arrow-down-z-a:before,.fa-sort-alpha-desc:before,.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-mitten:before{content:"\f7b5"}.fa-person-rays:before{content:"\e54d"}.fa-users:before{content:"\f0c0"}.fa-eye-slash:before{content:"\f070"}.fa-flask-vial:before{content:"\e4f3"}.fa-hand-paper:before,.fa-hand:before{content:"\f256"}.fa-om:before{content:"\f679"}.fa-worm:before{content:"\e599"}.fa-house-circle-xmark:before{content:"\e50b"}.fa-plug:before{content:"\f1e6"}.fa-chevron-up:before{content:"\f077"}.fa-hand-spock:before{content:"\f259"}.fa-stopwatch:before{content:"\f2f2"}.fa-face-kiss:before,.fa-kiss:before{content:"\f596"}.fa-bridge-circle-xmark:before{content:"\e4cb"}.fa-face-grin-tongue:before,.fa-grin-tongue:before{content:"\f589"}.fa-chess-bishop:before{content:"\f43a"}.fa-face-grin-wink:before,.fa-grin-wink:before{content:"\f58c"}.fa-deaf:before,.fa-deafness:before,.fa-ear-deaf:before,.fa-hard-of-hearing:before{content:"\f2a4"}.fa-road-circle-check:before{content:"\e564"}.fa-dice-five:before{content:"\f523"}.fa-rss-square:before,.fa-square-rss:before{content:"\f143"}.fa-land-mine-on:before{content:"\e51b"}.fa-i-cursor:before{content:"\f246"}.fa-stamp:before{content:"\f5bf"}.fa-stairs:before{content:"\e289"}.fa-i:before{content:"\49"}.fa-hryvnia-sign:before,.fa-hryvnia:before{content:"\f6f2"}.fa-pills:before{content:"\f484"}.fa-face-grin-wide:before,.fa-grin-alt:before{content:"\f581"}.fa-tooth:before{content:"\f5c9"}.fa-v:before{content:"\56"}.fa-bangladeshi-taka-sign:before{content:"\e2e6"}.fa-bicycle:before{content:"\f206"}.fa-rod-asclepius:before,.fa-rod-snake:before,.fa-staff-aesculapius:before,.fa-staff-snake:before{content:"\e579"}.fa-head-side-cough-slash:before{content:"\e062"}.fa-ambulance:before,.fa-truck-medical:before{content:"\f0f9"}.fa-wheat-awn-circle-exclamation:before{content:"\e598"}.fa-snowman:before{content:"\f7d0"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-road-barrier:before{content:"\e562"}.fa-school:before{content:"\f549"}.fa-igloo:before{content:"\f7ae"}.fa-joint:before{content:"\f595"}.fa-angle-right:before{content:"\f105"}.fa-horse:before{content:"\f6f0"}.fa-q:before{content:"\51"}.fa-g:before{content:"\47"}.fa-notes-medical:before{content:"\f481"}.fa-temperature-2:before,.fa-temperature-half:before,.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-dong-sign:before{content:"\e169"}.fa-capsules:before{content:"\f46b"}.fa-poo-bolt:before,.fa-poo-storm:before{content:"\f75a"}.fa-face-frown-open:before,.fa-frown-open:before{content:"\f57a"}.fa-hand-point-up:before{content:"\f0a6"}.fa-money-bill:before{content:"\f0d6"}.fa-bookmark:before{content:"\f02e"}.fa-align-justify:before{content:"\f039"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-helmet-un:before{content:"\e503"}.fa-bullseye:before{content:"\f140"}.fa-bacon:before{content:"\f7e5"}.fa-hand-point-down:before{content:"\f0a7"}.fa-arrow-up-from-bracket:before{content:"\e09a"}.fa-folder-blank:before,.fa-folder:before{content:"\f07b"}.fa-file-medical-alt:before,.fa-file-waveform:before{content:"\f478"}.fa-radiation:before{content:"\f7b9"}.fa-chart-simple:before{content:"\e473"}.fa-mars-stroke:before{content:"\f229"}.fa-vial:before{content:"\f492"}.fa-dashboard:before,.fa-gauge-med:before,.fa-gauge:before,.fa-tachometer-alt-average:before{content:"\f624"}.fa-magic-wand-sparkles:before,.fa-wand-magic-sparkles:before{content:"\e2ca"}.fa-e:before{content:"\45"}.fa-pen-alt:before,.fa-pen-clip:before{content:"\f305"}.fa-bridge-circle-exclamation:before{content:"\e4ca"}.fa-user:before{content:"\f007"}.fa-school-circle-check:before{content:"\e56b"}.fa-dumpster:before{content:"\f793"}.fa-shuttle-van:before,.fa-van-shuttle:before{content:"\f5b6"}.fa-building-user:before{content:"\e4da"}.fa-caret-square-left:before,.fa-square-caret-left:before{content:"\f191"}.fa-highlighter:before{content:"\f591"}.fa-key:before{content:"\f084"}.fa-bullhorn:before{content:"\f0a1"}.fa-globe:before{content:"\f0ac"}.fa-synagogue:before{content:"\f69b"}.fa-person-half-dress:before{content:"\e548"}.fa-road-bridge:before{content:"\e563"}.fa-location-arrow:before{content:"\f124"}.fa-c:before{content:"\43"}.fa-tablet-button:before{content:"\f10a"}.fa-building-lock:before{content:"\e4d6"}.fa-pizza-slice:before{content:"\f818"}.fa-money-bill-wave:before{content:"\f53a"}.fa-area-chart:before,.fa-chart-area:before{content:"\f1fe"}.fa-house-flag:before{content:"\e50d"}.fa-person-circle-minus:before{content:"\e540"}.fa-ban:before,.fa-cancel:before{content:"\f05e"}.fa-camera-rotate:before{content:"\e0d8"}.fa-air-freshener:before,.fa-spray-can-sparkles:before{content:"\f5d0"}.fa-star:before{content:"\f005"}.fa-repeat:before{content:"\f363"}.fa-cross:before{content:"\f654"}.fa-box:before{content:"\f466"}.fa-venus-mars:before{content:"\f228"}.fa-arrow-pointer:before,.fa-mouse-pointer:before{content:"\f245"}.fa-expand-arrows-alt:before,.fa-maximize:before{content:"\f31e"}.fa-charging-station:before{content:"\f5e7"}.fa-shapes:before,.fa-triangle-circle-square:before{content:"\f61f"}.fa-random:before,.fa-shuffle:before{content:"\f074"}.fa-person-running:before,.fa-running:before{content:"\f70c"}.fa-mobile-retro:before{content:"\e527"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-spider:before{content:"\f717"}.fa-hands-bound:before{content:"\e4f9"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-plane-circle-exclamation:before{content:"\e556"}.fa-x-ray:before{content:"\f497"}.fa-spell-check:before{content:"\f891"}.fa-slash:before{content:"\f715"}.fa-computer-mouse:before,.fa-mouse:before{content:"\f8cc"}.fa-arrow-right-to-bracket:before,.fa-sign-in:before{content:"\f090"}.fa-shop-slash:before,.fa-store-alt-slash:before{content:"\e070"}.fa-server:before{content:"\f233"}.fa-virus-covid-slash:before{content:"\e4a9"}.fa-shop-lock:before{content:"\e4a5"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-blender-phone:before{content:"\f6b6"}.fa-building-wheat:before{content:"\e4db"}.fa-person-breastfeeding:before{content:"\e53a"}.fa-right-to-bracket:before,.fa-sign-in-alt:before{content:"\f2f6"}.fa-venus:before{content:"\f221"}.fa-passport:before{content:"\f5ab"}.fa-heart-pulse:before,.fa-heartbeat:before{content:"\f21e"}.fa-people-carry-box:before,.fa-people-carry:before{content:"\f4ce"}.fa-temperature-high:before{content:"\f769"}.fa-microchip:before{content:"\f2db"}.fa-crown:before{content:"\f521"}.fa-weight-hanging:before{content:"\f5cd"}.fa-xmarks-lines:before{content:"\e59a"}.fa-file-prescription:before{content:"\f572"}.fa-weight-scale:before,.fa-weight:before{content:"\f496"}.fa-user-friends:before,.fa-user-group:before{content:"\f500"}.fa-arrow-up-a-z:before,.fa-sort-alpha-up:before{content:"\f15e"}.fa-chess-knight:before{content:"\f441"}.fa-face-laugh-squint:before,.fa-laugh-squint:before{content:"\f59b"}.fa-wheelchair:before{content:"\f193"}.fa-arrow-circle-up:before,.fa-circle-arrow-up:before{content:"\f0aa"}.fa-toggle-on:before{content:"\f205"}.fa-person-walking:before,.fa-walking:before{content:"\f554"}.fa-l:before{content:"\4c"}.fa-fire:before{content:"\f06d"}.fa-bed-pulse:before,.fa-procedures:before{content:"\f487"}.fa-shuttle-space:before,.fa-space-shuttle:before{content:"\f197"}.fa-face-laugh:before,.fa-laugh:before{content:"\f599"}.fa-folder-open:before{content:"\f07c"}.fa-heart-circle-plus:before{content:"\e500"}.fa-code-fork:before{content:"\e13b"}.fa-city:before{content:"\f64f"}.fa-microphone-alt:before,.fa-microphone-lines:before{content:"\f3c9"}.fa-pepper-hot:before{content:"\f816"}.fa-unlock:before{content:"\f09c"}.fa-colon-sign:before{content:"\e140"}.fa-headset:before{content:"\f590"}.fa-store-slash:before{content:"\e071"}.fa-road-circle-xmark:before{content:"\e566"}.fa-user-minus:before{content:"\f503"}.fa-mars-stroke-up:before,.fa-mars-stroke-v:before{content:"\f22a"}.fa-champagne-glasses:before,.fa-glass-cheers:before{content:"\f79f"}.fa-clipboard:before{content:"\f328"}.fa-house-circle-exclamation:before{content:"\e50a"}.fa-file-arrow-up:before,.fa-file-upload:before{content:"\f574"}.fa-wifi-3:before,.fa-wifi-strong:before,.fa-wifi:before{content:"\f1eb"}.fa-bath:before,.fa-bathtub:before{content:"\f2cd"}.fa-underline:before{content:"\f0cd"}.fa-user-edit:before,.fa-user-pen:before{content:"\f4ff"}.fa-signature:before{content:"\f5b7"}.fa-stroopwafel:before{content:"\f551"}.fa-bold:before{content:"\f032"}.fa-anchor-lock:before{content:"\e4ad"}.fa-building-ngo:before{content:"\e4d7"}.fa-manat-sign:before{content:"\e1d5"}.fa-not-equal:before{content:"\f53e"}.fa-border-style:before,.fa-border-top-left:before{content:"\f853"}.fa-map-location-dot:before,.fa-map-marked-alt:before{content:"\f5a0"}.fa-jedi:before{content:"\f669"}.fa-poll:before,.fa-square-poll-vertical:before{content:"\f681"}.fa-mug-hot:before{content:"\f7b6"}.fa-battery-car:before,.fa-car-battery:before{content:"\f5df"}.fa-gift:before{content:"\f06b"}.fa-dice-two:before{content:"\f528"}.fa-chess-queen:before{content:"\f445"}.fa-glasses:before{content:"\f530"}.fa-chess-board:before{content:"\f43c"}.fa-building-circle-check:before{content:"\e4d2"}.fa-person-chalkboard:before{content:"\e53d"}.fa-mars-stroke-h:before,.fa-mars-stroke-right:before{content:"\f22b"}.fa-hand-back-fist:before,.fa-hand-rock:before{content:"\f255"}.fa-caret-square-up:before,.fa-square-caret-up:before{content:"\f151"}.fa-cloud-showers-water:before{content:"\e4e4"}.fa-bar-chart:before,.fa-chart-bar:before{content:"\f080"}.fa-hands-bubbles:before,.fa-hands-wash:before{content:"\e05e"}.fa-less-than-equal:before{content:"\f537"}.fa-train:before{content:"\f238"}.fa-eye-low-vision:before,.fa-low-vision:before{content:"\f2a8"}.fa-crow:before{content:"\f520"}.fa-sailboat:before{content:"\e445"}.fa-window-restore:before{content:"\f2d2"}.fa-plus-square:before,.fa-square-plus:before{content:"\f0fe"}.fa-torii-gate:before{content:"\f6a1"}.fa-frog:before{content:"\f52e"}.fa-bucket:before{content:"\e4cf"}.fa-image:before{content:"\f03e"}.fa-microphone:before{content:"\f130"}.fa-cow:before{content:"\f6c8"}.fa-caret-up:before{content:"\f0d8"}.fa-screwdriver:before{content:"\f54a"}.fa-folder-closed:before{content:"\e185"}.fa-house-tsunami:before{content:"\e515"}.fa-square-nfi:before{content:"\e576"}.fa-arrow-up-from-ground-water:before{content:"\e4b5"}.fa-glass-martini-alt:before,.fa-martini-glass:before{content:"\f57b"}.fa-rotate-back:before,.fa-rotate-backward:before,.fa-rotate-left:before,.fa-undo-alt:before{content:"\f2ea"}.fa-columns:before,.fa-table-columns:before{content:"\f0db"}.fa-lemon:before{content:"\f094"}.fa-head-side-mask:before{content:"\e063"}.fa-handshake:before{content:"\f2b5"}.fa-gem:before{content:"\f3a5"}.fa-dolly-box:before,.fa-dolly:before{content:"\f472"}.fa-smoking:before{content:"\f48d"}.fa-compress-arrows-alt:before,.fa-minimize:before{content:"\f78c"}.fa-monument:before{content:"\f5a6"}.fa-snowplow:before{content:"\f7d2"}.fa-angle-double-right:before,.fa-angles-right:before{content:"\f101"}.fa-cannabis:before{content:"\f55f"}.fa-circle-play:before,.fa-play-circle:before{content:"\f144"}.fa-tablets:before{content:"\f490"}.fa-ethernet:before{content:"\f796"}.fa-eur:before,.fa-euro-sign:before,.fa-euro:before{content:"\f153"}.fa-chair:before{content:"\f6c0"}.fa-check-circle:before,.fa-circle-check:before{content:"\f058"}.fa-circle-stop:before,.fa-stop-circle:before{content:"\f28d"}.fa-compass-drafting:before,.fa-drafting-compass:before{content:"\f568"}.fa-plate-wheat:before{content:"\e55a"}.fa-icicles:before{content:"\f7ad"}.fa-person-shelter:before{content:"\e54f"}.fa-neuter:before{content:"\f22c"}.fa-id-badge:before{content:"\f2c1"}.fa-marker:before{content:"\f5a1"}.fa-face-laugh-beam:before,.fa-laugh-beam:before{content:"\f59a"}.fa-helicopter-symbol:before{content:"\e502"}.fa-universal-access:before{content:"\f29a"}.fa-chevron-circle-up:before,.fa-circle-chevron-up:before{content:"\f139"}.fa-lari-sign:before{content:"\e1c8"}.fa-volcano:before{content:"\f770"}.fa-person-walking-dashed-line-arrow-right:before{content:"\e553"}.fa-gbp:before,.fa-pound-sign:before,.fa-sterling-sign:before{content:"\f154"}.fa-viruses:before{content:"\e076"}.fa-square-person-confined:before{content:"\e577"}.fa-user-tie:before{content:"\f508"}.fa-arrow-down-long:before,.fa-long-arrow-down:before{content:"\f175"}.fa-tent-arrow-down-to-line:before{content:"\e57e"}.fa-certificate:before{content:"\f0a3"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-suitcase:before{content:"\f0f2"}.fa-person-skating:before,.fa-skating:before{content:"\f7c5"}.fa-filter-circle-dollar:before,.fa-funnel-dollar:before{content:"\f662"}.fa-camera-retro:before{content:"\f083"}.fa-arrow-circle-down:before,.fa-circle-arrow-down:before{content:"\f0ab"}.fa-arrow-right-to-file:before,.fa-file-import:before{content:"\f56f"}.fa-external-link-square:before,.fa-square-arrow-up-right:before{content:"\f14c"}.fa-box-open:before{content:"\f49e"}.fa-scroll:before{content:"\f70e"}.fa-spa:before{content:"\f5bb"}.fa-location-pin-lock:before{content:"\e51f"}.fa-pause:before{content:"\f04c"}.fa-hill-avalanche:before{content:"\e507"}.fa-temperature-0:before,.fa-temperature-empty:before,.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-bomb:before{content:"\f1e2"}.fa-registered:before{content:"\f25d"}.fa-address-card:before,.fa-contact-card:before,.fa-vcard:before{content:"\f2bb"}.fa-balance-scale-right:before,.fa-scale-unbalanced-flip:before{content:"\f516"}.fa-subscript:before{content:"\f12c"}.fa-diamond-turn-right:before,.fa-directions:before{content:"\f5eb"}.fa-burst:before{content:"\e4dc"}.fa-house-laptop:before,.fa-laptop-house:before{content:"\e066"}.fa-face-tired:before,.fa-tired:before{content:"\f5c8"}.fa-money-bills:before{content:"\e1f3"}.fa-smog:before{content:"\f75f"}.fa-crutch:before{content:"\f7f7"}.fa-cloud-arrow-up:before,.fa-cloud-upload-alt:before,.fa-cloud-upload:before{content:"\f0ee"}.fa-palette:before{content:"\f53f"}.fa-arrows-turn-right:before{content:"\e4c0"}.fa-vest:before{content:"\e085"}.fa-ferry:before{content:"\e4ea"}.fa-arrows-down-to-people:before{content:"\e4b9"}.fa-seedling:before,.fa-sprout:before{content:"\f4d8"}.fa-arrows-alt-h:before,.fa-left-right:before{content:"\f337"}.fa-boxes-packing:before{content:"\e4c7"}.fa-arrow-circle-left:before,.fa-circle-arrow-left:before{content:"\f0a8"}.fa-group-arrows-rotate:before{content:"\e4f6"}.fa-bowl-food:before{content:"\e4c6"}.fa-candy-cane:before{content:"\f786"}.fa-arrow-down-wide-short:before,.fa-sort-amount-asc:before,.fa-sort-amount-down:before{content:"\f160"}.fa-cloud-bolt:before,.fa-thunderstorm:before{content:"\f76c"}.fa-remove-format:before,.fa-text-slash:before{content:"\f87d"}.fa-face-smile-wink:before,.fa-smile-wink:before{content:"\f4da"}.fa-file-word:before{content:"\f1c2"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-arrows-h:before,.fa-arrows-left-right:before{content:"\f07e"}.fa-house-lock:before{content:"\e510"}.fa-cloud-arrow-down:before,.fa-cloud-download-alt:before,.fa-cloud-download:before{content:"\f0ed"}.fa-children:before{content:"\e4e1"}.fa-blackboard:before,.fa-chalkboard:before{content:"\f51b"}.fa-user-alt-slash:before,.fa-user-large-slash:before{content:"\f4fa"}.fa-envelope-open:before{content:"\f2b6"}.fa-handshake-alt-slash:before,.fa-handshake-simple-slash:before{content:"\e05f"}.fa-mattress-pillow:before{content:"\e525"}.fa-guarani-sign:before{content:"\e19a"}.fa-arrows-rotate:before,.fa-refresh:before,.fa-sync:before{content:"\f021"}.fa-fire-extinguisher:before{content:"\f134"}.fa-cruzeiro-sign:before{content:"\e152"}.fa-greater-than-equal:before{content:"\f532"}.fa-shield-alt:before,.fa-shield-halved:before{content:"\f3ed"}.fa-atlas:before,.fa-book-atlas:before{content:"\f558"}.fa-virus:before{content:"\e074"}.fa-envelope-circle-check:before{content:"\e4e8"}.fa-layer-group:before{content:"\f5fd"}.fa-arrows-to-dot:before{content:"\e4be"}.fa-archway:before{content:"\f557"}.fa-heart-circle-check:before{content:"\e4fd"}.fa-house-chimney-crack:before,.fa-house-damage:before{content:"\f6f1"}.fa-file-archive:before,.fa-file-zipper:before{content:"\f1c6"}.fa-square:before{content:"\f0c8"}.fa-glass-martini:before,.fa-martini-glass-empty:before{content:"\f000"}.fa-couch:before{content:"\f4b8"}.fa-cedi-sign:before{content:"\e0df"}.fa-italic:before{content:"\f033"}.fa-church:before{content:"\f51d"}.fa-comments-dollar:before{content:"\f653"}.fa-democrat:before{content:"\f747"}.fa-z:before{content:"\5a"}.fa-person-skiing:before,.fa-skiing:before{content:"\f7c9"}.fa-road-lock:before{content:"\e567"}.fa-a:before{content:"\41"}.fa-temperature-arrow-down:before,.fa-temperature-down:before{content:"\e03f"}.fa-feather-alt:before,.fa-feather-pointed:before{content:"\f56b"}.fa-p:before{content:"\50"}.fa-snowflake:before{content:"\f2dc"}.fa-newspaper:before{content:"\f1ea"}.fa-ad:before,.fa-rectangle-ad:before{content:"\f641"}.fa-arrow-circle-right:before,.fa-circle-arrow-right:before{content:"\f0a9"}.fa-filter-circle-xmark:before{content:"\e17b"}.fa-locust:before{content:"\e520"}.fa-sort:before,.fa-unsorted:before{content:"\f0dc"}.fa-list-1-2:before,.fa-list-numeric:before,.fa-list-ol:before{content:"\f0cb"}.fa-person-dress-burst:before{content:"\e544"}.fa-money-check-alt:before,.fa-money-check-dollar:before{content:"\f53d"}.fa-vector-square:before{content:"\f5cb"}.fa-bread-slice:before{content:"\f7ec"}.fa-language:before{content:"\f1ab"}.fa-face-kiss-wink-heart:before,.fa-kiss-wink-heart:before{content:"\f598"}.fa-filter:before{content:"\f0b0"}.fa-question:before{content:"\3f"}.fa-file-signature:before{content:"\f573"}.fa-arrows-alt:before,.fa-up-down-left-right:before{content:"\f0b2"}.fa-house-chimney-user:before{content:"\e065"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-puzzle-piece:before{content:"\f12e"}.fa-money-check:before{content:"\f53c"}.fa-star-half-alt:before,.fa-star-half-stroke:before{content:"\f5c0"}.fa-code:before{content:"\f121"}.fa-glass-whiskey:before,.fa-whiskey-glass:before{content:"\f7a0"}.fa-building-circle-exclamation:before{content:"\e4d3"}.fa-magnifying-glass-chart:before{content:"\e522"}.fa-arrow-up-right-from-square:before,.fa-external-link:before{content:"\f08e"}.fa-cubes-stacked:before{content:"\e4e6"}.fa-krw:before,.fa-won-sign:before,.fa-won:before{content:"\f159"}.fa-virus-covid:before{content:"\e4a8"}.fa-austral-sign:before{content:"\e0a9"}.fa-f:before{content:"\46"}.fa-leaf:before{content:"\f06c"}.fa-road:before{content:"\f018"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-person-circle-plus:before{content:"\e541"}.fa-chart-pie:before,.fa-pie-chart:before{content:"\f200"}.fa-bolt-lightning:before{content:"\e0b7"}.fa-sack-xmark:before{content:"\e56a"}.fa-file-excel:before{content:"\f1c3"}.fa-file-contract:before{content:"\f56c"}.fa-fish-fins:before{content:"\e4f2"}.fa-building-flag:before{content:"\e4d5"}.fa-face-grin-beam:before,.fa-grin-beam:before{content:"\f582"}.fa-object-ungroup:before{content:"\f248"}.fa-poop:before{content:"\f619"}.fa-location-pin:before,.fa-map-marker:before{content:"\f041"}.fa-kaaba:before{content:"\f66b"}.fa-toilet-paper:before{content:"\f71e"}.fa-hard-hat:before,.fa-hat-hard:before,.fa-helmet-safety:before{content:"\f807"}.fa-eject:before{content:"\f052"}.fa-arrow-alt-circle-right:before,.fa-circle-right:before{content:"\f35a"}.fa-plane-circle-check:before{content:"\e555"}.fa-face-rolling-eyes:before,.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-object-group:before{content:"\f247"}.fa-chart-line:before,.fa-line-chart:before{content:"\f201"}.fa-mask-ventilator:before{content:"\e524"}.fa-arrow-right:before{content:"\f061"}.fa-map-signs:before,.fa-signs-post:before{content:"\f277"}.fa-cash-register:before{content:"\f788"}.fa-person-circle-question:before{content:"\e542"}.fa-h:before{content:"\48"}.fa-tarp:before{content:"\e57b"}.fa-screwdriver-wrench:before,.fa-tools:before{content:"\f7d9"}.fa-arrows-to-eye:before{content:"\e4bf"}.fa-plug-circle-bolt:before{content:"\e55b"}.fa-heart:before{content:"\f004"}.fa-mars-and-venus:before{content:"\f224"}.fa-home-user:before,.fa-house-user:before{content:"\e1b0"}.fa-dumpster-fire:before{content:"\f794"}.fa-house-crack:before{content:"\e3b1"}.fa-cocktail:before,.fa-martini-glass-citrus:before{content:"\f561"}.fa-face-surprise:before,.fa-surprise:before{content:"\f5c2"}.fa-bottle-water:before{content:"\e4c5"}.fa-circle-pause:before,.fa-pause-circle:before{content:"\f28b"}.fa-toilet-paper-slash:before{content:"\e072"}.fa-apple-alt:before,.fa-apple-whole:before{content:"\f5d1"}.fa-kitchen-set:before{content:"\e51a"}.fa-r:before{content:"\52"}.fa-temperature-1:before,.fa-temperature-quarter:before,.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-cube:before{content:"\f1b2"}.fa-bitcoin-sign:before{content:"\e0b4"}.fa-shield-dog:before{content:"\e573"}.fa-solar-panel:before{content:"\f5ba"}.fa-lock-open:before{content:"\f3c1"}.fa-elevator:before{content:"\e16d"}.fa-money-bill-transfer:before{content:"\e528"}.fa-money-bill-trend-up:before{content:"\e529"}.fa-house-flood-water-circle-arrow-right:before{content:"\e50f"}.fa-poll-h:before,.fa-square-poll-horizontal:before{content:"\f682"}.fa-circle:before{content:"\f111"}.fa-backward-fast:before,.fa-fast-backward:before{content:"\f049"}.fa-recycle:before{content:"\f1b8"}.fa-user-astronaut:before{content:"\f4fb"}.fa-plane-slash:before{content:"\e069"}.fa-trademark:before{content:"\f25c"}.fa-basketball-ball:before,.fa-basketball:before{content:"\f434"}.fa-satellite-dish:before{content:"\f7c0"}.fa-arrow-alt-circle-up:before,.fa-circle-up:before{content:"\f35b"}.fa-mobile-alt:before,.fa-mobile-screen-button:before{content:"\f3cd"}.fa-volume-high:before,.fa-volume-up:before{content:"\f028"}.fa-users-rays:before{content:"\e593"}.fa-wallet:before{content:"\f555"}.fa-clipboard-check:before{content:"\f46c"}.fa-file-audio:before{content:"\f1c7"}.fa-burger:before,.fa-hamburger:before{content:"\f805"}.fa-wrench:before{content:"\f0ad"}.fa-bugs:before{content:"\e4d0"}.fa-rupee-sign:before,.fa-rupee:before{content:"\f156"}.fa-file-image:before{content:"\f1c5"}.fa-circle-question:before,.fa-question-circle:before{content:"\f059"}.fa-plane-departure:before{content:"\f5b0"}.fa-handshake-slash:before{content:"\e060"}.fa-book-bookmark:before{content:"\e0bb"}.fa-code-branch:before{content:"\f126"}.fa-hat-cowboy:before{content:"\f8c0"}.fa-bridge:before{content:"\e4c8"}.fa-phone-alt:before,.fa-phone-flip:before{content:"\f879"}.fa-truck-front:before{content:"\e2b7"}.fa-cat:before{content:"\f6be"}.fa-anchor-circle-exclamation:before{content:"\e4ab"}.fa-truck-field:before{content:"\e58d"}.fa-route:before{content:"\f4d7"}.fa-clipboard-question:before{content:"\e4e3"}.fa-panorama:before{content:"\e209"}.fa-comment-medical:before{content:"\f7f5"}.fa-teeth-open:before{content:"\f62f"}.fa-file-circle-minus:before{content:"\e4ed"}.fa-tags:before{content:"\f02c"}.fa-wine-glass:before{content:"\f4e3"}.fa-fast-forward:before,.fa-forward-fast:before{content:"\f050"}.fa-face-meh-blank:before,.fa-meh-blank:before{content:"\f5a4"}.fa-parking:before,.fa-square-parking:before{content:"\f540"}.fa-house-signal:before{content:"\e012"}.fa-bars-progress:before,.fa-tasks-alt:before{content:"\f828"}.fa-faucet-drip:before{content:"\e006"}.fa-cart-flatbed:before,.fa-dolly-flatbed:before{content:"\f474"}.fa-ban-smoking:before,.fa-smoking-ban:before{content:"\f54d"}.fa-terminal:before{content:"\f120"}.fa-mobile-button:before{content:"\f10b"}.fa-house-medical-flag:before{content:"\e514"}.fa-basket-shopping:before,.fa-shopping-basket:before{content:"\f291"}.fa-tape:before{content:"\f4db"}.fa-bus-alt:before,.fa-bus-simple:before{content:"\f55e"}.fa-eye:before{content:"\f06e"}.fa-face-sad-cry:before,.fa-sad-cry:before{content:"\f5b3"}.fa-audio-description:before{content:"\f29e"}.fa-person-military-to-person:before{content:"\e54c"}.fa-file-shield:before{content:"\e4f0"}.fa-user-slash:before{content:"\f506"}.fa-pen:before{content:"\f304"}.fa-tower-observation:before{content:"\e586"}.fa-file-code:before{content:"\f1c9"}.fa-signal-5:before,.fa-signal-perfect:before,.fa-signal:before{content:"\f012"}.fa-bus:before{content:"\f207"}.fa-heart-circle-xmark:before{content:"\e501"}.fa-home-lg:before,.fa-house-chimney:before{content:"\e3af"}.fa-window-maximize:before{content:"\f2d0"}.fa-face-frown:before,.fa-frown:before{content:"\f119"}.fa-prescription:before{content:"\f5b1"}.fa-shop:before,.fa-store-alt:before{content:"\f54f"}.fa-floppy-disk:before,.fa-save:before{content:"\f0c7"}.fa-vihara:before{content:"\f6a7"}.fa-balance-scale-left:before,.fa-scale-unbalanced:before{content:"\f515"}.fa-sort-asc:before,.fa-sort-up:before{content:"\f0de"}.fa-comment-dots:before,.fa-commenting:before{content:"\f4ad"}.fa-plant-wilt:before{content:"\e5aa"}.fa-diamond:before{content:"\f219"}.fa-face-grin-squint:before,.fa-grin-squint:before{content:"\f585"}.fa-hand-holding-dollar:before,.fa-hand-holding-usd:before{content:"\f4c0"}.fa-bacterium:before{content:"\e05a"}.fa-hand-pointer:before{content:"\f25a"}.fa-drum-steelpan:before{content:"\f56a"}.fa-hand-scissors:before{content:"\f257"}.fa-hands-praying:before,.fa-praying-hands:before{content:"\f684"}.fa-arrow-right-rotate:before,.fa-arrow-rotate-forward:before,.fa-arrow-rotate-right:before,.fa-redo:before{content:"\f01e"}.fa-biohazard:before{content:"\f780"}.fa-location-crosshairs:before,.fa-location:before{content:"\f601"}.fa-mars-double:before{content:"\f227"}.fa-child-dress:before{content:"\e59c"}.fa-users-between-lines:before{content:"\e591"}.fa-lungs-virus:before{content:"\e067"}.fa-face-grin-tears:before,.fa-grin-tears:before{content:"\f588"}.fa-phone:before{content:"\f095"}.fa-calendar-times:before,.fa-calendar-xmark:before{content:"\f273"}.fa-child-reaching:before{content:"\e59d"}.fa-head-side-virus:before{content:"\e064"}.fa-user-cog:before,.fa-user-gear:before{content:"\f4fe"}.fa-arrow-up-1-9:before,.fa-sort-numeric-up:before{content:"\f163"}.fa-door-closed:before{content:"\f52a"}.fa-shield-virus:before{content:"\e06c"}.fa-dice-six:before{content:"\f526"}.fa-mosquito-net:before{content:"\e52c"}.fa-bridge-water:before{content:"\e4ce"}.fa-person-booth:before{content:"\f756"}.fa-text-width:before{content:"\f035"}.fa-hat-wizard:before{content:"\f6e8"}.fa-pen-fancy:before{content:"\f5ac"}.fa-digging:before,.fa-person-digging:before{content:"\f85e"}.fa-trash:before{content:"\f1f8"}.fa-gauge-simple-med:before,.fa-gauge-simple:before,.fa-tachometer-average:before{content:"\f629"}.fa-book-medical:before{content:"\f7e6"}.fa-poo:before{content:"\f2fe"}.fa-quote-right-alt:before,.fa-quote-right:before{content:"\f10e"}.fa-shirt:before,.fa-t-shirt:before,.fa-tshirt:before{content:"\f553"}.fa-cubes:before{content:"\f1b3"}.fa-divide:before{content:"\f529"}.fa-tenge-sign:before,.fa-tenge:before{content:"\f7d7"}.fa-headphones:before{content:"\f025"}.fa-hands-holding:before{content:"\f4c2"}.fa-hands-clapping:before{content:"\e1a8"}.fa-republican:before{content:"\f75e"}.fa-arrow-left:before{content:"\f060"}.fa-person-circle-xmark:before{content:"\e543"}.fa-ruler:before{content:"\f545"}.fa-align-left:before{content:"\f036"}.fa-dice-d6:before{content:"\f6d1"}.fa-restroom:before{content:"\f7bd"}.fa-j:before{content:"\4a"}.fa-users-viewfinder:before{content:"\e595"}.fa-file-video:before{content:"\f1c8"}.fa-external-link-alt:before,.fa-up-right-from-square:before{content:"\f35d"}.fa-table-cells:before,.fa-th:before{content:"\f00a"}.fa-file-pdf:before{content:"\f1c1"}.fa-bible:before,.fa-book-bible:before{content:"\f647"}.fa-o:before{content:"\4f"}.fa-medkit:before,.fa-suitcase-medical:before{content:"\f0fa"}.fa-user-secret:before{content:"\f21b"}.fa-otter:before{content:"\f700"}.fa-female:before,.fa-person-dress:before{content:"\f182"}.fa-comment-dollar:before{content:"\f651"}.fa-briefcase-clock:before,.fa-business-time:before{content:"\f64a"}.fa-table-cells-large:before,.fa-th-large:before{content:"\f009"}.fa-book-tanakh:before,.fa-tanakh:before{content:"\f827"}.fa-phone-volume:before,.fa-volume-control-phone:before{content:"\f2a0"}.fa-hat-cowboy-side:before{content:"\f8c1"}.fa-clipboard-user:before{content:"\f7f3"}.fa-child:before{content:"\f1ae"}.fa-lira-sign:before{content:"\f195"}.fa-satellite:before{content:"\f7bf"}.fa-plane-lock:before{content:"\e558"}.fa-tag:before{content:"\f02b"}.fa-comment:before{content:"\f075"}.fa-birthday-cake:before,.fa-cake-candles:before,.fa-cake:before{content:"\f1fd"}.fa-envelope:before{content:"\f0e0"}.fa-angle-double-up:before,.fa-angles-up:before{content:"\f102"}.fa-paperclip:before{content:"\f0c6"}.fa-arrow-right-to-city:before{content:"\e4b3"}.fa-ribbon:before{content:"\f4d6"}.fa-lungs:before{content:"\f604"}.fa-arrow-up-9-1:before,.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-litecoin-sign:before{content:"\e1d3"}.fa-border-none:before{content:"\f850"}.fa-circle-nodes:before{content:"\e4e2"}.fa-parachute-box:before{content:"\f4cd"}.fa-indent:before{content:"\f03c"}.fa-truck-field-un:before{content:"\e58e"}.fa-hourglass-empty:before,.fa-hourglass:before{content:"\f254"}.fa-mountain:before{content:"\f6fc"}.fa-user-doctor:before,.fa-user-md:before{content:"\f0f0"}.fa-circle-info:before,.fa-info-circle:before{content:"\f05a"}.fa-cloud-meatball:before{content:"\f73b"}.fa-camera-alt:before,.fa-camera:before{content:"\f030"}.fa-square-virus:before{content:"\e578"}.fa-meteor:before{content:"\f753"}.fa-car-on:before{content:"\e4dd"}.fa-sleigh:before{content:"\f7cc"}.fa-arrow-down-1-9:before,.fa-sort-numeric-asc:before,.fa-sort-numeric-down:before{content:"\f162"}.fa-hand-holding-droplet:before,.fa-hand-holding-water:before{content:"\f4c1"}.fa-water:before{content:"\f773"}.fa-calendar-check:before{content:"\f274"}.fa-braille:before{content:"\f2a1"}.fa-prescription-bottle-alt:before,.fa-prescription-bottle-medical:before{content:"\f486"}.fa-landmark:before{content:"\f66f"}.fa-truck:before{content:"\f0d1"}.fa-crosshairs:before{content:"\f05b"}.fa-person-cane:before{content:"\e53c"}.fa-tent:before{content:"\e57d"}.fa-vest-patches:before{content:"\e086"}.fa-check-double:before{content:"\f560"}.fa-arrow-down-a-z:before,.fa-sort-alpha-asc:before,.fa-sort-alpha-down:before{content:"\f15d"}.fa-money-bill-wheat:before{content:"\e52a"}.fa-cookie:before{content:"\f563"}.fa-arrow-left-rotate:before,.fa-arrow-rotate-back:before,.fa-arrow-rotate-backward:before,.fa-arrow-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-hard-drive:before,.fa-hdd:before{content:"\f0a0"}.fa-face-grin-squint-tears:before,.fa-grin-squint-tears:before{content:"\f586"}.fa-dumbbell:before{content:"\f44b"}.fa-list-alt:before,.fa-rectangle-list:before{content:"\f022"}.fa-tarp-droplet:before{content:"\e57c"}.fa-house-medical-circle-check:before{content:"\e511"}.fa-person-skiing-nordic:before,.fa-skiing-nordic:before{content:"\f7ca"}.fa-calendar-plus:before{content:"\f271"}.fa-plane-arrival:before{content:"\f5af"}.fa-arrow-alt-circle-left:before,.fa-circle-left:before{content:"\f359"}.fa-subway:before,.fa-train-subway:before{content:"\f239"}.fa-chart-gantt:before{content:"\e0e4"}.fa-indian-rupee-sign:before,.fa-indian-rupee:before,.fa-inr:before{content:"\e1bc"}.fa-crop-alt:before,.fa-crop-simple:before{content:"\f565"}.fa-money-bill-1:before,.fa-money-bill-alt:before{content:"\f3d1"}.fa-left-long:before,.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-dna:before{content:"\f471"}.fa-virus-slash:before{content:"\e075"}.fa-minus:before,.fa-subtract:before{content:"\f068"}.fa-chess:before{content:"\f439"}.fa-arrow-left-long:before,.fa-long-arrow-left:before{content:"\f177"}.fa-plug-circle-check:before{content:"\e55c"}.fa-street-view:before{content:"\f21d"}.fa-franc-sign:before{content:"\e18f"}.fa-volume-off:before{content:"\f026"}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before,.fa-hands-american-sign-language-interpreting:before,.fa-hands-asl-interpreting:before{content:"\f2a3"}.fa-cog:before,.fa-gear:before{content:"\f013"}.fa-droplet-slash:before,.fa-tint-slash:before{content:"\f5c7"}.fa-mosque:before{content:"\f678"}.fa-mosquito:before{content:"\e52b"}.fa-star-of-david:before{content:"\f69a"}.fa-person-military-rifle:before{content:"\e54b"}.fa-cart-shopping:before,.fa-shopping-cart:before{content:"\f07a"}.fa-vials:before{content:"\f493"}.fa-plug-circle-plus:before{content:"\e55f"}.fa-place-of-worship:before{content:"\f67f"}.fa-grip-vertical:before{content:"\f58e"}.fa-arrow-turn-up:before,.fa-level-up:before{content:"\f148"}.fa-u:before{content:"\55"}.fa-square-root-alt:before,.fa-square-root-variable:before{content:"\f698"}.fa-clock-four:before,.fa-clock:before{content:"\f017"}.fa-backward-step:before,.fa-step-backward:before{content:"\f048"}.fa-pallet:before{content:"\f482"}.fa-faucet:before{content:"\e005"}.fa-baseball-bat-ball:before{content:"\f432"}.fa-s:before{content:"\53"}.fa-timeline:before{content:"\e29c"}.fa-keyboard:before{content:"\f11c"}.fa-caret-down:before{content:"\f0d7"}.fa-clinic-medical:before,.fa-house-chimney-medical:before{content:"\f7f2"}.fa-temperature-3:before,.fa-temperature-three-quarters:before,.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-mobile-android-alt:before,.fa-mobile-screen:before{content:"\f3cf"}.fa-plane-up:before{content:"\e22d"}.fa-piggy-bank:before{content:"\f4d3"}.fa-battery-3:before,.fa-battery-half:before{content:"\f242"}.fa-mountain-city:before{content:"\e52e"}.fa-coins:before{content:"\f51e"}.fa-khanda:before{content:"\f66d"}.fa-sliders-h:before,.fa-sliders:before{content:"\f1de"}.fa-folder-tree:before{content:"\f802"}.fa-network-wired:before{content:"\f6ff"}.fa-map-pin:before{content:"\f276"}.fa-hamsa:before{content:"\f665"}.fa-cent-sign:before{content:"\e3f5"}.fa-flask:before{content:"\f0c3"}.fa-person-pregnant:before{content:"\e31e"}.fa-wand-sparkles:before{content:"\f72b"}.fa-ellipsis-v:before,.fa-ellipsis-vertical:before{content:"\f142"}.fa-ticket:before{content:"\f145"}.fa-power-off:before{content:"\f011"}.fa-long-arrow-alt-right:before,.fa-right-long:before{content:"\f30b"}.fa-flag-usa:before{content:"\f74d"}.fa-laptop-file:before{content:"\e51d"}.fa-teletype:before,.fa-tty:before{content:"\f1e4"}.fa-diagram-next:before{content:"\e476"}.fa-person-rifle:before{content:"\e54e"}.fa-house-medical-circle-exclamation:before{content:"\e512"}.fa-closed-captioning:before{content:"\f20a"}.fa-hiking:before,.fa-person-hiking:before{content:"\f6ec"}.fa-venus-double:before{content:"\f226"}.fa-images:before{content:"\f302"}.fa-calculator:before{content:"\f1ec"}.fa-people-pulling:before{content:"\e535"}.fa-n:before{content:"\4e"}.fa-cable-car:before,.fa-tram:before{content:"\f7da"}.fa-cloud-rain:before{content:"\f73d"}.fa-building-circle-xmark:before{content:"\e4d4"}.fa-ship:before{content:"\f21a"}.fa-arrows-down-to-line:before{content:"\e4b8"}.fa-download:before{content:"\f019"}.fa-face-grin:before,.fa-grin:before{content:"\f580"}.fa-backspace:before,.fa-delete-left:before{content:"\f55a"}.fa-eye-dropper-empty:before,.fa-eye-dropper:before,.fa-eyedropper:before{content:"\f1fb"}.fa-file-circle-check:before{content:"\e5a0"}.fa-forward:before{content:"\f04e"}.fa-mobile-android:before,.fa-mobile-phone:before,.fa-mobile:before{content:"\f3ce"}.fa-face-meh:before,.fa-meh:before{content:"\f11a"}.fa-align-center:before{content:"\f037"}.fa-book-dead:before,.fa-book-skull:before{content:"\f6b7"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-heart-circle-exclamation:before{content:"\e4fe"}.fa-home-alt:before,.fa-home-lg-alt:before,.fa-home:before,.fa-house:before{content:"\f015"}.fa-calendar-week:before{content:"\f784"}.fa-laptop-medical:before{content:"\f812"}.fa-b:before{content:"\42"}.fa-file-medical:before{content:"\f477"}.fa-dice-one:before{content:"\f525"}.fa-kiwi-bird:before{content:"\f535"}.fa-arrow-right-arrow-left:before,.fa-exchange:before{content:"\f0ec"}.fa-redo-alt:before,.fa-rotate-forward:before,.fa-rotate-right:before{content:"\f2f9"}.fa-cutlery:before,.fa-utensils:before{content:"\f2e7"}.fa-arrow-up-wide-short:before,.fa-sort-amount-up:before{content:"\f161"}.fa-mill-sign:before{content:"\e1ed"}.fa-bowl-rice:before{content:"\e2eb"}.fa-skull:before{content:"\f54c"}.fa-broadcast-tower:before,.fa-tower-broadcast:before{content:"\f519"}.fa-truck-pickup:before{content:"\f63c"}.fa-long-arrow-alt-up:before,.fa-up-long:before{content:"\f30c"}.fa-stop:before{content:"\f04d"}.fa-code-merge:before{content:"\f387"}.fa-upload:before{content:"\f093"}.fa-hurricane:before{content:"\f751"}.fa-mound:before{content:"\e52d"}.fa-toilet-portable:before{content:"\e583"}.fa-compact-disc:before{content:"\f51f"}.fa-file-arrow-down:before,.fa-file-download:before{content:"\f56d"}.fa-caravan:before{content:"\f8ff"}.fa-shield-cat:before{content:"\e572"}.fa-bolt:before,.fa-zap:before{content:"\f0e7"}.fa-glass-water:before{content:"\e4f4"}.fa-oil-well:before{content:"\e532"}.fa-vault:before{content:"\e2c5"}.fa-mars:before{content:"\f222"}.fa-toilet:before{content:"\f7d8"}.fa-plane-circle-xmark:before{content:"\e557"}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen-sign:before,.fa-yen:before{content:"\f157"}.fa-rouble:before,.fa-rub:before,.fa-ruble-sign:before,.fa-ruble:before{content:"\f158"}.fa-sun:before{content:"\f185"}.fa-guitar:before{content:"\f7a6"}.fa-face-laugh-wink:before,.fa-laugh-wink:before{content:"\f59c"}.fa-horse-head:before{content:"\f7ab"}.fa-bore-hole:before{content:"\e4c3"}.fa-industry:before{content:"\f275"}.fa-arrow-alt-circle-down:before,.fa-circle-down:before{content:"\f358"}.fa-arrows-turn-to-dots:before{content:"\e4c1"}.fa-florin-sign:before{content:"\e184"}.fa-arrow-down-short-wide:before,.fa-sort-amount-desc:before,.fa-sort-amount-down-alt:before{content:"\f884"}.fa-less-than:before{content:"\3c"}.fa-angle-down:before{content:"\f107"}.fa-car-tunnel:before{content:"\e4de"}.fa-head-side-cough:before{content:"\e061"}.fa-grip-lines:before{content:"\f7a4"}.fa-thumbs-down:before{content:"\f165"}.fa-user-lock:before{content:"\f502"}.fa-arrow-right-long:before,.fa-long-arrow-right:before{content:"\f178"}.fa-anchor-circle-xmark:before{content:"\e4ac"}.fa-ellipsis-h:before,.fa-ellipsis:before{content:"\f141"}.fa-chess-pawn:before{content:"\f443"}.fa-first-aid:before,.fa-kit-medical:before{content:"\f479"}.fa-person-through-window:before{content:"\e5a9"}.fa-toolbox:before{content:"\f552"}.fa-hands-holding-circle:before{content:"\e4fb"}.fa-bug:before{content:"\f188"}.fa-credit-card-alt:before,.fa-credit-card:before{content:"\f09d"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-hand-holding-hand:before{content:"\e4f7"}.fa-book-open-reader:before,.fa-book-reader:before{content:"\f5da"}.fa-mountain-sun:before{content:"\e52f"}.fa-arrows-left-right-to-line:before{content:"\e4ba"}.fa-dice-d20:before{content:"\f6cf"}.fa-truck-droplet:before{content:"\e58c"}.fa-file-circle-xmark:before{content:"\e5a1"}.fa-temperature-arrow-up:before,.fa-temperature-up:before{content:"\e040"}.fa-medal:before{content:"\f5a2"}.fa-bed:before{content:"\f236"}.fa-h-square:before,.fa-square-h:before{content:"\f0fd"}.fa-podcast:before{content:"\f2ce"}.fa-temperature-4:before,.fa-temperature-full:before,.fa-thermometer-4:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-bell:before{content:"\f0f3"}.fa-superscript:before{content:"\f12b"}.fa-plug-circle-xmark:before{content:"\e560"}.fa-star-of-life:before{content:"\f621"}.fa-phone-slash:before{content:"\f3dd"}.fa-paint-roller:before{content:"\f5aa"}.fa-hands-helping:before,.fa-handshake-angle:before{content:"\f4c4"}.fa-location-dot:before,.fa-map-marker-alt:before{content:"\f3c5"}.fa-file:before{content:"\f15b"}.fa-greater-than:before{content:"\3e"}.fa-person-swimming:before,.fa-swimmer:before{content:"\f5c4"}.fa-arrow-down:before{content:"\f063"}.fa-droplet:before,.fa-tint:before{content:"\f043"}.fa-eraser:before{content:"\f12d"}.fa-earth-america:before,.fa-earth-americas:before,.fa-earth:before,.fa-globe-americas:before{content:"\f57d"}.fa-person-burst:before{content:"\e53b"}.fa-dove:before{content:"\f4ba"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-socks:before{content:"\f696"}.fa-inbox:before{content:"\f01c"}.fa-section:before{content:"\e447"}.fa-gauge-high:before,.fa-tachometer-alt-fast:before,.fa-tachometer-alt:before{content:"\f625"}.fa-envelope-open-text:before{content:"\f658"}.fa-hospital-alt:before,.fa-hospital-wide:before,.fa-hospital:before{content:"\f0f8"}.fa-wine-bottle:before{content:"\f72f"}.fa-chess-rook:before{content:"\f447"}.fa-bars-staggered:before,.fa-reorder:before,.fa-stream:before{content:"\f550"}.fa-dharmachakra:before{content:"\f655"}.fa-hotdog:before{content:"\f80f"}.fa-blind:before,.fa-person-walking-with-cane:before{content:"\f29d"}.fa-drum:before{content:"\f569"}.fa-ice-cream:before{content:"\f810"}.fa-heart-circle-bolt:before{content:"\e4fc"}.fa-fax:before{content:"\f1ac"}.fa-paragraph:before{content:"\f1dd"}.fa-check-to-slot:before,.fa-vote-yea:before{content:"\f772"}.fa-star-half:before{content:"\f089"}.fa-boxes-alt:before,.fa-boxes-stacked:before,.fa-boxes:before{content:"\f468"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-assistive-listening-systems:before,.fa-ear-listen:before{content:"\f2a2"}.fa-tree-city:before{content:"\e587"}.fa-play:before{content:"\f04b"}.fa-font:before{content:"\f031"}.fa-rupiah-sign:before{content:"\e23d"}.fa-magnifying-glass:before,.fa-search:before{content:"\f002"}.fa-ping-pong-paddle-ball:before,.fa-table-tennis-paddle-ball:before,.fa-table-tennis:before{content:"\f45d"}.fa-diagnoses:before,.fa-person-dots-from-line:before{content:"\f470"}.fa-trash-can-arrow-up:before,.fa-trash-restore-alt:before{content:"\f82a"}.fa-naira-sign:before{content:"\e1f6"}.fa-cart-arrow-down:before{content:"\f218"}.fa-walkie-talkie:before{content:"\f8ef"}.fa-file-edit:before,.fa-file-pen:before{content:"\f31c"}.fa-receipt:before{content:"\f543"}.fa-pen-square:before,.fa-pencil-square:before,.fa-square-pen:before{content:"\f14b"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-person-circle-exclamation:before{content:"\e53f"}.fa-chevron-down:before{content:"\f078"}.fa-battery-5:before,.fa-battery-full:before,.fa-battery:before{content:"\f240"}.fa-skull-crossbones:before{content:"\f714"}.fa-code-compare:before{content:"\e13a"}.fa-list-dots:before,.fa-list-ul:before{content:"\f0ca"}.fa-school-lock:before{content:"\e56f"}.fa-tower-cell:before{content:"\e585"}.fa-down-long:before,.fa-long-arrow-alt-down:before{content:"\f309"}.fa-ranking-star:before{content:"\e561"}.fa-chess-king:before{content:"\f43f"}.fa-person-harassing:before{content:"\e549"}.fa-brazilian-real-sign:before{content:"\e46c"}.fa-landmark-alt:before,.fa-landmark-dome:before{content:"\f752"}.fa-arrow-up:before{content:"\f062"}.fa-television:before,.fa-tv-alt:before,.fa-tv:before{content:"\f26c"}.fa-shrimp:before{content:"\e448"}.fa-list-check:before,.fa-tasks:before{content:"\f0ae"}.fa-jug-detergent:before{content:"\e519"}.fa-circle-user:before,.fa-user-circle:before{content:"\f2bd"}.fa-user-shield:before{content:"\f505"}.fa-wind:before{content:"\f72e"}.fa-car-burst:before,.fa-car-crash:before{content:"\f5e1"}.fa-y:before{content:"\59"}.fa-person-snowboarding:before,.fa-snowboarding:before{content:"\f7ce"}.fa-shipping-fast:before,.fa-truck-fast:before{content:"\f48b"}.fa-fish:before{content:"\f578"}.fa-user-graduate:before{content:"\f501"}.fa-adjust:before,.fa-circle-half-stroke:before{content:"\f042"}.fa-clapperboard:before{content:"\e131"}.fa-circle-radiation:before,.fa-radiation-alt:before{content:"\f7ba"}.fa-baseball-ball:before,.fa-baseball:before{content:"\f433"}.fa-jet-fighter-up:before{content:"\e518"}.fa-diagram-project:before,.fa-project-diagram:before{content:"\f542"}.fa-copy:before{content:"\f0c5"}.fa-volume-mute:before,.fa-volume-times:before,.fa-volume-xmark:before{content:"\f6a9"}.fa-hand-sparkles:before{content:"\e05d"}.fa-grip-horizontal:before,.fa-grip:before{content:"\f58d"}.fa-share-from-square:before,.fa-share-square:before{content:"\f14d"}.fa-child-combatant:before,.fa-child-rifle:before{content:"\e4e0"}.fa-gun:before{content:"\e19b"}.fa-phone-square:before,.fa-square-phone:before{content:"\f098"}.fa-add:before,.fa-plus:before{content:"\2b"}.fa-expand:before{content:"\f065"}.fa-computer:before{content:"\e4e5"}.fa-close:before,.fa-multiply:before,.fa-remove:before,.fa-times:before,.fa-xmark:before{content:"\f00d"}.fa-arrows-up-down-left-right:before,.fa-arrows:before{content:"\f047"}.fa-chalkboard-teacher:before,.fa-chalkboard-user:before{content:"\f51c"}.fa-peso-sign:before{content:"\e222"}.fa-building-shield:before{content:"\e4d8"}.fa-baby:before{content:"\f77c"}.fa-users-line:before{content:"\e592"}.fa-quote-left-alt:before,.fa-quote-left:before{content:"\f10d"}.fa-tractor:before{content:"\f722"}.fa-trash-arrow-up:before,.fa-trash-restore:before{content:"\f829"}.fa-arrow-down-up-lock:before{content:"\e4b0"}.fa-lines-leaning:before{content:"\e51e"}.fa-ruler-combined:before{content:"\f546"}.fa-copyright:before{content:"\f1f9"}.fa-equals:before{content:"\3d"}.fa-blender:before{content:"\f517"}.fa-teeth:before{content:"\f62e"}.fa-ils:before,.fa-shekel-sign:before,.fa-shekel:before,.fa-sheqel-sign:before,.fa-sheqel:before{content:"\f20b"}.fa-map:before{content:"\f279"}.fa-rocket:before{content:"\f135"}.fa-photo-film:before,.fa-photo-video:before{content:"\f87c"}.fa-folder-minus:before{content:"\f65d"}.fa-store:before{content:"\f54e"}.fa-arrow-trend-up:before{content:"\e098"}.fa-plug-circle-minus:before{content:"\e55e"}.fa-sign-hanging:before,.fa-sign:before{content:"\f4d9"}.fa-bezier-curve:before{content:"\f55b"}.fa-bell-slash:before{content:"\f1f6"}.fa-tablet-android:before,.fa-tablet:before{content:"\f3fb"}.fa-school-flag:before{content:"\e56e"}.fa-fill:before{content:"\f575"}.fa-angle-up:before{content:"\f106"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-holly-berry:before{content:"\f7aa"}.fa-chevron-left:before{content:"\f053"}.fa-bacteria:before{content:"\e059"}.fa-hand-lizard:before{content:"\f258"}.fa-notdef:before{content:"\e1fe"}.fa-disease:before{content:"\f7fa"}.fa-briefcase-medical:before{content:"\f469"}.fa-genderless:before{content:"\f22d"}.fa-chevron-right:before{content:"\f054"}.fa-retweet:before{content:"\f079"}.fa-car-alt:before,.fa-car-rear:before{content:"\f5de"}.fa-pump-soap:before{content:"\e06b"}.fa-video-slash:before{content:"\f4e2"}.fa-battery-2:before,.fa-battery-quarter:before{content:"\f243"}.fa-radio:before{content:"\f8d7"}.fa-baby-carriage:before,.fa-carriage-baby:before{content:"\f77d"}.fa-traffic-light:before{content:"\f637"}.fa-thermometer:before{content:"\f491"}.fa-vr-cardboard:before{content:"\f729"}.fa-hand-middle-finger:before{content:"\f806"}.fa-percent:before,.fa-percentage:before{content:"\25"}.fa-truck-moving:before{content:"\f4df"}.fa-glass-water-droplet:before{content:"\e4f5"}.fa-display:before{content:"\e163"}.fa-face-smile:before,.fa-smile:before{content:"\f118"}.fa-thumb-tack:before,.fa-thumbtack:before{content:"\f08d"}.fa-trophy:before{content:"\f091"}.fa-person-praying:before,.fa-pray:before{content:"\f683"}.fa-hammer:before{content:"\f6e3"}.fa-hand-peace:before{content:"\f25b"}.fa-rotate:before,.fa-sync-alt:before{content:"\f2f1"}.fa-spinner:before{content:"\f110"}.fa-robot:before{content:"\f544"}.fa-peace:before{content:"\f67c"}.fa-cogs:before,.fa-gears:before{content:"\f085"}.fa-warehouse:before{content:"\f494"}.fa-arrow-up-right-dots:before{content:"\e4b7"}.fa-splotch:before{content:"\f5bc"}.fa-face-grin-hearts:before,.fa-grin-hearts:before{content:"\f584"}.fa-dice-four:before{content:"\f524"}.fa-sim-card:before{content:"\f7c4"}.fa-transgender-alt:before,.fa-transgender:before{content:"\f225"}.fa-mercury:before{content:"\f223"}.fa-arrow-turn-down:before,.fa-level-down:before{content:"\f149"}.fa-person-falling-burst:before{content:"\e547"}.fa-award:before{content:"\f559"}.fa-ticket-alt:before,.fa-ticket-simple:before{content:"\f3ff"}.fa-building:before{content:"\f1ad"}.fa-angle-double-left:before,.fa-angles-left:before{content:"\f100"}.fa-qrcode:before{content:"\f029"}.fa-clock-rotate-left:before,.fa-history:before{content:"\f1da"}.fa-face-grin-beam-sweat:before,.fa-grin-beam-sweat:before{content:"\f583"}.fa-arrow-right-from-file:before,.fa-file-export:before{content:"\f56e"}.fa-shield-blank:before,.fa-shield:before{content:"\f132"}.fa-arrow-up-short-wide:before,.fa-sort-amount-up-alt:before{content:"\f885"}.fa-house-medical:before{content:"\e3b2"}.fa-golf-ball-tee:before,.fa-golf-ball:before{content:"\f450"}.fa-chevron-circle-left:before,.fa-circle-chevron-left:before{content:"\f137"}.fa-house-chimney-window:before{content:"\e00d"}.fa-pen-nib:before{content:"\f5ad"}.fa-tent-arrow-turn-left:before{content:"\e580"}.fa-tents:before{content:"\e582"}.fa-magic:before,.fa-wand-magic:before{content:"\f0d0"}.fa-dog:before{content:"\f6d3"}.fa-carrot:before{content:"\f787"}.fa-moon:before{content:"\f186"}.fa-wine-glass-alt:before,.fa-wine-glass-empty:before{content:"\f5ce"}.fa-cheese:before{content:"\f7ef"}.fa-yin-yang:before{content:"\f6ad"}.fa-music:before{content:"\f001"}.fa-code-commit:before{content:"\f386"}.fa-temperature-low:before{content:"\f76b"}.fa-biking:before,.fa-person-biking:before{content:"\f84a"}.fa-broom:before{content:"\f51a"}.fa-shield-heart:before{content:"\e574"}.fa-gopuram:before{content:"\f664"}.fa-earth-oceania:before,.fa-globe-oceania:before{content:"\e47b"}.fa-square-xmark:before,.fa-times-square:before,.fa-xmark-square:before{content:"\f2d3"}.fa-hashtag:before{content:"\23"}.fa-expand-alt:before,.fa-up-right-and-down-left-from-center:before{content:"\f424"}.fa-oil-can:before{content:"\f613"}.fa-t:before{content:"\54"}.fa-hippo:before{content:"\f6ed"}.fa-chart-column:before{content:"\e0e3"}.fa-infinity:before{content:"\f534"}.fa-vial-circle-check:before{content:"\e596"}.fa-person-arrow-down-to-line:before{content:"\e538"}.fa-voicemail:before{content:"\f897"}.fa-fan:before{content:"\f863"}.fa-person-walking-luggage:before{content:"\e554"}.fa-arrows-alt-v:before,.fa-up-down:before{content:"\f338"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-calendar:before{content:"\f133"}.fa-trailer:before{content:"\e041"}.fa-bahai:before,.fa-haykal:before{content:"\f666"}.fa-sd-card:before{content:"\f7c2"}.fa-dragon:before{content:"\f6d5"}.fa-shoe-prints:before{content:"\f54b"}.fa-circle-plus:before,.fa-plus-circle:before{content:"\f055"}.fa-face-grin-tongue-wink:before,.fa-grin-tongue-wink:before{content:"\f58b"}.fa-hand-holding:before{content:"\f4bd"}.fa-plug-circle-exclamation:before{content:"\e55d"}.fa-chain-broken:before,.fa-chain-slash:before,.fa-link-slash:before,.fa-unlink:before{content:"\f127"}.fa-clone:before{content:"\f24d"}.fa-person-walking-arrow-loop-left:before{content:"\e551"}.fa-arrow-up-z-a:before,.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-fire-alt:before,.fa-fire-flame-curved:before{content:"\f7e4"}.fa-tornado:before{content:"\f76f"}.fa-file-circle-plus:before{content:"\e494"}.fa-book-quran:before,.fa-quran:before{content:"\f687"}.fa-anchor:before{content:"\f13d"}.fa-border-all:before{content:"\f84c"}.fa-angry:before,.fa-face-angry:before{content:"\f556"}.fa-cookie-bite:before{content:"\f564"}.fa-arrow-trend-down:before{content:"\e097"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-draw-polygon:before{content:"\f5ee"}.fa-balance-scale:before,.fa-scale-balanced:before{content:"\f24e"}.fa-gauge-simple-high:before,.fa-tachometer-fast:before,.fa-tachometer:before{content:"\f62a"}.fa-shower:before{content:"\f2cc"}.fa-desktop-alt:before,.fa-desktop:before{content:"\f390"}.fa-m:before{content:"\4d"}.fa-table-list:before,.fa-th-list:before{content:"\f00b"}.fa-comment-sms:before,.fa-sms:before{content:"\f7cd"}.fa-book:before{content:"\f02d"}.fa-user-plus:before{content:"\f234"}.fa-check:before{content:"\f00c"}.fa-battery-4:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-house-circle-check:before{content:"\e509"}.fa-angle-left:before{content:"\f104"}.fa-diagram-successor:before{content:"\e47a"}.fa-truck-arrow-right:before{content:"\e58b"}.fa-arrows-split-up-and-left:before{content:"\e4bc"}.fa-fist-raised:before,.fa-hand-fist:before{content:"\f6de"}.fa-cloud-moon:before{content:"\f6c3"}.fa-briefcase:before{content:"\f0b1"}.fa-person-falling:before{content:"\e546"}.fa-image-portrait:before,.fa-portrait:before{content:"\f3e0"}.fa-user-tag:before{content:"\f507"}.fa-rug:before{content:"\e569"}.fa-earth-europe:before,.fa-globe-europe:before{content:"\f7a2"}.fa-cart-flatbed-suitcase:before,.fa-luggage-cart:before{content:"\f59d"}.fa-rectangle-times:before,.fa-rectangle-xmark:before,.fa-times-rectangle:before,.fa-window-close:before{content:"\f410"}.fa-baht-sign:before{content:"\e0ac"}.fa-book-open:before{content:"\f518"}.fa-book-journal-whills:before,.fa-journal-whills:before{content:"\f66a"}.fa-handcuffs:before{content:"\e4f8"}.fa-exclamation-triangle:before,.fa-triangle-exclamation:before,.fa-warning:before{content:"\f071"}.fa-database:before{content:"\f1c0"}.fa-arrow-turn-right:before,.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-bottle-droplet:before{content:"\e4c4"}.fa-mask-face:before{content:"\e1d7"}.fa-hill-rockslide:before{content:"\e508"}.fa-exchange-alt:before,.fa-right-left:before{content:"\f362"}.fa-paper-plane:before{content:"\f1d8"}.fa-road-circle-exclamation:before{content:"\e565"}.fa-dungeon:before{content:"\f6d9"}.fa-align-right:before{content:"\f038"}.fa-money-bill-1-wave:before,.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-life-ring:before{content:"\f1cd"}.fa-hands:before,.fa-sign-language:before,.fa-signing:before{content:"\f2a7"}.fa-calendar-day:before{content:"\f783"}.fa-ladder-water:before,.fa-swimming-pool:before,.fa-water-ladder:before{content:"\f5c5"}.fa-arrows-up-down:before,.fa-arrows-v:before{content:"\f07d"}.fa-face-grimace:before,.fa-grimace:before{content:"\f57f"}.fa-wheelchair-alt:before,.fa-wheelchair-move:before{content:"\e2ce"}.fa-level-down-alt:before,.fa-turn-down:before{content:"\f3be"}.fa-person-walking-arrow-right:before{content:"\e552"}.fa-envelope-square:before,.fa-square-envelope:before{content:"\f199"}.fa-dice:before{content:"\f522"}.fa-bowling-ball:before{content:"\f436"}.fa-brain:before{content:"\f5dc"}.fa-band-aid:before,.fa-bandage:before{content:"\f462"}.fa-calendar-minus:before{content:"\f272"}.fa-circle-xmark:before,.fa-times-circle:before,.fa-xmark-circle:before{content:"\f057"}.fa-gifts:before{content:"\f79c"}.fa-hotel:before{content:"\f594"}.fa-earth-asia:before,.fa-globe-asia:before{content:"\f57e"}.fa-id-card-alt:before,.fa-id-card-clip:before{content:"\f47f"}.fa-magnifying-glass-plus:before,.fa-search-plus:before{content:"\f00e"}.fa-thumbs-up:before{content:"\f164"}.fa-user-clock:before{content:"\f4fd"}.fa-allergies:before,.fa-hand-dots:before{content:"\f461"}.fa-file-invoice:before{content:"\f570"}.fa-window-minimize:before{content:"\f2d1"}.fa-coffee:before,.fa-mug-saucer:before{content:"\f0f4"}.fa-brush:before{content:"\f55d"}.fa-mask:before{content:"\f6fa"}.fa-magnifying-glass-minus:before,.fa-search-minus:before{content:"\f010"}.fa-ruler-vertical:before{content:"\f548"}.fa-user-alt:before,.fa-user-large:before{content:"\f406"}.fa-train-tram:before{content:"\e5b4"}.fa-user-nurse:before{content:"\f82f"}.fa-syringe:before{content:"\f48e"}.fa-cloud-sun:before{content:"\f6c4"}.fa-stopwatch-20:before{content:"\e06f"}.fa-square-full:before{content:"\f45c"}.fa-magnet:before{content:"\f076"}.fa-jar:before{content:"\e516"}.fa-note-sticky:before,.fa-sticky-note:before{content:"\f249"}.fa-bug-slash:before{content:"\e490"}.fa-arrow-up-from-water-pump:before{content:"\e4b6"}.fa-bone:before{content:"\f5d7"}.fa-user-injured:before{content:"\f728"}.fa-face-sad-tear:before,.fa-sad-tear:before{content:"\f5b4"}.fa-plane:before{content:"\f072"}.fa-tent-arrows-down:before{content:"\e581"}.fa-exclamation:before{content:"\21"}.fa-arrows-spin:before{content:"\e4bb"}.fa-print:before{content:"\f02f"}.fa-try:before,.fa-turkish-lira-sign:before,.fa-turkish-lira:before{content:"\e2bb"}.fa-dollar-sign:before,.fa-dollar:before,.fa-usd:before{content:"\24"}.fa-x:before{content:"\58"}.fa-magnifying-glass-dollar:before,.fa-search-dollar:before{content:"\f688"}.fa-users-cog:before,.fa-users-gear:before{content:"\f509"}.fa-person-military-pointing:before{content:"\e54a"}.fa-bank:before,.fa-building-columns:before,.fa-institution:before,.fa-museum:before,.fa-university:before{content:"\f19c"}.fa-umbrella:before{content:"\f0e9"}.fa-trowel:before{content:"\e589"}.fa-d:before{content:"\44"}.fa-stapler:before{content:"\e5af"}.fa-masks-theater:before,.fa-theater-masks:before{content:"\f630"}.fa-kip-sign:before{content:"\e1c4"}.fa-hand-point-left:before{content:"\f0a5"}.fa-handshake-alt:before,.fa-handshake-simple:before{content:"\f4c6"}.fa-fighter-jet:before,.fa-jet-fighter:before{content:"\f0fb"}.fa-share-alt-square:before,.fa-square-share-nodes:before{content:"\f1e1"}.fa-barcode:before{content:"\f02a"}.fa-plus-minus:before{content:"\e43c"}.fa-video-camera:before,.fa-video:before{content:"\f03d"}.fa-graduation-cap:before,.fa-mortar-board:before{content:"\f19d"}.fa-hand-holding-medical:before{content:"\e05c"}.fa-person-circle-check:before{content:"\e53e"}.fa-level-up-alt:before,.fa-turn-up:before{content:"\f3bf"} +.fa-sr-only,.fa-sr-only-focusable:not(:focus),.sr-only,.sr-only-focusable:not(:focus){position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}:host,:root{--fa-style-family-brands:"Font Awesome 6 Brands";--fa-font-brands:normal 400 1em/1 "Font Awesome 6 Brands"}@font-face{font-family:"Font Awesome 6 Brands";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}.fa-brands,.fab{font-weight:400}.fa-monero:before{content:"\f3d0"}.fa-hooli:before{content:"\f427"}.fa-yelp:before{content:"\f1e9"}.fa-cc-visa:before{content:"\f1f0"}.fa-lastfm:before{content:"\f202"}.fa-shopware:before{content:"\f5b5"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-aws:before{content:"\f375"}.fa-redhat:before{content:"\f7bc"}.fa-yoast:before{content:"\f2b1"}.fa-cloudflare:before{content:"\e07d"}.fa-ups:before{content:"\f7e0"}.fa-wpexplorer:before{content:"\f2de"}.fa-dyalog:before{content:"\f399"}.fa-bity:before{content:"\f37a"}.fa-stackpath:before{content:"\f842"}.fa-buysellads:before{content:"\f20d"}.fa-first-order:before{content:"\f2b0"}.fa-modx:before{content:"\f285"}.fa-guilded:before{content:"\e07e"}.fa-vnv:before{content:"\f40b"}.fa-js-square:before,.fa-square-js:before{content:"\f3b9"}.fa-microsoft:before{content:"\f3ca"}.fa-qq:before{content:"\f1d6"}.fa-orcid:before{content:"\f8d2"}.fa-java:before{content:"\f4e4"}.fa-invision:before{content:"\f7b0"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-centercode:before{content:"\f380"}.fa-glide-g:before{content:"\f2a6"}.fa-drupal:before{content:"\f1a9"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-unity:before{content:"\e049"}.fa-whmcs:before{content:"\f40d"}.fa-rocketchat:before{content:"\f3e8"}.fa-vk:before{content:"\f189"}.fa-untappd:before{content:"\f405"}.fa-mailchimp:before{content:"\f59e"}.fa-css3-alt:before{content:"\f38b"}.fa-reddit-square:before,.fa-square-reddit:before{content:"\f1a2"}.fa-vimeo-v:before{content:"\f27d"}.fa-contao:before{content:"\f26d"}.fa-square-font-awesome:before{content:"\e5ad"}.fa-deskpro:before{content:"\f38f"}.fa-sistrix:before{content:"\f3ee"}.fa-instagram-square:before,.fa-square-instagram:before{content:"\e055"}.fa-battle-net:before{content:"\f835"}.fa-the-red-yeti:before{content:"\f69d"}.fa-hacker-news-square:before,.fa-square-hacker-news:before{content:"\f3af"}.fa-edge:before{content:"\f282"}.fa-napster:before{content:"\f3d2"}.fa-snapchat-square:before,.fa-square-snapchat:before{content:"\f2ad"}.fa-google-plus-g:before{content:"\f0d5"}.fa-artstation:before{content:"\f77a"}.fa-markdown:before{content:"\f60f"}.fa-sourcetree:before{content:"\f7d3"}.fa-google-plus:before{content:"\f2b3"}.fa-diaspora:before{content:"\f791"}.fa-foursquare:before{content:"\f180"}.fa-stack-overflow:before{content:"\f16c"}.fa-github-alt:before{content:"\f113"}.fa-phoenix-squadron:before{content:"\f511"}.fa-pagelines:before{content:"\f18c"}.fa-algolia:before{content:"\f36c"}.fa-red-river:before{content:"\f3e3"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-safari:before{content:"\f267"}.fa-google:before{content:"\f1a0"}.fa-font-awesome-alt:before,.fa-square-font-awesome-stroke:before{content:"\f35c"}.fa-atlassian:before{content:"\f77b"}.fa-linkedin-in:before{content:"\f0e1"}.fa-digital-ocean:before{content:"\f391"}.fa-nimblr:before{content:"\f5a8"}.fa-chromecast:before{content:"\f838"}.fa-evernote:before{content:"\f839"}.fa-hacker-news:before{content:"\f1d4"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-adversal:before{content:"\f36a"}.fa-creative-commons:before{content:"\f25e"}.fa-watchman-monitoring:before{content:"\e087"}.fa-fonticons:before{content:"\f280"}.fa-weixin:before{content:"\f1d7"}.fa-shirtsinbulk:before{content:"\f214"}.fa-codepen:before{content:"\f1cb"}.fa-git-alt:before{content:"\f841"}.fa-lyft:before{content:"\f3c3"}.fa-rev:before{content:"\f5b2"}.fa-windows:before{content:"\f17a"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-square-viadeo:before,.fa-viadeo-square:before{content:"\f2aa"}.fa-meetup:before{content:"\f2e0"}.fa-centos:before{content:"\f789"}.fa-adn:before{content:"\f170"}.fa-cloudsmith:before{content:"\f384"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-dribbble-square:before,.fa-square-dribbble:before{content:"\f397"}.fa-codiepie:before{content:"\f284"}.fa-node:before{content:"\f419"}.fa-mix:before{content:"\f3cb"}.fa-steam:before{content:"\f1b6"}.fa-cc-apple-pay:before{content:"\f416"}.fa-scribd:before{content:"\f28a"}.fa-openid:before{content:"\f19b"}.fa-instalod:before{content:"\e081"}.fa-expeditedssl:before{content:"\f23e"}.fa-sellcast:before{content:"\f2da"}.fa-square-twitter:before,.fa-twitter-square:before{content:"\f081"}.fa-r-project:before{content:"\f4f7"}.fa-delicious:before{content:"\f1a5"}.fa-freebsd:before{content:"\f3a4"}.fa-vuejs:before{content:"\f41f"}.fa-accusoft:before{content:"\f369"}.fa-ioxhost:before{content:"\f208"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-app-store:before{content:"\f36f"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-itunes-note:before{content:"\f3b5"}.fa-golang:before{content:"\e40f"}.fa-kickstarter:before{content:"\f3bb"}.fa-grav:before{content:"\f2d6"}.fa-weibo:before{content:"\f18a"}.fa-uncharted:before{content:"\e084"}.fa-firstdraft:before{content:"\f3a1"}.fa-square-youtube:before,.fa-youtube-square:before{content:"\f431"}.fa-wikipedia-w:before{content:"\f266"}.fa-rendact:before,.fa-wpressr:before{content:"\f3e4"}.fa-angellist:before{content:"\f209"}.fa-galactic-republic:before{content:"\f50c"}.fa-nfc-directional:before{content:"\e530"}.fa-skype:before{content:"\f17e"}.fa-joget:before{content:"\f3b7"}.fa-fedora:before{content:"\f798"}.fa-stripe-s:before{content:"\f42a"}.fa-meta:before{content:"\e49b"}.fa-laravel:before{content:"\f3bd"}.fa-hotjar:before{content:"\f3b1"}.fa-bluetooth-b:before{content:"\f294"}.fa-sticker-mule:before{content:"\f3f7"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-hips:before{content:"\f452"}.fa-behance:before{content:"\f1b4"}.fa-reddit:before{content:"\f1a1"}.fa-discord:before{content:"\f392"}.fa-chrome:before{content:"\f268"}.fa-app-store-ios:before{content:"\f370"}.fa-cc-discover:before{content:"\f1f2"}.fa-wpbeginner:before{content:"\f297"}.fa-confluence:before{content:"\f78d"}.fa-mdb:before{content:"\f8ca"}.fa-dochub:before{content:"\f394"}.fa-accessible-icon:before{content:"\f368"}.fa-ebay:before{content:"\f4f4"}.fa-amazon:before{content:"\f270"}.fa-unsplash:before{content:"\e07c"}.fa-yarn:before{content:"\f7e3"}.fa-square-steam:before,.fa-steam-square:before{content:"\f1b7"}.fa-500px:before{content:"\f26e"}.fa-square-vimeo:before,.fa-vimeo-square:before{content:"\f194"}.fa-asymmetrik:before{content:"\f372"}.fa-font-awesome-flag:before,.fa-font-awesome-logo-full:before,.fa-font-awesome:before{content:"\f2b4"}.fa-gratipay:before{content:"\f184"}.fa-apple:before{content:"\f179"}.fa-hive:before{content:"\e07f"}.fa-gitkraken:before{content:"\f3a6"}.fa-keybase:before{content:"\f4f5"}.fa-apple-pay:before{content:"\f415"}.fa-padlet:before{content:"\e4a0"}.fa-amazon-pay:before{content:"\f42c"}.fa-github-square:before,.fa-square-github:before{content:"\f092"}.fa-stumbleupon:before{content:"\f1a4"}.fa-fedex:before{content:"\f797"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-shopify:before{content:"\e057"}.fa-neos:before{content:"\f612"}.fa-hackerrank:before{content:"\f5f7"}.fa-researchgate:before{content:"\f4f8"}.fa-swift:before{content:"\f8e1"}.fa-angular:before{content:"\f420"}.fa-speakap:before{content:"\f3f3"}.fa-angrycreative:before{content:"\f36e"}.fa-y-combinator:before{content:"\f23b"}.fa-empire:before{content:"\f1d1"}.fa-envira:before{content:"\f299"}.fa-gitlab-square:before,.fa-square-gitlab:before{content:"\e5ae"}.fa-studiovinari:before{content:"\f3f8"}.fa-pied-piper:before{content:"\f2ae"}.fa-wordpress:before{content:"\f19a"}.fa-product-hunt:before{content:"\f288"}.fa-firefox:before{content:"\f269"}.fa-linode:before{content:"\f2b8"}.fa-goodreads:before{content:"\f3a8"}.fa-odnoklassniki-square:before,.fa-square-odnoklassniki:before{content:"\f264"}.fa-jsfiddle:before{content:"\f1cc"}.fa-sith:before{content:"\f512"}.fa-themeisle:before{content:"\f2b2"}.fa-page4:before{content:"\f3d7"}.fa-hashnode:before{content:"\e499"}.fa-react:before{content:"\f41b"}.fa-cc-paypal:before{content:"\f1f4"}.fa-squarespace:before{content:"\f5be"}.fa-cc-stripe:before{content:"\f1f5"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-bitcoin:before{content:"\f379"}.fa-keycdn:before{content:"\f3ba"}.fa-opera:before{content:"\f26a"}.fa-itch-io:before{content:"\f83a"}.fa-umbraco:before{content:"\f8e8"}.fa-galactic-senate:before{content:"\f50d"}.fa-ubuntu:before{content:"\f7df"}.fa-draft2digital:before{content:"\f396"}.fa-stripe:before{content:"\f429"}.fa-houzz:before{content:"\f27c"}.fa-gg:before{content:"\f260"}.fa-dhl:before{content:"\f790"}.fa-pinterest-square:before,.fa-square-pinterest:before{content:"\f0d3"}.fa-xing:before{content:"\f168"}.fa-blackberry:before{content:"\f37b"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-playstation:before{content:"\f3df"}.fa-quinscape:before{content:"\f459"}.fa-less:before{content:"\f41d"}.fa-blogger-b:before{content:"\f37d"}.fa-opencart:before{content:"\f23d"}.fa-vine:before{content:"\f1ca"}.fa-paypal:before{content:"\f1ed"}.fa-gitlab:before{content:"\f296"}.fa-typo3:before{content:"\f42b"}.fa-reddit-alien:before{content:"\f281"}.fa-yahoo:before{content:"\f19e"}.fa-dailymotion:before{content:"\e052"}.fa-affiliatetheme:before{content:"\f36b"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-bootstrap:before{content:"\f836"}.fa-odnoklassniki:before{content:"\f263"}.fa-nfc-symbol:before{content:"\e531"}.fa-ethereum:before{content:"\f42e"}.fa-speaker-deck:before{content:"\f83c"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-patreon:before{content:"\f3d9"}.fa-avianex:before{content:"\f374"}.fa-ello:before{content:"\f5f1"}.fa-gofore:before{content:"\f3a7"}.fa-bimobject:before{content:"\f378"}.fa-facebook-f:before{content:"\f39e"}.fa-google-plus-square:before,.fa-square-google-plus:before{content:"\f0d4"}.fa-mandalorian:before{content:"\f50f"}.fa-first-order-alt:before{content:"\f50a"}.fa-osi:before{content:"\f41a"}.fa-google-wallet:before{content:"\f1ee"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-periscope:before{content:"\f3da"}.fa-fulcrum:before{content:"\f50b"}.fa-cloudscale:before{content:"\f383"}.fa-forumbee:before{content:"\f211"}.fa-mizuni:before{content:"\f3cc"}.fa-schlix:before{content:"\f3ea"}.fa-square-xing:before,.fa-xing-square:before{content:"\f169"}.fa-bandcamp:before{content:"\f2d5"}.fa-wpforms:before{content:"\f298"}.fa-cloudversify:before{content:"\f385"}.fa-usps:before{content:"\f7e1"}.fa-megaport:before{content:"\f5a3"}.fa-magento:before{content:"\f3c4"}.fa-spotify:before{content:"\f1bc"}.fa-optin-monster:before{content:"\f23c"}.fa-fly:before{content:"\f417"}.fa-aviato:before{content:"\f421"}.fa-itunes:before{content:"\f3b4"}.fa-cuttlefish:before{content:"\f38c"}.fa-blogger:before{content:"\f37c"}.fa-flickr:before{content:"\f16e"}.fa-viber:before{content:"\f409"}.fa-soundcloud:before{content:"\f1be"}.fa-digg:before{content:"\f1a6"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-symfony:before{content:"\f83d"}.fa-maxcdn:before{content:"\f136"}.fa-etsy:before{content:"\f2d7"}.fa-facebook-messenger:before{content:"\f39f"}.fa-audible:before{content:"\f373"}.fa-think-peaks:before{content:"\f731"}.fa-bilibili:before{content:"\e3d9"}.fa-erlang:before{content:"\f39d"}.fa-cotton-bureau:before{content:"\f89e"}.fa-dashcube:before{content:"\f210"}.fa-42-group:before,.fa-innosoft:before{content:"\e080"}.fa-stack-exchange:before{content:"\f18d"}.fa-elementor:before{content:"\f430"}.fa-pied-piper-square:before,.fa-square-pied-piper:before{content:"\e01e"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-palfed:before{content:"\f3d8"}.fa-superpowers:before{content:"\f2dd"}.fa-resolving:before{content:"\f3e7"}.fa-xbox:before{content:"\f412"}.fa-searchengin:before{content:"\f3eb"}.fa-tiktok:before{content:"\e07b"}.fa-facebook-square:before,.fa-square-facebook:before{content:"\f082"}.fa-renren:before{content:"\f18b"}.fa-linux:before{content:"\f17c"}.fa-glide:before{content:"\f2a5"}.fa-linkedin:before{content:"\f08c"}.fa-hubspot:before{content:"\f3b2"}.fa-deploydog:before{content:"\f38e"}.fa-twitch:before{content:"\f1e8"}.fa-ravelry:before{content:"\f2d9"}.fa-mixer:before{content:"\e056"}.fa-lastfm-square:before,.fa-square-lastfm:before{content:"\f203"}.fa-vimeo:before{content:"\f40a"}.fa-mendeley:before{content:"\f7b3"}.fa-uniregistry:before{content:"\f404"}.fa-figma:before{content:"\f799"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-dropbox:before{content:"\f16b"}.fa-instagram:before{content:"\f16d"}.fa-cmplid:before{content:"\e360"}.fa-facebook:before{content:"\f09a"}.fa-gripfire:before{content:"\f3ac"}.fa-jedi-order:before{content:"\f50e"}.fa-uikit:before{content:"\f403"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-phabricator:before{content:"\f3db"}.fa-ussunnah:before{content:"\f407"}.fa-earlybirds:before{content:"\f39a"}.fa-trade-federation:before{content:"\f513"}.fa-autoprefixer:before{content:"\f41c"}.fa-whatsapp:before{content:"\f232"}.fa-slideshare:before{content:"\f1e7"}.fa-google-play:before{content:"\f3ab"}.fa-viadeo:before{content:"\f2a9"}.fa-line:before{content:"\f3c0"}.fa-google-drive:before{content:"\f3aa"}.fa-servicestack:before{content:"\f3ec"}.fa-simplybuilt:before{content:"\f215"}.fa-bitbucket:before{content:"\f171"}.fa-imdb:before{content:"\f2d8"}.fa-deezer:before{content:"\e077"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-jira:before{content:"\f7b1"}.fa-docker:before{content:"\f395"}.fa-screenpal:before{content:"\e570"}.fa-bluetooth:before{content:"\f293"}.fa-gitter:before{content:"\f426"}.fa-d-and-d:before{content:"\f38d"}.fa-microblog:before{content:"\e01a"}.fa-cc-diners-club:before{content:"\f24c"}.fa-gg-circle:before{content:"\f261"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-yandex:before{content:"\f413"}.fa-readme:before{content:"\f4d5"}.fa-html5:before{content:"\f13b"}.fa-sellsy:before{content:"\f213"}.fa-sass:before{content:"\f41e"}.fa-wirsindhandwerk:before,.fa-wsh:before{content:"\e2d0"}.fa-buromobelexperte:before{content:"\f37f"}.fa-salesforce:before{content:"\f83b"}.fa-octopus-deploy:before{content:"\e082"}.fa-medapps:before{content:"\f3c6"}.fa-ns8:before{content:"\f3d5"}.fa-pinterest-p:before{content:"\f231"}.fa-apper:before{content:"\f371"}.fa-fort-awesome:before{content:"\f286"}.fa-waze:before{content:"\f83f"}.fa-cc-jcb:before{content:"\f24b"}.fa-snapchat-ghost:before,.fa-snapchat:before{content:"\f2ab"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-rust:before{content:"\e07a"}.fa-wix:before{content:"\f5cf"}.fa-behance-square:before,.fa-square-behance:before{content:"\f1b5"}.fa-supple:before{content:"\f3f9"}.fa-rebel:before{content:"\f1d0"}.fa-css3:before{content:"\f13c"}.fa-staylinked:before{content:"\f3f5"}.fa-kaggle:before{content:"\f5fa"}.fa-space-awesome:before{content:"\e5ac"}.fa-deviantart:before{content:"\f1bd"}.fa-cpanel:before{content:"\f388"}.fa-goodreads-g:before{content:"\f3a9"}.fa-git-square:before,.fa-square-git:before{content:"\f1d2"}.fa-square-tumblr:before,.fa-tumblr-square:before{content:"\f174"}.fa-trello:before{content:"\f181"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-get-pocket:before{content:"\f265"}.fa-perbyte:before{content:"\e083"}.fa-grunt:before{content:"\f3ad"}.fa-weebly:before{content:"\f5cc"}.fa-connectdevelop:before{content:"\f20e"}.fa-leanpub:before{content:"\f212"}.fa-black-tie:before{content:"\f27e"}.fa-themeco:before{content:"\f5c6"}.fa-python:before{content:"\f3e2"}.fa-android:before{content:"\f17b"}.fa-bots:before{content:"\e340"}.fa-free-code-camp:before{content:"\f2c5"}.fa-hornbill:before{content:"\f592"}.fa-js:before{content:"\f3b8"}.fa-ideal:before{content:"\e013"}.fa-git:before{content:"\f1d3"}.fa-dev:before{content:"\f6cc"}.fa-sketch:before{content:"\f7c6"}.fa-yandex-international:before{content:"\f414"}.fa-cc-amex:before{content:"\f1f3"}.fa-uber:before{content:"\f402"}.fa-github:before{content:"\f09b"}.fa-php:before{content:"\f457"}.fa-alipay:before{content:"\f642"}.fa-youtube:before{content:"\f167"}.fa-skyatlas:before{content:"\f216"}.fa-firefox-browser:before{content:"\e007"}.fa-replyd:before{content:"\f3e6"}.fa-suse:before{content:"\f7d6"}.fa-jenkins:before{content:"\f3b6"}.fa-twitter:before{content:"\f099"}.fa-rockrms:before{content:"\f3e9"}.fa-pinterest:before{content:"\f0d2"}.fa-buffer:before{content:"\f837"}.fa-npm:before{content:"\f3d4"}.fa-yammer:before{content:"\f840"}.fa-btc:before{content:"\f15a"}.fa-dribbble:before{content:"\f17d"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-internet-explorer:before{content:"\f26b"}.fa-stubber:before{content:"\e5c7"}.fa-telegram-plane:before,.fa-telegram:before{content:"\f2c6"}.fa-old-republic:before{content:"\f510"}.fa-odysee:before{content:"\e5c6"}.fa-square-whatsapp:before,.fa-whatsapp-square:before{content:"\f40c"}.fa-node-js:before{content:"\f3d3"}.fa-edge-legacy:before{content:"\e078"}.fa-slack-hash:before,.fa-slack:before{content:"\f198"}.fa-medrt:before{content:"\f3c8"}.fa-usb:before{content:"\f287"}.fa-tumblr:before{content:"\f173"}.fa-vaadin:before{content:"\f408"}.fa-quora:before{content:"\f2c4"}.fa-reacteurope:before{content:"\f75d"}.fa-medium-m:before,.fa-medium:before{content:"\f23a"}.fa-amilia:before{content:"\f36d"}.fa-mixcloud:before{content:"\f289"}.fa-flipboard:before{content:"\f44d"}.fa-viacoin:before{content:"\f237"}.fa-critical-role:before{content:"\f6c9"}.fa-sitrox:before{content:"\e44a"}.fa-discourse:before{content:"\f393"}.fa-joomla:before{content:"\f1aa"}.fa-mastodon:before{content:"\f4f6"}.fa-airbnb:before{content:"\f834"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-buy-n-large:before{content:"\f8a6"}.fa-gulp:before{content:"\f3ae"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-strava:before{content:"\f428"}.fa-ember:before{content:"\f423"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-teamspeak:before{content:"\f4f9"}.fa-pushed:before{content:"\f3e1"}.fa-wordpress-simple:before{content:"\f411"}.fa-nutritionix:before{content:"\f3d6"}.fa-wodu:before{content:"\e088"}.fa-google-pay:before{content:"\e079"}.fa-intercom:before{content:"\f7af"}.fa-zhihu:before{content:"\f63f"}.fa-korvue:before{content:"\f42f"}.fa-pix:before{content:"\e43a"}.fa-steam-symbol:before{content:"\f3f6"}:host,:root{--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-weight:400}:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}@font-face{font-family:"Font Awesome 5 Brands";font-display:block;font-weight:400;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:900;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:400;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f27a} \ No newline at end of file diff --git a/frontend/libs/fontawesome/webfonts/fa-brands-400.woff2 b/frontend/libs/fontawesome/webfonts/fa-brands-400.woff2 new file mode 100644 index 0000000..71e3185 Binary files /dev/null and b/frontend/libs/fontawesome/webfonts/fa-brands-400.woff2 differ diff --git a/frontend/libs/fontawesome/webfonts/fa-regular-400.woff2 b/frontend/libs/fontawesome/webfonts/fa-regular-400.woff2 new file mode 100644 index 0000000..7f02168 Binary files /dev/null and b/frontend/libs/fontawesome/webfonts/fa-regular-400.woff2 differ diff --git a/frontend/libs/fontawesome/webfonts/fa-solid-900.woff2 b/frontend/libs/fontawesome/webfonts/fa-solid-900.woff2 new file mode 100644 index 0000000..5c16cd3 Binary files /dev/null and b/frontend/libs/fontawesome/webfonts/fa-solid-900.woff2 differ diff --git a/frontend/libs/vue.global.js b/frontend/libs/vue.global.js new file mode 100644 index 0000000..3b164ab --- /dev/null +++ b/frontend/libs/vue.global.js @@ -0,0 +1,18323 @@ +/** +* vue v3.5.23 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/ +var Vue = (function (exports) { + 'use strict'; + + // @__NO_SIDE_EFFECTS__ + function makeMap(str) { + const map = /* @__PURE__ */ Object.create(null); + for (const key of str.split(",")) map[key] = 1; + return (val) => val in map; + } + + const EMPTY_OBJ = Object.freeze({}) ; + const EMPTY_ARR = Object.freeze([]) ; + const NOOP = () => { + }; + const NO = () => false; + const isOn = (key) => key.charCodeAt(0) === 111 && key.charCodeAt(1) === 110 && // uppercase letter + (key.charCodeAt(2) > 122 || key.charCodeAt(2) < 97); + const isModelListener = (key) => key.startsWith("onUpdate:"); + const extend = Object.assign; + const remove = (arr, el) => { + const i = arr.indexOf(el); + if (i > -1) { + arr.splice(i, 1); + } + }; + const hasOwnProperty$1 = Object.prototype.hasOwnProperty; + const hasOwn = (val, key) => hasOwnProperty$1.call(val, key); + const isArray = Array.isArray; + const isMap = (val) => toTypeString(val) === "[object Map]"; + const isSet = (val) => toTypeString(val) === "[object Set]"; + const isDate = (val) => toTypeString(val) === "[object Date]"; + const isRegExp = (val) => toTypeString(val) === "[object RegExp]"; + const isFunction = (val) => typeof val === "function"; + const isString = (val) => typeof val === "string"; + const isSymbol = (val) => typeof val === "symbol"; + const isObject = (val) => val !== null && typeof val === "object"; + const isPromise = (val) => { + return (isObject(val) || isFunction(val)) && isFunction(val.then) && isFunction(val.catch); + }; + const objectToString = Object.prototype.toString; + const toTypeString = (value) => objectToString.call(value); + const toRawType = (value) => { + return toTypeString(value).slice(8, -1); + }; + const isPlainObject = (val) => toTypeString(val) === "[object Object]"; + const isIntegerKey = (key) => isString(key) && key !== "NaN" && key[0] !== "-" && "" + parseInt(key, 10) === key; + const isReservedProp = /* @__PURE__ */ makeMap( + // the leading comma is intentional so empty string "" is also included + ",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted" + ); + const isBuiltInDirective = /* @__PURE__ */ makeMap( + "bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo" + ); + const cacheStringFunction = (fn) => { + const cache = /* @__PURE__ */ Object.create(null); + return ((str) => { + const hit = cache[str]; + return hit || (cache[str] = fn(str)); + }); + }; + const camelizeRE = /-\w/g; + const camelize = cacheStringFunction( + (str) => { + return str.replace(camelizeRE, (c) => c.slice(1).toUpperCase()); + } + ); + const hyphenateRE = /\B([A-Z])/g; + const hyphenate = cacheStringFunction( + (str) => str.replace(hyphenateRE, "-$1").toLowerCase() + ); + const capitalize = cacheStringFunction((str) => { + return str.charAt(0).toUpperCase() + str.slice(1); + }); + const toHandlerKey = cacheStringFunction( + (str) => { + const s = str ? `on${capitalize(str)}` : ``; + return s; + } + ); + const hasChanged = (value, oldValue) => !Object.is(value, oldValue); + const invokeArrayFns = (fns, ...arg) => { + for (let i = 0; i < fns.length; i++) { + fns[i](...arg); + } + }; + const def = (obj, key, value, writable = false) => { + Object.defineProperty(obj, key, { + configurable: true, + enumerable: false, + writable, + value + }); + }; + const looseToNumber = (val) => { + const n = parseFloat(val); + return isNaN(n) ? val : n; + }; + const toNumber = (val) => { + const n = isString(val) ? Number(val) : NaN; + return isNaN(n) ? val : n; + }; + let _globalThis; + const getGlobalThis = () => { + return _globalThis || (_globalThis = typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : {}); + }; + function genCacheKey(source, options) { + return source + JSON.stringify( + options, + (_, val) => typeof val === "function" ? val.toString() : val + ); + } + + const PatchFlagNames = { + [1]: `TEXT`, + [2]: `CLASS`, + [4]: `STYLE`, + [8]: `PROPS`, + [16]: `FULL_PROPS`, + [32]: `NEED_HYDRATION`, + [64]: `STABLE_FRAGMENT`, + [128]: `KEYED_FRAGMENT`, + [256]: `UNKEYED_FRAGMENT`, + [512]: `NEED_PATCH`, + [1024]: `DYNAMIC_SLOTS`, + [2048]: `DEV_ROOT_FRAGMENT`, + [-1]: `CACHED`, + [-2]: `BAIL` + }; + + const slotFlagsText = { + [1]: "STABLE", + [2]: "DYNAMIC", + [3]: "FORWARDED" + }; + + const GLOBALS_ALLOWED = "Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error,Symbol"; + const isGloballyAllowed = /* @__PURE__ */ makeMap(GLOBALS_ALLOWED); + + const range = 2; + function generateCodeFrame(source, start = 0, end = source.length) { + start = Math.max(0, Math.min(start, source.length)); + end = Math.max(0, Math.min(end, source.length)); + if (start > end) return ""; + let lines = source.split(/(\r?\n)/); + const newlineSequences = lines.filter((_, idx) => idx % 2 === 1); + lines = lines.filter((_, idx) => idx % 2 === 0); + let count = 0; + const res = []; + for (let i = 0; i < lines.length; i++) { + count += lines[i].length + (newlineSequences[i] && newlineSequences[i].length || 0); + if (count >= start) { + for (let j = i - range; j <= i + range || end > count; j++) { + if (j < 0 || j >= lines.length) continue; + const line = j + 1; + res.push( + `${line}${" ".repeat(Math.max(3 - String(line).length, 0))}| ${lines[j]}` + ); + const lineLength = lines[j].length; + const newLineSeqLength = newlineSequences[j] && newlineSequences[j].length || 0; + if (j === i) { + const pad = start - (count - (lineLength + newLineSeqLength)); + const length = Math.max( + 1, + end > count ? lineLength - pad : end - start + ); + res.push(` | ` + " ".repeat(pad) + "^".repeat(length)); + } else if (j > i) { + if (end > count) { + const length = Math.max(Math.min(end - count, lineLength), 1); + res.push(` | ` + "^".repeat(length)); + } + count += lineLength + newLineSeqLength; + } + } + break; + } + } + return res.join("\n"); + } + + function normalizeStyle(value) { + if (isArray(value)) { + const res = {}; + for (let i = 0; i < value.length; i++) { + const item = value[i]; + const normalized = isString(item) ? parseStringStyle(item) : normalizeStyle(item); + if (normalized) { + for (const key in normalized) { + res[key] = normalized[key]; + } + } + } + return res; + } else if (isString(value) || isObject(value)) { + return value; + } + } + const listDelimiterRE = /;(?![^(]*\))/g; + const propertyDelimiterRE = /:([^]+)/; + const styleCommentRE = /\/\*[^]*?\*\//g; + function parseStringStyle(cssText) { + const ret = {}; + cssText.replace(styleCommentRE, "").split(listDelimiterRE).forEach((item) => { + if (item) { + const tmp = item.split(propertyDelimiterRE); + tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim()); + } + }); + return ret; + } + function stringifyStyle(styles) { + if (!styles) return ""; + if (isString(styles)) return styles; + let ret = ""; + for (const key in styles) { + const value = styles[key]; + if (isString(value) || typeof value === "number") { + const normalizedKey = key.startsWith(`--`) ? key : hyphenate(key); + ret += `${normalizedKey}:${value};`; + } + } + return ret; + } + function normalizeClass(value) { + let res = ""; + if (isString(value)) { + res = value; + } else if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + const normalized = normalizeClass(value[i]); + if (normalized) { + res += normalized + " "; + } + } + } else if (isObject(value)) { + for (const name in value) { + if (value[name]) { + res += name + " "; + } + } + } + return res.trim(); + } + function normalizeProps(props) { + if (!props) return null; + let { class: klass, style } = props; + if (klass && !isString(klass)) { + props.class = normalizeClass(klass); + } + if (style) { + props.style = normalizeStyle(style); + } + return props; + } + + const HTML_TAGS = "html,body,base,head,link,meta,style,title,address,article,aside,footer,header,hgroup,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,summary,template,blockquote,iframe,tfoot"; + const SVG_TAGS = "svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,defs,desc,discard,ellipse,feBlend,feColorMatrix,feComponentTransfer,feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,feDistantLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,feGaussianBlur,feImage,feMerge,feMergeNode,feMorphology,feOffset,fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence,filter,foreignObject,g,hatch,hatchpath,image,line,linearGradient,marker,mask,mesh,meshgradient,meshpatch,meshrow,metadata,mpath,path,pattern,polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,text,textPath,title,tspan,unknown,use,view"; + const MATH_TAGS = "annotation,annotation-xml,maction,maligngroup,malignmark,math,menclose,merror,mfenced,mfrac,mfraction,mglyph,mi,mlabeledtr,mlongdiv,mmultiscripts,mn,mo,mover,mpadded,mphantom,mprescripts,mroot,mrow,ms,mscarries,mscarry,msgroup,msline,mspace,msqrt,msrow,mstack,mstyle,msub,msubsup,msup,mtable,mtd,mtext,mtr,munder,munderover,none,semantics"; + const VOID_TAGS = "area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr"; + const isHTMLTag = /* @__PURE__ */ makeMap(HTML_TAGS); + const isSVGTag = /* @__PURE__ */ makeMap(SVG_TAGS); + const isMathMLTag = /* @__PURE__ */ makeMap(MATH_TAGS); + const isVoidTag = /* @__PURE__ */ makeMap(VOID_TAGS); + + const specialBooleanAttrs = `itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`; + const isSpecialBooleanAttr = /* @__PURE__ */ makeMap(specialBooleanAttrs); + const isBooleanAttr = /* @__PURE__ */ makeMap( + specialBooleanAttrs + `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,inert,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected` + ); + function includeBooleanAttr(value) { + return !!value || value === ""; + } + const isKnownHtmlAttr = /* @__PURE__ */ makeMap( + `accept,accept-charset,accesskey,action,align,allow,alt,async,autocapitalize,autocomplete,autofocus,autoplay,background,bgcolor,border,buffered,capture,challenge,charset,checked,cite,class,code,codebase,color,cols,colspan,content,contenteditable,contextmenu,controls,coords,crossorigin,csp,data,datetime,decoding,default,defer,dir,dirname,disabled,download,draggable,dropzone,enctype,enterkeyhint,for,form,formaction,formenctype,formmethod,formnovalidate,formtarget,headers,height,hidden,high,href,hreflang,http-equiv,icon,id,importance,inert,integrity,ismap,itemprop,keytype,kind,label,lang,language,loading,list,loop,low,manifest,max,maxlength,minlength,media,min,multiple,muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,preload,radiogroup,readonly,referrerpolicy,rel,required,reversed,rows,rowspan,sandbox,scope,scoped,selected,shape,size,sizes,slot,span,spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,target,title,translate,type,usemap,value,width,wrap` + ); + const isKnownSvgAttr = /* @__PURE__ */ makeMap( + `xmlns,accent-height,accumulate,additive,alignment-baseline,alphabetic,amplitude,arabic-form,ascent,attributeName,attributeType,azimuth,baseFrequency,baseline-shift,baseProfile,bbox,begin,bias,by,calcMode,cap-height,class,clip,clipPathUnits,clip-path,clip-rule,color,color-interpolation,color-interpolation-filters,color-profile,color-rendering,contentScriptType,contentStyleType,crossorigin,cursor,cx,cy,d,decelerate,descent,diffuseConstant,direction,display,divisor,dominant-baseline,dur,dx,dy,edgeMode,elevation,enable-background,end,exponent,fill,fill-opacity,fill-rule,filter,filterRes,filterUnits,flood-color,flood-opacity,font-family,font-size,font-size-adjust,font-stretch,font-style,font-variant,font-weight,format,from,fr,fx,fy,g1,g2,glyph-name,glyph-orientation-horizontal,glyph-orientation-vertical,glyphRef,gradientTransform,gradientUnits,hanging,height,href,hreflang,horiz-adv-x,horiz-origin-x,id,ideographic,image-rendering,in,in2,intercept,k,k1,k2,k3,k4,kernelMatrix,kernelUnitLength,kerning,keyPoints,keySplines,keyTimes,lang,lengthAdjust,letter-spacing,lighting-color,limitingConeAngle,local,marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mask,maskContentUnits,maskUnits,mathematical,max,media,method,min,mode,name,numOctaves,offset,opacity,operator,order,orient,orientation,origin,overflow,overline-position,overline-thickness,panose-1,paint-order,path,pathLength,patternContentUnits,patternTransform,patternUnits,ping,pointer-events,points,pointsAtX,pointsAtY,pointsAtZ,preserveAlpha,preserveAspectRatio,primitiveUnits,r,radius,referrerPolicy,refX,refY,rel,rendering-intent,repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,result,rotate,rx,ry,scale,seed,shape-rendering,slope,spacing,specularConstant,specularExponent,speed,spreadMethod,startOffset,stdDeviation,stemh,stemv,stitchTiles,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,string,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,stroke-width,style,surfaceScale,systemLanguage,tabindex,tableValues,target,targetX,targetY,text-anchor,text-decoration,text-rendering,textLength,to,transform,transform-origin,type,u1,u2,underline-position,underline-thickness,unicode,unicode-bidi,unicode-range,units-per-em,v-alphabetic,v-hanging,v-ideographic,v-mathematical,values,vector-effect,version,vert-adv-y,vert-origin-x,vert-origin-y,viewBox,viewTarget,visibility,width,widths,word-spacing,writing-mode,x,x-height,x1,x2,xChannelSelector,xlink:actuate,xlink:arcrole,xlink:href,xlink:role,xlink:show,xlink:title,xlink:type,xmlns:xlink,xml:base,xml:lang,xml:space,y,y1,y2,yChannelSelector,z,zoomAndPan` + ); + function isRenderableAttrValue(value) { + if (value == null) { + return false; + } + const type = typeof value; + return type === "string" || type === "number" || type === "boolean"; + } + + const cssVarNameEscapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g; + function getEscapedCssVarName(key, doubleEscape) { + return key.replace( + cssVarNameEscapeSymbolsRE, + (s) => `\\${s}` + ); + } + + function looseCompareArrays(a, b) { + if (a.length !== b.length) return false; + let equal = true; + for (let i = 0; equal && i < a.length; i++) { + equal = looseEqual(a[i], b[i]); + } + return equal; + } + function looseEqual(a, b) { + if (a === b) return true; + let aValidType = isDate(a); + let bValidType = isDate(b); + if (aValidType || bValidType) { + return aValidType && bValidType ? a.getTime() === b.getTime() : false; + } + aValidType = isSymbol(a); + bValidType = isSymbol(b); + if (aValidType || bValidType) { + return a === b; + } + aValidType = isArray(a); + bValidType = isArray(b); + if (aValidType || bValidType) { + return aValidType && bValidType ? looseCompareArrays(a, b) : false; + } + aValidType = isObject(a); + bValidType = isObject(b); + if (aValidType || bValidType) { + if (!aValidType || !bValidType) { + return false; + } + const aKeysCount = Object.keys(a).length; + const bKeysCount = Object.keys(b).length; + if (aKeysCount !== bKeysCount) { + return false; + } + for (const key in a) { + const aHasKey = a.hasOwnProperty(key); + const bHasKey = b.hasOwnProperty(key); + if (aHasKey && !bHasKey || !aHasKey && bHasKey || !looseEqual(a[key], b[key])) { + return false; + } + } + } + return String(a) === String(b); + } + function looseIndexOf(arr, val) { + return arr.findIndex((item) => looseEqual(item, val)); + } + + const isRef$1 = (val) => { + return !!(val && val["__v_isRef"] === true); + }; + const toDisplayString = (val) => { + return isString(val) ? val : val == null ? "" : isArray(val) || isObject(val) && (val.toString === objectToString || !isFunction(val.toString)) ? isRef$1(val) ? toDisplayString(val.value) : JSON.stringify(val, replacer, 2) : String(val); + }; + const replacer = (_key, val) => { + if (isRef$1(val)) { + return replacer(_key, val.value); + } else if (isMap(val)) { + return { + [`Map(${val.size})`]: [...val.entries()].reduce( + (entries, [key, val2], i) => { + entries[stringifySymbol(key, i) + " =>"] = val2; + return entries; + }, + {} + ) + }; + } else if (isSet(val)) { + return { + [`Set(${val.size})`]: [...val.values()].map((v) => stringifySymbol(v)) + }; + } else if (isSymbol(val)) { + return stringifySymbol(val); + } else if (isObject(val) && !isArray(val) && !isPlainObject(val)) { + return String(val); + } + return val; + }; + const stringifySymbol = (v, i = "") => { + var _a; + return ( + // Symbol.description in es2019+ so we need to cast here to pass + // the lib: es2016 check + isSymbol(v) ? `Symbol(${(_a = v.description) != null ? _a : i})` : v + ); + }; + + function normalizeCssVarValue(value) { + if (value == null) { + return "initial"; + } + if (typeof value === "string") { + return value === "" ? " " : value; + } + if (typeof value !== "number" || !Number.isFinite(value)) { + { + console.warn( + "[Vue warn] Invalid value used for CSS binding. Expected a string or a finite number but received:", + value + ); + } + } + return String(value); + } + + function warn$2(msg, ...args) { + console.warn(`[Vue warn] ${msg}`, ...args); + } + + let activeEffectScope; + class EffectScope { + constructor(detached = false) { + this.detached = detached; + /** + * @internal + */ + this._active = true; + /** + * @internal track `on` calls, allow `on` call multiple times + */ + this._on = 0; + /** + * @internal + */ + this.effects = []; + /** + * @internal + */ + this.cleanups = []; + this._isPaused = false; + this.parent = activeEffectScope; + if (!detached && activeEffectScope) { + this.index = (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push( + this + ) - 1; + } + } + get active() { + return this._active; + } + pause() { + if (this._active) { + this._isPaused = true; + let i, l; + if (this.scopes) { + for (i = 0, l = this.scopes.length; i < l; i++) { + this.scopes[i].pause(); + } + } + for (i = 0, l = this.effects.length; i < l; i++) { + this.effects[i].pause(); + } + } + } + /** + * Resumes the effect scope, including all child scopes and effects. + */ + resume() { + if (this._active) { + if (this._isPaused) { + this._isPaused = false; + let i, l; + if (this.scopes) { + for (i = 0, l = this.scopes.length; i < l; i++) { + this.scopes[i].resume(); + } + } + for (i = 0, l = this.effects.length; i < l; i++) { + this.effects[i].resume(); + } + } + } + } + run(fn) { + if (this._active) { + const currentEffectScope = activeEffectScope; + try { + activeEffectScope = this; + return fn(); + } finally { + activeEffectScope = currentEffectScope; + } + } else { + warn$2(`cannot run an inactive effect scope.`); + } + } + /** + * This should only be called on non-detached scopes + * @internal + */ + on() { + if (++this._on === 1) { + this.prevScope = activeEffectScope; + activeEffectScope = this; + } + } + /** + * This should only be called on non-detached scopes + * @internal + */ + off() { + if (this._on > 0 && --this._on === 0) { + activeEffectScope = this.prevScope; + this.prevScope = void 0; + } + } + stop(fromParent) { + if (this._active) { + this._active = false; + let i, l; + for (i = 0, l = this.effects.length; i < l; i++) { + this.effects[i].stop(); + } + this.effects.length = 0; + for (i = 0, l = this.cleanups.length; i < l; i++) { + this.cleanups[i](); + } + this.cleanups.length = 0; + if (this.scopes) { + for (i = 0, l = this.scopes.length; i < l; i++) { + this.scopes[i].stop(true); + } + this.scopes.length = 0; + } + if (!this.detached && this.parent && !fromParent) { + const last = this.parent.scopes.pop(); + if (last && last !== this) { + this.parent.scopes[this.index] = last; + last.index = this.index; + } + } + this.parent = void 0; + } + } + } + function effectScope(detached) { + return new EffectScope(detached); + } + function getCurrentScope() { + return activeEffectScope; + } + function onScopeDispose(fn, failSilently = false) { + if (activeEffectScope) { + activeEffectScope.cleanups.push(fn); + } else if (!failSilently) { + warn$2( + `onScopeDispose() is called when there is no active effect scope to be associated with.` + ); + } + } + + let activeSub; + const pausedQueueEffects = /* @__PURE__ */ new WeakSet(); + class ReactiveEffect { + constructor(fn) { + this.fn = fn; + /** + * @internal + */ + this.deps = void 0; + /** + * @internal + */ + this.depsTail = void 0; + /** + * @internal + */ + this.flags = 1 | 4; + /** + * @internal + */ + this.next = void 0; + /** + * @internal + */ + this.cleanup = void 0; + this.scheduler = void 0; + if (activeEffectScope && activeEffectScope.active) { + activeEffectScope.effects.push(this); + } + } + pause() { + this.flags |= 64; + } + resume() { + if (this.flags & 64) { + this.flags &= -65; + if (pausedQueueEffects.has(this)) { + pausedQueueEffects.delete(this); + this.trigger(); + } + } + } + /** + * @internal + */ + notify() { + if (this.flags & 2 && !(this.flags & 32)) { + return; + } + if (!(this.flags & 8)) { + batch(this); + } + } + run() { + if (!(this.flags & 1)) { + return this.fn(); + } + this.flags |= 2; + cleanupEffect(this); + prepareDeps(this); + const prevEffect = activeSub; + const prevShouldTrack = shouldTrack; + activeSub = this; + shouldTrack = true; + try { + return this.fn(); + } finally { + if (activeSub !== this) { + warn$2( + "Active effect was not restored correctly - this is likely a Vue internal bug." + ); + } + cleanupDeps(this); + activeSub = prevEffect; + shouldTrack = prevShouldTrack; + this.flags &= -3; + } + } + stop() { + if (this.flags & 1) { + for (let link = this.deps; link; link = link.nextDep) { + removeSub(link); + } + this.deps = this.depsTail = void 0; + cleanupEffect(this); + this.onStop && this.onStop(); + this.flags &= -2; + } + } + trigger() { + if (this.flags & 64) { + pausedQueueEffects.add(this); + } else if (this.scheduler) { + this.scheduler(); + } else { + this.runIfDirty(); + } + } + /** + * @internal + */ + runIfDirty() { + if (isDirty(this)) { + this.run(); + } + } + get dirty() { + return isDirty(this); + } + } + let batchDepth = 0; + let batchedSub; + let batchedComputed; + function batch(sub, isComputed = false) { + sub.flags |= 8; + if (isComputed) { + sub.next = batchedComputed; + batchedComputed = sub; + return; + } + sub.next = batchedSub; + batchedSub = sub; + } + function startBatch() { + batchDepth++; + } + function endBatch() { + if (--batchDepth > 0) { + return; + } + if (batchedComputed) { + let e = batchedComputed; + batchedComputed = void 0; + while (e) { + const next = e.next; + e.next = void 0; + e.flags &= -9; + e = next; + } + } + let error; + while (batchedSub) { + let e = batchedSub; + batchedSub = void 0; + while (e) { + const next = e.next; + e.next = void 0; + e.flags &= -9; + if (e.flags & 1) { + try { + ; + e.trigger(); + } catch (err) { + if (!error) error = err; + } + } + e = next; + } + } + if (error) throw error; + } + function prepareDeps(sub) { + for (let link = sub.deps; link; link = link.nextDep) { + link.version = -1; + link.prevActiveLink = link.dep.activeLink; + link.dep.activeLink = link; + } + } + function cleanupDeps(sub) { + let head; + let tail = sub.depsTail; + let link = tail; + while (link) { + const prev = link.prevDep; + if (link.version === -1) { + if (link === tail) tail = prev; + removeSub(link); + removeDep(link); + } else { + head = link; + } + link.dep.activeLink = link.prevActiveLink; + link.prevActiveLink = void 0; + link = prev; + } + sub.deps = head; + sub.depsTail = tail; + } + function isDirty(sub) { + for (let link = sub.deps; link; link = link.nextDep) { + if (link.dep.version !== link.version || link.dep.computed && (refreshComputed(link.dep.computed) || link.dep.version !== link.version)) { + return true; + } + } + if (sub._dirty) { + return true; + } + return false; + } + function refreshComputed(computed) { + if (computed.flags & 4 && !(computed.flags & 16)) { + return; + } + computed.flags &= -17; + if (computed.globalVersion === globalVersion) { + return; + } + computed.globalVersion = globalVersion; + if (!computed.isSSR && computed.flags & 128 && (!computed.deps && !computed._dirty || !isDirty(computed))) { + return; + } + computed.flags |= 2; + const dep = computed.dep; + const prevSub = activeSub; + const prevShouldTrack = shouldTrack; + activeSub = computed; + shouldTrack = true; + try { + prepareDeps(computed); + const value = computed.fn(computed._value); + if (dep.version === 0 || hasChanged(value, computed._value)) { + computed.flags |= 128; + computed._value = value; + dep.version++; + } + } catch (err) { + dep.version++; + throw err; + } finally { + activeSub = prevSub; + shouldTrack = prevShouldTrack; + cleanupDeps(computed); + computed.flags &= -3; + } + } + function removeSub(link, soft = false) { + const { dep, prevSub, nextSub } = link; + if (prevSub) { + prevSub.nextSub = nextSub; + link.prevSub = void 0; + } + if (nextSub) { + nextSub.prevSub = prevSub; + link.nextSub = void 0; + } + if (dep.subsHead === link) { + dep.subsHead = nextSub; + } + if (dep.subs === link) { + dep.subs = prevSub; + if (!prevSub && dep.computed) { + dep.computed.flags &= -5; + for (let l = dep.computed.deps; l; l = l.nextDep) { + removeSub(l, true); + } + } + } + if (!soft && !--dep.sc && dep.map) { + dep.map.delete(dep.key); + } + } + function removeDep(link) { + const { prevDep, nextDep } = link; + if (prevDep) { + prevDep.nextDep = nextDep; + link.prevDep = void 0; + } + if (nextDep) { + nextDep.prevDep = prevDep; + link.nextDep = void 0; + } + } + function effect(fn, options) { + if (fn.effect instanceof ReactiveEffect) { + fn = fn.effect.fn; + } + const e = new ReactiveEffect(fn); + if (options) { + extend(e, options); + } + try { + e.run(); + } catch (err) { + e.stop(); + throw err; + } + const runner = e.run.bind(e); + runner.effect = e; + return runner; + } + function stop(runner) { + runner.effect.stop(); + } + let shouldTrack = true; + const trackStack = []; + function pauseTracking() { + trackStack.push(shouldTrack); + shouldTrack = false; + } + function resetTracking() { + const last = trackStack.pop(); + shouldTrack = last === void 0 ? true : last; + } + function cleanupEffect(e) { + const { cleanup } = e; + e.cleanup = void 0; + if (cleanup) { + const prevSub = activeSub; + activeSub = void 0; + try { + cleanup(); + } finally { + activeSub = prevSub; + } + } + } + + let globalVersion = 0; + class Link { + constructor(sub, dep) { + this.sub = sub; + this.dep = dep; + this.version = dep.version; + this.nextDep = this.prevDep = this.nextSub = this.prevSub = this.prevActiveLink = void 0; + } + } + class Dep { + // TODO isolatedDeclarations "__v_skip" + constructor(computed) { + this.computed = computed; + this.version = 0; + /** + * Link between this dep and the current active effect + */ + this.activeLink = void 0; + /** + * Doubly linked list representing the subscribing effects (tail) + */ + this.subs = void 0; + /** + * For object property deps cleanup + */ + this.map = void 0; + this.key = void 0; + /** + * Subscriber counter + */ + this.sc = 0; + /** + * @internal + */ + this.__v_skip = true; + { + this.subsHead = void 0; + } + } + track(debugInfo) { + if (!activeSub || !shouldTrack || activeSub === this.computed) { + return; + } + let link = this.activeLink; + if (link === void 0 || link.sub !== activeSub) { + link = this.activeLink = new Link(activeSub, this); + if (!activeSub.deps) { + activeSub.deps = activeSub.depsTail = link; + } else { + link.prevDep = activeSub.depsTail; + activeSub.depsTail.nextDep = link; + activeSub.depsTail = link; + } + addSub(link); + } else if (link.version === -1) { + link.version = this.version; + if (link.nextDep) { + const next = link.nextDep; + next.prevDep = link.prevDep; + if (link.prevDep) { + link.prevDep.nextDep = next; + } + link.prevDep = activeSub.depsTail; + link.nextDep = void 0; + activeSub.depsTail.nextDep = link; + activeSub.depsTail = link; + if (activeSub.deps === link) { + activeSub.deps = next; + } + } + } + if (activeSub.onTrack) { + activeSub.onTrack( + extend( + { + effect: activeSub + }, + debugInfo + ) + ); + } + return link; + } + trigger(debugInfo) { + this.version++; + globalVersion++; + this.notify(debugInfo); + } + notify(debugInfo) { + startBatch(); + try { + if (true) { + for (let head = this.subsHead; head; head = head.nextSub) { + if (head.sub.onTrigger && !(head.sub.flags & 8)) { + head.sub.onTrigger( + extend( + { + effect: head.sub + }, + debugInfo + ) + ); + } + } + } + for (let link = this.subs; link; link = link.prevSub) { + if (link.sub.notify()) { + ; + link.sub.dep.notify(); + } + } + } finally { + endBatch(); + } + } + } + function addSub(link) { + link.dep.sc++; + if (link.sub.flags & 4) { + const computed = link.dep.computed; + if (computed && !link.dep.subs) { + computed.flags |= 4 | 16; + for (let l = computed.deps; l; l = l.nextDep) { + addSub(l); + } + } + const currentTail = link.dep.subs; + if (currentTail !== link) { + link.prevSub = currentTail; + if (currentTail) currentTail.nextSub = link; + } + if (link.dep.subsHead === void 0) { + link.dep.subsHead = link; + } + link.dep.subs = link; + } + } + const targetMap = /* @__PURE__ */ new WeakMap(); + const ITERATE_KEY = Symbol( + "Object iterate" + ); + const MAP_KEY_ITERATE_KEY = Symbol( + "Map keys iterate" + ); + const ARRAY_ITERATE_KEY = Symbol( + "Array iterate" + ); + function track(target, type, key) { + if (shouldTrack && activeSub) { + let depsMap = targetMap.get(target); + if (!depsMap) { + targetMap.set(target, depsMap = /* @__PURE__ */ new Map()); + } + let dep = depsMap.get(key); + if (!dep) { + depsMap.set(key, dep = new Dep()); + dep.map = depsMap; + dep.key = key; + } + { + dep.track({ + target, + type, + key + }); + } + } + } + function trigger(target, type, key, newValue, oldValue, oldTarget) { + const depsMap = targetMap.get(target); + if (!depsMap) { + globalVersion++; + return; + } + const run = (dep) => { + if (dep) { + { + dep.trigger({ + target, + type, + key, + newValue, + oldValue, + oldTarget + }); + } + } + }; + startBatch(); + if (type === "clear") { + depsMap.forEach(run); + } else { + const targetIsArray = isArray(target); + const isArrayIndex = targetIsArray && isIntegerKey(key); + if (targetIsArray && key === "length") { + const newLength = Number(newValue); + depsMap.forEach((dep, key2) => { + if (key2 === "length" || key2 === ARRAY_ITERATE_KEY || !isSymbol(key2) && key2 >= newLength) { + run(dep); + } + }); + } else { + if (key !== void 0 || depsMap.has(void 0)) { + run(depsMap.get(key)); + } + if (isArrayIndex) { + run(depsMap.get(ARRAY_ITERATE_KEY)); + } + switch (type) { + case "add": + if (!targetIsArray) { + run(depsMap.get(ITERATE_KEY)); + if (isMap(target)) { + run(depsMap.get(MAP_KEY_ITERATE_KEY)); + } + } else if (isArrayIndex) { + run(depsMap.get("length")); + } + break; + case "delete": + if (!targetIsArray) { + run(depsMap.get(ITERATE_KEY)); + if (isMap(target)) { + run(depsMap.get(MAP_KEY_ITERATE_KEY)); + } + } + break; + case "set": + if (isMap(target)) { + run(depsMap.get(ITERATE_KEY)); + } + break; + } + } + } + endBatch(); + } + function getDepFromReactive(object, key) { + const depMap = targetMap.get(object); + return depMap && depMap.get(key); + } + + function reactiveReadArray(array) { + const raw = toRaw(array); + if (raw === array) return raw; + track(raw, "iterate", ARRAY_ITERATE_KEY); + return isShallow(array) ? raw : raw.map(toReactive); + } + function shallowReadArray(arr) { + track(arr = toRaw(arr), "iterate", ARRAY_ITERATE_KEY); + return arr; + } + const arrayInstrumentations = { + __proto__: null, + [Symbol.iterator]() { + return iterator(this, Symbol.iterator, toReactive); + }, + concat(...args) { + return reactiveReadArray(this).concat( + ...args.map((x) => isArray(x) ? reactiveReadArray(x) : x) + ); + }, + entries() { + return iterator(this, "entries", (value) => { + value[1] = toReactive(value[1]); + return value; + }); + }, + every(fn, thisArg) { + return apply(this, "every", fn, thisArg, void 0, arguments); + }, + filter(fn, thisArg) { + return apply(this, "filter", fn, thisArg, (v) => v.map(toReactive), arguments); + }, + find(fn, thisArg) { + return apply(this, "find", fn, thisArg, toReactive, arguments); + }, + findIndex(fn, thisArg) { + return apply(this, "findIndex", fn, thisArg, void 0, arguments); + }, + findLast(fn, thisArg) { + return apply(this, "findLast", fn, thisArg, toReactive, arguments); + }, + findLastIndex(fn, thisArg) { + return apply(this, "findLastIndex", fn, thisArg, void 0, arguments); + }, + // flat, flatMap could benefit from ARRAY_ITERATE but are not straight-forward to implement + forEach(fn, thisArg) { + return apply(this, "forEach", fn, thisArg, void 0, arguments); + }, + includes(...args) { + return searchProxy(this, "includes", args); + }, + indexOf(...args) { + return searchProxy(this, "indexOf", args); + }, + join(separator) { + return reactiveReadArray(this).join(separator); + }, + // keys() iterator only reads `length`, no optimization required + lastIndexOf(...args) { + return searchProxy(this, "lastIndexOf", args); + }, + map(fn, thisArg) { + return apply(this, "map", fn, thisArg, void 0, arguments); + }, + pop() { + return noTracking(this, "pop"); + }, + push(...args) { + return noTracking(this, "push", args); + }, + reduce(fn, ...args) { + return reduce(this, "reduce", fn, args); + }, + reduceRight(fn, ...args) { + return reduce(this, "reduceRight", fn, args); + }, + shift() { + return noTracking(this, "shift"); + }, + // slice could use ARRAY_ITERATE but also seems to beg for range tracking + some(fn, thisArg) { + return apply(this, "some", fn, thisArg, void 0, arguments); + }, + splice(...args) { + return noTracking(this, "splice", args); + }, + toReversed() { + return reactiveReadArray(this).toReversed(); + }, + toSorted(comparer) { + return reactiveReadArray(this).toSorted(comparer); + }, + toSpliced(...args) { + return reactiveReadArray(this).toSpliced(...args); + }, + unshift(...args) { + return noTracking(this, "unshift", args); + }, + values() { + return iterator(this, "values", toReactive); + } + }; + function iterator(self, method, wrapValue) { + const arr = shallowReadArray(self); + const iter = arr[method](); + if (arr !== self && !isShallow(self)) { + iter._next = iter.next; + iter.next = () => { + const result = iter._next(); + if (!result.done) { + result.value = wrapValue(result.value); + } + return result; + }; + } + return iter; + } + const arrayProto = Array.prototype; + function apply(self, method, fn, thisArg, wrappedRetFn, args) { + const arr = shallowReadArray(self); + const needsWrap = arr !== self && !isShallow(self); + const methodFn = arr[method]; + if (methodFn !== arrayProto[method]) { + const result2 = methodFn.apply(self, args); + return needsWrap ? toReactive(result2) : result2; + } + let wrappedFn = fn; + if (arr !== self) { + if (needsWrap) { + wrappedFn = function(item, index) { + return fn.call(this, toReactive(item), index, self); + }; + } else if (fn.length > 2) { + wrappedFn = function(item, index) { + return fn.call(this, item, index, self); + }; + } + } + const result = methodFn.call(arr, wrappedFn, thisArg); + return needsWrap && wrappedRetFn ? wrappedRetFn(result) : result; + } + function reduce(self, method, fn, args) { + const arr = shallowReadArray(self); + let wrappedFn = fn; + if (arr !== self) { + if (!isShallow(self)) { + wrappedFn = function(acc, item, index) { + return fn.call(this, acc, toReactive(item), index, self); + }; + } else if (fn.length > 3) { + wrappedFn = function(acc, item, index) { + return fn.call(this, acc, item, index, self); + }; + } + } + return arr[method](wrappedFn, ...args); + } + function searchProxy(self, method, args) { + const arr = toRaw(self); + track(arr, "iterate", ARRAY_ITERATE_KEY); + const res = arr[method](...args); + if ((res === -1 || res === false) && isProxy(args[0])) { + args[0] = toRaw(args[0]); + return arr[method](...args); + } + return res; + } + function noTracking(self, method, args = []) { + pauseTracking(); + startBatch(); + const res = toRaw(self)[method].apply(self, args); + endBatch(); + resetTracking(); + return res; + } + + const isNonTrackableKeys = /* @__PURE__ */ makeMap(`__proto__,__v_isRef,__isVue`); + const builtInSymbols = new Set( + /* @__PURE__ */ Object.getOwnPropertyNames(Symbol).filter((key) => key !== "arguments" && key !== "caller").map((key) => Symbol[key]).filter(isSymbol) + ); + function hasOwnProperty(key) { + if (!isSymbol(key)) key = String(key); + const obj = toRaw(this); + track(obj, "has", key); + return obj.hasOwnProperty(key); + } + class BaseReactiveHandler { + constructor(_isReadonly = false, _isShallow = false) { + this._isReadonly = _isReadonly; + this._isShallow = _isShallow; + } + get(target, key, receiver) { + if (key === "__v_skip") return target["__v_skip"]; + const isReadonly2 = this._isReadonly, isShallow2 = this._isShallow; + if (key === "__v_isReactive") { + return !isReadonly2; + } else if (key === "__v_isReadonly") { + return isReadonly2; + } else if (key === "__v_isShallow") { + return isShallow2; + } else if (key === "__v_raw") { + if (receiver === (isReadonly2 ? isShallow2 ? shallowReadonlyMap : readonlyMap : isShallow2 ? shallowReactiveMap : reactiveMap).get(target) || // receiver is not the reactive proxy, but has the same prototype + // this means the receiver is a user proxy of the reactive proxy + Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)) { + return target; + } + return; + } + const targetIsArray = isArray(target); + if (!isReadonly2) { + let fn; + if (targetIsArray && (fn = arrayInstrumentations[key])) { + return fn; + } + if (key === "hasOwnProperty") { + return hasOwnProperty; + } + } + const res = Reflect.get( + target, + key, + // if this is a proxy wrapping a ref, return methods using the raw ref + // as receiver so that we don't have to call `toRaw` on the ref in all + // its class methods + isRef(target) ? target : receiver + ); + if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { + return res; + } + if (!isReadonly2) { + track(target, "get", key); + } + if (isShallow2) { + return res; + } + if (isRef(res)) { + const value = targetIsArray && isIntegerKey(key) ? res : res.value; + return isReadonly2 && isObject(value) ? readonly(value) : value; + } + if (isObject(res)) { + return isReadonly2 ? readonly(res) : reactive(res); + } + return res; + } + } + class MutableReactiveHandler extends BaseReactiveHandler { + constructor(isShallow2 = false) { + super(false, isShallow2); + } + set(target, key, value, receiver) { + let oldValue = target[key]; + if (!this._isShallow) { + const isOldValueReadonly = isReadonly(oldValue); + if (!isShallow(value) && !isReadonly(value)) { + oldValue = toRaw(oldValue); + value = toRaw(value); + } + if (!isArray(target) && isRef(oldValue) && !isRef(value)) { + if (isOldValueReadonly) { + { + warn$2( + `Set operation on key "${String(key)}" failed: target is readonly.`, + target[key] + ); + } + return true; + } else { + oldValue.value = value; + return true; + } + } + } + const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key); + const result = Reflect.set( + target, + key, + value, + isRef(target) ? target : receiver + ); + if (target === toRaw(receiver)) { + if (!hadKey) { + trigger(target, "add", key, value); + } else if (hasChanged(value, oldValue)) { + trigger(target, "set", key, value, oldValue); + } + } + return result; + } + deleteProperty(target, key) { + const hadKey = hasOwn(target, key); + const oldValue = target[key]; + const result = Reflect.deleteProperty(target, key); + if (result && hadKey) { + trigger(target, "delete", key, void 0, oldValue); + } + return result; + } + has(target, key) { + const result = Reflect.has(target, key); + if (!isSymbol(key) || !builtInSymbols.has(key)) { + track(target, "has", key); + } + return result; + } + ownKeys(target) { + track( + target, + "iterate", + isArray(target) ? "length" : ITERATE_KEY + ); + return Reflect.ownKeys(target); + } + } + class ReadonlyReactiveHandler extends BaseReactiveHandler { + constructor(isShallow2 = false) { + super(true, isShallow2); + } + set(target, key) { + { + warn$2( + `Set operation on key "${String(key)}" failed: target is readonly.`, + target + ); + } + return true; + } + deleteProperty(target, key) { + { + warn$2( + `Delete operation on key "${String(key)}" failed: target is readonly.`, + target + ); + } + return true; + } + } + const mutableHandlers = /* @__PURE__ */ new MutableReactiveHandler(); + const readonlyHandlers = /* @__PURE__ */ new ReadonlyReactiveHandler(); + const shallowReactiveHandlers = /* @__PURE__ */ new MutableReactiveHandler(true); + const shallowReadonlyHandlers = /* @__PURE__ */ new ReadonlyReactiveHandler(true); + + const toShallow = (value) => value; + const getProto = (v) => Reflect.getPrototypeOf(v); + function createIterableMethod(method, isReadonly2, isShallow2) { + return function(...args) { + const target = this["__v_raw"]; + const rawTarget = toRaw(target); + const targetIsMap = isMap(rawTarget); + const isPair = method === "entries" || method === Symbol.iterator && targetIsMap; + const isKeyOnly = method === "keys" && targetIsMap; + const innerIterator = target[method](...args); + const wrap = isShallow2 ? toShallow : isReadonly2 ? toReadonly : toReactive; + !isReadonly2 && track( + rawTarget, + "iterate", + isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY + ); + return { + // iterator protocol + next() { + const { value, done } = innerIterator.next(); + return done ? { value, done } : { + value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value), + done + }; + }, + // iterable protocol + [Symbol.iterator]() { + return this; + } + }; + }; + } + function createReadonlyMethod(type) { + return function(...args) { + { + const key = args[0] ? `on key "${args[0]}" ` : ``; + warn$2( + `${capitalize(type)} operation ${key}failed: target is readonly.`, + toRaw(this) + ); + } + return type === "delete" ? false : type === "clear" ? void 0 : this; + }; + } + function createInstrumentations(readonly, shallow) { + const instrumentations = { + get(key) { + const target = this["__v_raw"]; + const rawTarget = toRaw(target); + const rawKey = toRaw(key); + if (!readonly) { + if (hasChanged(key, rawKey)) { + track(rawTarget, "get", key); + } + track(rawTarget, "get", rawKey); + } + const { has } = getProto(rawTarget); + const wrap = shallow ? toShallow : readonly ? toReadonly : toReactive; + if (has.call(rawTarget, key)) { + return wrap(target.get(key)); + } else if (has.call(rawTarget, rawKey)) { + return wrap(target.get(rawKey)); + } else if (target !== rawTarget) { + target.get(key); + } + }, + get size() { + const target = this["__v_raw"]; + !readonly && track(toRaw(target), "iterate", ITERATE_KEY); + return target.size; + }, + has(key) { + const target = this["__v_raw"]; + const rawTarget = toRaw(target); + const rawKey = toRaw(key); + if (!readonly) { + if (hasChanged(key, rawKey)) { + track(rawTarget, "has", key); + } + track(rawTarget, "has", rawKey); + } + return key === rawKey ? target.has(key) : target.has(key) || target.has(rawKey); + }, + forEach(callback, thisArg) { + const observed = this; + const target = observed["__v_raw"]; + const rawTarget = toRaw(target); + const wrap = shallow ? toShallow : readonly ? toReadonly : toReactive; + !readonly && track(rawTarget, "iterate", ITERATE_KEY); + return target.forEach((value, key) => { + return callback.call(thisArg, wrap(value), wrap(key), observed); + }); + } + }; + extend( + instrumentations, + readonly ? { + add: createReadonlyMethod("add"), + set: createReadonlyMethod("set"), + delete: createReadonlyMethod("delete"), + clear: createReadonlyMethod("clear") + } : { + add(value) { + if (!shallow && !isShallow(value) && !isReadonly(value)) { + value = toRaw(value); + } + const target = toRaw(this); + const proto = getProto(target); + const hadKey = proto.has.call(target, value); + if (!hadKey) { + target.add(value); + trigger(target, "add", value, value); + } + return this; + }, + set(key, value) { + if (!shallow && !isShallow(value) && !isReadonly(value)) { + value = toRaw(value); + } + const target = toRaw(this); + const { has, get } = getProto(target); + let hadKey = has.call(target, key); + if (!hadKey) { + key = toRaw(key); + hadKey = has.call(target, key); + } else { + checkIdentityKeys(target, has, key); + } + const oldValue = get.call(target, key); + target.set(key, value); + if (!hadKey) { + trigger(target, "add", key, value); + } else if (hasChanged(value, oldValue)) { + trigger(target, "set", key, value, oldValue); + } + return this; + }, + delete(key) { + const target = toRaw(this); + const { has, get } = getProto(target); + let hadKey = has.call(target, key); + if (!hadKey) { + key = toRaw(key); + hadKey = has.call(target, key); + } else { + checkIdentityKeys(target, has, key); + } + const oldValue = get ? get.call(target, key) : void 0; + const result = target.delete(key); + if (hadKey) { + trigger(target, "delete", key, void 0, oldValue); + } + return result; + }, + clear() { + const target = toRaw(this); + const hadItems = target.size !== 0; + const oldTarget = isMap(target) ? new Map(target) : new Set(target) ; + const result = target.clear(); + if (hadItems) { + trigger( + target, + "clear", + void 0, + void 0, + oldTarget + ); + } + return result; + } + } + ); + const iteratorMethods = [ + "keys", + "values", + "entries", + Symbol.iterator + ]; + iteratorMethods.forEach((method) => { + instrumentations[method] = createIterableMethod(method, readonly, shallow); + }); + return instrumentations; + } + function createInstrumentationGetter(isReadonly2, shallow) { + const instrumentations = createInstrumentations(isReadonly2, shallow); + return (target, key, receiver) => { + if (key === "__v_isReactive") { + return !isReadonly2; + } else if (key === "__v_isReadonly") { + return isReadonly2; + } else if (key === "__v_raw") { + return target; + } + return Reflect.get( + hasOwn(instrumentations, key) && key in target ? instrumentations : target, + key, + receiver + ); + }; + } + const mutableCollectionHandlers = { + get: /* @__PURE__ */ createInstrumentationGetter(false, false) + }; + const shallowCollectionHandlers = { + get: /* @__PURE__ */ createInstrumentationGetter(false, true) + }; + const readonlyCollectionHandlers = { + get: /* @__PURE__ */ createInstrumentationGetter(true, false) + }; + const shallowReadonlyCollectionHandlers = { + get: /* @__PURE__ */ createInstrumentationGetter(true, true) + }; + function checkIdentityKeys(target, has, key) { + const rawKey = toRaw(key); + if (rawKey !== key && has.call(target, rawKey)) { + const type = toRawType(target); + warn$2( + `Reactive ${type} contains both the raw and reactive versions of the same object${type === `Map` ? ` as keys` : ``}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.` + ); + } + } + + const reactiveMap = /* @__PURE__ */ new WeakMap(); + const shallowReactiveMap = /* @__PURE__ */ new WeakMap(); + const readonlyMap = /* @__PURE__ */ new WeakMap(); + const shallowReadonlyMap = /* @__PURE__ */ new WeakMap(); + function targetTypeMap(rawType) { + switch (rawType) { + case "Object": + case "Array": + return 1 /* COMMON */; + case "Map": + case "Set": + case "WeakMap": + case "WeakSet": + return 2 /* COLLECTION */; + default: + return 0 /* INVALID */; + } + } + function getTargetType(value) { + return value["__v_skip"] || !Object.isExtensible(value) ? 0 /* INVALID */ : targetTypeMap(toRawType(value)); + } + function reactive(target) { + if (isReadonly(target)) { + return target; + } + return createReactiveObject( + target, + false, + mutableHandlers, + mutableCollectionHandlers, + reactiveMap + ); + } + function shallowReactive(target) { + return createReactiveObject( + target, + false, + shallowReactiveHandlers, + shallowCollectionHandlers, + shallowReactiveMap + ); + } + function readonly(target) { + return createReactiveObject( + target, + true, + readonlyHandlers, + readonlyCollectionHandlers, + readonlyMap + ); + } + function shallowReadonly(target) { + return createReactiveObject( + target, + true, + shallowReadonlyHandlers, + shallowReadonlyCollectionHandlers, + shallowReadonlyMap + ); + } + function createReactiveObject(target, isReadonly2, baseHandlers, collectionHandlers, proxyMap) { + if (!isObject(target)) { + { + warn$2( + `value cannot be made ${isReadonly2 ? "readonly" : "reactive"}: ${String( + target + )}` + ); + } + return target; + } + if (target["__v_raw"] && !(isReadonly2 && target["__v_isReactive"])) { + return target; + } + const targetType = getTargetType(target); + if (targetType === 0 /* INVALID */) { + return target; + } + const existingProxy = proxyMap.get(target); + if (existingProxy) { + return existingProxy; + } + const proxy = new Proxy( + target, + targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers + ); + proxyMap.set(target, proxy); + return proxy; + } + function isReactive(value) { + if (isReadonly(value)) { + return isReactive(value["__v_raw"]); + } + return !!(value && value["__v_isReactive"]); + } + function isReadonly(value) { + return !!(value && value["__v_isReadonly"]); + } + function isShallow(value) { + return !!(value && value["__v_isShallow"]); + } + function isProxy(value) { + return value ? !!value["__v_raw"] : false; + } + function toRaw(observed) { + const raw = observed && observed["__v_raw"]; + return raw ? toRaw(raw) : observed; + } + function markRaw(value) { + if (!hasOwn(value, "__v_skip") && Object.isExtensible(value)) { + def(value, "__v_skip", true); + } + return value; + } + const toReactive = (value) => isObject(value) ? reactive(value) : value; + const toReadonly = (value) => isObject(value) ? readonly(value) : value; + + function isRef(r) { + return r ? r["__v_isRef"] === true : false; + } + function ref(value) { + return createRef(value, false); + } + function shallowRef(value) { + return createRef(value, true); + } + function createRef(rawValue, shallow) { + if (isRef(rawValue)) { + return rawValue; + } + return new RefImpl(rawValue, shallow); + } + class RefImpl { + constructor(value, isShallow2) { + this.dep = new Dep(); + this["__v_isRef"] = true; + this["__v_isShallow"] = false; + this._rawValue = isShallow2 ? value : toRaw(value); + this._value = isShallow2 ? value : toReactive(value); + this["__v_isShallow"] = isShallow2; + } + get value() { + { + this.dep.track({ + target: this, + type: "get", + key: "value" + }); + } + return this._value; + } + set value(newValue) { + const oldValue = this._rawValue; + const useDirectValue = this["__v_isShallow"] || isShallow(newValue) || isReadonly(newValue); + newValue = useDirectValue ? newValue : toRaw(newValue); + if (hasChanged(newValue, oldValue)) { + this._rawValue = newValue; + this._value = useDirectValue ? newValue : toReactive(newValue); + { + this.dep.trigger({ + target: this, + type: "set", + key: "value", + newValue, + oldValue + }); + } + } + } + } + function triggerRef(ref2) { + if (ref2.dep) { + { + ref2.dep.trigger({ + target: ref2, + type: "set", + key: "value", + newValue: ref2._value + }); + } + } + } + function unref(ref2) { + return isRef(ref2) ? ref2.value : ref2; + } + function toValue(source) { + return isFunction(source) ? source() : unref(source); + } + const shallowUnwrapHandlers = { + get: (target, key, receiver) => key === "__v_raw" ? target : unref(Reflect.get(target, key, receiver)), + set: (target, key, value, receiver) => { + const oldValue = target[key]; + if (isRef(oldValue) && !isRef(value)) { + oldValue.value = value; + return true; + } else { + return Reflect.set(target, key, value, receiver); + } + } + }; + function proxyRefs(objectWithRefs) { + return isReactive(objectWithRefs) ? objectWithRefs : new Proxy(objectWithRefs, shallowUnwrapHandlers); + } + class CustomRefImpl { + constructor(factory) { + this["__v_isRef"] = true; + this._value = void 0; + const dep = this.dep = new Dep(); + const { get, set } = factory(dep.track.bind(dep), dep.trigger.bind(dep)); + this._get = get; + this._set = set; + } + get value() { + return this._value = this._get(); + } + set value(newVal) { + this._set(newVal); + } + } + function customRef(factory) { + return new CustomRefImpl(factory); + } + function toRefs(object) { + if (!isProxy(object)) { + warn$2(`toRefs() expects a reactive object but received a plain one.`); + } + const ret = isArray(object) ? new Array(object.length) : {}; + for (const key in object) { + ret[key] = propertyToRef(object, key); + } + return ret; + } + class ObjectRefImpl { + constructor(_object, _key, _defaultValue) { + this._object = _object; + this._key = _key; + this._defaultValue = _defaultValue; + this["__v_isRef"] = true; + this._value = void 0; + } + get value() { + const val = this._object[this._key]; + return this._value = val === void 0 ? this._defaultValue : val; + } + set value(newVal) { + this._object[this._key] = newVal; + } + get dep() { + return getDepFromReactive(toRaw(this._object), this._key); + } + } + class GetterRefImpl { + constructor(_getter) { + this._getter = _getter; + this["__v_isRef"] = true; + this["__v_isReadonly"] = true; + this._value = void 0; + } + get value() { + return this._value = this._getter(); + } + } + function toRef(source, key, defaultValue) { + if (isRef(source)) { + return source; + } else if (isFunction(source)) { + return new GetterRefImpl(source); + } else if (isObject(source) && arguments.length > 1) { + return propertyToRef(source, key, defaultValue); + } else { + return ref(source); + } + } + function propertyToRef(source, key, defaultValue) { + const val = source[key]; + return isRef(val) ? val : new ObjectRefImpl(source, key, defaultValue); + } + + class ComputedRefImpl { + constructor(fn, setter, isSSR) { + this.fn = fn; + this.setter = setter; + /** + * @internal + */ + this._value = void 0; + /** + * @internal + */ + this.dep = new Dep(this); + /** + * @internal + */ + this.__v_isRef = true; + // TODO isolatedDeclarations "__v_isReadonly" + // A computed is also a subscriber that tracks other deps + /** + * @internal + */ + this.deps = void 0; + /** + * @internal + */ + this.depsTail = void 0; + /** + * @internal + */ + this.flags = 16; + /** + * @internal + */ + this.globalVersion = globalVersion - 1; + /** + * @internal + */ + this.next = void 0; + // for backwards compat + this.effect = this; + this["__v_isReadonly"] = !setter; + this.isSSR = isSSR; + } + /** + * @internal + */ + notify() { + this.flags |= 16; + if (!(this.flags & 8) && // avoid infinite self recursion + activeSub !== this) { + batch(this, true); + return true; + } + } + get value() { + const link = this.dep.track({ + target: this, + type: "get", + key: "value" + }) ; + refreshComputed(this); + if (link) { + link.version = this.dep.version; + } + return this._value; + } + set value(newValue) { + if (this.setter) { + this.setter(newValue); + } else { + warn$2("Write operation failed: computed value is readonly"); + } + } + } + function computed$1(getterOrOptions, debugOptions, isSSR = false) { + let getter; + let setter; + if (isFunction(getterOrOptions)) { + getter = getterOrOptions; + } else { + getter = getterOrOptions.get; + setter = getterOrOptions.set; + } + const cRef = new ComputedRefImpl(getter, setter, isSSR); + if (debugOptions && !isSSR) { + cRef.onTrack = debugOptions.onTrack; + cRef.onTrigger = debugOptions.onTrigger; + } + return cRef; + } + + const TrackOpTypes = { + "GET": "get", + "HAS": "has", + "ITERATE": "iterate" + }; + const TriggerOpTypes = { + "SET": "set", + "ADD": "add", + "DELETE": "delete", + "CLEAR": "clear" + }; + + const INITIAL_WATCHER_VALUE = {}; + const cleanupMap = /* @__PURE__ */ new WeakMap(); + let activeWatcher = void 0; + function getCurrentWatcher() { + return activeWatcher; + } + function onWatcherCleanup(cleanupFn, failSilently = false, owner = activeWatcher) { + if (owner) { + let cleanups = cleanupMap.get(owner); + if (!cleanups) cleanupMap.set(owner, cleanups = []); + cleanups.push(cleanupFn); + } else if (!failSilently) { + warn$2( + `onWatcherCleanup() was called when there was no active watcher to associate with.` + ); + } + } + function watch$1(source, cb, options = EMPTY_OBJ) { + const { immediate, deep, once, scheduler, augmentJob, call } = options; + const warnInvalidSource = (s) => { + (options.onWarn || warn$2)( + `Invalid watch source: `, + s, + `A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types.` + ); + }; + const reactiveGetter = (source2) => { + if (deep) return source2; + if (isShallow(source2) || deep === false || deep === 0) + return traverse(source2, 1); + return traverse(source2); + }; + let effect; + let getter; + let cleanup; + let boundCleanup; + let forceTrigger = false; + let isMultiSource = false; + if (isRef(source)) { + getter = () => source.value; + forceTrigger = isShallow(source); + } else if (isReactive(source)) { + getter = () => reactiveGetter(source); + forceTrigger = true; + } else if (isArray(source)) { + isMultiSource = true; + forceTrigger = source.some((s) => isReactive(s) || isShallow(s)); + getter = () => source.map((s) => { + if (isRef(s)) { + return s.value; + } else if (isReactive(s)) { + return reactiveGetter(s); + } else if (isFunction(s)) { + return call ? call(s, 2) : s(); + } else { + warnInvalidSource(s); + } + }); + } else if (isFunction(source)) { + if (cb) { + getter = call ? () => call(source, 2) : source; + } else { + getter = () => { + if (cleanup) { + pauseTracking(); + try { + cleanup(); + } finally { + resetTracking(); + } + } + const currentEffect = activeWatcher; + activeWatcher = effect; + try { + return call ? call(source, 3, [boundCleanup]) : source(boundCleanup); + } finally { + activeWatcher = currentEffect; + } + }; + } + } else { + getter = NOOP; + warnInvalidSource(source); + } + if (cb && deep) { + const baseGetter = getter; + const depth = deep === true ? Infinity : deep; + getter = () => traverse(baseGetter(), depth); + } + const scope = getCurrentScope(); + const watchHandle = () => { + effect.stop(); + if (scope && scope.active) { + remove(scope.effects, effect); + } + }; + if (once && cb) { + const _cb = cb; + cb = (...args) => { + _cb(...args); + watchHandle(); + }; + } + let oldValue = isMultiSource ? new Array(source.length).fill(INITIAL_WATCHER_VALUE) : INITIAL_WATCHER_VALUE; + const job = (immediateFirstRun) => { + if (!(effect.flags & 1) || !effect.dirty && !immediateFirstRun) { + return; + } + if (cb) { + const newValue = effect.run(); + if (deep || forceTrigger || (isMultiSource ? newValue.some((v, i) => hasChanged(v, oldValue[i])) : hasChanged(newValue, oldValue))) { + if (cleanup) { + cleanup(); + } + const currentWatcher = activeWatcher; + activeWatcher = effect; + try { + const args = [ + newValue, + // pass undefined as the old value when it's changed for the first time + oldValue === INITIAL_WATCHER_VALUE ? void 0 : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE ? [] : oldValue, + boundCleanup + ]; + oldValue = newValue; + call ? call(cb, 3, args) : ( + // @ts-expect-error + cb(...args) + ); + } finally { + activeWatcher = currentWatcher; + } + } + } else { + effect.run(); + } + }; + if (augmentJob) { + augmentJob(job); + } + effect = new ReactiveEffect(getter); + effect.scheduler = scheduler ? () => scheduler(job, false) : job; + boundCleanup = (fn) => onWatcherCleanup(fn, false, effect); + cleanup = effect.onStop = () => { + const cleanups = cleanupMap.get(effect); + if (cleanups) { + if (call) { + call(cleanups, 4); + } else { + for (const cleanup2 of cleanups) cleanup2(); + } + cleanupMap.delete(effect); + } + }; + { + effect.onTrack = options.onTrack; + effect.onTrigger = options.onTrigger; + } + if (cb) { + if (immediate) { + job(true); + } else { + oldValue = effect.run(); + } + } else if (scheduler) { + scheduler(job.bind(null, true), true); + } else { + effect.run(); + } + watchHandle.pause = effect.pause.bind(effect); + watchHandle.resume = effect.resume.bind(effect); + watchHandle.stop = watchHandle; + return watchHandle; + } + function traverse(value, depth = Infinity, seen) { + if (depth <= 0 || !isObject(value) || value["__v_skip"]) { + return value; + } + seen = seen || /* @__PURE__ */ new Map(); + if ((seen.get(value) || 0) >= depth) { + return value; + } + seen.set(value, depth); + depth--; + if (isRef(value)) { + traverse(value.value, depth, seen); + } else if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + traverse(value[i], depth, seen); + } + } else if (isSet(value) || isMap(value)) { + value.forEach((v) => { + traverse(v, depth, seen); + }); + } else if (isPlainObject(value)) { + for (const key in value) { + traverse(value[key], depth, seen); + } + for (const key of Object.getOwnPropertySymbols(value)) { + if (Object.prototype.propertyIsEnumerable.call(value, key)) { + traverse(value[key], depth, seen); + } + } + } + return value; + } + + const stack$1 = []; + function pushWarningContext(vnode) { + stack$1.push(vnode); + } + function popWarningContext() { + stack$1.pop(); + } + let isWarning = false; + function warn$1(msg, ...args) { + if (isWarning) return; + isWarning = true; + pauseTracking(); + const instance = stack$1.length ? stack$1[stack$1.length - 1].component : null; + const appWarnHandler = instance && instance.appContext.config.warnHandler; + const trace = getComponentTrace(); + if (appWarnHandler) { + callWithErrorHandling( + appWarnHandler, + instance, + 11, + [ + // eslint-disable-next-line no-restricted-syntax + msg + args.map((a) => { + var _a, _b; + return (_b = (_a = a.toString) == null ? void 0 : _a.call(a)) != null ? _b : JSON.stringify(a); + }).join(""), + instance && instance.proxy, + trace.map( + ({ vnode }) => `at <${formatComponentName(instance, vnode.type)}>` + ).join("\n"), + trace + ] + ); + } else { + const warnArgs = [`[Vue warn]: ${msg}`, ...args]; + if (trace.length && // avoid spamming console during tests + true) { + warnArgs.push(` +`, ...formatTrace(trace)); + } + console.warn(...warnArgs); + } + resetTracking(); + isWarning = false; + } + function getComponentTrace() { + let currentVNode = stack$1[stack$1.length - 1]; + if (!currentVNode) { + return []; + } + const normalizedStack = []; + while (currentVNode) { + const last = normalizedStack[0]; + if (last && last.vnode === currentVNode) { + last.recurseCount++; + } else { + normalizedStack.push({ + vnode: currentVNode, + recurseCount: 0 + }); + } + const parentInstance = currentVNode.component && currentVNode.component.parent; + currentVNode = parentInstance && parentInstance.vnode; + } + return normalizedStack; + } + function formatTrace(trace) { + const logs = []; + trace.forEach((entry, i) => { + logs.push(...i === 0 ? [] : [` +`], ...formatTraceEntry(entry)); + }); + return logs; + } + function formatTraceEntry({ vnode, recurseCount }) { + const postfix = recurseCount > 0 ? `... (${recurseCount} recursive calls)` : ``; + const isRoot = vnode.component ? vnode.component.parent == null : false; + const open = ` at <${formatComponentName( + vnode.component, + vnode.type, + isRoot + )}`; + const close = `>` + postfix; + return vnode.props ? [open, ...formatProps(vnode.props), close] : [open + close]; + } + function formatProps(props) { + const res = []; + const keys = Object.keys(props); + keys.slice(0, 3).forEach((key) => { + res.push(...formatProp(key, props[key])); + }); + if (keys.length > 3) { + res.push(` ...`); + } + return res; + } + function formatProp(key, value, raw) { + if (isString(value)) { + value = JSON.stringify(value); + return raw ? value : [`${key}=${value}`]; + } else if (typeof value === "number" || typeof value === "boolean" || value == null) { + return raw ? value : [`${key}=${value}`]; + } else if (isRef(value)) { + value = formatProp(key, toRaw(value.value), true); + return raw ? value : [`${key}=Ref<`, value, `>`]; + } else if (isFunction(value)) { + return [`${key}=fn${value.name ? `<${value.name}>` : ``}`]; + } else { + value = toRaw(value); + return raw ? value : [`${key}=`, value]; + } + } + function assertNumber(val, type) { + if (val === void 0) { + return; + } else if (typeof val !== "number") { + warn$1(`${type} is not a valid number - got ${JSON.stringify(val)}.`); + } else if (isNaN(val)) { + warn$1(`${type} is NaN - the duration expression might be incorrect.`); + } + } + + const ErrorCodes = { + "SETUP_FUNCTION": 0, + "0": "SETUP_FUNCTION", + "RENDER_FUNCTION": 1, + "1": "RENDER_FUNCTION", + "NATIVE_EVENT_HANDLER": 5, + "5": "NATIVE_EVENT_HANDLER", + "COMPONENT_EVENT_HANDLER": 6, + "6": "COMPONENT_EVENT_HANDLER", + "VNODE_HOOK": 7, + "7": "VNODE_HOOK", + "DIRECTIVE_HOOK": 8, + "8": "DIRECTIVE_HOOK", + "TRANSITION_HOOK": 9, + "9": "TRANSITION_HOOK", + "APP_ERROR_HANDLER": 10, + "10": "APP_ERROR_HANDLER", + "APP_WARN_HANDLER": 11, + "11": "APP_WARN_HANDLER", + "FUNCTION_REF": 12, + "12": "FUNCTION_REF", + "ASYNC_COMPONENT_LOADER": 13, + "13": "ASYNC_COMPONENT_LOADER", + "SCHEDULER": 14, + "14": "SCHEDULER", + "COMPONENT_UPDATE": 15, + "15": "COMPONENT_UPDATE", + "APP_UNMOUNT_CLEANUP": 16, + "16": "APP_UNMOUNT_CLEANUP" + }; + const ErrorTypeStrings$1 = { + ["sp"]: "serverPrefetch hook", + ["bc"]: "beforeCreate hook", + ["c"]: "created hook", + ["bm"]: "beforeMount hook", + ["m"]: "mounted hook", + ["bu"]: "beforeUpdate hook", + ["u"]: "updated", + ["bum"]: "beforeUnmount hook", + ["um"]: "unmounted hook", + ["a"]: "activated hook", + ["da"]: "deactivated hook", + ["ec"]: "errorCaptured hook", + ["rtc"]: "renderTracked hook", + ["rtg"]: "renderTriggered hook", + [0]: "setup function", + [1]: "render function", + [2]: "watcher getter", + [3]: "watcher callback", + [4]: "watcher cleanup function", + [5]: "native event handler", + [6]: "component event handler", + [7]: "vnode hook", + [8]: "directive hook", + [9]: "transition hook", + [10]: "app errorHandler", + [11]: "app warnHandler", + [12]: "ref function", + [13]: "async component loader", + [14]: "scheduler flush", + [15]: "component update", + [16]: "app unmount cleanup function" + }; + function callWithErrorHandling(fn, instance, type, args) { + try { + return args ? fn(...args) : fn(); + } catch (err) { + handleError(err, instance, type); + } + } + function callWithAsyncErrorHandling(fn, instance, type, args) { + if (isFunction(fn)) { + const res = callWithErrorHandling(fn, instance, type, args); + if (res && isPromise(res)) { + res.catch((err) => { + handleError(err, instance, type); + }); + } + return res; + } + if (isArray(fn)) { + const values = []; + for (let i = 0; i < fn.length; i++) { + values.push(callWithAsyncErrorHandling(fn[i], instance, type, args)); + } + return values; + } else { + warn$1( + `Invalid value type passed to callWithAsyncErrorHandling(): ${typeof fn}` + ); + } + } + function handleError(err, instance, type, throwInDev = true) { + const contextVNode = instance ? instance.vnode : null; + const { errorHandler, throwUnhandledErrorInProduction } = instance && instance.appContext.config || EMPTY_OBJ; + if (instance) { + let cur = instance.parent; + const exposedInstance = instance.proxy; + const errorInfo = ErrorTypeStrings$1[type] ; + while (cur) { + const errorCapturedHooks = cur.ec; + if (errorCapturedHooks) { + for (let i = 0; i < errorCapturedHooks.length; i++) { + if (errorCapturedHooks[i](err, exposedInstance, errorInfo) === false) { + return; + } + } + } + cur = cur.parent; + } + if (errorHandler) { + pauseTracking(); + callWithErrorHandling(errorHandler, null, 10, [ + err, + exposedInstance, + errorInfo + ]); + resetTracking(); + return; + } + } + logError(err, type, contextVNode, throwInDev, throwUnhandledErrorInProduction); + } + function logError(err, type, contextVNode, throwInDev = true, throwInProd = false) { + { + const info = ErrorTypeStrings$1[type]; + if (contextVNode) { + pushWarningContext(contextVNode); + } + warn$1(`Unhandled error${info ? ` during execution of ${info}` : ``}`); + if (contextVNode) { + popWarningContext(); + } + if (throwInDev) { + throw err; + } else { + console.error(err); + } + } + } + + const queue = []; + let flushIndex = -1; + const pendingPostFlushCbs = []; + let activePostFlushCbs = null; + let postFlushIndex = 0; + const resolvedPromise = /* @__PURE__ */ Promise.resolve(); + let currentFlushPromise = null; + const RECURSION_LIMIT = 100; + function nextTick(fn) { + const p = currentFlushPromise || resolvedPromise; + return fn ? p.then(this ? fn.bind(this) : fn) : p; + } + function findInsertionIndex(id) { + let start = flushIndex + 1; + let end = queue.length; + while (start < end) { + const middle = start + end >>> 1; + const middleJob = queue[middle]; + const middleJobId = getId(middleJob); + if (middleJobId < id || middleJobId === id && middleJob.flags & 2) { + start = middle + 1; + } else { + end = middle; + } + } + return start; + } + function queueJob(job) { + if (!(job.flags & 1)) { + const jobId = getId(job); + const lastJob = queue[queue.length - 1]; + if (!lastJob || // fast path when the job id is larger than the tail + !(job.flags & 2) && jobId >= getId(lastJob)) { + queue.push(job); + } else { + queue.splice(findInsertionIndex(jobId), 0, job); + } + job.flags |= 1; + queueFlush(); + } + } + function queueFlush() { + if (!currentFlushPromise) { + currentFlushPromise = resolvedPromise.then(flushJobs); + } + } + function queuePostFlushCb(cb) { + if (!isArray(cb)) { + if (activePostFlushCbs && cb.id === -1) { + activePostFlushCbs.splice(postFlushIndex + 1, 0, cb); + } else if (!(cb.flags & 1)) { + pendingPostFlushCbs.push(cb); + cb.flags |= 1; + } + } else { + pendingPostFlushCbs.push(...cb); + } + queueFlush(); + } + function flushPreFlushCbs(instance, seen, i = flushIndex + 1) { + { + seen = seen || /* @__PURE__ */ new Map(); + } + for (; i < queue.length; i++) { + const cb = queue[i]; + if (cb && cb.flags & 2) { + if (instance && cb.id !== instance.uid) { + continue; + } + if (checkRecursiveUpdates(seen, cb)) { + continue; + } + queue.splice(i, 1); + i--; + if (cb.flags & 4) { + cb.flags &= -2; + } + cb(); + if (!(cb.flags & 4)) { + cb.flags &= -2; + } + } + } + } + function flushPostFlushCbs(seen) { + if (pendingPostFlushCbs.length) { + const deduped = [...new Set(pendingPostFlushCbs)].sort( + (a, b) => getId(a) - getId(b) + ); + pendingPostFlushCbs.length = 0; + if (activePostFlushCbs) { + activePostFlushCbs.push(...deduped); + return; + } + activePostFlushCbs = deduped; + { + seen = seen || /* @__PURE__ */ new Map(); + } + for (postFlushIndex = 0; postFlushIndex < activePostFlushCbs.length; postFlushIndex++) { + const cb = activePostFlushCbs[postFlushIndex]; + if (checkRecursiveUpdates(seen, cb)) { + continue; + } + if (cb.flags & 4) { + cb.flags &= -2; + } + if (!(cb.flags & 8)) cb(); + cb.flags &= -2; + } + activePostFlushCbs = null; + postFlushIndex = 0; + } + } + const getId = (job) => job.id == null ? job.flags & 2 ? -1 : Infinity : job.id; + function flushJobs(seen) { + { + seen = seen || /* @__PURE__ */ new Map(); + } + const check = (job) => checkRecursiveUpdates(seen, job) ; + try { + for (flushIndex = 0; flushIndex < queue.length; flushIndex++) { + const job = queue[flushIndex]; + if (job && !(job.flags & 8)) { + if (check(job)) { + continue; + } + if (job.flags & 4) { + job.flags &= ~1; + } + callWithErrorHandling( + job, + job.i, + job.i ? 15 : 14 + ); + if (!(job.flags & 4)) { + job.flags &= ~1; + } + } + } + } finally { + for (; flushIndex < queue.length; flushIndex++) { + const job = queue[flushIndex]; + if (job) { + job.flags &= -2; + } + } + flushIndex = -1; + queue.length = 0; + flushPostFlushCbs(seen); + currentFlushPromise = null; + if (queue.length || pendingPostFlushCbs.length) { + flushJobs(seen); + } + } + } + function checkRecursiveUpdates(seen, fn) { + const count = seen.get(fn) || 0; + if (count > RECURSION_LIMIT) { + const instance = fn.i; + const componentName = instance && getComponentName(instance.type); + handleError( + `Maximum recursive updates exceeded${componentName ? ` in component <${componentName}>` : ``}. This means you have a reactive effect that is mutating its own dependencies and thus recursively triggering itself. Possible sources include component template, render function, updated hook or watcher source function.`, + null, + 10 + ); + return true; + } + seen.set(fn, count + 1); + return false; + } + + let isHmrUpdating = false; + const hmrDirtyComponents = /* @__PURE__ */ new Map(); + { + getGlobalThis().__VUE_HMR_RUNTIME__ = { + createRecord: tryWrap(createRecord), + rerender: tryWrap(rerender), + reload: tryWrap(reload) + }; + } + const map = /* @__PURE__ */ new Map(); + function registerHMR(instance) { + const id = instance.type.__hmrId; + let record = map.get(id); + if (!record) { + createRecord(id, instance.type); + record = map.get(id); + } + record.instances.add(instance); + } + function unregisterHMR(instance) { + map.get(instance.type.__hmrId).instances.delete(instance); + } + function createRecord(id, initialDef) { + if (map.has(id)) { + return false; + } + map.set(id, { + initialDef: normalizeClassComponent(initialDef), + instances: /* @__PURE__ */ new Set() + }); + return true; + } + function normalizeClassComponent(component) { + return isClassComponent(component) ? component.__vccOpts : component; + } + function rerender(id, newRender) { + const record = map.get(id); + if (!record) { + return; + } + record.initialDef.render = newRender; + [...record.instances].forEach((instance) => { + if (newRender) { + instance.render = newRender; + normalizeClassComponent(instance.type).render = newRender; + } + instance.renderCache = []; + isHmrUpdating = true; + if (!(instance.job.flags & 8)) { + instance.update(); + } + isHmrUpdating = false; + }); + } + function reload(id, newComp) { + const record = map.get(id); + if (!record) return; + newComp = normalizeClassComponent(newComp); + updateComponentDef(record.initialDef, newComp); + const instances = [...record.instances]; + for (let i = 0; i < instances.length; i++) { + const instance = instances[i]; + const oldComp = normalizeClassComponent(instance.type); + let dirtyInstances = hmrDirtyComponents.get(oldComp); + if (!dirtyInstances) { + if (oldComp !== record.initialDef) { + updateComponentDef(oldComp, newComp); + } + hmrDirtyComponents.set(oldComp, dirtyInstances = /* @__PURE__ */ new Set()); + } + dirtyInstances.add(instance); + instance.appContext.propsCache.delete(instance.type); + instance.appContext.emitsCache.delete(instance.type); + instance.appContext.optionsCache.delete(instance.type); + if (instance.ceReload) { + dirtyInstances.add(instance); + instance.ceReload(newComp.styles); + dirtyInstances.delete(instance); + } else if (instance.parent) { + queueJob(() => { + if (!(instance.job.flags & 8)) { + isHmrUpdating = true; + instance.parent.update(); + isHmrUpdating = false; + dirtyInstances.delete(instance); + } + }); + } else if (instance.appContext.reload) { + instance.appContext.reload(); + } else if (typeof window !== "undefined") { + window.location.reload(); + } else { + console.warn( + "[HMR] Root or manually mounted instance modified. Full reload required." + ); + } + if (instance.root.ce && instance !== instance.root) { + instance.root.ce._removeChildStyle(oldComp); + } + } + queuePostFlushCb(() => { + hmrDirtyComponents.clear(); + }); + } + function updateComponentDef(oldComp, newComp) { + extend(oldComp, newComp); + for (const key in oldComp) { + if (key !== "__file" && !(key in newComp)) { + delete oldComp[key]; + } + } + } + function tryWrap(fn) { + return (id, arg) => { + try { + return fn(id, arg); + } catch (e) { + console.error(e); + console.warn( + `[HMR] Something went wrong during Vue component hot-reload. Full reload required.` + ); + } + }; + } + + let devtools$1; + let buffer = []; + let devtoolsNotInstalled = false; + function emit$1(event, ...args) { + if (devtools$1) { + devtools$1.emit(event, ...args); + } else if (!devtoolsNotInstalled) { + buffer.push({ event, args }); + } + } + function setDevtoolsHook$1(hook, target) { + var _a, _b; + devtools$1 = hook; + if (devtools$1) { + devtools$1.enabled = true; + buffer.forEach(({ event, args }) => devtools$1.emit(event, ...args)); + buffer = []; + } else if ( + // handle late devtools injection - only do this if we are in an actual + // browser environment to avoid the timer handle stalling test runner exit + // (#4815) + typeof window !== "undefined" && // some envs mock window but not fully + window.HTMLElement && // also exclude jsdom + // eslint-disable-next-line no-restricted-syntax + !((_b = (_a = window.navigator) == null ? void 0 : _a.userAgent) == null ? void 0 : _b.includes("jsdom")) + ) { + const replay = target.__VUE_DEVTOOLS_HOOK_REPLAY__ = target.__VUE_DEVTOOLS_HOOK_REPLAY__ || []; + replay.push((newHook) => { + setDevtoolsHook$1(newHook, target); + }); + setTimeout(() => { + if (!devtools$1) { + target.__VUE_DEVTOOLS_HOOK_REPLAY__ = null; + devtoolsNotInstalled = true; + buffer = []; + } + }, 3e3); + } else { + devtoolsNotInstalled = true; + buffer = []; + } + } + function devtoolsInitApp(app, version) { + emit$1("app:init" /* APP_INIT */, app, version, { + Fragment, + Text, + Comment, + Static + }); + } + function devtoolsUnmountApp(app) { + emit$1("app:unmount" /* APP_UNMOUNT */, app); + } + const devtoolsComponentAdded = /* @__PURE__ */ createDevtoolsComponentHook("component:added" /* COMPONENT_ADDED */); + const devtoolsComponentUpdated = /* @__PURE__ */ createDevtoolsComponentHook("component:updated" /* COMPONENT_UPDATED */); + const _devtoolsComponentRemoved = /* @__PURE__ */ createDevtoolsComponentHook( + "component:removed" /* COMPONENT_REMOVED */ + ); + const devtoolsComponentRemoved = (component) => { + if (devtools$1 && typeof devtools$1.cleanupBuffer === "function" && // remove the component if it wasn't buffered + !devtools$1.cleanupBuffer(component)) { + _devtoolsComponentRemoved(component); + } + }; + // @__NO_SIDE_EFFECTS__ + function createDevtoolsComponentHook(hook) { + return (component) => { + emit$1( + hook, + component.appContext.app, + component.uid, + component.parent ? component.parent.uid : void 0, + component + ); + }; + } + const devtoolsPerfStart = /* @__PURE__ */ createDevtoolsPerformanceHook("perf:start" /* PERFORMANCE_START */); + const devtoolsPerfEnd = /* @__PURE__ */ createDevtoolsPerformanceHook("perf:end" /* PERFORMANCE_END */); + function createDevtoolsPerformanceHook(hook) { + return (component, type, time) => { + emit$1(hook, component.appContext.app, component.uid, component, type, time); + }; + } + function devtoolsComponentEmit(component, event, params) { + emit$1( + "component:emit" /* COMPONENT_EMIT */, + component.appContext.app, + component, + event, + params + ); + } + + let currentRenderingInstance = null; + let currentScopeId = null; + function setCurrentRenderingInstance(instance) { + const prev = currentRenderingInstance; + currentRenderingInstance = instance; + currentScopeId = instance && instance.type.__scopeId || null; + return prev; + } + function pushScopeId(id) { + currentScopeId = id; + } + function popScopeId() { + currentScopeId = null; + } + const withScopeId = (_id) => withCtx; + function withCtx(fn, ctx = currentRenderingInstance, isNonScopedSlot) { + if (!ctx) return fn; + if (fn._n) { + return fn; + } + const renderFnWithContext = (...args) => { + if (renderFnWithContext._d) { + setBlockTracking(-1); + } + const prevInstance = setCurrentRenderingInstance(ctx); + let res; + try { + res = fn(...args); + } finally { + setCurrentRenderingInstance(prevInstance); + if (renderFnWithContext._d) { + setBlockTracking(1); + } + } + { + devtoolsComponentUpdated(ctx); + } + return res; + }; + renderFnWithContext._n = true; + renderFnWithContext._c = true; + renderFnWithContext._d = true; + return renderFnWithContext; + } + + function validateDirectiveName(name) { + if (isBuiltInDirective(name)) { + warn$1("Do not use built-in directive ids as custom directive id: " + name); + } + } + function withDirectives(vnode, directives) { + if (currentRenderingInstance === null) { + warn$1(`withDirectives can only be used inside render functions.`); + return vnode; + } + const instance = getComponentPublicInstance(currentRenderingInstance); + const bindings = vnode.dirs || (vnode.dirs = []); + for (let i = 0; i < directives.length; i++) { + let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]; + if (dir) { + if (isFunction(dir)) { + dir = { + mounted: dir, + updated: dir + }; + } + if (dir.deep) { + traverse(value); + } + bindings.push({ + dir, + instance, + value, + oldValue: void 0, + arg, + modifiers + }); + } + } + return vnode; + } + function invokeDirectiveHook(vnode, prevVNode, instance, name) { + const bindings = vnode.dirs; + const oldBindings = prevVNode && prevVNode.dirs; + for (let i = 0; i < bindings.length; i++) { + const binding = bindings[i]; + if (oldBindings) { + binding.oldValue = oldBindings[i].value; + } + let hook = binding.dir[name]; + if (hook) { + pauseTracking(); + callWithAsyncErrorHandling(hook, instance, 8, [ + vnode.el, + binding, + vnode, + prevVNode + ]); + resetTracking(); + } + } + } + + const TeleportEndKey = Symbol("_vte"); + const isTeleport = (type) => type.__isTeleport; + const isTeleportDisabled = (props) => props && (props.disabled || props.disabled === ""); + const isTeleportDeferred = (props) => props && (props.defer || props.defer === ""); + const isTargetSVG = (target) => typeof SVGElement !== "undefined" && target instanceof SVGElement; + const isTargetMathML = (target) => typeof MathMLElement === "function" && target instanceof MathMLElement; + const resolveTarget = (props, select) => { + const targetSelector = props && props.to; + if (isString(targetSelector)) { + if (!select) { + warn$1( + `Current renderer does not support string target for Teleports. (missing querySelector renderer option)` + ); + return null; + } else { + const target = select(targetSelector); + if (!target && !isTeleportDisabled(props)) { + warn$1( + `Failed to locate Teleport target with selector "${targetSelector}". Note the target element must exist before the component is mounted - i.e. the target cannot be rendered by the component itself, and ideally should be outside of the entire Vue component tree.` + ); + } + return target; + } + } else { + if (!targetSelector && !isTeleportDisabled(props)) { + warn$1(`Invalid Teleport target: ${targetSelector}`); + } + return targetSelector; + } + }; + const TeleportImpl = { + name: "Teleport", + __isTeleport: true, + process(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized, internals) { + const { + mc: mountChildren, + pc: patchChildren, + pbc: patchBlockChildren, + o: { insert, querySelector, createText, createComment } + } = internals; + const disabled = isTeleportDisabled(n2.props); + let { shapeFlag, children, dynamicChildren } = n2; + if (isHmrUpdating) { + optimized = false; + dynamicChildren = null; + } + if (n1 == null) { + const placeholder = n2.el = createComment("teleport start") ; + const mainAnchor = n2.anchor = createComment("teleport end") ; + insert(placeholder, container, anchor); + insert(mainAnchor, container, anchor); + const mount = (container2, anchor2) => { + if (shapeFlag & 16) { + mountChildren( + children, + container2, + anchor2, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + optimized + ); + } + }; + const mountToTarget = () => { + const target = n2.target = resolveTarget(n2.props, querySelector); + const targetAnchor = prepareAnchor(target, n2, createText, insert); + if (target) { + if (namespace !== "svg" && isTargetSVG(target)) { + namespace = "svg"; + } else if (namespace !== "mathml" && isTargetMathML(target)) { + namespace = "mathml"; + } + if (parentComponent && parentComponent.isCE) { + (parentComponent.ce._teleportTargets || (parentComponent.ce._teleportTargets = /* @__PURE__ */ new Set())).add(target); + } + if (!disabled) { + mount(target, targetAnchor); + updateCssVars(n2, false); + } + } else if (!disabled) { + warn$1( + "Invalid Teleport target on mount:", + target, + `(${typeof target})` + ); + } + }; + if (disabled) { + mount(container, mainAnchor); + updateCssVars(n2, true); + } + if (isTeleportDeferred(n2.props)) { + n2.el.__isMounted = false; + queuePostRenderEffect(() => { + mountToTarget(); + delete n2.el.__isMounted; + }, parentSuspense); + } else { + mountToTarget(); + } + } else { + if (isTeleportDeferred(n2.props) && n1.el.__isMounted === false) { + queuePostRenderEffect(() => { + TeleportImpl.process( + n1, + n2, + container, + anchor, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + optimized, + internals + ); + }, parentSuspense); + return; + } + n2.el = n1.el; + n2.targetStart = n1.targetStart; + const mainAnchor = n2.anchor = n1.anchor; + const target = n2.target = n1.target; + const targetAnchor = n2.targetAnchor = n1.targetAnchor; + const wasDisabled = isTeleportDisabled(n1.props); + const currentContainer = wasDisabled ? container : target; + const currentAnchor = wasDisabled ? mainAnchor : targetAnchor; + if (namespace === "svg" || isTargetSVG(target)) { + namespace = "svg"; + } else if (namespace === "mathml" || isTargetMathML(target)) { + namespace = "mathml"; + } + if (dynamicChildren) { + patchBlockChildren( + n1.dynamicChildren, + dynamicChildren, + currentContainer, + parentComponent, + parentSuspense, + namespace, + slotScopeIds + ); + traverseStaticChildren(n1, n2, false); + } else if (!optimized) { + patchChildren( + n1, + n2, + currentContainer, + currentAnchor, + parentComponent, + parentSuspense, + namespace, + slotScopeIds, + false + ); + } + if (disabled) { + if (!wasDisabled) { + moveTeleport( + n2, + container, + mainAnchor, + internals, + 1 + ); + } else { + if (n2.props && n1.props && n2.props.to !== n1.props.to) { + n2.props.to = n1.props.to; + } + } + } else { + if ((n2.props && n2.props.to) !== (n1.props && n1.props.to)) { + const nextTarget = n2.target = resolveTarget( + n2.props, + querySelector + ); + if (nextTarget) { + moveTeleport( + n2, + nextTarget, + null, + internals, + 0 + ); + } else { + warn$1( + "Invalid Teleport target on update:", + target, + `(${typeof target})` + ); + } + } else if (wasDisabled) { + moveTeleport( + n2, + target, + targetAnchor, + internals, + 1 + ); + } + } + updateCssVars(n2, disabled); + } + }, + remove(vnode, parentComponent, parentSuspense, { um: unmount, o: { remove: hostRemove } }, doRemove) { + const { + shapeFlag, + children, + anchor, + targetStart, + targetAnchor, + target, + props + } = vnode; + if (target) { + hostRemove(targetStart); + hostRemove(targetAnchor); + } + doRemove && hostRemove(anchor); + if (shapeFlag & 16) { + const shouldRemove = doRemove || !isTeleportDisabled(props); + for (let i = 0; i < children.length; i++) { + const child = children[i]; + unmount( + child, + parentComponent, + parentSuspense, + shouldRemove, + !!child.dynamicChildren + ); + } + } + }, + move: moveTeleport, + hydrate: hydrateTeleport + }; + function moveTeleport(vnode, container, parentAnchor, { o: { insert }, m: move }, moveType = 2) { + if (moveType === 0) { + insert(vnode.targetAnchor, container, parentAnchor); + } + const { el, anchor, shapeFlag, children, props } = vnode; + const isReorder = moveType === 2; + if (isReorder) { + insert(el, container, parentAnchor); + } + if (!isReorder || isTeleportDisabled(props)) { + if (shapeFlag & 16) { + for (let i = 0; i < children.length; i++) { + move( + children[i], + container, + parentAnchor, + 2 + ); + } + } + } + if (isReorder) { + insert(anchor, container, parentAnchor); + } + } + function hydrateTeleport(node, vnode, parentComponent, parentSuspense, slotScopeIds, optimized, { + o: { nextSibling, parentNode, querySelector, insert, createText } + }, hydrateChildren) { + function hydrateDisabledTeleport(node2, vnode2, targetStart, targetAnchor) { + vnode2.anchor = hydrateChildren( + nextSibling(node2), + vnode2, + parentNode(node2), + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + vnode2.targetStart = targetStart; + vnode2.targetAnchor = targetAnchor; + } + const target = vnode.target = resolveTarget( + vnode.props, + querySelector + ); + const disabled = isTeleportDisabled(vnode.props); + if (target) { + const targetNode = target._lpa || target.firstChild; + if (vnode.shapeFlag & 16) { + if (disabled) { + hydrateDisabledTeleport( + node, + vnode, + targetNode, + targetNode && nextSibling(targetNode) + ); + } else { + vnode.anchor = nextSibling(node); + let targetAnchor = targetNode; + while (targetAnchor) { + if (targetAnchor && targetAnchor.nodeType === 8) { + if (targetAnchor.data === "teleport start anchor") { + vnode.targetStart = targetAnchor; + } else if (targetAnchor.data === "teleport anchor") { + vnode.targetAnchor = targetAnchor; + target._lpa = vnode.targetAnchor && nextSibling(vnode.targetAnchor); + break; + } + } + targetAnchor = nextSibling(targetAnchor); + } + if (!vnode.targetAnchor) { + prepareAnchor(target, vnode, createText, insert); + } + hydrateChildren( + targetNode && nextSibling(targetNode), + vnode, + target, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } + } + updateCssVars(vnode, disabled); + } else if (disabled) { + if (vnode.shapeFlag & 16) { + hydrateDisabledTeleport(node, vnode, node, nextSibling(node)); + } + } + return vnode.anchor && nextSibling(vnode.anchor); + } + const Teleport = TeleportImpl; + function updateCssVars(vnode, isDisabled) { + const ctx = vnode.ctx; + if (ctx && ctx.ut) { + let node, anchor; + if (isDisabled) { + node = vnode.el; + anchor = vnode.anchor; + } else { + node = vnode.targetStart; + anchor = vnode.targetAnchor; + } + while (node && node !== anchor) { + if (node.nodeType === 1) node.setAttribute("data-v-owner", ctx.uid); + node = node.nextSibling; + } + ctx.ut(); + } + } + function prepareAnchor(target, vnode, createText, insert) { + const targetStart = vnode.targetStart = createText(""); + const targetAnchor = vnode.targetAnchor = createText(""); + targetStart[TeleportEndKey] = targetAnchor; + if (target) { + insert(targetStart, target); + insert(targetAnchor, target); + } + return targetAnchor; + } + + const leaveCbKey = Symbol("_leaveCb"); + const enterCbKey$1 = Symbol("_enterCb"); + function useTransitionState() { + const state = { + isMounted: false, + isLeaving: false, + isUnmounting: false, + leavingVNodes: /* @__PURE__ */ new Map() + }; + onMounted(() => { + state.isMounted = true; + }); + onBeforeUnmount(() => { + state.isUnmounting = true; + }); + return state; + } + const TransitionHookValidator = [Function, Array]; + const BaseTransitionPropsValidators = { + mode: String, + appear: Boolean, + persisted: Boolean, + // enter + onBeforeEnter: TransitionHookValidator, + onEnter: TransitionHookValidator, + onAfterEnter: TransitionHookValidator, + onEnterCancelled: TransitionHookValidator, + // leave + onBeforeLeave: TransitionHookValidator, + onLeave: TransitionHookValidator, + onAfterLeave: TransitionHookValidator, + onLeaveCancelled: TransitionHookValidator, + // appear + onBeforeAppear: TransitionHookValidator, + onAppear: TransitionHookValidator, + onAfterAppear: TransitionHookValidator, + onAppearCancelled: TransitionHookValidator + }; + const recursiveGetSubtree = (instance) => { + const subTree = instance.subTree; + return subTree.component ? recursiveGetSubtree(subTree.component) : subTree; + }; + const BaseTransitionImpl = { + name: `BaseTransition`, + props: BaseTransitionPropsValidators, + setup(props, { slots }) { + const instance = getCurrentInstance(); + const state = useTransitionState(); + return () => { + const children = slots.default && getTransitionRawChildren(slots.default(), true); + if (!children || !children.length) { + return; + } + const child = findNonCommentChild(children); + const rawProps = toRaw(props); + const { mode } = rawProps; + if (mode && mode !== "in-out" && mode !== "out-in" && mode !== "default") { + warn$1(`invalid mode: ${mode}`); + } + if (state.isLeaving) { + return emptyPlaceholder(child); + } + const innerChild = getInnerChild$1(child); + if (!innerChild) { + return emptyPlaceholder(child); + } + let enterHooks = resolveTransitionHooks( + innerChild, + rawProps, + state, + instance, + // #11061, ensure enterHooks is fresh after clone + (hooks) => enterHooks = hooks + ); + if (innerChild.type !== Comment) { + setTransitionHooks(innerChild, enterHooks); + } + let oldInnerChild = instance.subTree && getInnerChild$1(instance.subTree); + if (oldInnerChild && oldInnerChild.type !== Comment && !isSameVNodeType(oldInnerChild, innerChild) && recursiveGetSubtree(instance).type !== Comment) { + let leavingHooks = resolveTransitionHooks( + oldInnerChild, + rawProps, + state, + instance + ); + setTransitionHooks(oldInnerChild, leavingHooks); + if (mode === "out-in" && innerChild.type !== Comment) { + state.isLeaving = true; + leavingHooks.afterLeave = () => { + state.isLeaving = false; + if (!(instance.job.flags & 8)) { + instance.update(); + } + delete leavingHooks.afterLeave; + oldInnerChild = void 0; + }; + return emptyPlaceholder(child); + } else if (mode === "in-out" && innerChild.type !== Comment) { + leavingHooks.delayLeave = (el, earlyRemove, delayedLeave) => { + const leavingVNodesCache = getLeavingNodesForType( + state, + oldInnerChild + ); + leavingVNodesCache[String(oldInnerChild.key)] = oldInnerChild; + el[leaveCbKey] = () => { + earlyRemove(); + el[leaveCbKey] = void 0; + delete enterHooks.delayedLeave; + oldInnerChild = void 0; + }; + enterHooks.delayedLeave = () => { + delayedLeave(); + delete enterHooks.delayedLeave; + oldInnerChild = void 0; + }; + }; + } else { + oldInnerChild = void 0; + } + } else if (oldInnerChild) { + oldInnerChild = void 0; + } + return child; + }; + } + }; + function findNonCommentChild(children) { + let child = children[0]; + if (children.length > 1) { + let hasFound = false; + for (const c of children) { + if (c.type !== Comment) { + if (hasFound) { + warn$1( + " can only be used on a single element or component. Use for lists." + ); + break; + } + child = c; + hasFound = true; + } + } + } + return child; + } + const BaseTransition = BaseTransitionImpl; + function getLeavingNodesForType(state, vnode) { + const { leavingVNodes } = state; + let leavingVNodesCache = leavingVNodes.get(vnode.type); + if (!leavingVNodesCache) { + leavingVNodesCache = /* @__PURE__ */ Object.create(null); + leavingVNodes.set(vnode.type, leavingVNodesCache); + } + return leavingVNodesCache; + } + function resolveTransitionHooks(vnode, props, state, instance, postClone) { + const { + appear, + mode, + persisted = false, + onBeforeEnter, + onEnter, + onAfterEnter, + onEnterCancelled, + onBeforeLeave, + onLeave, + onAfterLeave, + onLeaveCancelled, + onBeforeAppear, + onAppear, + onAfterAppear, + onAppearCancelled + } = props; + const key = String(vnode.key); + const leavingVNodesCache = getLeavingNodesForType(state, vnode); + const callHook = (hook, args) => { + hook && callWithAsyncErrorHandling( + hook, + instance, + 9, + args + ); + }; + const callAsyncHook = (hook, args) => { + const done = args[1]; + callHook(hook, args); + if (isArray(hook)) { + if (hook.every((hook2) => hook2.length <= 1)) done(); + } else if (hook.length <= 1) { + done(); + } + }; + const hooks = { + mode, + persisted, + beforeEnter(el) { + let hook = onBeforeEnter; + if (!state.isMounted) { + if (appear) { + hook = onBeforeAppear || onBeforeEnter; + } else { + return; + } + } + if (el[leaveCbKey]) { + el[leaveCbKey]( + true + /* cancelled */ + ); + } + const leavingVNode = leavingVNodesCache[key]; + if (leavingVNode && isSameVNodeType(vnode, leavingVNode) && leavingVNode.el[leaveCbKey]) { + leavingVNode.el[leaveCbKey](); + } + callHook(hook, [el]); + }, + enter(el) { + let hook = onEnter; + let afterHook = onAfterEnter; + let cancelHook = onEnterCancelled; + if (!state.isMounted) { + if (appear) { + hook = onAppear || onEnter; + afterHook = onAfterAppear || onAfterEnter; + cancelHook = onAppearCancelled || onEnterCancelled; + } else { + return; + } + } + let called = false; + const done = el[enterCbKey$1] = (cancelled) => { + if (called) return; + called = true; + if (cancelled) { + callHook(cancelHook, [el]); + } else { + callHook(afterHook, [el]); + } + if (hooks.delayedLeave) { + hooks.delayedLeave(); + } + el[enterCbKey$1] = void 0; + }; + if (hook) { + callAsyncHook(hook, [el, done]); + } else { + done(); + } + }, + leave(el, remove) { + const key2 = String(vnode.key); + if (el[enterCbKey$1]) { + el[enterCbKey$1]( + true + /* cancelled */ + ); + } + if (state.isUnmounting) { + return remove(); + } + callHook(onBeforeLeave, [el]); + let called = false; + const done = el[leaveCbKey] = (cancelled) => { + if (called) return; + called = true; + remove(); + if (cancelled) { + callHook(onLeaveCancelled, [el]); + } else { + callHook(onAfterLeave, [el]); + } + el[leaveCbKey] = void 0; + if (leavingVNodesCache[key2] === vnode) { + delete leavingVNodesCache[key2]; + } + }; + leavingVNodesCache[key2] = vnode; + if (onLeave) { + callAsyncHook(onLeave, [el, done]); + } else { + done(); + } + }, + clone(vnode2) { + const hooks2 = resolveTransitionHooks( + vnode2, + props, + state, + instance, + postClone + ); + if (postClone) postClone(hooks2); + return hooks2; + } + }; + return hooks; + } + function emptyPlaceholder(vnode) { + if (isKeepAlive(vnode)) { + vnode = cloneVNode(vnode); + vnode.children = null; + return vnode; + } + } + function getInnerChild$1(vnode) { + if (!isKeepAlive(vnode)) { + if (isTeleport(vnode.type) && vnode.children) { + return findNonCommentChild(vnode.children); + } + return vnode; + } + if (vnode.component) { + return vnode.component.subTree; + } + const { shapeFlag, children } = vnode; + if (children) { + if (shapeFlag & 16) { + return children[0]; + } + if (shapeFlag & 32 && isFunction(children.default)) { + return children.default(); + } + } + } + function setTransitionHooks(vnode, hooks) { + if (vnode.shapeFlag & 6 && vnode.component) { + vnode.transition = hooks; + setTransitionHooks(vnode.component.subTree, hooks); + } else if (vnode.shapeFlag & 128) { + vnode.ssContent.transition = hooks.clone(vnode.ssContent); + vnode.ssFallback.transition = hooks.clone(vnode.ssFallback); + } else { + vnode.transition = hooks; + } + } + function getTransitionRawChildren(children, keepComment = false, parentKey) { + let ret = []; + let keyedFragmentCount = 0; + for (let i = 0; i < children.length; i++) { + let child = children[i]; + const key = parentKey == null ? child.key : String(parentKey) + String(child.key != null ? child.key : i); + if (child.type === Fragment) { + if (child.patchFlag & 128) keyedFragmentCount++; + ret = ret.concat( + getTransitionRawChildren(child.children, keepComment, key) + ); + } else if (keepComment || child.type !== Comment) { + ret.push(key != null ? cloneVNode(child, { key }) : child); + } + } + if (keyedFragmentCount > 1) { + for (let i = 0; i < ret.length; i++) { + ret[i].patchFlag = -2; + } + } + return ret; + } + + // @__NO_SIDE_EFFECTS__ + function defineComponent(options, extraOptions) { + return isFunction(options) ? ( + // #8236: extend call and options.name access are considered side-effects + // by Rollup, so we have to wrap it in a pure-annotated IIFE. + /* @__PURE__ */ (() => extend({ name: options.name }, extraOptions, { setup: options }))() + ) : options; + } + + function useId() { + const i = getCurrentInstance(); + if (i) { + return (i.appContext.config.idPrefix || "v") + "-" + i.ids[0] + i.ids[1]++; + } else { + warn$1( + `useId() is called when there is no active component instance to be associated with.` + ); + } + return ""; + } + function markAsyncBoundary(instance) { + instance.ids = [instance.ids[0] + instance.ids[2]++ + "-", 0, 0]; + } + + const knownTemplateRefs = /* @__PURE__ */ new WeakSet(); + function useTemplateRef(key) { + const i = getCurrentInstance(); + const r = shallowRef(null); + if (i) { + const refs = i.refs === EMPTY_OBJ ? i.refs = {} : i.refs; + let desc; + if ((desc = Object.getOwnPropertyDescriptor(refs, key)) && !desc.configurable) { + warn$1(`useTemplateRef('${key}') already exists.`); + } else { + Object.defineProperty(refs, key, { + enumerable: true, + get: () => r.value, + set: (val) => r.value = val + }); + } + } else { + warn$1( + `useTemplateRef() is called when there is no active component instance to be associated with.` + ); + } + const ret = readonly(r) ; + { + knownTemplateRefs.add(ret); + } + return ret; + } + + const pendingSetRefMap = /* @__PURE__ */ new WeakMap(); + function setRef(rawRef, oldRawRef, parentSuspense, vnode, isUnmount = false) { + if (isArray(rawRef)) { + rawRef.forEach( + (r, i) => setRef( + r, + oldRawRef && (isArray(oldRawRef) ? oldRawRef[i] : oldRawRef), + parentSuspense, + vnode, + isUnmount + ) + ); + return; + } + if (isAsyncWrapper(vnode) && !isUnmount) { + if (vnode.shapeFlag & 512 && vnode.type.__asyncResolved && vnode.component.subTree.component) { + setRef(rawRef, oldRawRef, parentSuspense, vnode.component.subTree); + } + return; + } + const refValue = vnode.shapeFlag & 4 ? getComponentPublicInstance(vnode.component) : vnode.el; + const value = isUnmount ? null : refValue; + const { i: owner, r: ref } = rawRef; + if (!owner) { + warn$1( + `Missing ref owner context. ref cannot be used on hoisted vnodes. A vnode with ref must be created inside the render function.` + ); + return; + } + const oldRef = oldRawRef && oldRawRef.r; + const refs = owner.refs === EMPTY_OBJ ? owner.refs = {} : owner.refs; + const setupState = owner.setupState; + const rawSetupState = toRaw(setupState); + const canSetSetupRef = setupState === EMPTY_OBJ ? NO : (key) => { + { + if (hasOwn(rawSetupState, key) && !isRef(rawSetupState[key])) { + warn$1( + `Template ref "${key}" used on a non-ref value. It will not work in the production build.` + ); + } + if (knownTemplateRefs.has(rawSetupState[key])) { + return false; + } + } + return hasOwn(rawSetupState, key); + }; + const canSetRef = (ref2) => { + return !knownTemplateRefs.has(ref2); + }; + if (oldRef != null && oldRef !== ref) { + invalidatePendingSetRef(oldRawRef); + if (isString(oldRef)) { + refs[oldRef] = null; + if (canSetSetupRef(oldRef)) { + setupState[oldRef] = null; + } + } else if (isRef(oldRef)) { + if (canSetRef(oldRef)) { + oldRef.value = null; + } + const oldRawRefAtom = oldRawRef; + if (oldRawRefAtom.k) refs[oldRawRefAtom.k] = null; + } + } + if (isFunction(ref)) { + callWithErrorHandling(ref, owner, 12, [value, refs]); + } else { + const _isString = isString(ref); + const _isRef = isRef(ref); + if (_isString || _isRef) { + const doSet = () => { + if (rawRef.f) { + const existing = _isString ? canSetSetupRef(ref) ? setupState[ref] : refs[ref] : canSetRef(ref) || !rawRef.k ? ref.value : refs[rawRef.k]; + if (isUnmount) { + isArray(existing) && remove(existing, refValue); + } else { + if (!isArray(existing)) { + if (_isString) { + refs[ref] = [refValue]; + if (canSetSetupRef(ref)) { + setupState[ref] = refs[ref]; + } + } else { + const newVal = [refValue]; + if (canSetRef(ref)) { + ref.value = newVal; + } + if (rawRef.k) refs[rawRef.k] = newVal; + } + } else if (!existing.includes(refValue)) { + existing.push(refValue); + } + } + } else if (_isString) { + refs[ref] = value; + if (canSetSetupRef(ref)) { + setupState[ref] = value; + } + } else if (_isRef) { + if (canSetRef(ref)) { + ref.value = value; + } + if (rawRef.k) refs[rawRef.k] = value; + } else { + warn$1("Invalid template ref type:", ref, `(${typeof ref})`); + } + }; + if (value) { + const job = () => { + doSet(); + pendingSetRefMap.delete(rawRef); + }; + job.id = -1; + pendingSetRefMap.set(rawRef, job); + queuePostRenderEffect(job, parentSuspense); + } else { + invalidatePendingSetRef(rawRef); + doSet(); + } + } else { + warn$1("Invalid template ref type:", ref, `(${typeof ref})`); + } + } + } + function invalidatePendingSetRef(rawRef) { + const pendingSetRef = pendingSetRefMap.get(rawRef); + if (pendingSetRef) { + pendingSetRef.flags |= 8; + pendingSetRefMap.delete(rawRef); + } + } + + let hasLoggedMismatchError = false; + const logMismatchError = () => { + if (hasLoggedMismatchError) { + return; + } + console.error("Hydration completed but contains mismatches."); + hasLoggedMismatchError = true; + }; + const isSVGContainer = (container) => container.namespaceURI.includes("svg") && container.tagName !== "foreignObject"; + const isMathMLContainer = (container) => container.namespaceURI.includes("MathML"); + const getContainerType = (container) => { + if (container.nodeType !== 1) return void 0; + if (isSVGContainer(container)) return "svg"; + if (isMathMLContainer(container)) return "mathml"; + return void 0; + }; + const isComment = (node) => node.nodeType === 8; + function createHydrationFunctions(rendererInternals) { + const { + mt: mountComponent, + p: patch, + o: { + patchProp, + createText, + nextSibling, + parentNode, + remove, + insert, + createComment + } + } = rendererInternals; + const hydrate = (vnode, container) => { + if (!container.hasChildNodes()) { + warn$1( + `Attempting to hydrate existing markup but container is empty. Performing full mount instead.` + ); + patch(null, vnode, container); + flushPostFlushCbs(); + container._vnode = vnode; + return; + } + hydrateNode(container.firstChild, vnode, null, null, null); + flushPostFlushCbs(); + container._vnode = vnode; + }; + const hydrateNode = (node, vnode, parentComponent, parentSuspense, slotScopeIds, optimized = false) => { + optimized = optimized || !!vnode.dynamicChildren; + const isFragmentStart = isComment(node) && node.data === "["; + const onMismatch = () => handleMismatch( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + isFragmentStart + ); + const { type, ref, shapeFlag, patchFlag } = vnode; + let domType = node.nodeType; + vnode.el = node; + { + def(node, "__vnode", vnode, true); + def(node, "__vueParentComponent", parentComponent, true); + } + if (patchFlag === -2) { + optimized = false; + vnode.dynamicChildren = null; + } + let nextNode = null; + switch (type) { + case Text: + if (domType !== 3) { + if (vnode.children === "") { + insert(vnode.el = createText(""), parentNode(node), node); + nextNode = node; + } else { + nextNode = onMismatch(); + } + } else { + if (node.data !== vnode.children) { + warn$1( + `Hydration text mismatch in`, + node.parentNode, + ` + - rendered on server: ${JSON.stringify( + node.data + )} + - expected on client: ${JSON.stringify(vnode.children)}` + ); + logMismatchError(); + node.data = vnode.children; + } + nextNode = nextSibling(node); + } + break; + case Comment: + if (isTemplateNode(node)) { + nextNode = nextSibling(node); + replaceNode( + vnode.el = node.content.firstChild, + node, + parentComponent + ); + } else if (domType !== 8 || isFragmentStart) { + nextNode = onMismatch(); + } else { + nextNode = nextSibling(node); + } + break; + case Static: + if (isFragmentStart) { + node = nextSibling(node); + domType = node.nodeType; + } + if (domType === 1 || domType === 3) { + nextNode = node; + const needToAdoptContent = !vnode.children.length; + for (let i = 0; i < vnode.staticCount; i++) { + if (needToAdoptContent) + vnode.children += nextNode.nodeType === 1 ? nextNode.outerHTML : nextNode.data; + if (i === vnode.staticCount - 1) { + vnode.anchor = nextNode; + } + nextNode = nextSibling(nextNode); + } + return isFragmentStart ? nextSibling(nextNode) : nextNode; + } else { + onMismatch(); + } + break; + case Fragment: + if (!isFragmentStart) { + nextNode = onMismatch(); + } else { + nextNode = hydrateFragment( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } + break; + default: + if (shapeFlag & 1) { + if ((domType !== 1 || vnode.type.toLowerCase() !== node.tagName.toLowerCase()) && !isTemplateNode(node)) { + nextNode = onMismatch(); + } else { + nextNode = hydrateElement( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } + } else if (shapeFlag & 6) { + vnode.slotScopeIds = slotScopeIds; + const container = parentNode(node); + if (isFragmentStart) { + nextNode = locateClosingAnchor(node); + } else if (isComment(node) && node.data === "teleport start") { + nextNode = locateClosingAnchor(node, node.data, "teleport end"); + } else { + nextNode = nextSibling(node); + } + mountComponent( + vnode, + container, + null, + parentComponent, + parentSuspense, + getContainerType(container), + optimized + ); + if (isAsyncWrapper(vnode) && !vnode.type.__asyncResolved) { + let subTree; + if (isFragmentStart) { + subTree = createVNode(Fragment); + subTree.anchor = nextNode ? nextNode.previousSibling : container.lastChild; + } else { + subTree = node.nodeType === 3 ? createTextVNode("") : createVNode("div"); + } + subTree.el = node; + vnode.component.subTree = subTree; + } + } else if (shapeFlag & 64) { + if (domType !== 8) { + nextNode = onMismatch(); + } else { + nextNode = vnode.type.hydrate( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized, + rendererInternals, + hydrateChildren + ); + } + } else if (shapeFlag & 128) { + nextNode = vnode.type.hydrate( + node, + vnode, + parentComponent, + parentSuspense, + getContainerType(parentNode(node)), + slotScopeIds, + optimized, + rendererInternals, + hydrateNode + ); + } else { + warn$1("Invalid HostVNode type:", type, `(${typeof type})`); + } + } + if (ref != null) { + setRef(ref, null, parentSuspense, vnode); + } + return nextNode; + }; + const hydrateElement = (el, vnode, parentComponent, parentSuspense, slotScopeIds, optimized) => { + optimized = optimized || !!vnode.dynamicChildren; + const { type, props, patchFlag, shapeFlag, dirs, transition } = vnode; + const forcePatch = type === "input" || type === "option"; + { + if (dirs) { + invokeDirectiveHook(vnode, null, parentComponent, "created"); + } + let needCallTransitionHooks = false; + if (isTemplateNode(el)) { + needCallTransitionHooks = needTransition( + null, + // no need check parentSuspense in hydration + transition + ) && parentComponent && parentComponent.vnode.props && parentComponent.vnode.props.appear; + const content = el.content.firstChild; + if (needCallTransitionHooks) { + const cls = content.getAttribute("class"); + if (cls) content.$cls = cls; + transition.beforeEnter(content); + } + replaceNode(content, el, parentComponent); + vnode.el = el = content; + } + if (shapeFlag & 16 && // skip if element has innerHTML / textContent + !(props && (props.innerHTML || props.textContent))) { + let next = hydrateChildren( + el.firstChild, + vnode, + el, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + let hasWarned = false; + while (next) { + if (!isMismatchAllowed(el, 1 /* CHILDREN */)) { + if (!hasWarned) { + warn$1( + `Hydration children mismatch on`, + el, + ` +Server rendered element contains more child nodes than client vdom.` + ); + hasWarned = true; + } + logMismatchError(); + } + const cur = next; + next = next.nextSibling; + remove(cur); + } + } else if (shapeFlag & 8) { + let clientText = vnode.children; + if (clientText[0] === "\n" && (el.tagName === "PRE" || el.tagName === "TEXTAREA")) { + clientText = clientText.slice(1); + } + const { textContent } = el; + if (textContent !== clientText && // innerHTML normalize \r\n or \r into a single \n in the DOM + textContent !== clientText.replace(/\r\n|\r/g, "\n")) { + if (!isMismatchAllowed(el, 0 /* TEXT */)) { + warn$1( + `Hydration text content mismatch on`, + el, + ` + - rendered on server: ${textContent} + - expected on client: ${clientText}` + ); + logMismatchError(); + } + el.textContent = vnode.children; + } + } + if (props) { + { + const isCustomElement = el.tagName.includes("-"); + for (const key in props) { + if (// #11189 skip if this node has directives that have created hooks + // as it could have mutated the DOM in any possible way + !(dirs && dirs.some((d) => d.dir.created)) && propHasMismatch(el, key, props[key], vnode, parentComponent)) { + logMismatchError(); + } + if (forcePatch && (key.endsWith("value") || key === "indeterminate") || isOn(key) && !isReservedProp(key) || // force hydrate v-bind with .prop modifiers + key[0] === "." || isCustomElement) { + patchProp(el, key, null, props[key], void 0, parentComponent); + } + } + } + } + let vnodeHooks; + if (vnodeHooks = props && props.onVnodeBeforeMount) { + invokeVNodeHook(vnodeHooks, parentComponent, vnode); + } + if (dirs) { + invokeDirectiveHook(vnode, null, parentComponent, "beforeMount"); + } + if ((vnodeHooks = props && props.onVnodeMounted) || dirs || needCallTransitionHooks) { + queueEffectWithSuspense(() => { + vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode); + needCallTransitionHooks && transition.enter(el); + dirs && invokeDirectiveHook(vnode, null, parentComponent, "mounted"); + }, parentSuspense); + } + } + return el.nextSibling; + }; + const hydrateChildren = (node, parentVNode, container, parentComponent, parentSuspense, slotScopeIds, optimized) => { + optimized = optimized || !!parentVNode.dynamicChildren; + const children = parentVNode.children; + const l = children.length; + let hasWarned = false; + for (let i = 0; i < l; i++) { + const vnode = optimized ? children[i] : children[i] = normalizeVNode(children[i]); + const isText = vnode.type === Text; + if (node) { + if (isText && !optimized) { + if (i + 1 < l && normalizeVNode(children[i + 1]).type === Text) { + insert( + createText( + node.data.slice(vnode.children.length) + ), + container, + nextSibling(node) + ); + node.data = vnode.children; + } + } + node = hydrateNode( + node, + vnode, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + } else if (isText && !vnode.children) { + insert(vnode.el = createText(""), container); + } else { + if (!isMismatchAllowed(container, 1 /* CHILDREN */)) { + if (!hasWarned) { + warn$1( + `Hydration children mismatch on`, + container, + ` +Server rendered element contains fewer child nodes than client vdom.` + ); + hasWarned = true; + } + logMismatchError(); + } + patch( + null, + vnode, + container, + null, + parentComponent, + parentSuspense, + getContainerType(container), + slotScopeIds + ); + } + } + return node; + }; + const hydrateFragment = (node, vnode, parentComponent, parentSuspense, slotScopeIds, optimized) => { + const { slotScopeIds: fragmentSlotScopeIds } = vnode; + if (fragmentSlotScopeIds) { + slotScopeIds = slotScopeIds ? slotScopeIds.concat(fragmentSlotScopeIds) : fragmentSlotScopeIds; + } + const container = parentNode(node); + const next = hydrateChildren( + nextSibling(node), + vnode, + container, + parentComponent, + parentSuspense, + slotScopeIds, + optimized + ); + if (next && isComment(next) && next.data === "]") { + return nextSibling(vnode.anchor = next); + } else { + logMismatchError(); + insert(vnode.anchor = createComment(`]`), container, next); + return next; + } + }; + const handleMismatch = (node, vnode, parentComponent, parentSuspense, slotScopeIds, isFragment) => { + if (!isMismatchAllowed(node.parentElement, 1 /* CHILDREN */)) { + warn$1( + `Hydration node mismatch: +- rendered on server:`, + node, + node.nodeType === 3 ? `(text)` : isComment(node) && node.data === "[" ? `(start of fragment)` : ``, + ` +- expected on client:`, + vnode.type + ); + logMismatchError(); + } + vnode.el = null; + if (isFragment) { + const end = locateClosingAnchor(node); + while (true) { + const next2 = nextSibling(node); + if (next2 && next2 !== end) { + remove(next2); + } else { + break; + } + } + } + const next = nextSibling(node); + const container = parentNode(node); + remove(node); + patch( + null, + vnode, + container, + next, + parentComponent, + parentSuspense, + getContainerType(container), + slotScopeIds + ); + if (parentComponent) { + parentComponent.vnode.el = vnode.el; + updateHOCHostEl(parentComponent, vnode.el); + } + return next; + }; + const locateClosingAnchor = (node, open = "[", close = "]") => { + let match = 0; + while (node) { + node = nextSibling(node); + if (node && isComment(node)) { + if (node.data === open) match++; + if (node.data === close) { + if (match === 0) { + return nextSibling(node); + } else { + match--; + } + } + } + } + return node; + }; + const replaceNode = (newNode, oldNode, parentComponent) => { + const parentNode2 = oldNode.parentNode; + if (parentNode2) { + parentNode2.replaceChild(newNode, oldNode); + } + let parent = parentComponent; + while (parent) { + if (parent.vnode.el === oldNode) { + parent.vnode.el = parent.subTree.el = newNode; + } + parent = parent.parent; + } + }; + const isTemplateNode = (node) => { + return node.nodeType === 1 && node.tagName === "TEMPLATE"; + }; + return [hydrate, hydrateNode]; + } + function propHasMismatch(el, key, clientValue, vnode, instance) { + let mismatchType; + let mismatchKey; + let actual; + let expected; + if (key === "class") { + if (el.$cls) { + actual = el.$cls; + delete el.$cls; + } else { + actual = el.getAttribute("class"); + } + expected = normalizeClass(clientValue); + if (!isSetEqual(toClassSet(actual || ""), toClassSet(expected))) { + mismatchType = 2 /* CLASS */; + mismatchKey = `class`; + } + } else if (key === "style") { + actual = el.getAttribute("style") || ""; + expected = isString(clientValue) ? clientValue : stringifyStyle(normalizeStyle(clientValue)); + const actualMap = toStyleMap(actual); + const expectedMap = toStyleMap(expected); + if (vnode.dirs) { + for (const { dir, value } of vnode.dirs) { + if (dir.name === "show" && !value) { + expectedMap.set("display", "none"); + } + } + } + if (instance) { + resolveCssVars(instance, vnode, expectedMap); + } + if (!isMapEqual(actualMap, expectedMap)) { + mismatchType = 3 /* STYLE */; + mismatchKey = "style"; + } + } else if (el instanceof SVGElement && isKnownSvgAttr(key) || el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key))) { + if (isBooleanAttr(key)) { + actual = el.hasAttribute(key); + expected = includeBooleanAttr(clientValue); + } else if (clientValue == null) { + actual = el.hasAttribute(key); + expected = false; + } else { + if (el.hasAttribute(key)) { + actual = el.getAttribute(key); + } else if (key === "value" && el.tagName === "TEXTAREA") { + actual = el.value; + } else { + actual = false; + } + expected = isRenderableAttrValue(clientValue) ? String(clientValue) : false; + } + if (actual !== expected) { + mismatchType = 4 /* ATTRIBUTE */; + mismatchKey = key; + } + } + if (mismatchType != null && !isMismatchAllowed(el, mismatchType)) { + const format = (v) => v === false ? `(not rendered)` : `${mismatchKey}="${v}"`; + const preSegment = `Hydration ${MismatchTypeString[mismatchType]} mismatch on`; + const postSegment = ` + - rendered on server: ${format(actual)} + - expected on client: ${format(expected)} + Note: this mismatch is check-only. The DOM will not be rectified in production due to performance overhead. + You should fix the source of the mismatch.`; + { + warn$1(preSegment, el, postSegment); + } + return true; + } + return false; + } + function toClassSet(str) { + return new Set(str.trim().split(/\s+/)); + } + function isSetEqual(a, b) { + if (a.size !== b.size) { + return false; + } + for (const s of a) { + if (!b.has(s)) { + return false; + } + } + return true; + } + function toStyleMap(str) { + const styleMap = /* @__PURE__ */ new Map(); + for (const item of str.split(";")) { + let [key, value] = item.split(":"); + key = key.trim(); + value = value && value.trim(); + if (key && value) { + styleMap.set(key, value); + } + } + return styleMap; + } + function isMapEqual(a, b) { + if (a.size !== b.size) { + return false; + } + for (const [key, value] of a) { + if (value !== b.get(key)) { + return false; + } + } + return true; + } + function resolveCssVars(instance, vnode, expectedMap) { + const root = instance.subTree; + if (instance.getCssVars && (vnode === root || root && root.type === Fragment && root.children.includes(vnode))) { + const cssVars = instance.getCssVars(); + for (const key in cssVars) { + const value = normalizeCssVarValue(cssVars[key]); + expectedMap.set(`--${getEscapedCssVarName(key)}`, value); + } + } + if (vnode === root && instance.parent) { + resolveCssVars(instance.parent, instance.vnode, expectedMap); + } + } + const allowMismatchAttr = "data-allow-mismatch"; + const MismatchTypeString = { + [0 /* TEXT */]: "text", + [1 /* CHILDREN */]: "children", + [2 /* CLASS */]: "class", + [3 /* STYLE */]: "style", + [4 /* ATTRIBUTE */]: "attribute" + }; + function isMismatchAllowed(el, allowedType) { + if (allowedType === 0 /* TEXT */ || allowedType === 1 /* CHILDREN */) { + while (el && !el.hasAttribute(allowMismatchAttr)) { + el = el.parentElement; + } + } + const allowedAttr = el && el.getAttribute(allowMismatchAttr); + if (allowedAttr == null) { + return false; + } else if (allowedAttr === "") { + return true; + } else { + const list = allowedAttr.split(","); + if (allowedType === 0 /* TEXT */ && list.includes("children")) { + return true; + } + return list.includes(MismatchTypeString[allowedType]); + } + } + + const requestIdleCallback = getGlobalThis().requestIdleCallback || ((cb) => setTimeout(cb, 1)); + const cancelIdleCallback = getGlobalThis().cancelIdleCallback || ((id) => clearTimeout(id)); + const hydrateOnIdle = (timeout = 1e4) => (hydrate) => { + const id = requestIdleCallback(hydrate, { timeout }); + return () => cancelIdleCallback(id); + }; + function elementIsVisibleInViewport(el) { + const { top, left, bottom, right } = el.getBoundingClientRect(); + const { innerHeight, innerWidth } = window; + return (top > 0 && top < innerHeight || bottom > 0 && bottom < innerHeight) && (left > 0 && left < innerWidth || right > 0 && right < innerWidth); + } + const hydrateOnVisible = (opts) => (hydrate, forEach) => { + const ob = new IntersectionObserver((entries) => { + for (const e of entries) { + if (!e.isIntersecting) continue; + ob.disconnect(); + hydrate(); + break; + } + }, opts); + forEach((el) => { + if (!(el instanceof Element)) return; + if (elementIsVisibleInViewport(el)) { + hydrate(); + ob.disconnect(); + return false; + } + ob.observe(el); + }); + return () => ob.disconnect(); + }; + const hydrateOnMediaQuery = (query) => (hydrate) => { + if (query) { + const mql = matchMedia(query); + if (mql.matches) { + hydrate(); + } else { + mql.addEventListener("change", hydrate, { once: true }); + return () => mql.removeEventListener("change", hydrate); + } + } + }; + const hydrateOnInteraction = (interactions = []) => (hydrate, forEach) => { + if (isString(interactions)) interactions = [interactions]; + let hasHydrated = false; + const doHydrate = (e) => { + if (!hasHydrated) { + hasHydrated = true; + teardown(); + hydrate(); + e.target.dispatchEvent(new e.constructor(e.type, e)); + } + }; + const teardown = () => { + forEach((el) => { + for (const i of interactions) { + el.removeEventListener(i, doHydrate); + } + }); + }; + forEach((el) => { + for (const i of interactions) { + el.addEventListener(i, doHydrate, { once: true }); + } + }); + return teardown; + }; + function forEachElement(node, cb) { + if (isComment(node) && node.data === "[") { + let depth = 1; + let next = node.nextSibling; + while (next) { + if (next.nodeType === 1) { + const result = cb(next); + if (result === false) { + break; + } + } else if (isComment(next)) { + if (next.data === "]") { + if (--depth === 0) break; + } else if (next.data === "[") { + depth++; + } + } + next = next.nextSibling; + } + } else { + cb(node); + } + } + + const isAsyncWrapper = (i) => !!i.type.__asyncLoader; + // @__NO_SIDE_EFFECTS__ + function defineAsyncComponent(source) { + if (isFunction(source)) { + source = { loader: source }; + } + const { + loader, + loadingComponent, + errorComponent, + delay = 200, + hydrate: hydrateStrategy, + timeout, + // undefined = never times out + suspensible = true, + onError: userOnError + } = source; + let pendingRequest = null; + let resolvedComp; + let retries = 0; + const retry = () => { + retries++; + pendingRequest = null; + return load(); + }; + const load = () => { + let thisRequest; + return pendingRequest || (thisRequest = pendingRequest = loader().catch((err) => { + err = err instanceof Error ? err : new Error(String(err)); + if (userOnError) { + return new Promise((resolve, reject) => { + const userRetry = () => resolve(retry()); + const userFail = () => reject(err); + userOnError(err, userRetry, userFail, retries + 1); + }); + } else { + throw err; + } + }).then((comp) => { + if (thisRequest !== pendingRequest && pendingRequest) { + return pendingRequest; + } + if (!comp) { + warn$1( + `Async component loader resolved to undefined. If you are using retry(), make sure to return its return value.` + ); + } + if (comp && (comp.__esModule || comp[Symbol.toStringTag] === "Module")) { + comp = comp.default; + } + if (comp && !isObject(comp) && !isFunction(comp)) { + throw new Error(`Invalid async component load result: ${comp}`); + } + resolvedComp = comp; + return comp; + })); + }; + return defineComponent({ + name: "AsyncComponentWrapper", + __asyncLoader: load, + __asyncHydrate(el, instance, hydrate) { + let patched = false; + (instance.bu || (instance.bu = [])).push(() => patched = true); + const performHydrate = () => { + if (patched) { + { + warn$1( + `Skipping lazy hydration for component '${getComponentName(resolvedComp) || resolvedComp.__file}': it was updated before lazy hydration performed.` + ); + } + return; + } + hydrate(); + }; + const doHydrate = hydrateStrategy ? () => { + const teardown = hydrateStrategy( + performHydrate, + (cb) => forEachElement(el, cb) + ); + if (teardown) { + (instance.bum || (instance.bum = [])).push(teardown); + } + } : performHydrate; + if (resolvedComp) { + doHydrate(); + } else { + load().then(() => !instance.isUnmounted && doHydrate()); + } + }, + get __asyncResolved() { + return resolvedComp; + }, + setup() { + const instance = currentInstance; + markAsyncBoundary(instance); + if (resolvedComp) { + return () => createInnerComp(resolvedComp, instance); + } + const onError = (err) => { + pendingRequest = null; + handleError( + err, + instance, + 13, + !errorComponent + ); + }; + if (suspensible && instance.suspense || false) { + return load().then((comp) => { + return () => createInnerComp(comp, instance); + }).catch((err) => { + onError(err); + return () => errorComponent ? createVNode(errorComponent, { + error: err + }) : null; + }); + } + const loaded = ref(false); + const error = ref(); + const delayed = ref(!!delay); + if (delay) { + setTimeout(() => { + delayed.value = false; + }, delay); + } + if (timeout != null) { + setTimeout(() => { + if (!loaded.value && !error.value) { + const err = new Error( + `Async component timed out after ${timeout}ms.` + ); + onError(err); + error.value = err; + } + }, timeout); + } + load().then(() => { + loaded.value = true; + if (instance.parent && isKeepAlive(instance.parent.vnode)) { + instance.parent.update(); + } + }).catch((err) => { + onError(err); + error.value = err; + }); + return () => { + if (loaded.value && resolvedComp) { + return createInnerComp(resolvedComp, instance); + } else if (error.value && errorComponent) { + return createVNode(errorComponent, { + error: error.value + }); + } else if (loadingComponent && !delayed.value) { + return createInnerComp( + loadingComponent, + instance + ); + } + }; + } + }); + } + function createInnerComp(comp, parent) { + const { ref: ref2, props, children, ce } = parent.vnode; + const vnode = createVNode(comp, props, children); + vnode.ref = ref2; + vnode.ce = ce; + delete parent.vnode.ce; + return vnode; + } + + const isKeepAlive = (vnode) => vnode.type.__isKeepAlive; + const KeepAliveImpl = { + name: `KeepAlive`, + // Marker for special handling inside the renderer. We are not using a === + // check directly on KeepAlive in the renderer, because importing it directly + // would prevent it from being tree-shaken. + __isKeepAlive: true, + props: { + include: [String, RegExp, Array], + exclude: [String, RegExp, Array], + max: [String, Number] + }, + setup(props, { slots }) { + const instance = getCurrentInstance(); + const sharedContext = instance.ctx; + const cache = /* @__PURE__ */ new Map(); + const keys = /* @__PURE__ */ new Set(); + let current = null; + { + instance.__v_cache = cache; + } + const parentSuspense = instance.suspense; + const { + renderer: { + p: patch, + m: move, + um: _unmount, + o: { createElement } + } + } = sharedContext; + const storageContainer = createElement("div"); + sharedContext.activate = (vnode, container, anchor, namespace, optimized) => { + const instance2 = vnode.component; + move(vnode, container, anchor, 0, parentSuspense); + patch( + instance2.vnode, + vnode, + container, + anchor, + instance2, + parentSuspense, + namespace, + vnode.slotScopeIds, + optimized + ); + queuePostRenderEffect(() => { + instance2.isDeactivated = false; + if (instance2.a) { + invokeArrayFns(instance2.a); + } + const vnodeHook = vnode.props && vnode.props.onVnodeMounted; + if (vnodeHook) { + invokeVNodeHook(vnodeHook, instance2.parent, vnode); + } + }, parentSuspense); + { + devtoolsComponentAdded(instance2); + } + }; + sharedContext.deactivate = (vnode) => { + const instance2 = vnode.component; + invalidateMount(instance2.m); + invalidateMount(instance2.a); + move(vnode, storageContainer, null, 1, parentSuspense); + queuePostRenderEffect(() => { + if (instance2.da) { + invokeArrayFns(instance2.da); + } + const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted; + if (vnodeHook) { + invokeVNodeHook(vnodeHook, instance2.parent, vnode); + } + instance2.isDeactivated = true; + }, parentSuspense); + { + devtoolsComponentAdded(instance2); + } + { + instance2.__keepAliveStorageContainer = storageContainer; + } + }; + function unmount(vnode) { + resetShapeFlag(vnode); + _unmount(vnode, instance, parentSuspense, true); + } + function pruneCache(filter) { + cache.forEach((vnode, key) => { + const name = getComponentName(vnode.type); + if (name && !filter(name)) { + pruneCacheEntry(key); + } + }); + } + function pruneCacheEntry(key) { + const cached = cache.get(key); + if (cached && (!current || !isSameVNodeType(cached, current))) { + unmount(cached); + } else if (current) { + resetShapeFlag(current); + } + cache.delete(key); + keys.delete(key); + } + watch( + () => [props.include, props.exclude], + ([include, exclude]) => { + include && pruneCache((name) => matches(include, name)); + exclude && pruneCache((name) => !matches(exclude, name)); + }, + // prune post-render after `current` has been updated + { flush: "post", deep: true } + ); + let pendingCacheKey = null; + const cacheSubtree = () => { + if (pendingCacheKey != null) { + if (isSuspense(instance.subTree.type)) { + queuePostRenderEffect(() => { + cache.set(pendingCacheKey, getInnerChild(instance.subTree)); + }, instance.subTree.suspense); + } else { + cache.set(pendingCacheKey, getInnerChild(instance.subTree)); + } + } + }; + onMounted(cacheSubtree); + onUpdated(cacheSubtree); + onBeforeUnmount(() => { + cache.forEach((cached) => { + const { subTree, suspense } = instance; + const vnode = getInnerChild(subTree); + if (cached.type === vnode.type && cached.key === vnode.key) { + resetShapeFlag(vnode); + const da = vnode.component.da; + da && queuePostRenderEffect(da, suspense); + return; + } + unmount(cached); + }); + }); + return () => { + pendingCacheKey = null; + if (!slots.default) { + return current = null; + } + const children = slots.default(); + const rawVNode = children[0]; + if (children.length > 1) { + { + warn$1(`KeepAlive should contain exactly one component child.`); + } + current = null; + return children; + } else if (!isVNode(rawVNode) || !(rawVNode.shapeFlag & 4) && !(rawVNode.shapeFlag & 128)) { + current = null; + return rawVNode; + } + let vnode = getInnerChild(rawVNode); + if (vnode.type === Comment) { + current = null; + return vnode; + } + const comp = vnode.type; + const name = getComponentName( + isAsyncWrapper(vnode) ? vnode.type.__asyncResolved || {} : comp + ); + const { include, exclude, max } = props; + if (include && (!name || !matches(include, name)) || exclude && name && matches(exclude, name)) { + vnode.shapeFlag &= -257; + current = vnode; + return rawVNode; + } + const key = vnode.key == null ? comp : vnode.key; + const cachedVNode = cache.get(key); + if (vnode.el) { + vnode = cloneVNode(vnode); + if (rawVNode.shapeFlag & 128) { + rawVNode.ssContent = vnode; + } + } + pendingCacheKey = key; + if (cachedVNode) { + vnode.el = cachedVNode.el; + vnode.component = cachedVNode.component; + if (vnode.transition) { + setTransitionHooks(vnode, vnode.transition); + } + vnode.shapeFlag |= 512; + keys.delete(key); + keys.add(key); + } else { + keys.add(key); + if (max && keys.size > parseInt(max, 10)) { + pruneCacheEntry(keys.values().next().value); + } + } + vnode.shapeFlag |= 256; + current = vnode; + return isSuspense(rawVNode.type) ? rawVNode : vnode; + }; + } + }; + const KeepAlive = KeepAliveImpl; + function matches(pattern, name) { + if (isArray(pattern)) { + return pattern.some((p) => matches(p, name)); + } else if (isString(pattern)) { + return pattern.split(",").includes(name); + } else if (isRegExp(pattern)) { + pattern.lastIndex = 0; + return pattern.test(name); + } + return false; + } + function onActivated(hook, target) { + registerKeepAliveHook(hook, "a", target); + } + function onDeactivated(hook, target) { + registerKeepAliveHook(hook, "da", target); + } + function registerKeepAliveHook(hook, type, target = currentInstance) { + const wrappedHook = hook.__wdc || (hook.__wdc = () => { + let current = target; + while (current) { + if (current.isDeactivated) { + return; + } + current = current.parent; + } + return hook(); + }); + injectHook(type, wrappedHook, target); + if (target) { + let current = target.parent; + while (current && current.parent) { + if (isKeepAlive(current.parent.vnode)) { + injectToKeepAliveRoot(wrappedHook, type, target, current); + } + current = current.parent; + } + } + } + function injectToKeepAliveRoot(hook, type, target, keepAliveRoot) { + const injected = injectHook( + type, + hook, + keepAliveRoot, + true + /* prepend */ + ); + onUnmounted(() => { + remove(keepAliveRoot[type], injected); + }, target); + } + function resetShapeFlag(vnode) { + vnode.shapeFlag &= -257; + vnode.shapeFlag &= -513; + } + function getInnerChild(vnode) { + return vnode.shapeFlag & 128 ? vnode.ssContent : vnode; + } + + function injectHook(type, hook, target = currentInstance, prepend = false) { + if (target) { + const hooks = target[type] || (target[type] = []); + const wrappedHook = hook.__weh || (hook.__weh = (...args) => { + pauseTracking(); + const reset = setCurrentInstance(target); + const res = callWithAsyncErrorHandling(hook, target, type, args); + reset(); + resetTracking(); + return res; + }); + if (prepend) { + hooks.unshift(wrappedHook); + } else { + hooks.push(wrappedHook); + } + return wrappedHook; + } else { + const apiName = toHandlerKey(ErrorTypeStrings$1[type].replace(/ hook$/, "")); + warn$1( + `${apiName} is called when there is no active component instance to be associated with. Lifecycle injection APIs can only be used during execution of setup().` + (` If you are using async setup(), make sure to register lifecycle hooks before the first await statement.` ) + ); + } + } + const createHook = (lifecycle) => (hook, target = currentInstance) => { + if (!isInSSRComponentSetup || lifecycle === "sp") { + injectHook(lifecycle, (...args) => hook(...args), target); + } + }; + const onBeforeMount = createHook("bm"); + const onMounted = createHook("m"); + const onBeforeUpdate = createHook( + "bu" + ); + const onUpdated = createHook("u"); + const onBeforeUnmount = createHook( + "bum" + ); + const onUnmounted = createHook("um"); + const onServerPrefetch = createHook( + "sp" + ); + const onRenderTriggered = createHook("rtg"); + const onRenderTracked = createHook("rtc"); + function onErrorCaptured(hook, target = currentInstance) { + injectHook("ec", hook, target); + } + + const COMPONENTS = "components"; + const DIRECTIVES = "directives"; + function resolveComponent(name, maybeSelfReference) { + return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name; + } + const NULL_DYNAMIC_COMPONENT = Symbol.for("v-ndc"); + function resolveDynamicComponent(component) { + if (isString(component)) { + return resolveAsset(COMPONENTS, component, false) || component; + } else { + return component || NULL_DYNAMIC_COMPONENT; + } + } + function resolveDirective(name) { + return resolveAsset(DIRECTIVES, name); + } + function resolveAsset(type, name, warnMissing = true, maybeSelfReference = false) { + const instance = currentRenderingInstance || currentInstance; + if (instance) { + const Component = instance.type; + if (type === COMPONENTS) { + const selfName = getComponentName( + Component, + false + ); + if (selfName && (selfName === name || selfName === camelize(name) || selfName === capitalize(camelize(name)))) { + return Component; + } + } + const res = ( + // local registration + // check instance[type] first which is resolved for options API + resolve(instance[type] || Component[type], name) || // global registration + resolve(instance.appContext[type], name) + ); + if (!res && maybeSelfReference) { + return Component; + } + if (warnMissing && !res) { + const extra = type === COMPONENTS ? ` +If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.` : ``; + warn$1(`Failed to resolve ${type.slice(0, -1)}: ${name}${extra}`); + } + return res; + } else { + warn$1( + `resolve${capitalize(type.slice(0, -1))} can only be used in render() or setup().` + ); + } + } + function resolve(registry, name) { + return registry && (registry[name] || registry[camelize(name)] || registry[capitalize(camelize(name))]); + } + + function renderList(source, renderItem, cache, index) { + let ret; + const cached = cache && cache[index]; + const sourceIsArray = isArray(source); + if (sourceIsArray || isString(source)) { + const sourceIsReactiveArray = sourceIsArray && isReactive(source); + let needsWrap = false; + let isReadonlySource = false; + if (sourceIsReactiveArray) { + needsWrap = !isShallow(source); + isReadonlySource = isReadonly(source); + source = shallowReadArray(source); + } + ret = new Array(source.length); + for (let i = 0, l = source.length; i < l; i++) { + ret[i] = renderItem( + needsWrap ? isReadonlySource ? toReadonly(toReactive(source[i])) : toReactive(source[i]) : source[i], + i, + void 0, + cached && cached[i] + ); + } + } else if (typeof source === "number") { + if (!Number.isInteger(source)) { + warn$1(`The v-for range expect an integer value but got ${source}.`); + } + ret = new Array(source); + for (let i = 0; i < source; i++) { + ret[i] = renderItem(i + 1, i, void 0, cached && cached[i]); + } + } else if (isObject(source)) { + if (source[Symbol.iterator]) { + ret = Array.from( + source, + (item, i) => renderItem(item, i, void 0, cached && cached[i]) + ); + } else { + const keys = Object.keys(source); + ret = new Array(keys.length); + for (let i = 0, l = keys.length; i < l; i++) { + const key = keys[i]; + ret[i] = renderItem(source[key], key, i, cached && cached[i]); + } + } + } else { + ret = []; + } + if (cache) { + cache[index] = ret; + } + return ret; + } + + function createSlots(slots, dynamicSlots) { + for (let i = 0; i < dynamicSlots.length; i++) { + const slot = dynamicSlots[i]; + if (isArray(slot)) { + for (let j = 0; j < slot.length; j++) { + slots[slot[j].name] = slot[j].fn; + } + } else if (slot) { + slots[slot.name] = slot.key ? (...args) => { + const res = slot.fn(...args); + if (res) res.key = slot.key; + return res; + } : slot.fn; + } + } + return slots; + } + + function renderSlot(slots, name, props = {}, fallback, noSlotted) { + if (currentRenderingInstance.ce || currentRenderingInstance.parent && isAsyncWrapper(currentRenderingInstance.parent) && currentRenderingInstance.parent.ce) { + const hasProps = Object.keys(props).length > 0; + if (name !== "default") props.name = name; + return openBlock(), createBlock( + Fragment, + null, + [createVNode("slot", props, fallback && fallback())], + hasProps ? -2 : 64 + ); + } + let slot = slots[name]; + if (slot && slot.length > 1) { + warn$1( + `SSR-optimized slot function detected in a non-SSR-optimized render function. You need to mark this component with $dynamic-slots in the parent template.` + ); + slot = () => []; + } + if (slot && slot._c) { + slot._d = false; + } + openBlock(); + const validSlotContent = slot && ensureValidVNode(slot(props)); + const slotKey = props.key || // slot content array of a dynamic conditional slot may have a branch + // key attached in the `createSlots` helper, respect that + validSlotContent && validSlotContent.key; + const rendered = createBlock( + Fragment, + { + key: (slotKey && !isSymbol(slotKey) ? slotKey : `_${name}`) + // #7256 force differentiate fallback content from actual content + (!validSlotContent && fallback ? "_fb" : "") + }, + validSlotContent || (fallback ? fallback() : []), + validSlotContent && slots._ === 1 ? 64 : -2 + ); + if (!noSlotted && rendered.scopeId) { + rendered.slotScopeIds = [rendered.scopeId + "-s"]; + } + if (slot && slot._c) { + slot._d = true; + } + return rendered; + } + function ensureValidVNode(vnodes) { + return vnodes.some((child) => { + if (!isVNode(child)) return true; + if (child.type === Comment) return false; + if (child.type === Fragment && !ensureValidVNode(child.children)) + return false; + return true; + }) ? vnodes : null; + } + + function toHandlers(obj, preserveCaseIfNecessary) { + const ret = {}; + if (!isObject(obj)) { + warn$1(`v-on with no argument expects an object value.`); + return ret; + } + for (const key in obj) { + ret[preserveCaseIfNecessary && /[A-Z]/.test(key) ? `on:${key}` : toHandlerKey(key)] = obj[key]; + } + return ret; + } + + const getPublicInstance = (i) => { + if (!i) return null; + if (isStatefulComponent(i)) return getComponentPublicInstance(i); + return getPublicInstance(i.parent); + }; + const publicPropertiesMap = ( + // Move PURE marker to new line to workaround compiler discarding it + // due to type annotation + /* @__PURE__ */ extend(/* @__PURE__ */ Object.create(null), { + $: (i) => i, + $el: (i) => i.vnode.el, + $data: (i) => i.data, + $props: (i) => shallowReadonly(i.props) , + $attrs: (i) => shallowReadonly(i.attrs) , + $slots: (i) => shallowReadonly(i.slots) , + $refs: (i) => shallowReadonly(i.refs) , + $parent: (i) => getPublicInstance(i.parent), + $root: (i) => getPublicInstance(i.root), + $host: (i) => i.ce, + $emit: (i) => i.emit, + $options: (i) => resolveMergedOptions(i) , + $forceUpdate: (i) => i.f || (i.f = () => { + queueJob(i.update); + }), + $nextTick: (i) => i.n || (i.n = nextTick.bind(i.proxy)), + $watch: (i) => instanceWatch.bind(i) + }) + ); + const isReservedPrefix = (key) => key === "_" || key === "$"; + const hasSetupBinding = (state, key) => state !== EMPTY_OBJ && !state.__isScriptSetup && hasOwn(state, key); + const PublicInstanceProxyHandlers = { + get({ _: instance }, key) { + if (key === "__v_skip") { + return true; + } + const { ctx, setupState, data, props, accessCache, type, appContext } = instance; + if (key === "__isVue") { + return true; + } + let normalizedProps; + if (key[0] !== "$") { + const n = accessCache[key]; + if (n !== void 0) { + switch (n) { + case 1 /* SETUP */: + return setupState[key]; + case 2 /* DATA */: + return data[key]; + case 4 /* CONTEXT */: + return ctx[key]; + case 3 /* PROPS */: + return props[key]; + } + } else if (hasSetupBinding(setupState, key)) { + accessCache[key] = 1 /* SETUP */; + return setupState[key]; + } else if (data !== EMPTY_OBJ && hasOwn(data, key)) { + accessCache[key] = 2 /* DATA */; + return data[key]; + } else if ( + // only cache other properties when instance has declared (thus stable) + // props + (normalizedProps = instance.propsOptions[0]) && hasOwn(normalizedProps, key) + ) { + accessCache[key] = 3 /* PROPS */; + return props[key]; + } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) { + accessCache[key] = 4 /* CONTEXT */; + return ctx[key]; + } else if (shouldCacheAccess) { + accessCache[key] = 0 /* OTHER */; + } + } + const publicGetter = publicPropertiesMap[key]; + let cssModule, globalProperties; + if (publicGetter) { + if (key === "$attrs") { + track(instance.attrs, "get", ""); + markAttrsAccessed(); + } else if (key === "$slots") { + track(instance, "get", key); + } + return publicGetter(instance); + } else if ( + // css module (injected by vue-loader) + (cssModule = type.__cssModules) && (cssModule = cssModule[key]) + ) { + return cssModule; + } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) { + accessCache[key] = 4 /* CONTEXT */; + return ctx[key]; + } else if ( + // global properties + globalProperties = appContext.config.globalProperties, hasOwn(globalProperties, key) + ) { + { + return globalProperties[key]; + } + } else if (currentRenderingInstance && (!isString(key) || // #1091 avoid internal isRef/isVNode checks on component instance leading + // to infinite warning loop + key.indexOf("__v") !== 0)) { + if (data !== EMPTY_OBJ && isReservedPrefix(key[0]) && hasOwn(data, key)) { + warn$1( + `Property ${JSON.stringify( + key + )} must be accessed via $data because it starts with a reserved character ("$" or "_") and is not proxied on the render context.` + ); + } else if (instance === currentRenderingInstance) { + warn$1( + `Property ${JSON.stringify(key)} was accessed during render but is not defined on instance.` + ); + } + } + }, + set({ _: instance }, key, value) { + const { data, setupState, ctx } = instance; + if (hasSetupBinding(setupState, key)) { + setupState[key] = value; + return true; + } else if (setupState.__isScriptSetup && hasOwn(setupState, key)) { + warn$1(`Cannot mutate + + diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..2b9ddd4 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,34 @@ +server { + listen 80; + server_name localhost; + + # 前端静态文件 + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ =404; + } + + # 后端API反向代理 + location /api/ { + proxy_pass http://backend:40001; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # 使用上游传递的协议,如果没有则使用当前协议 + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + proxy_cache_bypass $http_upgrade; + } + + # 分享链接重定向 + location /s/ { + proxy_pass http://backend:40001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + } +} diff --git a/nginx/nginx.conf.example b/nginx/nginx.conf.example new file mode 100644 index 0000000..eb84b66 --- /dev/null +++ b/nginx/nginx.conf.example @@ -0,0 +1,52 @@ +server { + listen 80; + server_name your-domain.com; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$server_name$request_uri; + } +} + +server { + listen 443 ssl http2; + server_name your-domain.com; + + ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + + # 前端静态文件 + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ =404; + } + + # 后端API反向代理 + location /api/ { + proxy_pass http://backend:40001; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + + # 分享链接重定向 + location /s/ { + proxy_pass http://backend:40001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} diff --git a/upload-tool/README.txt b/upload-tool/README.txt new file mode 100644 index 0000000..32125ed --- /dev/null +++ b/upload-tool/README.txt @@ -0,0 +1,92 @@ +============================================ +玩玩云上传工具 v2.0 使用说明 +============================================ + +【新版本特性】 +✨ 支持多文件上传 +✨ 支持文件夹上传(递归扫描所有文件) +✨ 智能上传队列管理 +✨ 自动检测可写目录(容错机制) +✨ 实时显示队列状态 + +【功能介绍】 +本工具用于快速上传文件到您的SFTP服务器。 +新版本支持批量上传和文件夹上传,大大提升工作效率! + +【使用方法】 +1. 双击运行"玩玩云上传工具.exe" +2. 等待程序连接服务器并测试上传目录 + - 程序会自动测试多个目录的可写性 + - 显示绿色✓表示连接成功 + - 显示当前使用的上传目录 +3. 拖拽文件或文件夹到窗口中 + - 可以一次拖拽多个文件 + - 可以拖拽整个文件夹(自动扫描所有文件) + - 混合拖拽也支持 +4. 查看队列状态 + - 界面显示"队列: X 个文件等待上传" + - 文件会按顺序依次上传 +5. 实时查看上传进度 + - 每个文件都有独立的进度显示 + - 日志区域显示详细的上传信息 + +【目录容错机制】 +程序会按以下优先级自动测试并选择可写目录: +1. /(根目录) +2. /upload +3. /uploads +4. /files +5. /home +6. /tmp + +如果根目录没有写权限,程序会自动切换到其他可用目录。 + +【注意事项】 +- 文件夹上传会递归扫描所有子文件夹 +- 同名文件会被覆盖 +- 上传大量文件时请确保网络稳定 +- 所有文件会按顺序依次上传 +- 上传目录会在启动时自动检测并显示 + +【界面说明】 +- 拖拽区域:显示"支持多文件和文件夹" +- 队列状态:显示等待上传的文件数量 +- 进度条:显示当前文件的上传进度 +- 日志区域:显示详细的操作记录 + +【版本更新】 +v2.0 (2025-11-09) +- ✅ 新增多文件上传支持 +- ✅ 新增文件夹上传支持 +- ✅ 新增上传队列管理 +- ✅ 新增目录容错机制 +- ✅ 优化界面显示 +- ✅ 优化日志输出 + +v1.0 +- 基础单文件上传功能 + +【常见问题】 + +Q: 支持上传多少个文件? +A: 理论上无限制,所有文件会加入队列依次上传 + +Q: 文件夹上传包括子文件夹吗? +A: 是的,会递归扫描所有子文件夹中的文件 + +Q: 上传目录是哪里? +A: 程序启动时会自动检测并显示在界面上 + +Q: 提示"API密钥无效或已过期"怎么办? +A: 请重新从网站下载最新的上传工具 + +Q: 上传速度慢怎么办? +A: 速度取决于您的网络和SFTP服务器性能 + +Q: 可以中途取消上传吗? +A: 当前版本暂不支持取消,请等待队列完成 + +【技术支持】 +如有问题请联系管理员 + +============================================ diff --git a/upload-tool/build.bat b/upload-tool/build.bat new file mode 100644 index 0000000..e4fa176 --- /dev/null +++ b/upload-tool/build.bat @@ -0,0 +1,52 @@ +@echo off +chcp 65001 > nul +echo ======================================== +echo 玩玩云上传工具打包脚本 +echo ======================================== +echo. + +REM 检查Python是否安装 +python --version > nul 2>&1 +if errorlevel 1 ( + echo [错误] 未检测到Python,请先安装Python 3.7+ + pause + exit /b 1 +) + +echo [1/4] 安装依赖包... +pip install -r requirements.txt +if errorlevel 1 ( + echo [错误] 依赖安装失败 + pause + exit /b 1 +) + +echo. +echo [2/4] 安装PyInstaller... +pip install pyinstaller +if errorlevel 1 ( + echo [错误] PyInstaller安装失败 + pause + exit /b 1 +) + +echo. +echo [3/4] 打包程序... +pyinstaller --onefile --windowed --name="玩玩云上传工具" --icon=NONE upload_tool.py +if errorlevel 1 ( + echo [错误] 打包失败 + pause + exit /b 1 +) + +echo. +echo [4/4] 清理临时文件... +rmdir /s /q build +del /q *.spec + +echo. +echo ======================================== +echo 打包完成! +echo 输出文件: dist\玩玩云上传工具.exe +echo ======================================== +pause diff --git a/upload-tool/requirements.txt b/upload-tool/requirements.txt new file mode 100644 index 0000000..735d771 --- /dev/null +++ b/upload-tool/requirements.txt @@ -0,0 +1,3 @@ +PyQt5==5.15.9 +paramiko==3.4.0 +requests==2.31.0 diff --git a/upload-tool/upload_tool.py b/upload-tool/upload_tool.py new file mode 100644 index 0000000..ae250fa --- /dev/null +++ b/upload-tool/upload_tool.py @@ -0,0 +1,499 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +import os +import json +import requests +import paramiko +from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QVBoxLayout, + QWidget, QProgressBar, QTextEdit, QPushButton) +from PyQt5.QtCore import Qt, QThread, pyqtSignal +from PyQt5.QtGui import QDragEnterEvent, QDropEvent, QFont + +class TestDirectoryThread(QThread): + """测试目录可写性线程""" + result = pyqtSignal(bool, str) # 成功/失败,目录路径或错误信息 + + def __init__(self, test_dir, sftp_config): + super().__init__() + self.test_dir = test_dir + self.sftp_config = sftp_config + + def run(self): + try: + transport = paramiko.Transport(( + self.sftp_config['host'], + self.sftp_config['port'] + )) + transport.connect( + username=self.sftp_config['username'], + password=self.sftp_config['password'] + ) + sftp = paramiko.SFTPClient.from_transport(transport) + + # 测试文件名 + test_file = f"{self.test_dir}/.wwy_test_{os.getpid()}" + if not self.test_dir.endswith('/'): + test_file = f"{self.test_dir}/.wwy_test_{os.getpid()}" + else: + test_file = f"{self.test_dir}.wwy_test_{os.getpid()}" + + # 尝试创建测试文件 + try: + with sftp.open(test_file, 'w') as f: + f.write('test') + + # 删除测试文件 + try: + sftp.remove(test_file) + except: + pass + + sftp.close() + transport.close() + self.result.emit(True, self.test_dir) + + except Exception as e: + sftp.close() + transport.close() + self.result.emit(False, str(e)) + + except Exception as e: + self.result.emit(False, str(e)) + +class UploadThread(QThread): + """上传线程""" + progress = pyqtSignal(int, str) # 进度,状态信息 + finished = pyqtSignal(bool, str) # 成功/失败,消息 + + def __init__(self, sftp_config, file_path, remote_dir): + super().__init__() + self.sftp_config = sftp_config + self.file_path = file_path + self.remote_dir = remote_dir + + def run(self): + try: + # 连接SFTP + self.progress.emit(10, f'正在连接服务器...') + transport = paramiko.Transport(( + self.sftp_config['host'], + self.sftp_config['port'] + )) + transport.connect( + username=self.sftp_config['username'], + password=self.sftp_config['password'] + ) + sftp = paramiko.SFTPClient.from_transport(transport) + + self.progress.emit(30, f'连接成功,开始上传...') + + # 获取文件名 + filename = os.path.basename(self.file_path) + # 构建远程路径 + if self.remote_dir.endswith('/'): + remote_path = f'{self.remote_dir}{filename}' + else: + remote_path = f'{self.remote_dir}/{filename}' + + # 上传文件(带进度)- 使用临时文件避免.fuse_hidden问题 + file_size = os.path.getsize(self.file_path) + uploaded = 0 + + # 使用临时文件名 + import time + temp_remote_path = f'{remote_path}.uploading_{int(time.time() * 1000)}' + + def callback(transferred, total): + nonlocal uploaded + uploaded = transferred + percent = int((transferred / total) * 100) if total > 0 else 0 + # 进度从30%到90% + progress_value = 30 + int(percent * 0.6) + + # 计算速度 + size_mb = transferred / (1024 * 1024) + self.progress.emit(progress_value, f'上传中: {filename} ({size_mb:.2f} MB / {total/(1024*1024):.2f} MB)') + + # 第一步:上传到临时文件 + sftp.put(self.file_path, temp_remote_path, callback=callback) + + # 第二步:删除旧文件(如果存在) + try: + sftp.stat(remote_path) + sftp.remove(remote_path) + except: + pass # 文件不存在,无需删除 + + # 第三步:重命名临时文件为目标文件 + sftp.rename(temp_remote_path, remote_path) + + # 关闭连接 + sftp.close() + transport.close() + + self.progress.emit(100, f'上传完成!') + self.finished.emit(True, f'文件 {filename} 上传成功!') + + except Exception as e: + self.finished.emit(False, f'上传失败: {str(e)}') + + +class UploadWindow(QMainWindow): + def __init__(self): + super().__init__() + self.config = self.load_config() + self.sftp_config = None + self.remote_dir = '/' # 默认上传目录 + self.upload_queue = [] # 上传队列 + self.is_uploading = False # 是否正在上传 + self.initUI() + self.get_sftp_config() + + def load_config(self): + """加载配置文件""" + try: + # PyInstaller打包后使用sys._MEIPASS + if getattr(sys, 'frozen', False): + # 打包后的exe + base_path = os.path.dirname(sys.executable) + else: + # 开发环境 + base_path = os.path.dirname(__file__) + + config_path = os.path.join(base_path, 'config.json') + + if not os.path.exists(config_path): + from PyQt5.QtWidgets import QMessageBox + QMessageBox.critical(None, '错误', f'找不到配置文件: {config_path}\n\n请确保config.json与程序在同一目录下!') + sys.exit(1) + + with open(config_path, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + from PyQt5.QtWidgets import QMessageBox + QMessageBox.critical(None, '错误', f'加载配置失败:\n{str(e)}') + sys.exit(1) + + def get_sftp_config(self): + """从服务器获取SFTP配置""" + try: + response = requests.post( + f"{self.config['api_base_url']}/api/upload/get-config", + json={'api_key': self.config['api_key']}, + timeout=10 + ) + + if response.status_code == 200: + data = response.json() + if data['success']: + self.sftp_config = data['sftp_config'] + # 自动测试并设置上传目录 + self.test_and_set_upload_directory() + else: + self.show_error(data.get('message', '获取配置失败')) + else: + self.show_error(f'服务器错误: {response.status_code}') + + except Exception as e: + self.show_error(f'无法连接到服务器: {str(e)}') + + def test_and_set_upload_directory(self): + """测试并设置上传目录""" + if not self.sftp_config: + return + + self.log('开始测试上传目录...') + self.status_label.setText( + f'

玩玩云上传工具 v2.0

' + f'

正在测试上传目录...

' + ) + + # 按优先级测试目录 + self.test_dirs = ['/', '/upload', '/uploads', '/files', '/home', '/tmp'] + self.current_test_index = 0 + + self.test_next_directory() + + def test_next_directory(self): + """测试下一个目录""" + if self.current_test_index >= len(self.test_dirs): + self.log('✗ 所有目录都无法写入,请检查SFTP权限') + self.show_error('无法找到可写入的目录,请检查SFTP权限') + return + + test_dir = self.test_dirs[self.current_test_index] + self.log(f'测试目录: {test_dir}') + + self.test_thread = TestDirectoryThread(test_dir, self.sftp_config) + self.test_thread.result.connect(self.on_test_result) + self.test_thread.start() + + def on_test_result(self, success, message): + """处理测试结果""" + if success: + self.remote_dir = message + self.log(f'✓ 已设置上传目录为: {message}') + self.status_label.setText( + f'

玩玩云上传工具 v2.0

' + f'

✓ 已连接 - 用户: {self.config["username"]}

' + f'

拖拽文件/文件夹到此处上传

' + f'

上传目录: {self.remote_dir}

' + ) + else: + self.log(f'✗ 目录 {self.test_dirs[self.current_test_index]} 不可写: {message}') + self.current_test_index += 1 + self.test_next_directory() + + def show_error(self, message): + """显示错误信息""" + self.status_label.setText( + f'

玩玩云上传工具 v2.0

' + f'

✗ 错误: {message}

' + f'

请检查网络连接或联系管理员

' + ) + + def initUI(self): + """初始化界面""" + self.setWindowTitle('玩玩云上传工具 v2.0') + self.setGeometry(300, 300, 500, 450) + + # 设置接受拖拽 + self.setAcceptDrops(True) + + # 中心部件 + central_widget = QWidget() + self.setCentralWidget(central_widget) + + # 布局 + layout = QVBoxLayout() + + # 状态标签 + self.status_label = QLabel('正在连接服务器...') + self.status_label.setAlignment(Qt.AlignCenter) + self.status_label.setFont(QFont('Arial', 11)) + self.status_label.setWordWrap(True) + self.status_label.setStyleSheet('padding: 20px;') + layout.addWidget(self.status_label) + + # 拖拽提示区域 + self.drop_area = QLabel('📁\n\n支持多文件和文件夹') + self.drop_area.setAlignment(Qt.AlignCenter) + self.drop_area.setStyleSheet(""" + QLabel { + font-size: 50px; + color: #667eea; + border: 3px dashed #667eea; + border-radius: 10px; + background-color: #f5f7fa; + padding: 40px; + } + """) + layout.addWidget(self.drop_area) + + # 队列状态标签 + self.queue_label = QLabel('队列: 0 个文件等待上传') + self.queue_label.setAlignment(Qt.AlignCenter) + self.queue_label.setStyleSheet('color: #2c3e50; font-weight: bold; padding: 5px;') + layout.addWidget(self.queue_label) + + # 进度条 + self.progress_bar = QProgressBar() + self.progress_bar.setValue(0) + self.progress_bar.setVisible(False) + self.progress_bar.setStyleSheet(""" + QProgressBar { + border: 2px solid #e0e0e0; + border-radius: 5px; + text-align: center; + height: 25px; + } + QProgressBar::chunk { + background-color: #667eea; + } + """) + layout.addWidget(self.progress_bar) + + # 进度信息 + self.progress_label = QLabel('') + self.progress_label.setAlignment(Qt.AlignCenter) + self.progress_label.setVisible(False) + layout.addWidget(self.progress_label) + + # 日志区域 + self.log_text = QTextEdit() + self.log_text.setReadOnly(True) + self.log_text.setMaximumHeight(100) + self.log_text.setStyleSheet(""" + QTextEdit { + background-color: #f9f9f9; + border: 1px solid #e0e0e0; + border-radius: 5px; + padding: 5px; + font-family: 'Courier New', monospace; + font-size: 11px; + } + """) + layout.addWidget(self.log_text) + + central_widget.setLayout(layout) + + self.log('程序已启动 - 版本 v2.0') + + def log(self, message): + """添加日志""" + self.log_text.append(f'[{self.get_time()}] {message}') + # 自动滚动到底部 + self.log_text.verticalScrollBar().setValue( + self.log_text.verticalScrollBar().maximum() + ) + + def get_time(self): + """获取当前时间""" + from datetime import datetime + return datetime.now().strftime('%H:%M:%S') + + def dragEnterEvent(self, event: QDragEnterEvent): + """拖拽进入事件""" + if event.mimeData().hasUrls(): + event.acceptProposedAction() + self.drop_area.setStyleSheet(""" + QLabel { + font-size: 50px; + color: #667eea; + border: 3px dashed #667eea; + border-radius: 10px; + background-color: #e8ecf7; + padding: 40px; + } + """) + + def dragLeaveEvent(self, event): + """拖拽离开事件""" + self.drop_area.setStyleSheet(""" + QLabel { + font-size: 50px; + color: #667eea; + border: 3px dashed #667eea; + border-radius: 10px; + background-color: #f5f7fa; + padding: 40px; + } + """) + + def dropEvent(self, event: QDropEvent): + """拖拽放下事件""" + self.drop_area.setStyleSheet(""" + QLabel { + font-size: 50px; + color: #667eea; + border: 3px dashed #667eea; + border-radius: 10px; + background-color: #f5f7fa; + padding: 40px; + } + """) + + if not self.sftp_config: + self.log('错误: 未获取到SFTP配置') + return + + paths = [url.toLocalFile() for url in event.mimeData().urls()] + + all_files = [] + for path in paths: + if os.path.isfile(path): + all_files.append(path) + elif os.path.isdir(path): + self.log(f'扫描文件夹: {os.path.basename(path)}') + folder_files = self.scan_folder(path) + all_files.extend(folder_files) + self.log(f'找到 {len(folder_files)} 个文件') + + if all_files: + self.upload_queue.extend(all_files) + self.update_queue_label() + self.log(f'添加 {len(all_files)} 个文件到上传队列') + + if not self.is_uploading: + self.process_upload_queue() + + def scan_folder(self, folder_path): + """递归扫描文件夹""" + files = [] + try: + for root, dirs, filenames in os.walk(folder_path): + for filename in filenames: + file_path = os.path.join(root, filename) + files.append(file_path) + except Exception as e: + self.log(f'扫描文件夹失败: {str(e)}') + + return files + + def update_queue_label(self): + """更新队列标签""" + count = len(self.upload_queue) + self.queue_label.setText(f'队列: {count} 个文件等待上传') + + def process_upload_queue(self): + """处理上传队列""" + if not self.upload_queue: + self.is_uploading = False + self.update_queue_label() + self.log('✓ 所有文件上传完成!') + return + + self.is_uploading = True + file_path = self.upload_queue.pop(0) + self.update_queue_label() + + self.upload_file(file_path) + + def upload_file(self, file_path): + """上传文件""" + self.log(f'开始上传: {os.path.basename(file_path)}') + + # 显示进度控件 + self.progress_bar.setVisible(True) + self.progress_bar.setValue(0) + self.progress_label.setVisible(True) + self.progress_label.setText('准备上传...') + + # 创建上传线程 + self.upload_thread = UploadThread(self.sftp_config, file_path, self.remote_dir) + self.upload_thread.progress.connect(self.on_progress) + self.upload_thread.finished.connect(self.on_finished) + self.upload_thread.start() + + def on_progress(self, value, message): + """上传进度更新""" + self.progress_bar.setValue(value) + self.progress_label.setText(message) + + def on_finished(self, success, message): + """上传完成""" + self.log(message) + + if success: + self.progress_label.setText('✓ ' + message) + self.progress_label.setStyleSheet('color: green; font-weight: bold;') + else: + self.progress_label.setText('✗ ' + message) + self.progress_label.setStyleSheet('color: red; font-weight: bold;') + + # 继续处理队列 + from PyQt5.QtCore import QTimer + QTimer.singleShot(1000, self.process_upload_queue) + + +def main(): + app = QApplication(sys.argv) + window = UploadWindow() + window.show() + sys.exit(app.exec_()) + + +if __name__ == '__main__': + main()