diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index ecfbe1e..c211a85 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -97,11 +97,13 @@ jobs: - name: Container Security Scan (Trivy) if: steps.check_docker.outputs.exists == 'true' continue-on-error: true - run: | - VERSION="0.56.1" - echo "Installing Trivy $VERSION..." - curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin "v$VERSION" - trivy config . --format json --output trivy_result.json --severity CRITICAL,HIGH || true + uses: aquasecurity/trivy-action@0.20.0 + with: + scan-type: config + scan-ref: . + format: json + output: trivy_result.json + severity: CRITICAL,HIGH - name: Generate Security Report if: always() diff --git a/Dockerfile b/Dockerfile index e923f8c..197526a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,16 +24,19 @@ FROM node:24-bookworm-slim AS runner WORKDIR /app # We only need backend/node_modules since we're running the backend server which serves static frontend files. -COPY --from=builder /app/backend/node_modules ./backend/node_modules +COPY --from=builder --chown=node:node /app/backend/node_modules ./backend/node_modules # Copy dist files -COPY --from=builder /app/frontend/dist ./frontend/dist -COPY --from=builder /app/backend/dist ./backend/dist -COPY --from=builder /app/backend/schema.sql ./ +COPY --from=builder --chown=node:node /app/frontend/dist ./frontend/dist +COPY --from=builder --chown=node:node /app/backend/dist ./backend/dist +COPY --from=builder --chown=node:node /app/backend/schema.sql ./ ENV NODE_ENV=production ENV PORT=3000 +# Use non-root user +USER node + # Expose the default port EXPOSE 3000 diff --git a/README.md b/README.md index 141b669..15e9494 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ --- -## 📊 功能对比:为什么选择 2FAuth Worker? +## 📊 同类型产品功能对比 | 功能特性 | **2FAuth Worker** (本项目) | Google / MS Auth | Authy | 2FAS / Aegis | 1Password / Bitwarden | | :--- | :--- | :--- | :--- | :--- | :--- | @@ -75,8 +75,8 @@ * `ENCRYPTION_KEY`:32位以上随机密钥。 * `JWT_SECRET`:32位以上随机JWT密钥。 * `OAUTH_ALLOWED_USERS`:你的邮箱@example.com - * `OAUTH_GITHUB_CLIENT_ID`:你的ID - * `OAUTH_GITHUB_CLIENT_SECRET`:你的Secret + * `OAUTH_GITHUB_CLIENT_ID`:你的CLIENT_ID + * `OAUTH_GITHUB_CLIENT_SECRET`:你的CLIENT_SECRET * `OAUTH_GITHUB_REDIRECT_URI`:你的回调地址
@@ -134,6 +134,16 @@ docker run -d --name 2fauth-worker \ 如果你想对代码进行持续部署,或者更精确地管理数据库,建议使用此方式。 #### 1. 准备工作 + +* 在 `存储和数据库` -> `D1 SQL 数据库` 中创建一个名为 `2fauth-db` 的数据库,并记录其 **Database ID**。 +
+点击查看:创建 D1 SQL 数据库的具体步骤 +image
+image
+image +
+ + * 在 Cloudflare 控制面板获取 **API 令牌**(需要 `编辑 Cloudflare Workers` 权限)。
点击查看:获取 Cloudflare Worker 部署令牌的具体步骤 @@ -147,12 +157,9 @@ docker run -d --name 2fauth-worker \ 7. 复制生成的令牌 image
- image
-* 在 `存储和数据库` -> `D1 SQL 数据库` 中创建一个名为 `2fauth-db` 的数据库,并记录其 **Database ID**。 - #### 2. 配置仓库 1. **Fork** 本仓库到你的 GitHub 账号。 2. 前往仓库的 `Settings` -> `Secrets and variables` -> `Actions`。 @@ -221,9 +228,11 @@ docker run -d --name 2fauth-worker \ 1. 在 Telegram 搜索并添加官方机器人 **[@BotFather](https://t.me/BotFather)**,按照提示创建机器人。 2. 记录生成的 **Token** (`OAUTH_TELEGRAM_BOT_TOKEN`) 和 **用户名** (`OAUTH_TELEGRAM_BOT_NAME`)。 3. 请务必向 @BotFather 发送 `/setdomain` 指令,选择您的机器人,并输入您的**应用域名**(不需要 `https://`)。 -4. 由于本项目是 Serverless 架构,您需要手动注册 Webhook。将下方链接中的 `` 和 `<域名>` 替换后,在浏览器中访问一次: +4. 由于本项目是 Serverless 架构,您需要手动注册 Webhook。
+ 将下方链接中的 `` 和 `<域名>` 替换后,在浏览器中访问一次:
`https://api.telegram.org/bot/setWebhook?url=https://<域名>/api/telegram/webhook` -5. 将 `OAUTH_TELEGRAM_BOT_TOKEN`、`OAUTH_TELEGRAM_BOT_NAME` 和 `OAUTH_TELEGRAM_REDIRECT_URI` 填入环境变量。 + +5. 将 `OAUTH_TELEGRAM_BOT_TOKEN`、`OAUTH_TELEGRAM_BOT_NAME` 填入环境变量。 #### 如何配置Cloudflared Access 第三方登录? 1. 进入 **Cloudflare Zero Trust Dashboard** -> **Access** -> **Applications**。 @@ -257,7 +266,7 @@ docker run -d --name 2fauth-worker \ 4. **端到端隔离**:前端生成的敏感操作逻辑经过严格过滤,严禁任何 XSS (跨站脚本) 攻击。 5. **隐私第一**:本项目不包含任何追踪代码、分析工具或第三方统计插件。你的数据只属于你。 -> 📊 [**点击查看由 GitHub Actions 自动生成的实时安全审计报告**](https://github.com/nap0o/2fauth-worker/blob/security-audit/README.md) +> 📊 [**点击查看由 GitHub Actions 自动生成的安全审计报告**](https://github.com/nap0o/2fauth-worker/blob/security-audit/README.md) --- diff --git a/README_EN.md b/README_EN.md index eec6156..f42bd57 100644 --- a/README_EN.md +++ b/README_EN.md @@ -4,6 +4,7 @@ English | [中文](README.md) **High-security, lightweight Two-Factor Authentication (2FA/TOTP) management system supporting both Cloudflare Serverless and Docker private deployment.** + [![Cloudflare Workers](https://img.shields.io/badge/Cloudflare-Workers-F38020?style=flat&logo=cloudflare)](https://workers.cloudflare.com/) [![Docker](https://img.shields.io/badge/Docker-Supported-2496ED?logo=docker&logoColor=white)](https://hub.docker.com/r/nap0o/2fauth-worker) [![GitHub Actions](https://img.shields.io/badge/GitHub-Actions-181717?style=flat&logo=github)](https://github.com/nap0o/2fauth-worker/actions) @@ -35,7 +36,7 @@ Whether you're leveraging Cloudflare's free tier or deploying on your own NAS/mi --- -## 📊 Feature Comparison: Why Choose 2FAuth Worker? +## 📊 Product Feature Comparison | Feature | **2FAuth Worker** (This Project) | Google / MS Auth | Authy | 2FAS / Aegis | 1Password / Bitwarden | | :--- | :--- | :--- | :--- | :--- | :--- | @@ -74,8 +75,8 @@ The most hassle-free way. No server required; Cloudflare hosts it for you for fr * `ENCRYPTION_KEY`: A random key with at least 32 characters. * `JWT_SECRET`: A random JWT secret with at least 32 characters. * `OAUTH_ALLOWED_USERS`: your_email@example.com - * `OAUTH_GITHUB_CLIENT_ID`: Your Client ID - * `OAUTH_GITHUB_CLIENT_SECRET`: Your Client Secret + * `OAUTH_GITHUB_CLIENT_ID`: Your CLIENT_ID + * `OAUTH_GITHUB_CLIENT_SECRET`: Your CLIENT_SECRET * `OAUTH_GITHUB_REDIRECT_URI`: Your Callback URL
@@ -133,9 +134,18 @@ docker run -d --name 2fauth-worker \ Choose this for continuous deployment or more precise database management. #### 1. Prerequisites + +* Create a D1 database named `2fauth-db` in `Storage & Databases` -> `D1 SQL Database` and record its **Database ID**. +
+Click to view: Specific steps to create D1 SQL Database +image
+image
+image +
+ * Obtain a **Cloudflare API Token** (requires `Edit Cloudflare Workers` permissions).
-Click to view: Steps to obtain Cloudflare Worker Token +Click to view: Steps to obtain Cloudflare Worker Deployment Token 1. Log in to Cloudflare Dashboard 2. [Go to API Tokens](https://dash.cloudflare.com/profile/api-tokens) @@ -146,12 +156,9 @@ Choose this for continuous deployment or more precise database management. 7. Copy the generated token image
- image
-* Create a D1 database named `2fauth-db` in `Storage & Databases` -> `D1` and record its **Database ID**. - #### 2. Repository Configuration 1. **Fork** this repository. 2. Go to `Settings` -> `Secrets and variables` -> `Actions`. @@ -163,9 +170,9 @@ Choose this for continuous deployment or more precise database management. * `ENCRYPTION_KEY`: A 32+ character random key. * `JWT_SECRET`: A 32+ character random JWT secret. * `OAUTH_ALLOWED_USERS`: your_email@example.com - * `OAUTH_GITHUB_CLIENT_ID`: your_id - * `OAUTH_GITHUB_CLIENT_SECRET`: your_secret - * `OAUTH_GITHUB_REDIRECT_URI`: your_callback_url + * `OAUTH_GITHUB_CLIENT_ID`: Your ID + * `OAUTH_GITHUB_CLIENT_SECRET`: Your Secret + * `OAUTH_GITHUB_REDIRECT_URI`: Your Callback URL
Click to view: Secrets Configuration Example @@ -192,16 +199,17 @@ Regardless of the deployment method, these parameters are critical: | `JWT_SECRET` | Auth Token Secret | Requirement: 32+ characters. | | `OAUTH_ALLOWED_USERS` | **Whitelist**: Access Control | Emails or Telegram IDs, separated by commas. | -### Variable Mapping for Different Providers: + +### Variable Mapping for Different Providers (Choose at least one): | Provider | Client ID Variable | Client Secret Variable | Redirect URI (Example: `https://xxx.dev/oauth/callback`) | | :--- | :--- | :--- | :--- | | **GitHub** | `OAUTH_GITHUB_CLIENT_ID` | `OAUTH_GITHUB_CLIENT_SECRET` | `OAUTH_GITHUB_REDIRECT_URI` | | **Telegram** | `OAUTH_TELEGRAM_BOT_NAME` | `OAUTH_TELEGRAM_BOT_TOKEN` | *Not required for Telegram* | -| **Google** | `OAUTH_GOOGLE_CLIENT_ID` | `OAUTH_GOOGLE_CLIENT_SECRET" | `OAUTH_GOOGLE_REDIRECT_URI` | +| **Google** | `OAUTH_GOOGLE_CLIENT_ID` | `OAUTH_GOOGLE_CLIENT_SECRET` | `OAUTH_GOOGLE_REDIRECT_URI` | | **Cloudflare Access** | `OAUTH_CLOUDFLARE_CLIENT_ID` | `OAUTH_CLOUDFLARE_CLIENT_SECRET` | `OAUTH_CLOUDFLARE_REDIRECT_URI` (requires `OAUTH_CLOUDFLARE_ORG_DOMAIN`) | -| **Gitee** | `OAUTH_GITEE_CLIENT_ID` | `OAUTH_GITEE_CLIENT_SECRET" | `OAUTH_GITEE_REDIRECT_URI` | -| **NodeLoc** | `OAUTH_NODELOC_CLIENT_ID` | `OAUTH_NODELOC_CLIENT_SECRET" | `OAUTH_NODELOC_REDIRECT_URI` | +| **Gitee** | `OAUTH_GITEE_CLIENT_ID` | `OAUTH_GITEE_CLIENT_SECRET` | `OAUTH_GITEE_REDIRECT_URI` | +| **NodeLoc** | `OAUTH_NODELOC_CLIENT_ID` | `OAUTH_NODELOC_CLIENT_SECRET` | `OAUTH_NODELOC_REDIRECT_URI` | --- @@ -219,9 +227,11 @@ Regardless of the deployment method, these parameters are critical: 1. Search and add **[@BotFather](https://t.me/BotFather)** on Telegram, following prompts to create a bot. 2. Record the **Token** (`OAUTH_TELEGRAM_BOT_TOKEN`) and **Username** (`OAUTH_TELEGRAM_BOT_NAME`). 3. You MUST send the `/setdomain` command to @BotFather, select your bot, and enter your **application domain** (without `https://`). -4. Since this is a Serverless architecture, you need to manually register the Webhook. Replace `` and `` in the link below and visit it once in your browser: +4. Since this is a Serverless architecture, you need to manually register the Webhook.
+ Replace `` and `` in the link below and visit it once in your browser:
`https://api.telegram.org/bot/setWebhook?url=https:///api/telegram/webhook` -5. Fill `OAUTH_TELEGRAM_BOT_TOKEN`, `OAUTH_TELEGRAM_BOT_NAME` and `OAUTH_TELEGRAM_REDIRECT_URI` into environment variables. + +5. Fill `OAUTH_TELEGRAM_BOT_TOKEN`, `OAUTH_TELEGRAM_BOT_NAME` into environment variables. #### How to configure Cloudflare Access Login? 1. Enter **Cloudflare Zero Trust Dashboard** -> **Access** -> **Applications**. @@ -281,4 +291,3 @@ npm run dev ## 📄 License This project is open-sourced under the [GNU AGPL v3](LICENSE) license. Since this software involves 2FA security and network services, we maintain open-source fairness: if you run a modified version of this project on a server and provide services to the public, you must open your source code to the users. - diff --git a/backend/src/features/auth/authRoutes.ts b/backend/src/features/auth/authRoutes.ts index 9e21c4e..ae2197a 100644 --- a/backend/src/features/auth/authRoutes.ts +++ b/backend/src/features/auth/authRoutes.ts @@ -216,4 +216,17 @@ auth.delete('/webauthn/credentials/:id', authMiddleware, async (c) => { return c.json(result); }); +// 7. 更新凭证 (需已登录) +auth.put('/webauthn/credentials/:id', authMiddleware, async (c) => { + const credentialId = c.req.param('id'); + const body = await c.req.json(); + const { name } = body; + if (!name) { + throw new AppError('credential_name_required', 400); + } + const service = getWebAuthnService(c); + const result = await service.updateCredentialName(credentialId, name); + return c.json(result); +}); + export default auth; \ No newline at end of file diff --git a/backend/src/features/auth/webAuthnService.ts b/backend/src/features/auth/webAuthnService.ts index a2deb9a..eda60be 100644 --- a/backend/src/features/auth/webAuthnService.ts +++ b/backend/src/features/auth/webAuthnService.ts @@ -194,6 +194,16 @@ export class WebAuthnService { return results; } + /** + * 更新凭证名称 + */ + async updateCredentialName(credentialId: string, name: string) { + await this.env.DB.update(schema.authPasskeys) + .set({ name }) + .where(eq(schema.authPasskeys.credentialId, credentialId)); + return { success: true }; + } + /** * 删除凭证 */ diff --git a/frontend/src/app/app.vue b/frontend/src/app/app.vue index 0c23112..991d35b 100644 --- a/frontend/src/app/app.vue +++ b/frontend/src/app/app.vue @@ -73,7 +73,7 @@ onBeforeUnmount(() => {
- +
diff --git a/frontend/src/app/styles/modules/layout/containers.css b/frontend/src/app/styles/modules/layout/containers.css index 6f3c615..5507a21 100644 --- a/frontend/src/app/styles/modules/layout/containers.css +++ b/frontend/src/app/styles/modules/layout/containers.css @@ -98,6 +98,18 @@ main { scrollbar-gutter: stable; } +.is-loading { + margin-bottom: 20px; +} + +.el-icon.is-loading { + color: var(--el-color-primary) !important; +} + +div.is-loading { + color: var(--el-color-primary); +} + @media (max-width: 768px) { .main-content { overflow: visible !important; diff --git a/frontend/src/features/auth/service/webAuthnService.js b/frontend/src/features/auth/service/webAuthnService.js index c6b3bf5..e424d73 100644 --- a/frontend/src/features/auth/service/webAuthnService.js +++ b/frontend/src/features/auth/service/webAuthnService.js @@ -66,5 +66,15 @@ export const webAuthnService = { */ async deleteCredential(id) { return await request(`/api/oauth/webauthn/credentials/${id}`, { method: 'DELETE' }); + }, + + /** + * 修改凭证名称 + */ + async updateCredentialName(id, name) { + return await request(`/api/oauth/webauthn/credentials/${id}`, { + method: 'PUT', + body: JSON.stringify({ name }) + }); } }; diff --git a/frontend/src/features/auth/views/oauthCallback.vue b/frontend/src/features/auth/views/oauthCallback.vue index 05c37d0..45069af 100644 --- a/frontend/src/features/auth/views/oauthCallback.vue +++ b/frontend/src/features/auth/views/oauthCallback.vue @@ -8,7 +8,7 @@ :sub-title="$t('auth.secure_login_sub', { provider: providerName })" > diff --git a/frontend/src/features/auth/views/securitySettings.vue b/frontend/src/features/auth/views/securitySettings.vue index 03e6d7c..fa9a8b0 100644 --- a/frontend/src/features/auth/views/securitySettings.vue +++ b/frontend/src/features/auth/views/securitySettings.vue @@ -16,7 +16,7 @@
-

{{ $t('common.loading') }}

+

{{ $t('common.loading_data') }}

@@ -29,8 +29,35 @@ @@ -89,8 +116,8 @@