diff --git a/CHANGELOG.md b/CHANGELOG.md index 001fd02..255c3b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - feat: |邮件| 新增可选的已读/未读状态,支持点击邮件自动已读和手动切换状态 - feat: |Admin| 数据库页面新增 D1 存储容量展示,支持选择并保存 Free 或 Workers Paid 套餐,对比当前数据库大小和容量上限 - feat: |Admin| 创建邮箱页面支持一键生成随机邮箱名称(issue #1126) +- feat: |Admin API| 新增 `ADMIN_API_IP_WHITELIST`,可按来源 IP 限制所有管理接口访问 - feat: |用户系统| 用户中心新增发送邮件、与收件箱一致的可按绑定地址过滤的发件箱,以及地址管理凭证弹框;提供使用用户 JWT 的地址设置、发信权限申请、发信及发件箱 API ### Bug Fixes diff --git a/CHANGELOG_EN.md b/CHANGELOG_EN.md index d241234..486b659 100644 --- a/CHANGELOG_EN.md +++ b/CHANGELOG_EN.md @@ -13,6 +13,7 @@ - feat: |Mail| Add optional read/unread status with click-to-read and manual status switching - feat: |Admin| Add D1 storage capacity details to the database page, with persistent Free and Workers Paid plan selection and a comparison between the current database size and capacity limit - feat: |Admin| Add one-click random email-name generation to the address creation page (issue #1126) +- feat: |Admin API| Add `ADMIN_API_IP_WHITELIST` to restrict all admin endpoints by source IP - feat: |User| Add mail composition, inbox-style sent-item filtering by bound address, and the shared address-credentials dialog to the user center, backed by User JWT APIs for address settings, send-access requests, sending, and sent-item management ### Bug Fixes diff --git a/vitepress-docs/docs/en/guide/cli/worker.md b/vitepress-docs/docs/en/guide/cli/worker.md index 05d2b05..4469e98 100644 --- a/vitepress-docs/docs/en/guide/cli/worker.md +++ b/vitepress-docs/docs/en/guide/cli/worker.md @@ -67,6 +67,8 @@ JWT_SECRET = "xxx" # Admin console password, if not configured, console access is not allowed # ADMIN_PASSWORDS = ["123", "456"] +# Admin API IP whitelist; applies to admin-password and Admin user-token access +# ADMIN_API_IP_WHITELIST = ["203.0.113.10"] # Whether to allow users to create emails, not allowed if not configured ENABLE_USER_CREATE_EMAIL = true diff --git a/vitepress-docs/docs/en/guide/worker-vars.md b/vitepress-docs/docs/en/guide/worker-vars.md index 903e030..08bee26 100644 --- a/vitepress-docs/docs/en/guide/worker-vars.md +++ b/vitepress-docs/docs/en/guide/worker-vars.md @@ -23,8 +23,11 @@ | Variable Name | Type | Description | Example | | ------------------------------ | --------- | ------------------------------------------------------- | ---------------- | | `PASSWORDS` | JSON | Website private passwords, required after configuration | `["123", "456"]` | +| `ADMIN_API_IP_WHITELIST` | JSON | Admin API IP whitelist; when configured, only listed IPs may access `/admin/*` | `["203.0.113.10"]` | | `DISABLE_ADMIN_PASSWORD_CHECK` | Text/JSON | Warning: Admin console without password or user check | `false` | +When `ADMIN_API_IP_WHITELIST` is unset or empty, source IPs are not restricted. Once configured, it applies to both admin-password and Admin user-token access, trusts only Cloudflare's `CF-Connecting-IP` header, and denies requests without that header. + ## Email Related Variables | Variable Name | Type | Description | Example | diff --git a/vitepress-docs/docs/zh/guide/cli/worker.md b/vitepress-docs/docs/zh/guide/cli/worker.md index 2a10316..21353ed 100644 --- a/vitepress-docs/docs/zh/guide/cli/worker.md +++ b/vitepress-docs/docs/zh/guide/cli/worker.md @@ -67,6 +67,8 @@ JWT_SECRET = "xxx" # admin 控制台密码, 不配置则不允许访问控制台 # ADMIN_PASSWORDS = ["123", "456"] +# Admin API IP 白名单,同时限制管理员密码和 Admin 用户令牌访问 +# ADMIN_API_IP_WHITELIST = ["203.0.113.10"] # 是否允许用户创建邮件, 不配置则不允许 ENABLE_USER_CREATE_EMAIL = true diff --git a/vitepress-docs/docs/zh/guide/worker-vars.md b/vitepress-docs/docs/zh/guide/worker-vars.md index 6b3ec25..6a4ec84 100644 --- a/vitepress-docs/docs/zh/guide/worker-vars.md +++ b/vitepress-docs/docs/zh/guide/worker-vars.md @@ -23,8 +23,11 @@ | 变量名 | 类型 | 说明 | 示例 | | ------------------------------ | --------- | ------------------------------------ | ---------------- | | `PASSWORDS` | JSON | 网站私有密码, 配置后需要密码才能访问 | `["123", "456"]` | +| `ADMIN_API_IP_WHITELIST` | JSON | Admin API IP 白名单,配置后所有 `/admin/*` 请求仅允许列表中的 IP | `["203.0.113.10"]` | | `DISABLE_ADMIN_PASSWORD_CHECK` | 文本/JSON | 警告: 管理员控制台没有密码或用户检查 | `false` | +`ADMIN_API_IP_WHITELIST` 未配置或为空数组时不限制来源 IP。配置后,它会同时限制管理员密码和 Admin 用户令牌访问,只信任 Cloudflare 提供的 `CF-Connecting-IP`,缺少该请求头也会拒绝访问。 + ## 邮箱相关变量 | 变量名 | 类型 | 说明 | 示例 | diff --git a/worker/src/i18n/en.ts b/worker/src/i18n/en.ts index c92b85b..7dbc977 100644 --- a/worker/src/i18n/en.ts +++ b/worker/src/i18n/en.ts @@ -6,6 +6,7 @@ const messages: LocaleMessages = { UserAcceesTokenExpiredMsg: "Your access token has expired, please refresh the page", UserRoleIsNotAdminMsg: "Your user role is not admin, no access to visit this page", NeedAdminPasswordMsg: "You need to provide the admin password to access this page", + AdminApiIpNotAllowedMsg: "Your IP address isn't on the admin whitelist", KVNotAvailableMsg: "KV is not available, please contact the administrator", DBNotAvailableMsg: "DB is not available, please contact the administrator", diff --git a/worker/src/i18n/type.ts b/worker/src/i18n/type.ts index a677c0a..d18e79f 100644 --- a/worker/src/i18n/type.ts +++ b/worker/src/i18n/type.ts @@ -4,6 +4,7 @@ export type LocaleMessages = { UserAcceesTokenExpiredMsg: string UserRoleIsNotAdminMsg: string NeedAdminPasswordMsg: string + AdminApiIpNotAllowedMsg: string KVNotAvailableMsg: string DBNotAvailableMsg: string diff --git a/worker/src/i18n/zh.ts b/worker/src/i18n/zh.ts index c4837e3..e4e1709 100644 --- a/worker/src/i18n/zh.ts +++ b/worker/src/i18n/zh.ts @@ -6,6 +6,7 @@ const messages: LocaleMessages = { UserAcceesTokenExpiredMsg: "您的访问令牌已过期, 请刷新页面", UserRoleIsNotAdminMsg: "您的用户角色不是管理员, 无权访问", NeedAdminPasswordMsg: "您需要提供管理员密码才能访问此页面", + AdminApiIpNotAllowedMsg: "当前 IP 不在管理员白名单中", KVNotAvailableMsg: "KV 不可用, 请联系管理员", DBNotAvailableMsg: "DB 不可用, 请联系管理员", diff --git a/worker/src/types.d.ts b/worker/src/types.d.ts index 0488d5e..8dc64f9 100644 --- a/worker/src/types.d.ts +++ b/worker/src/types.d.ts @@ -49,6 +49,7 @@ type Bindings = { DOMAIN_LABELS: string | string[] | undefined PASSWORDS: string | string[] | undefined ADMIN_PASSWORDS: string | string[] | undefined + ADMIN_API_IP_WHITELIST: string | string[] | undefined DISABLE_ADMIN_PASSWORD_CHECK: string | boolean | undefined JWT_SECRET: string BLACK_LIST: string | undefined diff --git a/worker/src/worker.ts b/worker/src/worker.ts index 65bbdbb..8326fa2 100644 --- a/worker/src/worker.ts +++ b/worker/src/worker.ts @@ -14,7 +14,7 @@ import { api as telegramApi } from './telegram_api' import i18n from './i18n'; import { email } from './email'; import { scheduled } from './scheduled'; -import { getPasswords, getBooleanValue, getDomains, checkIsAdmin } from './utils'; +import { getPasswords, getBooleanValue, getDomains, checkIsAdmin, getEnvStringList } from './utils'; import { checkAccessControl } from './ip_blacklist'; const API_PATHS = [ @@ -221,14 +221,28 @@ app.use('/user_api/*', async (c, next) => { }); // admin auth app.use('/admin/*', async (c, next) => { + const lang = c.req.raw.headers.get("x-lang") || c.env.DEFAULT_LANG; + const msgs = i18n.getMessages(lang); + try { + const ipWhitelist = getEnvStringList(c.env.ADMIN_API_IP_WHITELIST) + .filter(ip => typeof ip === "string") + .map(ip => ip.trim()) + .filter(Boolean); + if (ipWhitelist.length > 0) { + const reqIp = c.req.raw.headers.get("cf-connecting-ip")?.trim(); + if (!reqIp || !ipWhitelist.includes(reqIp)) { + return c.text(msgs.AdminApiIpNotAllowedMsg, 403); + } + } + } catch (e) { + console.error("Failed to check admin API IP whitelist", e); + } // check header x-admin-auth if (checkIsAdmin(c)) { await next(); return; } - const lang = c.req.raw.headers.get("x-lang") || c.env.DEFAULT_LANG; - const msgs = i18n.getMessages(lang); // check if user is admin const access_token = c.req.raw.headers.get("x-user-access-token"); if (c.env.ADMIN_USER_ROLE && access_token) { diff --git a/worker/wrangler.toml.template b/worker/wrangler.toml.template index 83e6a8e..f77b89f 100644 --- a/worker/wrangler.toml.template +++ b/worker/wrangler.toml.template @@ -45,6 +45,8 @@ PREFIX = "tmp" # PASSWORDS = ["123", "456"] # For admin panel # ADMIN_PASSWORDS = ["123", "456"] +# Restrict all /admin/* APIs to exact client IPv4 or IPv6 addresses +# ADMIN_API_IP_WHITELIST = ["203.0.113.10", "2001:db8::1"] # warning: no password or user check for admin portal # DISABLE_ADMIN_PASSWORD_CHECK = false # ADMIN CONTACT, CAN BE ANY STRING