From e8a64d1064a89874dd1d26a41d01aa17f2b8785b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=BD=A6=E8=B6=85?= Date: Fri, 8 Aug 2025 22:57:19 +0800 Subject: [PATCH 01/39] Update Footer.vue --- assets/Footer.vue | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/assets/Footer.vue b/assets/Footer.vue index 4527c6b..61f8987 100644 --- a/assets/Footer.vue +++ b/assets/Footer.vue @@ -40,10 +40,10 @@ export default { name: "Footer", data() { return { - homeUrl: "https://www.sztcrs.com/", - blogUrl: "https://blog.sztcrs.com/", - githubUrl: "https://github.com/52op", - emailUrl: "mailto:admin@sztcrs.com" + homeUrl: "https://blog.liuyc.uk/about.html", + blogUrl: "https://blog.liuyc.uk/", + githubUrl: "https://github.com/marktube", + emailUrl: "mailto:liuyc@sem.tsinghua.edu.cn" }; } }; @@ -117,4 +117,4 @@ export default { .powered a:hover { color: #222; } - \ No newline at end of file + From 02eac8f5f80a96ede336c6bcf49e3e73d53502ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=BD=A6=E8=B6=85?= Date: Wed, 13 Aug 2025 13:55:20 +0800 Subject: [PATCH 02/39] Update [[path]].ts --- functions/api/children/[[path]].ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/functions/api/children/[[path]].ts b/functions/api/children/[[path]].ts index 5483bcd..f62620a 100644 --- a/functions/api/children/[[path]].ts +++ b/functions/api/children/[[path]].ts @@ -47,8 +47,8 @@ export async function onRequestGet(context) { if(headers.get('Authorization')) { const Authorization = headers.get('Authorization').split("Basic ")[1]; const account = atob(Authorization); - if(account && context.env[account]) { - const allow = context.env[account].split(","); + if(account && (context.env[account] || context.env[account + ':r'])) { + const allow = [...context.env[account].split(","), ...context.env[account + ':r'].split(",")]; // 如果不是管理员,需要过滤内容 if (!allow.includes("*")) { From 8b687dff8fbec4042ad7868ee89afe8e7efc2b2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=BD=A6=E8=B6=85?= Date: Wed, 13 Aug 2025 14:04:21 +0800 Subject: [PATCH 03/39] Update [[path]].ts fix bug in showing readonly user folders --- functions/api/children/[[path]].ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/functions/api/children/[[path]].ts b/functions/api/children/[[path]].ts index f62620a..4f1a257 100644 --- a/functions/api/children/[[path]].ts +++ b/functions/api/children/[[path]].ts @@ -48,7 +48,10 @@ export async function onRequestGet(context) { const Authorization = headers.get('Authorization').split("Basic ")[1]; const account = atob(Authorization); if(account && (context.env[account] || context.env[account + ':r'])) { - const allow = [...context.env[account].split(","), ...context.env[account + ':r'].split(",")]; + if(context.env[account]) + const allow = context.env[account].split(","); + else + const allow = context.env[account + ':r'].split(","); // 如果不是管理员,需要过滤内容 if (!allow.includes("*")) { From f1448f6422768673bc5415b5bb9143e5fcf6a1de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=BD=A6=E8=B6=85?= Date: Wed, 13 Aug 2025 14:09:24 +0800 Subject: [PATCH 04/39] Update [[path]].ts --- functions/api/children/[[path]].ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/functions/api/children/[[path]].ts b/functions/api/children/[[path]].ts index 4f1a257..0ea995c 100644 --- a/functions/api/children/[[path]].ts +++ b/functions/api/children/[[path]].ts @@ -48,10 +48,11 @@ export async function onRequestGet(context) { const Authorization = headers.get('Authorization').split("Basic ")[1]; const account = atob(Authorization); if(account && (context.env[account] || context.env[account + ':r'])) { - if(context.env[account]) + if(context.env[account]){ const allow = context.env[account].split(","); - else + }else{ const allow = context.env[account + ':r'].split(","); + } // 如果不是管理员,需要过滤内容 if (!allow.includes("*")) { From fbe139db7c6c31d15a629812b3a9721f4077182e Mon Sep 17 00:00:00 2001 From: liuyanchao Date: Wed, 13 Aug 2025 15:10:37 +0800 Subject: [PATCH 05/39] roll back --- functions/api/children/[[path]].ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/functions/api/children/[[path]].ts b/functions/api/children/[[path]].ts index 0ea995c..5483bcd 100644 --- a/functions/api/children/[[path]].ts +++ b/functions/api/children/[[path]].ts @@ -47,12 +47,8 @@ export async function onRequestGet(context) { if(headers.get('Authorization')) { const Authorization = headers.get('Authorization').split("Basic ")[1]; const account = atob(Authorization); - if(account && (context.env[account] || context.env[account + ':r'])) { - if(context.env[account]){ - const allow = context.env[account].split(","); - }else{ - const allow = context.env[account + ':r'].split(","); - } + if(account && context.env[account]) { + const allow = context.env[account].split(","); // 如果不是管理员,需要过滤内容 if (!allow.includes("*")) { From c0337e12d89ba6157e3d6a23016f1fec2855b23a Mon Sep 17 00:00:00 2001 From: liuyanchao Date: Wed, 13 Aug 2025 15:49:18 +0800 Subject: [PATCH 06/39] modify children --- functions/api/children/[[path]].ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/functions/api/children/[[path]].ts b/functions/api/children/[[path]].ts index 5483bcd..6dbb109 100644 --- a/functions/api/children/[[path]].ts +++ b/functions/api/children/[[path]].ts @@ -77,6 +77,34 @@ export async function onRequestGet(context) { return false; }); } + }else if(account && context.env[account + ':r']){ + // 处理只读用户 + const allow = context.env[account + ':r'].split(","); + + // 获取游客权限,已登录用户也应该能访问游客目录 + const guestEnv = context.env["GUEST"] || context.env["guest"]; + const allow_guest = guestEnv ? guestEnv.split(",") : []; + + // 合并用户权限和游客权限 + const combinedPermissions = [...allow, ...allow_guest]; + + // 过滤文件:显示用户有权限的文件 + 游客可访问的文件 + objKeys = objKeys.filter(file => { + for (var a of combinedPermissions) { + if (a == "*") return true; + if (file.key.startsWith(a)) return true; + } + return false; + }); + + // 过滤文件夹:显示用户有权限的文件夹 + 游客可访问的文件夹 + folders = folders.filter(folder => { + for (var a of combinedPermissions) { + if (a == "*") return true; + if (folder.startsWith(a)) return true; + } + return false; + }); } } } else { From 3db842e8df212fa1f4493cfae06e6da3c8441c4a Mon Sep 17 00:00:00 2001 From: liuyanchao Date: Wed, 13 Aug 2025 22:01:32 +0800 Subject: [PATCH 07/39] add new bg;modify footer;add description in README --- README.md | 4 ++++ assets/App.vue | 2 +- assets/Footer.vue | 6 ++++-- assets/bg-cat.jpeg | Bin 0 -> 788416 bytes 4 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 assets/bg-cat.jpeg diff --git a/README.md b/README.md index 9c38e9c..0609343 100644 --- a/README.md +++ b/README.md @@ -469,3 +469,7 @@ GUEST=public/ - 确认已先复制文件到剪贴板 - 检查用户权限是否允许写操作 - 移动端查看底部是否显示粘贴工具栏 + +--- + +**当前版本修复了只读用户显示所有目录的bug,只显示权限目录** \ No newline at end of file diff --git a/assets/App.vue b/assets/App.vue index 850263f..d21b015 100644 --- a/assets/App.vue +++ b/assets/App.vue @@ -582,7 +582,7 @@ export default { showUploadPopup: false, uploadProgress: null, uploadQueue: [], - backgroundImageUrl: "/assets/bg-light.webp", + backgroundImageUrl: "/assets/bg-cat.jpeg", needLogin: false, isGuest: true, // 默认为游客状态 isLoggedIn: false, diff --git a/assets/Footer.vue b/assets/Footer.vue index 61f8987..120b760 100644 --- a/assets/Footer.vue +++ b/assets/Footer.vue @@ -15,11 +15,13 @@ -
+ - +
-
+
+ +
@@ -441,6 +447,13 @@ export default { this.translateX = 0; this.translateY = 0; }, + panoImage() { + pannellum.viewer('pr-img-view', { + "type": "equirectangular", + "panorama": this.currentMedia.url, + "showFullscreenCtrl": true + }); + }, // 双击放大 onDoubleClick() { From 9ea722acfc38fe479c10e96e83dbc5e7b21a0fac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=BD=A6=E8=B6=85?= Date: Fri, 29 Aug 2025 16:27:35 +0800 Subject: [PATCH 12/39] Update index.html add pannellum.js --- index.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.html b/index.html index 70adc20..6b6a78c 100644 --- a/index.html +++ b/index.html @@ -13,6 +13,8 @@ + +
From 5243ddd62e5ac8e222dcedc95f7ee3dddc6d92f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=BD=A6=E8=B6=85?= Date: Fri, 29 Aug 2025 16:35:31 +0800 Subject: [PATCH 13/39] Update MediaPreview.vue fix click link --- assets/MediaPreview.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/MediaPreview.vue b/assets/MediaPreview.vue index 3caff5d..d446775 100644 --- a/assets/MediaPreview.vue +++ b/assets/MediaPreview.vue @@ -183,7 +183,7 @@ -
  • - +
  • - From 29b9dc8987f266691705b6506ef0fa0a3f6e5564 Mon Sep 17 00:00:00 2001 From: liuyanchao Date: Thu, 10 Sep 2026 21:36:42 +0800 Subject: [PATCH 32/39] add d1 auth & fix secure bug --- README.md | 101 +++++++++++- assets/App.vue | 133 +++++++++++++++- assets/MediaPreview.vue | 6 +- functions/api/auth/ban-status.ts | 10 +- functions/api/auth/change-password.ts | 78 +++++++++ functions/api/auth/check-write-permission.ts | 54 ++----- functions/api/auth/login.ts | 25 ++- functions/api/auth/register.ts | 111 +++++++++++++ functions/api/auth/unban.ts | 10 +- functions/api/children/[[path]].ts | 39 +---- functions/api/write/items/[[path]].ts | 37 +++-- functions/api/write/test/[[path]].ts | 7 +- functions/raw/[[path]].ts | 34 ++++ migrations/0001_create_users.sql | 18 +++ utils/auth.ts | 67 ++++++-- utils/csrf.ts | 31 ++++ utils/d1auth.ts | 159 +++++++++++++++++++ "\344\270\212\344\274\240.bat" | 14 -- 18 files changed, 785 insertions(+), 149 deletions(-) create mode 100644 functions/api/auth/change-password.ts create mode 100644 functions/api/auth/register.ts create mode 100644 migrations/0001_create_users.sql create mode 100644 utils/csrf.ts create mode 100644 utils/d1auth.ts delete mode 100644 "\344\270\212\344\274\240.bat" diff --git a/README.md b/README.md index 3acbf57..f3b8188 100644 --- a/README.md +++ b/README.md @@ -218,7 +218,106 @@ BUCKET > 📝 详细配置说明请参考 `登录限制功能配置说明.md` 文件 -### 6. 重新部署项目 +### 6. 配置 D1 数据库账户体系(可选,推荐) + +[#6-配置-d1-数据库账户体系可选推荐](#6-配置-d1-数据库账户体系可选推荐) + +默认情况下,账户仍然使用第 3 步中基于环境变量的 `username:password=dir1/,dir2/` 配置方式(无需任何改动即可继续使用)。 + +增强版额外支持将账户存储在 **Cloudflare D1** 数据库中,优点是: + +- ✅ 用户可以**自助修改密码**,无需管理员改环境变量、重新部署 +- ✅ 密码使用 PBKDF2-SHA256 加盐哈希存储,不落明文 +- ✅ 登录时优先查询 D1,查不到再自动回退到旧版环境变量账户,**两种方式可以混用,完全向后兼容** + +#### 6.1 创建 D1 数据库 + +1. 进入 Cloudflare 控制台 → **Workers & Pages** → **D1 SQL Database** +2. 点击 **Create Database**,输入一个名称(例如 `flaredrive-users`) + +#### 6.2 绑定 D1 到项目 + +1. 进入 Pages 项目设置 → **Settings** → **Functions** +2. 在 **D1 database bindings** 部分点击 **Add binding** +3. **Variable name**:`DB`(必须叫这个名字,代码里写死了这个绑定名) +4. **D1 database**:选择刚创建的数据库 + +#### 6.3 初始化表结构 + +在本地使用 Wrangler 执行仓库自带的迁移文件: + +```bash +# 首次在本地测试可以不加 --remote,先在本地模拟数据库里建表 +wrangler d1 execute flaredrive-users --file=./migrations/0001_create_users.sql + +# 确认无误后,对线上(生产)数据库执行: +wrangler d1 execute flaredrive-users --file=./migrations/0001_create_users.sql --remote +``` + +`flaredrive-users` 替换成你实际的数据库名称。 + +#### 6.4 创建第一个 D1 账户 + +D1 里还没有任何账户时,无法直接注册(`/api/auth/register` 接口本身也需要一个管理员身份才能调用)。推荐用你在第 3 步配置的**环境变量管理员账户**(例如 `admin:123456=*`)来创建第一批 D1 账户,这样可以平滑过渡: + +```bash +curl -X POST https://你的域名/api/auth/register \ + -H "Authorization: Basic $(echo -n 'admin:123456' | base64)" \ + -H "Content-Type: application/json" \ + -d '{ + "username": "alice", + "password": "至少6位的密码", + "permissions": ["personal/", "work/"], + "isReadOnly": false + }' +``` + +- `permissions` 传 `["*"]` 表示管理员(拥有全部目录权限) +- `isReadOnly` 为 `true` 时创建只读账户 +- 之后也可以直接用某个已存在的 D1 管理员账户(`isAdmin` 为真)来调用该接口创建更多用户 + +**用户名重复时**,接口会返回 `409 Conflict` 和如下结构,方便前端直接判断并提示: + +```json +{ + "success": false, + "code": "USERNAME_TAKEN", + "message": "用户名 \"alice\" 已存在,请换一个用户名" +} +``` + +重复判定包含两种情况:D1 数据库里已有同名账户,或环境变量里已配置同名账户(因为登录时优先匹配 D1 账户,同名会导致原有的环境变量账户失效)。 + +创建成功后,该用户就可以像环境变量账户一样正常登录、上传、下载了,同时还能使用第 6.5 节的接口自助改密码。 + +#### 6.5 用户自助修改密码 + +已登录用户可以调用 `/api/auth/change-password` 接口修改自己的密码(仅对 D1 账户生效,环境变量账户请管理员直接改环境变量): + +```bash +curl -X POST https://你的域名/api/auth/change-password \ + -H "Authorization: Basic $(echo -n 'alice:旧密码' | base64)" \ + -H "Content-Type: application/json" \ + -d '{ "newPassword": "至少6位的新密码" }' +``` + +前端登录后,点击右上角用户菜单里的「修改密码」即可打开对话框完成上述操作,无需手动调用接口。 + +> ⚠️ D1 与环境变量账户可以同名同时存在,此时登录会优先匹配 D1 中的账户。建议不要让两者重名,避免混淆。 + +### 🔒 安全加固说明 + +本次改动顺带修复了几个安全问题,升级时请留意: + +1. **未鉴权的文件写入(严重)**:`/api/write/items/[[path]].ts` 的分片上传接口(`POST ?uploads` 创建、`POST ?uploadId` 完成)此前完全没有做权限校验,配合已有鉴权的 `PUT` 分片上传步骤,任何人不登录即可写入任意目录。现已补上校验,与 `PUT`/`DELETE` 保持一致。 +2. **`/raw/` 文件预览的存储型 XSS**:上传一个 `Content-Type` 为 `text/html`(或 `.svg` 等)的文件后,通过 `/raw/文件路径` 直接打开会在本域名下执行其中的脚本,从而窃取保存在 `localStorage` 里的登录凭据。现已给 `/raw/` 响应统一加上 `X-Content-Type-Options: nosniff` 和 `Content-Security-Policy: sandbox`(彻底禁止脚本执行、表单提交等,不影响图片/视频/音频的正常预览),并对 `text/html`、`image/svg+xml` 等类型额外强制 `Content-Disposition: attachment`。 +3. **游客缩略图写入范围过大**:只要配置了 `GUEST` 环境变量(不论允许哪些目录),未登录用户此前都能写入共享的 `_$flaredrive$/thumbnails/` 目录。现在缩略图路径和普通文件一样,按 `GUEST` 配置的具体目录做匹配,需要显式在 `GUEST` 里加上该路径(或 `*`)才允许游客写入。 +4. **Basic Auth 凭据被跨站请求利用的风险**:写操作接口此前在 401 响应里带有 `WWW-Authenticate` 头,会导致浏览器弹出原生登录框并缓存凭据,而缓存后的 Basic 凭据会被浏览器自动附加到**任意来源**发起的同域请求上,构成 CSRF 隐患。现已移除该响应头,并为 `PUT`/`POST`/`DELETE` 增加了 `Origin` 同源校验(仅在请求带有 `Origin` 且与当前域名不一致时才拒绝,不影响 curl 等不带 `Origin` 头的合法调用)。 +5. **权限校验路径与实际写入路径解码方式不一致**、以及仓库里附带的 `test-permission.html` / `test-login-limit.html` 调试页面此前会被一起部署到线上、且没有任何鉴权:已将权限校验路径改为与实际写入路径一致的解码方式,并移除了这两个调试页面(如果你本地还需要用它们做权限测试,可以从 Git 历史中找回,但不建议再部署到线上)。 + +如果你是从旧版本升级,建议同时检查一下自己的 `GUEST` 环境变量配置是否符合预期(第 3 条的行为变化可能影响到依赖旧行为的部署)。 + +### 8. 重新部署项目 完成所有设置后,回到 Pages 控制台,点击「Deployments」页面右上角的「Trigger Redeploy」以重新部署服务。 diff --git a/assets/App.vue b/assets/App.vue index 0dbb749..8416dcc 100644 --- a/assets/App.vue +++ b/assets/App.vue @@ -14,12 +14,12 @@
  • -
  • @@ -2163,6 +2163,71 @@ export default { } }, + // 删除文件夹:此前这里只会删除 "_$folder$" 这个空标记文件, + // 文件夹里实际的文件和子文件夹会被静默留下(既没被删掉,也脱离了正常管理)。 + // 现在改为先用 getAllItems 递归枚举文件夹下的全部内容(文件 + 子文件夹标记), + // 逐个删除,最后再删除文件夹自身的标记,和"移动文件夹"用的是同一套递归逻辑。 + async removeFolder(folderPath) { + // 检查写权限 + if (!this.canWrite) { + this.showPermissionDialog('删除文件夹'); + return; + } + + // 关闭右键菜单 + this.showContextMenu = false; + + const folderName = folderPath.split('/').filter(Boolean).pop() || folderPath; + const folderMarkerKey = folderPath + '_$folder$'; + + try { + const confirmed = await this.showConfirmPrompt( + '删除文件夹', + `确定要删除文件夹 "${folderName}" 吗?文件夹内的所有文件和子文件夹都会被永久删除,此操作无法撤销。`, + { type: 'danger', confirmText: '删除', cancelText: '取消' } + ); + if (!confirmed) return; + + // 递归获取文件夹内的所有文件和子文件夹标记 + const allItems = await this.getAllItems(folderPath); + + const total = allItems.length; + let done = 0; + if (total > 0) this.uploadProgress = 0; + + for (const item of allItems) { + try { + await this.deleteFile(item.key); + } catch (itemError) { + // 权限错误直接中止整个删除流程并向上抛出 + if (itemError.isAuthError) throw itemError; + // 单个文件删除失败不阻断整体流程,记录后继续删除其余文件, + // 避免因为某一个文件失败导致整个文件夹删不掉 + console.error(`删除 ${item.key} 失败:`, itemError); + } + done++; + if (total > 0) this.uploadProgress = (done / total) * 100; + } + + // 最后删除文件夹自身的标记 + await this.deleteFile(folderMarkerKey); + + this.uploadProgress = null; + this.fetchFiles(); + this.showCustomToast(`文件夹 "${folderName}" 已删除`, 'success'); + } catch (error) { + this.uploadProgress = null; + if (error === false) return; // 用户取消 + + if (error.isAuthError) { + this.showPermissionDialog('删除文件夹'); + return; + } + console.error('删除文件夹失败:', error); + this.showCustomToast('删除文件夹失败: ' + (error.message || '未知错误'), 'error'); + } + }, + async singleDownload(key){ // 关闭右键菜单 this.showContextMenu = false; diff --git a/assets/MimeIcon.vue b/assets/MimeIcon.vue index 5a67f9d..b6f02a7 100644 --- a/assets/MimeIcon.vue +++ b/assets/MimeIcon.vue @@ -18,7 +18,7 @@ defineProps({