mirror of
https://github.com/ZSCGR/CloudFlare-ImgBed.git
synced 2026-08-13 04:03:42 +08:00
v2beta
This commit is contained in:
@@ -1,55 +0,0 @@
|
||||
name: Create Release with Zip
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# 检出当前仓库的代码
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# 获取提交信息和当前日期
|
||||
- name: Get Commit Info
|
||||
id: get_commit_info
|
||||
run: |
|
||||
echo "commit_sha=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
echo "commit_message=$(git log -1 --pretty=%B)" >> $GITHUB_ENV
|
||||
echo "release_date=$(date +"%Y-%m-%d")" >> $GITHUB_ENV
|
||||
|
||||
# 将当前目录下的所有文件打包为 zip 文件
|
||||
- name: Zip repository files
|
||||
run: |
|
||||
zip -r release-v1.0.${{ github.run_number }}.zip ./*
|
||||
|
||||
# 创建 Release
|
||||
- name: Create GitHub Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: v1.0.${{ github.run_number }} # 自动递增版本号
|
||||
release_name: "Release v1.0.${{ github.run_number }} - ${{ env.release_date }}"
|
||||
body: |
|
||||
### Release Notes
|
||||
- **Commit SHA**: ${{ env.commit_sha }}
|
||||
- **Commit Message**: ${{ env.commit_message }}
|
||||
- **Release Date**: ${{ env.release_date }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
# 上传压缩文件到 Release
|
||||
- name: Upload Release Assets
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: release-v1.0.${{ github.run_number }}.zip # 上传压缩文件
|
||||
asset_name: release-v1.0.${{ github.run_number }}.zip # 上传文件的名称
|
||||
asset_content_type: application/zip
|
||||
@@ -1,15 +1,42 @@
|
||||
# CloudFlare-ImgBed
|
||||
|
||||
免费图片/文件托管解决方案,基于 Cloudflare Pages 和 Telegram,支持 Telegram Bot 存储渠道和 Cloudflare R2 存储渠道。
|
||||
免费图片/文件托管解决方案,基于 Cloudflare Pages 和 Telegram,支持 Telegram Bot 、 Cloudflare R2 ,S3 API 等多种不同存储渠道。
|
||||
|
||||
**前端仓库**:[MarSeventh/Sanyue-ImgHub](https://github.com/MarSeventh/Sanyue-ImgHub)
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> **v2.0 版本升级注意事项请查看公告!**
|
||||
|
||||
<details>
|
||||
<summary>公告</summary>
|
||||
部署使用出现问题,请先仔细查阅文档、常见问题解答以及已有issues。
|
||||
|
||||
|
||||
|
||||
## 置顶
|
||||
|
||||
部署使用出现问题,请先仔细查阅文档、常见问题解答以及已有issues。
|
||||
**注意**:本仓库为[Telegraph-Image](https://github.com/cf-pages/Telegraph-Image)项目的重制版,如果你觉得本项目不错,在支持本项目的同时,也请支持原项目。
|
||||
|
||||
## 2025.2.6 V2.0 版本升级注意事项
|
||||
|
||||
> v2.0 beta 版已发布,相较于 v1.0 版本进行了大量改动和优化,但 beta 版本可能存在潜在不稳定性,若您追求稳定,可选择暂缓更新。
|
||||
>
|
||||
> 由于**构建命令发生了变化**,此次更新需要您**手动进行**,请按照以下步骤进行操作:
|
||||
>
|
||||
> - 同步fork的仓库至最新版(若已自动同步可忽略)
|
||||
>
|
||||
> - 前往 pages 管理页面,进入`设置`->`构建`,编辑`构建配置`,在`构建命令`处填写`npm install`
|
||||
>
|
||||
> 
|
||||
>
|
||||
> - 新版本所有设置项已**迁移至 管理端->系统设置 界面**,原则上无需再通过环境变量的方式进行设置,通过系统设置界面进行的设置将**覆盖掉**环境变量中的设置,但为了保证 **Telegram渠道的图片** 能够与旧版本相兼容,**若您之前设置了 Telegram 渠道相关的环境变量,请将其保留!**
|
||||
>
|
||||
> - 确保上述设置完成无误后,前往 pages 管理页面,进入`部署`,对最后一次不成功的部署进行`重试操作`
|
||||
|
||||
## 关于切换到 Telegram 渠道的通知
|
||||
|
||||
|
||||
> 由于telegraph图床被滥用,该项目上传渠道已切换至Telegram Channel,请**更新至最新版(更新方式见第3.1章最后一节)**,按照文档中的部署要求**设置`TG_BOT_TOKEN`和`TG_CHAT_ID`**,否则将无法正常使用上传功能。
|
||||
>
|
||||
> 此外,目前**KV数据库为必须配置**,如果以前未配置请按照文档说明配置。
|
||||
@@ -45,6 +72,31 @@
|
||||
<summary>更新日志</summary>
|
||||
|
||||
|
||||
## 2025.2.6
|
||||
|
||||
**v2.0版本焕新登场**,带来多项新功能和优化,给您焕然一新的用户体验:
|
||||
|
||||
💪**更强大**:
|
||||
|
||||
- 接入 S3 API 渠道,支持 Cloudflare R2 , Backblaze B2 ,七牛云,又拍云等多个服务商的对象存储服务
|
||||
- 支持设置多个 Telegram 和 S3 渠道,支持多渠道负载均衡
|
||||
- 上传文件支持短链接命名方式
|
||||
|
||||
✈️**更高效**:
|
||||
|
||||
- 全部设置项迁移到管理端系统设置界面,无需进行环境变量的繁琐配置,立即设置立即生效
|
||||
- 管理端 Gallery 和 用户管理 等页面实现分页读取,提升前端渲染速度,优化使用体验
|
||||
- 支持禁用、启用渠道,渠道管理自在掌握
|
||||
- 多个设置项加入提示弹窗,不用到处翻阅文档,设置更踏实
|
||||
|
||||
✨**更精致**:
|
||||
|
||||
- 全局支持深色模式,根据用户喜好和时间自动切换,凸显满满高级感
|
||||
- 登陆页面、图库页面、用户管理页面等多个页面细节重新打磨,操作更直观
|
||||
- 上传页全新 Tab 栏,一拉一合,灵动又便捷
|
||||
- Logo 焕新,纯手工打造,能力有限,不喜勿喷(
|
||||
- 支持自定义隐藏页脚,强迫症患者有救啦
|
||||
|
||||
## 2024.12.27
|
||||
|
||||
Add Features:
|
||||
@@ -221,7 +273,7 @@ Add Features:
|
||||
|
||||
此外,上传**并没有严格限制文件类型**,理论上你可以上传**任何**文件,但是暂时不会针对图片和视频外的文件进行特殊优化和适配。
|
||||
|
||||

|
||||

|
||||
|
||||
# 2.Features
|
||||
|
||||
@@ -236,12 +288,13 @@ Add Features:
|
||||
|
||||
- 流畅丝滑的过渡动画~
|
||||
- 上传文件实现呼吸灯效果
|
||||
- 灵动的操作体验
|
||||
|
||||
- **人性化上传**
|
||||
|
||||
- **覆盖大多数文件格式**:支持绝大多数常见**图片、视频、动图**等,同时也支持其他大多数格式的文件
|
||||
|
||||
- **支持多种存储渠道**:支持 **Telegram Bot**, **Cloudflare R2** 等多种存储渠道一键切换
|
||||
- **支持多种存储渠道**:支持 **Telegram Bot**, **Cloudflare R2**, **S3** 等多种存储渠道一键切换
|
||||
|
||||
> Telegram Bot渠道:上传文件大小限制为20MB,提供客户端和服务端压缩功能
|
||||
>
|
||||
@@ -282,6 +335,7 @@ Add Features:
|
||||
> 1. 自定义图床名称和Logo
|
||||
> 2. 自定义网站标题和Icon
|
||||
> 3. 页脚传送门自定义
|
||||
> 4. 支持页脚隐藏
|
||||
|
||||
- **一些小功能**
|
||||
- 支持**随机图**API,从图床中随机返回一张图片
|
||||
@@ -359,6 +413,8 @@ Add Features:
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
- **S3** API渠道:在服务提供商处,准备`S3_ACCESS_KEY_ID`、`S3_SECRET_ACCESS_KEY`、`S3_BUCKET_NAME`、`S3_ENDPOINT`等必须参数。
|
||||
|
||||
</details>
|
||||
|
||||
@@ -373,7 +429,53 @@ Add Features:
|
||||
需准备一个**Cloudflare账户**,然后按照以下步骤即可完成部署。
|
||||
|
||||
<details>
|
||||
<summary>部署在Cloudflare上的方式</summary>
|
||||
<summary>v2.0版本 部署在Cloudflare上的方式</summary>
|
||||
|
||||
按照以下步骤部署图床在 CloudFlare Pages 上。
|
||||
|
||||
1. Fork 本仓库
|
||||
|
||||
2. 打开 Cloudflare Dashboard,进入 Pages 管理页面,选择创建项目,选择`连接到 Git 提供程序`
|
||||
|
||||

|
||||
|
||||
3. 按照页面提示输入项目名称,选择需要连接的 git 仓库,点击`开始设置`
|
||||
|
||||
4. 填写`项目名称`,构建命令填写`npm install`,点击`保存并部署`
|
||||
|
||||

|
||||
|
||||
5. **绑定KV数据库**:
|
||||
|
||||
- 创建一个新的KV数据库
|
||||
|
||||
> 
|
||||
>
|
||||
> 
|
||||
|
||||
- 进入项目对应`设置`->`绑定`->`添加`->`KV 命名空间`->`变量名称`,填写`img_url`,`KV命名空间`选择刚才创建好的KV数据库
|
||||
|
||||
6. **重试部署**:前往项目管理界面->`部署`->`最新一次部署后面的···`->`重试部署`
|
||||
|
||||
7. 根据**所需存储渠道**进行相关设置:
|
||||
|
||||
- `Cloudflare R2`渠道:
|
||||
|
||||
将前面新建的存储桶绑定到项目(和KV绑定地方一样),**名称**为`img_r2`
|
||||
|
||||
> 
|
||||
|
||||
若要启用 R2 渠道的图像审查,请进入 项目管理页面->系统设置->上传设置 处设置 R2 的公开访问链接,值为前面记下的**R2存储桶公网访问链接**
|
||||
|
||||
- `其他渠道`:进入项目管理页面`https://你的域名/systemConfig#upload`,按照页面提示将3.1中获取的渠道参数填写到对应渠道中,保存设置即可
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary>v1.0版本 部署在Cloudflare上的方式</summary>
|
||||
|
||||
|
||||
|
||||
依托于CF的强大能力,只需简单几步,即可部署本项目,拥有自己的图床。
|
||||
@@ -386,7 +488,7 @@ Add Features:
|
||||
|
||||
3. 按照页面提示输入项目名称,选择需要连接的 git 仓库,点击`部署站点`
|
||||
|
||||
3. 根据**所需存储渠道**进行相关设置:
|
||||
4. 根据**所需存储渠道**进行相关设置:
|
||||
|
||||
- `Telegram 渠道`:将3.1.1中获取的`TG_BOT_TOKEN`和`TG_CHAT_ID`分别添加到环境变量中,对应**环境变量名为`TG_BOT_TOKEN`和`TG_CHAT_ID`**
|
||||
|
||||
@@ -399,6 +501,8 @@ Add Features:
|
||||
- 如果后续要开启**图像审查**,需要设置`R2PublicUrl`环境变量,值为前面记下的**R2存储桶公网访问链接**:
|
||||
|
||||
> 
|
||||
|
||||
- `S3 渠道`:将前面准备好的`S3_ACCESS_KEY_ID`、`S3_SECRET_ACCESS_KEY`、`S3_BUCKET_NAME`、`S3_ENDPOINT`及其对应值填入环境变量。
|
||||
|
||||
3. **绑定KV数据库**:
|
||||
|
||||
@@ -421,7 +525,46 @@ Add Features:
|
||||
注意由于服务器操作系统、硬件版本复杂多样,相关教程**无法确保适合每一位用户**,遇到报错请尽量利用搜索引擎解决,无法解决也可以提issue寻求帮助。
|
||||
|
||||
<details>
|
||||
<summary>部署在服务器上的方式</summary>
|
||||
<summary>v2.0 部署在服务器上的方式</summary>
|
||||
|
||||
|
||||
|
||||
1. 安装服务器操作系统对应的`node.js`,经测试`v22.5.1`版本可以正常使用。(安装教程自行search)
|
||||
|
||||
2. 切换到项目根目录,运行`npm install`,安装所需依赖。
|
||||
|
||||
3. 在项目根目录下新建`wrangler.toml`配置文件,其内容为项目名称,环境变量等。(详情参见官方文档[Configuration - Wrangler (cloudflare.com)](https://developers.cloudflare.com/workers/wrangler/configuration/))
|
||||
|
||||
> 配置文件样例:
|
||||
>
|
||||
> ```toml
|
||||
> name = "cloudflare-imgbed"
|
||||
> compatibility_date = "2024-07-24"
|
||||
> ```
|
||||
|
||||
4. 在项目根目录下运行`npm run start`,至此,正常情况下项目已经成功部署。项目默认支持通过服务器**本地模拟的R2存储上传**,可通过**管理端系统设置页面**进行其他设置。
|
||||
|
||||
程序默认运行在`8080`端口上,使用`nginx`等服务器反代`127.0.0.1:8080`即可外网访问服务。如需修改端口,可在`package.json`中修改`start`脚本的`port`参数(如下)。
|
||||
|
||||
```toml
|
||||
"scripts": {
|
||||
"ci-test": "concurrently --kill-others \"npm start\" \"wait-on http://localhost:8080 && mocha\"",
|
||||
"test": "mocha",
|
||||
"start": "npx wrangler pages dev ./ --kv \"img_url\" --r2 \"img_r2\" --port 8080 --persist-to ./data"
|
||||
}
|
||||
```
|
||||
|
||||
正常启动,控制台输出如下:
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary>v1.0 部署在服务器上的方式</summary>
|
||||
|
||||
|
||||
|
||||
1. 安装服务器操作系统对应的`node.js`,经测试`v22.5.1`版本可以正常使用。(安装教程自行search)
|
||||
@@ -468,7 +611,19 @@ Add Features:
|
||||
### 3.1.3可选配置
|
||||
|
||||
<details>
|
||||
<summary>后台认证、自定义页面、缓存清除等设置</summary>
|
||||
<summary>v2.0 后台认证、自定义页面、缓存清除等设置</summary>
|
||||
|
||||
|
||||
|
||||
请前往 管理端->系统设置 界面,按照提示进行设置,如果有不清楚的设置项,可参考下方v1.0版本的设置介绍内容。
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary>v1.0 后台认证、自定义页面、缓存清除等设置</summary>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -517,18 +672,19 @@ Web端在登录页面输入你的**认证码**即可登录使用;API端需要
|
||||
|
||||
环境变量增加`USER_CONFIG`,JSON格式(设置时类型选`text`即可),具体字段用途及内容规范见下表。
|
||||
|
||||
| 字段名 | 用途 | 类型 | 内容规范 |
|
||||
| ----------- | -------------------- | ------------- | ------------------------------------------------------------ |
|
||||
| loginBkImg | 自定义登录页面背景 | 列表/字符串 | 1、当字段类型为`列表`时,列表中元素为需要添加到轮播列表中的图片链接(列表中只有一张图时即为固定背景),形如`["1.jpg","2.jpg"]`<br />2、当字段类型为`字符串`时,目前**仅支持**字符串值为`bing`,设置为该值时启用bing随机图片轮播模式。 |
|
||||
| uploadBkImg | 自定义上传页面背景 | 列表/字符串 | 同上 |
|
||||
| bkInterval | 轮播背景切换时间间隔 | 正整数 | 设置为背景图的轮播时间,默认`3000`,单位`ms`。<br />例如你希望10s切换一次,设置为`10000`即可。 |
|
||||
| bkOpacity | 背景图透明度 | (0,1]的浮点数 | 展示的背景图透明度,默认为`1`。<br />如果你觉得显示效果不佳,可以自定义,如`0.8` |
|
||||
| ownerName | 页内图床名称 | 字符串 | 只支持`字符串`类型,设置为你自定义的图床名称(默认为`Sanyue`) |
|
||||
| logoUrl | 页内图床Logo | 字符串 | 只支持`字符串`类型,设置为你自定义的图床Logo链接 |
|
||||
| siteTitle | 网站标题 | 字符串 | 只支持`字符串`类型,设置为你自定义的网站标题 |
|
||||
| siteIcon | 网站图标 | 字符串 | 只支持`字符串`类型,设置为你自定义的网站图标链接 |
|
||||
| footerLink | 页脚传送门链接 | 字符串 | 只支持`字符串`类型,设置为你自定义的传送地址(如个人博客链接) |
|
||||
| urlPrefix | 全局默认链接前缀 | 字符串 | 只支持`字符串`类型,设置为自定义的全局默认链接前缀,该前缀会覆盖原始默认前缀,但不会覆盖用户自定义的链接前缀 |
|
||||
| 字段名 | 用途 | 类型 | 内容规范 |
|
||||
| ------------- | -------------------- | ------------- | ------------------------------------------------------------ |
|
||||
| loginBkImg | 自定义登录页面背景 | 列表/字符串 | 1、当字段类型为`列表`时,列表中元素为需要添加到轮播列表中的图片链接(列表中只有一张图时即为固定背景),形如`["1.jpg","2.jpg"]`<br />2、当字段类型为`字符串`时,目前**仅支持**字符串值为`bing`,设置为该值时启用bing随机图片轮播模式。 |
|
||||
| uploadBkImg | 自定义上传页面背景 | 列表/字符串 | 同上 |
|
||||
| bkInterval | 轮播背景切换时间间隔 | 正整数 | 设置为背景图的轮播时间,默认`3000`,单位`ms`。<br />例如你希望10s切换一次,设置为`10000`即可。 |
|
||||
| bkOpacity | 背景图透明度 | (0,1]的浮点数 | 展示的背景图透明度,默认为`1`。<br />如果你觉得显示效果不佳,可以自定义,如`0.8` |
|
||||
| ownerName | 页内图床名称 | 字符串 | 只支持`字符串`类型,设置为你自定义的图床名称(默认为`Sanyue`) |
|
||||
| logoUrl | 页内图床Logo | 字符串 | 只支持`字符串`类型,设置为你自定义的图床Logo链接 |
|
||||
| siteTitle | 网站标题 | 字符串 | 只支持`字符串`类型,设置为你自定义的网站标题 |
|
||||
| siteIcon | 网站图标 | 字符串 | 只支持`字符串`类型,设置为你自定义的网站图标链接 |
|
||||
| footerLink | 页脚传送门链接 | 字符串 | 只支持`字符串`类型,设置为你自定义的传送地址(如个人博客链接) |
|
||||
| disableFooter | 禁用页脚 | boolean | 支持`boolean`类型,设为`true`时禁用页脚,默认`false` |
|
||||
| urlPrefix | 全局默认链接前缀 | 字符串 | 只支持`字符串`类型,设置为自定义的全局默认链接前缀,该前缀会覆盖原始默认前缀,但不会覆盖用户自定义的链接前缀 |
|
||||
|
||||
> 整体示例:
|
||||
>
|
||||
@@ -613,11 +769,15 @@ Web端在登录页面输入你的**认证码**即可登录使用;API端需要
|
||||
|
||||
# 4.Usage
|
||||
|
||||
## 4.1Web端使用方式
|
||||
## 4.1Web端使用
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 4.2API文档
|
||||
|
||||
@@ -630,7 +790,7 @@ Web端在登录页面输入你的**认证码**即可登录使用;API端需要
|
||||
| ------------ | ------------------------------------------------------------ |
|
||||
| **接口功能** | 上传图片或视频 |
|
||||
| **请求方法** | POST |
|
||||
| **请求参数** | **Query参数**:<br />`authCode`: string类型,即为你设置的认证码<br />`serverCompress`: boolean类型,表示是否开启服务端压缩(仅针对图片文件、Telegram上传渠道生效,默认为`true`)<br />`uploadChannel`: string类型,取值为`telegram`和`cfr2`,分别代表telegram bot渠道和Cloudflare R2渠道,默认为`telegram` 渠道<br />`autoRetry`: boolean类型,表示是否开启上传失败自动切换渠道重试,默认开启<br />`uploadNameType`: string类型,表示文件命名方式,可选值为`[default, index, origin]`,分别代表默认`前缀_原名`命名、`仅前缀`命名和`仅原名`命名法,默认为`default`<br />`returnFormat`:string类型,表示返回链接格式,可选值为`[default, full]`,分别代表默认的`/file/id`格式、完整链接格式<br />**Body参数(application/form-data)**:<br />`file`: file类型,你要上传的文件 |
|
||||
| **请求参数** | **Query参数**:<br />`authCode`: string类型,即为你设置的认证码<br />`serverCompress`: boolean类型,表示是否开启服务端压缩(仅针对图片文件、Telegram上传渠道生效,默认为`true`)<br />`uploadChannel`: string类型,取值为`telegram`和`cfr2`,分别代表telegram bot渠道和Cloudflare R2渠道,默认为`telegram` 渠道<br />`autoRetry`: boolean类型,表示是否开启上传失败自动切换渠道重试,默认开启<br />`uploadNameType`: string类型,表示文件命名方式,可选值为`[default, index, origin, short]`,分别代表默认`前缀_原名`命名、`仅前缀`命名、`仅原名`命名和`短链接`命名法,默认为`default`<br />`returnFormat`:string类型,表示返回链接格式,可选值为`[default, full]`,分别代表默认的`/file/id`格式、完整链接格式<br />**Body参数(application/form-data)**:<br />`file`: file类型,你要上传的文件 |
|
||||
| **返回响应** | `data[0].src`为获得的图片链接(注意不包含域名,需要自己添加) |
|
||||
|
||||
> **请求示例**:
|
||||
@@ -700,6 +860,7 @@ Web端在登录页面输入你的**认证码**即可登录使用;API端需要
|
||||
- ~~背景透明度支持自定义~~(2024.9.12已完成)
|
||||
- ~~页脚自定义传送门~~(2024.10.20已完成)
|
||||
- ~~全局自定义链接前缀~~(2024.12.27已完成)
|
||||
- ~~可隐藏页脚~~(2025.2.4已完成)
|
||||
5. :white_check_mark:~~增加随机图API~~(2024.7.25已完成)
|
||||
6. :white_check_mark:~~完善多格式链接展示形式,增加ubb格式链接支持~~(2024.8.21已完成)
|
||||
7. :white_check_mark:~~完善登录逻辑,后端增加认证码校验接口~~(2024.8.21已完成)
|
||||
@@ -718,20 +879,24 @@ Web端在登录页面输入你的**认证码**即可登录使用;API端需要
|
||||
20. :white_check_mark:~~Telegram Channel渠道上传文件记录机器人和频道数据,便于迁移和备份~~(2024.12.4已完成)
|
||||
21. :white_check_mark:~~支持自定义命名方式(仅原名 or 仅随机前缀 or 默认的随机前缀\_原名)~~(2024.12.4已完成)
|
||||
22. :white_check_mark:~~支持上传失败自动切换其他渠道尝试~~(2024.12.12已完成)
|
||||
23. :hourglass_flowing_sand:后端list接口实现分页功能
|
||||
23. :white_check_mark:~~后端list接口实现分页功能~~(2024.2.5已完成)
|
||||
24. :white_check_mark:~~支持自定义链接前缀~~(2024.12.4已完成)
|
||||
25. :memo:对接alist,或实现webdav(评估中)
|
||||
26. :white_check_mark:~~文件详情增加文件大小记录~~(2024.12.10已完成)
|
||||
27. :white_check_mark:支持管理员自定义全局默认链接前缀
|
||||
27. :white_check_mark:~~支持管理员自定义全局默认链接前缀~~(2025.2.1已完成)
|
||||
28. :white_check_mark:~~开放更多文件格式~~(2024.12.9已完成)
|
||||
29. :white_check_mark:~~进行删除、加入白名单、加入黑名单等操作时,自动清除CF CDN缓存,避免延迟生效~~(2024.12.11已完成)
|
||||
30. :white_check_mark:~~管理端批量选择时,记录用户选择的顺序~~(2024.12.20已完成)
|
||||
31. :memo:上传图片支持自定义上传路径,支持相册功能(评估中)
|
||||
32. :hourglass_flowing_sand:支持多个 Telegram Bot Token 负载均衡
|
||||
33. :hourglass_flowing_sand:管理端提供详细的设置信息和设置方式引导
|
||||
34. :hourglass_flowing_sand:Logo焕新、登录页面优化、多项展示效果优化
|
||||
35. :hourglass_flowing_sand:接入S3 API渠道、CF Bed渠道
|
||||
36. :hourglass_flowing_sand:支持短链接命名方式
|
||||
32. :white_check_mark:~~支持多个 Telegram Bot Token 负载均衡~~(2025.2.4已完成)
|
||||
33. :white_check_mark:~~管理端提供详细的设置信息和设置方式引导~~(2025.2.5已完成)
|
||||
34. :white_check_mark:~~Logo焕新、登录页面优化、设置提示项等多项展示效果优化~~(2025.2.2已完成)
|
||||
35. :white_check_mark:~~接入S3 API渠道~~(2024.2.3已完成)
|
||||
36. :white_check_mark:~~支持短链接命名方式~~(2025.2.1已完成)
|
||||
37. :white_check_mark:~~支持深色模式~~(2025.1.11已完成)
|
||||
38. :hourglass_flowing_sand:支持KV备份恢复功能
|
||||
39. :white_check_mark:~~页脚可自定义隐藏~~(2025.2.4已完成)
|
||||
40. :hourglass_flowing_sand:搜索功能增强
|
||||
|
||||
</details>
|
||||
|
||||
@@ -804,6 +969,15 @@ Web端在登录页面输入你的**认证码**即可登录使用;API端需要
|
||||
- **赞助**:项目维护不易,喜欢本项目的话,可以作者大大一点小小的鼓励哦,您的每一份支持都是我前进的动力\~
|
||||
|
||||
<a href="https://afdian.com/a/marseventh"><img width="200" src="https://pic1.afdiancdn.com/static/img/welcome/button-sponsorme.png" alt=""></a>
|
||||
|
||||
- **Sponsors**:感谢以下赞助者对本项目的支持!
|
||||
|
||||
<div style="display: flex; flex-wrap: wrap;">
|
||||
<div style="margin-right: 20px; text-align: center;">
|
||||
<img src="https://pic1.afdiancdn.com/user/e8af1436138e11ed945852540025c377/avatar/59db0533d82e4198f59e63df63a1917f_w640_h640_s114.jpeg?imageView2/1/w/240/h/240" width="100"/>
|
||||
<div style="margin-top: 10px;">暔晓</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
# 8.Star History
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
.page-footer[data-v-6eac6ae4]{display:flex;justify-content:center;align-items:center;width:100vw;color:var(--page-footer-text-color);font-size:large;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media (max-width:768px){.page-footer[data-v-6eac6ae4]{font-size:small}}.footer-name[data-v-6eac6ae4]{color:var(--page-footer-name-color);font-weight:700;text-decoration:none}.footer-link-icon[data-v-6eac6ae4]{color:var(--page-footer-name-color);margin-left:5px}#themeToggle[data-v-019ed83e]{border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;width:2.5rem;height:2.5rem}@media (max-width:768px){#themeToggle[data-v-019ed83e]{width:2rem;height:2rem}}.theme_toggle_circle1[data-v-019ed83e]{transition:cx .5s ease-in-out,cy .5s ease-in-out,r .5s ease-in-out}.theme_toggle_circle2[data-v-019ed83e]{transition:all .5s ease-in-out;fill:var(--theme-toggle-bg-color)}.theme_toggle_svg[data-v-019ed83e]{transition:transform .5s cubic-bezier(.68,-.55,.27,1.55);transform:rotate(90deg);color:var(--theme-toggle-color)}.dark.theme_toggle_svg[data-v-019ed83e]{transform:rotate(40deg)}.theme_toggle_g[data-v-019ed83e]{transition:opacity .5s ease-in-out}.login[data-v-d60f6b8c]{display:flex;justify-content:center;align-items:center;flex-direction:column;height:100vh;background:var(--admin-container-bg-color)}.login-title[data-v-d60f6b8c]{font-size:2.5rem;margin-bottom:15px;color:var(--login-title-color);font-family:Noto Sans SC,sans-serif}@media (max-width:768px){.login-title[data-v-d60f6b8c]{font-size:1.5rem}}.login-container[data-v-d60f6b8c]{display:flex;flex-direction:column;justify-content:space-around;align-items:center;height:40vh;width:40vw;border-radius:12px;box-shadow:var(--login-container-box-shadow);background-color:var(--login-container-bg-color);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);transition:all .3s ease}@media (max-width:768px){.login-container[data-v-d60f6b8c]{width:80vw}}.login-container[data-v-d60f6b8c]:hover{box-shadow:var(--login-container-hover-box-shadow);transform:translateY(-5px)}.input-container[data-v-d60f6b8c]{display:flex;align-items:center;width:35vw}@media (max-width:768px){.input-container[data-v-d60f6b8c]{width:75vw}}.input-name[data-v-d60f6b8c]{width:15%;color:var(--login-title-color);text-align:right;margin-right:10px}@media (max-width:768px){.input-name[data-v-d60f6b8c]{width:20%}}.submit[data-v-d60f6b8c]{margin-top:10px;width:40%;height:15%;border-radius:12px;background-color:var(--login-submit-btn-bg-color);transition:all .3s ease;border:none}.password-input[data-v-d60f6b8c]{width:80%;height:140%}.password-input[data-v-d60f6b8c] .el-input__wrapper{border-radius:12px;background-color:var(--password-input-bg-color);border:var(--password-input-border);box-shadow:none}@media (max-width:768px){.password-input[data-v-d60f6b8c]{width:75%}}.footer[data-v-d60f6b8c]{position:fixed;bottom:0;width:100vw}.toggle-dark[data-v-d60f6b8c]{position:fixed;top:30px;right:30px;border:none;transition:all .3s ease;background-color:var(--toolbar-button-bg-color);box-shadow:var(--toolbar-button-shadow);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border-radius:12px}
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1 +0,0 @@
|
||||
.page-footer[data-v-eaaaff46]{display:flex;justify-content:center;align-items:center;width:100vw;color:var(--page-footer-text-color);font-size:large;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media (max-width:768px){.page-footer[data-v-eaaaff46]{font-size:small}}.footer-name[data-v-eaaaff46]{color:var(--page-footer-name-color);font-weight:700;text-decoration:none}.footer-link-icon[data-v-eaaaff46]{color:var(--page-footer-name-color);margin-left:5px}#themeToggle[data-v-18658a10]{border:0;cursor:pointer;display:flex;align-items:center;justify-content:center;width:2.5rem;height:2.5rem;border-radius:.375rem;transition-property:background-color,border-color,color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;position:fixed;top:3vh;right:1.5vw}.theme_toggle_circle1[data-v-18658a10]{transition:cx .5s ease-in-out,cy .5s ease-in-out,r .5s ease-in-out}.theme_toggle_circle2[data-v-18658a10]{transition:all .5s ease-in-out;fill:var(--theme-toggle-bg-color)}.theme_toggle_svg[data-v-18658a10]{transition:transform .5s cubic-bezier(.68,-.55,.27,1.55);transform:rotate(90deg);color:var(--theme-toggle-color)}.dark.theme_toggle_svg[data-v-18658a10]{transform:rotate(40deg)}.theme_toggle_g[data-v-18658a10]{transition:opacity .5s ease-in-out}.login[data-v-fd423e52]{display:flex;justify-content:center;align-items:center;flex-direction:column;height:100vh;background:var(--admin-container-bg-color)}.login-title[data-v-fd423e52]{font-size:2.5rem;margin-bottom:15px;color:var(--login-title-color);font-family:Noto Sans SC,sans-serif}@media (max-width:768px){.login-title[data-v-fd423e52]{font-size:1.5rem}}.login-container[data-v-fd423e52]{display:flex;flex-direction:column;justify-content:space-around;align-items:center;height:40vh;width:40vw;border-radius:12px;box-shadow:0 0 12px rgba(0,0,0,.12);background-color:var(--login-container-bg-color);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);transition:all .3s ease}@media (max-width:768px){.login-container[data-v-fd423e52]{width:80vw}}.login-container[data-v-fd423e52]:hover{box-shadow:0 0 12px 4px rgba(0,0,0,.24);transform:translateY(-5px)}.input-container[data-v-fd423e52]{display:flex;align-items:center;width:35vw}@media (max-width:768px){.input-container[data-v-fd423e52]{width:75vw}}.input-name[data-v-fd423e52]{width:15%;color:var(--login-title-color)}@media (max-width:768px){.input-name[data-v-fd423e52]{width:20%}}.submit[data-v-fd423e52]{margin-top:10px;width:40%;height:10%;border-radius:12px}.password-input[data-v-fd423e52]{width:80%;height:120%}.password-input[data-v-fd423e52] .el-input__wrapper{border-radius:12px;background-color:var(--password-input-bg-color);border:var(--password-input-border);box-shadow:none}@media (max-width:768px){.password-input[data-v-fd423e52]{width:75%}}.footer[data-v-fd423e52]{position:fixed;bottom:0;width:100vw}
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1 +0,0 @@
|
||||
.page-footer[data-v-eaaaff46]{display:flex;justify-content:center;align-items:center;width:100vw;color:var(--page-footer-text-color);font-size:large;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media (max-width:768px){.page-footer[data-v-eaaaff46]{font-size:small}}.footer-name[data-v-eaaaff46]{color:var(--page-footer-name-color);font-weight:700;text-decoration:none}.footer-link-icon[data-v-eaaaff46]{color:var(--page-footer-name-color);margin-left:5px}#themeToggle[data-v-18658a10]{border:0;cursor:pointer;display:flex;align-items:center;justify-content:center;width:2.5rem;height:2.5rem;border-radius:.375rem;transition-property:background-color,border-color,color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;position:fixed;top:3vh;right:1.5vw}.theme_toggle_circle1[data-v-18658a10]{transition:cx .5s ease-in-out,cy .5s ease-in-out,r .5s ease-in-out}.theme_toggle_circle2[data-v-18658a10]{transition:all .5s ease-in-out;fill:var(--theme-toggle-bg-color)}.theme_toggle_svg[data-v-18658a10]{transition:transform .5s cubic-bezier(.68,-.55,.27,1.55);transform:rotate(90deg);color:var(--theme-toggle-color)}.dark.theme_toggle_svg[data-v-18658a10]{transform:rotate(40deg)}.theme_toggle_g[data-v-18658a10]{transition:opacity .5s ease-in-out}.login[data-v-507d6e22]{display:flex;justify-content:center;align-items:center;flex-direction:column;height:100vh;background-color:var(--bg-color)}.login-container[data-v-507d6e22]{display:flex;flex-direction:column;justify-content:space-around;align-items:center;height:40vh;width:40vw;border-radius:12px;box-shadow:0 0 12px rgba(0,0,0,.12);background-color:var(--login-container-bg-color);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);transition:all .3s ease}@media (max-width:768px){.login-container[data-v-507d6e22]{width:80vw}}.login-container[data-v-507d6e22]:hover{box-shadow:0 0 12px 4px rgba(0,0,0,.24);transform:translateY(-5px)}.login-title[data-v-507d6e22]{font-size:2.5rem;margin-bottom:15px;color:var(--login-title-color);font-family:Noto Sans SC,sans-serif}@media (max-width:768px){.login-title[data-v-507d6e22]{font-size:1.5rem}}.input-container[data-v-507d6e22]{display:flex;align-items:center;width:35vw}@media (max-width:768px){.input-container[data-v-507d6e22]{width:75vw}}.input-name[data-v-507d6e22]{width:15%;color:var(--login-title-color)}@media (max-width:768px){.input-name[data-v-507d6e22]{width:20%}}.password-input[data-v-507d6e22]{width:80%;height:120%}.password-input[data-v-507d6e22] .el-input__wrapper{border-radius:12px;background-color:var(--password-input-bg-color);border:var(--password-input-border);box-shadow:none}@media (max-width:768px){.password-input[data-v-507d6e22]{width:75%}}.submit[data-v-507d6e22]{margin-top:10px;width:40%;height:10%;border-radius:12px}.background-image1[data-v-507d6e22],.background-image2[data-v-507d6e22]{position:fixed;top:0;left:0;width:100%;height:100%;-o-object-fit:cover;object-fit:cover;z-index:-1;opacity:0;transition:all 1s ease-in-out}.footer[data-v-507d6e22]{position:fixed;bottom:0;width:100vw}
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
|
||||
.page-footer[data-v-6eac6ae4]{display:flex;justify-content:center;align-items:center;width:100vw;color:var(--page-footer-text-color);font-size:large;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media (max-width:768px){.page-footer[data-v-6eac6ae4]{font-size:small}}.footer-name[data-v-6eac6ae4]{color:var(--page-footer-name-color);font-weight:700;text-decoration:none}.footer-link-icon[data-v-6eac6ae4]{color:var(--page-footer-name-color);margin-left:5px}#themeToggle[data-v-019ed83e]{border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;width:2.5rem;height:2.5rem}@media (max-width:768px){#themeToggle[data-v-019ed83e]{width:2rem;height:2rem}}.theme_toggle_circle1[data-v-019ed83e]{transition:cx .5s ease-in-out,cy .5s ease-in-out,r .5s ease-in-out}.theme_toggle_circle2[data-v-019ed83e]{transition:all .5s ease-in-out;fill:var(--theme-toggle-bg-color)}.theme_toggle_svg[data-v-019ed83e]{transition:transform .5s cubic-bezier(.68,-.55,.27,1.55);transform:rotate(90deg);color:var(--theme-toggle-color)}.dark.theme_toggle_svg[data-v-019ed83e]{transform:rotate(40deg)}.theme_toggle_g[data-v-019ed83e]{transition:opacity .5s ease-in-out}.login[data-v-7ecd2a2b]{display:flex;justify-content:center;align-items:center;flex-direction:column;height:100vh;background:var(--bg-color)}.login-container[data-v-7ecd2a2b]{display:flex;flex-direction:column;justify-content:space-around;align-items:center;height:40vh;width:40vw;border-radius:12px;box-shadow:var(--login-container-box-shadow);background-color:var(--login-container-bg-color);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);transition:all .3s ease}@media (max-width:768px){.login-container[data-v-7ecd2a2b]{width:80vw}}.login-container[data-v-7ecd2a2b]:hover{box-shadow:var(--login-container-hover-box-shadow);transform:translateY(-5px)}.login-title[data-v-7ecd2a2b]{font-size:2.1rem;margin-bottom:15px;color:var(--login-title-color);font-family:Noto Sans SC,sans-serif}@media (max-width:768px){.login-title[data-v-7ecd2a2b]{font-size:1.5rem}}.input-container[data-v-7ecd2a2b]{display:flex;align-items:center;justify-content:start;width:35vw}@media (max-width:768px){.input-container[data-v-7ecd2a2b]{width:75vw}}.input-name[data-v-7ecd2a2b]{width:14%;color:var(--login-title-color);text-align:right;margin-right:10px}@media (max-width:768px){.input-name[data-v-7ecd2a2b]{width:20%}}.password-input[data-v-7ecd2a2b]{width:78%;height:140%}.password-input[data-v-7ecd2a2b] .el-input__wrapper{border-radius:12px;background-color:var(--password-input-bg-color);border:var(--password-input-border);box-shadow:none}@media (max-width:768px){.password-input[data-v-7ecd2a2b]{width:75%}}.submit[data-v-7ecd2a2b]{margin-top:10px;width:40%;height:15%;border-radius:12px;background-color:var(--login-submit-btn-bg-color);transition:all .3s ease;border:none}.background-image1[data-v-7ecd2a2b],.background-image2[data-v-7ecd2a2b]{position:fixed;top:0;left:0;width:100%;height:100%;-o-object-fit:cover;object-fit:cover;z-index:-1;opacity:0;transition:all 1s ease-in-out}.footer[data-v-7ecd2a2b]{position:fixed;bottom:0;width:100vw}.toggle-dark[data-v-7ecd2a2b]{position:fixed;top:30px;right:30px;border:none;transition:all .3s ease;background-color:var(--toolbar-button-bg-color);box-shadow:var(--toolbar-button-shadow);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border-radius:12px}
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1,3 +1,5 @@
|
||||
import { fetchSecurityConfig } from "../utils/sysConfig";
|
||||
|
||||
export async function onRequestPost(context) {
|
||||
// Contents of context object
|
||||
const {
|
||||
@@ -11,8 +13,13 @@ export async function onRequestPost(context) {
|
||||
//从POST请求中获取authCode
|
||||
const jsonRequest = await request.json();
|
||||
const authCode = jsonRequest.authCode;
|
||||
|
||||
// 读取安全设置
|
||||
const securityConfig = await fetchSecurityConfig(env);
|
||||
const rightAuthCode = securityConfig.auth.user.authCode;
|
||||
|
||||
//验证authCode
|
||||
if (env.AUTH_CODE !== undefined && authCode !== env.AUTH_CODE) {
|
||||
if (rightAuthCode !== undefined && rightAuthCode !== '' && authCode !== rightAuthCode) {
|
||||
return new Response('Unauthorized', { status: 401 })
|
||||
}
|
||||
//返回登录成功
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { fetchSecurityConfig } from "../../utils/sysConfig";
|
||||
|
||||
let securityConfig = {}
|
||||
let basicUser = ""
|
||||
let basicPass = ""
|
||||
|
||||
async function errorHandling(context) {
|
||||
try {
|
||||
return await context.next();
|
||||
@@ -6,99 +12,102 @@ async function errorHandling(context) {
|
||||
}
|
||||
}
|
||||
|
||||
function basicAuthentication(request) {
|
||||
const Authorization = request.headers.get('Authorization');
|
||||
|
||||
const [scheme, encoded] = Authorization.split(' ');
|
||||
|
||||
// The Authorization header must start with Basic, followed by a space.
|
||||
if (!encoded || scheme !== 'Basic') {
|
||||
throw new BadRequestException('Malformed authorization header.');
|
||||
}
|
||||
|
||||
// Decodes the base64 value and performs unicode normalization.
|
||||
// @see https://datatracker.ietf.org/doc/html/rfc7613#section-3.3.2 (and #section-4.2.2)
|
||||
// @see https://dev.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/normalize
|
||||
const buffer = Uint8Array.from(atob(encoded), character => character.charCodeAt(0));
|
||||
const decoded = new TextDecoder().decode(buffer).normalize();
|
||||
|
||||
// The username & password are split by the first colon.
|
||||
//=> example: "username:password"
|
||||
const index = decoded.indexOf(':');
|
||||
|
||||
// The user & password are split by the first colon and MUST NOT contain control characters.
|
||||
// @see https://tools.ietf.org/html/rfc5234#appendix-B.1 (=> "CTL = %x00-1F / %x7F")
|
||||
if (index === -1 || /[\0-\x1F\x7F]/.test(decoded)) {
|
||||
throw new BadRequestException('Invalid authorization value.');
|
||||
}
|
||||
|
||||
return {
|
||||
user: decoded.substring(0, index),
|
||||
pass: decoded.substring(index + 1),
|
||||
};
|
||||
}
|
||||
|
||||
function UnauthorizedException(reason) {
|
||||
return new Response(reason, {
|
||||
status: 401,
|
||||
statusText: 'Unauthorized',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain;charset=UTF-8',
|
||||
// Disables caching by default.
|
||||
'Cache-Control': 'no-store',
|
||||
// Returns the "Content-Length" header for HTTP HEAD requests.
|
||||
'Content-Length': reason.length,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function BadRequestException(reason) {
|
||||
return new Response(reason, {
|
||||
status: 400,
|
||||
statusText: 'Bad Request',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain;charset=UTF-8',
|
||||
// Disables caching by default.
|
||||
'Cache-Control': 'no-store',
|
||||
// Returns the "Content-Length" header for HTTP HEAD requests.
|
||||
'Content-Length': reason.length,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function authentication(context) {
|
||||
//context.env.BASIC_USER="admin"
|
||||
//context.env.BASIC_PASS="admin"
|
||||
//check if the env variables Disable_Dashboard are set
|
||||
if (typeof context.env.img_url == "undefined" || context.env.img_url == null || context.env.img_url == "") {
|
||||
return new Response('Dashboard is disabled. Please bind a KV namespace to use this feature.', { status: 200 });
|
||||
}
|
||||
function basicAuthentication(request) {
|
||||
const Authorization = request.headers.get('Authorization');
|
||||
|
||||
if(typeof context.env.BASIC_USER == "undefined" || context.env.BASIC_USER == null || context.env.BASIC_USER == ""){
|
||||
return context.next();
|
||||
}else{
|
||||
if (context.request.headers.has('Authorization')) {
|
||||
// Throws exception when authorization fails.
|
||||
const { user, pass } = basicAuthentication(context.request);
|
||||
if (context.env.BASIC_USER !== user || context.env.BASIC_PASS !== pass) {
|
||||
return UnauthorizedException('Invalid credentials.');
|
||||
}else{
|
||||
return context.next();
|
||||
}
|
||||
|
||||
} else {
|
||||
return new Response('You need to login.', {
|
||||
status: 401,
|
||||
headers: {
|
||||
// Prompts the user for credentials.
|
||||
'WWW-Authenticate': 'Basic realm="my scope", charset="UTF-8"',
|
||||
// 'WWW-Authenticate': 'None',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const [scheme, encoded] = Authorization.split(' ');
|
||||
|
||||
// The Authorization header must start with Basic, followed by a space.
|
||||
if (!encoded || scheme !== 'Basic') {
|
||||
throw new BadRequestException('Malformed authorization header.');
|
||||
}
|
||||
|
||||
// Decodes the base64 value and performs unicode normalization.
|
||||
// @see https://datatracker.ietf.org/doc/html/rfc7613#section-3.3.2 (and #section-4.2.2)
|
||||
// @see https://dev.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/normalize
|
||||
const buffer = Uint8Array.from(atob(encoded), character => character.charCodeAt(0));
|
||||
const decoded = new TextDecoder().decode(buffer).normalize();
|
||||
|
||||
// The username & password are split by the first colon.
|
||||
//=> example: "username:password"
|
||||
const index = decoded.indexOf(':');
|
||||
|
||||
// The user & password are split by the first colon and MUST NOT contain control characters.
|
||||
// @see https://tools.ietf.org/html/rfc5234#appendix-B.1 (=> "CTL = %x00-1F / %x7F")
|
||||
if (index === -1 || /[\0-\x1F\x7F]/.test(decoded)) {
|
||||
throw new BadRequestException('Invalid authorization value.');
|
||||
}
|
||||
|
||||
return {
|
||||
user: decoded.substring(0, index),
|
||||
pass: decoded.substring(index + 1),
|
||||
};
|
||||
}
|
||||
|
||||
function UnauthorizedException(reason) {
|
||||
return new Response(reason, {
|
||||
status: 401,
|
||||
statusText: 'Unauthorized',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain;charset=UTF-8',
|
||||
// Disables caching by default.
|
||||
'Cache-Control': 'no-store',
|
||||
// Returns the "Content-Length" header for HTTP HEAD requests.
|
||||
'Content-Length': reason.length,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function BadRequestException(reason) {
|
||||
return new Response(reason, {
|
||||
status: 400,
|
||||
statusText: 'Bad Request',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain;charset=UTF-8',
|
||||
// Disables caching by default.
|
||||
'Cache-Control': 'no-store',
|
||||
// Returns the "Content-Length" header for HTTP HEAD requests.
|
||||
'Content-Length': reason.length,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function authentication(context) {
|
||||
// 读取安全配置
|
||||
securityConfig = await fetchSecurityConfig(context.env);
|
||||
basicUser = securityConfig.auth.admin.adminUsername
|
||||
basicPass = securityConfig.auth.admin.adminPassword
|
||||
|
||||
//check if the env variables Disable_Dashboard are set
|
||||
if (typeof context.env.img_url == "undefined" || context.env.img_url == null || context.env.img_url == "") {
|
||||
return new Response('Dashboard is disabled. Please bind a KV namespace to use this feature.', { status: 200 });
|
||||
}
|
||||
|
||||
if(typeof basicUser == "undefined" || basicUser == null || basicUser == ""){
|
||||
return context.next();
|
||||
}else{
|
||||
if (context.request.headers.has('Authorization')) {
|
||||
// Throws exception when authorization fails.
|
||||
const { user, pass } = basicAuthentication(context.request);
|
||||
if (basicUser !== user || basicPass !== pass) {
|
||||
return UnauthorizedException('Invalid credentials.');
|
||||
}else{
|
||||
return context.next();
|
||||
}
|
||||
|
||||
} else {
|
||||
return new Response('You need to login.', {
|
||||
status: 401,
|
||||
headers: {
|
||||
// Prompts the user for credentials.
|
||||
'WWW-Authenticate': 'Basic realm="my scope", charset="UTF-8"',
|
||||
// 'WWW-Authenticate': 'None',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const onRequest = [errorHandling, authentication];
|
||||
}
|
||||
|
||||
export const onRequest = [errorHandling, authentication];
|
||||
@@ -1,3 +1,5 @@
|
||||
import { purgeCFCache } from "../../../utils/purgeCache";
|
||||
|
||||
export async function onRequest(context) {
|
||||
// Contents of context object
|
||||
const {
|
||||
@@ -8,6 +10,7 @@ export async function onRequest(context) {
|
||||
next, // used for middleware or to fetch assets
|
||||
data, // arbitrary space for passing data between middlewares
|
||||
} = context;
|
||||
|
||||
// 组装 CDN URL
|
||||
const url = new URL(request.url);
|
||||
const cdnUrl = `https://${url.hostname}/file/${params.id}`;
|
||||
@@ -24,12 +27,7 @@ export async function onRequest(context) {
|
||||
const info = JSON.stringify(value.metadata);
|
||||
|
||||
// 清除CDN缓存
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json', 'X-Auth-Email': `${env.CF_EMAIL}`, 'X-Auth-Key': `${env.CF_API_KEY}`},
|
||||
body: `{"files":["${ cdnUrl }"]}`
|
||||
};
|
||||
await fetch(`https://api.cloudflare.com/client/v4/zones/${ env.CF_ZONE_ID }/purge_cache`, options);
|
||||
await purgeCFCache(env, cdnUrl);
|
||||
|
||||
return new Response(info);
|
||||
|
||||
|
||||
@@ -8,10 +8,6 @@ export async function onRequest(context) {
|
||||
next, // used for middleware or to fetch assets
|
||||
data, // arbitrary space for passing data between middlewares
|
||||
} = context;
|
||||
if(typeof context.env.BASIC_USER == "undefined" || context.env.BASIC_USER == null || context.env.BASIC_USER == ""){
|
||||
return new Response('Not using basic auth.', { status: 200 });
|
||||
}else{
|
||||
return new Response('true', { status: 200 });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return new Response('true', { status: 200 });
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
export async function onRequest(context) {
|
||||
// Contents of context object
|
||||
const {
|
||||
request, // same as existing Worker API
|
||||
env, // same as existing Worker API
|
||||
params, // if filename includes [id] or [[path]]
|
||||
waitUntil, // same as ctx.waitUntil in existing Worker API
|
||||
next, // used for middleware or to fetch assets
|
||||
data, // arbitrary space for passing data between middlewares
|
||||
} = context;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// 解析 URL 中的参数
|
||||
let start = parseInt(url.searchParams.get('start'), 10) || 0;
|
||||
let count = parseInt(url.searchParams.get('count'), 10) || 10;
|
||||
|
||||
start = Math.max(0, start); // start 不能小于 0
|
||||
count = Math.max(1, count); // count 不能小于 1
|
||||
|
||||
let allRecords = [];
|
||||
|
||||
allRecords = await getAllRecords(env);
|
||||
|
||||
// 按照 IP 分组
|
||||
const dealedData = await dealByIP(allRecords);
|
||||
|
||||
// 按照分组中的count倒序排序
|
||||
dealedData.sort((a, b) => {
|
||||
return b.count - a.count;
|
||||
});
|
||||
|
||||
const resultRecords = dealedData.slice(start, start + count);
|
||||
|
||||
// 只返回 `count` 条数据
|
||||
return new Response(JSON.stringify(resultRecords), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
async function getAllRecords(env) {
|
||||
let recordsFetched = 0;
|
||||
let allRecords = [];
|
||||
let cursor = null;
|
||||
|
||||
while (true) {
|
||||
const limit = 1000; // 读取所需的最少数量
|
||||
const response = await env.img_url.list({ limit, cursor });
|
||||
|
||||
// 过滤掉以 "manage@" 开头的 key
|
||||
const filteredRecords = response.keys.filter(item => !item.name.startsWith("manage@"));
|
||||
|
||||
allRecords.push(...filteredRecords);
|
||||
recordsFetched += filteredRecords.length;
|
||||
cursor = response.cursor;
|
||||
|
||||
if (!cursor) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return allRecords;
|
||||
}
|
||||
|
||||
async function dealByIP(data) {
|
||||
let dealedData = [];
|
||||
let ipSet = new Set();
|
||||
|
||||
data.forEach(item => {
|
||||
if (item.metadata?.UploadIP) {
|
||||
ipSet.add(item.metadata.UploadIP);
|
||||
}
|
||||
});
|
||||
ipSet.forEach(ip => {
|
||||
let ipData = data.filter(item => item.metadata?.UploadIP === ip);
|
||||
let count = ipData.length;
|
||||
dealedData.push({ip, count, data: ipData});
|
||||
});
|
||||
return dealedData;
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
import { S3Client, DeleteObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { purgeCFCache } from "../../../utils/purgeCache";
|
||||
|
||||
export async function onRequest(context) {
|
||||
// Contents of context object
|
||||
const {
|
||||
@@ -21,19 +24,44 @@ export async function onRequest(context) {
|
||||
|
||||
// 如果是R2渠道的图片,删除R2中对应的图片
|
||||
if (img.metadata?.Channel === 'CloudflareR2') {
|
||||
await env.img_r2.delete(params.id);
|
||||
await env.img_r2.delete(params.id);
|
||||
}
|
||||
|
||||
// S3 渠道的图片,删除S3中对应的图片
|
||||
if (img.metadata?.Channel === "S3") {
|
||||
const s3Client = new S3Client({
|
||||
region: img.metadata?.S3Region || "auto", // 默认使用 auto 区域
|
||||
endpoint: img.metadata?.S3Endpoint,
|
||||
credentials: {
|
||||
accessKeyId: img.metadata?.S3AccessKeyId,
|
||||
secretAccessKey: img.metadata?.S3SecretAccessKey
|
||||
},
|
||||
forcePathStyle: true
|
||||
});
|
||||
|
||||
const bucketName = img.metadata?.S3BucketName;
|
||||
const key = img.metadata?.S3FileKey;
|
||||
|
||||
try {
|
||||
const command = new DeleteObjectCommand({
|
||||
Bucket: bucketName,
|
||||
Key: key,
|
||||
});
|
||||
|
||||
await s3Client.send(command);
|
||||
|
||||
} catch (error) {
|
||||
return new Response(`Error: S3 Delete Failed - ${error.message}`, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// 删除KV中的图片信息
|
||||
await env.img_url.delete(params.id);
|
||||
const info = JSON.stringify(params.id);
|
||||
|
||||
// 清除CDN缓存
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json', 'X-Auth-Email': `${env.CF_EMAIL}`, 'X-Auth-Key': `${env.CF_API_KEY}`},
|
||||
body: `{"files":["${ cdnUrl }"]}`
|
||||
};
|
||||
await fetch(`https://api.cloudflare.com/client/v4/zones/${ env.CF_ZONE_ID }/purge_cache`, options);
|
||||
await purgeCFCache(env, cdnUrl);
|
||||
|
||||
// 清除api/randomFileList API缓存
|
||||
try {
|
||||
const cache = caches.default;
|
||||
|
||||
@@ -8,27 +8,74 @@ export async function onRequest(context) {
|
||||
next, // used for middleware or to fetch assets
|
||||
data, // arbitrary space for passing data between middlewares
|
||||
} = context;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// 解析 URL 中的参数
|
||||
let start = parseInt(url.searchParams.get('start'), 10) || 0;
|
||||
let count = parseInt(url.searchParams.get('count'), 10) || 50;
|
||||
let sum = url.searchParams.get('sum') || false;
|
||||
|
||||
// count 为 -1 时,返回所有数据
|
||||
if (count === -1) {
|
||||
const allRecords = await getAllRecords(env);
|
||||
// sum 参数为 true 时,只返回数据总数
|
||||
if (sum === 'true') {
|
||||
return new Response(JSON.stringify(
|
||||
{
|
||||
sum: allRecords.length
|
||||
}
|
||||
), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
} else {
|
||||
return new Response(JSON.stringify(allRecords), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 倒序返回指定数量的数据
|
||||
|
||||
start = Math.max(0, start); // start 不能小于 0
|
||||
count = Math.max(1, count); // count 不能小于 1
|
||||
|
||||
let allRecords = [];
|
||||
|
||||
allRecords = await getAllRecords(env);
|
||||
|
||||
// 按照 metadata 中的时间戳倒序排序
|
||||
allRecords.sort((a, b) => {
|
||||
return b.metadata.TimeStamp - a.metadata.TimeStamp;
|
||||
});
|
||||
|
||||
const resultRecords = allRecords.slice(start, start + count);
|
||||
|
||||
// 只返回 `count` 条数据
|
||||
return new Response(JSON.stringify(resultRecords), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
|
||||
async function getAllRecords(env) {
|
||||
let recordsFetched = 0;
|
||||
let allRecords = [];
|
||||
let cursor = null;
|
||||
|
||||
do {
|
||||
const records = await env.img_url.list({
|
||||
limit: 1000,
|
||||
cursor,
|
||||
});
|
||||
// 除去records中key以manage@开头的记录
|
||||
records.keys = records.keys.filter(item => !item.name.startsWith("manage@"));
|
||||
allRecords.push(...records.keys);
|
||||
cursor = records.cursor;
|
||||
} while (cursor);
|
||||
while (true) {
|
||||
const limit = 1000; // 读取所需的最少数量
|
||||
const response = await env.img_url.list({ limit, cursor });
|
||||
|
||||
const info = JSON.stringify(allRecords);
|
||||
let res = new Response(info, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// 过滤掉以 "manage@" 开头的 key
|
||||
const filteredRecords = response.keys.filter(item => !item.name.startsWith("manage@"));
|
||||
|
||||
allRecords.push(...filteredRecords);
|
||||
recordsFetched += filteredRecords.length;
|
||||
cursor = response.cursor;
|
||||
|
||||
if (!cursor) {
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res;
|
||||
return allRecords;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
export async function onRequest(context) {
|
||||
// 其他设置相关,GET方法读取设置,POST方法保存设置
|
||||
const {
|
||||
request, // same as existing Worker API
|
||||
env, // same as existing Worker API
|
||||
params, // if filename includes [id] or [[path]]
|
||||
waitUntil, // same as ctx.waitUntil in existing Worker API
|
||||
next, // used for middleware or to fetch assets
|
||||
data, // arbitrary space for passing data between middlewares
|
||||
} = context;
|
||||
|
||||
const kv = env.img_url
|
||||
|
||||
// GET读取设置
|
||||
if (request.method === 'GET') {
|
||||
const settings = await getOthersConfig(kv, env)
|
||||
|
||||
return new Response(JSON.stringify(settings), {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// POST保存设置
|
||||
if (request.method === 'POST') {
|
||||
const body = await request.json()
|
||||
const settings = body
|
||||
|
||||
// 写入 KV
|
||||
await kv.put('manage@sysConfig@others', JSON.stringify(settings))
|
||||
|
||||
return new Response(JSON.stringify(settings), {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export async function getOthersConfig(kv, env) {
|
||||
const settings = {}
|
||||
// 读取KV中的设置
|
||||
const settingsStr = await kv.get('manage@sysConfig@others')
|
||||
const settingsKV = settingsStr ? JSON.parse(settingsStr) : {}
|
||||
|
||||
// 远端遥测
|
||||
settings.telemetry = {
|
||||
enabled: !env.disable_telemetry === 'true',
|
||||
fixed: false,
|
||||
}
|
||||
|
||||
// 随机图API
|
||||
settings.randomImageAPI = {
|
||||
enabled: env.AllowRandom === 'true',
|
||||
fixed: false,
|
||||
}
|
||||
|
||||
// CloudFlare API Token
|
||||
settings.cloudflareApiToken = {
|
||||
CF_ZONE_ID: env.CF_ZONE_ID,
|
||||
CF_EMAIL: env.CF_EMAIL,
|
||||
CF_API_KEY: env.CF_API_KEY,
|
||||
fixed: false,
|
||||
}
|
||||
|
||||
// 用KV存储的设置覆盖默认设置
|
||||
Object.assign(settings, settingsKV)
|
||||
|
||||
return settings;
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
export async function onRequest(context) {
|
||||
// 页面设置相关,GET方法读取设置,POST方法保存设置
|
||||
const {
|
||||
request, // same as existing Worker API
|
||||
env, // same as existing Worker API
|
||||
params, // if filename includes [id] or [[path]]
|
||||
waitUntil, // same as ctx.waitUntil in existing Worker API
|
||||
next, // used for middleware or to fetch assets
|
||||
data, // arbitrary space for passing data between middlewares
|
||||
} = context;
|
||||
|
||||
const kv = env.img_url
|
||||
|
||||
// GET读取设置
|
||||
if (request.method === 'GET') {
|
||||
const settings = await getPageConfig(kv, env)
|
||||
|
||||
return new Response(JSON.stringify(settings), {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// POST保存设置
|
||||
if (request.method === 'POST') {
|
||||
const body = await request.json()
|
||||
const settings = body
|
||||
// 写入 KV
|
||||
await kv.put('manage@sysConfig@page', JSON.stringify(settings))
|
||||
|
||||
return new Response(JSON.stringify(settings), {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export async function getPageConfig(kv, env) {
|
||||
const settings = {}
|
||||
// 读取KV中的设置
|
||||
const settingsStr = await kv.get('manage@sysConfig@page')
|
||||
const settingsKV = settingsStr ? JSON.parse(settingsStr) : {}
|
||||
|
||||
const config = []
|
||||
settings.config = config
|
||||
config.push(
|
||||
{
|
||||
id: 'siteTitle',
|
||||
label: '网站标题',
|
||||
placeholder: 'Sanyue ImgHub',
|
||||
},
|
||||
{
|
||||
id: 'siteIcon',
|
||||
label: '网站图标',
|
||||
},
|
||||
{
|
||||
id: 'ownerName',
|
||||
label: '图床名称',
|
||||
placeholder: 'Sanyue ImgHub',
|
||||
},
|
||||
{
|
||||
id: 'logoUrl',
|
||||
label: '图床Logo',
|
||||
},
|
||||
{
|
||||
id: 'loginBkImg',
|
||||
label: '登录页背景图',
|
||||
tooltip: '1.填写 bing 使用必应壁纸轮播 <br/> 2.填写 ["url1","url2"] 使用多张图片轮播 <br/> 3.填写 ["url"] 使用单张图片',
|
||||
},
|
||||
{
|
||||
id: 'uploadBkImg',
|
||||
label: '上传页背景图',
|
||||
tooltip: '1.填写 bing 使用必应壁纸轮播 <br/> 2.填写 ["url1","url2"] 使用多张图片轮播 <br/> 3.填写 ["url"] 使用单张图片',
|
||||
},
|
||||
{
|
||||
id: 'bkInterval',
|
||||
label: '背景切换间隔',
|
||||
placeholder: '3000',
|
||||
tooltip: '单位:毫秒 ms',
|
||||
},
|
||||
{
|
||||
id: 'bkOpacity',
|
||||
label: '背景图透明度',
|
||||
placeholder: '1',
|
||||
tooltip: '0-1 之间的小数',
|
||||
},
|
||||
{
|
||||
id: 'footerLink',
|
||||
label: '页脚传送门链接',
|
||||
},
|
||||
{
|
||||
id: 'disableFooter',
|
||||
label: '隐藏页脚',
|
||||
placeholder: 'false',
|
||||
},
|
||||
{
|
||||
id: 'urlPrefix',
|
||||
label: '默认URL前缀',
|
||||
tooltip: '自定义URL前缀,如:https://img.a.com/file/,留空则使用当前域名 <br/> 设置后将应用于上传和管理界面',
|
||||
}
|
||||
)
|
||||
|
||||
const userConfig = env.USER_CONFIG
|
||||
if (userConfig) {
|
||||
try {
|
||||
const parsedConfig = JSON.parse(userConfig)
|
||||
if (typeof parsedConfig === 'object' && parsedConfig !== null) {
|
||||
// 搜索config中的id,如果存在则更新
|
||||
for (let i = 0; i < config.length; i++) {
|
||||
if (parsedConfig[config[i].id]) {
|
||||
config[i].value = parsedConfig[config[i].id]
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
// 用KV中的设置覆盖默认设置
|
||||
for (let i = 0; i < settingsKV.config?.length; i++) {
|
||||
const item = settingsKV.config[i]
|
||||
const index = config.findIndex(x => x.id === item.id)
|
||||
if (index !== -1) {
|
||||
config[index].value = item.value
|
||||
}
|
||||
}
|
||||
|
||||
return settings
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
export async function onRequest(context) {
|
||||
// 安全设置相关,GET方法读取设置,POST方法保存设置
|
||||
const {
|
||||
request, // same as existing Worker API
|
||||
env, // same as existing Worker API
|
||||
params, // if filename includes [id] or [[path]]
|
||||
waitUntil, // same as ctx.waitUntil in existing Worker API
|
||||
next, // used for middleware or to fetch assets
|
||||
data, // arbitrary space for passing data between middlewares
|
||||
} = context;
|
||||
|
||||
const kv = env.img_url
|
||||
|
||||
// GET读取设置
|
||||
if (request.method === 'GET') {
|
||||
const settings = await getSecurityConfig(kv, env)
|
||||
|
||||
return new Response(JSON.stringify(settings), {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// POST保存设置
|
||||
if (request.method === 'POST') {
|
||||
const body = await request.json()
|
||||
const settings = body
|
||||
|
||||
// 写入 KV
|
||||
await kv.put('manage@sysConfig@security', JSON.stringify(settings))
|
||||
|
||||
return new Response(JSON.stringify(settings), {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export async function getSecurityConfig(kv, env) {
|
||||
const settings = {}
|
||||
// 读取KV中的设置
|
||||
const settingsStr = await kv.get('manage@sysConfig@security')
|
||||
const settingsKV = settingsStr ? JSON.parse(settingsStr) : {}
|
||||
|
||||
// 认证管理
|
||||
const auth = {
|
||||
user: {
|
||||
authCode: env.AUTH_CODE
|
||||
},
|
||||
admin: {
|
||||
adminUsername: env.BASIC_USER,
|
||||
adminPassword: env.BASIC_PASS,
|
||||
}
|
||||
}
|
||||
settings.auth = auth
|
||||
|
||||
// 上传管理
|
||||
const upload = {
|
||||
moderate: {
|
||||
channel: 'moderatecontent.com',
|
||||
apiKey: env.ModerateContentApiKey,
|
||||
}
|
||||
}
|
||||
settings.upload = upload
|
||||
|
||||
// 访问管理
|
||||
const access = {
|
||||
allowedDomains: env.ALLOWED_DOMAINS,
|
||||
whiteListMode: env.WhiteList_Mode === 'true',
|
||||
}
|
||||
settings.access = access
|
||||
|
||||
// 用 KV 中的设置覆盖默认设置
|
||||
Object.assign(settings, settingsKV)
|
||||
|
||||
return settings;
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
export async function onRequest(context) {
|
||||
// 上传设置相关,GET方法读取设置,POST方法保存设置
|
||||
const {
|
||||
request, // same as existing Worker API
|
||||
env, // same as existing Worker API
|
||||
params, // if filename includes [id] or [[path]]
|
||||
waitUntil, // same as ctx.waitUntil in existing Worker API
|
||||
next, // used for middleware or to fetch assets
|
||||
data, // arbitrary space for passing data between middlewares
|
||||
} = context;
|
||||
|
||||
const kv = env.img_url
|
||||
|
||||
// GET读取设置
|
||||
if (request.method === 'GET') {
|
||||
const settings = await getUploadConfig(kv, env)
|
||||
|
||||
return new Response(JSON.stringify(settings), {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// POST保存设置
|
||||
if (request.method === 'POST') {
|
||||
const body = await request.json()
|
||||
const settings = body
|
||||
|
||||
// 写入 KV
|
||||
await kv.put('manage@sysConfig@upload', JSON.stringify(settings))
|
||||
|
||||
return new Response(JSON.stringify(settings), {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export async function getUploadConfig(kv, env) {
|
||||
const settings = {}
|
||||
// 读取KV中的设置
|
||||
const settingsStr = await kv.get('manage@sysConfig@upload')
|
||||
const settingsKV = settingsStr ? JSON.parse(settingsStr) : {}
|
||||
|
||||
// =====================读取tg渠道配置=====================
|
||||
const telegram = {}
|
||||
|
||||
const telegramChannels = []
|
||||
telegram.channels = telegramChannels
|
||||
if (env.TG_BOT_TOKEN) {
|
||||
telegramChannels.push({
|
||||
id: 1,
|
||||
name: 'Telegram_env',
|
||||
type: 'telegram',
|
||||
savePath: 'environment variable',
|
||||
botToken: env.TG_BOT_TOKEN,
|
||||
chatId: env.TG_CHAT_ID,
|
||||
enabled: true,
|
||||
fixed: true,
|
||||
})
|
||||
}
|
||||
for (const tg of settingsKV.telegram?.channels || []) {
|
||||
// 如果savePath是environment variable,修改可变参数
|
||||
if (tg.savePath === 'environment variable') {
|
||||
// 如果环境变量未删除,进行覆盖操作
|
||||
if (telegramChannels[0]) {
|
||||
telegramChannels[0].enabled = tg.enabled
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
// id自增
|
||||
tg.id = telegramChannels.length + 1
|
||||
telegramChannels.push(tg)
|
||||
}
|
||||
|
||||
// 负载均衡
|
||||
const tgLoadBalance = settingsKV.telegram?.loadBalance || {
|
||||
enabled: false,
|
||||
channels: [],
|
||||
}
|
||||
telegram.loadBalance = tgLoadBalance
|
||||
|
||||
|
||||
|
||||
// =====================读取r2渠道配置=====================
|
||||
const cfr2 = {}
|
||||
const cfr2Channels = []
|
||||
cfr2.channels = cfr2Channels
|
||||
if (env.img_r2) {
|
||||
cfr2Channels.push({
|
||||
id: 1,
|
||||
name: 'Cloudflare R2_env',
|
||||
type: 'cfr2',
|
||||
savePath: 'environment variable',
|
||||
publicUrl: env.R2PublicUrl,
|
||||
enabled: true,
|
||||
fixed: true,
|
||||
})
|
||||
}
|
||||
for (const r2 of settingsKV.cfr2?.channels || []) {
|
||||
// 如果savePath是environment variable,修改可变参数
|
||||
if (r2.savePath === 'environment variable') {
|
||||
// 如果环境变量未删除,进行覆盖操作
|
||||
if (cfr2Channels[0]) {
|
||||
cfr2Channels[0].publicUrl = r2.publicUrl
|
||||
cfr2Channels[0].enabled = r2.enabled
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
// id自增
|
||||
r2.id = cfr2Channels.length + 1
|
||||
cfr2Channels.push(r2)
|
||||
}
|
||||
|
||||
// 负载均衡
|
||||
const r2LoadBalance = settingsKV.cfr2?.loadBalance || {
|
||||
enabled: false,
|
||||
channels: [],
|
||||
}
|
||||
cfr2.loadBalance = r2LoadBalance
|
||||
|
||||
|
||||
// =====================读取s3渠道配置=====================
|
||||
const s3 = {}
|
||||
const s3Channels = []
|
||||
s3.channels = s3Channels
|
||||
if (env.S3_ACCESS_KEY_ID) {
|
||||
s3Channels.push({
|
||||
id: 1,
|
||||
name: 'S3_env',
|
||||
type: 's3',
|
||||
savePath: 'environment variable',
|
||||
accessKeyId: env.S3_ACCESS_KEY_ID,
|
||||
secretAccessKey: env.S3_SECRET_ACCESS_KEY,
|
||||
region: env.S3_REGION || 'auto',
|
||||
bucketName: env.S3_BUCKET_NAME,
|
||||
endpoint: env.S3_ENDPOINT,
|
||||
enabled: true,
|
||||
fixed: true,
|
||||
})
|
||||
}
|
||||
for (const s of settingsKV.s3?.channels || []) {
|
||||
// 如果savePath是environment variable,修改可变参数
|
||||
if (s.savePath === 'environment variable') {
|
||||
// 如果环境变量未删除,进行覆盖操作
|
||||
if (s3Channels[0]) {
|
||||
s3Channels[0].enabled = s.enabled
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
// id自增
|
||||
s.id = s3Channels.length + 1
|
||||
s3Channels.push(s)
|
||||
}
|
||||
|
||||
// 负载均衡
|
||||
const s3LoadBalance = settingsKV.s3?.loadBalance || {
|
||||
enabled: false,
|
||||
channels: [],
|
||||
}
|
||||
s3.loadBalance = s3LoadBalance
|
||||
|
||||
|
||||
|
||||
settings.telegram = telegram
|
||||
settings.cfr2 = cfr2
|
||||
settings.s3 = s3
|
||||
|
||||
return settings;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { purgeCFCache } from "../../../utils/purgeCache";
|
||||
|
||||
export async function onRequest(context) {
|
||||
// Contents of context object
|
||||
const {
|
||||
@@ -24,12 +26,7 @@ export async function onRequest(context) {
|
||||
const info = JSON.stringify(value.metadata);
|
||||
|
||||
// 清除CDN缓存
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json', 'X-Auth-Email': `${env.CF_EMAIL}`, 'X-Auth-Key': `${env.CF_API_KEY}`},
|
||||
body: `{"files":["${ cdnUrl }"]}`
|
||||
};
|
||||
await fetch(`https://api.cloudflare.com/client/v4/zones/${ env.CF_ZONE_ID }/purge_cache`, options);
|
||||
await purgeCFCache(env, cdnUrl);
|
||||
|
||||
return new Response(info);
|
||||
|
||||
|
||||
+69
-9
@@ -1,4 +1,10 @@
|
||||
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { fetchSecurityConfig } from "../utils/sysConfig";
|
||||
|
||||
let targetUrl = '';
|
||||
let securityConfig = {};
|
||||
let allowedDomains = null;
|
||||
let whiteListMode = false;
|
||||
|
||||
export async function onRequest(context) { // Contents of context object
|
||||
const {
|
||||
@@ -16,14 +22,19 @@ export async function onRequest(context) { // Contents of context object
|
||||
} catch (e) {
|
||||
return new Response('Error: Decode Image ID Failed', { status: 400 });
|
||||
}
|
||||
|
||||
// 读取安全配置
|
||||
securityConfig = await fetchSecurityConfig(env);
|
||||
allowedDomains = securityConfig.access.allowedDomains;
|
||||
whiteListMode = securityConfig.access.whiteListMode;
|
||||
|
||||
const url = new URL(request.url);
|
||||
let Referer = request.headers.get('Referer')
|
||||
if (Referer) {
|
||||
try {
|
||||
let refererUrl = new URL(Referer);
|
||||
if (env.ALLOWED_DOMAINS && env.ALLOWED_DOMAINS.trim() !== '') {
|
||||
let allowedDomains = env.ALLOWED_DOMAINS.split(',');
|
||||
if (allowedDomains && allowedDomains.trim() !== '') {
|
||||
let allowedDomains = allowedDomains.split(',');
|
||||
let isAllowed = allowedDomains.some(domain => {
|
||||
let domainPattern = new RegExp(`(^|\\.)${domain.replace('.', '\\.')}$`); // Escape dot in domain
|
||||
return domainPattern.test(refererUrl.hostname);
|
||||
@@ -93,6 +104,53 @@ export async function onRequest(context) { // Contents of context object
|
||||
}
|
||||
|
||||
|
||||
// S3渠道
|
||||
if (imgRecord.metadata?.Channel === "S3") {
|
||||
const s3Client = new S3Client({
|
||||
region: imgRecord.metadata?.S3Region || "auto", // 默认使用 auto 区域
|
||||
endpoint: imgRecord.metadata?.S3Endpoint,
|
||||
credentials: {
|
||||
accessKeyId: imgRecord.metadata?.S3AccessKeyId,
|
||||
secretAccessKey: imgRecord.metadata?.S3SecretAccessKey
|
||||
},
|
||||
forcePathStyle: true
|
||||
});
|
||||
|
||||
const bucketName = imgRecord.metadata?.S3BucketName;
|
||||
const key = imgRecord.metadata?.S3FileKey;
|
||||
|
||||
try {
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: bucketName,
|
||||
Key: key
|
||||
});
|
||||
|
||||
|
||||
const response = await s3Client.send(command);
|
||||
|
||||
// 设置响应头
|
||||
const headers = new Headers();
|
||||
headers.set("Content-Disposition", `inline; filename="${encodedFileName}"; filename*=UTF-8''${encodedFileName}`);
|
||||
headers.set("Access-Control-Allow-Origin", "*");
|
||||
|
||||
if (fileType) {
|
||||
headers.set("Content-Type", fileType);
|
||||
}
|
||||
|
||||
// 根据Referer设置CDN缓存策略
|
||||
if (Referer && Referer.includes(url.origin)) {
|
||||
headers.set('Cache-Control', 'private, max-age=86400');
|
||||
} else {
|
||||
headers.set('Cache-Control', 'public, max-age=604800');
|
||||
}
|
||||
|
||||
// 返回 S3 文件流
|
||||
return new Response(response.Body, { status: 200, headers });
|
||||
|
||||
} catch (error) {
|
||||
return new Response(`Error: Failed to fetch from S3 - ${error.message}`, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Telegram及Telegraph渠道
|
||||
@@ -113,11 +171,12 @@ export async function onRequest(context) { // Contents of context object
|
||||
// 构建目标 URL
|
||||
if (isTgChannel(imgRecord)) {
|
||||
// 获取TG图片真实地址
|
||||
const filePath = await getFilePath(env, TgFileID);
|
||||
const TgBotToken = imgRecord.metadata?.TgBotToken || env.TG_BOT_TOKEN;
|
||||
const filePath = await getFilePath(TgBotToken, TgFileID);
|
||||
if (filePath === null) {
|
||||
return new Response('Error: Failed to fetch image path', { status: 500 });
|
||||
}
|
||||
targetUrl = `https://api.telegram.org/file/bot${env.TG_BOT_TOKEN}/${filePath}`;
|
||||
targetUrl = `https://api.telegram.org/file/bot${TgBotToken}/${filePath}`;
|
||||
} else {
|
||||
targetUrl = 'https://telegra.ph/' + url.pathname + url.search;
|
||||
}
|
||||
@@ -167,7 +226,8 @@ async function returnWithCheck(request, env, url, imgRecord) {
|
||||
return response;
|
||||
}
|
||||
|
||||
if (typeof env.img_url == "undefined" || env.img_url == null || env.img_url == "") { } else {
|
||||
if (typeof env.img_url == "undefined" || env.img_url == null || env.img_url == "") {
|
||||
} else {
|
||||
//check the record from kv
|
||||
const record = imgRecord;
|
||||
if (record.metadata === null) {
|
||||
@@ -181,7 +241,7 @@ async function returnWithCheck(request, env, url, imgRecord) {
|
||||
return await returnBlockImg(url);
|
||||
}
|
||||
//check if the env variables WhiteList_Mode are set
|
||||
if (env.WhiteList_Mode == "true") {
|
||||
if (whiteListMode) {
|
||||
//if the env variables WhiteList_Mode are set, redirect to the image
|
||||
return await returnWhiteListImg(url);
|
||||
} else {
|
||||
@@ -217,9 +277,9 @@ async function getFileContent(request, max_retries = 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function getFilePath(env, file_id) {
|
||||
async function getFilePath(bot_token, file_id) {
|
||||
try {
|
||||
const url = `https://api.telegram.org/bot${env.TG_BOT_TOKEN}/getFile?file_id=${file_id}`;
|
||||
const url = `https://api.telegram.org/bot${bot_token}/getFile?file_id=${file_id}`;
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@@ -300,7 +360,7 @@ async function returnWhiteListImg(url) {
|
||||
})
|
||||
} else {
|
||||
return new Response(WhiteListImg.body, {
|
||||
status: 200,
|
||||
status: 403,
|
||||
headers: {
|
||||
"Content-Type": "image/png",
|
||||
"Content-Disposition": "inline",
|
||||
|
||||
+10
-1
@@ -1,3 +1,8 @@
|
||||
import { fetchOthersConfig } from "./utils/sysConfig";
|
||||
|
||||
let othersConfig = {};
|
||||
let allowRandom = false;
|
||||
|
||||
export async function onRequest(context) {
|
||||
// Contents of context object
|
||||
const {
|
||||
@@ -10,8 +15,12 @@ export async function onRequest(context) {
|
||||
} = context;
|
||||
const requestUrl = new URL(request.url);
|
||||
|
||||
// 读取其他设置
|
||||
othersConfig = await fetchOthersConfig(env);
|
||||
allowRandom = othersConfig.randomImageAPI.enabled;
|
||||
|
||||
// 检查是否启用了随机图功能
|
||||
if (env.AllowRandom != "true") {
|
||||
if (allowRandom != true) {
|
||||
return new Response(JSON.stringify({ error: "Random is disabled" }), { status: 403 });
|
||||
}
|
||||
|
||||
|
||||
+201
-28
@@ -1,4 +1,12 @@
|
||||
import { errorHandling, telemetryData } from "./utils/middleware";
|
||||
import { fetchUploadConfig, fetchSecurityConfig } from "./utils/sysConfig";
|
||||
import { purgeCFCache } from "./utils/purgeCache";
|
||||
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
||||
|
||||
let uploadConfig = {};
|
||||
let securityConfig = {};
|
||||
let rightAuthCode = null;
|
||||
let moderateContentApiKey = null;
|
||||
|
||||
function UnauthorizedException(reason) {
|
||||
return new Response(reason, {
|
||||
@@ -54,7 +62,7 @@ function authCheck(env, url, request) {
|
||||
authCode = getCookieValue(cookies, 'authCode');
|
||||
}
|
||||
}
|
||||
if (isAuthCodeDefined(env.AUTH_CODE) && !isValidAuthCode(env.AUTH_CODE, authCode)) {
|
||||
if (isAuthCodeDefined(rightAuthCode) && !isValidAuthCode(rightAuthCode, authCode)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -66,6 +74,11 @@ export async function onRequestPost(context) { // Contents of context object
|
||||
const url = new URL(request.url);
|
||||
const clonedRequest = await request.clone();
|
||||
|
||||
// 读取安全配置
|
||||
securityConfig = await fetchSecurityConfig(env);
|
||||
rightAuthCode = securityConfig.auth.user.authCode;
|
||||
moderateContentApiKey = securityConfig.upload.moderate.apiKey;
|
||||
|
||||
// 鉴权
|
||||
if (!authCheck(env, url, request)) {
|
||||
return UnauthorizedException('Unauthorized');
|
||||
@@ -79,6 +92,9 @@ export async function onRequestPost(context) { // Contents of context object
|
||||
return new Response('Error: Your IP is blocked', { status: 403 });
|
||||
}
|
||||
|
||||
// 读取上传配置
|
||||
uploadConfig = await fetchUploadConfig(env);
|
||||
|
||||
// 获得上传渠道
|
||||
const urlParamUploadChannel = url.searchParams.get('uploadChannel');
|
||||
let uploadChannel = 'TelegramNew';
|
||||
@@ -89,14 +105,19 @@ export async function onRequestPost(context) { // Contents of context object
|
||||
case 'cfr2':
|
||||
uploadChannel = 'CloudflareR2';
|
||||
break;
|
||||
case 's3':
|
||||
uploadChannel = 'S3';
|
||||
break;
|
||||
default:
|
||||
uploadChannel = 'TelegramNew';
|
||||
break;
|
||||
}
|
||||
|
||||
// 错误处理和遥测
|
||||
await errorHandling(context);
|
||||
telemetryData(context);
|
||||
if (env.dev_mode === undefined || env.dev_mode === null || env.dev_mode !== 'true') {
|
||||
await errorHandling(context);
|
||||
telemetryData(context);
|
||||
}
|
||||
|
||||
// img_url 未定义或为空的处理逻辑
|
||||
if (typeof env.img_url == "undefined" || env.img_url == null || env.img_url == "") {
|
||||
@@ -116,6 +137,7 @@ export async function onRequestPost(context) { // Contents of context object
|
||||
UploadIP: uploadIp,
|
||||
ListType: "None",
|
||||
TimeStamp: time,
|
||||
Label: "None",
|
||||
}
|
||||
|
||||
|
||||
@@ -139,10 +161,23 @@ export async function onRequestPost(context) { // Contents of context object
|
||||
const unique_index = time + Math.floor(Math.random() * 10000);
|
||||
let fullId = '';
|
||||
if (nameType === 'index') {
|
||||
// 仅前缀
|
||||
fullId = unique_index + '.' + fileExt;
|
||||
} else if (nameType === 'origin') {
|
||||
// 仅文件名
|
||||
fullId = fileName? fileName : unique_index + '.' + fileExt;
|
||||
} else if (nameType === 'short') {
|
||||
// 短链接,8位大小写字母+数字的随机字符
|
||||
while (true) {
|
||||
const shortId = generateShortId(8);
|
||||
const testFullId = shortId + '.' + fileExt;
|
||||
if (await env.img_url.get(testFullId) === null) {
|
||||
fullId = shortId + '.' + fileExt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 默认方式:前缀+文件名
|
||||
fullId = fileName? unique_index + '_' + fileName : unique_index + '.' + fileExt;
|
||||
}
|
||||
|
||||
@@ -168,7 +203,15 @@ export async function onRequestPost(context) { // Contents of context object
|
||||
// 上传到不同渠道
|
||||
if (uploadChannel === 'CloudflareR2') {
|
||||
// -------------CloudFlare R2 渠道---------------
|
||||
const res = await uploadFileToCloudflareR2(env, formdata, fullId, metadata, returnLink);
|
||||
const res = await uploadFileToCloudflareR2(env, formdata, fullId, metadata, returnLink, url);
|
||||
if (res.status === 200 || !autoRetry) {
|
||||
return res;
|
||||
} else {
|
||||
err = await res.text();
|
||||
}
|
||||
} else if (uploadChannel === 'S3') {
|
||||
// -------------S3 渠道---------------
|
||||
const res = await uploadFileToS3(env, formdata, fullId, metadata, returnLink, url);
|
||||
if (res.status === 200 || !autoRetry) {
|
||||
return res;
|
||||
} else {
|
||||
@@ -193,17 +236,20 @@ export async function onRequestPost(context) { // Contents of context object
|
||||
// 自动切换渠道重试
|
||||
async function tryRetry(err, env, uploadChannel, formdata, fullId, metadata, fileExt, fileName, fileType, url, clonedRequest, returnLink) {
|
||||
// 渠道列表
|
||||
const channelList = ['CloudflareR2', 'TelegramNew'];
|
||||
const channelList = ['CloudflareR2', 'TelegramNew', 'S3'];
|
||||
const errMessages = {};
|
||||
errMessages[uploadChannel] = 'Error: ' + uploadChannel + err;
|
||||
for (let i = 0; i < channelList.length; i++) {
|
||||
if (channelList[i] !== uploadChannel) {
|
||||
let res = null;
|
||||
if (channelList[i] === 'CloudflareR2') {
|
||||
res = await uploadFileToCloudflareR2(env, formdata, fullId, metadata, returnLink);
|
||||
res = await uploadFileToCloudflareR2(env, formdata, fullId, metadata, returnLink, url);
|
||||
} else if (channelList[i] === 'TelegramNew') {
|
||||
res = await uploadFileToTelegram(env, formdata, fullId, metadata, fileExt, fileName, fileType, url, clonedRequest, returnLink);
|
||||
} else if (channelList[i] === 'S3') {
|
||||
res = await uploadFileToS3(env, formdata, fullId, metadata, returnLink, url);
|
||||
}
|
||||
|
||||
if (res.status === 200) {
|
||||
return res;
|
||||
} else {
|
||||
@@ -217,25 +263,35 @@ async function tryRetry(err, env, uploadChannel, formdata, fullId, metadata, fil
|
||||
|
||||
|
||||
// 上传到Cloudflare R2
|
||||
async function uploadFileToCloudflareR2(env, formdata, fullId, metadata, returnLink) {
|
||||
async function uploadFileToCloudflareR2(env, formdata, fullId, metadata, returnLink, originUrl) {
|
||||
// 检查R2数据库是否配置
|
||||
if (typeof env.img_r2 == "undefined" || env.img_r2 == null || env.img_r2 == "") {
|
||||
return new Response('Error: Please configure R2 database', { status: 500 });
|
||||
}
|
||||
// 检查 R2 渠道是否启用
|
||||
const r2Settings = uploadConfig.cfr2;
|
||||
if (!r2Settings.channels || r2Settings.channels.length === 0) {
|
||||
return new Response('Error: No R2 channel provided', { status: 400 });
|
||||
}
|
||||
|
||||
const r2Channel = r2Settings.channels[0];
|
||||
|
||||
const R2DataBase = env.img_r2;
|
||||
|
||||
// 写入R2数据库
|
||||
await R2DataBase.put(fullId, formdata.get('file'));
|
||||
|
||||
// 图像审查
|
||||
const R2PublicUrl = env.R2PublicUrl;
|
||||
const moderateUrl = `${R2PublicUrl}/${fullId}`;
|
||||
// 更新metadata
|
||||
metadata.Channel = "CloudflareR2";
|
||||
metadata.ChannelName = "R2_env";
|
||||
|
||||
// 图像审查,采用R2的publicUrl
|
||||
const R2PublicUrl = r2Channel.publicUrl;
|
||||
let moderateUrl = `${R2PublicUrl}/${fullId}`;
|
||||
metadata = await moderateContent(env, moderateUrl, metadata);
|
||||
|
||||
// 更新metadata,写入KV数据库
|
||||
// 写入KV数据库
|
||||
try {
|
||||
metadata.Channel = "CloudflareR2";
|
||||
await env.img_url.put(fullId, "", {
|
||||
metadata: metadata,
|
||||
});
|
||||
@@ -255,8 +311,108 @@ async function uploadFileToCloudflareR2(env, formdata, fullId, metadata, returnL
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 上传到 S3(支持自定义端点)
|
||||
async function uploadFileToS3(env, formdata, fullId, metadata, returnLink, originUrl) {
|
||||
const s3Settings = uploadConfig.s3;
|
||||
const s3Channels = s3Settings.channels;
|
||||
const s3Channel = s3Settings.loadBalance.enabled
|
||||
? s3Channels[Math.floor(Math.random() * s3Channels.length)]
|
||||
: s3Channels[0];
|
||||
|
||||
if (!s3Channel) {
|
||||
return new Response('Error: No S3 channel provided', { status: 400 });
|
||||
}
|
||||
|
||||
const { endpoint, accessKeyId, secretAccessKey, bucketName, region } = s3Channel;
|
||||
|
||||
// 创建 S3 客户端
|
||||
const s3Client = new S3Client({
|
||||
region: region || "auto", // R2 可用 "auto"
|
||||
endpoint, // 自定义 S3 端点
|
||||
credentials: {
|
||||
accessKeyId,
|
||||
secretAccessKey
|
||||
},
|
||||
forcePathStyle: true // 确保使用路径风格(适配 R2、MinIO)
|
||||
});
|
||||
|
||||
// 获取文件
|
||||
const file = formdata.get("file");
|
||||
if (!file) return new Response("Error: No file provided", { status: 400 });
|
||||
|
||||
// 转换 Blob 为 Uint8Array
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
|
||||
const s3FileName = fullId;
|
||||
|
||||
try {
|
||||
// S3 上传参数
|
||||
const putObjectParams = {
|
||||
Bucket: bucketName,
|
||||
Key: s3FileName,
|
||||
Body: uint8Array, // 直接使用 Blob
|
||||
ContentType: file.type
|
||||
};
|
||||
|
||||
// 执行上传
|
||||
await s3Client.send(new PutObjectCommand(putObjectParams));
|
||||
|
||||
// 更新 metadata
|
||||
metadata.Channel = "S3";
|
||||
metadata.ChannelName = s3Channel.name;
|
||||
|
||||
metadata.S3Location = `${endpoint}/${bucketName}/${s3FileName}`;
|
||||
metadata.S3Endpoint = endpoint;
|
||||
metadata.S3AccessKeyId = accessKeyId;
|
||||
metadata.S3SecretAccessKey = secretAccessKey;
|
||||
metadata.S3Region = region || "auto";
|
||||
metadata.S3BucketName = bucketName;
|
||||
metadata.S3FileKey = s3FileName;
|
||||
|
||||
// 图像审查
|
||||
if (moderateContentApiKey) {
|
||||
try {
|
||||
await env.img_url.put(fullId, "", { metadata });
|
||||
} catch {
|
||||
return new Response("Error: Failed to write to KV database", { status: 500 });
|
||||
}
|
||||
|
||||
const moderateUrl = `https://${originUrl.hostname}/file/${fullId}`;
|
||||
metadata = await moderateContent(env, moderateUrl, metadata);
|
||||
await purgeCDNCache(env, moderateUrl, originUrl);
|
||||
}
|
||||
|
||||
// 写入 KV 数据库
|
||||
try {
|
||||
await env.img_url.put(fullId, "", { metadata });
|
||||
} catch {
|
||||
return new Response("Error: Failed to write to KV database", { status: 500 });
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify([{ src: returnLink }]), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(`Error: Failed to upload to S3 - ${error.message}`, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// 上传到Telegram
|
||||
async function uploadFileToTelegram(env, formdata, fullId, metadata, fileExt, fileName, fileType, url, clonedRequest, returnLink) {
|
||||
// 选择一个 Telegram 渠道上传,若负载均衡开启,则随机选择一个;否则选择第一个
|
||||
const tgSettings = uploadConfig.telegram;
|
||||
const tgChannels = tgSettings.channels;
|
||||
const tgChannel = tgSettings.loadBalance.enabled? tgChannels[Math.floor(Math.random() * tgChannels.length)] : tgChannels[0];
|
||||
if (!tgChannel) {
|
||||
return new Response('Error: No Telegram channel provided', { status: 400 });
|
||||
}
|
||||
|
||||
const tgBotToken = tgChannel.botToken;
|
||||
const tgChatId = tgChannel.chatId;
|
||||
|
||||
// 由于TG会把gif后缀的文件转为视频,所以需要修改后缀名绕过限制
|
||||
if (fileExt === 'gif') {
|
||||
const newFileName = fileName.replace(/\.gif$/, '.jpeg');
|
||||
@@ -294,13 +450,13 @@ async function uploadFileToTelegram(env, formdata, fullId, metadata, fileExt, fi
|
||||
|
||||
// 根据发送接口向表单嵌入chat_id
|
||||
let newFormdata = new FormData();
|
||||
newFormdata.append('chat_id', env.TG_CHAT_ID);
|
||||
newFormdata.append('chat_id', tgChatId);
|
||||
newFormdata.append(sendFunction.type, formdata.get('file'));
|
||||
|
||||
|
||||
// 构建目标 URL
|
||||
// const targetUrl = new URL(url.pathname, 'https://telegra.ph'); // telegraph接口,已失效,缅怀
|
||||
const targetUrl = new URL(`https://api.telegram.org/bot${env.TG_BOT_TOKEN}/${sendFunction.url}`); // telegram接口
|
||||
const targetUrl = new URL(`https://api.telegram.org/bot${tgBotToken}/${sendFunction.url}`); // telegram接口
|
||||
// 目标 URL 剔除 authCode 参数
|
||||
url.searchParams.forEach((value, key) => {
|
||||
if (key !== 'authCode') {
|
||||
@@ -324,7 +480,7 @@ async function uploadFileToTelegram(env, formdata, fullId, metadata, fileExt, fi
|
||||
});
|
||||
const clonedRes = await response.clone().json(); // 等待响应克隆和解析完成
|
||||
const fileInfo = getFile(clonedRes);
|
||||
const filePath = await getFilePath(env, fileInfo.file_id);
|
||||
const filePath = await getFilePath(tgBotToken, fileInfo.file_id);
|
||||
const id = fileInfo.file_id;
|
||||
// 更新FileSize
|
||||
metadata.FileSize = (fileInfo.file_size / 1024 / 1024).toFixed(2);
|
||||
@@ -342,15 +498,17 @@ async function uploadFileToTelegram(env, formdata, fullId, metadata, fileExt, fi
|
||||
|
||||
|
||||
// 图像审查
|
||||
const moderateUrl = `https://api.telegram.org/file/bot${env.TG_BOT_TOKEN}/${filePath}`;
|
||||
const moderateUrl = `https://api.telegram.org/file/bot${tgBotToken}/${filePath}`;
|
||||
metadata = await moderateContent(env, moderateUrl, metadata);
|
||||
|
||||
// 更新metadata,写入KV数据库
|
||||
try {
|
||||
metadata.Channel = "TelegramNew";
|
||||
metadata.ChannelName = tgChannel.name;
|
||||
|
||||
metadata.TgFileId = id;
|
||||
metadata.TgChatId = env.TG_CHAT_ID;
|
||||
metadata.TgBotToken = env.TG_BOT_TOKEN;
|
||||
metadata.TgChatId = tgChatId;
|
||||
metadata.TgBotToken = tgBotToken;
|
||||
await env.img_url.put(fullId, "", {
|
||||
metadata: metadata,
|
||||
});
|
||||
@@ -367,7 +525,7 @@ async function uploadFileToTelegram(env, formdata, fullId, metadata, fileExt, fi
|
||||
|
||||
// 图像审查
|
||||
async function moderateContent(env, url, metadata) {
|
||||
const apikey = env.ModerateContentApiKey;
|
||||
const apikey = moderateContentApiKey;
|
||||
if (apikey == undefined || apikey == null || apikey == "") {
|
||||
metadata.Label = "None";
|
||||
} else {
|
||||
@@ -377,7 +535,9 @@ async function moderateContent(env, url, metadata) {
|
||||
throw new Error(`HTTP error! status: ${fetchResponse.status}`);
|
||||
}
|
||||
const moderate_data = await fetchResponse.json();
|
||||
metadata.Label = moderate_data.rating_label;
|
||||
if (moderate_data.rating_label) {
|
||||
metadata.Label = moderate_data.rating_label;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Moderate Error:', error);
|
||||
// 将不带审查的图片写入数据库
|
||||
@@ -427,9 +587,9 @@ function getFile(response) {
|
||||
}
|
||||
}
|
||||
|
||||
async function getFilePath(env, file_id) {
|
||||
async function getFilePath(bot_token, file_id) {
|
||||
try {
|
||||
const url = `https://api.telegram.org/bot${env.TG_BOT_TOKEN}/getFile?file_id=${file_id}`;
|
||||
const url = `https://api.telegram.org/bot${bot_token}/getFile?file_id=${file_id}`;
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@@ -450,13 +610,16 @@ async function getFilePath(env, file_id) {
|
||||
}
|
||||
|
||||
async function purgeCDNCache(env, cdnUrl, url) {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json', 'X-Auth-Email': `${env.CF_EMAIL}`, 'X-Auth-Key': `${env.CF_API_KEY}`},
|
||||
body: `{"files":["${ cdnUrl }"]}`
|
||||
};
|
||||
if (env.dev_mode === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
await fetch(`https://api.cloudflare.com/client/v4/zones/${ env.CF_ZONE_ID }/purge_cache`, options);
|
||||
// 清除CDN缓存
|
||||
try {
|
||||
await purgeCFCache(env, cdnUrl);
|
||||
} catch (error) {
|
||||
console.error('Failed to clear CDN cache:', error);
|
||||
}
|
||||
|
||||
// 清除api/randomFileList API缓存
|
||||
try {
|
||||
@@ -495,4 +658,14 @@ async function isBlockedUploadIp(env, uploadIp) {
|
||||
}
|
||||
|
||||
return list.includes(uploadIp);
|
||||
}
|
||||
|
||||
// 生成短链接
|
||||
function generateShortId(length = 8) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
+12
-2
@@ -1,6 +1,16 @@
|
||||
import { fetchPageConfig } from "./utils/sysConfig";
|
||||
|
||||
export async function onRequestGet(context) {
|
||||
const { request, env, params, waitUntil, next, data } = context;
|
||||
const userConfig = env.USER_CONFIG;
|
||||
const PageConfig = await fetchPageConfig(env);
|
||||
const userConfigList = PageConfig.config;
|
||||
const userConfig = {};
|
||||
for (const config of userConfigList) {
|
||||
if (config.value) {
|
||||
// 将config解析为JSON对象
|
||||
userConfig[config.id] = JSON.parse(config.value);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 USER_CONFIG 是否为空或未定义
|
||||
if (!userConfig) {
|
||||
@@ -9,7 +19,7 @@ export async function onRequestGet(context) {
|
||||
|
||||
try {
|
||||
// 尝试解析 USER_CONFIG 为 JSON
|
||||
const parsedConfig = JSON.parse(userConfig);
|
||||
const parsedConfig = userConfig;
|
||||
// 检查解析后的结果是否为对象
|
||||
if (typeof parsedConfig === 'object' && parsedConfig !== null) {
|
||||
return new Response(JSON.stringify(parsedConfig), { status: 200 });
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import sentryPlugin from "@cloudflare/pages-plugin-sentry";
|
||||
import '@sentry/tracing';
|
||||
import { fetchOthersConfig } from "./sysConfig";
|
||||
|
||||
let disableTelemetry = false;
|
||||
|
||||
export async function errorHandling(context) {
|
||||
// 读取KV中的设置
|
||||
const othersConfig = await fetchOthersConfig(context.env);
|
||||
disableTelemetry = !othersConfig.telemetry.enabled;
|
||||
|
||||
const env = context.env;
|
||||
if (typeof env.disable_telemetry == "undefined" || env.disable_telemetry == null || env.disable_telemetry == "") {
|
||||
if (!disableTelemetry) {
|
||||
context.data.telemetry = true;
|
||||
let remoteSampleRate = 0.001;
|
||||
try {
|
||||
@@ -22,9 +29,13 @@ export async function errorHandling(context) {
|
||||
return context.next();
|
||||
}
|
||||
|
||||
export function telemetryData(context) {
|
||||
export async function telemetryData(context) {
|
||||
const env = context.env;
|
||||
if (typeof env.disable_telemetry == "undefined" || env.disable_telemetry == null || env.disable_telemetry == "") {
|
||||
// 读取KV中的设置
|
||||
const othersConfig = await fetchOthersConfig(context.env);
|
||||
disableTelemetry = !othersConfig.telemetry.enabled;
|
||||
|
||||
if (!disableTelemetry) {
|
||||
try {
|
||||
const parsedHeaders = {};
|
||||
context.request.headers.forEach((value, key) => {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { fetchOthersConfig } from "./sysConfig";
|
||||
|
||||
let othersConfig = {};
|
||||
let cfZoneId = "";
|
||||
let cfEmail = "";
|
||||
let cfApiKey = "";
|
||||
|
||||
export async function purgeCFCache(env, cdnUrl) {
|
||||
// 读取其他设置
|
||||
othersConfig = await fetchOthersConfig(env);
|
||||
cfZoneId = othersConfig.cloudflareApiToken.CF_ZONE_ID;
|
||||
cfEmail = othersConfig.cloudflareApiToken.CF_EMAIL;
|
||||
cfApiKey = othersConfig.cloudflareApiToken.CF_API_KEY;
|
||||
|
||||
// 清除CDN缓存
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json', 'X-Auth-Email': `${cfEmail}`, 'X-Auth-Key': `${cfApiKey}`},
|
||||
body: `{"files":["${ cdnUrl }"]}`
|
||||
};
|
||||
await fetch(`https://api.cloudflare.com/client/v4/zones/${ cfZoneId }/purge_cache`, options);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { getUploadConfig } from '../api/manage/sysConfig/upload';
|
||||
import { getSecurityConfig } from '../api/manage/sysConfig/security';
|
||||
import { getPageConfig } from '../api/manage/sysConfig/page';
|
||||
import { getOthersConfig } from '../api/manage/sysConfig/others';
|
||||
|
||||
export async function fetchUploadConfig(env) {
|
||||
const kv = env.img_url;
|
||||
const settings = await getUploadConfig(kv, env);
|
||||
// 去除 已禁用 的渠道
|
||||
settings.telegram.channels = settings.telegram.channels.filter((channel) => channel.enabled);
|
||||
settings.cfr2.channels = settings.cfr2.channels.filter((channel) => channel.enabled);
|
||||
settings.s3.channels = settings.s3.channels.filter((channel) => channel.enabled);
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
export async function fetchSecurityConfig(env) {
|
||||
const kv = env.img_url;
|
||||
const settings = await getSecurityConfig(kv, env);
|
||||
return settings;
|
||||
}
|
||||
|
||||
export async function fetchPageConfig(env) {
|
||||
const kv = env.img_url;
|
||||
const settings = await getPageConfig(kv, env);
|
||||
return settings;
|
||||
}
|
||||
|
||||
export async function fetchOthersConfig(env) {
|
||||
const kv = env.img_url;
|
||||
const settings = await getOthersConfig(kv, env);
|
||||
return settings;
|
||||
}
|
||||
+7
-1
@@ -1,4 +1,5 @@
|
||||
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/logo.png"><title>Sanyue ImgHub</title><script defer="defer" src="/js/app.18219e81.js"></script><link href="/css/app.897f96ee.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but sanyue_imghub doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html><style>.el-dropdown__popper.el-popper {
|
||||
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/logo.png"><title>Sanyue ImgHub</title><script defer="defer" src="/js/app.c63fc528.js"></script><link href="/css/app.c47cba6a.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but sanyue_imghub doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html><style>/* 下拉菜单样式 */
|
||||
.el-dropdown__popper.el-popper {
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
background-color: var(--popper-bg-color);
|
||||
@@ -16,4 +17,9 @@
|
||||
.el-popper.is-light>.el-popper__arrow::before {
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* 加载动画样式 */
|
||||
.el-loading-mask {
|
||||
border-radius: 12px;
|
||||
}</style>
|
||||
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
"use strict";(self["webpackChunksanyue_imghub"]=self["webpackChunksanyue_imghub"]||[]).push([[172],{2542:function(e,t,a){a.d(t,{A:function(){return h}});var s=a(6768),r=a(4232);const n={key:0,class:"page-footer"},o=["href"];function i(e,t,a,i,l,c){const d=(0,s.g2)("font-awesome-icon");return c.disableFooter?(0,s.Q3)("",!0):((0,s.uX)(),(0,s.CE)("div",n,[(0,s.Lk)("p",null,[(0,s.eW)("© 2024-"+(0,r.v_)(c.thisYear)+" Designed by ",1),t[0]||(t[0]=(0,s.Lk)("a",{class:"footer-name",href:"https://github.com/MarSeventh",target:"_blank"},"SanyueQi",-1)),t[1]||(t[1]=(0,s.eW)(" for You! ")),(0,s.Lk)("a",{href:c.footerLink,target:"_blank"},[(0,s.bF)(d,{icon:"paper-plane",class:"footer-link-icon"})],8,o)])]))}var l=a(782),c={name:"Footer",computed:{...(0,l.L8)(["userConfig"]),footerLink(){return this.userConfig?.footerLink||"https://github.com/MarSeventh/CloudFlare-ImgBed"},thisYear(){return(new Date).getFullYear()},disableFooter(){return this.userConfig?.disableFooter||!1}}},d=a(1241);const u=(0,d.A)(c,[["render",i],["__scopeId","data-v-6eac6ae4"]]);var h=u},3525:function(e,t,a){a.d(t,{A:function(){return k}});var s=a(6768),r=a(4232);const n={id:"themeMask"},o=["cx","cy","r"],i=["r"],l=["opacity"];function c(e,t,a,c,d,u){return(0,s.uX)(),(0,s.CE)("div",{id:"themeToggle",onClick:t[0]||(t[0]=(...e)=>u.handleToggleClick&&u.handleToggleClick(...e))},[((0,s.uX)(),(0,s.CE)("svg",{class:(0,r.C4)(["theme_toggle_svg",{dark:d.isDark}]),width:"1.5em",height:"1.5em",viewBox:"0 0 24 24",fill:"none","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",stroke:"currentColor"},[(0,s.Lk)("mask",n,[t[1]||(t[1]=(0,s.Lk)("rect",{x:"0",y:"0",width:"100%",height:"100%",fill:"white"},null,-1)),(0,s.Lk)("circle",{class:"theme_toggle_circle1",fill:"black",cx:d.isDark?"50%":"100%",cy:d.isDark?"23%":"0%",r:d.isDark?"9":"5"},null,8,o)]),(0,s.Lk)("circle",{class:"theme_toggle_circle2",cx:"12",cy:"12",r:d.isDark?"9":"5",mask:"url(#themeMask)"},null,8,i),(0,s.Lk)("g",{class:"theme_toggle_g",stroke:"currentColor",opacity:d.isDark?0:1},t[2]||(t[2]=[(0,s.Fv)('<line x1="12" y1="1" x2="12" y2="3" data-v-019ed83e></line><line x1="12" y1="21" x2="12" y2="23" data-v-019ed83e></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64" data-v-019ed83e></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78" data-v-019ed83e></line><line x1="1" y1="12" x2="3" y2="12" data-v-019ed83e></line><line x1="21" y1="12" x2="23" y2="12" data-v-019ed83e></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36" data-v-019ed83e></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22" data-v-019ed83e></line>',8)]),8,l)],2))])}var d={name:"ToggleDark",data(){return{isDark:this.$store.getters.useDarkMode}},methods:{handleToggleClick(){this.isDark=!this.isDark,this.$store.commit("setUseDarkMode",this.isDark),this.$store.commit("setCusDarkMode",!0)}}},u=a(1241);const h=(0,u.A)(d,[["render",c],["__scopeId","data-v-019ed83e"]]);var k=h},4172:function(e,t,a){a.r(t),a.d(t,{default:function(){return v}});var s=a(2542),r=a(47),n=(a(5331),a(9648),a(2105)),o=(a(9092),a(3525)),i=a(6768),l=a(5130);const c={class:"login"},d={class:"login-container"},u={class:"input-container"},h={class:"input-container"};function k(e,t,a,k,g,m){const p=o.A,v=n.WK,y=r.S2,f=s.A;return(0,i.uX)(),(0,i.CE)("div",c,[(0,i.bF)(p,{class:"toggle-dark"}),(0,i.Lk)("div",d,[t[5]||(t[5]=(0,i.Lk)("h1",{class:"login-title"},"管理端登录",-1)),(0,i.Lk)("div",u,[t[2]||(t[2]=(0,i.Lk)("a",{class:"input-name"},"用户名",-1)),(0,i.bF)(v,{modelValue:g.username,"onUpdate:modelValue":t[0]||(t[0]=e=>g.username=e),placeholder:"请输入用户名",class:"password-input"},null,8,["modelValue"])]),(0,i.Lk)("div",h,[t[3]||(t[3]=(0,i.Lk)("a",{class:"input-name"},"密码",-1)),(0,i.bF)(v,{modelValue:g.password,"onUpdate:modelValue":t[1]||(t[1]=e=>g.password=e),placeholder:"请输入密码",class:"password-input",type:"password","show-password":"",onKeyup:(0,l.jR)(m.login,["enter","native"])},null,8,["modelValue","onKeyup"])]),(0,i.bF)(y,{class:"submit",type:"primary",onClick:m.login},{default:(0,i.k6)((()=>t[4]||(t[4]=[(0,i.eW)("登录")]))),_:1},8,["onClick"])]),(0,i.bF)(f,{class:"footer"})])}a(4114),a(4979);var g={data(){return{password:"",username:""}},components:{Footer:s.A,ToggleDark:o.A},methods:{async login(){const e=btoa(`${this.username}:${this.password}`);try{const t=await fetch("/api/manage/check",{method:"GET",headers:{Authorization:`Basic ${e}`},credentials:"include"});401===t.status?this.$message.error("用户名或密码错误"):200===t.status?(this.$store.commit("setCredentials",e),this.$router.push("/dashboard")):this.$message.error("用户名或密码错误")}catch(t){this.$message.error("服务器错误")}}}},m=a(1241);const p=(0,m.A)(g,[["render",k],["__scopeId","data-v-d60f6b8c"]]);var v=p}}]);
|
||||
//# sourceMappingURL=172.2e22a600.js.map
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1,2 +0,0 @@
|
||||
"use strict";(self["webpackChunksanyue_imghub"]=self["webpackChunksanyue_imghub"]||[]).push([[270],{8898:function(e,a,t){t.d(a,{A:function(){return h}});var s=t(6768),r=t(4232);const n={class:"page-footer"},o=["href"];function i(e,a,t,i,l,c){const u=(0,s.g2)("font-awesome-icon");return(0,s.uX)(),(0,s.CE)("div",n,[(0,s.Lk)("p",null,[(0,s.eW)("© 2024-"+(0,r.v_)(c.thisYear)+" Designed by ",1),a[0]||(a[0]=(0,s.Lk)("a",{class:"footer-name",href:"https://github.com/MarSeventh",target:"_blank"},"SanyueQi",-1)),a[1]||(a[1]=(0,s.eW)(" for You! ")),(0,s.Lk)("a",{href:c.footerLink,target:"_blank"},[(0,s.bF)(u,{icon:"paper-plane",class:"footer-link-icon"})],8,o)])])}var l=t(782),c={name:"Footer",computed:{...(0,l.L8)(["userConfig"]),footerLink(){return this.userConfig?.footerLink||"https://github.com/MarSeventh/CloudFlare-ImgBed"},thisYear(){return(new Date).getFullYear()}}},u=t(1241);const d=(0,u.A)(c,[["render",i],["__scopeId","data-v-eaaaff46"]]);var h=d},3154:function(e,a,t){t.d(a,{A:function(){return k}});var s=t(6768),r=t(4232);const n={id:"themeMask"},o=["cx","cy","r"],i=["r"],l=["opacity"];function c(e,a,t,c,u,d){return(0,s.uX)(),(0,s.CE)("div",{id:"themeToggle",onClick:a[0]||(a[0]=(...e)=>d.handleToggleClick&&d.handleToggleClick(...e))},[((0,s.uX)(),(0,s.CE)("svg",{class:(0,r.C4)(["theme_toggle_svg",{dark:u.isDark}]),width:"1.5em",height:"1.5em",viewBox:"0 0 24 24",fill:"none","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",stroke:"currentColor"},[(0,s.Lk)("mask",n,[a[1]||(a[1]=(0,s.Lk)("rect",{x:"0",y:"0",width:"100%",height:"100%",fill:"white"},null,-1)),(0,s.Lk)("circle",{class:"theme_toggle_circle1",fill:"black",cx:u.isDark?"50%":"100%",cy:u.isDark?"23%":"0%",r:u.isDark?"9":"5"},null,8,o)]),(0,s.Lk)("circle",{class:"theme_toggle_circle2",cx:"12",cy:"12",r:u.isDark?"9":"5",mask:"url(#themeMask)"},null,8,i),(0,s.Lk)("g",{class:"theme_toggle_g",stroke:"currentColor",opacity:u.isDark?0:1},a[2]||(a[2]=[(0,s.Fv)('<line x1="12" y1="1" x2="12" y2="3" data-v-18658a10></line><line x1="12" y1="21" x2="12" y2="23" data-v-18658a10></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64" data-v-18658a10></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78" data-v-18658a10></line><line x1="1" y1="12" x2="3" y2="12" data-v-18658a10></line><line x1="21" y1="12" x2="23" y2="12" data-v-18658a10></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36" data-v-18658a10></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22" data-v-18658a10></line>',8)]),8,l)],2))])}var u={name:"ToggleDark",data(){return{isDark:this.$store.getters.useDarkMode}},methods:{handleToggleClick(){this.isDark=!this.isDark,this.$store.commit("setUseDarkMode",this.isDark),this.$store.commit("setCusDarkMode",!0)}}},d=t(1241);const h=(0,d.A)(u,[["render",c],["__scopeId","data-v-18658a10"]]);var k=h},8270:function(e,a,t){t.r(a),t.d(a,{default:function(){return f}});var s=t(8898),r=t(47),n=(t(5331),t(9648),t(2105)),o=(t(9092),t(3154)),i=t(6768),l=t(5130);const c={class:"login"},u={class:"login-container"},d={class:"input-container"},h={class:"input-container"};function k(e,a,t,k,g,m){const p=o.A,f=n.WK,v=r.S2,y=s.A;return(0,i.uX)(),(0,i.CE)("div",c,[(0,i.bF)(p),(0,i.Lk)("div",u,[a[5]||(a[5]=(0,i.Lk)("h1",{class:"login-title"},"管理端登录",-1)),(0,i.Lk)("div",d,[a[2]||(a[2]=(0,i.Lk)("a",{class:"input-name"},"用户名:",-1)),(0,i.bF)(f,{modelValue:g.username,"onUpdate:modelValue":a[0]||(a[0]=e=>g.username=e),placeholder:"请输入用户名",class:"password-input"},null,8,["modelValue"])]),(0,i.Lk)("div",h,[a[3]||(a[3]=(0,i.Lk)("a",{class:"input-name"},"密码:",-1)),(0,i.bF)(f,{modelValue:g.password,"onUpdate:modelValue":a[1]||(a[1]=e=>g.password=e),placeholder:"请输入密码",class:"password-input",type:"password","show-password":"",onKeyup:(0,l.jR)(m.login,["enter","native"])},null,8,["modelValue","onKeyup"])]),(0,i.bF)(v,{class:"submit",type:"primary",onClick:m.login},{default:(0,i.k6)((()=>a[4]||(a[4]=[(0,i.eW)("登录")]))),_:1},8,["onClick"])]),(0,i.bF)(y,{class:"footer"})])}t(4114),t(4979);var g={data(){return{password:"",username:""}},components:{Footer:s.A,ToggleDark:o.A},methods:{async login(){const e=btoa(`${this.username}:${this.password}`);try{const a=await fetch("/api/manage/check",{method:"GET",headers:{Authorization:`Basic ${e}`},credentials:"include"});401===a.status?this.$message.error("用户名或密码错误"):200===a.status?(this.$store.commit("setCredentials",e),this.$router.push("/dashboard")):this.$message.error("用户名或密码错误")}catch(a){this.$message.error("服务器错误")}}}},m=t(1241);const p=(0,m.A)(g,[["render",k],["__scopeId","data-v-fd423e52"]]);var f=p}}]);
|
||||
//# sourceMappingURL=270.bc2581a7.js.map
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user