diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml new file mode 100644 index 0000000..9db7264 --- /dev/null +++ b/.github/workflows/sync-upstream.yml @@ -0,0 +1,28 @@ +name: Sync upstream + +on: + schedule: + - cron: "0 3 * * *" + workflow_dispatch: + +permissions: + contents: write + +jobs: + sync: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - run: | + git remote add upstream https://github.com/shuaiplus/nodewarden.git || true + git fetch upstream + + # 强制让当前分支完全等于 upstream + git reset --hard upstream/main + + # 强制推送 + git push origin main --force diff --git a/README.md b/README.md index 152fc75..66b448c 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,47 @@ # NodeWarden -中文文档:[`README_ZH.md`](./README_ZH.md) +English:[`README_ZH.md`](./README_EN.md) -A **Bitwarden-compatible** server that runs on **Cloudflare Workers**. +运行在 **Cloudflare Workers** 上的 **Bitwarden 第三方服务端**。 -- Simple deploy (no VPS) -- Focused feature set -- Low maintenance - - -> Disclaimer -> - This project is **not affiliated** with Bitwarden. -> - Use at your own risk. Keep regular backups of your vault. +> **免责声明** +> 本项目仅供学习交流使用。我们不对任何数据丢失负责,强烈建议定期备份您的密码库。 +> 本项目与 Bitwarden 官方无关,请勿向 Bitwarden 官方反馈问题。 --- -## Features +## 特性 +- ✅ **完全免费,不需要在服务器上部署,再次感谢大善人!** +- ✅ 数据存储基于 Cloudflare D1(SQLite) +- ✅ 完整的密码、笔记、卡片、身份信息管理 +- ✅ 文件夹和收藏功能 +- ✅ 文件附件支持(基于 R2 存储) +- ✅ 导入/导出功能 +- ✅ 网站图标获取 +- ✅ 端到端加密(服务器无法查看明文) +- ✅ 兼容常见的 Bitwarden 官方客户端 -- ✅ **Free to use. No server to manage.** -- ✅ Full support for logins, notes, cards, and identities -- ✅ Folders and favorites -- ✅ Attachments (Cloudflare R2) -- ✅ Import / export -- ✅ Website icons -- ✅ End-to-end encryption (the server can’t see plaintext) -- ✅ Compatible with common Bitwarden official clients +## 测试情况: +- ✅ Windows 客户端(v2026.1.0) +- ✅ Android App(v2026.1.0) +- ✅ 浏览器扩展(v2026.1.0) +- ⬜ macOS 客户端(未测试) +- ⬜ Linux 客户端(未测试) +--- -## Tested clients / platforms +# 快速开始 -- ✅ Windows desktop client (v2026.1.0) -- ✅ Android app (v2026.1.0) -- ✅ Browser extension (v2026.1.0) -- ⬜ macOS desktop client (not tested) -- ⬜ Linux desktop client (not tested) +### 一键部署 + +**部署步骤:** + +1. [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/shuaiplus/nodewarden) +2. 打开部署后生成的链接,并根据网页提示完成后续操作。 --- -# Quick start +## 本地开发 -### One-click deploy - -[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/shuaiplus/nodewarden) - -**Deploy steps:** - -1. Sign in with GitHub and authorize -2. Sign in to Cloudflare -3. **Important**: set `JWT_SECRET` to a strong random string (recommended: `openssl rand -hex 32`) -4. D1 database and R2 bucket will be created automatically -5. Click **Deploy** and wait for it to finish -6. After deploy, open the Cloudflare-provided Workers URL (your service URL), and register on the web page - -> ⚠️ **Reminder**: always use a strong random `JWT_SECRET`. Weak secrets may put your account at risk. - -### Configure your client - -In any Bitwarden client: - -1. Open **Settings** -2. Choose **Self-hosted environment** -3. Set **Server URL** to your Worker URL (for example: `https://your-project.your-subdomain.workers.dev`) -4. Save, then go back to the login screen - -## 🧑‍💻 Local development - -This repo is a Cloudflare Workers TypeScript project (Wrangler). +这是一个 Cloudflare Workers 的 TypeScript 项目(Wrangler)。 ```bash npm install @@ -72,37 +50,27 @@ npm run dev --- -## Tech stack +## 常见问题 -- **Runtime**: Cloudflare Workers -- **Data storage**: Cloudflare D1 (SQLite) -- **File storage**: Cloudflare R2 -- **Language**: TypeScript -- **Crypto**: Client-side AES-256-CBC, JWT uses HS256 +**Q: 如何备份数据?** +A: 在客户端中选择「导出密码库」,保存 JSON 文件。 + +**Q: 忘记主密码怎么办?** +A: 无法恢复,这是端到端加密的特性。建议妥善保管主密码。 + +**Q: 可以多人使用吗?** +A: 不建议。本项目为单用户设计,多人使用请选择 Vaultwarden。 --- -## FAQ - -**Q: How do I back up my data?** -A: Use **Export vault** in your client and save the JSON file. - -**Q: What if I forget the master password?** -A: It can’t be recovered (end-to-end encryption). Keep it safe. - -**Q: Can multiple people use it?** -A: Not recommended. This project is designed for single-user usage. - ---- - -## License +## 开源协议 LGPL-3.0 License --- -## Credits +## 致谢 -- [Bitwarden](https://bitwarden.com/) - original design and clients -- [Vaultwarden](https://github.com/dani-garcia/vaultwarden) - server implementation reference -- [Cloudflare Workers](https://workers.cloudflare.com/) - serverless platform +- [Bitwarden](https://bitwarden.com/) - 原始设计和客户端 +- [Vaultwarden](https://github.com/dani-garcia/vaultwarden) - 服务器实现参考 +- [Cloudflare Workers](https://workers.cloudflare.com/) - 无服务器平台 diff --git a/README_EN.md b/README_EN.md new file mode 100644 index 0000000..c42220a --- /dev/null +++ b/README_EN.md @@ -0,0 +1,79 @@ +# NodeWarden +中文文档:[`README.md`](./README.md) + +A **Bitwarden-compatible** server that runs on **Cloudflare Workers**. + +> Disclaimer +> - This project is for learning and communication only. +> - We are not responsible for any data loss. Regular vault backups are strongly recommended. +> - This project is not affiliated with Bitwarden. Please do not report issues to the official Bitwarden team. + +--- + +## Features + +- ✅ **Completely free, no server deployment needed. Thanks again to the generous sponsor!** +- ✅ Data storage on Cloudflare D1 (SQLite) +- ✅ Full support for logins, notes, cards, and identities +- ✅ Folders and favorites +- ✅ Attachments (Cloudflare R2) +- ✅ Import / export +- ✅ Website icons +- ✅ End-to-end encryption (the server can’t see plaintext) +- ✅ Compatible with common Bitwarden official clients + +## Tested clients / platforms + +- ✅ Windows desktop client (v2026.1.0) +- ✅ Android app (v2026.1.0) +- ✅ Browser extension (v2026.1.0) +- ⬜ macOS desktop client (not tested) +- ⬜ Linux desktop client (not tested) + +--- + +# Quick start + +### One-click deploy + +**Deploy steps:** + +1. [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/shuaiplus/nodewarden) +2. Open the generated service URL and follow the on-page instructions. + + +## Local development + +This repo is a Cloudflare Workers TypeScript project (Wrangler). + +```bash +npm install +npm run dev +``` + +--- + +## FAQ + +**Q: How do I back up my data?** +A: Use **Export vault** in your client and save the JSON file. + +**Q: What if I forget the master password?** +A: It can’t be recovered (end-to-end encryption). Keep it safe. + +**Q: Can multiple people use it?** +A: Not recommended. This project is designed for single-user usage. For multi-user usage, choose Vaultwarden. + +--- + +## License + +LGPL-3.0 License + +--- + +## Credits + +- [Bitwarden](https://bitwarden.com/) - original design and clients +- [Vaultwarden](https://github.com/dani-garcia/vaultwarden) - server implementation reference +- [Cloudflare Workers](https://workers.cloudflare.com/) - serverless platform \ No newline at end of file diff --git a/README_ZH.md b/README_ZH.md deleted file mode 100644 index 2a668f0..0000000 --- a/README_ZH.md +++ /dev/null @@ -1,114 +0,0 @@ - -# NodeWarden -English:[`README.md`](./README.md) - -一个运行在 **Cloudflare Workers** 上的 **Bitwarden 兼容**服务端实现。 - -- 部署简单(不需要 VPS) -- 功能聚焦 -- 维护成本低 - - - -> **免责声明** -> 本项目仅供学习交流使用。我们不对任何数据丢失负责,强烈建议定期备份您的密码库。 -> 本项目与 Bitwarden 官方无关,请勿向 Bitwarden 官方反馈问题。 - ---- - -## 特性 -- ✅ **完全免费,不需要在服务器上部署,再次感谢大善人!** -- ✅ 完整的密码、笔记、卡片、身份信息管理 -- ✅ 文件夹和收藏功能 -- ✅ 文件附件支持(基于 R2 存储) -- ✅ 导入/导出功能 -- ✅ 网站图标获取 -- ✅ 端到端加密(服务器无法查看明文) -- ✅ 兼容常见的 Bitwarden 官方客户端 - -## 测试情况: -- ✅ Windows 客户端(v2026.1.0) -- ✅ Android App(v2026.1.0) -- ✅ 浏览器扩展(v2026.1.0) -- ⬜ macOS 客户端(未测试) -- ⬜ Linux 客户端(未测试) ---- - -# 快速开始 - -### 一键部署 - -点击下方按钮部署到 Cloudflare Workers: - -[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/shuaiplus/nodewarden) - -**部署步骤:** - -1. 使用 GitHub 登录并授权 -2. 登录 Cloudflare 账户 -3. **重要**:设置 `JWT_SECRET` 为强随机字符串(推荐使用 `openssl rand -hex 32` 生成) -4. D1 数据库和 R2 存储桶将自动创建 -5. 点击 Deploy 等待部署完成 -6. 部署完成后,先打开 Cloudflare 给你的 Workers 链接(也就是你的服务地址),在网页上填写信息完成注册。 - -> ⚠️ **再次提醒**:请务必使用强随机的 `JWT_SECRET`,使用默认或弱密钥可能导致账户被入侵,**后果自负!** - -### 配置客户端 - -部署完成后,在任意 Bitwarden 客户端中: - -1. 打开设置(⚙️) -2. 选择「自托管环境」 -3. 服务器 URL 填入:`https://你的项目名` -4. 保存并返回登录页面 - - - ---- - -## 本地开发 - -这是一个 Cloudflare Workers 的 TypeScript 项目(Wrangler)。 - -```bash -npm install -npm run dev -``` - ---- - - -## 技术栈 - -- **运行环境**:Cloudflare Workers -- **数据存储**:Cloudflare D1(SQLite) -- **文件存储**:Cloudflare R2 -- **开发语言**:TypeScript -- **加密算法**:客户端 AES-256-CBC,JWT 使用 HS256 - ---- - -## 常见问题 - -**Q: 如何备份数据?** -A: 在客户端中选择「导出密码库」,保存 JSON 文件。 - -**Q: 忘记主密码怎么办?** -A: 无法恢复,这是端到端加密的特性。建议妥善保管主密码。 - -**Q: 可以多人使用吗?** -A: 不建议。本项目为单用户设计,多人使用请选择 Vaultwarden。 - ---- - -## 开源协议 - -LGPL-3.0 License - ---- - -## 致谢 - -- [Bitwarden](https://bitwarden.com/) - 原始设计和客户端 -- [Vaultwarden](https://github.com/dani-garcia/vaultwarden) - 服务器实现参考 -- [Cloudflare Workers](https://workers.cloudflare.com/) - 无服务器平台 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 30da3c5..ac60a7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,17 @@ { "name": "nodewarden", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nodewarden", - "version": "0.1.0", + "version": "0.2.0", "license": "LGPL-3.0", "devDependencies": { "@cloudflare/workers-types": "^4.20260131.0", + "@types/node": "^25.2.3", + "tsx": "^4.21.0", "typescript": "^5.9.3", "wrangler": "^4.61.1" } @@ -1166,6 +1168,16 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/@types/node": { + "version": "25.2.3", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.2.3.tgz", + "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, "node_modules/blake3-wasm": { "version": "2.1.5", "resolved": "https://registry.npmmirror.com/blake3-wasm/-/blake3-wasm-2.1.5.tgz", @@ -1264,6 +1276,19 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmmirror.com/kleur/-/kleur-4.1.5.tgz", @@ -1309,6 +1334,16 @@ "dev": true, "license": "MIT" }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz", @@ -1388,6 +1423,26 @@ "license": "0BSD", "optional": true }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", @@ -1412,6 +1467,13 @@ "node": ">=20.18.1" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, "node_modules/unenv": { "version": "2.0.0-rc.24", "resolved": "https://registry.npmmirror.com/unenv/-/unenv-2.0.0-rc.24.tgz", diff --git a/package.json b/package.json index 752e2b4..2487f54 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "scripts": { "dev": "wrangler dev -c wrangler.toml", "deploymy": "wrangler deploy -c wrangler.my.toml", - "deploy": "wrangler deploy " + "deploy": "wrangler deploy", + "selfcheck": "npx tsx tests/selfcheck.ts" }, "keywords": [ "bitwarden", @@ -33,8 +34,9 @@ }, "devDependencies": { "@cloudflare/workers-types": "^4.20260131.0", + "@types/node": "^25.2.3", + "tsx": "^4.21.0", "typescript": "^5.9.3", "wrangler": "^4.61.1" } } - diff --git a/src/handlers/setup.ts b/src/handlers/setup.ts index b4fdd0a..60aaa1d 100644 --- a/src/handlers/setup.ts +++ b/src/handlers/setup.ts @@ -1,9 +1,10 @@ import { Env, DEFAULT_DEV_SECRET } from '../types'; import { StorageService } from '../services/storage'; -import { jsonResponse, htmlResponse, errorResponse } from '../utils/response'; -import { renderJwtSecretWarningPage, JwtSecretState } from './setupPages'; +import { jsonResponse, errorResponse } from '../utils/response'; import { handleRegisterPage } from './setupRegisterPage'; +type JwtSecretState = 'missing' | 'default' | 'too_short'; + function getJwtSecretState(env: Env): JwtSecretState | null { const secret = (env.JWT_SECRET || '').trim(); if (!secret) return 'missing'; @@ -21,14 +22,9 @@ export async function handleSetupPage(request: Request, env: Env): Promise = { - app: 'NodeWarden', - tag: '部署在 Cloudflare Workers 上的 Bitwarden 兼容服务端。', - - // Config warning page - cfgTitle: '需要配置 JWT_SECRET', - cfgDescMissing: '当前服务没有配置 JWT_SECRET(用于签名登录令牌)。为了安全起见,必须先配置后才能注册/使用。', - cfgDescDefault: '检测到你正在使用示例/默认 JWT_SECRET。为了安全起见,请先修改为随机强密钥后再注册/使用。', - cfgDescTooShort: '检测到 JWT_SECRET 长度不足 32 个字符。为了安全起见,请使用至少 32 位的随机字符串。', - cfgStepsTitle: '如何在 Cloudflare 修改 JWT_SECRET', - cfgStepsAdd: '打开 Cloudflare 控制台 → Workers 和 Pages → 选择 nodewarden → 设置 → 变量和机密 → 添加变量。\n类型:密钥\n名称:JWT_SECRET\n值:粘贴你生成的随机密钥\n保存后,等待重新部署生效。', - cfgStepsEdit: '打开 Cloudflare 控制台 → Workers 和 Pages → 选择 nodewarden → 设置 → 变量和机密 → 找到 JWT_SECRET 并编辑。\n类型:密钥\n名称:JWT_SECRET\n值:替换为新的随机强密钥\n保存后,等待重新部署生效。', - cfgGenTitle: '随机密钥生成器', - cfgGenHint: '建议长度:至少 32 字符(推荐 64+)。点击刷新生成新的随机值。', - cfgCopy: '复制', - cfgCopied: '已复制', - cfgRefresh: '刷新', - - // Shared - by: '作者', - github: 'GitHub', - }; - - const en: Record = { - app: 'NodeWarden', - tag: 'Minimal Bitwarden-compatible server on Cloudflare Workers.', - - // Config warning page - cfgTitle: 'JWT_SECRET is required', - cfgDescMissing: 'This server has no JWT_SECRET configured (used to sign login tokens). For safety, you must configure it before registration/usage.', - cfgDescDefault: 'You are using the sample/default JWT_SECRET. For safety, please change it to a strong random secret before registration/usage.', - cfgDescTooShort: 'JWT_SECRET is shorter than 32 characters. For safety, use a random string with at least 32 characters.', - cfgStepsTitle: 'How to set JWT_SECRET in Cloudflare', - cfgStepsAdd: 'Open Cloudflare Dashboard → Workers & Pages → select nodewarden → Settings → Variables and Secrets → Add variable.\nType: Secret\nName: JWT_SECRET\nValue: paste a random secret\nSave, and wait for redeploy to take effect.', - cfgStepsEdit: 'Open Cloudflare Dashboard → Workers & Pages → select nodewarden → Settings → Variables and Secrets → find JWT_SECRET and edit it.\nType: Secret\nName: JWT_SECRET\nValue: replace with a new strong random secret\nSave, and wait for redeploy to take effect.', - cfgGenTitle: 'Random secret generator', - cfgGenHint: 'Recommended length: 32+ characters (64+ preferred). Click refresh to generate a new one.', - cfgCopy: 'Copy', - cfgCopied: 'Copied', - cfgRefresh: 'Refresh', - - // Shared - by: 'By', - github: 'GitHub', - }; - - return (lang === 'zh' ? zh : en)[key] ?? key; -} - -function baseStyles(): string { - // Keep consistent with existing setup page look & feel. - return ` + return ` + + + + + NodeWarden + + a { color: #175cd3; text-decoration: none; } + a:hover { text-decoration: underline; } +
@@ -214,93 +305,887 @@ export function renderJwtSecretWarningPage(request: Request, state: JwtSecretSta
NW
-

${t(lang, 'app')}

-

${t(lang, 'tag')}

+

NodeWarden

+

Minimal Bitwarden-compatible server on Cloudflare Workers.

-

${t(lang, 'cfgTitle')}

-
${t(lang, descKey)}
+
-
-
-

${t(lang, 'cfgStepsTitle')}

-

${t(lang, stepsKey) - .replace(/^类型:密钥/m, '类型:密钥') - .replace(/^名称:JWT_SECRET/m, '名称:JWT_SECRET') - .replace(/^Type: Secret/m, 'Type: Secret') - .replace(/^Name: JWT_SECRET/m, 'Name: JWT_SECRET') - }

-
- -
-

${t(lang, 'cfgGenTitle')}

-

${t(lang, 'cfgGenHint')}

-
-
-
- - +
+
+

Welcome

+

+
+

Highlights

+
    +
  • +
  • +
  • +
-
+ + +
+

JWT secret check

+

+ +
+
+

Fix steps

+
    +
    +
    +

    Random JWT_SECRET

    +

    +
    +
    +
    + + +
    +
    +
    +
    + +
    +

    Create account

    +

    + +
    +
    +
    +
    + + +
    +
    + + +
    +
    + +
    + + +

    Choose a strong password you can remember. The server cannot recover it.

    +
    + +
    + + +
    + +
    + +
    +
    +
    + + +
    + +
    +

    Sync setup

    +

    + +
    +

    Common required steps

    +
      +
    1. +
    2. +
    3. +
    +
    + +
    +
    + + +
    + +
    +

    +
      +
    1. +
    2. +
    +
    + +
    +

    +
      +
    1. +
    2. +
    3. +
    +
    +
    + +

    +
    + +
    +

    Done

    +

    + +
    +

    Server URL

    +
    +
    + +
    +

    Important

    +

    +
    + +
    +

    Hide setup page

    +

    +
    + +
    +
    +
    + +
    +
    + +
    +
    + + + + + +
    +
    + +
    +
    `; } + +export async function handleRegisterPage(request: Request, env: Env, jwtState: JwtSecretState | null): Promise { + const storage = new StorageService(env.DB); + const disabled = await storage.isSetupDisabled(); + if (disabled) { + return new Response(null, { status: 404 }); + } + return htmlResponse(renderRegisterPageHTML(jwtState)); +} \ No newline at end of file diff --git a/src/handlers/setupRegisterPage.ts b/src/handlers/setupRegisterPage.ts index 0312bd6..56524e4 100644 --- a/src/handlers/setupRegisterPage.ts +++ b/src/handlers/setupRegisterPage.ts @@ -2,9 +2,12 @@ import { Env } from '../types'; import { StorageService } from '../services/storage'; import { htmlResponse } from '../utils/response'; -// Registration/setup page HTML (single-file, no external assets) -// Split out from the old monolithic `setup.ts` as requested. -const registerPageHTML = ` +type JwtSecretState = 'missing' | 'default' | 'too_short'; + +function renderRegisterPageHTML(jwtState: JwtSecretState | null): string { + const jwtStateJson = JSON.stringify(jwtState); + + return ` @@ -19,7 +22,6 @@ const registerPageHTML = ` --text: #101828; --muted: #475467; --muted2: #667085; - --accent: #111418; --danger: #b42318; --ok: #027a48; --shadow: 0 16px 44px rgba(16, 24, 40, 0.08); @@ -39,20 +41,48 @@ const registerPageHTML = ` justify-content: center; padding: 40px 24px; } - .shell { width: min(920px, 100%); } + + .shell { width: min(980px, 100%); } + .panel { padding: 40px; border: 1px solid var(--border); background: var(--card); border-radius: var(--radius); box-shadow: var(--shadow); + display: flex; + flex-direction: column; + position: relative; } + + .lang-toggle { + position: absolute; + top: 14px; + right: 14px; + height: 32px; + min-width: 62px; + padding: 0 10px; + border-radius: 10px; + border: 1px solid #d5dae1; + background: #ffffff; + color: #111418; + font-size: 13px; + font-weight: 700; + line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + } + .lang-toggle:hover { background: #f8fafc; } + .top { display: flex; gap: 14px; align-items: center; margin-bottom: 14px; } + .mark { width: 60px; height: 60px; @@ -70,16 +100,40 @@ const registerPageHTML = ` text-transform: uppercase; user-select: none; } + .title { display: flex; flex-direction: column; gap: 4px; } .title h1 { font-size: 30px; margin: 0; letter-spacing: -0.6px; } .title p { margin: 0; color: var(--muted); font-size: 15px; line-height: 1.6; } - h2 { font-size: 22px; margin: 20px 0 14px 0; letter-spacing: -0.3px; } + h2 { font-size: 22px; margin: 10px 0 14px 0; letter-spacing: -0.3px; } + h3 { font-size: 17px; margin: 0 0 10px 0; color: #1d2939; } + .lead { margin: 0; color: #344054; font-size: 16px; line-height: 1.75; } + + .step-container { + position: relative; + height: 442px; + overflow: hidden; + } + .step { + position: absolute; + inset: 0; + opacity: 0; + transform: translateX(10px); + pointer-events: none; + transition: opacity 170ms ease, transform 170ms ease; + overflow-y: auto; + padding-right: 4px; + } + .step.active { + opacity: 1; + transform: translateX(0); + pointer-events: auto; + } .message { display: none; border-radius: 12px; - padding: 14px 14px; + padding: 14px; margin: 0 0 12px 0; font-size: 15px; line-height: 1.45; @@ -99,8 +153,18 @@ const registerPageHTML = ` color: var(--ok); } + .kv { + border-radius: var(--radius2); + border: 1px solid var(--border); + background: #fafbfc; + padding: 18px; + margin-bottom: 14px; + } + .kv p { margin: 0; font-size: 15px; line-height: 1.65; color: var(--muted); } + .kv ul, .kv ol { margin: 8px 0 0 18px; padding: 0; color: var(--muted); line-height: 1.7; } + .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; } - @media (max-width: 540px) { .grid { grid-template-columns: 1fr; } } + @media (max-width: 760px) { .grid { grid-template-columns: 1fr; } } .field { display: flex; flex-direction: column; gap: 7px; } label { font-size: 14px; color: var(--muted); letter-spacing: 0.1px; } @@ -120,36 +184,98 @@ const registerPageHTML = ` border-color: #111418; box-shadow: 0 0 0 5px rgba(17, 20, 24, 0.08); } - .hint { margin: 0; color: var(--muted2); font-size: 14px; line-height: 1.6; } - .actions { margin-top: 16px; display: flex; gap: 10px; align-items: center; } - .primary { - width: 100%; - height: 52px; + .hint { margin: 0; color: var(--muted2); font-size: 14px; line-height: 1.6; } + .muted { color: var(--muted); } + + .compat-wrap { + margin-top: 12px; + border: 1px solid var(--border); + border-radius: 12px; + background: #ffffff; + padding: 10px 12px; + } + .compat-title { + margin: 0 0 8px 0; + font-size: 13px; + font-weight: 700; + color: #344054; + letter-spacing: 0.1px; + } + .compat-grid { + display: flex; + flex-wrap: wrap; + gap: 8px; + } + .compat-chip { + display: inline-flex; + align-items: center; + gap: 6px; + height: 30px; + padding: 0 10px; + border-radius: 999px; + border: 1px solid #d5dae1; + background: #f8fafc; + font-size: 13px; + color: #1d2939; + line-height: 1; + white-space: nowrap; + } + .compat-chip.ok { + border-color: #abefc6; + background: #f0fdf4; + color: #027a48; + } + .compat-chip.na { + border-color: #e4e7ec; + background: #f9fafb; + color: #667085; + } + .compat-dot { + width: 8px; + height: 8px; + border-radius: 999px; + background: currentColor; + opacity: 0.9; + } + + .btn { + height: 46px; + padding: 0 16px; border-radius: 14px; - border: 1px solid #111418; + border: 1px solid #d5dae1; + background: #ffffff; + color: #111418; + font-weight: 700; + font-size: 15px; + display: inline-flex; + align-items: center; + justify-content: center; + text-align: center; + cursor: pointer; + white-space: nowrap; + } + .btn.primary { + border-color: #111418; background: #111418; color: #ffffff; - font-weight: 700; - font-size: 16px; - letter-spacing: 0.1px; - cursor: pointer; - transition: transform 120ms ease, filter 120ms ease; } - .primary:hover { filter: brightness(1.03); } - .primary:active { transform: translateY(1px) scale(0.99); } - .primary:disabled { opacity: 0.55; cursor: not-allowed; transform: none; } + .btn:disabled { opacity: 0.55; cursor: not-allowed; } - .sideCard { display: flex; flex-direction: column; gap: 12px; } - .kv { - border-radius: var(--radius2); - border: 1px solid var(--border); - background: #fafbfc; - padding: 18px; - margin-bottom: 14px; + .mode-tabs { display: inline-flex; border: 1px solid #d5dae1; border-radius: 12px; overflow: hidden; } + .mode-tab { + border: none; + background: #fff; + color: #111418; + padding: 9px 12px; + cursor: pointer; + font-weight: 700; + font-size: 14px; } - .kv h3 { margin: 0 0 10px 0; font-size: 17px; color: #1d2939; } - .kv p { margin: 0; font-size: 15px; line-height: 1.65; color: var(--muted); } + .mode-tab.active { background: #111418; color: #fff; } + + .mode-panel { display: none; margin-top: 12px; } + .mode-panel.active { display: block; } .server { margin-top: 10px; @@ -162,11 +288,45 @@ const registerPageHTML = ` word-break: break-all; color: #111418; } + + .flow-bottom { + margin-top: 14px; + padding: 0 8px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + } + .flow-actions { display: flex; align-items: center; gap: 8px; width: 132px; } + .flow-actions .btn { width: 120px; padding: 0; } + + .dots { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + min-height: 26px; + } + .dot { + width: 9px; + height: 9px; + border-radius: 999px; + background: #cfd5de; + transition: all 120ms ease; + } + .dot.active { + width: 24px; + height: 10px; + border-radius: 999px; + background: #111418; + } + a { color: #175cd3; text-decoration: none; } a:hover { text-decoration: underline; } + .footer { - margin-top: 24px; - padding-top: 18px; + margin-top: 15px; + padding-top: 10px; border-top: 1px solid var(--border); display: flex; justify-content: space-between; @@ -175,12 +335,60 @@ const registerPageHTML = ` font-size: 14px; color: var(--muted2); } - .muted { color: var(--muted); } + + .modal-mask { + position: fixed; + inset: 0; + background: rgba(16, 24, 40, 0.45); + display: none; + align-items: center; + justify-content: center; + z-index: 9999; + padding: 20px; + } + .modal-mask.show { display: flex; } + .modal { + width: min(520px, 100%); + border-radius: 16px; + border: 1px solid var(--border); + background: #ffffff; + box-shadow: 0 24px 56px rgba(16, 24, 40, 0.18); + padding: 20px; + } + .modal h3 { + margin: 0 0 8px 0; + font-size: 18px; + color: #101828; + } + .modal p { + margin: 0; + font-size: 15px; + line-height: 1.7; + color: #475467; + } + .modal-warn { + margin-top: 10px; + border: 1px solid #fecdca; + background: #fff6f5; + color: #b42318; + border-radius: 12px; + padding: 10px 12px; + font-size: 14px; + line-height: 1.6; + } + .modal-actions { + margin-top: 16px; + display: flex; + justify-content: flex-end; + gap: 8px; + }
    + +
    +
    + +
    +
    + + + + +
    +
    + +
    +
    +
    + + `; +} -export async function handleRegisterPage(request: Request, env: Env): Promise { +export async function handleRegisterPage(request: Request, env: Env, jwtState: JwtSecretState | null): Promise { const storage = new StorageService(env.DB); const disabled = await storage.isSetupDisabled(); if (disabled) { return new Response(null, { status: 404 }); } - return htmlResponse(registerPageHTML); + return htmlResponse(renderRegisterPageHTML(jwtState)); } diff --git a/wrangler.toml b/wrangler.toml index a40be04..4537671 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -1,4 +1,4 @@ -name = "nodewarden-test" +name = "nodewarden" main = "src/index.ts" compatibility_date = "2024-01-01"