feat(cloudflare): 支持 API Token 连接验证并发布 v1.6.7
This commit is contained in:
@@ -2,6 +2,24 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [v1.6.7] - 2026-09-28
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
|
||||||
|
- **API Token 连接验证**:应用配置的 Token 区域拆分为两个输入:
|
||||||
|
- 隧道 Token(Tunnel Token):用于 cloudflared 运行
|
||||||
|
- API Token(`cfat_...`):用于连接验证,调用 Cloudflare 官方 `GET /user/tokens/verify` 接口
|
||||||
|
- 「测试连接」优先使用 API Token;未配置 API Token 时回退到隧道 Token 的临时连接测试;输入以 `cfat_` 开头时也会自动走 API 验证
|
||||||
|
- 新增 `cf_settings.api_token_enc` 字段(迁移 `032`),API Token 独立加密存储与掩码回显
|
||||||
|
|
||||||
|
### 变更
|
||||||
|
|
||||||
|
- 版本号提升到 1.6.7;`AgentVersion` 保持 **0.14.1**
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
|
||||||
|
- `go test ./...`、`go vet ./...`、`npm run typecheck`、`npm test`、`npm run build` 通过
|
||||||
|
|
||||||
## [v1.6.6] - 2026-09-28
|
## [v1.6.6] - 2026-09-28
|
||||||
|
|
||||||
### 修复
|
### 修复
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
# Fonu 文档索引
|
# Fonu 文档索引
|
||||||
|
|
||||||
> 维护约定:新增方案 / 审查文档时在下方登记;方案落地后在「状态」列标注实施结果,并给已被推翻的描述加时效说明。
|
> 维护约定:新增方案 / 审查文档时在下方登记;方案落地后在「状态」列标注实施结果,并给已被推翻的描述加时效说明。
|
||||||
> 最近更新:2026-09-28(v1.6.6)
|
> 最近更新:2026-09-28(v1.6.7)
|
||||||
|
|
||||||
## 方案与设计(本目录)
|
## 方案与设计(本目录)
|
||||||
|
|
||||||
|
|||||||
@@ -238,16 +238,16 @@ FONU_VERSION=<旧版本> docker compose up -d --no-deps --force-recreate fonu
|
|||||||
|
|
||||||
## 10. 在线升级验证
|
## 10. 在线升级验证
|
||||||
|
|
||||||
示例页面用于验证升级结果。v1.6.6 的验证流程:
|
示例页面用于验证升级结果。v1.6.7 的验证流程:
|
||||||
|
|
||||||
1. 确认当前线上实例为 v1.3.0,且 `fonu-updater` sidecar 已运行。
|
1. 确认当前线上实例为 v1.3.0,且 `fonu-updater` sidecar 已运行。
|
||||||
2. 确认远端 `main` 分支的 `VERSION` 为 `1.6.6`。
|
2. 确认远端 `main` 分支的 `VERSION` 为 `1.6.7`。
|
||||||
3. 在侧栏看到「一键升级」后点击并确认。
|
3. 在侧栏看到「一键升级」后点击并确认。
|
||||||
4. 等待升级弹窗显示成功,页面自动刷新。
|
4. 等待升级弹窗显示成功,页面自动刷新。
|
||||||
5. 打开侧栏「示例页面」,确认:
|
5. 打开侧栏「示例页面」,确认:
|
||||||
- 页面路由为 `/example`;
|
- 页面路由为 `/example`;
|
||||||
- 页面显示的版本为 `1.6.6`;
|
- 页面显示的版本为 `1.6.7`;
|
||||||
- 页面显示验证标识 `v1.6.6-example-page`;
|
- 页面显示验证标识 `v1.6.7-example-page`;
|
||||||
- 页面显示独立的「本次升级验证」区块;
|
- 页面显示独立的「本次升级验证」区块;
|
||||||
- 「升级验证」显示为「通过」。
|
- 「升级验证」显示为「通过」。
|
||||||
|
|
||||||
|
|||||||
@@ -192,12 +192,13 @@ func (h *CloudflareHandler) SaveAppSettings(w http.ResponseWriter, r *http.Reque
|
|||||||
func (h *CloudflareHandler) TestToken(w http.ResponseWriter, r *http.Request) {
|
func (h *CloudflareHandler) TestToken(w http.ResponseWriter, r *http.Request) {
|
||||||
var request struct {
|
var request struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&request); err != nil {
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&request); err != nil {
|
||||||
writeError(r, w, http.StatusBadRequest, "请求格式无效")
|
writeError(r, w, http.StatusBadRequest, "请求格式无效")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
message, err := h.svc.TestToken(r.Context(), request.Token)
|
message, err := h.svc.TestToken(r.Context(), request.Token, request.Kind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(r, w, http.StatusBadGateway, err.Error())
|
writeError(r, w, http.StatusBadGateway, err.Error())
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ type CreateInput struct {
|
|||||||
type AppSettings struct {
|
type AppSettings struct {
|
||||||
DefaultTokenConfigured bool `json:"default_token_configured"`
|
DefaultTokenConfigured bool `json:"default_token_configured"`
|
||||||
DefaultTokenMasked string `json:"default_token_masked"`
|
DefaultTokenMasked string `json:"default_token_masked"`
|
||||||
|
APITokenConfigured bool `json:"api_token_configured"`
|
||||||
|
APITokenMasked string `json:"api_token_masked"`
|
||||||
Mirror string `json:"mirror"`
|
Mirror string `json:"mirror"`
|
||||||
Network NetworkSettings `json:"network"`
|
Network NetworkSettings `json:"network"`
|
||||||
}
|
}
|
||||||
@@ -81,6 +83,7 @@ type AppSettings struct {
|
|||||||
// AppSettingsInput 是保存应用配置的入参;Token 留空表示保留原值。
|
// AppSettingsInput 是保存应用配置的入参;Token 留空表示保留原值。
|
||||||
type AppSettingsInput struct {
|
type AppSettingsInput struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
|
APIToken string `json:"api_token"`
|
||||||
Mirror string `json:"mirror"`
|
Mirror string `json:"mirror"`
|
||||||
Network NetworkSettings `json:"network"`
|
Network NetworkSettings `json:"network"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ package cloudflared
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -211,6 +214,12 @@ func (s *Service) AppSettings(ctx context.Context) (AppSettings, error) {
|
|||||||
out.DefaultTokenMasked = MaskSecret(plain)
|
out.DefaultTokenMasked = MaskSecret(plain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if row.APITokenEnc != "" {
|
||||||
|
out.APITokenConfigured = true
|
||||||
|
if plain, err := s.secretBox.Decrypt(row.APITokenEnc); err == nil {
|
||||||
|
out.APITokenMasked = MaskSecret(plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,6 +239,13 @@ func (s *Service) SaveAppSettings(ctx context.Context, in AppSettingsInput) (App
|
|||||||
return AppSettings{}, err
|
return AppSettings{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
apiTokenEnc := row.APITokenEnc
|
||||||
|
if apiToken := strings.TrimSpace(in.APIToken); apiToken != "" {
|
||||||
|
apiTokenEnc, err = s.secretBox.Encrypt(apiToken)
|
||||||
|
if err != nil {
|
||||||
|
return AppSettings{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
mirror := strings.TrimSpace(in.Mirror)
|
mirror := strings.TrimSpace(in.Mirror)
|
||||||
if mirror == "" {
|
if mirror == "" {
|
||||||
mirror = row.Mirror
|
mirror = row.Mirror
|
||||||
@@ -241,7 +257,7 @@ func (s *Service) SaveAppSettings(ctx context.Context, in AppSettingsInput) (App
|
|||||||
if strings.TrimSpace(network.TransportProtocol) == "" {
|
if strings.TrimSpace(network.TransportProtocol) == "" {
|
||||||
network = row.Network
|
network = row.Network
|
||||||
}
|
}
|
||||||
if err := s.store.SaveSettings(ctx, tokenEnc, mirror, network); err != nil {
|
if err := s.store.SaveSettings(ctx, tokenEnc, apiTokenEnc, mirror, network); err != nil {
|
||||||
return AppSettings{}, err
|
return AppSettings{}, err
|
||||||
}
|
}
|
||||||
return s.AppSettings(ctx)
|
return s.AppSettings(ctx)
|
||||||
@@ -277,8 +293,61 @@ func (s *Service) resolveToken(ctx context.Context, input string) (string, error
|
|||||||
return plain, nil
|
return plain, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VerifyAPIToken 调用 Cloudflare 官方 tokens/verify 接口验证 API Token(cfat_...)。
|
||||||
|
func (s *Service) VerifyAPIToken(ctx context.Context, input string) (string, error) {
|
||||||
|
token := strings.TrimSpace(input)
|
||||||
|
if token == "" {
|
||||||
|
row, err := s.store.GetSettingsRaw(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if row.APITokenEnc == "" {
|
||||||
|
return "", fmt.Errorf("请填写 API Token(cfat_...),或先在应用配置中保存")
|
||||||
|
}
|
||||||
|
token, err = s.secretBox.Decrypt(row.APITokenEnc)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("API Token 解密失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.cloudflare.com/client/v4/user/tokens/verify", nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
client := &http.Client{Timeout: 15 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("请求 Cloudflare API 失败: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||||
|
var parsed struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Result struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
} `json:"result"`
|
||||||
|
Errors []struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
} `json:"errors"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &parsed); err != nil {
|
||||||
|
return "", fmt.Errorf("Cloudflare API 响应格式无效(HTTP %d)", resp.StatusCode)
|
||||||
|
}
|
||||||
|
if !parsed.Success || parsed.Result.Status != "active" {
|
||||||
|
message := "API Token 无效或未激活"
|
||||||
|
if len(parsed.Errors) > 0 && strings.TrimSpace(parsed.Errors[0].Message) != "" {
|
||||||
|
message = parsed.Errors[0].Message
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("%s", message)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("API Token 有效(状态:%s)", parsed.Result.Status), nil
|
||||||
|
}
|
||||||
|
|
||||||
// TestToken 用 cloudflared 临时启动一次连接,验证 token 能否注册到 Cloudflare 边缘。
|
// TestToken 用 cloudflared 临时启动一次连接,验证 token 能否注册到 Cloudflare 边缘。
|
||||||
func (s *Service) TestToken(ctx context.Context, input string) (string, error) {
|
func (s *Service) TestToken(ctx context.Context, input, kind string) (string, error) {
|
||||||
|
if kind == "api" || strings.HasPrefix(strings.TrimSpace(input), "cfat_") {
|
||||||
|
return s.VerifyAPIToken(ctx, input)
|
||||||
|
}
|
||||||
token, err := s.resolveToken(ctx, input)
|
token, err := s.resolveToken(ctx, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|||||||
@@ -98,34 +98,36 @@ func (s *Store) SetRuntime(ctx context.Context, id int64, status, lastError stri
|
|||||||
// settingsRow 是应用配置的原始行;TokenEnc 只在 Service 内部解密,不直接回给前端。
|
// settingsRow 是应用配置的原始行;TokenEnc 只在 Service 内部解密,不直接回给前端。
|
||||||
type settingsRow struct {
|
type settingsRow struct {
|
||||||
TokenEnc string
|
TokenEnc string
|
||||||
|
APITokenEnc string
|
||||||
Mirror string
|
Mirror string
|
||||||
Network NetworkSettings
|
Network NetworkSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetSettingsRaw(ctx context.Context) (settingsRow, error) {
|
func (s *Store) GetSettingsRaw(ctx context.Context) (settingsRow, error) {
|
||||||
var tokenEnc, mirror, networkJSON string
|
var tokenEnc, apiTokenEnc, mirror, networkJSON string
|
||||||
err := s.db.QueryRowContext(ctx, `
|
err := s.db.QueryRowContext(ctx, `
|
||||||
SELECT COALESCE(default_token_enc, ''), COALESCE(mirror, 'official'), COALESCE(network_json, '{}')
|
SELECT COALESCE(default_token_enc, ''), COALESCE(api_token_enc, ''),
|
||||||
|
COALESCE(mirror, 'official'), COALESCE(network_json, '{}')
|
||||||
FROM cf_settings WHERE id = 1
|
FROM cf_settings WHERE id = 1
|
||||||
`).Scan(&tokenEnc, &mirror, &networkJSON)
|
`).Scan(&tokenEnc, &apiTokenEnc, &mirror, &networkJSON)
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return settingsRow{Mirror: "official", Network: DefaultNetworkSettings()}, nil
|
return settingsRow{Mirror: "official", Network: DefaultNetworkSettings()}, nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return settingsRow{}, err
|
return settingsRow{}, err
|
||||||
}
|
}
|
||||||
row := settingsRow{TokenEnc: tokenEnc, Mirror: mirror, Network: DefaultNetworkSettings()}
|
row := settingsRow{TokenEnc: tokenEnc, APITokenEnc: apiTokenEnc, Mirror: mirror, Network: DefaultNetworkSettings()}
|
||||||
_ = json.Unmarshal([]byte(networkJSON), &row.Network)
|
_ = json.Unmarshal([]byte(networkJSON), &row.Network)
|
||||||
return row, nil
|
return row, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) SaveSettings(ctx context.Context, tokenEnc, mirror string, network NetworkSettings) error {
|
func (s *Store) SaveSettings(ctx context.Context, tokenEnc, apiTokenEnc, mirror string, network NetworkSettings) error {
|
||||||
networkJSON, _ := json.Marshal(network)
|
networkJSON, _ := json.Marshal(network)
|
||||||
_, err := s.db.ExecContext(ctx, `
|
_, err := s.db.ExecContext(ctx, `
|
||||||
UPDATE cf_settings
|
UPDATE cf_settings
|
||||||
SET default_token_enc = ?, mirror = ?, network_json = ?, updated_at = datetime('now')
|
SET default_token_enc = ?, api_token_enc = ?, mirror = ?, network_json = ?, updated_at = datetime('now')
|
||||||
WHERE id = 1
|
WHERE id = 1
|
||||||
`, tokenEnc, mirror, string(networkJSON))
|
`, tokenEnc, apiTokenEnc, mirror, string(networkJSON))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- Cloudflare API Token(cfat_...):用于连接验证与后续云端同步,与隧道 Token 分开存储。
|
||||||
|
ALTER TABLE cf_settings ADD COLUMN api_token_enc TEXT NOT NULL DEFAULT '';
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "fonu-web",
|
"name": "fonu-web",
|
||||||
"version": "1.6.6",
|
"version": "1.6.7",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "fonu-web",
|
"name": "fonu-web",
|
||||||
"version": "1.6.6",
|
"version": "1.6.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/commands": "^6.11.1",
|
"@codemirror/commands": "^6.11.1",
|
||||||
"@codemirror/lang-javascript": "^6.2.5",
|
"@codemirror/lang-javascript": "^6.2.5",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "fonu-web",
|
"name": "fonu-web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.6.6",
|
"version": "1.6.7",
|
||||||
"packageManager": "npm@10.9.4",
|
"packageManager": "npm@10.9.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -512,9 +512,9 @@ export const api = {
|
|||||||
getCfSettings: () => request<CfAppSettings>('/api/cloudflare/settings'),
|
getCfSettings: () => request<CfAppSettings>('/api/cloudflare/settings'),
|
||||||
saveCfSettings: (payload: CfAppSettingsInput) =>
|
saveCfSettings: (payload: CfAppSettingsInput) =>
|
||||||
request<CfAppSettings>('/api/cloudflare/settings', { method: 'PUT', body: JSON.stringify(payload) }),
|
request<CfAppSettings>('/api/cloudflare/settings', { method: 'PUT', body: JSON.stringify(payload) }),
|
||||||
testCfToken: (token: string) =>
|
testCfToken: (token: string, kind: 'tunnel' | 'api') =>
|
||||||
request<{ ok: boolean; message: string }>('/api/cloudflare/settings/test', {
|
request<{ ok: boolean; message: string }>('/api/cloudflare/settings/test', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ token }),
|
body: JSON.stringify({ token, kind }),
|
||||||
}, 60_000),
|
}, 60_000),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1102,12 +1102,15 @@ export interface CfTunnelPayload {
|
|||||||
export interface CfAppSettings {
|
export interface CfAppSettings {
|
||||||
default_token_configured: boolean
|
default_token_configured: boolean
|
||||||
default_token_masked: string
|
default_token_masked: string
|
||||||
|
api_token_configured: boolean
|
||||||
|
api_token_masked: string
|
||||||
mirror: string
|
mirror: string
|
||||||
network: CfNetworkSettings
|
network: CfNetworkSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CfAppSettingsInput {
|
export interface CfAppSettingsInput {
|
||||||
token: string
|
token: string
|
||||||
|
api_token: string
|
||||||
mirror: string
|
mirror: string
|
||||||
network: CfNetworkSettings
|
network: CfNetworkSettings
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,12 +124,20 @@
|
|||||||
|
|
||||||
<n-divider title-placement="left">Token 配置</n-divider>
|
<n-divider title-placement="left">Token 配置</n-divider>
|
||||||
<n-form label-placement="top">
|
<n-form label-placement="top">
|
||||||
<n-form-item label="默认 Token">
|
<n-form-item label="隧道 Token(Tunnel Token)">
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="settingsForm.token"
|
v-model:value="settingsForm.token"
|
||||||
type="password"
|
type="password"
|
||||||
show-password-on="click"
|
show-password-on="click"
|
||||||
:placeholder="appSettings.default_token_configured ? `已配置 ${appSettings.default_token_masked},留空保留` : '粘贴 Cloudflare 隧道 token'"
|
:placeholder="appSettings.default_token_configured ? `已配置 ${appSettings.default_token_masked},留空保留` : '粘贴 Cloudflare 隧道 token(eyJ...)'"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="API Token(可选,用于连接验证)">
|
||||||
|
<n-input
|
||||||
|
v-model:value="settingsForm.api_token"
|
||||||
|
type="password"
|
||||||
|
show-password-on="click"
|
||||||
|
:placeholder="appSettings.api_token_configured ? `已配置 ${appSettings.api_token_masked},留空保留` : 'cfat_...'"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
@@ -140,7 +148,7 @@
|
|||||||
<n-alert v-if="tokenTestResult" :type="tokenTestOk ? 'success' : 'error'" :bordered="false" class="cf-mode-alert">
|
<n-alert v-if="tokenTestResult" :type="tokenTestOk ? 'success' : 'error'" :bordered="false" class="cf-mode-alert">
|
||||||
{{ tokenTestResult }}
|
{{ tokenTestResult }}
|
||||||
</n-alert>
|
</n-alert>
|
||||||
<p class="cf-hint">保存后,新建隧道时可以留空 token,直接使用这里的默认值。</p>
|
<p class="cf-hint">隧道 Token 用于 cloudflared 运行;API Token 用于验证 Cloudflare 账号连接(格式 cfat_...)。测试连接优先使用 API Token。</p>
|
||||||
|
|
||||||
<n-divider title-placement="left">默认网络设置</n-divider>
|
<n-divider title-placement="left">默认网络设置</n-divider>
|
||||||
<n-form label-placement="top">
|
<n-form label-placement="top">
|
||||||
@@ -188,10 +196,12 @@ const tokenTestOk = ref(false)
|
|||||||
const appSettings = ref<CfAppSettings>({
|
const appSettings = ref<CfAppSettings>({
|
||||||
default_token_configured: false,
|
default_token_configured: false,
|
||||||
default_token_masked: '',
|
default_token_masked: '',
|
||||||
|
api_token_configured: false,
|
||||||
|
api_token_masked: '',
|
||||||
mirror: 'official',
|
mirror: 'official',
|
||||||
network: emptyNetwork(),
|
network: emptyNetwork(),
|
||||||
})
|
})
|
||||||
const settingsForm = ref<CfAppSettingsInput>({ token: '', mirror: 'official', network: emptyNetwork() })
|
const settingsForm = ref<CfAppSettingsInput>({ token: '', api_token: '', mirror: 'official', network: emptyNetwork() })
|
||||||
const editorVisible = ref(false)
|
const editorVisible = ref(false)
|
||||||
const detailVisible = ref(false)
|
const detailVisible = ref(false)
|
||||||
const editing = ref<CfTunnel | null>(null)
|
const editing = ref<CfTunnel | null>(null)
|
||||||
@@ -409,7 +419,7 @@ async function loadAppSettings() {
|
|||||||
try {
|
try {
|
||||||
const result = await api.getCfSettings()
|
const result = await api.getCfSettings()
|
||||||
appSettings.value = result
|
appSettings.value = result
|
||||||
settingsForm.value = { token: '', mirror: result.mirror || 'official', network: { ...result.network } }
|
settingsForm.value = { token: '', api_token: '', mirror: result.mirror || 'official', network: { ...result.network } }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error instanceof Error ? error.message : '读取应用配置失败')
|
message.error(error instanceof Error ? error.message : '读取应用配置失败')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -420,8 +430,13 @@ async function loadAppSettings() {
|
|||||||
async function testToken() {
|
async function testToken() {
|
||||||
tokenTesting.value = true
|
tokenTesting.value = true
|
||||||
tokenTestResult.value = ''
|
tokenTestResult.value = ''
|
||||||
|
const apiToken = (settingsForm.value.api_token || '').trim()
|
||||||
|
const useAPI = apiToken !== '' || appSettings.value.api_token_configured
|
||||||
try {
|
try {
|
||||||
const result = await api.testCfToken(settingsForm.value.token)
|
const result = await api.testCfToken(
|
||||||
|
useAPI ? apiToken : (settingsForm.value.token || ''),
|
||||||
|
useAPI ? 'api' : 'tunnel',
|
||||||
|
)
|
||||||
tokenTestOk.value = true
|
tokenTestOk.value = true
|
||||||
tokenTestResult.value = result.message || '连接成功'
|
tokenTestResult.value = result.message || '连接成功'
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -437,7 +452,7 @@ async function saveSettings() {
|
|||||||
try {
|
try {
|
||||||
const result = await api.saveCfSettings(settingsForm.value)
|
const result = await api.saveCfSettings(settingsForm.value)
|
||||||
appSettings.value = result
|
appSettings.value = result
|
||||||
settingsForm.value = { token: '', mirror: result.mirror, network: { ...result.network } }
|
settingsForm.value = { token: '', api_token: '', mirror: result.mirror, network: { ...result.network } }
|
||||||
message.success('应用配置已保存')
|
message.success('应用配置已保存')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error instanceof Error ? error.message : '保存应用配置失败')
|
message.error(error instanceof Error ? error.message : '保存应用配置失败')
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<n-alert type="success" :bordered="false" class="example-alert" title="v1.6.6 示例页面验证标识已生效">
|
<n-alert type="success" :bordered="false" class="example-alert" title="v1.6.7 示例页面验证标识已生效">
|
||||||
当前页面由 v{{ version }} 提供,验证标识为 <span class="mono">{{ verificationMarker }}</span>。
|
当前页面由 v{{ version }} 提供,验证标识为 <span class="mono">{{ verificationMarker }}</span>。
|
||||||
如果你是在升级后首次看到这行内容,说明前端构建、镜像重建和服务重启均已成功。
|
如果你是在升级后首次看到这行内容,说明前端构建、镜像重建和服务重启均已成功。
|
||||||
</n-alert>
|
</n-alert>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<strong class="example-proof__value">{{ verificationRound }}</strong>
|
<strong class="example-proof__value">{{ verificationRound }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="example-proof__hint">
|
<div class="example-proof__hint">
|
||||||
看到这个新区块,说明前端资源已经从旧版本切换到 v1.6.6。
|
看到这个新区块,说明前端资源已经从旧版本切换到 v1.6.7。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -33,16 +33,16 @@
|
|||||||
<StatCard label="本次验证标识" :value="verificationMarker" sub="用于区分升级前后的前端资源" tone="blue" value-small>
|
<StatCard label="本次验证标识" :value="verificationMarker" sub="用于区分升级前后的前端资源" tone="blue" value-small>
|
||||||
<template #icon><n-icon :component="FlashOutline" /></template>
|
<template #icon><n-icon :component="FlashOutline" /></template>
|
||||||
</StatCard>
|
</StatCard>
|
||||||
<StatCard label="页面状态" value="已更新" sub="v1.6.6 前端资源已加载" tone="teal">
|
<StatCard label="页面状态" value="已更新" sub="v1.6.7 前端资源已加载" tone="teal">
|
||||||
<template #icon><n-icon :component="CheckmarkCircleOutline" /></template>
|
<template #icon><n-icon :component="CheckmarkCircleOutline" /></template>
|
||||||
</StatCard>
|
</StatCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FonuCard title="本次示例页变化" subtitle="用于确认升级后的页面内容确实发生变化">
|
<FonuCard title="本次示例页变化" subtitle="用于确认升级后的页面内容确实发生变化">
|
||||||
<ul class="example-changes">
|
<ul class="example-changes">
|
||||||
<li>页面版本提示更新为 v1.6.6。</li>
|
<li>页面版本提示更新为 v1.6.7。</li>
|
||||||
<li>「本次升级验证」区块和页面状态同步更新为 v1.6.6。</li>
|
<li>「本次升级验证」区块和页面状态同步更新为 v1.6.7。</li>
|
||||||
<li>验证标识更新为 <span class="mono">v1.6.6-example-page</span>。</li>
|
<li>验证标识更新为 <span class="mono">v1.6.7-example-page</span>。</li>
|
||||||
</ul>
|
</ul>
|
||||||
</FonuCard>
|
</FonuCard>
|
||||||
|
|
||||||
@@ -87,8 +87,8 @@ const message = useMessage()
|
|||||||
const version = ref(__APP_VERSION__)
|
const version = ref(__APP_VERSION__)
|
||||||
const checkedAt = ref('')
|
const checkedAt = ref('')
|
||||||
const refreshing = ref(false)
|
const refreshing = ref(false)
|
||||||
const verificationRound = 'v1.6.6'
|
const verificationRound = 'v1.6.7'
|
||||||
const verificationMarker = 'v1.6.6-example-page'
|
const verificationMarker = 'v1.6.7-example-page'
|
||||||
|
|
||||||
async function refreshVersion() {
|
async function refreshVersion() {
|
||||||
refreshing.value = true
|
refreshing.value = true
|
||||||
|
|||||||
Reference in New Issue
Block a user