Browse Source

fix(warp): set license against Cloudflare API and surface errors inline

The license update was always failing because the Cloudflare response has
no `success` field — the check rejected every successful PUT. On real
errors (e.g. "Too many connected devices."), the toast leaked the raw URL
+ JSON body. Now the WARP API's error envelope is parsed into a clean
message and shown inline next to the Update button.
MHSanaei 1 day ago
parent
commit
adc262a238
2 changed files with 43 additions and 10 deletions
  1. 23 3
      frontend/src/pages/xray/WarpModal.vue
  2. 20 7
      web/service/warp.go

+ 23 - 3
frontend/src/pages/xray/WarpModal.vue

@@ -27,6 +27,7 @@ const loading = ref(false);
 const warpData = ref(null);
 const warpConfig = ref(null);
 const warpPlus = ref('');
+const licenseError = ref('');
 // Held in memory so the parent's add/reset handlers receive the same
 // object the modal computed from getConfig().
 const stagedOutbound = ref(null);
@@ -41,6 +42,7 @@ watch(() => props.open, (next) => {
   if (!next) return;
   warpConfig.value = null;
   stagedOutbound.value = null;
+  licenseError.value = '';
   fetchData();
 });
 
@@ -89,12 +91,15 @@ async function getConfig() {
 async function updateLicense() {
   if (warpPlus.value.length < 26) return;
   loading.value = true;
+  licenseError.value = '';
   try {
     const msg = await HttpUtil.post('/panel/xray/warp/license', { license: warpPlus.value });
     if (msg?.success) {
       warpData.value = JSON.parse(msg.obj);
       warpConfig.value = null;
       warpPlus.value = '';
+    } else {
+      licenseError.value = msg?.msg || 'Failed to set WARP license.';
     }
   } finally {
     loading.value = false;
@@ -233,9 +238,12 @@ const hasConfig = computed(() => !ObjectUtil.isEmpty(warpConfig.value));
         <a-collapse-panel header="WARP / WARP+ license key">
           <a-form :colon="false" :label-col="{ md: { span: 6 } }" :wrapper-col="{ md: { span: 14 } }">
             <a-form-item label="Key">
-              <a-input v-model:value="warpPlus" placeholder="26-char WARP+ key" />
-              <a-button type="primary" class="mt-8" :disabled="warpPlus.length < 26" :loading="loading"
-                @click="updateLicense">Update</a-button>
+              <a-input v-model:value="warpPlus" placeholder="26-char WARP+ key" @update:value="licenseError = ''" />
+              <div class="license-actions mt-8">
+                <a-button type="primary" :disabled="warpPlus.length < 26" :loading="loading"
+                  @click="updateLicense">Update</a-button>
+                <a-alert v-if="licenseError" :message="licenseError" type="error" show-icon class="license-error" />
+              </div>
             </a-form-item>
           </a-form>
         </a-collapse-panel>
@@ -358,4 +366,16 @@ const hasConfig = computed(() => !ObjectUtil.isEmpty(warpConfig.value));
 .ml-8 {
   margin-left: 8px;
 }
+
+.license-actions {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  flex-wrap: wrap;
+}
+
+.license-error {
+  flex: 1;
+  min-width: 0;
+}
 </style>

+ 20 - 7
web/service/warp.go

@@ -152,13 +152,8 @@ func (s *WarpService) SetWarpLicense(license string) (string, error) {
 	if err := json.Unmarshal(body, &response); err != nil {
 		return "", err
 	}
-	if success, _ := response["success"].(bool); !success {
-		if errorArr, ok := response["errors"].([]any); ok && len(errorArr) > 0 {
-			if errorObj, ok := errorArr[0].(map[string]any); ok {
-				return "", common.NewError(errorObj["code"], errorObj["message"])
-			}
-		}
-		return "", common.NewError("warp set license failed: unknown error")
+	if _, ok := response["id"].(string); !ok {
+		return "", common.NewErrorf("warp set license failed: unexpected response: %s", string(body))
 	}
 
 	warpData["license_key"] = license
@@ -202,8 +197,26 @@ func doWarpRequest(req *http.Request) ([]byte, error) {
 		return nil, err
 	}
 	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+		if msg := parseWarpError(body); msg != "" {
+			return nil, common.NewError(msg)
+		}
 		return nil, common.NewErrorf("warp api %s %s returned status %d: %s",
 			req.Method, req.URL.Path, resp.StatusCode, string(body))
 	}
 	return body, nil
 }
+
+func parseWarpError(body []byte) string {
+	var env struct {
+		Errors []struct {
+			Message string `json:"message"`
+		} `json:"errors"`
+	}
+	if err := json.Unmarshal(body, &env); err != nil {
+		return ""
+	}
+	if len(env.Errors) == 0 || env.Errors[0].Message == "" {
+		return ""
+	}
+	return env.Errors[0].Message
+}