doc: 更新README,补齐部署指南关键步骤示意图
This commit is contained in:
1 parent
3d5a29bbaf
commit
fa1b0ffaa0
12 files changed
+185
-43
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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.**
|
||||
|
||||
|
||||
[](https://workers.cloudflare.com/)
|
||||
[](https://hub.docker.com/r/nap0o/2fauth-worker)
|
||||
[](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.
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除凭证
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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...')
|
||||
])
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user