From 3075ee578aa0f944bbff2e28ec89dba24253859d Mon Sep 17 00:00:00 2001 From: bashrusakh Date: Mon, 17 Aug 2026 22:31:49 +1100 Subject: [PATCH 1/3] fix(ui): show GitHub build save errors --- admin-ui/src/api/github_build_config.js | 2 +- admin-ui/src/utils/i18n/en.json | 3 +++ admin-ui/src/utils/i18n/ru.json | 3 +++ admin-ui/src/utils/i18n/zh_CN.json | 3 +++ admin-ui/src/views/server/github-build.vue | 25 +++++++++++++++++++--- 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/admin-ui/src/api/github_build_config.js b/admin-ui/src/api/github_build_config.js index d663b66..8f2c77a 100644 --- a/admin-ui/src/api/github_build_config.js +++ b/admin-ui/src/api/github_build_config.js @@ -5,7 +5,7 @@ export function get () { } export function save (data) { - return request({ url: '/github_build_config/save', method: 'post', data }) + return request({ url: '/github_build_config/save', method: 'post', data, skipErrorMessage: true }) } export function getWorkflowTags () { diff --git a/admin-ui/src/utils/i18n/en.json b/admin-ui/src/utils/i18n/en.json index a657a90..58128d9 100644 --- a/admin-ui/src/utils/i18n/en.json +++ b/admin-ui/src/utils/i18n/en.json @@ -125,6 +125,9 @@ "WorkflowApprovalRequestFailed": { "One": "Approval was not recorded. Try again after reviewing the provider status." }, + "GithubBuildSaveError": { + "One": "GitHub build settings could not be saved. Check the configuration and try again." + }, "Status": { "One": "Status" }, diff --git a/admin-ui/src/utils/i18n/ru.json b/admin-ui/src/utils/i18n/ru.json index 60e561a..2b0005c 100644 --- a/admin-ui/src/utils/i18n/ru.json +++ b/admin-ui/src/utils/i18n/ru.json @@ -125,6 +125,9 @@ "WorkflowApprovalRequestFailed": { "One": "Подтверждение не записано. Проверьте состояние провайдера и повторите попытку." }, + "GithubBuildSaveError": { + "One": "Не удалось сохранить настройки GitHub Build. Проверьте конфигурацию и повторите попытку." + }, "Status": { "One": "Статус" }, diff --git a/admin-ui/src/utils/i18n/zh_CN.json b/admin-ui/src/utils/i18n/zh_CN.json index 07931ea..d462901 100644 --- a/admin-ui/src/utils/i18n/zh_CN.json +++ b/admin-ui/src/utils/i18n/zh_CN.json @@ -125,6 +125,9 @@ "WorkflowApprovalRequestFailed": { "One": "审批未记录。请查看服务商状态后重试。" }, + "GithubBuildSaveError": { + "One": "无法保存 GitHub 构建设置。请检查配置后重试。" + }, "Status": { "One": "状态" }, diff --git a/admin-ui/src/views/server/github-build.vue b/admin-ui/src/views/server/github-build.vue index 64f7ee7..55fa1c9 100644 --- a/admin-ui/src/views/server/github-build.vue +++ b/admin-ui/src/views/server/github-build.vue @@ -154,6 +154,9 @@ Trigger test build + + {{ saveError }} + {{ testResult.message }} @@ -208,6 +211,7 @@ const form = reactive({ const generatedKey = ref('') const testResult = ref(null) const dispatchResult = ref(null) +const saveError = ref('') async function load () { loading.value = true @@ -300,18 +304,33 @@ const workflowRefStatusType = computed(() => { async function onSave () { saving.value = true + saveError.value = '' try { await api.save({ repo: form.repo, token: form.token, payload_key: form.payload_key, }) - form.token = '' - form.payload_key = '' - await load() + } catch (e) { + saveError.value = extractSaveError(e) + return } finally { saving.value = false } + form.token = '' + form.payload_key = '' + await load() +} + +function extractSaveError (error) { + const envelopes = [error, error?.response?.data] + for (const envelope of envelopes) { + if (Number.isInteger(envelope?.code) && envelope.code !== 0 + && typeof envelope.message === 'string' && envelope.message.trim()) { + return envelope.message.trim() + } + } + return T('GithubBuildSaveError') } async function onGenerate () { From fafb8ed04012b567b23eb06fa5f218b6f0194650 Mon Sep 17 00:00:00 2001 From: bashrusakh Date: Thu, 20 Aug 2026 15:47:57 +1100 Subject: [PATCH 2/3] fix(admin): harden build and password flows --- PLAN.md | 12 +- README.md | 6 + admin-ui/src/api/custom_client.js | 4 + admin-ui/src/api/github_build_config.js | 22 +- admin-ui/src/utils/i18n/en.json | 82 +- admin-ui/src/utils/i18n/ru.json | 82 +- admin-ui/src/utils/i18n/zh_CN.json | 82 +- admin-ui/src/utils/request.js | 96 ++- admin-ui/src/views/custom-client/index.vue | 809 ++++++++++++++---- admin-ui/src/views/server/github-build.vue | 395 +++++++-- api/README.md | 25 +- api/docs/admin/admin_docs.go | 4 +- api/docs/admin/admin_swagger.json | 6 +- api/docs/admin/admin_swagger.yaml | 3 +- api/http/controller/admin/custom_build.go | 43 +- api/http/controller/admin/custom_platform.go | 54 +- .../controller/admin/custom_platform_test.go | 195 +++++ api/http/controller/admin/custom_preset.go | 1 + .../admin/github_build_config_test.go | 10 +- api/http/request/admin/custom_build_test.go | 36 + api/http/request/admin/custom_preset.go | 26 +- api/http/request/admin/custom_preset_test.go | 20 + api/http/request/admin/user.go | 2 +- api/http/request/admin/user_test.go | 50 ++ api/model/custom_preset.go | 34 +- api/model/custom_safe_test.go | 29 +- api/service/build_provenance_test.go | 75 +- api/service/custom_build.go | 43 +- api/service/custom_build_spec.go | 45 +- api/service/custom_build_spec_test.go | 35 + api/service/custom_persistence_test.go | 340 +++++++- api/service/custom_preset.go | 91 +- api/service/github_build_config.go | 66 +- api/service/github_build_config_test.go | 65 +- api/service/version_catalog.go | 6 +- api/service/version_catalog_test.go | 13 + api/service/workflow_mapping.go | 19 +- api/service/workflow_mapping_test.go | 29 + api/utils/secretcrypt.go | 19 + github-build/README.md | 56 +- plans/deskforge-workflow-migration/plan.md | 17 +- plans/deskforge-workflow-migration/todo.md | 14 +- 42 files changed, 2651 insertions(+), 410 deletions(-) create mode 100644 api/http/request/admin/custom_build_test.go create mode 100644 api/http/request/admin/custom_preset_test.go create mode 100644 api/http/request/admin/user_test.go diff --git a/PLAN.md b/PLAN.md index 0d0f7f8..81ac924 100644 --- a/PLAN.md +++ b/PLAN.md @@ -89,7 +89,7 @@ are recorded, and no full sovereignty claim is made. See [LICENSE](LICENSE) and admin-ui (Custom Client form) ↓ custom-build request Go API (DeskForge) - ↓ workflow_dispatch + authenticated DFP1 enc_payload (AES-256-CBC + PBKDF2 + HMAC) + ↓ workflow_dispatch + provider-derived public workflow_sha + authenticated DFP1 enc_payload (AES-256-CBC + PBKDF2 + HMAC) GitHub Actions [configured RustDesk fork, owned platform workflow] ↓ L1: config.rs (server + key) ↓ L2: custom_.txt (permanent password, allowCustom patch) @@ -100,9 +100,13 @@ Go API validates/extracts/publishes locally → admin-ui Download ``` **Security:** password never published — `enc_payload`, decrypted inside runner via -GitHub Secret `WORKFLOW_PAYLOAD_KEY`. The runner does not callback to the API; -the API retrieves the artifact through the provider API and publishes it locally, -not to a public release. +GitHub Secret `WORKFLOW_PAYLOAD_KEY`. The provider-derived outer `workflow_sha` is +checked against `github.sha` before secret-bearing jobs; the same authenticated inner +payload field is checked again before exports, checkout, or build use. This is defense +in depth, not an atomic defense against a malicious workflow file; the verified tag +and active no-bypass ruleset remain required controls. The runner does not callback to +the API; the API retrieves the artifact through the provider API and publishes it +locally, not to a public release. ### Workflow approval and schema evidence diff --git a/README.md b/README.md index 52a45f0..575c150 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,12 @@ only; no APK/package/install/runtime evidence exists. The local schema target is `DatabaseVersion 282`; the published DeskForge schema is 272, and SQLite-only checks do not establish cross-database verification. +### GitHub Actions PAT permissions + +See the [fine-grained PAT permission checklist](api/README.md#github-fine-grained-pat-permissions) +for the custom-client workflow, including why Administration, Actions, and +Secrets write access are required. + **Not implemented (vs RustDesk Pro):** 2FA, RBAC, session recording, device policy, remote script, HA, backup/restore. diff --git a/admin-ui/src/api/custom_client.js b/admin-ui/src/api/custom_client.js index 3023336..607904f 100644 --- a/admin-ui/src/api/custom_client.js +++ b/admin-ui/src/api/custom_client.js @@ -12,6 +12,7 @@ export function create (data) { url: '/custom_build/create', method: 'post', data, + useServerErrorMessage: true, }) } @@ -27,11 +28,14 @@ export function download (id) { return request({ url: `/custom_build/download/${id}`, responseType: 'blob', + useServerErrorMessage: true, }) } export function getVersions () { return request({ url: '/custom_build/versions', + skipErrorMessage: true, + useServerErrorMessage: true, }) } diff --git a/admin-ui/src/api/github_build_config.js b/admin-ui/src/api/github_build_config.js index 8f2c77a..616117f 100644 --- a/admin-ui/src/api/github_build_config.js +++ b/admin-ui/src/api/github_build_config.js @@ -1,15 +1,15 @@ import request from '@/utils/request' export function get () { - return request({ url: '/github_build_config/get' }) + return request({ url: '/github_build_config/get', skipErrorMessage: true, useServerErrorMessage: true }) } export function save (data) { - return request({ url: '/github_build_config/save', method: 'post', data, skipErrorMessage: true }) + return request({ url: '/github_build_config/save', method: 'post', data, skipErrorMessage: true, useServerErrorMessage: true }) } export function getWorkflowTags () { - return request({ url: '/github_build_config/workflow_tags' }) + return request({ url: '/github_build_config/workflow_tags', skipErrorMessage: true, useServerErrorMessage: true }) } export function approveWorkflowRef (workflowTag) { @@ -17,22 +17,30 @@ export function approveWorkflowRef (workflowTag) { url: '/github_build_config/approve_workflow_ref', method: 'post', data: { confirm: true, workflow_tag: workflowTag }, + skipErrorMessage: true, + useServerErrorMessage: true, }) } export function generateKey () { - return request({ url: '/github_build_config/generate_key', method: 'post' }) + return request({ url: '/github_build_config/generate_key', method: 'post', skipErrorMessage: true, useServerErrorMessage: true }) } export function test () { - return request({ url: '/github_build_config/test', method: 'post' }) + return request({ url: '/github_build_config/test', method: 'post', skipErrorMessage: true, useServerErrorMessage: true }) } export function syncSecret () { - return request({ url: '/github_build_config/sync_secret', method: 'post' }) + return request({ url: '/github_build_config/sync_secret', method: 'post', skipErrorMessage: true, useServerErrorMessage: true }) } export function dispatchTest () { // B-009: confirm=true — это реальный билд (тратит минуты Actions), не дешёвый чек. - return request({ url: '/github_build_config/dispatch_test', method: 'post', data: { confirm: true } }) + return request({ + url: '/github_build_config/dispatch_test', + method: 'post', + data: { confirm: true }, + skipErrorMessage: true, + useServerErrorMessage: true, + }) } diff --git a/admin-ui/src/utils/i18n/en.json b/admin-ui/src/utils/i18n/en.json index 58128d9..0aa06eb 100644 --- a/admin-ui/src/utils/i18n/en.json +++ b/admin-ui/src/utils/i18n/en.json @@ -20,6 +20,21 @@ "Password": { "One": "Password" }, + "ShowPassword": { + "One": "Show password" + }, + "HidePassword": { + "One": "Hide password" + }, + "GeneratedKeyLabel": { + "One": "Generated encryption key" + }, + "GeneratedKeyWarning": { + "One": "Copy this key now; it will not be shown again." + }, + "GeneratedKeyCreated": { + "One": "A new encryption key was generated." + }, "LoginSuccess": { "One": "Login Success" }, @@ -38,6 +53,33 @@ "ParamRequired": { "One": "{param} is required" }, + "CustomClientPlatformRequired": { + "One": "Choose Windows as the supported build platform." + }, + "CustomClientVersionRequired": { + "One": "Choose the client version to build." + }, + "CustomClientAppNameRequired": { + "One": "Enter the artifact application name." + }, + "CustomClientHostRequired": { + "One": "Provide the ID server endpoint (hostname/IP and optional port)." + }, + "CustomClientKeyRequired": { + "One": "Provide the public key." + }, + "CustomClientApiServerRequired": { + "One": "Provide the API server URL." + }, + "CustomClientRelayServerRequired": { + "One": "Provide the relay endpoint (hostname/IP and optional port)." + }, + "CustomClientPermanentPasswordRequired": { + "One": "Provide a permanent password when connection management is hidden." + }, + "ClearSavedPassword": { + "One": "Clear saved password" + }, "HasBind": { "One": "Has bind" }, @@ -125,9 +167,42 @@ "WorkflowApprovalRequestFailed": { "One": "Approval was not recorded. Try again after reviewing the provider status." }, + "GithubPatPermissionsIntro": { + "One": "Required fine-grained PAT repository permissions:" + }, + "GithubPatPermissionMetadata": { + "One": "Metadata — Read" + }, + "GithubPatPermissionContents": { + "One": "Contents — Read" + }, + "GithubPatPermissionActions": { + "One": "Actions — Read and write" + }, + "GithubPatPermissionAdministration": { + "One": "Administration — Read and write" + }, + "GithubPatPermissionSecrets": { + "One": "Secrets — Read and write" + }, + "GithubPatPermissionsNote": { + "One": "Actions write is required for dispatch; Administration write is required for ruleset bypass metadata; Secrets write is required for secret synchronization. Empty value keeps the existing token." + }, "GithubBuildSaveError": { "One": "GitHub build settings could not be saved. Check the configuration and try again." }, + "GithubBuildSaveSaving": { + "One": "Saving GitHub build settings..." + }, + "GithubBuildSaveSuccess": { + "One": "GitHub build settings saved." + }, + "GithubBuildRepositoryRequired": { + "One": "Enter a repository in owner/name format before continuing." + }, + "ViewBuildLog": { + "One": "View build log" + }, "Status": { "One": "Status" }, @@ -809,7 +884,7 @@ "One": "Host" }, "HostEndpointHint": { - "One": "Optional. Accepts a hostname or IP address with an optional port. RustDesk uses port 21116 only when the port is omitted." + "One": "Provide the ID server endpoint: a hostname or IP address with an optional port. RustDesk uses port 21116 when omitted." }, "HostEndpointPlaceholder": { "One": "e.g. your-server.com or your-server.com:21116 (default when omitted)" @@ -821,7 +896,7 @@ "One": "Relay Server" }, "RelayEndpointHint": { - "One": "Optional. Accepts a hostname or IP address with an optional port. RustDesk uses port 21117 only when the port is omitted." + "One": "Provide the relay endpoint: a hostname or IP address with an optional port. RustDesk uses port 21117 when omitted." }, "RelayEndpointPlaceholder": { "One": "e.g. your-server.com or your-server.com:21117 (default when omitted)" @@ -946,6 +1021,9 @@ "BuildStatus": { "One": "Status" }, + "BuildHistoryStatusChanged": { + "One": "Build status updated: {param}." + }, "Pending": { "One": "Pending" }, diff --git a/admin-ui/src/utils/i18n/ru.json b/admin-ui/src/utils/i18n/ru.json index 2b0005c..367a5e4 100644 --- a/admin-ui/src/utils/i18n/ru.json +++ b/admin-ui/src/utils/i18n/ru.json @@ -20,6 +20,21 @@ "Password": { "One": "Пароль" }, + "ShowPassword": { + "One": "Показать пароль" + }, + "HidePassword": { + "One": "Скрыть пароль" + }, + "GeneratedKeyLabel": { + "One": "Созданный ключ шифрования" + }, + "GeneratedKeyWarning": { + "One": "Скопируйте этот ключ сейчас — повторно он показан не будет." + }, + "GeneratedKeyCreated": { + "One": "Создан новый ключ шифрования." + }, "LoginSuccess": { "One": "Успешный вход" }, @@ -38,6 +53,33 @@ "ParamRequired": { "One": "Поле {param} обязательно" }, + "CustomClientPlatformRequired": { + "One": "Выберите поддерживаемую платформу сборки (Windows)." + }, + "CustomClientVersionRequired": { + "One": "Выберите версию клиента для сборки." + }, + "CustomClientAppNameRequired": { + "One": "Введите имя приложения для артефакта." + }, + "CustomClientHostRequired": { + "One": "Укажите адрес ID-сервера (имя хоста/IP и необязательный порт)." + }, + "CustomClientKeyRequired": { + "One": "Укажите публичный ключ." + }, + "CustomClientApiServerRequired": { + "One": "Укажите URL API-сервера." + }, + "CustomClientRelayServerRequired": { + "One": "Укажите адрес relay-сервера (имя хоста/IP и необязательный порт)." + }, + "CustomClientPermanentPasswordRequired": { + "One": "Если управление подключением скрыто, укажите постоянный пароль." + }, + "ClearSavedPassword": { + "One": "Очистить сохранённый пароль" + }, "HasBind": { "One": "Связано" }, @@ -125,9 +167,45 @@ "WorkflowApprovalRequestFailed": { "One": "Подтверждение не записано. Проверьте состояние провайдера и повторите попытку." }, + "GithubPatPermissionsIntro": { + "One": "Необходимые разрешения репозитория для fine-grained PAT:" + }, + "GithubPatPermissionMetadata": { + "One": "Metadata — Read" + }, + "GithubPatPermissionContents": { + "One": "Contents — Read" + }, + "GithubPatPermissionActions": { + "One": "Actions — Read and write" + }, + "GithubPatPermissionAdministration": { + "One": "Administration — Read and write" + }, + "GithubPatPermissionSecrets": { + "One": "Secrets — Read and write" + }, + "GithubPatPermissionsNote": { + "One": "Для dispatch требуется Actions write; для метаданных обхода защиты workflow требуется Administration write; для синхронизации секретов требуется Secrets write. Пустое значение сохраняет текущий токен." + }, "GithubBuildSaveError": { "One": "Не удалось сохранить настройки GitHub Build. Проверьте конфигурацию и повторите попытку." }, + "GithubBuildSaveSaving": { + "One": "Сохранение настроек GitHub Build..." + }, + "GithubBuildSaveSuccess": { + "One": "Настройки GitHub Build сохранены." + }, + "GithubBuildRepositoryRequired": { + "One": "Перед продолжением укажите репозиторий в формате owner/name." + }, + "ViewBuildLog": { + "One": "Просмотреть журнал сборки" + }, + "BuildHistoryStatusChanged": { + "One": "Статус сборки обновлён: {param}." + }, "Status": { "One": "Статус" }, @@ -686,13 +764,13 @@ "One": "Загрузить" }, "HostEndpointHint": { - "One": "Необязательное поле. Принимает имя хоста или IP-адрес с необязательным портом. RustDesk использует порт 21116 только если порт не указан." + "One": "Укажите адрес ID-сервера: имя хоста или IP-адрес с необязательным портом. RustDesk использует порт 21116, если порт не указан." }, "HostEndpointPlaceholder": { "One": "например, your-server.com или your-server.com:21116 (по умолчанию, если порт не указан)" }, "RelayEndpointHint": { - "One": "Необязательное поле. Принимает имя хоста или IP-адрес с необязательным портом. RustDesk использует порт 21117 только если порт не указан." + "One": "Укажите адрес relay-сервера: имя хоста или IP-адрес с необязательным портом. RustDesk использует порт 21117, если порт не указан." }, "RelayEndpointPlaceholder": { "One": "например, your-server.com или your-server.com:21117 (по умолчанию, если порт не указан)" diff --git a/admin-ui/src/utils/i18n/zh_CN.json b/admin-ui/src/utils/i18n/zh_CN.json index d462901..231e685 100644 --- a/admin-ui/src/utils/i18n/zh_CN.json +++ b/admin-ui/src/utils/i18n/zh_CN.json @@ -20,6 +20,21 @@ "Password": { "One": "密码" }, + "ShowPassword": { + "One": "显示密码" + }, + "HidePassword": { + "One": "隐藏密码" + }, + "GeneratedKeyLabel": { + "One": "生成的加密密钥" + }, + "GeneratedKeyWarning": { + "One": "请立即复制此密钥;之后将不再显示。" + }, + "GeneratedKeyCreated": { + "One": "已生成新的加密密钥。" + }, "LoginSuccess": { "One": "登录成功" }, @@ -38,6 +53,33 @@ "ParamRequired": { "One": "{param} 是必须的" }, + "CustomClientPlatformRequired": { + "One": "请选择受支持的构建平台(Windows)。" + }, + "CustomClientVersionRequired": { + "One": "请选择要构建的客户端版本。" + }, + "CustomClientAppNameRequired": { + "One": "请输入产物应用名称。" + }, + "CustomClientHostRequired": { + "One": "请输入 ID 服务器端点(主机名/IP,可选端口)。" + }, + "CustomClientKeyRequired": { + "One": "请输入公钥。" + }, + "CustomClientApiServerRequired": { + "One": "请输入 API 服务器 URL。" + }, + "CustomClientRelayServerRequired": { + "One": "请输入中继端点(主机名/IP,可选端口)。" + }, + "CustomClientPermanentPasswordRequired": { + "One": "隐藏连接管理时,请输入永久密码。" + }, + "ClearSavedPassword": { + "One": "清除已保存的密码" + }, "HasBind": { "One": "已绑定" }, @@ -125,9 +167,45 @@ "WorkflowApprovalRequestFailed": { "One": "审批未记录。请查看服务商状态后重试。" }, + "GithubPatPermissionsIntro": { + "One": "细粒度 PAT 所需的仓库权限:" + }, + "GithubPatPermissionMetadata": { + "One": "Metadata — Read" + }, + "GithubPatPermissionContents": { + "One": "Contents — Read" + }, + "GithubPatPermissionActions": { + "One": "Actions — Read and write" + }, + "GithubPatPermissionAdministration": { + "One": "Administration — Read and write" + }, + "GithubPatPermissionSecrets": { + "One": "Secrets — Read and write" + }, + "GithubPatPermissionsNote": { + "One": "触发 workflow 需要 Actions write;读取规则绕过元数据需要 Administration write;同步仓库密钥需要 Secrets write。留空将保留现有令牌。" + }, "GithubBuildSaveError": { "One": "无法保存 GitHub 构建设置。请检查配置后重试。" }, + "GithubBuildSaveSaving": { + "One": "正在保存 GitHub 构建设置..." + }, + "GithubBuildSaveSuccess": { + "One": "GitHub 构建设置已保存。" + }, + "GithubBuildRepositoryRequired": { + "One": "继续之前,请输入 owner/name 格式的仓库。" + }, + "ViewBuildLog": { + "One": "查看构建日志" + }, + "BuildHistoryStatusChanged": { + "One": "构建状态已更新:{param}。" + }, "Status": { "One": "状态" }, @@ -699,13 +777,13 @@ "One": "上传" }, "HostEndpointHint": { - "One": "可选字段。支持主机名或 IP 地址,可选择是否指定端口。仅在未指定端口时,RustDesk 使用默认端口 21116。" + "One": "请输入 ID 服务器端点:主机名或 IP 地址,可选择是否指定端口。未指定端口时使用 21116。" }, "HostEndpointPlaceholder": { "One": "例如 your-server.com 或 your-server.com:21116(未填写端口时使用默认端口)" }, "RelayEndpointHint": { - "One": "可选字段。支持主机名或 IP 地址,可选择是否指定端口。仅在未指定端口时,RustDesk 使用默认端口 21117。" + "One": "请输入中继端点:主机名或 IP 地址,可选择是否指定端口。未指定端口时使用 21117。" }, "RelayEndpointPlaceholder": { "One": "例如 your-server.com 或 your-server.com:21117(未填写端口时使用默认端口)" diff --git a/admin-ui/src/utils/request.js b/admin-ui/src/utils/request.js index a409181..2f8efc6 100644 --- a/admin-ui/src/utils/request.js +++ b/admin-ui/src/utils/request.js @@ -12,6 +12,51 @@ const service = axios.create({ timeout: 50000, // request timeout }) +const getEnvelopeErrorMessage = data => { + const messages = [data?.message, data?.data?.message] + + return messages.find(message => typeof message === 'string' && message.trim()) || '' +} + +const redirectToLogin = config => { + if (!config?.skipAuthRedirect) { + removeToken() + window.location.reload() + } +} + +const redirectOnEnvelopeAuthFailure = (config, code) => { + if (code === 403) { + redirectToLogin(config) + } +} + +const redirectOnHttpAuthFailure = (config, status) => { + if (config?.useServerErrorMessage && (status === 401 || status === 403)) { + redirectToLogin(config) + } +} + +const GENERIC_RESPONSE_ERROR = 'Unable to process server response' + +const markInterceptorHandled = error => { + Object.defineProperty(error, 'interceptorHandled', { value: true }) + return error +} + +const getResponseErrorMessage = async error => { + if (!error.config?.useServerErrorMessage) return '' + let data = error.response?.data + if (error.config?.responseType === 'blob' && typeof Blob !== 'undefined' && data instanceof Blob) { + try { + data = JSON.parse(await data.text()) + } catch (_) { + return '' + } + } + return getEnvelopeErrorMessage(data) +} + // request interceptor service.interceptors.request.use( config => { @@ -70,22 +115,38 @@ service.interceptors.response.use( // Auth/API failures can still arrive as a JSON envelope with HTTP 200. // Parse those responses so a failed download is not saved as a .zip file. return response.data.text().then(text => { - const res = JSON.parse(text) + let res + try { + res = JSON.parse(text) + } catch (_) { + const malformedResponseError = new Error(GENERIC_RESPONSE_ERROR) + if (response.config.useServerErrorMessage && !response.config.skipErrorMessage) { + ElMessage({ + message: GENERIC_RESPONSE_ERROR, + type: 'error', + duration: 5 * 1000, + }) + return Promise.reject(markInterceptorHandled(malformedResponseError)) + } + return Promise.reject(malformedResponseError) + } + if (res.code !== 0) { - ElMessage({ - message: res.message || 'error', - type: 'error', - duration: 5 * 1000, - }) - - if (res.code === 403) { - removeToken() - window.location.reload() + if (!response.config.skipErrorMessage) { + ElMessage({ + message: response.config.useServerErrorMessage + ? getEnvelopeErrorMessage(res) || GENERIC_RESPONSE_ERROR + : res.message || 'error', + type: 'error', + duration: 5 * 1000, + }) } + + redirectOnEnvelopeAuthFailure(response.config, res.code) return Promise.reject(res) } return response - }).catch(error => Promise.reject(error)) + }) } const res = response.data @@ -106,27 +167,28 @@ service.interceptors.response.use( }) } - if (res.code === 403 && !response.config.skipAuthRedirect) { - removeToken() - window.location.reload() - } + redirectOnEnvelopeAuthFailure(response.config, res.code) return Promise.reject(res) } else { return res } }, - error => { + async error => { if (error.code === 'ECONNABORTED' && error.message.indexOf('timeout') > -1) { error.message = 'Connection Time Out!' } if (!error.config?.skipErrorMessage) { + const message = await getResponseErrorMessage(error) ElMessage({ - message: error.message, + message: error.config?.useServerErrorMessage + ? message || GENERIC_RESPONSE_ERROR + : error.message, type: 'error', duration: 5 * 1000, }) } + redirectOnHttpAuthFailure(error.config, error.response?.status) return Promise.reject(error) }, ) diff --git a/admin-ui/src/views/custom-client/index.vue b/admin-ui/src/views/custom-client/index.vue index 29b0bea..7cc3050 100644 --- a/admin-ui/src/views/custom-client/index.vue +++ b/admin-ui/src/views/custom-client/index.vue @@ -7,49 +7,99 @@ pulse="warning" /> - + - - + +
+ {{ p.name }} {{ p.platform }} - {{ T('Delete') }} - - {{ T('SaveAsPreset') }} + +
+
+ {{ T('SaveAsPreset') }} +
+
+ {{ T('Delete') }} +
+
+
{{ T('Platform') }} - + - - - - - -