doc: 更新README,补齐部署指南关键步骤示意图

This commit is contained in:
nap0o committed 2026-03-11 01:51:25 +08:00
1 parent 3d5a29bbaf
commit fa1b0ffaa0
12 files changed
+185 -43

No files matched your search

+7 -5
View File
@@ -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()
+7 -4
View File
@@ -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
+18 -9
View File
@@ -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`:你的回调地址
<details>
@@ -134,6 +134,16 @@ docker run -d --name 2fauth-worker \
如果你想对代码进行持续部署,或者更精确地管理数据库,建议使用此方式。
#### 1. 准备工作
* 在 `存储和数据库` -> `D1 SQL 数据库` 中创建一个名为 `2fauth-db` 的数据库,并记录其 **Database ID**。
<details>
<summary>点击查看:创建 D1 SQL 数据库的具体步骤</summary>
<img height="500" alt="image" src="https://github.com/user-attachments/assets/80824e1b-73f8-4d13-992c-a51dc4e53308" /><br />
<img height="350" alt="image" src="https://github.com/user-attachments/assets/560c9977-2f89-4135-839d-bdf37208bfdc" /><br />
<img height="350" alt="image" src="https://github.com/user-attachments/assets/25261345-8da6-40de-86b6-a23e910e737d" />
</details>
* 在 Cloudflare 控制面板获取 **API 令牌**(需要 `编辑 Cloudflare Workers` 权限)。
<details>
<summary>点击查看:获取 Cloudflare Worker 部署令牌的具体步骤</summary>
@@ -147,12 +157,9 @@ docker run -d --name 2fauth-worker \
7. 复制生成的令牌
<img width="500" alt="image" src="https://github.com/user-attachments/assets/6487aa6e-e505-4980-aef4-e08172116746" /><br />
<img width="800" alt="image" src="https://github.com/user-attachments/assets/d4c737f7-2d9f-4cfb-a712-b1af416c8ef6" />
</details>
* 在 `存储和数据库` -> `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。将下方链接中的 `<Token>` 和 `<域名>` 替换后,在浏览器中访问一次:
4. 由于本项目是 Serverless 架构,您需要手动注册 Webhook。<br/>
将下方链接中的 `<Token>` 和 `<域名>` 替换后,在浏览器中访问一次:<br/>
`https://api.telegram.org/bot<Token>/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)
---
+26 -17
View File
@@ -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
<details>
@@ -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**.
<details>
<summary>Click to view: Specific steps to create D1 SQL Database</summary>
<img height="500" alt="image" src="https://github.com/user-attachments/assets/80824e1b-73f8-4d13-992c-a51dc4e53308" /><br />
<img height="350" alt="image" src="https://github.com/user-attachments/assets/560c9977-2f89-4135-839d-bdf37208bfdc" /><br />
<img height="350" alt="image" src="https://github.com/user-attachments/assets/25261345-8da6-40de-86b6-a23e910e737d" />
</details>
* Obtain a **Cloudflare API Token** (requires `Edit Cloudflare Workers` permissions).
<details>
<summary>Click to view: Steps to obtain Cloudflare Worker Token</summary>
<summary>Click to view: Steps to obtain Cloudflare Worker Deployment Token</summary>
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
<img width="500" alt="image" src="https://github.com/user-attachments/assets/6487aa6e-e505-4980-aef4-e08172116746" /><br />
<img width="800" alt="image" src="https://github.com/user-attachments/assets/d4c737f7-2d9f-4cfb-a712-b1af416c8ef6" />
</details>
* 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
<details>
<summary>Click to view: Secrets Configuration Example</summary>
@@ -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 `<Token>` and `<Domain>` 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.<br/>
Replace `<Token>` and `<Domain>` in the link below and visit it once in your browser:<br/>
`https://api.telegram.org/bot<Token>/setWebhook?url=https://<Domain>/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.
+13
View File
@@ -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;
@@ -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 };
}
/**
* 删除凭证
*/
+1 -1
View File
@@ -73,7 +73,7 @@ onBeforeUnmount(() => {
<!-- 全局初始加载白屏遮罩 (主要用于 /login 和 /health 等外部页面隐藏身份验证时的跳转闪烁) -->
<div v-show="isAppLoading && route.path !== '/'" class="global-app-shell">
<div class="shell-content">
<el-icon class="is-loading shell-spinner" :size="48" color="#409eff"><Loading /></el-icon>
<el-icon class="is-loading shell-spinner" :size="48"><Loading /></el-icon>
</div>
</div>
@@ -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;
@@ -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 })
});
}
};
@@ -8,7 +8,7 @@
:sub-title="$t('auth.secure_login_sub', { provider: providerName })"
>
<template #extra>
<el-icon class="is-loading" :size="40" color="#409eff"><Loading /></el-icon>
<el-icon class="is-loading" :size="40"><Loading /></el-icon>
</template>
</el-result>
@@ -16,7 +16,7 @@
<!-- 全局加载状态 -->
<div v-if="loading && credentials.length === 0" class="flex-column flex-center min-h-200 text-secondary">
<el-icon class="is-loading mb-20 text-primary" :size="48"><Loading /></el-icon>
<p class="text-16 ls-1">{{ $t('common.loading') }}</p>
<p class="text-16 ls-1">{{ $t('common.loading_data') }}</p>
</div>
<!-- 空状态 -->
@@ -29,8 +29,35 @@
<el-table :data="credentials" style="width: 100%">
<el-table-column :label="$t('security.credential_name')" min-width="150">
<template #default="{ row }">
<div class="flex-align-center">
<span>{{ row.name || $t('security.default_name') }}</span>
<div class="flex-align-center" style="min-height: 32px;">
<template v-if="editingId === row.id">
<el-input
v-model="editName"
size="small"
@keyup.enter="saveEdit(row)"
@keyup.esc="cancelEdit"
@blur="cancelEdit"
ref="editInputRef"
style="width: 150px; margin-right: 8px;"
/>
<el-button
type="success"
link
:icon="Check"
@mousedown.prevent
@click="saveEdit(row)"
/>
</template>
<template v-else>
<span>{{ row.name || $t('security.default_name') }}</span>
<el-button
type="primary"
link
:icon="Edit"
@click="startEdit(row)"
class="ml-2"
/>
</template>
</div>
</template>
</el-table-column>
@@ -89,8 +116,8 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { Plus, Delete, Cpu, CircleCheck, Loading } from '@element-plus/icons-vue'
import { ref, onMounted, nextTick } from 'vue'
import { Plus, Delete, Cpu, CircleCheck, Loading, Edit, Check } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useI18n } from 'vue-i18n'
import { webAuthnService } from '@/features/auth/service/webAuthnService'
@@ -104,6 +131,53 @@ const addForm = ref({
name: ''
})
const editingId = ref(null)
const editName = ref('')
const editInputRef = ref(null)
const startEdit = (row) => {
editingId.value = row.id
editName.value = row.name || t('security.default_name')
nextTick(() => {
if (editInputRef.value) {
if (Array.isArray(editInputRef.value)) {
editInputRef.value[0]?.focus()
} else {
editInputRef.value.focus()
}
}
})
}
const cancelEdit = () => {
editingId.value = null
editName.value = ''
}
const saveEdit = async (row) => {
const newName = editName.value.trim()
if (!newName) {
ElMessage.error(t('common.name_required', '名称不能为空'))
return
}
if (newName === row.name) {
cancelEdit()
return
}
try {
const res = await webAuthnService.updateCredentialName(row.id, newName)
if (res.success) {
ElMessage.success(t('common.success'))
cancelEdit()
await fetchCredentials()
}
} catch (error) {
console.error('Failed to update credential name:', error)
}
}
const fetchCredentials = async () => {
loading.value = true
try {
+1 -1
View File
@@ -16,7 +16,7 @@ export function createAsyncComponent(loader) {
}, [
h('div', {
class: 'is-loading',
style: 'color: var(--el-color-primary); font-size: 24px;'
style: 'font-size: 24px;'
}, 'Loading...')
])
}