Compare commits

...

18 Commits

Author SHA1 Message Date
Dream Hunter
d0ccc3ded1 v0.6.1 2024-07-22 13:09:42 +08:00
Dream Hunter
163d9451f7 feat: worker: newAddress if domain is not set, use the first domain (#358) 2024-07-22 13:05:50 +08:00
Dream Hunter
60dda7e3fe feat: add ANNOUNCEMENT (#357) 2024-07-22 13:01:38 +08:00
Dream Hunter
384eb9b041 fix: imap proxy do not support password && cleanup days translate (#356) 2024-07-19 22:40:53 +08:00
tqjason
38816cbf0f Add new workflow action and Fix cleanup bug (#355)
* Create frontend_pagefunction_deploy.yaml

* Update frontend_pagefunction_deploy.yaml

* Update cleanup_api.ts

* Update common.ts

* Update cleanup_api.ts

* Update common.ts
2024-07-19 22:34:01 +08:00
Dream Hunter
d7d1ba6b64 feat: wrangler d1 execute dev add --remote (#352) 2024-07-15 12:04:14 +08:00
Dream Hunter
14725e9e9f feat: add USER_DEFAULT_ROLE (#351) 2024-07-14 20:44:03 +08:00
Dream Hunter
2c1e63b8bc feat: add USER_DEFAULT_ROLE (#350) 2024-07-14 20:38:55 +08:00
Dream Hunter
f3a1d980c5 fix: roleDonotExist tip (#349) 2024-07-14 20:09:21 +08:00
Dream Hunter
75c48beb3b feat: add USER_ROLES && admin pages search when keybord enter && auto trim (#348)
* feat: add USER_ROLES

* feat: admin pages search when keybord enter && auto trim

* feat: update version to v0.6.0
2024-07-14 19:57:43 +08:00
Dream Hunter
26ccfdd6e0 feat: only allow address [a-z0-9] (#347) 2024-07-13 19:03:54 +08:00
刘志聪
aa8f3b4d46 fix: remove useless sql (#342) 2024-07-10 01:06:00 +08:00
Dream Hunter
a749c829d2 feat: update docs (#340) 2024-07-08 19:09:37 +08:00
Dream Hunter
4b2caf1a4b feat: update docs (#339) 2024-07-08 19:02:14 +08:00
Dream Hunter
80a8848ed8 feat: remove apiV1 and tables && update admin/statistics (#337) 2024-07-08 12:33:43 +08:00
Dream Hunter
dcfc1b3721 Update CHANGELOG.md 2024-07-07 12:55:29 +08:00
Dream Hunter
b0a0a6a1ef feat: updage dependencies (#336) 2024-07-06 20:26:54 +08:00
Dream Hunter
00c671cf14 feat: logo click 5 time to admin page && fix: 401 cannot show auth modal (#335) 2024-07-06 20:21:21 +08:00
44 changed files with 1289 additions and 920 deletions

View File

@@ -0,0 +1,39 @@
name: Deploy Frontend with page function
on:
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 18
- uses: pnpm/action-setup@v3
name: Install pnpm
id: pnpm-install
with:
version: 8
run_install: false
- name: Deploy Frontend for ${{ github.ref_name }}
run: |
cd frontend/
pnpm install --no-frozen-lockfile
pnpm build:pages
cd ../pages/
echo '${{ secrets.PAGE_TOML }}' > wrangler.toml
pnpm install --no-frozen-lockfile
pnpm run deploy
echo "Deploying prodcution for ${{ github.ref_name }}"
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

View File

@@ -1,6 +1,34 @@
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
# CHANGE LOG
## v0.6.1
- pages github actions && 修复清理邮件天数为 0 不生效 by @tqjason (#355)
- fix: imap proxy server 不支持 密码 by @dreamhunter2333 (#356)
- worker 新增 `ANNOUNCEMENT` 配置, 用于配置公告信息 by @dreamhunter2333 (#357)
- fix: telegram bot 新建地址默认选择第一个域名 by @dreamhunter2333 (#358)
## v0.6.0
### Breaking Changes
DB changes: 增加用户角色表, 需要执行 `db/2024-07-14-patch.sql` 更新 `D1` 数据库
### Changes
worker 配置文件新增 `DEFAULT_DOMAINS`, `USER_ROLES`, `USER_DEFAULT_ROLE`, 具体查看文档 [worker配置](https://temp-mail-docs.awsl.uk/zh/guide/cli/worker.html#%E4%BF%AE%E6%94%B9-wrangler-toml-%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6)
- 移除 `apiV1` 相关代码和相关的数据库表
- 更新 `admin/statistics` api, 添加用户统计信息
- 更新地址的规则,只允许小写+数字,对于历史的地址在查询邮件时会进行 `lowercase` 处理
- 增加用户角色功能,`admin` 可以设置用户角色(目前可配置每个角色域名和前缀)
- admin 页面搜索优化, 回车自动搜索, 输入内容自动 trim
## v0.5.4
- 点击 logo 5 次进入 admin 页面
- 修复 401 时无法跳转登录页面(admin 和 网站认证)
## v0.5.3
- 修复 smtp imap proxy sever 的一些 bug
@@ -292,7 +320,7 @@ The `mails` table will be discarded, and the `raw` text of the new `mail` will b
```bash
git checkout v0.2.0
cd worker
wrangler d1 execute dev --file=../db/2024-04-09-patch.sql
wrangler d1 execute dev --file=../db/2024-04-09-patch.sql --remote
pnpm run deploy
cd ../frontend
pnpm run deploy

9
db/2024-07-14-patch.sql Normal file
View File

@@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS user_roles (
id INTEGER PRIMARY KEY,
user_id INTEGER UNIQUE NOT NULL,
role_text TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id);

View File

@@ -1,15 +1,3 @@
CREATE TABLE IF NOT EXISTS mails (
id INTEGER PRIMARY KEY,
message_id TEXT,
source TEXT,
address TEXT,
subject TEXT,
message TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_mails_address ON mails(address);
CREATE TABLE IF NOT EXISTS raw_mails (
id INTEGER PRIMARY KEY,
message_id TEXT,
@@ -43,15 +31,6 @@ CREATE TABLE IF NOT EXISTS auto_reply_mails (
CREATE INDEX IF NOT EXISTS idx_auto_reply_mails_address ON auto_reply_mails(address);
CREATE TABLE IF NOT EXISTS attachments (
id INTEGER PRIMARY KEY,
source TEXT,
address TEXT,
message_id TEXT,
data TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS address_sender (
id INTEGER PRIMARY KEY,
address TEXT UNIQUE,
@@ -99,3 +78,13 @@ CREATE TABLE IF NOT EXISTS users_address (
CREATE INDEX IF NOT EXISTS idx_users_address_user_id ON users_address(user_id);
CREATE INDEX IF NOT EXISTS idx_users_address_address_id ON users_address(address_id);
CREATE TABLE IF NOT EXISTS user_roles (
id INTEGER PRIMARY KEY,
user_id INTEGER UNIQUE NOT NULL,
role_text TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id);

View File

@@ -1,6 +1,6 @@
{
"name": "cloudflare_temp_email",
"version": "0.5.3",
"version": "0.6.1",
"private": true,
"type": "module",
"scripts": {
@@ -17,7 +17,7 @@
"deploy:actions": "npm run build && wrangler pages deploy ./dist"
},
"dependencies": {
"@unhead/vue": "^1.9.14",
"@unhead/vue": "^1.9.15",
"@vicons/material": "^0.12.0",
"@vueuse/core": "^10.11.0",
"@wangeditor/editor": "^5.1.23",
@@ -38,11 +38,11 @@
"@vitejs/plugin-vue": "^5.0.5",
"unplugin-auto-import": "^0.17.6",
"unplugin-vue-components": "^0.27.2",
"vite": "^5.3.2",
"vite": "^5.3.3",
"vite-plugin-pwa": "^0.19.8",
"vite-plugin-top-level-await": "^1.4.1",
"vite-plugin-wasm": "^3.3.0",
"workbox-window": "^7.1.0",
"wrangler": "^3.62.0"
"wrangler": "^3.63.1"
}
}

354
frontend/pnpm-lock.yaml generated
View File

@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@unhead/vue':
specifier: ^1.9.14
version: 1.9.14(vue@3.4.31(typescript@5.4.5))
specifier: ^1.9.15
version: 1.9.15(vue@3.4.31(typescript@5.4.5))
'@vicons/material':
specifier: ^0.12.0
version: 0.12.0
@@ -59,7 +59,7 @@ importers:
version: 0.12.0
'@vitejs/plugin-vue':
specifier: ^5.0.5
version: 5.0.5(vite@5.3.2(@types/node@20.14.9)(terser@5.31.1))(vue@3.4.31(typescript@5.4.5))
version: 5.0.5(vite@5.3.3(@types/node@20.14.10)(terser@5.31.1))(vue@3.4.31(typescript@5.4.5))
unplugin-auto-import:
specifier: ^0.17.6
version: 0.17.6(@vueuse/core@10.11.0(vue@3.4.31(typescript@5.4.5)))(rollup@2.79.1)
@@ -67,23 +67,23 @@ importers:
specifier: ^0.27.2
version: 0.27.2(@babel/parser@7.24.7)(rollup@2.79.1)(vue@3.4.31(typescript@5.4.5))
vite:
specifier: ^5.3.2
version: 5.3.2(@types/node@20.14.9)(terser@5.31.1)
specifier: ^5.3.3
version: 5.3.3(@types/node@20.14.10)(terser@5.31.1)
vite-plugin-pwa:
specifier: ^0.19.8
version: 0.19.8(vite@5.3.2(@types/node@20.14.9)(terser@5.31.1))(workbox-build@7.0.0)(workbox-window@7.1.0)
version: 0.19.8(vite@5.3.3(@types/node@20.14.10)(terser@5.31.1))(workbox-build@7.0.0)(workbox-window@7.1.0)
vite-plugin-top-level-await:
specifier: ^1.4.1
version: 1.4.1(rollup@2.79.1)(vite@5.3.2(@types/node@20.14.9)(terser@5.31.1))
version: 1.4.1(rollup@2.79.1)(vite@5.3.3(@types/node@20.14.10)(terser@5.31.1))
vite-plugin-wasm:
specifier: ^3.3.0
version: 3.3.0(vite@5.3.2(@types/node@20.14.9)(terser@5.31.1))
version: 3.3.0(vite@5.3.3(@types/node@20.14.10)(terser@5.31.1))
workbox-window:
specifier: ^7.1.0
version: 7.1.0
wrangler:
specifier: ^3.62.0
version: 3.62.0
specifier: ^3.63.1
version: 3.63.1
packages:
@@ -680,32 +680,32 @@ packages:
resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==}
engines: {node: '>=16.13'}
'@cloudflare/workerd-darwin-64@1.20240620.1':
resolution: {integrity: sha512-YWeS2aE8jAzDefuus/3GmZcFGu3Ef94uCAoxsQuaEXNsiGM9NeAhPpKC1BJAlcv168U/Q1J+6hckcGtipf6ZcQ==}
'@cloudflare/workerd-darwin-64@1.20240701.0':
resolution: {integrity: sha512-XAZa4ZP+qyTn6JQQACCPH09hGZXP2lTnWKkmg5mPwT8EyRzCKLkczAf98vPP5bq7JZD/zORdFWRY0dOTap8zTQ==}
engines: {node: '>=16'}
cpu: [x64]
os: [darwin]
'@cloudflare/workerd-darwin-arm64@1.20240620.1':
resolution: {integrity: sha512-3rdND+EHpmCrwYX6hvxIBSBJ0f40tRNxond1Vfw7GiR1MJVi3gragiBx75UDFHCxfRw3J0GZ1qVlkRce2/Xbsg==}
'@cloudflare/workerd-darwin-arm64@1.20240701.0':
resolution: {integrity: sha512-w80ZVAgfH4UwTz7fXZtk7KmS2FzlXniuQm4ku4+cIgRTilBAuKqjpOjwUCbx5g13Gqcm9NuiHce+IDGtobRTIQ==}
engines: {node: '>=16'}
cpu: [arm64]
os: [darwin]
'@cloudflare/workerd-linux-64@1.20240620.1':
resolution: {integrity: sha512-tURcTrXGeSbYqeM5ISVcofY20StKbVIcdxjJvNYNZ+qmSV9Fvn+zr7rRE+q64pEloVZfhsEPAlUCnFso5VV4XQ==}
'@cloudflare/workerd-linux-64@1.20240701.0':
resolution: {integrity: sha512-UWLr/Anxwwe/25nGv451MNd2jhREmPt/ws17DJJqTLAx6JxwGWA15MeitAIzl0dbxRFAJa+0+R8ag2WR3F/D6g==}
engines: {node: '>=16'}
cpu: [x64]
os: [linux]
'@cloudflare/workerd-linux-arm64@1.20240620.1':
resolution: {integrity: sha512-TThvkwNxaZFKhHZnNjOGqIYCOk05DDWgO+wYMuXg15ymN/KZPnCicRAkuyqiM+R1Fgc4kwe/pehjP8pbmcf6sg==}
'@cloudflare/workerd-linux-arm64@1.20240701.0':
resolution: {integrity: sha512-3kCnF9kYgov1ggpuWbgpXt4stPOIYtVmPCa7MO2xhhA0TWP6JDUHRUOsnmIgKrvDjXuXqlK16cdg3v+EWsaPJg==}
engines: {node: '>=16'}
cpu: [arm64]
os: [linux]
'@cloudflare/workerd-windows-64@1.20240620.1':
resolution: {integrity: sha512-Y/BA9Yj0r7Al1HK3nDHcfISgFllw6NR3XMMPChev57vrVT9C9D4erBL3sUBfofHU+2U9L+ShLsl6obBpe3vvUw==}
'@cloudflare/workerd-windows-64@1.20240701.0':
resolution: {integrity: sha512-6IPGITRAeS67j3BH1rN4iwYWDt47SqJG7KlZJ5bB4UaNAia4mvMBSy/p2p4vA89bbXoDRjMtEvRu7Robu6O7hQ==}
engines: {node: '>=16'}
cpu: [x64]
os: [win32]
@@ -1191,68 +1191,68 @@ packages:
'@surma/rollup-plugin-off-main-thread@2.2.3':
resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==}
'@swc/core-darwin-arm64@1.6.5':
resolution: {integrity: sha512-RGQhMdni2v1/ANQ/2K+F+QYdzaucekYBewZcX1ogqJ8G5sbPaBdYdDN1qQ4kHLCIkPtGP6qC7c71qPEqL2RidQ==}
'@swc/core-darwin-arm64@1.6.13':
resolution: {integrity: sha512-SOF4buAis72K22BGJ3N8y88mLNfxLNprTuJUpzikyMGrvkuBFNcxYtMhmomO0XHsgLDzOJ+hWzcgjRNzjMsUcQ==}
engines: {node: '>=10'}
cpu: [arm64]
os: [darwin]
'@swc/core-darwin-x64@1.6.5':
resolution: {integrity: sha512-/pSN0/Jtcbbb9+ovS9rKxR3qertpFAM3OEJr/+Dh/8yy7jK5G5EFPIrfsw/7Q5987ERPIJIH6BspK2CBB2tgcg==}
'@swc/core-darwin-x64@1.6.13':
resolution: {integrity: sha512-AW8akFSC+tmPE6YQQvK9S2A1B8pjnXEINg+gGgw0KRUUXunvu1/OEOeC5L2Co1wAwhD7bhnaefi06Qi9AiwOag==}
engines: {node: '>=10'}
cpu: [x64]
os: [darwin]
'@swc/core-linux-arm-gnueabihf@1.6.5':
resolution: {integrity: sha512-B0g/dROCE747RRegs/jPHuKJgwXLracDhnqQa80kFdgWEMjlcb7OMCgs5OX86yJGRS4qcYbiMGD0Pp7Kbqn3yw==}
'@swc/core-linux-arm-gnueabihf@1.6.13':
resolution: {integrity: sha512-f4gxxvDXVUm2HLYXRd311mSrmbpQF2MZ4Ja6XCQz1hWAxXdhRl1gpnZ+LH/xIfGSwQChrtLLVrkxdYUCVuIjFg==}
engines: {node: '>=10'}
cpu: [arm]
os: [linux]
'@swc/core-linux-arm64-gnu@1.6.5':
resolution: {integrity: sha512-W8meapgXTq8AOtSvDG4yKR8ant2WWD++yOjgzAleB5VAC+oC+aa8YJROGxj8HepurU8kurqzcialwoMeq5SZZQ==}
'@swc/core-linux-arm64-gnu@1.6.13':
resolution: {integrity: sha512-Nf/eoW2CbG8s+9JoLtjl9FByBXyQ5cjdBsA4efO7Zw4p+YSuXDgc8HRPC+E2+ns0praDpKNZtLvDtmF2lL+2Gg==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
'@swc/core-linux-arm64-musl@1.6.5':
resolution: {integrity: sha512-jyCKqoX50Fg8rJUQqh4u5PqnE7nqYKXHjVH2WcYr114/MU21zlsI+YL6aOQU1XP8bJQ2gPQ1rnlnGJdEHiKS/w==}
'@swc/core-linux-arm64-musl@1.6.13':
resolution: {integrity: sha512-2OysYSYtdw79prJYuKIiux/Gj0iaGEbpS2QZWCIY4X9sGoETJ5iMg+lY+YCrIxdkkNYd7OhIbXdYFyGs/w5LDg==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
'@swc/core-linux-x64-gnu@1.6.5':
resolution: {integrity: sha512-G6HmUn/RRIlXC0YYFfBz2qh6OZkHS/KUPkhoG4X9ADcgWXXjOFh6JrefwsYj8VBAJEnr5iewzjNfj+nztwHaeA==}
'@swc/core-linux-x64-gnu@1.6.13':
resolution: {integrity: sha512-PkR4CZYJNk5hcd2+tMWBpnisnmYsUzazI1O5X7VkIGFcGePTqJ/bWlfUIVVExWxvAI33PQFzLbzmN5scyIUyGQ==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
'@swc/core-linux-x64-musl@1.6.5':
resolution: {integrity: sha512-AQpBjBnelQDSbeTJA50AXdS6+CP66LsXIMNTwhPSgUfE7Bx1ggZV11Fsi4Q5SGcs6a8Qw1cuYKN57ZfZC5QOuA==}
'@swc/core-linux-x64-musl@1.6.13':
resolution: {integrity: sha512-OdsY7wryTxCKwGQcwW9jwWg3cxaHBkTTHi91+5nm7hFPpmZMz1HivJrWAMwVE7iXFw+M4l6ugB/wCvpYrUAAjA==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
'@swc/core-win32-arm64-msvc@1.6.5':
resolution: {integrity: sha512-MZTWM8kUwS30pVrtbzSGEXtek46aXNb/mT9D6rsS7NvOuv2w+qZhjR1rzf4LNbbn5f8VnR4Nac1WIOYZmfC5ng==}
'@swc/core-win32-arm64-msvc@1.6.13':
resolution: {integrity: sha512-ap6uNmYjwk9M/+bFEuWRNl3hq4VqgQ/Lk+ID/F5WGqczNr0L7vEf+pOsRAn0F6EV+o/nyb3ePt8rLhE/wjHpPg==}
engines: {node: '>=10'}
cpu: [arm64]
os: [win32]
'@swc/core-win32-ia32-msvc@1.6.5':
resolution: {integrity: sha512-WZdu4gISAr3yOm1fVwKhhk6+MrP7kVX0KMP7+ZQFTN5zXQEiDSDunEJKVgjMVj3vlR+6mnAqa/L0V9Qa8+zKlQ==}
'@swc/core-win32-ia32-msvc@1.6.13':
resolution: {integrity: sha512-IJ8KH4yIUHTnS/U1jwQmtbfQals7zWPG0a9hbEfIr4zI0yKzjd83lmtS09lm2Q24QBWOCFGEEbuZxR4tIlvfzA==}
engines: {node: '>=10'}
cpu: [ia32]
os: [win32]
'@swc/core-win32-x64-msvc@1.6.5':
resolution: {integrity: sha512-ezXgucnMTzlFIxQZw7ls/5r2hseFaRoDL04cuXUOs97E8r+nJSmFsRQm/ygH5jBeXNo59nyZCalrjJAjwfgACA==}
'@swc/core-win32-x64-msvc@1.6.13':
resolution: {integrity: sha512-f6/sx6LMuEnbuxtiSL/EkR0Y6qUHFw1XVrh6rwzKXptTipUdOY+nXpKoh+1UsBm/r7H0/5DtOdrn3q5ZHbFZjQ==}
engines: {node: '>=10'}
cpu: [x64]
os: [win32]
'@swc/core@1.6.5':
resolution: {integrity: sha512-tyVvUK/HDOUUsK6/GmWvnqUtD9oDpPUA4f7f7JCOV8hXxtfjMtAZeBKf93yrB1XZet69TDR7EN0hFC6i4MF0Ig==}
'@swc/core@1.6.13':
resolution: {integrity: sha512-eailUYex6fkfaQTev4Oa3mwn0/e3mQU4H8y1WPuImYQESOQDtVrowwUGDSc19evpBbHpKtwM+hw8nLlhIsF+Tw==}
engines: {node: '>=10'}
peerDependencies:
'@swc/helpers': '*'
@@ -1290,8 +1290,8 @@ packages:
'@types/node-forge@1.3.11':
resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==}
'@types/node@20.14.9':
resolution: {integrity: sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==}
'@types/node@20.14.10':
resolution: {integrity: sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==}
'@types/resolve@1.17.1':
resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
@@ -1302,17 +1302,17 @@ packages:
'@types/web-bluetooth@0.0.20':
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
'@unhead/dom@1.9.14':
resolution: {integrity: sha512-XZSZ2Wmm1Sv7k9scSFGrarbteSIl3p3I3oOUprKPDboBTvuG5q81Qz8O99NKUGKGJ8BKUkxCqE982eH3S8DKJA==}
'@unhead/dom@1.9.15':
resolution: {integrity: sha512-4sdP/2Unt4zFRO8pBZVXvebidGmrLEvnDU6ZpasZfInjiiuuaQOVTJaiKnEnug3cmW2YjglPG2d1c2xAsHr3NQ==}
'@unhead/schema@1.9.14':
resolution: {integrity: sha512-60NYSM6QjfK/wx4/QfaYyZ3XnNtwxS9a1oij2abEkGHPmA2/fqBOXeuHtnBo4eD42/Eg+owcS5s3mClPL8AkXw==}
'@unhead/schema@1.9.15':
resolution: {integrity: sha512-9ADZuXOH+tOKHIjXsgg+SPINnh/YJEBMCjpg+8VLGgE2r5med3jAnOU8g7ALfuVEBRBrbFgs1qVKoKm1NkTXJQ==}
'@unhead/shared@1.9.14':
resolution: {integrity: sha512-7ZIC7uDV8gp3KHm5JxJ/NXMENQgkh+SCyTcsILSpOhkAGeszMHABrB6vjeZDGM4J9mRUxwyPn24KI2zG/R+XiQ==}
'@unhead/shared@1.9.15':
resolution: {integrity: sha512-+U5r04eRtCNcniWjzNPRtwVuF9rW/6EXxhGvuohJBDaIE57J6BHWo5cEp7Pqts7DlTFs7LiDtH8ONNDv4QqRaw==}
'@unhead/vue@1.9.14':
resolution: {integrity: sha512-Yc7Qv0ze+iLte4urHiA+ghkF7y+svrawrT+ZrCuGXkZ/eRTF/AY2SKex+rJQJZsP+fKEQ2pGb72IsI5kHFZT3A==}
'@unhead/vue@1.9.15':
resolution: {integrity: sha512-h866wYOs6Q6+lc0av4EU0CPTtTvaz9UWwwsiNoulzJa95QyUN/gDPI/NiDuKweHswY+a0SSzEqe9Nhg+LlmHew==}
peerDependencies:
vue: '>=2.7 || >=3'
@@ -1478,8 +1478,8 @@ packages:
resolution: {integrity: sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==}
engines: {node: '>=0.4.0'}
acorn@8.12.0:
resolution: {integrity: sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==}
acorn@8.12.1:
resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==}
engines: {node: '>=0.4.0'}
hasBin: true
@@ -1580,8 +1580,8 @@ packages:
resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==}
engines: {node: '>= 0.4'}
caniuse-lite@1.0.30001638:
resolution: {integrity: sha512-5SuJUJ7cZnhPpeLHaH0c/HPAnAHZvS6ElWyHK9GSIbVOQABLzowiI2pjmpvZ1WEbkyz46iFd4UXlOHR5SqgfMQ==}
caniuse-lite@1.0.30001640:
resolution: {integrity: sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==}
capnp-ts@0.7.0:
resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==}
@@ -1734,8 +1734,8 @@ packages:
engines: {node: '>=0.10.0'}
hasBin: true
electron-to-chromium@1.4.814:
resolution: {integrity: sha512-GVulpHjFu1Y9ZvikvbArHmAhZXtm3wHlpjTMcXNGKl4IQ4jMQjlnz8yMQYYqdLHKi/jEL2+CBC2akWVCoIGUdw==}
electron-to-chromium@1.4.818:
resolution: {integrity: sha512-eGvIk2V0dGImV9gWLq8fDfTTsCAeMDwZqEPMr+jMInxZdnp9Us8UpovYpRCf9NQ7VOFgrN2doNSgvISbsbNpxA==}
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
@@ -1969,8 +1969,8 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
highlight.js@11.9.0:
resolution: {integrity: sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==}
highlight.js@11.10.0:
resolution: {integrity: sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==}
engines: {node: '>=12.0.0'}
hookable@5.5.3:
@@ -2232,8 +2232,8 @@ packages:
engines: {node: '>=10.0.0'}
hasBin: true
miniflare@3.20240620.0:
resolution: {integrity: sha512-NBMzqUE2mMlh/hIdt6U5MP+aFhEjKDq3l8CAajXAQa1WkndJdciWvzB2mfLETwoVFhMl/lphaVzyEN2AgwJpbQ==}
miniflare@3.20240701.0:
resolution: {integrity: sha512-m9+I+7JNyqDGftCMKp9cK9pCZkK72hAL2mM9IWwhct+ZmucLBA8Uu6+rHQqA5iod86cpwOkrB2PrPA3wx9YNgw==}
engines: {node: '>=16.13'}
hasBin: true
@@ -2326,8 +2326,8 @@ packages:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
pkg-types@1.1.1:
resolution: {integrity: sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ==}
pkg-types@1.1.3:
resolution: {integrity: sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA==}
possible-typed-array-names@1.0.0:
resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==}
@@ -2336,12 +2336,12 @@ packages:
postal-mime@2.2.5:
resolution: {integrity: sha512-6eTJf+B47JMdDuLF/4MBiGpTinxl0W8bA9CzrSoiQrNVRqK8Vhe59VrS6sXh2lG/lgo0bxpZFcWOF4Dv1FaSfg==}
postcss@8.4.38:
resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==}
postcss@8.4.39:
resolution: {integrity: sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==}
engines: {node: ^10 || ^12 || >=14}
preact@10.22.0:
resolution: {integrity: sha512-RRurnSjJPj4rp5K6XoP45Ui33ncb7e4H7WiOHVpjbkvqvA3U+N8Z6Qbo0AE6leGYBV66n8EhEaFixvIu3SkxFw==}
preact@10.22.1:
resolution: {integrity: sha512-jRYbDDgMpIb5LHq3hkI0bbl+l/TQ9UnkdQ0ww+lp+4MMOdqaUYdFc5qeyP+IV8FAd/2Em7drVPeKdQxsiWCf/A==}
pretty-bytes@5.6.0:
resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==}
@@ -2665,8 +2665,8 @@ packages:
unenv-nightly@1.10.0-1717606461.a117952:
resolution: {integrity: sha512-u3TfBX02WzbHTpaEfWEKwDijDSFAHcgXkayUZ+MVDrjhLFvgAJzFGTSTmwlEhwWi2exyRQey23ah9wELMM6etg==}
unhead@1.9.14:
resolution: {integrity: sha512-npdYu6CfasX/IhB8OO27e3u4A1zhAY77T1FwWDIIUaJvugYTte5hjsolPX0/fG5jmjnWTFTuIkmbCSfj7bfIkg==}
unhead@1.9.15:
resolution: {integrity: sha512-/99Wft1CT0fxsWzmBeOwuH/k4HdMeyfDGyB4wFNVZVNTffRHDOqaqQ6RS+LHPsIiCKmm9FP7Vq7Rz09Zs/fQJQ==}
unicode-canonical-property-names-ecmascript@2.0.0:
resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==}
@@ -2720,16 +2720,16 @@ packages:
'@nuxt/kit':
optional: true
unplugin@1.10.1:
resolution: {integrity: sha512-d6Mhq8RJeGA8UfKCu54Um4lFA0eSaRa3XxdAJg8tIdxbu1ubW0hBCZUL7yI2uGyYCRndvbK8FLHzqy2XKfeMsg==}
unplugin@1.11.0:
resolution: {integrity: sha512-3r7VWZ/webh0SGgJScpWl2/MRCZK5d3ZYFcNaeci/GQ7Teop7zf0Nl2pUuz7G21BwPd9pcUPOC5KmJ2L3WgC5g==}
engines: {node: '>=14.0.0'}
upath@1.2.0:
resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==}
engines: {node: '>=4'}
update-browserslist-db@1.0.16:
resolution: {integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==}
update-browserslist-db@1.1.0:
resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
@@ -2771,8 +2771,8 @@ packages:
peerDependencies:
vite: ^2 || ^3 || ^4 || ^5
vite@5.3.2:
resolution: {integrity: sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==}
vite@5.3.3:
resolution: {integrity: sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
@@ -2921,13 +2921,13 @@ packages:
workbox-window@7.1.0:
resolution: {integrity: sha512-ZHeROyqR+AS5UPzholQRDttLFqGMwP0Np8MKWAdyxsDETxq3qOAyXvqessc3GniohG6e0mAqSQyKOHmT8zPF7g==}
workerd@1.20240620.1:
resolution: {integrity: sha512-Qoq+RrFNk4pvEO+kpJVn8uJ5TRE9YJx5jX5pC5LjdKlw1XeD8EdXt5k0TbByvWunZ4qgYIcF9lnVxhcDFo203g==}
workerd@1.20240701.0:
resolution: {integrity: sha512-qSgNVqauqzNCij9MaJLF2c2ko3AnFioVSIxMSryGbRK+LvtGr9BKBt6JOxCb24DoJASoJDx3pe3DJHBVydUiBg==}
engines: {node: '>=16'}
hasBin: true
wrangler@3.62.0:
resolution: {integrity: sha512-TM1Bd8+GzxFw/JzwsC3i/Oss4LTWvIEWXXo1vZhx+7PHcsxdbnQGBBwPurHNJDSu2Pw22+2pCZiUGKexmgJksw==}
wrangler@3.63.1:
resolution: {integrity: sha512-fxMPNEyDc9pZNtQOuYqRikzv6lL5eP4S1zv7L/kw24uu1cCEmJ39j8bfJGzrAEqKDNsiFXVjEka0RjlpgEVWPg==}
engines: {node: '>=16.17.0'}
hasBin: true
peerDependencies:
@@ -2939,8 +2939,8 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
ws@8.17.1:
resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==}
ws@8.18.0:
resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
@@ -3754,19 +3754,19 @@ snapshots:
dependencies:
mime: 3.0.0
'@cloudflare/workerd-darwin-64@1.20240620.1':
'@cloudflare/workerd-darwin-64@1.20240701.0':
optional: true
'@cloudflare/workerd-darwin-arm64@1.20240620.1':
'@cloudflare/workerd-darwin-arm64@1.20240701.0':
optional: true
'@cloudflare/workerd-linux-64@1.20240620.1':
'@cloudflare/workerd-linux-64@1.20240701.0':
optional: true
'@cloudflare/workerd-linux-arm64@1.20240620.1':
'@cloudflare/workerd-linux-arm64@1.20240701.0':
optional: true
'@cloudflare/workerd-windows-64@1.20240620.1':
'@cloudflare/workerd-windows-64@1.20240701.0':
optional: true
'@cspotcode/source-map-support@0.8.1':
@@ -4082,51 +4082,51 @@ snapshots:
magic-string: 0.25.9
string.prototype.matchall: 4.0.11
'@swc/core-darwin-arm64@1.6.5':
'@swc/core-darwin-arm64@1.6.13':
optional: true
'@swc/core-darwin-x64@1.6.5':
'@swc/core-darwin-x64@1.6.13':
optional: true
'@swc/core-linux-arm-gnueabihf@1.6.5':
'@swc/core-linux-arm-gnueabihf@1.6.13':
optional: true
'@swc/core-linux-arm64-gnu@1.6.5':
'@swc/core-linux-arm64-gnu@1.6.13':
optional: true
'@swc/core-linux-arm64-musl@1.6.5':
'@swc/core-linux-arm64-musl@1.6.13':
optional: true
'@swc/core-linux-x64-gnu@1.6.5':
'@swc/core-linux-x64-gnu@1.6.13':
optional: true
'@swc/core-linux-x64-musl@1.6.5':
'@swc/core-linux-x64-musl@1.6.13':
optional: true
'@swc/core-win32-arm64-msvc@1.6.5':
'@swc/core-win32-arm64-msvc@1.6.13':
optional: true
'@swc/core-win32-ia32-msvc@1.6.5':
'@swc/core-win32-ia32-msvc@1.6.13':
optional: true
'@swc/core-win32-x64-msvc@1.6.5':
'@swc/core-win32-x64-msvc@1.6.13':
optional: true
'@swc/core@1.6.5':
'@swc/core@1.6.13':
dependencies:
'@swc/counter': 0.1.3
'@swc/types': 0.1.9
optionalDependencies:
'@swc/core-darwin-arm64': 1.6.5
'@swc/core-darwin-x64': 1.6.5
'@swc/core-linux-arm-gnueabihf': 1.6.5
'@swc/core-linux-arm64-gnu': 1.6.5
'@swc/core-linux-arm64-musl': 1.6.5
'@swc/core-linux-x64-gnu': 1.6.5
'@swc/core-linux-x64-musl': 1.6.5
'@swc/core-win32-arm64-msvc': 1.6.5
'@swc/core-win32-ia32-msvc': 1.6.5
'@swc/core-win32-x64-msvc': 1.6.5
'@swc/core-darwin-arm64': 1.6.13
'@swc/core-darwin-x64': 1.6.13
'@swc/core-linux-arm-gnueabihf': 1.6.13
'@swc/core-linux-arm64-gnu': 1.6.13
'@swc/core-linux-arm64-musl': 1.6.13
'@swc/core-linux-x64-gnu': 1.6.13
'@swc/core-linux-x64-musl': 1.6.13
'@swc/core-win32-arm64-msvc': 1.6.13
'@swc/core-win32-ia32-msvc': 1.6.13
'@swc/core-win32-x64-msvc': 1.6.13
'@swc/counter@0.1.3': {}
@@ -4152,40 +4152,40 @@ snapshots:
'@types/node-forge@1.3.11':
dependencies:
'@types/node': 20.14.9
'@types/node': 20.14.10
'@types/node@20.14.9':
'@types/node@20.14.10':
dependencies:
undici-types: 5.26.5
'@types/resolve@1.17.1':
dependencies:
'@types/node': 20.14.9
'@types/node': 20.14.10
'@types/trusted-types@2.0.7': {}
'@types/web-bluetooth@0.0.20': {}
'@unhead/dom@1.9.14':
'@unhead/dom@1.9.15':
dependencies:
'@unhead/schema': 1.9.14
'@unhead/shared': 1.9.14
'@unhead/schema': 1.9.15
'@unhead/shared': 1.9.15
'@unhead/schema@1.9.14':
'@unhead/schema@1.9.15':
dependencies:
hookable: 5.5.3
zhead: 2.2.4
'@unhead/shared@1.9.14':
'@unhead/shared@1.9.15':
dependencies:
'@unhead/schema': 1.9.14
'@unhead/schema': 1.9.15
'@unhead/vue@1.9.14(vue@3.4.31(typescript@5.4.5))':
'@unhead/vue@1.9.15(vue@3.4.31(typescript@5.4.5))':
dependencies:
'@unhead/schema': 1.9.14
'@unhead/shared': 1.9.14
'@unhead/schema': 1.9.15
'@unhead/shared': 1.9.15
hookable: 5.5.3
unhead: 1.9.14
unhead: 1.9.15
vue: 3.4.31(typescript@5.4.5)
'@uppy/companion-client@2.2.2':
@@ -4202,7 +4202,7 @@ snapshots:
mime-match: 1.0.2
namespace-emitter: 2.0.1
nanoid: 3.3.7
preact: 10.22.0
preact: 10.22.1
'@uppy/store-default@2.1.1': {}
@@ -4221,9 +4221,9 @@ snapshots:
'@vicons/material@0.12.0': {}
'@vitejs/plugin-vue@5.0.5(vite@5.3.2(@types/node@20.14.9)(terser@5.31.1))(vue@3.4.31(typescript@5.4.5))':
'@vitejs/plugin-vue@5.0.5(vite@5.3.3(@types/node@20.14.10)(terser@5.31.1))(vue@3.4.31(typescript@5.4.5))':
dependencies:
vite: 5.3.2(@types/node@20.14.9)(terser@5.31.1)
vite: 5.3.3(@types/node@20.14.10)(terser@5.31.1)
vue: 3.4.31(typescript@5.4.5)
'@vue/compiler-core@3.4.31':
@@ -4248,7 +4248,7 @@ snapshots:
'@vue/shared': 3.4.31
estree-walker: 2.0.2
magic-string: 0.30.10
postcss: 8.4.38
postcss: 8.4.39
source-map-js: 1.2.0
'@vue/compiler-ssr@3.4.31':
@@ -4411,9 +4411,9 @@ snapshots:
acorn-walk@8.3.3:
dependencies:
acorn: 8.12.0
acorn: 8.12.1
acorn@8.12.0: {}
acorn@8.12.1: {}
ajv@8.16.0:
dependencies:
@@ -4520,10 +4520,10 @@ snapshots:
browserslist@4.23.1:
dependencies:
caniuse-lite: 1.0.30001638
electron-to-chromium: 1.4.814
caniuse-lite: 1.0.30001640
electron-to-chromium: 1.4.818
node-releases: 2.0.14
update-browserslist-db: 1.0.16(browserslist@4.23.1)
update-browserslist-db: 1.1.0(browserslist@4.23.1)
buffer-from@1.1.2: {}
@@ -4537,7 +4537,7 @@ snapshots:
get-intrinsic: 1.2.4
set-function-length: 1.2.2
caniuse-lite@1.0.30001638: {}
caniuse-lite@1.0.30001640: {}
capnp-ts@0.7.0:
dependencies:
@@ -4691,7 +4691,7 @@ snapshots:
dependencies:
jake: 10.9.1
electron-to-chromium@1.4.814: {}
electron-to-chromium@1.4.818: {}
entities@4.5.0: {}
@@ -5011,7 +5011,7 @@ snapshots:
dependencies:
function-bind: 1.1.2
highlight.js@11.9.0: {}
highlight.js@11.10.0: {}
hookable@5.5.3: {}
@@ -5138,7 +5138,7 @@ snapshots:
jest-worker@26.6.2:
dependencies:
'@types/node': 20.14.9
'@types/node': 20.14.10
merge-stream: 2.0.0
supports-color: 7.2.0
@@ -5180,7 +5180,7 @@ snapshots:
local-pkg@0.5.0:
dependencies:
mlly: 1.7.1
pkg-types: 1.1.1
pkg-types: 1.1.3
lodash-es@4.17.21: {}
@@ -5237,18 +5237,18 @@ snapshots:
mime@3.0.0: {}
miniflare@3.20240620.0:
miniflare@3.20240701.0:
dependencies:
'@cspotcode/source-map-support': 0.8.1
acorn: 8.12.0
acorn: 8.12.1
acorn-walk: 8.3.3
capnp-ts: 0.7.0
exit-hook: 2.2.1
glob-to-regexp: 0.4.1
stoppable: 1.1.0
undici: 5.28.4
workerd: 1.20240620.1
ws: 8.17.1
workerd: 1.20240701.0
ws: 8.18.0
youch: 3.3.3
zod: 3.23.8
transitivePeerDependencies:
@@ -5270,9 +5270,9 @@ snapshots:
mlly@1.7.1:
dependencies:
acorn: 8.12.0
acorn: 8.12.1
pathe: 1.1.2
pkg-types: 1.1.1
pkg-types: 1.1.3
ufo: 1.5.3
ms@2.1.2: {}
@@ -5292,7 +5292,7 @@ snapshots:
date-fns: 2.30.0
date-fns-tz: 2.0.1(date-fns@2.30.0)
evtd: 0.2.4
highlight.js: 11.9.0
highlight.js: 11.10.0
lodash: 4.17.21
lodash-es: 4.17.21
seemly: 0.3.8
@@ -5345,7 +5345,7 @@ snapshots:
picomatch@2.3.1: {}
pkg-types@1.1.1:
pkg-types@1.1.3:
dependencies:
confbox: 0.1.7
mlly: 1.7.1
@@ -5355,13 +5355,13 @@ snapshots:
postal-mime@2.2.5: {}
postcss@8.4.38:
postcss@8.4.39:
dependencies:
nanoid: 3.3.7
picocolors: 1.0.1
source-map-js: 1.2.0
preact@10.22.0: {}
preact@10.22.1: {}
pretty-bytes@5.6.0: {}
@@ -5665,7 +5665,7 @@ snapshots:
terser@5.31.1:
dependencies:
'@jridgewell/source-map': 0.3.6
acorn: 8.12.0
acorn: 8.12.1
commander: 2.20.3
source-map-support: 0.5.21
@@ -5750,11 +5750,11 @@ snapshots:
pathe: 1.1.2
ufo: 1.5.3
unhead@1.9.14:
unhead@1.9.15:
dependencies:
'@unhead/dom': 1.9.14
'@unhead/schema': 1.9.14
'@unhead/shared': 1.9.14
'@unhead/dom': 1.9.15
'@unhead/schema': 1.9.15
'@unhead/shared': 1.9.15
hookable: 5.5.3
unicode-canonical-property-names-ecmascript@2.0.0: {}
@@ -5771,7 +5771,7 @@ snapshots:
unimport@3.7.2(rollup@2.79.1):
dependencies:
'@rollup/pluginutils': 5.1.0(rollup@2.79.1)
acorn: 8.12.0
acorn: 8.12.1
escape-string-regexp: 5.0.0
estree-walker: 3.0.3
fast-glob: 3.3.2
@@ -5779,10 +5779,10 @@ snapshots:
magic-string: 0.30.10
mlly: 1.7.1
pathe: 1.1.2
pkg-types: 1.1.1
pkg-types: 1.1.3
scule: 1.3.0
strip-literal: 2.1.0
unplugin: 1.10.1
unplugin: 1.11.0
transitivePeerDependencies:
- rollup
@@ -5801,7 +5801,7 @@ snapshots:
magic-string: 0.30.10
minimatch: 9.0.5
unimport: 3.7.2(rollup@2.79.1)
unplugin: 1.10.1
unplugin: 1.11.0
optionalDependencies:
'@vueuse/core': 10.11.0(vue@3.4.31(typescript@5.4.5))
transitivePeerDependencies:
@@ -5818,7 +5818,7 @@ snapshots:
magic-string: 0.30.10
minimatch: 9.0.5
mlly: 1.7.1
unplugin: 1.10.1
unplugin: 1.11.0
vue: 3.4.31(typescript@5.4.5)
optionalDependencies:
'@babel/parser': 7.24.7
@@ -5826,16 +5826,16 @@ snapshots:
- rollup
- supports-color
unplugin@1.10.1:
unplugin@1.11.0:
dependencies:
acorn: 8.12.0
acorn: 8.12.1
chokidar: 3.6.0
webpack-sources: 3.2.3
webpack-virtual-modules: 0.6.2
upath@1.2.0: {}
update-browserslist-db@1.0.16(browserslist@4.23.1):
update-browserslist-db@1.1.0(browserslist@4.23.1):
dependencies:
browserslist: 4.23.1
escalade: 3.1.2
@@ -5854,38 +5854,38 @@ snapshots:
evtd: 0.2.4
vue: 3.4.31(typescript@5.4.5)
vite-plugin-pwa@0.19.8(vite@5.3.2(@types/node@20.14.9)(terser@5.31.1))(workbox-build@7.0.0)(workbox-window@7.1.0):
vite-plugin-pwa@0.19.8(vite@5.3.3(@types/node@20.14.10)(terser@5.31.1))(workbox-build@7.0.0)(workbox-window@7.1.0):
dependencies:
debug: 4.3.5
fast-glob: 3.3.2
pretty-bytes: 6.1.1
vite: 5.3.2(@types/node@20.14.9)(terser@5.31.1)
vite: 5.3.3(@types/node@20.14.10)(terser@5.31.1)
workbox-build: 7.0.0
workbox-window: 7.1.0
transitivePeerDependencies:
- supports-color
vite-plugin-top-level-await@1.4.1(rollup@2.79.1)(vite@5.3.2(@types/node@20.14.9)(terser@5.31.1)):
vite-plugin-top-level-await@1.4.1(rollup@2.79.1)(vite@5.3.3(@types/node@20.14.10)(terser@5.31.1)):
dependencies:
'@rollup/plugin-virtual': 3.0.2(rollup@2.79.1)
'@swc/core': 1.6.5
'@swc/core': 1.6.13
uuid: 9.0.1
vite: 5.3.2(@types/node@20.14.9)(terser@5.31.1)
vite: 5.3.3(@types/node@20.14.10)(terser@5.31.1)
transitivePeerDependencies:
- '@swc/helpers'
- rollup
vite-plugin-wasm@3.3.0(vite@5.3.2(@types/node@20.14.9)(terser@5.31.1)):
vite-plugin-wasm@3.3.0(vite@5.3.3(@types/node@20.14.10)(terser@5.31.1)):
dependencies:
vite: 5.3.2(@types/node@20.14.9)(terser@5.31.1)
vite: 5.3.3(@types/node@20.14.10)(terser@5.31.1)
vite@5.3.2(@types/node@20.14.9)(terser@5.31.1):
vite@5.3.3(@types/node@20.14.10)(terser@5.31.1):
dependencies:
esbuild: 0.21.5
postcss: 8.4.38
postcss: 8.4.39
rollup: 4.18.0
optionalDependencies:
'@types/node': 20.14.9
'@types/node': 20.14.10
fsevents: 2.3.3
terser: 5.31.1
@@ -6085,15 +6085,15 @@ snapshots:
'@types/trusted-types': 2.0.7
workbox-core: 7.1.0
workerd@1.20240620.1:
workerd@1.20240701.0:
optionalDependencies:
'@cloudflare/workerd-darwin-64': 1.20240620.1
'@cloudflare/workerd-darwin-arm64': 1.20240620.1
'@cloudflare/workerd-linux-64': 1.20240620.1
'@cloudflare/workerd-linux-arm64': 1.20240620.1
'@cloudflare/workerd-windows-64': 1.20240620.1
'@cloudflare/workerd-darwin-64': 1.20240701.0
'@cloudflare/workerd-darwin-arm64': 1.20240701.0
'@cloudflare/workerd-linux-64': 1.20240701.0
'@cloudflare/workerd-linux-arm64': 1.20240701.0
'@cloudflare/workerd-windows-64': 1.20240701.0
wrangler@3.62.0:
wrangler@3.63.1:
dependencies:
'@cloudflare/kv-asset-handler': 0.3.4
'@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19)
@@ -6102,7 +6102,7 @@ snapshots:
chokidar: 3.6.0
date-fns: 3.6.0
esbuild: 0.17.19
miniflare: 3.20240620.0
miniflare: 3.20240701.0
nanoid: 3.3.7
path-to-regexp: 6.2.2
resolve: 1.22.8
@@ -6120,7 +6120,7 @@ snapshots:
wrappy@1.0.2: {}
ws@8.17.1: {}
ws@8.18.0: {}
xxhash-wasm@1.0.2: {}

View File

@@ -54,7 +54,7 @@ onMounted(async () => {
<n-config-provider :locale="localeConfig" :theme="theme">
<n-global-style />
<n-spin description="loading..." :show="loading">
<n-message-provider>
<n-message-provider container-style="margin-top: 20px;">
<n-grid x-gap="12" :cols="12">
<n-gi v-if="showSideMargin" span="1"></n-gi>
<n-gi :span="!showSideMargin ? 12 : 10">

View File

@@ -4,13 +4,14 @@ import axios from 'axios'
const API_BASE = import.meta.env.VITE_API_BASE || "";
const {
loading, auth, jwt, settings, openSettings,
userOpenSettings, userSettings,
userOpenSettings, userSettings, announcement,
showAuth, adminAuth, showAdminAuth, userJwt
} = useGlobalState();
const instance = axios.create({
baseURL: API_BASE,
timeout: 30000
timeout: 30000,
validateStatus: (status) => status >= 200 && status <= 500
});
const apiFetch = async (path, options = {}) => {
@@ -27,14 +28,14 @@ const apiFetch = async (path, options = {}) => {
'Content-Type': 'application/json',
},
});
if (response.status === 401 && openSettings.value.auth) {
showAuth.value = true;
throw new Error("Unauthorized, you access password is wrong")
}
if (response.status === 401 && path.startsWith("/admin")) {
showAdminAuth.value = true;
throw new Error("Unauthorized, your admin password is wrong")
}
if (response.status === 401 && openSettings.value.auth) {
showAuth.value = true;
throw new Error("Unauthorized, you access password is wrong")
}
if (response.status >= 300) {
throw new Error(`${response.status} ${response.data}` || "error");
}
@@ -55,11 +56,13 @@ const getOpenSettings = async (message) => {
const res = await api.fetch("/open_api/settings");
const domainLabels = res["domainLabels"] || [];
Object.assign(openSettings.value, {
...res,
title: res["title"] || "",
prefix: res["prefix"] || "",
minAddressLen: res["minAddressLen"] || 1,
maxAddressLen: res["maxAddressLen"] || 30,
needAuth: res["needAuth"] || false,
defaultDomains: res["defaultDomains"] || [],
domains: res["domains"].map((domain, index) => {
return {
label: domainLabels.length > index ? domainLabels[index] : domain,
@@ -79,6 +82,14 @@ const getOpenSettings = async (message) => {
if (openSettings.value.needAuth) {
showAuth.value = true;
}
if (openSettings.value.announcement && openSettings.value.announcement != announcement.value) {
announcement.value = openSettings.value.announcement;
message.info(announcement.value, {
showIcon: false,
duration: 0,
closable: true
});
}
} catch (error) {
message.error(error.message || "error");
}

View File

@@ -1,5 +1,5 @@
<script setup>
import { ref, watch, defineModel, onMounted } from "vue";
import { ref, watch, onMounted } from "vue";
import { useI18n } from 'vue-i18n'
import { useGlobalState } from '../store'
const { openSettings, isDark } = useGlobalState()

View File

@@ -1,13 +1,15 @@
import { ref } from "vue";
import { createGlobalState, useStorage, useDark, useToggle } from '@vueuse/core'
import { createGlobalState, useStorage, useDark, useToggle, useLocalStorage } from '@vueuse/core'
export const useGlobalState = createGlobalState(
() => {
const isDark = useDark()
const toggleDark = useToggle(isDark)
const loading = ref(false);
const announcement = useLocalStorage('announcement', '');
const openSettings = ref({
title: '',
announcement: '',
prefix: '',
needAuth: false,
adminContact: '',
@@ -15,6 +17,8 @@ export const useGlobalState = createGlobalState(
enableUserDeleteEmail: false,
enableAutoReply: false,
enableIndexAbout: false,
/** @type {string[]} */
defaultDomains: [],
/** @type {Array<{label: string, value: string}>} */
domains: [],
copyright: 'Dream Hunter',
@@ -70,6 +74,8 @@ export const useGlobalState = createGlobalState(
user_email: '',
/** @type {number} */
user_id: 0,
/** @type {null | {domains: string[] | undefined | null, role: string, prefix: string | undefined | null}} */
user_role: null,
});
const telegramApp = ref(window.Telegram?.WebApp || {});
const isTelegram = ref(!!window.Telegram?.WebApp?.initData);
@@ -79,6 +85,7 @@ export const useGlobalState = createGlobalState(
loading,
settings,
sendMailModel,
announcement,
openSettings,
showAuth,
showAddressCredential,

View File

@@ -203,6 +203,24 @@ useHead({
]
});
const logoClickCount = ref(0);
const logoClick = async () => {
if (route.path.includes("admin")) {
logoClickCount.value = 0;
return;
}
if (logoClickCount.value >= 5) {
logoClickCount.value = 0;
message.info("Change to admin Page");
await router.push(getRouterPathWithLang('/admin', locale.value));
} else {
logoClickCount.value++;
}
if (logoClickCount.value > 0) {
message.info(`Click ${5 - logoClickCount.value + 1} times to enter the admin page`);
}
}
onMounted(async () => {
await api.getOpenSettings(message);
});
@@ -215,7 +233,9 @@ onMounted(async () => {
<h3>{{ openSettings.title || t('title') }}</h3>
</template>
<template #avatar>
<n-avatar style="margin-left: 10px;" src="/logo.png" />
<div @click="logoClick">
<n-avatar style="margin-left: 10px;" src="/logo.png" />
</div>
</template>
<template #extra>
<n-space>

View File

@@ -94,6 +94,7 @@ const deleteEmail = async () => {
const fetchData = async () => {
try {
addressQuery.value = addressQuery.value.trim()
const { results, count: addressCount } = await api.fetch(
`/admin/address`
+ `?limit=${pageSize.value}`
@@ -283,7 +284,8 @@ onMounted(async () => {
</template>
</n-modal>
<n-input-group>
<n-input v-model:value="addressQuery" clearable :placeholder="t('addressQueryTip')" />
<n-input v-model:value="addressQuery" clearable :placeholder="t('addressQueryTip')"
@keydown.enter="fetchData" />
<n-button @click="fetchData" type="primary" tertiary>
{{ t('query') }}
</n-button>

View File

@@ -29,12 +29,9 @@ const { t } = useI18n({
const mailBoxKey = ref("")
const mailKeyword = ref("")
watch([adminMailTabAddress, mailKeyword], () => {
const queryMail = () => {
adminMailTabAddress.value = adminMailTabAddress.value.trim();
mailKeyword.value = mailKeyword.value.trim();
});
const queryMail = () => {
mailBoxKey.value = Date.now();
}
@@ -63,8 +60,9 @@ onMounted(async () => {
<template>
<div style="margin-top: 10px;">
<n-input-group>
<n-input v-model:value="adminMailTabAddress" :placeholder="t('addressQueryTip')" />
<n-input v-model:value="mailKeyword" :placeholder="t('keywordQueryTip')" />
<n-input v-model:value="adminMailTabAddress" :placeholder="t('addressQueryTip')"
@keydown.enter="queryMail" />
<n-input v-model:value="mailKeyword" :placeholder="t('keywordQueryTip')" @keydown.enter="queryMail" />
<n-button @click="queryMail" type="primary" tertiary>
{{ t('query') }}
</n-button>

View File

@@ -22,10 +22,10 @@ const cleanupModel = ref({
const { t } = useI18n({
messages: {
en: {
tip: 'Please input the cleanup days',
mailBoxLabel: 'Clean up days for mailbox',
mailUnknowLabel: "Clean up days for unknow receiver",
sendBoxLabel: "Clean up days for sendbox",
tip: 'Please input the days',
mailBoxLabel: 'Cleanup the inbox before n days',
mailUnknowLabel: "Cleanup the unknow mail before n days",
sendBoxLabel: "Cleanup the sendbox before n days",
cleanupNow: "Cleanup now",
autoCleanup: "Auto cleanup",
cleanupSuccess: "Cleanup success",
@@ -33,10 +33,10 @@ const { t } = useI18n({
cronTip: "Enable cron cleanup, need to configure [crons] in worker, please refer to the document",
},
zh: {
tip: '请输入清理天数',
mailBoxLabel: '收件箱清理天数',
mailUnknowLabel: "无收件人邮件清理天数",
sendBoxLabel: "发件箱清理天数",
tip: '请输入天数',
mailBoxLabel: '清理 n 天前的收件箱',
mailUnknowLabel: "清理 n 天前的无收件人邮件",
sendBoxLabel: "清理 n 天前的发件箱",
autoCleanup: "自动清理",
cleanupSuccess: "清理成功",
cleanupNow: "立即清理",

View File

@@ -21,6 +21,7 @@ const { t } = useI18n({
});
const fetchData = async (limit, offset) => {
adminSendBoxTabAddress.value = adminSendBoxTabAddress.value.trim();
return await api.fetch(
`/admin/sendbox?limit=${limit}&offset=${offset}`
+ (adminSendBoxTabAddress.value ? `&address=${adminSendBoxTabAddress.value}` : '')
@@ -35,7 +36,7 @@ const deleteSenboxMail = async (curMailId) => {
<template>
<div>
<n-input-group>
<n-input v-model:value="adminSendBoxTabAddress" :placeholder="t('queryTip')" />
<n-input v-model:value="adminSendBoxTabAddress" :placeholder="t('queryTip')" @keydown.enter="fetchData" />
<n-button @click="fetchData" type="primary" tertiary>
{{ t('query') }}
</n-button>

View File

@@ -79,6 +79,7 @@ const updateData = async () => {
const fetchData = async () => {
try {
addressQuery.value = addressQuery.value.trim();
const { results, count: addressCount } = await api.fetch(
`/admin/address_sender`
+ `?limit=${pageSize.value}`
@@ -192,7 +193,7 @@ onMounted(async () => {
</template>
</n-modal>
<n-input-group>
<n-input v-model:value="addressQuery" />
<n-input v-model:value="addressQuery" @keydown.enter="fetchData" />
<n-button @click="fetchData" type="primary" tertiary>
{{ t('query') }}
</n-button>

View File

@@ -13,14 +13,18 @@ const message = useMessage()
const { t } = useI18n({
messages: {
en: {
userCount: 'Account Count',
activeUser: '7 days Active Mail Account',
userCount: 'User Count',
addressCount: 'Address Count',
activeAddressCount7days: '7 days Active Address Count',
activeAddressCount30days: '30 days Active Address Count',
mailCount: 'Mail Count',
sendMailCount: 'Send Mail Count'
},
zh: {
userCount: '地址总数',
activeUser: '周活跃邮箱地址',
userCount: '用户总数',
addressCount: '邮箱地址总数',
activeAddressCount7days: '7天活跃邮箱地址总数',
activeAddressCount30days: '30天活跃邮箱地址总数',
mailCount: '邮件总数',
sendMailCount: '发送邮件总数'
}
@@ -28,21 +32,27 @@ const { t } = useI18n({
});
const statistics = ref({
addressCount: 0,
userCount: 0,
mailCount: 0,
activeUserCount7days: 0,
activeAddressCount7days: 0,
activeAddressCount30days: 0,
sendMailCount: 0,
})
const fetchStatistics = async () => {
try {
const {
userCount, activeUserCount7days, mailCount, sendMailCount
userCount, mailCount, sendMailCount,
addressCount, activeAddressCount7days,
activeAddressCount30days,
} = await api.fetch(`/admin/statistics`);
statistics.value.mailCount = mailCount || 0;
statistics.value.userCount = userCount || 0;
statistics.value.activeUserCount7days = activeUserCount7days || 0;
statistics.value.sendMailCount = sendMailCount || 0;
statistics.value.userCount = userCount || 0;
statistics.value.addressCount = addressCount || 0;
statistics.value.activeAddressCount7days = activeAddressCount7days || 0;
statistics.value.activeAddressCount30days = activeAddressCount30days || 0;
} catch (error) {
console.log(error)
message.error(error.message || "error");
@@ -58,36 +68,63 @@ onMounted(async () => {
</script>
<template>
<n-card :bordered="false" embedded>
<n-row>
<n-col :span="6">
<n-statistic :label="t('userCount')" :value="statistics.userCount">
<template #prefix>
<n-icon :component="User" />
</template>
</n-statistic>
</n-col>
<n-col :span="6">
<n-statistic :label="t('activeUser')" :value="statistics.activeUserCount7days">
<template #prefix>
<n-icon :component="UserCheck" />
</template>
</n-statistic>
</n-col>
<n-col :span="6">
<n-statistic :label="t('mailCount')" :value="statistics.mailCount">
<template #prefix>
<n-icon :component="MailBulk" />
</template>
</n-statistic>
</n-col>
<n-col :span="6">
<n-statistic :label="t('sendMailCount')" :value="statistics.sendMailCount">
<template #prefix>
<n-icon :component="SendOutlined" />
</template>
</n-statistic>
</n-col>
</n-row>
</n-card>
<div>
<n-card :bordered="false" embedded>
<n-row>
<n-col :span="8">
<n-statistic :label="t('addressCount')" :value="statistics.addressCount">
<template #prefix>
<n-icon :component="User" />
</template>
</n-statistic>
</n-col>
<n-col :span="8">
<n-statistic :label="t('activeAddressCount7days')" :value="statistics.activeAddressCount7days">
<template #prefix>
<n-icon :component="UserCheck" />
</template>
</n-statistic>
</n-col>
<n-col :span="8">
<n-statistic :label="t('activeAddressCount30days')" :value="statistics.activeAddressCount30days">
<template #prefix>
<n-icon :component="UserCheck" />
</template>
</n-statistic>
</n-col>
</n-row>
</n-card>
<n-card :bordered="false" embedded>
<n-row>
<n-col :span="8">
<n-statistic :label="t('userCount')" :value="statistics.userCount">
<template #prefix>
<n-icon :component="User" />
</template>
</n-statistic>
</n-col>
<n-col :span="8">
<n-statistic :label="t('mailCount')" :value="statistics.mailCount">
<template #prefix>
<n-icon :component="MailBulk" />
</template>
</n-statistic>
</n-col>
<n-col :span="8">
<n-statistic :label="t('sendMailCount')" :value="statistics.sendMailCount">
<template #prefix>
<n-icon :component="SendOutlined" />
</template>
</n-statistic>
</n-col>
</n-row>
</n-card>
</div>
</template>
<style scoped>
.n-card {
margin-bottom: 20px;
}
</style>

View File

@@ -1,14 +1,14 @@
<script setup>
import { ref, h, onMounted, watch } from 'vue';
import { ref, h, onMounted, watch, computed } from 'vue';
import { useI18n } from 'vue-i18n'
import { NMenu, NButton, NBadge } from 'naive-ui';
import { NMenu, NButton, NBadge, NTag } from 'naive-ui';
import { MenuFilled } from '@vicons/material'
import { useGlobalState } from '../../store'
import { api } from '../../api'
import { hashPassword } from '../../utils';
const { loading } = useGlobalState()
const { loading, openSettings } = useGlobalState()
const message = useMessage()
const { t } = useI18n({
@@ -16,6 +16,7 @@ const { t } = useI18n({
en: {
success: 'Success',
user_email: 'User Email',
role: 'Role',
address_count: 'Address Count',
created_at: 'Created At',
actions: 'Actions',
@@ -29,10 +30,15 @@ const { t } = useI18n({
createUser: 'Create User',
email: 'Email',
password: 'Password',
changeRole: 'Change Role',
prefix: 'Prefix',
domains: 'Domains',
roleDonotExist: 'Current Role does not exist',
},
zh: {
success: '成功',
user_email: '用户邮箱',
role: '角色',
address_count: '地址数量',
created_at: '创建时间',
actions: '操作',
@@ -46,6 +52,10 @@ const { t } = useI18n({
createUser: '创建用户',
email: '邮箱',
password: '密码',
changeRole: '更改角色',
prefix: '前缀',
domains: '域名',
roleDonotExist: '当前角色不存在',
}
}
});
@@ -64,9 +74,31 @@ const user = ref({
email: "",
password: ""
})
const showChangeRole = ref(false)
const userRoles = ref([])
const curUserRole = ref('')
const userRolesOptions = computed(() => {
return userRoles.value.map(role => {
return {
label: role.role,
value: role.role
}
});
})
const fetchUserRoles = async () => {
try {
const results = await api.fetch(`/admin/user_roles`);
userRoles.value = results;
} catch (error) {
console.log(error)
message.error(error.message || "error");
}
}
const fetchData = async () => {
try {
userQuery.value = userQuery.value.trim()
const { results, count: userCount } = await api.fetch(
`/admin/users`
+ `?limit=${pageSize.value}`
@@ -138,6 +170,24 @@ const deleteUser = async () => {
}
}
const changeRole = async () => {
try {
await api.fetch(`/admin/user_roles`, {
method: "POST",
body: JSON.stringify({
user_id: curUserId.value,
role_text: curUserRole.value
})
});
message.success(t('success'));
showChangeRole.value = false;
await fetchData();
} catch (error) {
console.log(error)
message.error(error.message || "error");
}
}
const columns = [
{
title: "ID",
@@ -147,6 +197,19 @@ const columns = [
title: t('user_email'),
key: "user_email"
},
{
title: t('role'),
key: "role_text",
render(row) {
if (!row.role_text) return null;
return h(NTag, {
bordered: false,
type: "info"
}, {
default: () => row.role_text
})
}
},
{
title: t('address_count'),
key: "address_count",
@@ -176,6 +239,19 @@ const columns = [
icon: () => h(MenuFilled),
key: "action",
children: [
{
label: () => h(NButton,
{
text: true,
onClick: () => {
curUserId.value = row.id;
curUserRole.value = row.role_text;
showChangeRole.value = true;
}
},
{ default: () => t('changeRole') }
),
},
{
label: () => h(NButton,
{
@@ -212,12 +288,29 @@ const columns = [
}
]
const getRolePrefix = (role) => {
const res = userRoles.value.find(r => r.role === role)?.prefix;
if (res === undefined || res === null) return openSettings.value.prefix;
return res;
}
const getRoleDomains = (role) => {
const res = userRoles.value.find(r => r.role === role)?.domains;
if (res === undefined || res === null || res.length == 0) return openSettings.value.defaultDomains;
return res;
}
const roleDonotExist = computed(() => {
return curUserRole.value && !userRoles.value.some(r => r.role === curUserRole.value);
})
watch([page, pageSize], async () => {
await fetchData()
})
onMounted(async () => {
await fetchData()
await fetchUserRoles();
await fetchData();
})
</script>
@@ -256,8 +349,21 @@ onMounted(async () => {
</n-button>
</template>
</n-modal>
<n-modal v-model:show="showChangeRole" preset="dialog" :title="t('changeRole')">
<n-alert type="error" :bordered="false" v-if="roleDonotExist">
<span>{{ t('roleDonotExist') }}</span>
</n-alert>
<p>{{ t('prefix') + ": " + getRolePrefix(curUserRole) }}</p>
<p>{{ t('domains') + ": " + JSON.stringify(getRoleDomains(curUserRole)) }}</p>
<n-select clearable v-model:value="curUserRole" :options="userRolesOptions" />
<template #action>
<n-button :loading="loading" @click="changeRole" size="small" tertiary type="primary">
{{ t('changeRole') }}
</n-button>
</template>
</n-modal>
<n-input-group>
<n-input v-model:value="userQuery" />
<n-input v-model:value="userQuery" @keydown.enter="fetchData" />
<n-button @click="fetchData" type="primary" tertiary>
{{ t('query') }}
</n-button>

View File

@@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { NewLabelOutlined, EmailOutlined } from '@vicons/material'
@@ -72,7 +72,7 @@ const { locale, t } = useI18n({
login: 'Login',
pleaseGetNewEmail: 'Please login or click "Get New Email" button to get a new email address',
getNewEmail: 'Create New Email',
getNewEmailTip1: 'Please input the email you want to use. only allow ., a-z, A-Z and 0-9',
getNewEmailTip1: 'Please input the email you want to use. only allow a-z and 0-9',
getNewEmailTip2: 'Levaing it blank will generate a random email address.',
getNewEmailTip3: 'You can choose a domain from the dropdown list.',
credential: 'Email Address Credential',
@@ -87,7 +87,7 @@ const { locale, t } = useI18n({
login: '登录',
pleaseGetNewEmail: '请"登录"或点击 "注册新邮箱" 按钮来获取一个新的邮箱地址',
getNewEmail: '创建新邮箱',
getNewEmailTip1: '请输入你想要使用的邮箱地址, 只允许 ., a-z, A-Z, 0-9',
getNewEmailTip1: '请输入你想要使用的邮箱地址, 只允许 a-z, 0-9',
getNewEmailTip2: '留空将会生成一个随机的邮箱地址。',
getNewEmailTip3: '你可以从下拉列表中选择一个域名。',
credential: '邮箱地址凭据',
@@ -110,7 +110,7 @@ const generateName = async () => {
.split('@')[0]
.replace(/\s+/g, '.')
.replace(/\.{2,}/g, '.')
.replace(/[^a-zA-Z0-9.]/g, '')
.replace(/[^a-z0-9]/g, '')
.toLowerCase();
} catch (error) {
message.error(error.message || "error");
@@ -140,11 +140,39 @@ const newEmail = async () => {
}
};
const addressPrefix = computed(() => {
// if user has role, return role prefix
if (userSettings.value?.user_role) {
return userSettings.value.user_role.prefix || "";
}
// if user has no role, return default prefix
return openSettings.value.prefix;
});
const domainsOptions = computed(() => {
// if user has role, return role domains
if (userSettings.value.user_role) {
const allDomains = userSettings.value.user_role.domains;
if (!allDomains) return openSettings.value.domains;
return openSettings.value.domains.filter((domain) => {
return allDomains.includes(domain.value);
});
}
// if user has no role, return default domains
if (!openSettings.value.defaultDomains) {
return openSettings.value.domains;
}
// if user has no role and no default domains, return all domains
return openSettings.value.domains.filter((domain) => {
return openSettings.value.defaultDomains.includes(domain.value);
});
});
onMounted(async () => {
if (!openSettings.value.domains || openSettings.value.domains.length === 0) {
await api.getOpenSettings();
}
emailDomain.value = openSettings.value.domains ? openSettings.value.domains[0]?.value : "";
emailDomain.value = domainsOptions.value ? domainsOptions.value[0]?.value : "";
});
</script>
@@ -186,14 +214,14 @@ onMounted(async () => {
{{ t('generateName') }}
</n-button>
<n-input-group>
<n-input-group-label v-if="openSettings.prefix">
{{ openSettings.prefix }}
<n-input-group-label v-if="addressPrefix">
{{ addressPrefix }}
</n-input-group-label>
<n-input v-model:value="emailName" show-count :minlength="openSettings.minAddressLen"
:maxlength="openSettings.maxAddressLen" />
<n-input-group-label>@</n-input-group-label>
<n-select v-model:value="emailDomain" :consistent-menu-width="false"
:options="openSettings.domains" />
:options="domainsOptions" />
</n-input-group>
<Turnstile v-model:value="cfToken" />
<n-button type="primary" block secondary strong @click="newEmail" :loading="loading">

View File

@@ -13,6 +13,7 @@ class Settings(BaseSettings):
proxy_url: str = "http://localhost:8787"
port: int = 8025
imap_port: int = 11143
basic_password: str = ""
class Config:
env_file = ".env"

View File

@@ -121,6 +121,7 @@ class SimpleMailbox:
f"{settings.proxy_url}/api/mails?limit={limit}&offset={start - 1}",
headers={
"Authorization": f"Bearer {self.password}",
"x-custom-auth": f"{settings.basic_password}",
"Content-Type": "application/json"
}
)
@@ -147,6 +148,7 @@ class SimpleMailbox:
f"{settings.proxy_url}/api/sendbox?limit={limit}&offset={start - 1}",
headers={
"Authorization": f"Bearer {self.password}",
"x-custom-auth": f"{settings.basic_password}",
"Content-Type": "application/json"
}
)

View File

@@ -31,6 +31,16 @@ export default defineConfig({
logo: { src: '/logo.png', width: 24, height: 24 },
search: { provider: 'local' },
socialLinks: [
{
icon: 'discord',
link: 'https://discord.gg/dQEwTWhA6Q'
},
{
icon: {
svg: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 448 512"><path d="M446.7 98.6l-67.6 318.8c-5.1 22.5-18.4 28.1-37.3 17.5l-103-75.9l-49.7 47.8c-5.5 5.5-10.1 10.1-20.7 10.1l7.4-104.9l190.9-172.5c8.3-7.4-1.8-11.5-12.9-4.1L117.8 284L16.2 252.2c-22.1-6.9-22.5-22.1 4.6-32.7L418.2 66.4c18.4-6.9 34.5 4.1 28.5 32.2z" fill="currentColor"></path></svg>'
},
link: 'https://t.me/cloudflare_temp_email'
},
{
icon: 'github',
link: 'https://github.com/dreamhunter2333/cloudflare_temp_email'

View File

@@ -34,10 +34,10 @@ git clone https://github.com/dreamhunter2333/cloudflare_temp_email.git
```bash
# create a database, and copy the output to wrangler.toml in the next step
wrangler d1 create dev
wrangler d1 execute dev --file=db/schema.sql
wrangler d1 execute dev --file=db/schema.sql --remote
# schema update, if you have initialized the database before this date, you can execute this command to update
# wrangler d1 execute dev --file=db/2024-01-13-patch.sql
# wrangler d1 execute dev --file=db/2024-04-03-patch.sql
# wrangler d1 execute dev --file=db/2024-01-13-patch.sql --remote
# wrangler d1 execute dev --file=db/2024-04-03-patch.sql --remote
# create a namespace, and copy the output to wrangler.toml in the next step
wrangler kv:namespace create DEV
```
@@ -77,6 +77,7 @@ node_compat = true
# TITLE = "Custom Title" # The title of the site
PREFIX = "tmp" # The mailbox name prefix to be processed
# (min, max) length of the adderss, if not set, the default is (1, 30)
# ANNOUNCEMENT = "Custom Announcement"
# MIN_ADDRESS_LEN = 1
# MAX_ADDRESS_LEN = 30
# If you want your site to be private, uncomment below and change your password
@@ -85,9 +86,16 @@ PREFIX = "tmp" # The mailbox name prefix to be processed
# ADMIN_PASSWORDS = ["123", "456"]
# admin contact information. If not configured, it will not be displayed. Any string can be configured.
# ADMIN_CONTACT = "xx@xx.xxx"
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # your domain name
DEFAULT_DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # domain name for no role users
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # all your domain name
# For chinese domain name, you can use DOMAIN_LABELS to show chinese domain name
# DOMAIN_LABELS = ["中文.xxx", "xxx.xxx2"]
# USER_DEFAULT_ROLE = "vip" # default role for new users(only when enable mail verification)
# User roles configuration, if domains is empty will use default_domains, if prefix is null will use default prefix, if prefix is empty string will not use prefix
# USER_ROLES = [
# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "vip", prefix = "vip" },
# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "admin", prefix = "" },
# ]
JWT_SECRET = "xxx" # Key used to generate jwt
BLACK_LIST = "" # Blacklist, used to filter senders, comma separated
# Allow users to create email addresses

View File

@@ -8,7 +8,7 @@ hero:
actions:
- theme: brand
text: Try it now
link: https://mail.awsl.uk/
link: https://mail.awsl.uk/en
- theme: alt
text: command line deployment
link: /en/cli

View File

@@ -9,7 +9,7 @@ cd worker
cp wrangler.toml.template wrangler.toml
# 创建 D1 并执行 schema.sql
wrangler d1 create dev
wrangler d1 execute dev --file=../db/schema.sql
wrangler d1 execute dev --file=../db/schema.sql --remote
```
创建完成后,我们在 cloudflare 的控制台可以看到 D1 数据库
@@ -25,6 +25,6 @@ wrangler d1 execute dev --file=../db/schema.sql
```bash
cd worker
wrangler d1 execute dev --file=../db/2024-01-13-patch.sql
wrangler d1 execute dev --file=../db/2024-04-03-patch.sql
wrangler d1 execute dev --file=../db/2024-01-13-patch.sql --remote
wrangler d1 execute dev --file=../db/2024-04-03-patch.sql --remote
```

View File

@@ -45,6 +45,7 @@ node_compat = true
# TITLE = "Custom Title" # 自定义网站标题
PREFIX = "tmp" # 要处理的邮箱名称前缀,不需要后缀可配置为空字符串
# (min, max) adderss的长度如果不设置默认为(1, 30)
# ANNOUNCEMENT = "Custom Announcement" # 自定义公告
# MIN_ADDRESS_LEN = 1
# MAX_ADDRESS_LEN = 30
# 如果你想要你的网站私有,取消下面的注释,并修改密码
@@ -53,9 +54,18 @@ PREFIX = "tmp" # 要处理的邮箱名称前缀,不需要后缀可配置为空
# ADMIN_PASSWORDS = ["123", "456"]
# admin 联系方式,不配置则不显示,可配置任意字符串
# ADMIN_CONTACT = "xx@xx.xxx"
# DEFAULT_DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 默认用户可用的域名(未登录或未分配角色的用户)
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名, 支持多个域名
# 对于中文域名,可以使用 DOMAIN_LABELS 显示域名的中文展示名称
# DOMAIN_LABELS = ["中文.xxx", "xxx.xxx2"]
# 新用户默认角色, 仅在启用邮件验证时有效
# USER_DEFAULT_ROLE = "vip"
# 用户角色配置, 如果 domains 为空将使用 default_domains
# 如果 prefix 为 null 将使用默认前缀, 如果 prefix 为空字符串将不使用前缀
# USER_ROLES = [
# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "vip", prefix = "vip" },
# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "admin", prefix = "" },
# ]
JWT_SECRET = "xxx" # 用于生成 jwt 的密钥, jwt 用于给用户登录以及鉴权
BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔
# 是否允许用户创建邮件, 不配置则不允许

View File

@@ -4,9 +4,9 @@
"version": "0.2.6",
"type": "module",
"devDependencies": {
"@types/node": "^20.14.9",
"@types/node": "^20.14.10",
"vitepress": "^1.2.3",
"wrangler": "^3.62.0"
"wrangler": "^3.63.1"
},
"scripts": {
"dev": "vitepress dev docs",

View File

@@ -13,14 +13,14 @@ importers:
version: 3.10.1
devDependencies:
'@types/node':
specifier: ^20.14.9
version: 20.14.9
specifier: ^20.14.10
version: 20.14.10
vitepress:
specifier: ^1.2.3
version: 1.2.3(@algolia/client-search@4.24.0)(@types/node@20.14.9)(postcss@8.4.38)(search-insights@2.13.0)(typescript@5.4.5)
version: 1.2.3(@algolia/client-search@4.24.0)(@types/node@20.14.10)(postcss@8.4.39)(search-insights@2.13.0)(typescript@5.4.5)
wrangler:
specifier: ^3.62.0
version: 3.62.0
specifier: ^3.63.1
version: 3.63.1
packages:
@@ -110,32 +110,32 @@ packages:
resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==}
engines: {node: '>=16.13'}
'@cloudflare/workerd-darwin-64@1.20240620.1':
resolution: {integrity: sha512-YWeS2aE8jAzDefuus/3GmZcFGu3Ef94uCAoxsQuaEXNsiGM9NeAhPpKC1BJAlcv168U/Q1J+6hckcGtipf6ZcQ==}
'@cloudflare/workerd-darwin-64@1.20240701.0':
resolution: {integrity: sha512-XAZa4ZP+qyTn6JQQACCPH09hGZXP2lTnWKkmg5mPwT8EyRzCKLkczAf98vPP5bq7JZD/zORdFWRY0dOTap8zTQ==}
engines: {node: '>=16'}
cpu: [x64]
os: [darwin]
'@cloudflare/workerd-darwin-arm64@1.20240620.1':
resolution: {integrity: sha512-3rdND+EHpmCrwYX6hvxIBSBJ0f40tRNxond1Vfw7GiR1MJVi3gragiBx75UDFHCxfRw3J0GZ1qVlkRce2/Xbsg==}
'@cloudflare/workerd-darwin-arm64@1.20240701.0':
resolution: {integrity: sha512-w80ZVAgfH4UwTz7fXZtk7KmS2FzlXniuQm4ku4+cIgRTilBAuKqjpOjwUCbx5g13Gqcm9NuiHce+IDGtobRTIQ==}
engines: {node: '>=16'}
cpu: [arm64]
os: [darwin]
'@cloudflare/workerd-linux-64@1.20240620.1':
resolution: {integrity: sha512-tURcTrXGeSbYqeM5ISVcofY20StKbVIcdxjJvNYNZ+qmSV9Fvn+zr7rRE+q64pEloVZfhsEPAlUCnFso5VV4XQ==}
'@cloudflare/workerd-linux-64@1.20240701.0':
resolution: {integrity: sha512-UWLr/Anxwwe/25nGv451MNd2jhREmPt/ws17DJJqTLAx6JxwGWA15MeitAIzl0dbxRFAJa+0+R8ag2WR3F/D6g==}
engines: {node: '>=16'}
cpu: [x64]
os: [linux]
'@cloudflare/workerd-linux-arm64@1.20240620.1':
resolution: {integrity: sha512-TThvkwNxaZFKhHZnNjOGqIYCOk05DDWgO+wYMuXg15ymN/KZPnCicRAkuyqiM+R1Fgc4kwe/pehjP8pbmcf6sg==}
'@cloudflare/workerd-linux-arm64@1.20240701.0':
resolution: {integrity: sha512-3kCnF9kYgov1ggpuWbgpXt4stPOIYtVmPCa7MO2xhhA0TWP6JDUHRUOsnmIgKrvDjXuXqlK16cdg3v+EWsaPJg==}
engines: {node: '>=16'}
cpu: [arm64]
os: [linux]
'@cloudflare/workerd-windows-64@1.20240620.1':
resolution: {integrity: sha512-Y/BA9Yj0r7Al1HK3nDHcfISgFllw6NR3XMMPChev57vrVT9C9D4erBL3sUBfofHU+2U9L+ShLsl6obBpe3vvUw==}
'@cloudflare/workerd-windows-64@1.20240701.0':
resolution: {integrity: sha512-6IPGITRAeS67j3BH1rN4iwYWDt47SqJG7KlZJ5bB4UaNAia4mvMBSy/p2p4vA89bbXoDRjMtEvRu7Robu6O7hQ==}
engines: {node: '>=16'}
cpu: [x64]
os: [win32]
@@ -541,11 +541,11 @@ packages:
cpu: [x64]
os: [win32]
'@shikijs/core@1.10.0':
resolution: {integrity: sha512-BZcr6FCmPfP6TXaekvujZcnkFmJHZ/Yglu97r/9VjzVndQA56/F4WjUKtJRQUnK59Wi7p/UTAOekMfCJv7jnYg==}
'@shikijs/core@1.10.1':
resolution: {integrity: sha512-qdiJS5a/QGCff7VUFIqd0hDdWly9rDp8lhVmXVrS11aazX8LOTRLHAXkkEeONNsS43EcCd7gax9LLoOz4vlFQA==}
'@shikijs/transformers@1.10.0':
resolution: {integrity: sha512-5Eu/kuJu7/CzAjFlTJkyyPoLTLSVQZ31Ps81cjIeR/3PDJ2RUuX1/R8d0qFziBKToym1LXbNiXoJQq0mg5+Cwg==}
'@shikijs/transformers@1.10.1':
resolution: {integrity: sha512-0gLtcFyi6R6zcUkFajUEp1Qiv7lHBSFgOz4tQvS8nFsYCQSLI1/9pM+Me8jEIPXv7XLKAoUjw6InL+Sv+BHw/A==}
'@types/estree@1.0.5':
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
@@ -562,8 +562,8 @@ packages:
'@types/node-forge@1.3.11':
resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==}
'@types/node@20.14.9':
resolution: {integrity: sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==}
'@types/node@20.14.10':
resolution: {integrity: sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==}
'@types/web-bluetooth@0.0.20':
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
@@ -587,14 +587,14 @@ packages:
'@vue/compiler-ssr@3.4.31':
resolution: {integrity: sha512-RtefmITAje3fJ8FSg1gwgDhdKhZVntIVbwupdyZDSifZTRMiWxWehAOTCc8/KZDnBOcYQ4/9VWxsTbd3wT0hAA==}
'@vue/devtools-api@7.3.4':
resolution: {integrity: sha512-E5dJlLW+NgGb+WS33y99ioOJL2OXpVhje6VwXGJ/q5fNizJDpe67Ml0GBSrlYOKNSjZs2mwcZd7B3e12th3Q0g==}
'@vue/devtools-api@7.3.5':
resolution: {integrity: sha512-BSdBBu5hOIv+gBJC9jzYMh5bC27FQwjWLSb8fVAniqlL9gvsqvK27xTgczMf+hgctlszMYQnRm3bpY/j8vhPqw==}
'@vue/devtools-kit@7.3.4':
resolution: {integrity: sha512-DalQZWaFLRyA4qfKT0WT7e+q2AwvYoTwd0pWqswHqcpviXw+oU6FlSJHMrEACB3lBHjN1KBS9Kh527sWIe1vcg==}
'@vue/devtools-kit@7.3.5':
resolution: {integrity: sha512-wwfi10gJ1HMtjzcd8aIOnzBHlIRqsYDgcDyrKvkeyc0Gbcoe7UrkXRVHZUOtcxxoplHA0PwpT6wFg0uUCmi8Ww==}
'@vue/devtools-shared@7.3.4':
resolution: {integrity: sha512-5S5cHh7oWLZdboujnLteR3rT8UGfKHfA34aGLyFRB/B5TqBxmeLW1Rq32xW6TCDEy4isoYsYHGwJVp6DQcpiDA==}
'@vue/devtools-shared@7.3.5':
resolution: {integrity: sha512-Rqii3VazmWTi67a86rYopi61n5Ved05EybJCwyrfoO9Ok3MaS/4yRFl706ouoISMlyrASJFEzM0/AiDA6w4f9A==}
'@vue/reactivity@3.4.31':
resolution: {integrity: sha512-VGkTani8SOoVkZNds1PfJ/T1SlAIOf8E58PGAhIOUDYPC4GAmFA2u/E14TDAFcf3vVDKunc4QqCe/SHr8xC65Q==}
@@ -667,8 +667,8 @@ packages:
resolution: {integrity: sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==}
engines: {node: '>=0.4.0'}
acorn@8.12.0:
resolution: {integrity: sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==}
acorn@8.12.1:
resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==}
engines: {node: '>=0.4.0'}
hasBin: true
@@ -852,8 +852,8 @@ packages:
engines: {node: '>=10.0.0'}
hasBin: true
miniflare@3.20240620.0:
resolution: {integrity: sha512-NBMzqUE2mMlh/hIdt6U5MP+aFhEjKDq3l8CAajXAQa1WkndJdciWvzB2mfLETwoVFhMl/lphaVzyEN2AgwJpbQ==}
miniflare@3.20240701.0:
resolution: {integrity: sha512-m9+I+7JNyqDGftCMKp9cK9pCZkK72hAL2mM9IWwhct+ZmucLBA8Uu6+rHQqA5iod86cpwOkrB2PrPA3wx9YNgw==}
engines: {node: '>=16.13'}
hasBin: true
@@ -908,12 +908,12 @@ packages:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
postcss@8.4.38:
resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==}
postcss@8.4.39:
resolution: {integrity: sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==}
engines: {node: ^10 || ^12 || >=14}
preact@10.22.0:
resolution: {integrity: sha512-RRurnSjJPj4rp5K6XoP45Ui33ncb7e4H7WiOHVpjbkvqvA3U+N8Z6Qbo0AE6leGYBV66n8EhEaFixvIu3SkxFw==}
preact@10.22.1:
resolution: {integrity: sha512-jRYbDDgMpIb5LHq3hkI0bbl+l/TQ9UnkdQ0ww+lp+4MMOdqaUYdFc5qeyP+IV8FAd/2Em7drVPeKdQxsiWCf/A==}
printable-characters@1.0.42:
resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==}
@@ -967,8 +967,8 @@ packages:
setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
shiki@1.10.0:
resolution: {integrity: sha512-YD2sXQ+TMD/F9BimV9Jn0wj35pqOvywvOG/3PB6hGHyGKlM7TJ9tyJ02jOb2kF8F0HfJwKNYrh3sW7jEcuRlXA==}
shiki@1.10.1:
resolution: {integrity: sha512-uafV7WCgN4YYrccH6yxpnps6k38sSTlFRrwc4jycWmhWxJIm9dPrk+XkY1hZ2t0I7jmacMNb15Lf2fspa/Y3lg==}
source-map-js@1.2.0:
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
@@ -1039,8 +1039,8 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
vite@5.3.2:
resolution: {integrity: sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==}
vite@5.3.3:
resolution: {integrity: sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
@@ -1098,13 +1098,13 @@ packages:
typescript:
optional: true
workerd@1.20240620.1:
resolution: {integrity: sha512-Qoq+RrFNk4pvEO+kpJVn8uJ5TRE9YJx5jX5pC5LjdKlw1XeD8EdXt5k0TbByvWunZ4qgYIcF9lnVxhcDFo203g==}
workerd@1.20240701.0:
resolution: {integrity: sha512-qSgNVqauqzNCij9MaJLF2c2ko3AnFioVSIxMSryGbRK+LvtGr9BKBt6JOxCb24DoJASoJDx3pe3DJHBVydUiBg==}
engines: {node: '>=16'}
hasBin: true
wrangler@3.62.0:
resolution: {integrity: sha512-TM1Bd8+GzxFw/JzwsC3i/Oss4LTWvIEWXXo1vZhx+7PHcsxdbnQGBBwPurHNJDSu2Pw22+2pCZiUGKexmgJksw==}
wrangler@3.63.1:
resolution: {integrity: sha512-fxMPNEyDc9pZNtQOuYqRikzv6lL5eP4S1zv7L/kw24uu1cCEmJ39j8bfJGzrAEqKDNsiFXVjEka0RjlpgEVWPg==}
engines: {node: '>=16.17.0'}
hasBin: true
peerDependencies:
@@ -1113,8 +1113,8 @@ packages:
'@cloudflare/workers-types':
optional: true
ws@8.17.1:
resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==}
ws@8.18.0:
resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
@@ -1258,19 +1258,19 @@ snapshots:
dependencies:
mime: 3.0.0
'@cloudflare/workerd-darwin-64@1.20240620.1':
'@cloudflare/workerd-darwin-64@1.20240701.0':
optional: true
'@cloudflare/workerd-darwin-arm64@1.20240620.1':
'@cloudflare/workerd-darwin-arm64@1.20240701.0':
optional: true
'@cloudflare/workerd-linux-64@1.20240620.1':
'@cloudflare/workerd-linux-64@1.20240701.0':
optional: true
'@cloudflare/workerd-linux-arm64@1.20240620.1':
'@cloudflare/workerd-linux-arm64@1.20240701.0':
optional: true
'@cloudflare/workerd-windows-64@1.20240620.1':
'@cloudflare/workerd-windows-64@1.20240701.0':
optional: true
'@cspotcode/source-map-support@0.8.1':
@@ -1282,7 +1282,7 @@ snapshots:
'@docsearch/js@3.6.0(@algolia/client-search@4.24.0)(search-insights@2.13.0)':
dependencies:
'@docsearch/react': 3.6.0(@algolia/client-search@4.24.0)(search-insights@2.13.0)
preact: 10.22.0
preact: 10.22.1
transitivePeerDependencies:
- '@algolia/client-search'
- '@types/react'
@@ -1505,11 +1505,11 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.18.0':
optional: true
'@shikijs/core@1.10.0': {}
'@shikijs/core@1.10.1': {}
'@shikijs/transformers@1.10.0':
'@shikijs/transformers@1.10.1':
dependencies:
shiki: 1.10.0
shiki: 1.10.1
'@types/estree@1.0.5': {}
@@ -1524,17 +1524,17 @@ snapshots:
'@types/node-forge@1.3.11':
dependencies:
'@types/node': 20.14.9
'@types/node': 20.14.10
'@types/node@20.14.9':
'@types/node@20.14.10':
dependencies:
undici-types: 5.26.5
'@types/web-bluetooth@0.0.20': {}
'@vitejs/plugin-vue@5.0.5(vite@5.3.2(@types/node@20.14.9))(vue@3.4.31(typescript@5.4.5))':
'@vitejs/plugin-vue@5.0.5(vite@5.3.3(@types/node@20.14.10))(vue@3.4.31(typescript@5.4.5))':
dependencies:
vite: 5.3.2(@types/node@20.14.9)
vite: 5.3.3(@types/node@20.14.10)
vue: 3.4.31(typescript@5.4.5)
'@vue/compiler-core@3.4.31':
@@ -1559,7 +1559,7 @@ snapshots:
'@vue/shared': 3.4.31
estree-walker: 2.0.2
magic-string: 0.30.10
postcss: 8.4.38
postcss: 8.4.39
source-map-js: 1.2.0
'@vue/compiler-ssr@3.4.31':
@@ -1567,13 +1567,13 @@ snapshots:
'@vue/compiler-dom': 3.4.31
'@vue/shared': 3.4.31
'@vue/devtools-api@7.3.4':
'@vue/devtools-api@7.3.5':
dependencies:
'@vue/devtools-kit': 7.3.4
'@vue/devtools-kit': 7.3.5
'@vue/devtools-kit@7.3.4':
'@vue/devtools-kit@7.3.5':
dependencies:
'@vue/devtools-shared': 7.3.4
'@vue/devtools-shared': 7.3.5
birpc: 0.2.17
hookable: 5.5.3
mitt: 3.0.1
@@ -1581,7 +1581,7 @@ snapshots:
speakingurl: 14.0.1
superjson: 2.2.1
'@vue/devtools-shared@7.3.4':
'@vue/devtools-shared@7.3.5':
dependencies:
rfdc: 1.4.1
@@ -1641,9 +1641,9 @@ snapshots:
acorn-walk@8.3.3:
dependencies:
acorn: 8.12.0
acorn: 8.12.1
acorn@8.12.0: {}
acorn@8.12.1: {}
algoliasearch@4.24.0:
dependencies:
@@ -1861,18 +1861,18 @@ snapshots:
mime@3.0.0: {}
miniflare@3.20240620.0:
miniflare@3.20240701.0:
dependencies:
'@cspotcode/source-map-support': 0.8.1
acorn: 8.12.0
acorn: 8.12.1
acorn-walk: 8.3.3
capnp-ts: 0.7.0
exit-hook: 2.2.1
glob-to-regexp: 0.4.1
stoppable: 1.1.0
undici: 5.28.4
workerd: 1.20240620.1
ws: 8.17.1
workerd: 1.20240701.0
ws: 8.18.0
youch: 3.3.3
zod: 3.23.8
transitivePeerDependencies:
@@ -1910,13 +1910,13 @@ snapshots:
picomatch@2.3.1: {}
postcss@8.4.38:
postcss@8.4.39:
dependencies:
nanoid: 3.3.7
picocolors: 1.0.1
source-map-js: 1.2.0
preact@10.22.0: {}
preact@10.22.1: {}
printable-characters@1.0.42: {}
@@ -1993,9 +1993,9 @@ snapshots:
setimmediate@1.0.5: {}
shiki@1.10.0:
shiki@1.10.1:
dependencies:
'@shikijs/core': 1.10.0
'@shikijs/core': 1.10.1
source-map-js@1.2.0: {}
@@ -2054,35 +2054,35 @@ snapshots:
util-deprecate@1.0.2: {}
vite@5.3.2(@types/node@20.14.9):
vite@5.3.3(@types/node@20.14.10):
dependencies:
esbuild: 0.21.5
postcss: 8.4.38
postcss: 8.4.39
rollup: 4.18.0
optionalDependencies:
'@types/node': 20.14.9
'@types/node': 20.14.10
fsevents: 2.3.3
vitepress@1.2.3(@algolia/client-search@4.24.0)(@types/node@20.14.9)(postcss@8.4.38)(search-insights@2.13.0)(typescript@5.4.5):
vitepress@1.2.3(@algolia/client-search@4.24.0)(@types/node@20.14.10)(postcss@8.4.39)(search-insights@2.13.0)(typescript@5.4.5):
dependencies:
'@docsearch/css': 3.6.0
'@docsearch/js': 3.6.0(@algolia/client-search@4.24.0)(search-insights@2.13.0)
'@shikijs/core': 1.10.0
'@shikijs/transformers': 1.10.0
'@shikijs/core': 1.10.1
'@shikijs/transformers': 1.10.1
'@types/markdown-it': 14.1.1
'@vitejs/plugin-vue': 5.0.5(vite@5.3.2(@types/node@20.14.9))(vue@3.4.31(typescript@5.4.5))
'@vue/devtools-api': 7.3.4
'@vitejs/plugin-vue': 5.0.5(vite@5.3.3(@types/node@20.14.10))(vue@3.4.31(typescript@5.4.5))
'@vue/devtools-api': 7.3.5
'@vue/shared': 3.4.31
'@vueuse/core': 10.11.0(vue@3.4.31(typescript@5.4.5))
'@vueuse/integrations': 10.11.0(focus-trap@7.5.4)(vue@3.4.31(typescript@5.4.5))
focus-trap: 7.5.4
mark.js: 8.11.1
minisearch: 6.3.0
shiki: 1.10.0
vite: 5.3.2(@types/node@20.14.9)
shiki: 1.10.1
vite: 5.3.3(@types/node@20.14.10)
vue: 3.4.31(typescript@5.4.5)
optionalDependencies:
postcss: 8.4.38
postcss: 8.4.39
transitivePeerDependencies:
- '@algolia/client-search'
- '@types/node'
@@ -2124,15 +2124,15 @@ snapshots:
optionalDependencies:
typescript: 5.4.5
workerd@1.20240620.1:
workerd@1.20240701.0:
optionalDependencies:
'@cloudflare/workerd-darwin-64': 1.20240620.1
'@cloudflare/workerd-darwin-arm64': 1.20240620.1
'@cloudflare/workerd-linux-64': 1.20240620.1
'@cloudflare/workerd-linux-arm64': 1.20240620.1
'@cloudflare/workerd-windows-64': 1.20240620.1
'@cloudflare/workerd-darwin-64': 1.20240701.0
'@cloudflare/workerd-darwin-arm64': 1.20240701.0
'@cloudflare/workerd-linux-64': 1.20240701.0
'@cloudflare/workerd-linux-arm64': 1.20240701.0
'@cloudflare/workerd-windows-64': 1.20240701.0
wrangler@3.62.0:
wrangler@3.63.1:
dependencies:
'@cloudflare/kv-asset-handler': 0.3.4
'@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19)
@@ -2141,7 +2141,7 @@ snapshots:
chokidar: 3.6.0
date-fns: 3.6.0
esbuild: 0.17.19
miniflare: 3.20240620.0
miniflare: 3.20240701.0
nanoid: 3.3.7
path-to-regexp: 6.2.2
resolve: 1.22.8
@@ -2157,7 +2157,7 @@ snapshots:
- supports-color
- utf-8-validate
ws@8.17.1: {}
ws@8.18.0: {}
xxhash-wasm@1.0.2: {}

View File

@@ -14,14 +14,14 @@
"@cloudflare/workers-types": "^4.20240620.0",
"@eslint/js": "8.56.0",
"eslint": "8.56.0",
"globals": "^15.6.0",
"typescript-eslint": "^7.14.1",
"wrangler": "^3.62.0"
"globals": "^15.8.0",
"typescript-eslint": "^7.15.0",
"wrangler": "^3.63.1"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.600.0",
"@aws-sdk/s3-request-presigner": "^3.600.0",
"hono": "^4.4.9",
"@aws-sdk/client-s3": "^3.609.0",
"@aws-sdk/s3-request-presigner": "^3.609.0",
"hono": "^4.4.12",
"mimetext": "^3.0.24",
"postal-mime": "^2.2.5",
"resend": "^3.4.0",

751
worker/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
import { Context } from 'hono';
import { CONSTANTS } from '../constants';
import { getJsonSetting, saveSetting, checkUserPassword, getDomains } from '../utils';
import { getJsonSetting, saveSetting, checkUserPassword, getDomains, getUserRoles } from '../utils';
import { UserSettings, GeoData, UserInfo } from "../models";
import { handleListQuery } from '../common'
import { HonoCustomType } from '../types';
@@ -38,18 +38,22 @@ export default {
const { limit, offset, query } = c.req.query();
if (query) {
return await handleListQuery(c,
`SELECT u.id, u.user_email, u.created_at, u.updated_at,`
`SELECT u.id as id, u.user_email, u.created_at, u.updated_at,`
+ ` ur.role_text as role_text,`
+ ` (SELECT COUNT(*) FROM users_address WHERE user_id = u.id) AS address_count`
+ ` FROM users u`
+ ` LEFT JOIN user_roles ur ON u.id = ur.user_id`
+ ` where u.user_email like ?`,
`SELECT count(*) as count FROM users where user_email like ?`,
[`%${query}%`], limit, offset
);
}
return await handleListQuery(c,
`SELECT u.id, u.user_email, u.created_at, u.updated_at,`
`SELECT u.id as id, u.user_email, u.created_at, u.updated_at,`
+ ` ur.role_text as role_text,`
+ ` (SELECT COUNT(*) FROM users_address WHERE user_id = u.id) AS address_count`
+ ` FROM users u`,
+ ` FROM users u`
+ ` LEFT JOIN user_roles ur ON u.id = ur.user_id`,
`SELECT count(*) as count FROM users`,
[], limit, offset
);
@@ -114,4 +118,30 @@ export default {
}
return c.json({ success: true });
},
updateUserRoles: async (c: Context<HonoCustomType>) => {
const { user_id, role_text } = await c.req.json();
if (!user_id) return c.text("Invalid user_id", 400);
if (!role_text) {
const { success } = await c.env.DB.prepare(
`DELETE FROM user_roles WHERE user_id = ?`
).bind(user_id).run();
if (!success) {
return c.text("Failed to update user roles", 500)
}
return c.json({ success: true })
}
const user_roles = getUserRoles(c);
if (!user_roles.find((r) => r.role === role_text)) {
return c.text("Invalid role_text", 400)
}
const { success } = await c.env.DB.prepare(
`INSERT INTO user_roles (user_id, role_text)`
+ ` VALUES (?, ?)`
+ ` ON CONFLICT(user_id) DO UPDATE SET role_text = ?, updated_at = datetime('now')`
).bind(user_id, role_text, role_text).run();
if (!success) {
return c.text("Failed to update user roles", 500)
}
return c.json({ success: true })
}
}

View File

@@ -2,7 +2,7 @@ import { Hono } from 'hono'
import { Jwt } from 'hono/utils/jwt'
import { HonoCustomType } from '../types'
import { sendAdminInternalMail, getJsonSetting, saveSetting } from '../utils'
import { sendAdminInternalMail, getJsonSetting, saveSetting, getUserRoles } from '../utils'
import { newAddress, handleListQuery } from '../common'
import { CONSTANTS } from '../constants'
import cleanup_api from './cleanup_api'
@@ -40,7 +40,7 @@ api.post('/admin/new_address', async (c) => {
return c.text("Please provide a name", 400)
}
try {
const res = await newAddress(c, name, domain, enablePrefix, false);
const res = await newAddress(c, name, domain, enablePrefix, false, null, false);
return c.json(res);
} catch (e) {
return c.text(`Failed create address: ${(e as Error).message}`, 400)
@@ -219,16 +219,24 @@ api.get('/admin/statistics', async (c) => {
const { count: addressCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM address`
).first<{ count: number }>() || {};
const { count: activeUserCount7days } = await c.env.DB.prepare(
const { count: activeAddressCount7days } = await c.env.DB.prepare(
`SELECT count(*) as count FROM address where updated_at > datetime('now', '-7 day')`
).first<{ count: number }>() || {};
const { count: activeAddressCount30days } = await c.env.DB.prepare(
`SELECT count(*) as count FROM address where updated_at > datetime('now', '-30 day')`
).first<{ count: number }>() || {};
const { count: sendMailCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM sendbox`
).first<{ count: number }>() || {};
const { count: userCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM users`
).first<{ count: number }>() || {};
return c.json({
mailCount: mailCount,
userCount: addressCount,
activeUserCount7days: activeUserCount7days,
addressCount: addressCount,
activeAddressCount7days: activeAddressCount7days,
activeAddressCount30days: activeAddressCount30days,
userCount: userCount,
sendMailCount: sendMailCount
})
});
@@ -284,5 +292,7 @@ api.get('/admin/users', admin_user_api.getUsers)
api.delete('/admin/users/:user_id', admin_user_api.deleteUser)
api.post('/admin/users', admin_user_api.createUser)
api.post('/admin/users/:user_id/reset_password', admin_user_api.resetPassword)
api.get('/admin/user_roles', async (c) => c.json(getUserRoles(c)))
api.post('/admin/user_roles', admin_user_api.updateUserRoles)
api.get("/admin/webhook/settings", webhook_settings.getWebhookSettings);
api.post("/admin/webhook/settings", webhook_settings.saveWebhookSettings);

View File

@@ -1,6 +1,6 @@
import { Hono } from 'hono'
import { getDomains, getPasswords, getBooleanValue, getIntValue, getStringArray } from './utils';
import { getDomains, getPasswords, getBooleanValue, getIntValue, getStringArray, getDefaultDomains, getStringValue } from './utils';
import { CONSTANTS } from './constants';
import { HonoCustomType } from './types';
import { isS3Enabled } from './mails_api/s3_attachment';
@@ -17,9 +17,11 @@ api.get('/open_api/settings', async (c) => {
}
return c.json({
"title": c.env.TITLE,
"announcement": getStringValue(c.env.ANNOUNCEMENT),
"prefix": c.env.PREFIX,
"minAddressLen": getIntValue(c.env.MIN_ADDRESS_LEN, 1),
"maxAddressLen": getIntValue(c.env.MAX_ADDRESS_LEN, 30),
"defaultDomains": getDefaultDomains(c),
"domains": getDomains(c),
"domainLabels": getStringArray(c.env.DOMAIN_LABELS),
"needAuth": needAuth,

View File

@@ -1,18 +1,20 @@
import { Context } from 'hono';
import { Jwt } from 'hono/utils/jwt'
import { getBooleanValue, getDomains, getStringValue, getIntValue } from './utils';
import { HonoCustomType } from './types';
import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains } from './utils';
import { HonoCustomType, UserRole } from './types';
import { unbindTelegramByAddress } from './telegram_api/common';
export const newAddress = async (
c: Context<HonoCustomType>,
name: string, domain: string | undefined | null,
enablePrefix: boolean,
checkLengthByConfig: boolean = true
checkLengthByConfig: boolean = true,
addressPrefix: string | undefined | null = null,
checkAllowDomains: boolean = true
): Promise<{ address: string, jwt: string }> => {
// remove special characters
name = name.replace(/[^a-zA-Z0-9.]/g, '')
name = name.replace(/[^a-z0-9]/g, '')
// name min length min 1
const minAddressLength = Math.max(
checkLengthByConfig ? getIntValue(c.env.MIN_ADDRESS_LEN, 1) : 1,
@@ -30,14 +32,21 @@ export const newAddress = async (
if (name.length > maxAddressLength) {
throw new Error(`Name too long (max ${maxAddressLength})`);
}
// create address
if (enablePrefix) {
// create address with prefix
if (typeof addressPrefix === "string") {
name = addressPrefix + name;
} else if (enablePrefix) {
name = getStringValue(c.env.PREFIX) + name;
}
// check domain, generate random domain
const domains = getDomains(c);
if (!domain || !domains.includes(domain)) {
domain = domains[Math.floor(Math.random() * domains.length)];
// check domain
const allowDomains = checkAllowDomains ? await getAllowDomains(c) : getDomains(c);
// if domain is not set, use the first domain
if (!domain && allowDomains.length > 0) {
domain = allowDomains[0];
}
// check domain is valid
if (!domain || !allowDomains.includes(domain)) {
throw new Error("Invalid domain")
}
// create address
name = name + "@" + domain;
@@ -74,7 +83,7 @@ export const cleanup = async (
cleanType: string | undefined | null,
cleanDays: number | undefined | null
): Promise<boolean> => {
if (!cleanType || !cleanDays || cleanDays < 0 || cleanDays > 30) {
if (!cleanType || typeof cleanDays !== 'number' || cleanDays < 0 || cleanDays > 30) {
throw new Error("Invalid cleanType or cleanDays")
}
console.log(`Cleanup ${cleanType} before ${cleanDays} days`);
@@ -217,3 +226,34 @@ export const commonParseMail = async (raw_mail: string | undefined | null): Prom
}
return undefined;
}
export const commonGetUserRole = async (
c: Context<HonoCustomType>, user_id: number
): Promise<UserRole | undefined | null> => {
const user_roles = getUserRoles(c);
const role_text = await c.env.DB.prepare(
`SELECT role_text FROM user_roles where user_id = ?`
).bind(user_id).first<string | undefined | null>("role_text");
return role_text ? user_roles.find((r) => r.role === role_text) : null;
}
export const getAddressPrefix = async (c: Context<HonoCustomType>): Promise<string | undefined> => {
const user = c.get("userPayload");
if (!user) {
return c.env.PREFIX;
}
const user_role = await commonGetUserRole(c, user.user_id);
if (typeof user_role?.prefix === "string") {
return user_role.prefix;
}
return c.env.PREFIX;
}
export const getAllowDomains = async (c: Context<HonoCustomType>): Promise<string[]> => {
const user = c.get("userPayload");
if (!user) {
return getDefaultDomains(c);
}
const user_role = await commonGetUserRole(c, user.user_id);
return user_role?.domains || getDefaultDomains(c);;
}

View File

@@ -1,5 +1,5 @@
export const CONSTANTS = {
VERSION: 'v0.5.3',
VERSION: 'v0.6.1',
// DB settings
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',

View File

@@ -1,113 +0,0 @@
import { Hono } from 'hono'
// api v1 is deprecated
const api = new Hono()
api.get('/admin/v1/mails', async (c) => {
const { address, limit, offset } = c.req.query();
if (!limit || limit < 0 || limit > 100) {
return c.text("Invalid limit", 400)
}
if (!offset || offset < 0) {
return c.text("Invalid offset", 400)
}
const { results } = await c.env.DB.prepare(
`SELECT id, source, subject, message FROM mails where address = ? order by id desc limit ? offset ? `
).bind(address, limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: mailCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM mails where address = ? `
).bind(address).first();
count = mailCount;
}
return c.json({
results: results,
count: count
})
});
api.get('/admin/v1/mails_unknow', async (c) => {
const { limit, offset } = c.req.query();
if (!limit || limit < 0 || limit > 100) {
return c.text("Invalid limit", 400)
}
if (!offset || offset < 0) {
return c.text("Invalid offset", 400)
}
const { results } = await c.env.DB.prepare(`
SELECT id, source, subject, message FROM mails
where address NOT IN(select name from address)
order by id desc limit ? offset ? `
).bind(limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: mailCount } = await c.env.DB.prepare(`
SELECT count(*) as count FROM mails
where address NOT IN (select name from address)`
).first();
count = mailCount;
}
return c.json({
results: results,
count: count
})
});
api.get('/api/v1/mails', async (c) => {
const { address } = c.get("jwtPayload")
if (!address) {
return c.json({ "error": "No address" }, 400)
}
const { limit, offset } = c.req.query();
if (!limit || limit < 0 || limit > 100) {
return c.text("Invalid limit", 400)
}
if (!offset || offset < 0) {
return c.text("Invalid offset", 400)
}
const { results } = await c.env.DB.prepare(
`SELECT id, source, subject, message, message_id, created_at FROM mails where address = ? order by id desc limit ? offset ?`
).bind(address, limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: mailCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM mails where address = ?`
).bind(address).first();
count = mailCount;
}
// add attachments
let attachmentResults = [];
const message_ids = results.map((r) => r.message_id).filter((r) => r);
if (message_ids && message_ids.length > 0) {
const { results: innerAttachmentResults } = await c.env.DB.prepare(
`SELECT id, message_id FROM attachments where message_id in (${message_ids.map((id) => `'${id}'`).join(",")})`
).all();
attachmentResults = innerAttachmentResults || [];
}
results.forEach((r) => {
const attachment_id = attachmentResults.filter((ar) => ar.message_id == r.message_id).map((ar) => ar.id);
if (attachment_id && attachment_id.length > 0) {
r.attachment_id = attachment_id[0];
}
delete r.message_id;
})
return c.json({
results: results,
count: count
})
})
// attachments
api.get("/api/v1/attachment/:attachment_id", async (c) => {
const { attachment_id } = c.req.param();
const { data } = await c.env.DB.prepare(
`SELECT data FROM attachments where id = ? `
).bind(attachment_id).first();
if (!data) {
return c.text("Not found", 404)
}
return c.json(JSON.parse(data))
})
export { api }

View File

@@ -2,7 +2,7 @@ import { Hono } from 'hono'
import { HonoCustomType } from "../types";
import { getBooleanValue, getJsonSetting, checkCfTurnstile } from '../utils';
import { newAddress, handleListQuery, deleteAddressWithData } from '../common'
import { newAddress, handleListQuery, deleteAddressWithData, getAddressPrefix, getAllowDomains } from '../common'
import { CONSTANTS } from '../constants'
import auto_reply from './auto_reply'
import webhook_settings from './webhook_settings';
@@ -38,9 +38,10 @@ api.delete('/api/mails/:id', async (c) => {
}
const { address } = c.get("jwtPayload")
const { id } = c.req.param();
// TODO: add toLowerCase() to handle old data
const { success } = await c.env.DB.prepare(
`DELETE FROM raw_mails WHERE address = ? and id = ? `
).bind(address, id).run();
).bind(address.toLowerCase(), id).run();
return c.json({
success: success
})
@@ -117,7 +118,8 @@ api.post('/api/new_address', async (c) => {
console.error(error);
}
try {
const res = await newAddress(c, name, domain, true);
const addressPrefix = await getAddressPrefix(c);
const res = await newAddress(c, name, domain, true, true, addressPrefix);
return c.json(res);
} catch (e) {
return c.text(`Failed create address: ${(e as Error).message}`, 400)

View File

@@ -18,7 +18,7 @@ const COMMANDS = [
},
{
command: "new",
description: "新建邮箱地址, 如果要自定义邮箱地址, 请输入 /new <name>@<domain>, name [a-zA-Z0-9.] 有效"
description: "新建邮箱地址, 如果要自定义邮箱地址, 请输入 /new <name>@<domain>, name [a-z0-9] 有效"
},
{
command: "address",

10
worker/src/types.d.ts vendored
View File

@@ -1,3 +1,9 @@
export type UserRole = {
domains: string[] | undefined | null,
role: string,
prefix: string | undefined | null
}
export type Bindings = {
// bindings
DB: D1Database
@@ -7,10 +13,14 @@ export type Bindings = {
// config
TITLE: string | undefined
ANNOUNCEMENT: string | undefined | null
PREFIX: string | undefined
MIN_ADDRESS_LEN: string | number | undefined
MAX_ADDRESS_LEN: string | number | undefined
DEFAULT_DOMAINS: string | string[] | undefined
DOMAINS: string | string[] | undefined
USER_DEFAULT_ROLE: string | UserRole | undefined
USER_ROLES: string | UserRole[] | undefined
DOMAIN_LABELS: string | string[] | undefined
PASSWORDS: string | string[] | undefined
ADMIN_PASSWORDS: string | string[] | undefined

View File

@@ -2,8 +2,9 @@ import { Context } from "hono";
import { HonoCustomType } from "../types";
import { UserSettings } from "../models";
import { getJsonSetting } from "../utils"
import { getJsonSetting, getUserRoles } from "../utils"
import { CONSTANTS } from "../constants";
import { commonGetUserRole } from "../common";
export default {
openSettings: async (c: Context<HonoCustomType>) => {
@@ -19,10 +20,14 @@ export default {
// check if user exists
const db_user_id = await c.env.DB.prepare(
`SELECT id FROM users where id = ?`
).bind(user.user_id).first("id");
).bind(user.user_id).first<number | undefined | null>("id");
if (!db_user_id) {
return c.text("User not found", 400);
}
return c.json(user);
const user_role = await commonGetUserRole(c, db_user_id);
return c.json({
...user,
user_role: user_role
});
},
}

View File

@@ -2,7 +2,7 @@ import { Context } from 'hono';
import { Jwt } from 'hono/utils/jwt'
import { HonoCustomType } from '../types';
import { checkCfTurnstile, getJsonSetting, checkUserPassword } from "../utils"
import { checkCfTurnstile, getJsonSetting, checkUserPassword, getUserRoles, getStringValue } from "../utils"
import { CONSTANTS } from "../constants";
import { GeoData, UserInfo, UserSettings } from "../models";
import { sendMail } from "../mails_api/send_mail_api";
@@ -124,6 +124,28 @@ export default {
if (!success) {
return c.text("Failed to register", 500)
}
const defaultRole = getStringValue(c.env.USER_DEFAULT_ROLE);
if (!defaultRole) return c.json({ success: true })
const user_roles = getUserRoles(c);
if (!user_roles.find((r) => r.role === defaultRole)) {
return c.text("Invalid role_text", 400)
}
// find user_id
const user_id = await c.env.DB.prepare(
`SELECT id FROM users where user_email = ?`
).bind(email).first<number | undefined | null>("id");
if (!user_id) {
return c.text("User not found", 400)
}
// update user roles
const { success: success2 } = await c.env.DB.prepare(
`INSERT INTO user_roles (user_id, role_text)`
+ ` VALUES (?, ?)`
+ ` ON CONFLICT(user_id) DO NOTHING`
).bind(user_id, defaultRole).run();
if (!success2) {
return c.text("Failed to update user roles", 500)
}
return c.json({ success: true })
},
login: async (c: Context<HonoCustomType>) => {

View File

@@ -1,6 +1,7 @@
import { Context } from "hono";
import { createMimeMessage } from "mimetext";
import { HonoCustomType } from "./types";
import { HonoCustomType, UserRole } from "./types";
import { User } from "telegraf/types";
export const getJsonSetting = async (
c: Context<HonoCustomType>, key: string
@@ -97,6 +98,12 @@ export const getStringArray = (
return value;
}
export const getDefaultDomains = (c: Context<HonoCustomType>): string[] => {
const domains = getStringArray(c.env.DEFAULT_DOMAINS);
if (domains && domains.length > 0) return domains;
return getDomains(c);
}
export const getDomains = (c: Context<HonoCustomType>): string[] => {
if (!c.env.DOMAINS) {
return [];
@@ -113,6 +120,22 @@ export const getDomains = (c: Context<HonoCustomType>): string[] => {
return c.env.DOMAINS;
}
export const getUserRoles = (c: Context<HonoCustomType>): UserRole[] => {
if (!c.env.USER_ROLES) {
return [];
}
// check if USER_ROLES is an array, if not use json.parse
if (!Array.isArray(c.env.USER_ROLES)) {
try {
return JSON.parse(c.env.USER_ROLES);
} catch (e) {
console.error("Failed to parse USER_ROLES", e);
return [];
}
}
return c.env.USER_ROLES;
}
export const getPasswords = (c: Context<HonoCustomType>): string[] => {
if (!c.env.PASSWORDS) {
return [];

View File

@@ -1,11 +1,8 @@
import { Hono } from 'hono'
import { Context, Hono } from 'hono'
import { cors } from 'hono/cors';
import { jwt } from 'hono/jwt'
import { Jwt } from 'hono/utils/jwt'
// @ts-ignore
import { api as apiV1 } from './deprecated';
import { api as commonApi } from './commom_api';
import { api as mailsApi } from './mails_api'
import { api as userApi } from './user_api';
@@ -16,11 +13,16 @@ import { api as telegramApi } from './telegram_api'
import { email } from './email';
import { scheduled } from './scheduled';
import { getAdminPasswords, getPasswords, getBooleanValue } from './utils';
import { HonoCustomType } from './types';
import { HonoCustomType, UserPayload } from './types';
const app = new Hono<HonoCustomType>()
//cors
app.use('/*', cors());
// error handler
app.onError((err, c) => {
console.error(err)
return c.text(`${err.name} ${err.message}`, 500)
})
// rate limit
app.use('/*', async (c, next) => {
if (
@@ -53,6 +55,26 @@ app.use('/*', async (c, next) => {
}
await next()
});
const checkUserPayload = async (
c: Context<HonoCustomType>
): Promise<void> => {
try {
const token = c.req.raw.headers.get("x-user-token");
if (!token) return;
const payload = await Jwt.verify(token, c.env.JWT_SECRET, "HS256");
// check expired
if (!payload.exp) return;
// exp is in seconds
if (payload.exp < Math.floor(Date.now() / 1000)) {
return;
}
c.set("userPayload", payload as UserPayload);
} catch (e) {
console.error(e);
}
}
// api auth
app.use('/api/*', async (c, next) => {
// check header x-custom-auth
@@ -64,6 +86,7 @@ app.use('/api/*', async (c, next) => {
}
}
if (c.req.path.startsWith("/api/new_address")) {
await checkUserPayload(c);
await next();
return;
}
@@ -90,7 +113,7 @@ app.use('/user_api/*', async (c, next) => {
if (payload.exp < Math.floor(Date.now() / 1000)) {
return c.text("Token Expired", 401)
}
c.set("userPayload", payload);
c.set("userPayload", payload as UserPayload);
} catch (e) {
console.error(e);
return c.text("Need User Token", 401)
@@ -121,7 +144,6 @@ app.route('/', commonApi)
app.route('/', mailsApi)
app.route('/', userApi)
app.route('/', adminApi)
app.route('/', apiV1)
app.route('/', apiSendMail)
app.route('/', telegramApi)

View File

@@ -17,6 +17,7 @@ node_compat = true
[vars]
# TITLE = "Custom Title" # custom title
# ANNOUNCEMENT = "Custom Announcement"
PREFIX = "tmp"
# (min, max) length of the adderss, if not set, the default is (1, 30)
# MIN_ADDRESS_LEN = 1
@@ -27,9 +28,16 @@ PREFIX = "tmp"
# ADMIN_PASSWORDS = ["123", "456"]
# ADMIN CONTACT, CAN BE ANY STRING
# ADMIN_CONTACT = "xx@xx.xxx"
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"]
DEFAULT_DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # domain name for no role users
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # all domain names
# For chinese domain name, you can use DOMAIN_LABELS to show chinese domain name
# DOMAIN_LABELS = ["中文.xxx", "xxx.xxx2"]
# USER_DEFAULT_ROLE = "vip" # default role for new users(only when enable mail verification)
# User roles configuration, if domains is empty will use default_domains, if prefix is null will use default prefix, if prefix is empty string will not use prefix
# USER_ROLES = [
# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "vip", prefix = "vip" },
# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "admin", prefix = "" },
# ]
JWT_SECRET = "xxx"
BLACK_LIST = ""
# Allow users to create email addresses