diff --git a/backend/auth.js b/backend/auth.js index ce1edf8..e41ffb2 100644 --- a/backend/auth.js +++ b/backend/auth.js @@ -2,6 +2,8 @@ const jwt = require('jsonwebtoken'); const crypto = require('crypto'); const { UserDB } = require('./database'); const { decryptSecret } = require('./utils/encryption'); +const DEFAULT_LOCAL_STORAGE_QUOTA_BYTES = 1024 * 1024 * 1024; // 1GB +const DEFAULT_OSS_STORAGE_QUOTA_BYTES = 1024 * 1024 * 1024; // 1GB // JWT密钥(必须在环境变量中设置) const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'; @@ -169,6 +171,11 @@ function authMiddleware(req, res, next) { }); } + const rawOssQuota = Number(user.oss_storage_quota); + const effectiveOssQuota = Number.isFinite(rawOssQuota) && rawOssQuota > 0 + ? rawOssQuota + : DEFAULT_OSS_STORAGE_QUOTA_BYTES; + // 将用户信息附加到请求对象(包含所有存储相关字段) req.user = { id: user.id, @@ -187,8 +194,10 @@ function authMiddleware(req, res, next) { // 存储相关字段 storage_permission: user.storage_permission || 'oss_only', current_storage_type: user.current_storage_type || 'oss', - local_storage_quota: user.local_storage_quota || 1073741824, + local_storage_quota: user.local_storage_quota || DEFAULT_LOCAL_STORAGE_QUOTA_BYTES, local_storage_used: user.local_storage_used || 0, + oss_storage_quota: effectiveOssQuota, + storage_used: user.storage_used || 0, // 主题偏好 theme_preference: user.theme_preference || null }; diff --git a/backend/database.js b/backend/database.js index 1348b54..54f22f8 100644 --- a/backend/database.js +++ b/backend/database.js @@ -37,6 +37,8 @@ console.log(`[数据库] 路径: ${dbPath}`); // 创建或连接数据库 const db = new Database(dbPath); +const DEFAULT_LOCAL_STORAGE_QUOTA_BYTES = 1024 * 1024 * 1024; // 1GB +const DEFAULT_OSS_STORAGE_QUOTA_BYTES = 1024 * 1024 * 1024; // 1GB // ===== 性能优化配置(P0 优先级修复) ===== @@ -525,7 +527,8 @@ const UserDB = { 'has_oss_config': 'number', 'is_verified': 'number', 'local_storage_quota': 'number', - 'local_storage_used': 'number' + 'local_storage_used': 'number', + 'oss_storage_quota': 'number' }; const expectedType = FIELD_TYPES[fieldName]; @@ -585,6 +588,7 @@ const UserDB = { 'current_storage_type': 'current_storage_type', 'local_storage_quota': 'local_storage_quota', 'local_storage_used': 'local_storage_used', + 'oss_storage_quota': 'oss_storage_quota', // 偏好设置 'theme_preference': 'theme_preference' @@ -665,6 +669,7 @@ const UserDB = { 'current_storage_type': 'current_storage_type', 'local_storage_quota': 'local_storage_quota', 'local_storage_used': 'local_storage_used', + 'oss_storage_quota': 'oss_storage_quota', // 偏好设置 'theme_preference': 'theme_preference' @@ -717,7 +722,8 @@ const UserDB = { 'current_storage_type': 'string', 'theme_preference': 'string', 'is_admin': 'number', 'is_active': 'number', 'is_banned': 'number', 'has_oss_config': 'number', 'is_verified': 'number', - 'local_storage_quota': 'number', 'local_storage_used': 'number' + 'local_storage_quota': 'number', 'local_storage_used': 'number', + 'oss_storage_quota': 'number' }[key]; console.warn(`[类型检查] 字段 ${key} 值类型不符: 期望 ${expectedType}, 实际 ${typeof value}, 值: ${JSON.stringify(value)}`); @@ -883,11 +889,14 @@ const ShareDB = { // 根据分享码查找 // 增强: 检查分享者是否被封禁(被封禁用户的分享不可访问) // ===== 性能优化(P0 优先级修复):只查询必要字段,避免 N+1 查询 ===== - // 移除了敏感字段:oss_access_key_id, oss_access_key_secret(不需要传递给分享访问者) + // 不返回 oss_access_key_id / oss_access_key_secret; + // share_password 仅用于服务端验证,不会透传给前端。 findByCode(shareCode) { const result = db.prepare(` SELECT s.id, s.user_id, s.share_code, s.share_path, s.share_type, + s.storage_type, + s.share_password, s.view_count, s.download_count, s.created_at, s.expires_at, u.username, -- OSS 配置(访问分享文件所需) @@ -1172,8 +1181,9 @@ function migrateToV2() { 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_quota INTEGER DEFAULT ${DEFAULT_LOCAL_STORAGE_QUOTA_BYTES}; ALTER TABLE users ADD COLUMN local_storage_used INTEGER DEFAULT 0; + ALTER TABLE users ADD COLUMN oss_storage_quota INTEGER DEFAULT ${DEFAULT_OSS_STORAGE_QUOTA_BYTES}; `); console.log('[数据库迁移] ✓ 用户表已升级'); @@ -1247,6 +1257,34 @@ function migrateToOss() { } } + +// 数据库迁移 - OSS 存储配额字段 +function migrateOssQuotaField() { + try { + const columns = db.prepare("PRAGMA table_info(users)").all(); + const hasOssStorageQuota = columns.some(col => col.name === 'oss_storage_quota'); + + if (!hasOssStorageQuota) { + console.log('[数据库迁移] 添加 oss_storage_quota 字段...'); + db.exec(`ALTER TABLE users ADD COLUMN oss_storage_quota INTEGER DEFAULT ${DEFAULT_OSS_STORAGE_QUOTA_BYTES}`); + console.log('[数据库迁移] ✓ oss_storage_quota 字段已添加'); + } + + // 统一策略:未配置或无效值默认 1GB + const backfillResult = db.prepare(` + UPDATE users + SET oss_storage_quota = ? + WHERE oss_storage_quota IS NULL OR oss_storage_quota <= 0 + `).run(DEFAULT_OSS_STORAGE_QUOTA_BYTES); + + if (backfillResult.changes > 0) { + console.log(`[数据库迁移] ✓ OSS 配额默认值已回填: ${backfillResult.changes} 条记录`); + } + } catch (error) { + console.error('[数据库迁移] oss_storage_quota 迁移失败:', error); + } +} + // 系统日志操作 const SystemLogDB = { // 日志级别常量 @@ -1432,6 +1470,7 @@ initDefaultSettings(); migrateToV2(); // 执行数据库迁移 migrateThemePreference(); // 主题偏好迁移 migrateToOss(); // SFTP → OSS 迁移 +migrateOssQuotaField(); // OSS 配额字段迁移 module.exports = { db, diff --git a/backend/package-lock.json b/backend/package-lock.json index 5d63484..afec4cf 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,8 +9,8 @@ "version": "3.1.0", "license": "MIT", "dependencies": { - "@aws-sdk/client-s3": "^3.600.0", - "@aws-sdk/s3-request-presigner": "^3.600.0", + "@aws-sdk/client-s3": "^3.985.0", + "@aws-sdk/s3-request-presigner": "^3.985.0", "archiver": "^7.0.1", "bcryptjs": "^3.0.3", "better-sqlite3": "^11.8.1", @@ -21,8 +21,9 @@ "express-session": "^1.18.2", "express-validator": "^7.3.0", "jsonwebtoken": "^9.0.2", + "lodash": "^4.17.23", "multer": "^2.0.2", - "nodemailer": "^6.9.14", + "nodemailer": "^8.0.1", "svg-captcha": "^1.4.0" }, "devDependencies": { @@ -232,34 +233,34 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.971.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.971.0.tgz", - "integrity": "sha512-BBUne390fKa4C4QvZlUZ5gKcu+Uyid4IyQ20N4jl0vS7SK2xpfXlJcgKqPW5ts6kx6hWTQBk6sH5Lf12RvuJxg==", + "version": "3.985.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.985.0.tgz", + "integrity": "sha512-S9TqjzzZEEIKBnC7yFpvqM7CG9ALpY5qhQ5BnDBJtdG20NoGpjKLGUUfD2wmZItuhbrcM4Z8c6m6Fg0XYIOVvw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.970.0", - "@aws-sdk/credential-provider-node": "3.971.0", - "@aws-sdk/middleware-bucket-endpoint": "3.969.0", - "@aws-sdk/middleware-expect-continue": "3.969.0", - "@aws-sdk/middleware-flexible-checksums": "3.971.0", - "@aws-sdk/middleware-host-header": "3.969.0", - "@aws-sdk/middleware-location-constraint": "3.969.0", - "@aws-sdk/middleware-logger": "3.969.0", - "@aws-sdk/middleware-recursion-detection": "3.969.0", - "@aws-sdk/middleware-sdk-s3": "3.970.0", - "@aws-sdk/middleware-ssec": "3.971.0", - "@aws-sdk/middleware-user-agent": "3.970.0", - "@aws-sdk/region-config-resolver": "3.969.0", - "@aws-sdk/signature-v4-multi-region": "3.970.0", - "@aws-sdk/types": "3.969.0", - "@aws-sdk/util-endpoints": "3.970.0", - "@aws-sdk/util-user-agent-browser": "3.969.0", - "@aws-sdk/util-user-agent-node": "3.971.0", + "@aws-sdk/core": "^3.973.7", + "@aws-sdk/credential-provider-node": "^3.972.6", + "@aws-sdk/middleware-bucket-endpoint": "^3.972.3", + "@aws-sdk/middleware-expect-continue": "^3.972.3", + "@aws-sdk/middleware-flexible-checksums": "^3.972.5", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-location-constraint": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-sdk-s3": "^3.972.7", + "@aws-sdk/middleware-ssec": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.7", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/signature-v4-multi-region": "3.985.0", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.985.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.5", "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.20.6", + "@smithy/core": "^3.22.1", "@smithy/eventstream-serde-browser": "^4.2.8", "@smithy/eventstream-serde-config-resolver": "^4.3.8", "@smithy/eventstream-serde-node": "^4.2.8", @@ -270,25 +271,25 @@ "@smithy/invalid-dependency": "^4.2.8", "@smithy/md5-js": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.7", - "@smithy/middleware-retry": "^4.4.23", + "@smithy/middleware-endpoint": "^4.4.13", + "@smithy/middleware-retry": "^4.4.30", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", + "@smithy/node-http-handler": "^4.4.9", "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.10.8", + "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.22", - "@smithy/util-defaults-mode-node": "^4.2.25", + "@smithy/util-defaults-mode-browser": "^4.3.29", + "@smithy/util-defaults-mode-node": "^4.2.32", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", - "@smithy/util-stream": "^4.5.10", + "@smithy/util-stream": "^4.5.11", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.8", "tslib": "^2.6.2" @@ -298,44 +299,44 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.971.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.971.0.tgz", - "integrity": "sha512-Xx+w6DQqJxDdymYyIxyKJnRzPvVJ4e/Aw0czO7aC9L/iraaV7AG8QtRe93OGW6aoHSh72CIiinnpJJfLsQqP4g==", + "version": "3.985.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.985.0.tgz", + "integrity": "sha512-81J8iE8MuXhdbMfIz4sWFj64Pe41bFi/uqqmqOC5SlGv+kwoyLsyKS/rH2tW2t5buih4vTUxskRjxlqikTD4oQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.970.0", - "@aws-sdk/middleware-host-header": "3.969.0", - "@aws-sdk/middleware-logger": "3.969.0", - "@aws-sdk/middleware-recursion-detection": "3.969.0", - "@aws-sdk/middleware-user-agent": "3.970.0", - "@aws-sdk/region-config-resolver": "3.969.0", - "@aws-sdk/types": "3.969.0", - "@aws-sdk/util-endpoints": "3.970.0", - "@aws-sdk/util-user-agent-browser": "3.969.0", - "@aws-sdk/util-user-agent-node": "3.971.0", + "@aws-sdk/core": "^3.973.7", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.7", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.985.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.5", "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.20.6", + "@smithy/core": "^3.22.1", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.7", - "@smithy/middleware-retry": "^4.4.23", + "@smithy/middleware-endpoint": "^4.4.13", + "@smithy/middleware-retry": "^4.4.30", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", + "@smithy/node-http-handler": "^4.4.9", "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.10.8", + "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.22", - "@smithy/util-defaults-mode-node": "^4.2.25", + "@smithy/util-defaults-mode-browser": "^4.3.29", + "@smithy/util-defaults-mode-node": "^4.2.32", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", @@ -347,19 +348,19 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.970.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.970.0.tgz", - "integrity": "sha512-klpzObldOq8HXzDjDlY6K8rMhYZU6mXRz6P9F9N+tWnjoYFfeBMra8wYApydElTUYQKP1O7RLHwH1OKFfKcqIA==", + "version": "3.973.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.7.tgz", + "integrity": "sha512-wNZZQQNlJ+hzD49cKdo+PY6rsTDElO8yDImnrI69p2PLBa7QomeUKAJWYp9xnaR38nlHqWhMHZuYLCQ3oSX+xg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.969.0", - "@aws-sdk/xml-builder": "3.969.0", - "@smithy/core": "^3.20.6", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/xml-builder": "^3.972.4", + "@smithy/core": "^3.22.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", - "@smithy/smithy-client": "^4.10.8", + "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", @@ -371,9 +372,9 @@ } }, "node_modules/@aws-sdk/crc64-nvme": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.969.0.tgz", - "integrity": "sha512-IGNkP54HD3uuLnrPCYsv3ZD478UYq+9WwKrIVJ9Pdi3hxPg8562CH3ZHf8hEgfePN31P9Kj+Zu9kq2Qcjjt61A==", + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.0.tgz", + "integrity": "sha512-ThlLhTqX68jvoIVv+pryOdb5coP1cX1/MaTbB9xkGDCbWbsqQcLqzPxuSoW1DCnAAIacmXCWpzUNOB9pv+xXQw==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", @@ -384,13 +385,13 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.970.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.970.0.tgz", - "integrity": "sha512-rtVzXzEtAfZBfh+lq3DAvRar4c3jyptweOAJR2DweyXx71QSMY+O879hjpMwES7jl07a3O1zlnFIDo4KP/96kQ==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.5.tgz", + "integrity": "sha512-LxJ9PEO4gKPXzkufvIESUysykPIdrV7+Ocb9yAhbhJLE4TiAYqbCVUE+VuKP1leGR1bBfjWjYgSV5MxprlX3mQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.970.0", - "@aws-sdk/types": "3.969.0", + "@aws-sdk/core": "^3.973.7", + "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" @@ -400,20 +401,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.970.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.970.0.tgz", - "integrity": "sha512-CjDbWL7JxjLc9ZxQilMusWSw05yRvUJKRpz59IxDpWUnSMHC9JMMUUkOy5Izk8UAtzi6gupRWArp4NG4labt9Q==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.7.tgz", + "integrity": "sha512-L2uOGtvp2x3bTcxFTpSM+GkwFIPd8pHfGWO1764icMbo7e5xJh0nfhx1UwkXLnwvocTNEf8A7jISZLYjUSNaTg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.970.0", - "@aws-sdk/types": "3.969.0", + "@aws-sdk/core": "^3.973.7", + "@aws-sdk/types": "^3.973.1", "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/node-http-handler": "^4.4.8", + "@smithy/node-http-handler": "^4.4.9", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.10.8", + "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", - "@smithy/util-stream": "^4.5.10", + "@smithy/util-stream": "^4.5.11", "tslib": "^2.6.2" }, "engines": { @@ -421,20 +422,20 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.971.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.971.0.tgz", - "integrity": "sha512-c0TGJG4xyfTZz3SInXfGU8i5iOFRrLmy4Bo7lMyH+IpngohYMYGYl61omXqf2zdwMbDv+YJ9AviQTcCaEUKi8w==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.5.tgz", + "integrity": "sha512-SdDTYE6jkARzOeL7+kudMIM4DaFnP5dZVeatzw849k4bSXDdErDS188bgeNzc/RA2WGrlEpsqHUKP6G7sVXhZg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.970.0", - "@aws-sdk/credential-provider-env": "3.970.0", - "@aws-sdk/credential-provider-http": "3.970.0", - "@aws-sdk/credential-provider-login": "3.971.0", - "@aws-sdk/credential-provider-process": "3.970.0", - "@aws-sdk/credential-provider-sso": "3.971.0", - "@aws-sdk/credential-provider-web-identity": "3.971.0", - "@aws-sdk/nested-clients": "3.971.0", - "@aws-sdk/types": "3.969.0", + "@aws-sdk/core": "^3.973.7", + "@aws-sdk/credential-provider-env": "^3.972.5", + "@aws-sdk/credential-provider-http": "^3.972.7", + "@aws-sdk/credential-provider-login": "^3.972.5", + "@aws-sdk/credential-provider-process": "^3.972.5", + "@aws-sdk/credential-provider-sso": "^3.972.5", + "@aws-sdk/credential-provider-web-identity": "^3.972.5", + "@aws-sdk/nested-clients": "3.985.0", + "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -446,14 +447,14 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.971.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.971.0.tgz", - "integrity": "sha512-yhbzmDOsk0RXD3rTPhZra4AWVnVAC4nFWbTp+sUty1hrOPurUmhuz8bjpLqYTHGnlMbJp+UqkQONhS2+2LzW2g==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.5.tgz", + "integrity": "sha512-uYq1ILyTSI6ZDCMY5+vUsRM0SOCVI7kaW4wBrehVVkhAxC6y+e9rvGtnoZqCOWL1gKjTMouvsf4Ilhc5NCg1Aw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.970.0", - "@aws-sdk/nested-clients": "3.971.0", - "@aws-sdk/types": "3.969.0", + "@aws-sdk/core": "^3.973.7", + "@aws-sdk/nested-clients": "3.985.0", + "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -465,18 +466,18 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.971.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.971.0.tgz", - "integrity": "sha512-epUJBAKivtJqalnEBRsYIULKYV063o/5mXNJshZfyvkAgNIzc27CmmKRXTN4zaNOZg8g/UprFp25BGsi19x3nQ==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.6.tgz", + "integrity": "sha512-DZ3CnAAtSVtVz+G+ogqecaErMLgzph4JH5nYbHoBMgBkwTUV+SUcjsjOJwdBJTHu3Dm6l5LBYekZoU2nDqQk2A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.970.0", - "@aws-sdk/credential-provider-http": "3.970.0", - "@aws-sdk/credential-provider-ini": "3.971.0", - "@aws-sdk/credential-provider-process": "3.970.0", - "@aws-sdk/credential-provider-sso": "3.971.0", - "@aws-sdk/credential-provider-web-identity": "3.971.0", - "@aws-sdk/types": "3.969.0", + "@aws-sdk/credential-provider-env": "^3.972.5", + "@aws-sdk/credential-provider-http": "^3.972.7", + "@aws-sdk/credential-provider-ini": "^3.972.5", + "@aws-sdk/credential-provider-process": "^3.972.5", + "@aws-sdk/credential-provider-sso": "^3.972.5", + "@aws-sdk/credential-provider-web-identity": "^3.972.5", + "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -488,13 +489,13 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.970.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.970.0.tgz", - "integrity": "sha512-0XeT8OaT9iMA62DFV9+m6mZfJhrD0WNKf4IvsIpj2Z7XbaYfz3CoDDvNoALf3rPY9NzyMHgDxOspmqdvXP00mw==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.5.tgz", + "integrity": "sha512-HDKF3mVbLnuqGg6dMnzBf1VUOywE12/N286msI9YaK9mEIzdsGCtLTvrDhe3Up0R9/hGFbB+9l21/TwF5L1C6g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.970.0", - "@aws-sdk/types": "3.969.0", + "@aws-sdk/core": "^3.973.7", + "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", @@ -505,15 +506,15 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.971.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.971.0.tgz", - "integrity": "sha512-dY0hMQ7dLVPQNJ8GyqXADxa9w5wNfmukgQniLxGVn+dMRx3YLViMp5ZpTSQpFhCWNF0oKQrYAI5cHhUJU1hETw==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.5.tgz", + "integrity": "sha512-8urj3AoeNeQisjMmMBhFeiY2gxt6/7wQQbEGun0YV/OaOOiXrIudTIEYF8ZfD+NQI6X1FY5AkRsx6O/CaGiybA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.971.0", - "@aws-sdk/core": "3.970.0", - "@aws-sdk/token-providers": "3.971.0", - "@aws-sdk/types": "3.969.0", + "@aws-sdk/client-sso": "3.985.0", + "@aws-sdk/core": "^3.973.7", + "@aws-sdk/token-providers": "3.985.0", + "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", @@ -524,14 +525,14 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.971.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.971.0.tgz", - "integrity": "sha512-F1AwfNLr7H52T640LNON/h34YDiMuIqW/ZreGzhRR6vnFGaSPtNSKAKB2ssAMkLM8EVg8MjEAYD3NCUiEo+t/w==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.5.tgz", + "integrity": "sha512-OK3cULuJl6c+RcDZfPpaK5o3deTOnKZbxm7pzhFNGA3fI2hF9yDih17fGRazJzGGWaDVlR9ejZrpDef4DJCEsw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.970.0", - "@aws-sdk/nested-clients": "3.971.0", - "@aws-sdk/types": "3.969.0", + "@aws-sdk/core": "^3.973.7", + "@aws-sdk/nested-clients": "3.985.0", + "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", @@ -542,13 +543,13 @@ } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.969.0.tgz", - "integrity": "sha512-MlbrlixtkTVhYhoasblKOkr7n2yydvUZjjxTnBhIuHmkyBS1619oGnTfq/uLeGYb4NYXdeQ5OYcqsRGvmWSuTw==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.3.tgz", + "integrity": "sha512-fmbgWYirF67YF1GfD7cg5N6HHQ96EyRNx/rDIrTF277/zTWVuPI2qS/ZHgofwR1NZPe/NWvoppflQY01LrbVLg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.969.0", - "@aws-sdk/util-arn-parser": "3.968.0", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-arn-parser": "^3.972.2", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", @@ -560,12 +561,12 @@ } }, "node_modules/@aws-sdk/middleware-expect-continue": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.969.0.tgz", - "integrity": "sha512-qXygzSi8osok7tH9oeuS3HoKw6jRfbvg5Me/X5RlHOvSSqQz8c5O9f3MjUApaCUSwbAU92KrbZWasw2PKiaVHg==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.3.tgz", + "integrity": "sha512-4msC33RZsXQpUKR5QR4HnvBSNCPLGHmB55oDiROqqgyOc+TOfVu2xgi5goA7ms6MdZLeEh2905UfWMnMMF4mRg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.969.0", + "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" @@ -575,23 +576,23 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.971.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.971.0.tgz", - "integrity": "sha512-+hGUDUxeIw8s2kkjfeXym0XZxdh0cqkHkDpEanWYdS1gnWkIR+gf9u/DKbKqGHXILPaqHXhWpLTQTVlaB4sI7Q==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.972.5.tgz", + "integrity": "sha512-SF/1MYWx67OyCrLA4icIpWUfCkdlOi8Y1KecQ9xYxkL10GMjVdPTGPnYhAg0dw5U43Y9PVUWhAV2ezOaG+0BLg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "3.970.0", - "@aws-sdk/crc64-nvme": "3.969.0", - "@aws-sdk/types": "3.969.0", + "@aws-sdk/core": "^3.973.7", + "@aws-sdk/crc64-nvme": "3.972.0", + "@aws-sdk/types": "^3.973.1", "@smithy/is-array-buffer": "^4.2.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", - "@smithy/util-stream": "^4.5.10", + "@smithy/util-stream": "^4.5.11", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -600,12 +601,12 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.969.0.tgz", - "integrity": "sha512-AWa4rVsAfBR4xqm7pybQ8sUNJYnjyP/bJjfAw34qPuh3M9XrfGbAHG0aiAfQGrBnmS28jlO6Kz69o+c6PRw1dw==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.3.tgz", + "integrity": "sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.969.0", + "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" @@ -615,12 +616,12 @@ } }, "node_modules/@aws-sdk/middleware-location-constraint": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.969.0.tgz", - "integrity": "sha512-zH7pDfMLG/C4GWMOpvJEoYcSpj7XsNP9+irlgqwi667sUQ6doHQJ3yyDut3yiTk0maq1VgmriPFELyI9lrvH/g==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.3.tgz", + "integrity": "sha512-nIg64CVrsXp67vbK0U1/Is8rik3huS3QkRHn2DRDx4NldrEFMgdkZGI/+cZMKD9k4YOS110Dfu21KZLHrFA/1g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.969.0", + "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, @@ -629,12 +630,12 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.969.0.tgz", - "integrity": "sha512-xwrxfip7Y2iTtCMJ+iifN1E1XMOuhxIHY9DreMCvgdl4r7+48x2S1bCYPWH3eNY85/7CapBWdJ8cerpEl12sQQ==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.3.tgz", + "integrity": "sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.969.0", + "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, @@ -643,12 +644,12 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.969.0.tgz", - "integrity": "sha512-2r3PuNquU3CcS1Am4vn/KHFwLi8QFjMdA/R+CRDXT4AFO/0qxevF/YStW3gAKntQIgWgQV8ZdEtKAoJvLI4UWg==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.3.tgz", + "integrity": "sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.969.0", + "@aws-sdk/types": "^3.973.1", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", @@ -659,23 +660,23 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.970.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.970.0.tgz", - "integrity": "sha512-v/Y5F1lbFFY7vMeG5yYxuhnn0CAshz6KMxkz1pDyPxejNE9HtA0w8R6OTBh/bVdIm44QpjhbI7qeLdOE/PLzXQ==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.7.tgz", + "integrity": "sha512-VtZ7tMIw18VzjG+I6D6rh2eLkJfTtByiFoCIauGDtTTPBEUMQUiGaJ/zZrPlCY6BsvLLeFKz3+E5mntgiOWmIg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.970.0", - "@aws-sdk/types": "3.969.0", - "@aws-sdk/util-arn-parser": "3.968.0", - "@smithy/core": "^3.20.6", + "@aws-sdk/core": "^3.973.7", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-arn-parser": "^3.972.2", + "@smithy/core": "^3.22.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", - "@smithy/smithy-client": "^4.10.8", + "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-middleware": "^4.2.8", - "@smithy/util-stream": "^4.5.10", + "@smithy/util-stream": "^4.5.11", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -684,12 +685,12 @@ } }, "node_modules/@aws-sdk/middleware-ssec": { - "version": "3.971.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.971.0.tgz", - "integrity": "sha512-QGVhvRveYG64ZhnS/b971PxXM6N2NU79Fxck4EfQ7am8v1Br0ctoeDDAn9nXNblLGw87we9Z65F7hMxxiFHd3w==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.3.tgz", + "integrity": "sha512-dU6kDuULN3o3jEHcjm0c4zWJlY1zWVkjG9NPe9qxYLLpcbdj5kRYBS2DdWYD+1B9f910DezRuws7xDEqKkHQIg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.969.0", + "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, @@ -698,15 +699,15 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.970.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.970.0.tgz", - "integrity": "sha512-dnSJGGUGSFGEX2NzvjwSefH+hmZQ347AwbLhAsi0cdnISSge+pcGfOFrJt2XfBIypwFe27chQhlfuf/gWdzpZg==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.7.tgz", + "integrity": "sha512-HUD+geASjXSCyL/DHPQc/Ua7JhldTcIglVAoCV8kiVm99IaFSlAbTvEnyhZwdE6bdFyTL+uIaWLaCFSRsglZBQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.970.0", - "@aws-sdk/types": "3.969.0", - "@aws-sdk/util-endpoints": "3.970.0", - "@smithy/core": "^3.20.6", + "@aws-sdk/core": "^3.973.7", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.985.0", + "@smithy/core": "^3.22.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" @@ -716,44 +717,44 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.971.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.971.0.tgz", - "integrity": "sha512-TWaILL8GyYlhGrxxnmbkazM4QsXatwQgoWUvo251FXmUOsiXDFDVX3hoGIfB3CaJhV2pJPfebHUNJtY6TjZ11g==", + "version": "3.985.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.985.0.tgz", + "integrity": "sha512-TsWwKzb/2WHafAY0CE7uXgLj0FmnkBTgfioG9HO+7z/zCPcl1+YU+i7dW4o0y+aFxFgxTMG+ExBQpqT/k2ao8g==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.970.0", - "@aws-sdk/middleware-host-header": "3.969.0", - "@aws-sdk/middleware-logger": "3.969.0", - "@aws-sdk/middleware-recursion-detection": "3.969.0", - "@aws-sdk/middleware-user-agent": "3.970.0", - "@aws-sdk/region-config-resolver": "3.969.0", - "@aws-sdk/types": "3.969.0", - "@aws-sdk/util-endpoints": "3.970.0", - "@aws-sdk/util-user-agent-browser": "3.969.0", - "@aws-sdk/util-user-agent-node": "3.971.0", + "@aws-sdk/core": "^3.973.7", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.7", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.985.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.5", "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.20.6", + "@smithy/core": "^3.22.1", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.7", - "@smithy/middleware-retry": "^4.4.23", + "@smithy/middleware-endpoint": "^4.4.13", + "@smithy/middleware-retry": "^4.4.30", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", + "@smithy/node-http-handler": "^4.4.9", "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.10.8", + "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.22", - "@smithy/util-defaults-mode-node": "^4.2.25", + "@smithy/util-defaults-mode-browser": "^4.3.29", + "@smithy/util-defaults-mode-node": "^4.2.32", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", @@ -765,12 +766,12 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.969.0.tgz", - "integrity": "sha512-scj9OXqKpcjJ4jsFLtqYWz3IaNvNOQTFFvEY8XMJXTv+3qF5I7/x9SJtKzTRJEBF3spjzBUYPtGFbs9sj4fisQ==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.3.tgz", + "integrity": "sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.969.0", + "@aws-sdk/types": "^3.973.1", "@smithy/config-resolver": "^4.4.6", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", @@ -781,17 +782,17 @@ } }, "node_modules/@aws-sdk/s3-request-presigner": { - "version": "3.971.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.971.0.tgz", - "integrity": "sha512-j4wCCoQ//xm03JQn7/Jq6BJ0HV3VzlI/HrIQSQupWWjZTrdxyqa9PXBhcYNNtvZtF1adA/cRpYTMS+2SUsZGRg==", + "version": "3.985.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.985.0.tgz", + "integrity": "sha512-lPnf977GFM4cMLJ7X+ThktKMe/0CXIfX+wz1z+sUT7yagPL2IRyiNUPFZ0VTEGBo1gRhHEDPWy6yzk8WWRFsvg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/signature-v4-multi-region": "3.970.0", - "@aws-sdk/types": "3.969.0", - "@aws-sdk/util-format-url": "3.969.0", - "@smithy/middleware-endpoint": "^4.4.7", + "@aws-sdk/signature-v4-multi-region": "3.985.0", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-format-url": "^3.972.3", + "@smithy/middleware-endpoint": "^4.4.13", "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.10.8", + "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, @@ -800,13 +801,13 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.970.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.970.0.tgz", - "integrity": "sha512-z3syXfuK/x/IsKf/AeYmgc2NT7fcJ+3fHaGO+fkghkV9WEba3fPyOwtTBX4KpFMNb2t50zDGZwbzW1/5ighcUQ==", + "version": "3.985.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.985.0.tgz", + "integrity": "sha512-W6hTSOPiSbh4IdTYVxN7xHjpCh0qvfQU1GKGBzGQm0ZEIOaMmWqiDEvFfyGYKmfBvumT8vHKxQRTX0av9omtIg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.970.0", - "@aws-sdk/types": "3.969.0", + "@aws-sdk/middleware-sdk-s3": "^3.972.7", + "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/types": "^4.12.0", @@ -817,14 +818,14 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.971.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.971.0.tgz", - "integrity": "sha512-4hKGWZbmuDdONMJV0HJ+9jwTDb0zLfKxcCLx2GEnBY31Gt9GeyIQ+DZ97Bb++0voawj6pnZToFikXTyrEq2x+w==", + "version": "3.985.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.985.0.tgz", + "integrity": "sha512-+hwpHZyEq8k+9JL2PkE60V93v2kNhUIv7STFt+EAez1UJsJOQDhc5LpzEX66pNjclI5OTwBROs/DhJjC/BtMjQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.970.0", - "@aws-sdk/nested-clients": "3.971.0", - "@aws-sdk/types": "3.969.0", + "@aws-sdk/core": "^3.973.7", + "@aws-sdk/nested-clients": "3.985.0", + "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", @@ -835,9 +836,9 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.969.0.tgz", - "integrity": "sha512-7IIzM5TdiXn+VtgPdVLjmE6uUBUtnga0f4RiSEI1WW10RPuNvZ9U+pL3SwDiRDAdoGrOF9tSLJOFZmfuwYuVYQ==", + "version": "3.973.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.1.tgz", + "integrity": "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", @@ -848,9 +849,9 @@ } }, "node_modules/@aws-sdk/util-arn-parser": { - "version": "3.968.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.968.0.tgz", - "integrity": "sha512-gqqvYcitIIM2K4lrDX9de9YvOfXBcVdxfT/iLnvHJd4YHvSXlt+gs+AsL4FfPCxG4IG9A+FyulP9Sb1MEA75vw==", + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.2.tgz", + "integrity": "sha512-VkykWbqMjlSgBFDyrY3nOSqupMc6ivXuGmvci6Q3NnLq5kC+mKQe2QBZ4nrWRE/jqOxeFP2uYzLtwncYYcvQDg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -860,12 +861,12 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.970.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.970.0.tgz", - "integrity": "sha512-TZNZqFcMUtjvhZoZRtpEGQAdULYiy6rcGiXAbLU7e9LSpIYlRqpLa207oMNfgbzlL2PnHko+eVg8rajDiSOYCg==", + "version": "3.985.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.985.0.tgz", + "integrity": "sha512-vth7UfGSUR3ljvaq8V4Rc62FsM7GUTH/myxPWkaEgOrprz1/Pc72EgTXxj+cPPPDAfHFIpjhkB7T7Td0RJx+BA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.969.0", + "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", @@ -876,12 +877,12 @@ } }, "node_modules/@aws-sdk/util-format-url": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.969.0.tgz", - "integrity": "sha512-C7ZiE8orcrEF9In+XDlIKrZhMjp0HCPUH6u74pgadE3T2LRre5TmOQcTt785/wVS2G0we9cxkjlzMrfDsfPvFw==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.3.tgz", + "integrity": "sha512-n7F2ycckcKFXa01vAsT/SJdjFHfKH9s96QHcs5gn8AaaigASICeME8WdUL9uBp8XV/OVwEt8+6gzn6KFUgQa8g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.969.0", + "@aws-sdk/types": "^3.973.1", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" @@ -903,25 +904,25 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.969.0.tgz", - "integrity": "sha512-bpJGjuKmFr0rA6UKUCmN8D19HQFMLXMx5hKBXqBlPFdalMhxJSjcxzX9DbQh0Fn6bJtxCguFmRGOBdQqNOt49g==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.3.tgz", + "integrity": "sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.969.0", + "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.971.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.971.0.tgz", - "integrity": "sha512-Eygjo9mFzQYjbGY3MYO6CsIhnTwAMd3WmuFalCykqEmj2r5zf0leWrhPaqvA5P68V5JdGfPYgj7vhNOd6CtRBQ==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.5.tgz", + "integrity": "sha512-GsUDF+rXyxDZkkJxUsDxnA67FG+kc5W1dnloCFLl6fWzceevsCYzJpASBzT+BPjwUgREE6FngfJYYYMQUY5fZQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.970.0", - "@aws-sdk/types": "3.969.0", + "@aws-sdk/middleware-user-agent": "^3.972.7", + "@aws-sdk/types": "^3.973.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" @@ -939,13 +940,13 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.969.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.969.0.tgz", - "integrity": "sha512-BSe4Lx/qdRQQdX8cSSI7Et20vqBspzAjBy8ZmXVoyLkol3y4sXBXzn+BiLtR+oh60ExQn6o2DU4QjdOZbXaKIQ==", + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.4.tgz", + "integrity": "sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", - "fast-xml-parser": "5.2.5", + "fast-xml-parser": "5.3.4", "tslib": "^2.6.2" }, "engines": { @@ -1044,9 +1045,9 @@ } }, "node_modules/@smithy/core": { - "version": "3.20.7", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.20.7.tgz", - "integrity": "sha512-aO7jmh3CtrmPsIJxUwYIzI5WVlMK8BMCPQ4D4nTzqTqBhbzvxHNzBMGcEg13yg/z9R2Qsz49NUFl0F0lVbTVFw==", + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.22.1.tgz", + "integrity": "sha512-x3ie6Crr58MWrm4viHqqy2Du2rHYZjwu8BekasrQx4ca+Y24dzVAwq3yErdqIbc2G3I0kLQA13PQ+/rde+u65g==", "license": "Apache-2.0", "dependencies": { "@smithy/middleware-serde": "^4.2.9", @@ -1055,7 +1056,7 @@ "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.8", - "@smithy/util-stream": "^4.5.10", + "@smithy/util-stream": "^4.5.11", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" @@ -1264,12 +1265,12 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.8.tgz", - "integrity": "sha512-TV44qwB/T0OMMzjIuI+JeS0ort3bvlPJ8XIH0MSlGADraXpZqmyND27ueuAL3E14optleADWqtd7dUgc2w+qhQ==", + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.13.tgz", + "integrity": "sha512-x6vn0PjYmGdNuKh/juUJJewZh7MoQ46jYaJ2mvekF4EesMuFfrl4LaW/k97Zjf8PTCPQmPgMvwewg7eNoH9n5w==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.20.7", + "@smithy/core": "^3.22.1", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -1283,15 +1284,15 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.24", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.24.tgz", - "integrity": "sha512-yiUY1UvnbUFfP5izoKLtfxDSTRv724YRRwyiC/5HYY6vdsVDcDOXKSXmkJl/Hovcxt5r+8tZEUAdrOaCJwrl9Q==", + "version": "4.4.30", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.30.tgz", + "integrity": "sha512-CBGyFvN0f8hlnqKH/jckRDz78Snrp345+PVk8Ux7pnkUCW97Iinse59lY78hBt04h1GZ6hjBN94BRwZy1xC8Bg==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/service-error-classification": "^4.2.8", - "@smithy/smithy-client": "^4.10.9", + "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", @@ -1345,9 +1346,9 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.8.tgz", - "integrity": "sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg==", + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.9.tgz", + "integrity": "sha512-KX5Wml5mF+luxm1szW4QDz32e3NObgJ4Fyw+irhph4I/2geXwUy4jkIMUs5ZPGflRBeR6BUkC2wqIab4Llgm3w==", "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.2.8", @@ -1458,17 +1459,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.10.9", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.9.tgz", - "integrity": "sha512-Je0EvGXVJ0Vrrr2lsubq43JGRIluJ/hX17aN/W/A0WfE+JpoMdI8kwk2t9F0zTX9232sJDGcoH4zZre6m6f/sg==", + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.11.2.tgz", + "integrity": "sha512-SCkGmFak/xC1n7hKRsUr6wOnBTJ3L22Qd4e8H1fQIuKTAjntwgU8lrdMe7uHdiT2mJAOWA/60qaW9tiMu69n1A==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.20.7", - "@smithy/middleware-endpoint": "^4.4.8", + "@smithy/core": "^3.22.1", + "@smithy/middleware-endpoint": "^4.4.13", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", - "@smithy/util-stream": "^4.5.10", + "@smithy/util-stream": "^4.5.11", "tslib": "^2.6.2" }, "engines": { @@ -1565,13 +1566,13 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.23", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.23.tgz", - "integrity": "sha512-mMg+r/qDfjfF/0psMbV4zd7F/i+rpyp7Hjh0Wry7eY15UnzTEId+xmQTGDU8IdZtDfbGQxuWNfgBZKBj+WuYbA==", + "version": "4.3.29", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.29.tgz", + "integrity": "sha512-nIGy3DNRmOjaYaaKcQDzmWsro9uxlaqUOhZDHQed9MW/GmkBZPtnU70Pu1+GT9IBmUXwRdDuiyaeiy9Xtpn3+Q==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.2.8", - "@smithy/smithy-client": "^4.10.9", + "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, @@ -1580,16 +1581,16 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.26", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.26.tgz", - "integrity": "sha512-EQqe/WkbCinah0h1lMWh9ICl0Ob4lyl20/10WTB35SC9vDQfD8zWsOT+x2FIOXKAoZQ8z/y0EFMoodbcqWJY/w==", + "version": "4.2.32", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.32.tgz", + "integrity": "sha512-7dtFff6pu5fsjqrVve0YMhrnzJtccCWDacNKOkiZjJ++fmjGExmmSu341x+WU6Oc1IccL7lDuaUj7SfrHpWc5Q==", "license": "Apache-2.0", "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", - "@smithy/smithy-client": "^4.10.9", + "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, @@ -1651,13 +1652,13 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.10", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.10.tgz", - "integrity": "sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g==", + "version": "4.5.11", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.11.tgz", + "integrity": "sha512-lKmZ0S/3Qj2OF5H1+VzvDLb6kRxGzZHq6f3rAsoSu5cTLGsn3v3VQBA8czkNNXlLjoFEtVu3OQT2jEeOtOE2CA==", "license": "Apache-2.0", "dependencies": { "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/node-http-handler": "^4.4.8", + "@smithy/node-http-handler": "^4.4.9", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", @@ -1983,9 +1984,9 @@ } }, "node_modules/bowser": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz", - "integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", "license": "MIT" }, "node_modules/brace-expansion": { @@ -2602,9 +2603,9 @@ "license": "MIT" }, "node_modules/fast-xml-parser": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", - "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz", + "integrity": "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==", "funding": [ { "type": "github", @@ -3114,9 +3115,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/lodash.includes": { @@ -3351,9 +3352,9 @@ } }, "node_modules/nodemailer": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", - "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz", + "integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==", "license": "MIT-0", "engines": { "node": ">=6.0.0" diff --git a/backend/package.json b/backend/package.json index 917b632..a156713 100644 --- a/backend/package.json +++ b/backend/package.json @@ -18,6 +18,8 @@ "author": "玩玩云团队", "license": "MIT", "dependencies": { + "@aws-sdk/client-s3": "^3.985.0", + "@aws-sdk/s3-request-presigner": "^3.985.0", "archiver": "^7.0.1", "bcryptjs": "^3.0.3", "better-sqlite3": "^11.8.1", @@ -28,10 +30,9 @@ "express-session": "^1.18.2", "express-validator": "^7.3.0", "jsonwebtoken": "^9.0.2", + "lodash": "^4.17.23", "multer": "^2.0.2", - "nodemailer": "^6.9.14", - "@aws-sdk/client-s3": "^3.600.0", - "@aws-sdk/s3-request-presigner": "^3.600.0", + "nodemailer": "^8.0.1", "svg-captcha": "^1.4.0" }, "devDependencies": { diff --git a/backend/routes/index.js b/backend/routes/index.js index d70d588..e572cba 100644 --- a/backend/routes/index.js +++ b/backend/routes/index.js @@ -54,7 +54,7 @@ * - POST /api/share/:code/verify * - POST /api/share/:code/list * - POST /api/share/:code/download - * - GET /api/share/:code/download-url + * - POST /api/share/:code/download-url * - GET /api/share/:code/download-file * * 6. routes/admin.js - 管理员功能 diff --git a/backend/server.js b/backend/server.js index b165455..c6ce666 100644 --- a/backend/server.js +++ b/backend/server.js @@ -64,7 +64,7 @@ function clearOssUsageCache(userId) { const { db, UserDB, ShareDB, SettingsDB, VerificationDB, PasswordResetTokenDB, SystemLogDB, TransactionDB, WalManager } = require('./database'); const StorageUsageCache = require('./utils/storage-cache'); -const { generateToken, generateRefreshToken, refreshAccessToken, authMiddleware, adminMiddleware, requirePasswordConfirmation, isJwtSecretSecure } = require('./auth'); +const { JWT_SECRET, generateToken, generateRefreshToken, refreshAccessToken, authMiddleware, adminMiddleware, isJwtSecretSecure } = require('./auth'); const { StorageInterface, LocalStorageClient, OssStorageClient, formatFileSize, formatOssError } = require('./storage'); const { encryptSecret, decryptSecret } = require('./utils/encryption'); @@ -72,6 +72,8 @@ const app = express(); const PORT = process.env.PORT || 40001; const USERNAME_REGEX = /^[A-Za-z0-9_.\u4e00-\u9fa5-]{3,20}$/u; // 允许中英文、数字、下划线、点和短横线 const ENFORCE_HTTPS = process.env.ENFORCE_HTTPS === 'true'; +const DEFAULT_LOCAL_STORAGE_QUOTA_BYTES = 1024 * 1024 * 1024; // 1GB +const DEFAULT_OSS_STORAGE_QUOTA_BYTES = 1024 * 1024 * 1024; // 1GB // ===== 安全配置:公开域名白名单(防止 Host Header 注入) ===== // 必须配置 PUBLIC_BASE_URL 环境变量,用于生成邮件链接和分享链接 @@ -225,14 +227,13 @@ function csrfProtection(req, res, next) { return next(); } - // 白名单:某些公开 API 不需要 CSRF 保护(如分享页面的密码验证) - const csrfExemptPaths = [ - '/api/share/', // 分享相关的公开接口 - '/api/captcha', // 验证码 - '/api/health' // 健康检查 - ]; + // 仅对基于 Cookie 的浏览器会话启用 CSRF(Bearer API 客户端不强制) + const hasCookieAuth = !!( + req.cookies?.token || + req.cookies?.refreshToken + ); - if (csrfExemptPaths.some(path => req.path.startsWith(path))) { + if (!hasCookieAuth) { return next(); } @@ -250,11 +251,16 @@ function csrfProtection(req, res, next) { next(); } -// 注意:CSRF 保护将在 authMiddleware 后的路由中按需启用 -// 可以通过环境变量 ENABLE_CSRF=true 开启(默认关闭以保持向后兼容) -const ENABLE_CSRF = process.env.ENABLE_CSRF === 'true'; +// CSRF 开关策略: +// - 显式配置 ENABLE_CSRF 时按配置值 +// - 未配置时,生产环境默认开启 +const ENABLE_CSRF = process.env.ENABLE_CSRF !== undefined + ? process.env.ENABLE_CSRF === 'true' + : process.env.NODE_ENV === 'production'; + if (ENABLE_CSRF) { console.log('[安全] CSRF 保护已启用'); + app.use(csrfProtection); } // 强制HTTPS(可通过环境变量控制,默认关闭以兼容本地环境) @@ -303,7 +309,7 @@ if (DEFAULT_SESSION_SECRETS.includes(SESSION_SECRET)) { app.use(session({ secret: SESSION_SECRET, resave: false, - saveUninitialized: true, // 改为true,确保验证码请求时创建session + saveUninitialized: false, // 仅在写入 session 时创建,减少无效会话 name: 'captcha.sid', // 自定义session cookie名称 cookie: { secure: isSecureCookie, @@ -555,6 +561,125 @@ function getProtocol(req) { return req.protocol || (req.secure ? 'https' : 'http'); } +function normalizeOssQuota(rawQuota) { + const parsedQuota = Number(rawQuota); + if (!Number.isFinite(parsedQuota) || parsedQuota <= 0) { + return DEFAULT_OSS_STORAGE_QUOTA_BYTES; + } + return parsedQuota; +} + +// 构建用于存储客户端的用户对象(自动尝试解密 OSS Secret) +function buildStorageUserContext(user, overrides = {}) { + if (!user) { + return user; + } + + const storageUser = { + ...user, + ...overrides + }; + + if (typeof storageUser.oss_access_key_secret === 'string' && storageUser.oss_access_key_secret) { + try { + storageUser.oss_access_key_secret = decryptSecret(storageUser.oss_access_key_secret); + } catch { + // 已是明文或不是加密串,保持原值 + } + } + + return storageUser; +} + +// 为指定用户创建 OSS 客户端(兼容加密存储的密钥) +function createOssClientForUser(user, overrides = {}) { + const storageUser = buildStorageUserContext(user, { + current_storage_type: 'oss', + ...overrides + }); + return new OssStorageClient(storageUser); +} + +// 为指定用户创建可直接用于签名的 S3 客户端上下文 +function createS3ClientContextForUser(user, overrides = {}) { + const { S3Client } = require('@aws-sdk/client-s3'); + const ossClient = createOssClientForUser(user, overrides); + const effectiveConfig = ossClient.getEffectiveConfig(); + + // 修复:未 connect() 时 getBucket() 会回退到用户个人 bucket, + // 在启用统一 OSS 配置时可能拿错 bucket(如误用 user_2)。 + // 这里显式绑定当前有效配置,确保签名下载与上传使用同一 bucket。 + ossClient.currentConfig = effectiveConfig; + + return { + client: new S3Client(ossClient.buildConfig(effectiveConfig)), + bucket: effectiveConfig.oss_bucket, + ossClient + }; +} + +const EPHEMERAL_TOKEN_SECRET = JWT_SECRET; + +function signEphemeralToken(payload, expiresInSeconds = 900) { + const tokenPayload = { + ...payload, + exp: Math.floor(Date.now() / 1000) + expiresInSeconds + }; + + const encodedPayload = Buffer.from(JSON.stringify(tokenPayload)).toString('base64url'); + const signature = crypto + .createHmac('sha256', EPHEMERAL_TOKEN_SECRET) + .update(encodedPayload) + .digest('base64url'); + + return `${encodedPayload}.${signature}`; +} + +function verifyEphemeralToken(token, expectedType) { + if (!token || typeof token !== 'string') { + return { valid: false, error: 'token_missing' }; + } + + const parts = token.split('.'); + if (parts.length !== 2 || !parts[0] || !parts[1]) { + return { valid: false, error: 'token_format_invalid' }; + } + + const [encodedPayload, receivedSignature] = parts; + const expectedSignature = crypto + .createHmac('sha256', EPHEMERAL_TOKEN_SECRET) + .update(encodedPayload) + .digest('base64url'); + + const receivedBuffer = Buffer.from(receivedSignature); + const expectedBuffer = Buffer.from(expectedSignature); + if ( + receivedBuffer.length !== expectedBuffer.length || + !crypto.timingSafeEqual(receivedBuffer, expectedBuffer) + ) { + return { valid: false, error: 'token_signature_invalid' }; + } + + try { + const payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8')); + if (!payload || typeof payload !== 'object') { + return { valid: false, error: 'token_payload_invalid' }; + } + + if (expectedType && payload.type !== expectedType) { + return { valid: false, error: 'token_type_mismatch' }; + } + + if (!payload.exp || Math.floor(Date.now() / 1000) > payload.exp) { + return { valid: false, error: 'token_expired' }; + } + + return { valid: true, payload }; + } catch { + return { valid: false, error: 'token_parse_failed' }; + } +} + // ===== 系统日志工具函数 ===== // 从请求中提取日志信息 @@ -1895,8 +2020,10 @@ app.post('/api/login', // 存储相关字段 storage_permission: user.storage_permission || 'oss_only', current_storage_type: user.current_storage_type || 'oss', - local_storage_quota: user.local_storage_quota || 1073741824, + local_storage_quota: user.local_storage_quota || DEFAULT_LOCAL_STORAGE_QUOTA_BYTES, local_storage_used: user.local_storage_used || 0, + oss_storage_quota: normalizeOssQuota(user.oss_storage_quota), + storage_used: user.storage_used || 0, // OSS配置来源(重要:用于前端判断是否使用OSS直连上传) oss_config_source: SettingsDB.hasUnifiedOssConfig() ? 'unified' : (user.has_oss_config ? 'personal' : 'none') } @@ -2215,7 +2342,10 @@ app.get('/api/user/oss-usage', authMiddleware, async (req, res) => { // ===== P0 优化:优先使用数据库缓存 ===== // 从数据库 storage_used 字段读取(上传/删除时增量更新) const user = UserDB.findById(req.user.id); - const storageUsed = user.storage_used || 0; + const storageUsed = Number(user.storage_used || 0); + const ossQuota = normalizeOssQuota(user.oss_storage_quota); + const usagePercentage = Math.min(100, Math.round((storageUsed / ossQuota) * 100)); + const remainingSize = Math.max(ossQuota - storageUsed, 0); return res.json({ success: true, @@ -2223,7 +2353,13 @@ app.get('/api/user/oss-usage', authMiddleware, async (req, res) => { totalSize: storageUsed, totalSizeFormatted: formatFileSize(storageUsed), fileCount: null, // 缓存模式不提供文件数 - cached: true + cached: true, + quota: ossQuota, + quotaFormatted: formatFileSize(ossQuota), + quotaLimited: true, + remainingSize, + remainingSizeFormatted: formatFileSize(remainingSize), + usagePercentage }, cached: true }); @@ -2240,6 +2376,8 @@ app.get('/api/user/oss-usage', authMiddleware, async (req, res) => { // 获取 OSS 存储空间详细统计(全量统计,仅限管理员或需要精确统计时使用) // ===== P0 性能优化:此接口较慢,建议只在必要时调用 ===== app.get('/api/user/oss-usage-full', authMiddleware, async (req, res) => { + let ossClient; + try { // 检查是否有可用的OSS配置(个人配置或系统级统一配置) const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig(); @@ -2261,7 +2399,7 @@ app.get('/api/user/oss-usage-full', authMiddleware, async (req, res) => { } // 执行全量统计(较慢,仅在缓存未命中时执行) - const ossClient = new OssStorageClient(req.user); + ossClient = createOssClientForUser(req.user); await ossClient.connect(); let totalSize = 0; @@ -2271,7 +2409,7 @@ app.get('/api/user/oss-usage-full', authMiddleware, async (req, res) => { do { const { ListObjectsV2Command } = require('@aws-sdk/client-s3'); const command = new ListObjectsV2Command({ - Bucket: req.user.oss_bucket, + Bucket: ossClient.getBucket(), Prefix: `user_${req.user.id}/`, ContinuationToken: continuationToken }); @@ -2288,8 +2426,6 @@ app.get('/api/user/oss-usage-full', authMiddleware, async (req, res) => { continuationToken = response.NextContinuationToken; } while (continuationToken); - await ossClient.end(); - const usageData = { totalSize, totalSizeFormatted: formatFileSize(totalSize), @@ -2312,6 +2448,10 @@ app.get('/api/user/oss-usage-full', authMiddleware, async (req, res) => { success: false, message: '获取OSS空间使用情况失败: ' + error.message }); + } finally { + if (ossClient) { + await ossClient.end(); + } } }); @@ -2443,6 +2583,54 @@ app.post('/api/user/change-password', } ); +// 更新当前用户资料(目前支持邮箱) +app.post('/api/user/update-profile', + authMiddleware, + [ + body('email').isEmail().withMessage('邮箱格式不正确') + ], + (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + errors: errors.array() + }); + } + + try { + const { email } = req.body; + + const existingUser = UserDB.findByEmail(email); + if (existingUser && existingUser.id !== req.user.id) { + return res.status(400).json({ + success: false, + message: '邮箱已被使用' + }); + } + + UserDB.update(req.user.id, { email }); + + const updatedUser = UserDB.findById(req.user.id); + res.json({ + success: true, + message: '资料更新成功', + user: { + id: updatedUser.id, + username: updatedUser.username, + email: updatedUser.email, + is_admin: updatedUser.is_admin + } + }); + } catch (error) { + res.status(500).json({ + success: false, + message: getSafeErrorMessage(error, '更新资料失败,请稍后重试', '更新资料失败') + }); + } + } +); + // 修改当前用户名 app.post('/api/user/update-username', authMiddleware, @@ -2841,7 +3029,7 @@ app.post('/api/files/folder-info', authMiddleware, async (req, res) => { do { const command = new ListObjectsV2Command({ - Bucket: req.user.oss_bucket, + Bucket: storage.getBucket(), Prefix: prefix, ContinuationToken: continuationToken }); @@ -2986,6 +3174,7 @@ app.get('/api/files/upload-signature', authMiddleware, async (req, res) => { const filename = req.query.filename; const uploadPath = req.query.path || '/'; // 上传目标路径 const contentType = req.query.contentType || 'application/octet-stream'; + const fileSize = Number(req.query.size); if (!filename) { return res.status(400).json({ @@ -3019,6 +3208,22 @@ app.get('/api/files/upload-signature', authMiddleware, async (req, res) => { }); } + // 文件大小参数校验(用于 OSS 配额校验) + if (!Number.isFinite(fileSize) || fileSize <= 0) { + return res.status(400).json({ + success: false, + message: '缺少或无效的文件大小参数' + }); + } + + const maxUploadSize = parseInt(SettingsDB.get('max_upload_size') || '10737418240', 10); + if (Number.isFinite(maxUploadSize) && maxUploadSize > 0 && fileSize > maxUploadSize) { + return res.status(400).json({ + success: false, + message: `文件过大,最大允许 ${formatFileSize(maxUploadSize)}` + }); + } + // 路径安全验证:防止目录遍历攻击 if (uploadPath.includes('..') || uploadPath.includes('\x00')) { return res.status(400).json({ @@ -3037,15 +3242,10 @@ app.get('/api/files/upload-signature', authMiddleware, async (req, res) => { } try { - const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3'); + const { PutObjectCommand, HeadObjectCommand } = require('@aws-sdk/client-s3'); const { getSignedUrl } = require('@aws-sdk/s3-request-presigner'); - // 获取有效的 OSS 配置(系统配置优先) - const unifiedConfig = SettingsDB.getUnifiedOssConfig(); - const effectiveBucket = unifiedConfig ? unifiedConfig.bucket : req.user.oss_bucket; - - // 构建 S3 客户端 - const client = new S3Client(buildS3Config(req.user)); + const { client, bucket } = createS3ClientContextForUser(req.user); // 构建对象 Key(与 OssStorageClient.getObjectKey 格式一致) // 格式:user_${id}/${path}/${filename} @@ -3066,9 +3266,49 @@ app.get('/api/files/upload-signature', authMiddleware, async (req, res) => { objectKey = `user_${req.user.id}/${normalizedPath}/${sanitizedFilename}`; } + let previousSize = 0; + try { + const headResponse = await client.send(new HeadObjectCommand({ + Bucket: bucket, + Key: objectKey + })); + previousSize = Number(headResponse.ContentLength || 0); + if (!Number.isFinite(previousSize) || previousSize < 0) { + previousSize = 0; + } + } catch (headError) { + const statusCode = headError?.$metadata?.httpStatusCode; + if (headError?.name !== 'NotFound' && headError?.name !== 'NoSuchKey' && statusCode !== 404) { + throw headError; + } + } + + // OSS 配额校验(未配置时默认 1GB) + const latestUser = UserDB.findById(req.user.id); + const ossQuota = normalizeOssQuota(latestUser?.oss_storage_quota); + const currentUsage = Number(latestUser?.storage_used || 0); + const baseUsage = Math.max(0, currentUsage - previousSize); + const projectedUsage = baseUsage + fileSize; + + if (projectedUsage > ossQuota) { + const remainingBytes = Math.max(ossQuota - baseUsage, 0); + return res.status(400).json({ + success: false, + message: `OSS 配额不足:文件 ${formatFileSize(fileSize)},剩余 ${formatFileSize(remainingBytes)}(总配额 ${formatFileSize(ossQuota)})` + }); + } + + const completionToken = signEphemeralToken({ + type: 'upload_complete', + userId: req.user.id, + objectKey, + previousSize, + expectedSize: fileSize + }, 30 * 60); + // 创建 PutObject 命令 const command = new PutObjectCommand({ - Bucket: effectiveBucket, + Bucket: bucket, Key: objectKey, ContentType: contentType }); @@ -3080,6 +3320,8 @@ app.get('/api/files/upload-signature', authMiddleware, async (req, res) => { success: true, uploadUrl: signedUrl, objectKey: objectKey, + previousSize, + completionToken, expiresIn: 900 }); } catch (error) { @@ -3094,22 +3336,39 @@ app.get('/api/files/upload-signature', authMiddleware, async (req, res) => { // OSS 上传完成通知(用于更新缓存和数据库) // ===== P0 性能优化:使用增量更新替代全量统计 ===== app.post('/api/files/upload-complete', authMiddleware, async (req, res) => { - const { objectKey, size, path } = req.body; + const { objectKey, size, completionToken } = req.body; - if (!objectKey) { + const normalizedObjectKey = typeof objectKey === 'string' + ? objectKey.replace(/\\/g, '/').replace(/^\/+/, '') + : ''; + const reportedSize = Number(size); + + if (!normalizedObjectKey) { return res.status(400).json({ success: false, message: '缺少对象Key参数' }); } - if (!size || size < 0) { + if (!Number.isFinite(reportedSize) || reportedSize < 0) { return res.status(400).json({ success: false, message: '文件大小参数无效' }); } + const expectedPrefix = `user_${req.user.id}/`; + if ( + !normalizedObjectKey.startsWith(expectedPrefix) || + normalizedObjectKey.includes('..') || + normalizedObjectKey.includes('\x00') + ) { + return res.status(403).json({ + success: false, + message: '对象Key不属于当前用户或格式非法' + }); + } + // 安全检查:验证用户是否配置了OSS(个人配置或系统级统一配置) const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig(); if (!req.user.has_oss_config && !hasUnifiedConfig) { @@ -3119,17 +3378,94 @@ app.post('/api/files/upload-complete', authMiddleware, async (req, res) => { }); } + const completionTokenResult = verifyEphemeralToken(completionToken, 'upload_complete'); + if (!completionTokenResult.valid) { + return res.status(403).json({ + success: false, + message: '上传完成凭证无效或已过期' + }); + } + + const completionPayload = completionTokenResult.payload || {}; + if ( + Number(completionPayload.userId) !== Number(req.user.id) || + completionPayload.objectKey !== normalizedObjectKey + ) { + return res.status(403).json({ + success: false, + message: '上传完成凭证与对象不匹配' + }); + } + + const previousObjectSize = Number.isFinite(Number(completionPayload.previousSize)) && Number(completionPayload.previousSize) >= 0 + ? Number(completionPayload.previousSize) + : 0; + + let ossClient; + try { - // 更新存储使用量缓存(增量更新) - await StorageUsageCache.updateUsage(req.user.id, size); + const { HeadObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3'); + + ossClient = createOssClientForUser(req.user); + await ossClient.connect(); + + const headResponse = await ossClient.s3Client.send(new HeadObjectCommand({ + Bucket: ossClient.getBucket(), + Key: normalizedObjectKey + })); + + const verifiedSize = Number(headResponse.ContentLength || 0); + + if (!Number.isFinite(verifiedSize) || verifiedSize < 0) { + throw new Error('无法确认上传对象大小'); + } + + if (verifiedSize !== reportedSize) { + console.warn(`[上传完成] 用户 ${req.user.id} 上报大小(${reportedSize})与实际大小(${verifiedSize})不一致,已使用实际大小`); + } + + const deltaSize = verifiedSize - previousObjectSize; + + // 二次校验 OSS 配额(防止签名后并发上传导致超限) + const latestUser = UserDB.findById(req.user.id); + const ossQuota = normalizeOssQuota(latestUser?.oss_storage_quota); + const currentUsage = Number(latestUser?.storage_used || 0); + const projectedUsage = Math.max(0, currentUsage + deltaSize); + + if (projectedUsage > ossQuota) { + // 回滚:删除刚上传的对象,避免超配额文件残留 + await ossClient.s3Client.send(new DeleteObjectCommand({ + Bucket: ossClient.getBucket(), + Key: normalizedObjectKey + })); + + // 若为覆盖上传,旧对象已被替换,删除后需扣除旧对象体积 + if (previousObjectSize > 0) { + await StorageUsageCache.updateUsage(req.user.id, -previousObjectSize); + } + + clearOssUsageCache(req.user.id); + + return res.status(400).json({ + success: false, + message: `OSS 配额不足:已用 ${formatFileSize(currentUsage)},配额 ${formatFileSize(ossQuota)},上传后将超限` + }); + } + + // 更新存储使用量缓存(增量更新,覆盖上传只记录差值) + if (deltaSize !== 0) { + await StorageUsageCache.updateUsage(req.user.id, deltaSize); + } // 同时更新旧的内存缓存(保持兼容性) clearOssUsageCache(req.user.id); - console.log(`[上传完成] 用户 ${req.user.id} 文件大小: ${size} 字节`); + console.log(`[上传完成] 用户 ${req.user.id} 文件大小: ${verifiedSize} 字节, 增量: ${deltaSize} 字节`); res.json({ success: true, - message: '上传完成已记录' + message: '上传完成已记录', + recordedSize: verifiedSize, + deltaSize }); } catch (error) { console.error('[OSS上传] 记录上传完成失败:', error); @@ -3137,6 +3473,10 @@ app.post('/api/files/upload-complete', authMiddleware, async (req, res) => { success: false, message: '记录上传完成失败: ' + error.message }); + } finally { + if (ossClient) { + await ossClient.end(); + } } }); @@ -3170,23 +3510,15 @@ app.get('/api/files/download-url', authMiddleware, async (req, res) => { } try { - const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3'); + const { GetObjectCommand } = require('@aws-sdk/client-s3'); const { getSignedUrl } = require('@aws-sdk/s3-request-presigner'); - // 获取有效的 OSS 配置(系统配置优先) - const unifiedConfig = SettingsDB.getUnifiedOssConfig(); - const effectiveBucket = unifiedConfig ? unifiedConfig.bucket : req.user.oss_bucket; - - // 构建 S3 客户端 - const client = new S3Client(buildS3Config(req.user)); - - // 构建对象 Key(复用 OssStorageClient 的 getObjectKey 方法,确保路径格式正确) - const tempClient = new OssStorageClient(req.user); - const objectKey = tempClient.getObjectKey(normalizedPath); + const { client, bucket, ossClient } = createS3ClientContextForUser(req.user); + const objectKey = ossClient.getObjectKey(normalizedPath); // 创建 GetObject 命令 const command = new GetObjectCommand({ - Bucket: effectiveBucket, + Bucket: bucket, Key: objectKey, ResponseContentDisposition: `attachment; filename="${encodeURIComponent(filePath.split('/').pop())}"` }); @@ -3208,15 +3540,6 @@ app.get('/api/files/download-url', authMiddleware, async (req, res) => { } }); -// 辅助函数:构建 S3 配置(复用 OssStorageClient.buildConfig) -function buildS3Config(user) { - // 创建临时 OssStorageClient 实例并复用其 buildConfig 方法 - // OssStorageClient 已在文件顶部导入 - const tempClient = new OssStorageClient(user); - const config = tempClient.getEffectiveConfig(); // 先获取有效配置(系统配置优先) - return tempClient.buildConfig(config); // 然后传递给buildConfig -} - // 辅助函数:清理文件名(增强版安全处理) function sanitizeFilename(filename) { if (!filename || typeof filename !== 'string') { @@ -3742,12 +4065,34 @@ app.post('/api/share/create', authMiddleware, (req, res) => { } } - // 参数验证:密码长度限制 - if (password && (typeof password !== 'string' || password.length > 32)) { - return res.status(400).json({ - success: false, - message: '密码长度不能超过32个字符' - }); + const hasPasswordField = Object.prototype.hasOwnProperty.call(req.body || {}, 'password'); + let normalizedPassword = null; + + if (hasPasswordField) { + if (password !== null && password !== undefined && typeof password !== 'string') { + return res.status(400).json({ + success: false, + message: '密码格式无效' + }); + } + + if (typeof password === 'string') { + normalizedPassword = password.trim(); + } + + if (password !== null && password !== undefined && !normalizedPassword) { + return res.status(400).json({ + success: false, + message: '已启用密码保护时,访问密码不能为空' + }); + } + + if (normalizedPassword && normalizedPassword.length > 32) { + return res.status(400).json({ + success: false, + message: '密码长度不能超过32个字符' + }); + } } // 路径安全验证:防止路径遍历攻击 @@ -3770,7 +4115,7 @@ app.post('/api/share/create', authMiddleware, (req, res) => { share_type: actualShareType, file_path: file_path || '', file_name: file_name || '', - password: password || null, + password: normalizedPassword || null, expiry_days: expiry_days || null }); @@ -3783,7 +4128,7 @@ app.post('/api/share/create', authMiddleware, (req, res) => { // 记录分享创建日志 logShare(req, 'create_share', `用户创建分享: ${actualShareType === 'file' ? '文件' : '目录'} ${file_path}`, - { shareCode: result.share_code, sharePath: file_path, shareType: actualShareType, hasPassword: !!password } + { shareCode: result.share_code, sharePath: file_path, shareType: actualShareType, hasPassword: !!normalizedPassword } ); res.json({ @@ -3793,6 +4138,7 @@ app.post('/api/share/create', authMiddleware, (req, res) => { share_url: shareUrl, share_type: result.share_type, expires_at: result.expires_at, + has_password: !!normalizedPassword }); } catch (error) { console.error('创建分享链接失败:', error); @@ -3810,10 +4156,16 @@ app.get('/api/share/my', authMiddleware, (req, res) => { res.json({ success: true, - shares: shares.map(share => ({ - ...share, - share_url: `${getSecureBaseUrl(req)}/s/${share.share_code}` - })) + shares: shares.map(share => { + const hasPassword = !!share.share_password; + const { share_password, ...safeShare } = share; + + return { + ...safeShare, + has_password: hasPassword, + share_url: `${getSecureBaseUrl(req)}/s/${share.share_code}` + }; + }) }); } catch (error) { console.error('获取分享列表失败:', error); @@ -3953,7 +4305,8 @@ app.post('/api/share/:code/verify', shareRateLimitMiddleware, async (req, res) = } return res.status(401).json({ success: false, - message: '密码错误' + message: '密码错误', + needPassword: true }); } } @@ -4002,10 +4355,9 @@ app.post('/api/share/:code/verify', shareRateLimitMiddleware, async (req, res) = // 使用统一存储接口 const { StorageInterface } = require('./storage'); - const userForStorage = { - ...shareOwner, + const userForStorage = buildStorageUserContext(shareOwner, { current_storage_type: storageType - }; + }); const storageInterface = new StorageInterface(userForStorage); storage = await storageInterface.connect(); @@ -4097,15 +4449,20 @@ app.post('/api/share/:code/list', shareRateLimitMiddleware, async (req, res) => } // 验证密码 - if (share.share_password && !ShareDB.verifyPassword(password, share.share_password)) { - // 记录密码错误 - if (req.shareRateLimitKey) { - shareLimiter.recordFailure(req.shareRateLimitKey); + if (share.share_password) { + const normalizedPassword = typeof password === 'string' ? password : ''; + + if (!normalizedPassword || !ShareDB.verifyPassword(normalizedPassword, share.share_password)) { + // 记录密码错误 + if (req.shareRateLimitKey) { + shareLimiter.recordFailure(req.shareRateLimitKey); + } + return res.status(401).json({ + success: false, + message: '密码错误或未提供密码', + needPassword: true + }); } - return res.status(401).json({ - success: false, - message: '密码错误' - }); } // 清除失败记录(密码验证成功或无密码) @@ -4142,10 +4499,9 @@ app.post('/api/share/:code/list', shareRateLimitMiddleware, async (req, res) => console.log(`[分享列表] 存储类型: ${storageType}, 分享路径: ${share.share_path}`); // 临时构造用户对象以使用存储接口 - const userForStorage = { - ...shareOwner, + const userForStorage = buildStorageUserContext(shareOwner, { current_storage_type: storageType - }; + }); const storageInterface = new StorageInterface(userForStorage); storage = await storageInterface.connect(); @@ -4258,9 +4614,9 @@ app.post('/api/share/:code/download', shareRateLimitMiddleware, (req, res) => { }); // 生成分享文件下载签名 URL(OSS 直连下载,公开 API,添加限流保护) -app.get('/api/share/:code/download-url', shareRateLimitMiddleware, async (req, res) => { +app.post('/api/share/:code/download-url', shareRateLimitMiddleware, async (req, res) => { const { code } = req.params; - const { path: filePath, password } = req.query; + const { path: filePath, password } = req.body || {}; // 参数验证:code 不能为空 if (!code || typeof code !== 'string' || code.length < 1 || code.length > 32) { @@ -4270,13 +4626,20 @@ app.get('/api/share/:code/download-url', shareRateLimitMiddleware, async (req, r }); } - if (!filePath) { + if (!filePath || typeof filePath !== 'string') { return res.status(400).json({ success: false, message: '缺少文件路径参数' }); } + if (filePath.includes('\x00')) { + return res.status(400).json({ + success: false, + message: '文件路径非法' + }); + } + try { const share = ShareDB.findByCode(code); @@ -4290,11 +4653,19 @@ app.get('/api/share/:code/download-url', shareRateLimitMiddleware, async (req, r // 验证密码(如果需要) if (share.share_password) { if (!password || !ShareDB.verifyPassword(password, share.share_password)) { + if (req.shareRateLimitKey) { + shareLimiter.recordFailure(req.shareRateLimitKey); + } return res.status(401).json({ success: false, - message: '密码错误或未提供密码' + message: '密码错误或未提供密码', + needPassword: true }); } + + if (req.shareRateLimitKey) { + shareLimiter.recordSuccess(req.shareRateLimitKey); + } } // 安全验证:检查请求路径是否在分享范围内 @@ -4314,35 +4685,46 @@ app.get('/api/share/:code/download-url', shareRateLimitMiddleware, async (req, r }); } - // 检查是否使用 OSS(包括个人配置和系统级统一配置) - const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig(); - if (!shareOwner.has_oss_config && !hasUnifiedConfig) { - // 本地存储模式:返回后端下载 URL + const storageType = share.storage_type || 'oss'; + + // 本地存储模式:返回后端下载 URL(短期 token,避免在 URL 中传密码) + if (storageType !== 'oss') { + let downloadUrl = `${getSecureBaseUrl(req)}/api/share/${code}/download-file?path=${encodeURIComponent(filePath)}`; + + if (share.share_password) { + const downloadToken = signEphemeralToken({ + type: 'share_download', + code, + path: filePath + }, 15 * 60); + downloadUrl += `&token=${encodeURIComponent(downloadToken)}`; + } + return res.json({ success: true, - downloadUrl: `${req.protocol}://${req.get('host')}/api/share/${code}/download-file?path=${encodeURIComponent(filePath)}${password ? '&password=' + encodeURIComponent(password) : ''}`, + downloadUrl, direct: false }); } - // OSS 模式:生成签名 URL - const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3'); + // OSS 模式:检查配置并生成签名 URL + const hasUnifiedConfig = SettingsDB.hasUnifiedOssConfig(); + if (!shareOwner.has_oss_config && !hasUnifiedConfig) { + return res.status(400).json({ + success: false, + message: '分享者未配置 OSS 服务' + }); + } + + const { GetObjectCommand } = require('@aws-sdk/client-s3'); const { getSignedUrl } = require('@aws-sdk/s3-request-presigner'); - // 获取有效的 OSS 配置(系统配置优先) - const unifiedConfig = SettingsDB.getUnifiedOssConfig(); - const effectiveBucket = unifiedConfig ? unifiedConfig.bucket : shareOwner.oss_bucket; - - // 构建 S3 客户端 - const client = new S3Client(buildS3Config(shareOwner)); - - // 构建对象 Key(复用 OssStorageClient 的 getObjectKey 方法,确保路径格式正确) - const tempClient = new OssStorageClient(shareOwner); - const objectKey = tempClient.getObjectKey(filePath); + const { client, bucket, ossClient } = createS3ClientContextForUser(shareOwner); + const objectKey = ossClient.getObjectKey(filePath); // 创建 GetObject 命令 const command = new GetObjectCommand({ - Bucket: effectiveBucket, + Bucket: bucket, Key: objectKey, ResponseContentDisposition: `attachment; filename="${encodeURIComponent(filePath.split('/').pop())}"` }); @@ -4370,7 +4752,7 @@ app.get('/api/share/:code/download-url', shareRateLimitMiddleware, async (req, r // 注意:OSS 模式请使用 /api/share/:code/download-url 获取直连下载链接 app.get('/api/share/:code/download-file', shareRateLimitMiddleware, async (req, res) => { const { code } = req.params; - const { path: filePath, password } = req.query; + const { path: filePath, password, token } = req.query; let storage; let storageEnded = false; // 防止重复关闭 @@ -4411,17 +4793,32 @@ app.get('/api/share/:code/download-file', shareRateLimitMiddleware, async (req, }); } - // 验证密码(如果需要) + // 验证密码(如果需要),支持短期下载 token(避免密码出现在 URL) if (share.share_password) { - if (!password || !ShareDB.verifyPassword(password, share.share_password)) { - // 只在密码错误时记录失败 - if (req.shareRateLimitKey) { - shareLimiter.recordFailure(req.shareRateLimitKey); + let verifiedByToken = false; + + if (token) { + const tokenResult = verifyEphemeralToken(token, 'share_download'); + if (tokenResult.valid) { + const tokenPayload = tokenResult.payload || {}; + if (tokenPayload.code === code && tokenPayload.path === filePath) { + verifiedByToken = true; + } + } + } + + if (!verifiedByToken) { + if (!password || !ShareDB.verifyPassword(password, share.share_password)) { + // 只在密码错误时记录失败 + if (req.shareRateLimitKey) { + shareLimiter.recordFailure(req.shareRateLimitKey); + } + return res.status(401).json({ + success: false, + message: '密码错误或未提供密码', + needPassword: true + }); } - return res.status(401).json({ - success: false, - message: '密码错误或未提供密码' - }); } // 密码验证成功,清除失败记录 @@ -4456,10 +4853,9 @@ app.get('/api/share/:code/download-file', shareRateLimitMiddleware, async (req, console.log(`[分享下载] 存储类型: ${storageType} (分享记录), 文件路径: ${filePath}`); // 临时构造用户对象以使用存储接口 - const userForStorage = { - ...shareOwner, + const userForStorage = buildStorageUserContext(shareOwner, { current_storage_type: storageType - }; + }); const storageInterface = new StorageInterface(userForStorage); storage = await storageInterface.connect(); @@ -4675,7 +5071,6 @@ app.get('/api/admin/unified-oss-config', authMiddleware, adminMiddleware, (req, app.post('/api/admin/unified-oss-config', authMiddleware, adminMiddleware, - requirePasswordConfirmation, // 安全修复:添加密码二次验证 [ body('provider').isIn(['aliyun', 'tencent', 'aws']).withMessage('无效的OSS服务商'), body('region').notEmpty().withMessage('地域不能为空'), @@ -4817,7 +5212,6 @@ app.post('/api/admin/unified-oss-config/test', app.delete('/api/admin/unified-oss-config', authMiddleware, adminMiddleware, - requirePasswordConfirmation, // 安全修复:添加密码二次验证 (req, res) => { try { SettingsDB.clearUnifiedOssConfig(); @@ -5018,7 +5412,7 @@ app.get('/api/admin/health-check', authMiddleware, adminMiddleware, async (req, }); // 11. CSRF 保护检查 - const csrfEnabled = process.env.ENABLE_CSRF === 'true'; + const csrfEnabled = ENABLE_CSRF; checks.push({ name: 'CSRF保护', category: 'security', @@ -5101,7 +5495,6 @@ app.get('/api/admin/wal-info', authMiddleware, adminMiddleware, (req, res) => { app.post('/api/admin/wal-checkpoint', authMiddleware, adminMiddleware, - requirePasswordConfirmation, // 安全修复:WAL 检查点是敏感操作 (req, res) => { try { const beforeSize = WalManager.getWalFileSize(); @@ -5253,8 +5646,10 @@ app.get('/api/admin/users', authMiddleware, adminMiddleware, (req, res) => { // 新增:存储相关字段 storage_permission: u.storage_permission || 'oss_only', current_storage_type: u.current_storage_type || 'oss', - local_storage_quota: u.local_storage_quota || 1073741824, - local_storage_used: u.local_storage_used || 0 + local_storage_quota: u.local_storage_quota || DEFAULT_LOCAL_STORAGE_QUOTA_BYTES, + local_storage_used: u.local_storage_used || 0, + oss_storage_quota: normalizeOssQuota(u.oss_storage_quota), + storage_used: u.storage_used || 0 })) }); } catch (error) { @@ -5330,7 +5725,6 @@ app.get('/api/admin/logs/stats', authMiddleware, adminMiddleware, (req, res) => app.post('/api/admin/logs/cleanup', authMiddleware, adminMiddleware, - requirePasswordConfirmation, // 安全修复:添加密码二次验证(日志清理影响审计追踪) (req, res) => { try { const { keepDays = 90 } = req.body; @@ -5428,7 +5822,6 @@ app.get('/api/admin/storage-cache/check/:userId', app.post('/api/admin/storage-cache/rebuild/:userId', authMiddleware, adminMiddleware, - requirePasswordConfirmation, // 安全修复:重建缓存是敏感操作 async (req, res) => { try { const { userId } = req.params; @@ -5538,7 +5931,6 @@ app.get('/api/admin/storage-cache/check-all', app.post('/api/admin/storage-cache/auto-fix', authMiddleware, adminMiddleware, - requirePasswordConfirmation, // 安全修复:批量修复是敏感操作 async (req, res) => { try { const { threshold = 0 } = req.body; // 差异阈值(字节) @@ -5614,7 +6006,6 @@ app.post('/api/admin/storage-cache/auto-fix', app.post('/api/admin/users/:id/ban', authMiddleware, adminMiddleware, - requirePasswordConfirmation, // 安全修复:添加密码二次验证(封禁用户是敏感操作) (req, res) => { try { const { id } = req.params; @@ -5687,7 +6078,6 @@ app.post('/api/admin/users/:id/ban', app.delete('/api/admin/users/:id', authMiddleware, adminMiddleware, - requirePasswordConfirmation, // 安全修复:添加密码二次验证 async (req, res) => { try { const { id } = req.params; @@ -5841,9 +6231,10 @@ function getUserDirectorySize(dirPath) { app.post('/api/admin/users/:id/storage-permission', authMiddleware, adminMiddleware, - requirePasswordConfirmation, // 安全修复:添加密码二次验证(修改存储权限影响用户数据访问) [ - body('storage_permission').isIn(['local_only', 'oss_only', 'user_choice']).withMessage('无效的存储权限') + body('storage_permission').isIn(['local_only', 'oss_only', 'user_choice']).withMessage('无效的存储权限'), + body('local_storage_quota').optional({ nullable: true }).isInt({ min: 1048576, max: 10995116277760 }).withMessage('本地配额必须在 1MB 到 10TB 之间'), + body('oss_storage_quota').optional({ nullable: true }).isInt({ min: 1048576, max: 10995116277760 }).withMessage('OSS配额必须在 1MB 到 10TB 之间') ], (req, res) => { const errors = validationResult(req); @@ -5856,7 +6247,7 @@ app.post('/api/admin/users/:id/storage-permission', try { const { id } = req.params; - const { storage_permission, local_storage_quota } = req.body; + const { storage_permission, local_storage_quota, oss_storage_quota } = req.body; // 参数验证:验证 ID 格式 const userId = parseInt(id, 10); @@ -5869,11 +6260,16 @@ app.post('/api/admin/users/:id/storage-permission', const updates = { storage_permission }; - // 如果提供了配额,更新配额(单位:字节) - if (local_storage_quota !== undefined) { + // 如果提供了本地配额,更新配额(单位:字节) + if (local_storage_quota !== undefined && local_storage_quota !== null) { updates.local_storage_quota = parseInt(local_storage_quota, 10); } + // 如果提供了 OSS 配额,更新配额(单位:字节) + if (oss_storage_quota !== undefined && oss_storage_quota !== null) { + updates.oss_storage_quota = parseInt(oss_storage_quota, 10); + } + // 根据权限设置自动调整存储类型 const user = UserDB.findById(userId); if (!user) { @@ -5947,7 +6343,7 @@ app.get('/api/admin/users/:id/files', authMiddleware, adminMiddleware, async (re } // OssStorageClient 已在文件顶部导入 - ossClient = new OssStorageClient(user); + ossClient = createOssClientForUser(user); await ossClient.connect(); const list = await ossClient.list(dirPath); @@ -6005,7 +6401,6 @@ app.get('/api/admin/shares', authMiddleware, adminMiddleware, (req, res) => { app.delete('/api/admin/shares/:id', authMiddleware, adminMiddleware, - requirePasswordConfirmation, // 安全修复:添加密码二次验证(删除用户分享是敏感操作) (req, res) => { try { // 参数验证:验证 ID 格式 diff --git a/backend/storage.js b/backend/storage.js index 6214736..b048f94 100644 --- a/backend/storage.js +++ b/backend/storage.js @@ -1534,7 +1534,7 @@ class OssStorageClient { */ async createReadStream(filePath) { const key = this.getObjectKey(filePath); - const bucket = this.user.oss_bucket; + const bucket = this.getBucket(); const command = new GetObjectCommand({ Bucket: bucket, @@ -1589,7 +1589,7 @@ class OssStorageClient { */ async getPresignedUrl(filePath, expiresIn = 3600) { const key = this.getObjectKey(filePath); - const bucket = this.user.oss_bucket; + const bucket = this.getBucket(); try { const command = new GetObjectCommand({ @@ -1646,7 +1646,7 @@ class OssStorageClient { */ async getUploadPresignedUrl(filePath, expiresIn = 900, contentType = 'application/octet-stream') { const key = this.getObjectKey(filePath); - const bucket = this.user.oss_bucket; + const bucket = this.getBucket(); try { const command = new PutObjectCommand({ diff --git a/backend/test_share.js b/backend/test_share.js index bad4ba5..06e8df7 100644 --- a/backend/test_share.js +++ b/backend/test_share.js @@ -448,7 +448,7 @@ async function testGetDownloadUrl() { } try { - const res = await request('GET', `/api/share/${testShareCode}/download-url?path=/test-file.txt`); + const res = await request('POST', `/api/share/${testShareCode}/download-url`, { path: '/test-file.txt' }); // 如果文件存在 if (res.status === 200) { @@ -481,7 +481,7 @@ async function testDownloadWithPassword() { // 测试无密码 try { - const res1 = await request('GET', `/api/share/${passwordShareCode}/download-url?path=/test-file-password.txt`); + const res1 = await request('POST', `/api/share/${passwordShareCode}/download-url`, { path: '/test-file-password.txt' }); assert(res1.status === 401, '无密码应返回 401'); } catch (error) { console.log(` [ERROR] 测试无密码下载: ${error.message}`); @@ -489,7 +489,7 @@ async function testDownloadWithPassword() { // 测试带密码 try { - const res2 = await request('GET', `/api/share/${passwordShareCode}/download-url?path=/test-file-password.txt&password=test123`); + const res2 = await request('POST', `/api/share/${passwordShareCode}/download-url`, { path: '/test-file-password.txt', password: 'test123' }); // 密码正确,根据文件是否存在返回不同结果 if (res2.status === 200) { assert(res2.data.downloadUrl, '应返回下载链接'); @@ -535,7 +535,7 @@ async function testDownloadPathValidation() { // 测试越权访问 try { - const res = await request('GET', `/api/share/${testShareCode}/download-url?path=/other-file.txt`); + const res = await request('POST', `/api/share/${testShareCode}/download-url`, { path: '/other-file.txt' }); // 单文件分享应该禁止访问其他文件 assert(res.status === 403 || res.status === 404, '越权访问应被拒绝'); @@ -546,7 +546,7 @@ async function testDownloadPathValidation() { // 测试路径遍历 try { - const res2 = await request('GET', `/api/share/${testShareCode}/download-url?path=/../../../etc/passwd`); + const res2 = await request('POST', `/api/share/${testShareCode}/download-url`, { path: '/../../../etc/passwd' }); assert(res2.status === 403 || res2.status === 400, '路径遍历应被拒绝'); } catch (error) { console.log(` [ERROR] 路径遍历测试: ${error.message}`); @@ -682,7 +682,7 @@ async function testShareNotExists() { // 下载 try { - const res3 = await request('GET', `/api/share/${nonExistentCode}/download-url?path=/test.txt`); + const res3 = await request('POST', `/api/share/${nonExistentCode}/download-url`, { path: '/test.txt' }); assert(res3.status === 404, '下载不存在分享应返回 404'); } catch (error) { console.log(` [ERROR] ${error.message}`); diff --git a/backend/test_share_edge_cases.js b/backend/test_share_edge_cases.js index 4b696b6..5edc381 100644 --- a/backend/test_share_edge_cases.js +++ b/backend/test_share_edge_cases.js @@ -225,7 +225,7 @@ async function testPathTraversalAttacks() { let blocked = 0; for (const attackPath of attackPaths) { - const res = await request('GET', `/api/share/${shareCode}/download-url?path=${encodeURIComponent(attackPath)}`); + const res = await request('POST', `/api/share/${shareCode}/download-url`, { path: attackPath }); if (res.status === 403 || res.status === 400) { blocked++; diff --git a/backend/utils/storage-cache.js b/backend/utils/storage-cache.js index df9702b..9c8e35c 100644 --- a/backend/utils/storage-cache.js +++ b/backend/utils/storage-cache.js @@ -53,21 +53,20 @@ class StorageUsageCache { */ static async updateUsage(userId, deltaSize) { try { - // 使用 SQL 原子操作,避免并发问题 - const result = UserDB.update(userId, { - // 使用原始 SQL,因为 update 方法不支持表达式 - // 注意:这里需要在数据库层执行 UPDATE ... SET storage_used = storage_used + ? - }); + const numericDelta = Number(deltaSize); + if (!Number.isFinite(numericDelta)) { + throw new Error('deltaSize 必须是有效数字'); + } - // 直接执行 SQL 更新 + // 直接执行 SQL 原子更新,并保证不小于 0 const { db } = require('../database'); db.prepare(` UPDATE users - SET storage_used = storage_used + ? + SET storage_used = MAX(COALESCE(storage_used, 0) + ?, 0) WHERE id = ? - `).run(deltaSize, userId); + `).run(numericDelta, userId); - console.log(`[存储缓存] 用户 ${userId} 存储变化: ${deltaSize > 0 ? '+' : ''}${deltaSize} 字节`); + console.log(`[存储缓存] 用户 ${userId} 存储变化: ${numericDelta > 0 ? '+' : ''}${numericDelta} 字节`); return true; } catch (error) { console.error('[存储缓存] 更新失败:', error); diff --git a/frontend/app.html b/frontend/app.html index ffda332..e9e2adc 100644 --- a/frontend/app.html +++ b/frontend/app.html @@ -521,8 +521,30 @@ transform: translateY(-4px); box-shadow: 0 8px 24px rgba(102, 126, 234, 0.15); } + + .file-grid-item, + .file-grid-item *, + .file-list-row, + .file-list-row *, + .file-list-name-wrap, + .file-list-name-text { + -webkit-touch-callout: none; + -webkit-user-select: none; + user-select: none; + -webkit-tap-highlight-color: transparent; + } + .file-grid-item, + .file-list-row { + touch-action: pan-y; + } + .file-thumbnail, + .file-list-thumb { + -webkit-user-drag: none; + user-drag: none; + pointer-events: none; + } .file-icon { - margin-bottom: 10px; + margin-bottom: 8px; } .file-thumbnail { width: 64px; @@ -622,6 +644,17 @@ margin-bottom: 20px; } + + /* 管理端响应式增强 */ + .admin-tabs-card { + overflow: hidden; + } + .admin-users-table-wrap, + .admin-log-list { + -webkit-overflow-scrolling: touch; + } + + /* 移动端适配 */ @media (max-width: 768px) { /* 导航栏移动端优化 */ @@ -736,6 +769,12 @@ .file-list .file-icon { font-size: 16px !important; } + .file-list-action-col { + display: table-cell !important; + } + .file-row-mobile-more { + display: inline-flex !important; + } /* 列表视图视频图标容器移动端适配 */ .file-list div[style*="background: linear-gradient"] { width: 28px !important; @@ -791,6 +830,110 @@ .card table button i { font-size: 10px; } + + /* 管理员页面移动端增强 */ + .admin-tabs-nav { + display: grid !important; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 6px; + border-bottom: none !important; + padding: 8px; + } + .admin-tab-btn { + width: 100%; + padding: 10px 8px !important; + font-size: 13px !important; + border-radius: 8px !important; + } + .admin-debug-row, + .admin-health-header, + .admin-log-header { + flex-direction: column !important; + align-items: flex-start !important; + gap: 10px; + } + .admin-debug-toggle-btn { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 10px 12px !important; + font-size: 14px !important; + } + .admin-health-summary { + width: 100%; + flex-wrap: wrap; + gap: 8px !important; + } + .admin-log-actions { + width: 100%; + display: grid !important; + grid-template-columns: 1fr 1fr; + gap: 8px; + } + .admin-log-actions .btn { + width: 100%; + } + .admin-log-filters { + gap: 10px !important; + padding: 12px !important; + } + .admin-log-filter-item { + width: 100%; + flex-direction: column; + align-items: stretch !important; + gap: 6px !important; + } + .admin-log-filter-item select, + .admin-log-filter-item input { + width: 100%; + min-width: 0; + } + .admin-log-filter-search { + min-width: 0 !important; + } + .admin-log-row { + display: grid !important; + grid-template-columns: 1fr auto; + gap: 8px 10px; + } + .admin-log-time { + width: auto !important; + } + .admin-log-level { + width: auto !important; + justify-self: end; + } + .admin-log-category { + display: none !important; + } + .admin-log-content { + grid-column: 1 / -1; + } + .admin-log-pager { + flex-wrap: wrap; + } + .admin-log-pager .btn { + min-width: 110px; + } + .admin-users-table { + min-width: 700px !important; + } + .admin-users-table th:nth-child(1), + .admin-users-table td:nth-child(1), + .admin-users-table th:nth-child(6), + .admin-users-table td:nth-child(6) { + display: none; + } + .admin-user-actions { + justify-content: flex-start !important; + gap: 4px !important; + } + .admin-user-action-btn { + padding: 4px 8px !important; + } + } /* 超小屏幕优化 (手机竖屏) */ @@ -821,10 +964,40 @@ font-size: 11px; } + + /* 管理员页面超小屏优化 */ + .admin-tabs-nav { + grid-template-columns: 1fr; + } + .admin-log-actions { + grid-template-columns: 1fr; + } + .admin-health-summary { + font-size: 12px !important; + } + .admin-log-row { + padding: 8px 4px !important; + } + .admin-log-time { + font-size: 11px !important; + } + .admin-users-table { + min-width: 620px !important; + } + .admin-user-action-btn { + font-size: 10px !important; + } + .admin-users-table th:nth-child(3), + .admin-users-table td:nth-child(3), + .admin-users-table th:nth-child(7), + .admin-users-table td:nth-child(7) { + display: none; + } + /* 列表视图在超小屏幕隐藏文件大小列 */ .file-list th:nth-child(2), .file-list td:nth-child(2) { - font-size: 11px; + display: none; } } @@ -938,6 +1111,22 @@ background: rgba(0, 0, 0, 0.04); } + .file-list-action-col { + display: none; + } + .file-row-mobile-more { + display: none; + border: 1px solid var(--glass-border); + background: rgba(255, 255, 255, 0.08); + color: var(--text-secondary); + padding: 6px 10px; + border-radius: 8px; + min-width: 36px; + justify-content: center; + align-items: center; + touch-action: manipulation; + } + /* ========== 危险按钮 ========== */ .btn-danger { background: linear-gradient(135deg, #ef4444, #dc2626); @@ -1138,15 +1327,154 @@ min-width: 140px; cursor: pointer; } + + .share-password-toggle { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 12px; + border-radius: 10px; + border: 1px solid var(--glass-border); + background: rgba(255, 255, 255, 0.04); + color: var(--text-secondary); + font-size: 13px; + cursor: pointer; + user-select: none; + } + .share-password-toggle input { + accent-color: var(--accent-1); + width: 16px; + height: 16px; + } + .share-password-hint { + display: block; + margin-top: 8px; + color: var(--text-muted); + font-size: 12px; + line-height: 1.5; + } + .share-success-panel { + border: 1px solid rgba(34, 197, 94, 0.28); + border-radius: 12px; + padding: 14px; + background: rgba(34, 197, 94, 0.08); + color: var(--text-primary); + display: grid; + gap: 12px; + } + .share-success-head { + display: flex; + align-items: center; + gap: 10px; + } + .share-success-head > i { + color: #22c55e; + font-size: 22px; + } + .share-success-title { + font-size: 15px; + font-weight: 700; + color: #22c55e; + } + .share-success-subtitle { + margin-top: 2px; + font-size: 13px; + color: var(--text-secondary); + word-break: break-all; + } + .share-success-link { + border-radius: 10px; + border: 1px dashed rgba(34, 197, 94, 0.35); + background: rgba(0, 0, 0, 0.12); + padding: 10px 12px; + font-size: 13px; + color: var(--text-primary); + word-break: break-all; + } + .share-success-actions { + display: flex; + gap: 10px; + flex-wrap: wrap; + } + .share-success-actions .btn { + flex: 1; + min-width: 96px; + padding: 9px 12px; + font-size: 13px; + } + .share-success-meta { + display: flex; + flex-wrap: wrap; + gap: 8px; + } + .share-success-tip { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + padding: 10px 12px; + border-radius: 10px; + border: 1px solid transparent; + line-height: 1.5; + } + .share-success-tip.warn { + color: #f59e0b; + border-color: rgba(245, 158, 11, 0.28); + background: rgba(245, 158, 11, 0.12); + } + .share-success-tip.info { + color: #60a5fa; + border-color: rgba(59, 130, 246, 0.26); + background: rgba(59, 130, 246, 0.12); + } body.light-theme .share-card { box-shadow: 0 10px 30px rgba(0,0,0,0.08); } body.light-theme .share-card:hover { box-shadow: 0 14px 36px rgba(0,0,0,0.12); } + + @media (max-width: 768px) { + .share-toolbar { + flex-direction: column; + align-items: stretch; + } + .share-toolbar input, + .share-toolbar select { + width: 100%; + min-width: 0 !important; + } + .share-card-grid { + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + } + .share-card__actions .btn { + flex: 1; + min-width: 108px; + } + .share-success-actions { + flex-direction: column; + } + .share-success-actions .btn { + width: 100%; + min-width: 0; + } + } + + @media (max-width: 480px) { + .share-card-grid { + grid-template-columns: 1fr; + } + .share-card__meta { + grid-template-columns: 1fr; + } + .share-card__actions .btn { + width: 100%; + } + } + - +
@@ -1266,76 +1594,29 @@
-
- -
-
- - - 当前存储: {{ storageTypeText }} - -
-
-
- 配额使用情况 - {{ localUsedFormatted }} / {{ localQuotaFormatted }} ({{ quotaPercentage }}%) -
-
-
-
-
-
+
-
- - / + / -
- -
-
- - - - -
-
- - -
-
- @@ -1346,8 +1627,58 @@
-
-

文件夹是空的

+
+
+ 文件视图 · {{ fileStats.totalCount }} 项 +
+ + + {{ storageTypeText }} + +
+
+ + 本地 {{ localUsedFormatted }} / {{ localQuotaFormatted }} · {{ quotaPercentage }}% + + + OSS {{ ossUsedFormatted }} / {{ ossQuotaFormatted }} · {{ ossQuotaPercentage }}% + +
+
+
+ + +
+ + +
+
+
+
+
+ +
当前目录暂无文件
+
你可以先上传文件,或新建文件夹整理资料
+
+ + +
+
@@ -1359,7 +1690,7 @@
-
+
- + class="file-grid-video-icon"> +
- + - - - - - - + + + + + +
{{ getFileDisplayName(file) }}
{{ file.isDirectory ? '文件夹' : file.sizeFormatted }}
@@ -1388,52 +1719,71 @@
- +
- - - - + + + + + - - - + + +
文件名大小修改时间
文件名大小修改时间操作
-
+ @touchend="handleLongPressEnd" + @touchcancel="handleLongPressEnd" + @selectstart.prevent + @dragstart.prevent> +
+
+ class="file-list-thumb">
- + class="file-list-video-icon"> +
- + - - - - - - - {{ getFileDisplayName(file) }} + + + + + + +
+ {{ getFileDisplayName(file) }} + {{ file.isDirectory ? '文件夹' : file.sizeFormatted }} · {{ formatDate(file.modifiedTime) }} +
{{ file.isDirectory ? '-' : file.sizeFormatted }}{{ formatDate(file.modifiedTime) }}{{ file.isDirectory ? '-' : file.sizeFormatted }}{{ formatDate(file.modifiedTime) }} + +
+
@@ -1523,58 +1873,22 @@
- - -