This is an automated email from the ASF dual-hosted git repository.

gongchao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git


The following commit(s) were added to refs/heads/master by this push:
     new 3e6a7962f6 [feat] add API token management functionality (#4080)
3e6a7962f6 is described below

commit 3e6a7962f6f538bb0b9ee8b58e8929a465c1778d
Author: Duansg <[email protected]>
AuthorDate: Sat Mar 21 16:20:59 2026 +0800

    [feat] add API token management functionality (#4080)
    
    Co-authored-by: Tomsun28 <[email protected]>
---
 e2e/testsuite.yaml                                 |  60 ++++
 .../hertzbeat/common/entity/manager/AuthToken.java |  94 ++++++
 .../config/ApiTokenValidationConfiguration.java    |  40 +++
 .../manager/config/ApiTokenValidationFilter.java   | 168 ++++++++++
 .../manager/controller/AuthTokenController.java    |  62 +++-
 .../apache/hertzbeat/manager/dao/AuthTokenDao.java |  58 ++++
 .../hertzbeat/manager/service/AccountService.java  |  53 +++-
 .../manager/service/impl/AccountServiceImpl.java   | 191 +++++++++++-
 .../config/ApiTokenValidationFilterTest.java       | 263 ++++++++++++++++
 .../controller/AuthTokenControllerTest.java        | 226 ++++++++++++++
 .../manager/service/AccountServiceTest.java        | 343 ++++++++++++++++++++-
 hertzbeat-startup/src/main/resources/sureness.yml  |   3 +
 .../alert-integration.component.html               |  39 +--
 .../alert-integration.component.ts                 |  41 +--
 .../log-integration/log-integration.component.html |  39 +--
 .../log-integration/log-integration.component.ts   |  41 +--
 .../app/routes/setting/setting-routing.module.ts   |   6 +
 web-app/src/app/routes/setting/setting.module.ts   |   6 +-
 .../routes/setting/settings/settings.component.ts  |   5 +
 .../token-management.component.html                | 143 +++++++++
 .../token-management.component.less                |  26 ++
 .../token-management/token-management.component.ts | 185 +++++++++++
 web-app/src/app/service/auth.service.ts            |  19 +-
 web-app/src/assets/i18n/en-US.json                 |  26 ++
 web-app/src/assets/i18n/zh-CN.json                 |  26 ++
 25 files changed, 1979 insertions(+), 184 deletions(-)

diff --git a/e2e/testsuite.yaml b/e2e/testsuite.yaml
index 25a37f8433..9ada700442 100644
--- a/e2e/testsuite.yaml
+++ b/e2e/testsuite.yaml
@@ -29,6 +29,7 @@ param:
   monitorUDP: "{{randAlpha 6}}"
   monitorWebsite: "{{randAlpha 6}}"
   monitorFTP: "{{randAlpha 6}}"
+  apiTokenName: "e2e-token-{{randAlpha 6}}"
   labelName: "{{randAlpha 3}}"
 items:
 - name: login
@@ -69,8 +70,13 @@ items:
 - name: missing-auth-header
   request:
     api: /api/monitors
+    header:
+      Accept: application/json
   expect:
     statusCode: 401
+    bodyFieldsExpect:
+      code: 401
+      msg: Missing Authorization in header
 - name: createSitemapMonitor
   request:
     api: /api/monitor
@@ -385,6 +391,60 @@ items:
     header:
       Authorization: Bearer {{.login.data.token}}
 
+## API Token Management
+- name: generateApiToken
+  request:
+    api: /api/account/token/generate?name={{.apiTokenName}}&expireSeconds=-1
+    method: POST
+    header:
+      Authorization: Bearer {{.login.data.token}}
+  expect:
+    bodyFieldsExpect:
+      code: "0"
+- name: listApiTokens
+  request:
+    api: /api/account/token
+    header:
+      Authorization: Bearer {{.login.data.token}}
+  expect:
+    bodyFieldsExpect:
+      code: "0"
+      data.0.name: "{{.apiTokenName}}"
+      data.0.creator: "admin"
+- name: listApiTokensByGeneratedToken
+  request:
+    api: /api/account/token
+    header:
+      Authorization: Bearer {{.generateApiToken.data.token}}
+  expect:
+    bodyFieldsExpect:
+      code: "0"
+      data.0.name: "{{.apiTokenName}}"
+- name: useGeneratedTokenToCallApi
+  request:
+    api: /api/monitors?pageIndex=0&pageSize=8
+    header:
+      Authorization: Bearer {{.generateApiToken.data.token}}
+  expect:
+    bodyFieldsExpect:
+      code: "0"
+- name: deleteApiToken
+  request:
+    api: /api/account/token/{{(index .listApiTokens.data 0).id | int64}}
+    method: DELETE
+    header:
+      Authorization: Bearer {{.login.data.token}}
+  expect:
+    bodyFieldsExpect:
+      code: "0"
+- name: useRevokedTokenToCallApi
+  request:
+    api: /api/monitors?pageIndex=0&pageSize=8
+    header:
+      Authorization: Bearer {{.generateApiToken.data.token}}
+  expect:
+    statusCode: 401
+
 ## Config
 - name: getConfigEmail
   request:
diff --git 
a/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/AuthToken.java
 
b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/AuthToken.java
new file mode 100644
index 0000000000..1845c36e91
--- /dev/null
+++ 
b/hertzbeat-common-spring/src/main/java/org/apache/hertzbeat/common/entity/manager/AuthToken.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.common.entity.manager;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EntityListeners;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.CreatedBy;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import java.time.LocalDateTime;
+
+import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY;
+import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE;
+
+/**
+ * API Token Entity - stores generated non-expiring API tokens for management 
and revocation.
+ * <p>
+ * Only "managed" tokens (those generated after this feature was introduced) 
are stored here.
+ * Legacy tokens are not tracked and continue to work without DB validation.
+ * </p>
+ */
+@Entity
+@Table(name = "hzb_auth_token")
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Schema(description = "API Token entity")
+@EntityListeners(AuditingEntityListener.class)
+public class AuthToken {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Schema(title = "primary id", example = "1", accessMode = READ_ONLY)
+    private Long id;
+
+    @Schema(title = "Token name/description", example = "Alert integration 
token", accessMode = READ_WRITE)
+    @Column(length = 255)
+    private String name;
+
+    @Schema(title = "SHA-256 hash of the token value", accessMode = READ_ONLY)
+    @Column(length = 128, nullable = false, unique = true)
+    private String tokenHash;
+
+    @Schema(title = "Masked token for display", example = "eyJh****xMjM", 
accessMode = READ_ONLY)
+    @Column(length = 64)
+    private String tokenMask;
+
+    @Schema(title = "Token status: 0-active", accessMode = READ_ONLY)
+    @Column(nullable = false)
+    @Builder.Default
+    private Byte status = 0;
+
+    @Schema(title = "The creator of this token", accessMode = READ_ONLY)
+    @CreatedBy
+    private String creator;
+
+    @Schema(title = "Token creation time", accessMode = READ_ONLY)
+    @CreatedDate
+    private LocalDateTime gmtCreate;
+
+    @Schema(title = "Token expiration time, null means never expire", 
accessMode = READ_ONLY)
+    private LocalDateTime expireTime;
+
+    @Schema(title = "Token last used time", accessMode = READ_ONLY)
+    private LocalDateTime lastUsedTime;
+
+}
diff --git 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ApiTokenValidationConfiguration.java
 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ApiTokenValidationConfiguration.java
new file mode 100644
index 0000000000..ebd114de65
--- /dev/null
+++ 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ApiTokenValidationConfiguration.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * Registers the managed API token validation interceptor after servlet 
filters.
+ */
+@Configuration
+public class ApiTokenValidationConfiguration implements WebMvcConfigurer {
+
+    private final ApiTokenValidationFilter apiTokenValidationFilter;
+
+    public ApiTokenValidationConfiguration(ApiTokenValidationFilter 
apiTokenValidationFilter) {
+        this.apiTokenValidationFilter = apiTokenValidationFilter;
+    }
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        
registry.addInterceptor(apiTokenValidationFilter).addPathPatterns("/api/**");
+    }
+}
diff --git 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilter.java
 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilter.java
new file mode 100644
index 0000000000..4075d2e1d6
--- /dev/null
+++ 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilter.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.config;
+
+import com.usthe.sureness.subject.PrincipalMap;
+import com.usthe.sureness.subject.SubjectSum;
+import com.usthe.sureness.util.SurenessContextHolder;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
+import org.apache.hertzbeat.common.constants.NetworkConstants;
+import org.apache.hertzbeat.common.util.JsonUtil;
+import org.apache.hertzbeat.manager.service.AccountService;
+import org.apache.hertzbeat.manager.service.impl.AccountServiceImpl;
+import org.jspecify.annotations.NonNull;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+/**
+ * Interceptor to validate that managed API tokens have not been revoked 
(deleted).
+ * <p>
+ * This interceptor runs AFTER the Sureness authentication filter to ensure:
+ * 1. Only authenticated requests are checked (avoids unnecessary DB hits)
+ * 2. lastUsedTime is only updated for successfully authenticated requests
+ * <p>
+ * Responsibility split:
+ * - <b>Expiration</b>: handled by Sureness/JWT natively via the {@code exp} 
claim
+ * - <b>Revocation</b>: handled here by checking the token's status in the 
database
+ * - <b>Last used time</b>: updated here on each valid request (throttled)
+ * <p>
+ * Only tokens containing the "managed" claim (generated by the new token 
management system)
+ * are validated against the database. Legacy tokens without this claim are 
allowed to pass
+ * through for backward compatibility.
+ * </p>
+ */
+@Component
+@Slf4j
+public class ApiTokenValidationFilter implements HandlerInterceptor {
+
+    private static final String ROLES_CLAIM = "roles";
+    private static final String TOKEN_VALIDATION_UNAVAILABLE = "Token 
validation unavailable";
+
+    private final AccountService accountService;
+
+    public ApiTokenValidationFilter(AccountService accountService) {
+        this.accountService = accountService;
+    }
+
+    @Override
+    public boolean preHandle(@NonNull HttpServletRequest request, @NonNull 
HttpServletResponse response, @NonNull Object handler)
+        throws IOException {
+        SubjectSum subject = SurenessContextHolder.getBindSubject();
+        if (subject == null || !isManagedToken(subject)) {
+            return true;
+        }
+        String authorization = 
request.getHeader(NetworkConstants.AUTHORIZATION);
+
+        if (authorization != null && 
authorization.startsWith(DispatchConstants.BEARER)) {
+            String token = 
authorization.substring(DispatchConstants.BEARER.length()).trim();
+            if (!token.isEmpty()) {
+                try {
+                    String rejectReason = checkManagedToken(subject, token);
+                    if (rejectReason != null) {
+                        return writeError(response, HttpStatus.UNAUTHORIZED, 
rejectReason);
+                    }
+                } catch (RuntimeException e) {
+                    log.warn("Managed token validation failed", e);
+                    return writeError(response, 
HttpStatus.SERVICE_UNAVAILABLE, TOKEN_VALIDATION_UNAVAILABLE);
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Check if a managed token should be rejected.
+     * <p>
+     * Returns a rejection reason string if the token should be rejected, null 
if allowed.
+     * Checks:
+     * 1. Only managed tokens (with "managed" claim) are validated — legacy 
tokens pass through
+     * 2. Token must exist and be active in the database (not deleted/revoked)
+     * 3. Token owner must still exist, remain active, and still own the 
claimed roles
+     * If the token is valid, updates the last used time.
+     * </p>
+     */
+    private String checkManagedToken(SubjectSum subject, String token) {
+        String rejectReason = accountService.checkTokenStatus(token);
+        if (rejectReason != null) {
+            return rejectReason;
+        }
+        rejectReason = 
accountService.checkManagedTokenAccess(getCurrentUserId(subject), 
extractClaimedRoles(subject));
+        if (rejectReason != null) {
+            return rejectReason;
+        }
+        try {
+            accountService.touchTokenLastUsedTime(token);
+        } catch (RuntimeException e) {
+            log.debug("Failed to update token last used time", e);
+        }
+        return null;
+    }
+
+    private boolean isManagedToken(SubjectSum subject) {
+        PrincipalMap principalMap = subject.getPrincipalMap();
+        if (principalMap == null) {
+            return false;
+        }
+        Object managed = 
principalMap.getPrincipal(AccountServiceImpl.CLAIM_MANAGED);
+        return managed instanceof Boolean ? (Boolean) managed : 
Boolean.parseBoolean(String.valueOf(managed));
+    }
+
+    @SuppressWarnings("unchecked")
+    private List<String> extractClaimedRoles(SubjectSum subject) {
+        Object roles = subject.getRoles();
+        if (roles instanceof List<?>) {
+            return (List<String>) roles;
+        }
+        PrincipalMap principalMap = subject.getPrincipalMap();
+        if (principalMap == null) {
+            return Collections.emptyList();
+        }
+        Object claimedRoles = principalMap.getPrincipal(ROLES_CLAIM);
+        if (claimedRoles instanceof List<?>) {
+            return (List<String>) claimedRoles;
+        }
+        return Collections.emptyList();
+    }
+
+    private String getCurrentUserId(SubjectSum subject) {
+        Object principal = subject.getPrincipal();
+        return principal == null ? null : String.valueOf(principal);
+    }
+
+    private boolean writeError(HttpServletResponse response, HttpStatus 
status, String message) throws IOException {
+        response.setStatus(status.value());
+        response.setCharacterEncoding("UTF-8");
+        response.setContentType("application/json");
+        try (PrintWriter writer = response.getWriter()) {
+            writer.write(JsonUtil.toJson(Map.of("code", status.value(), "msg", 
message)));
+        }
+        return false;
+    }
+}
diff --git 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/AuthTokenController.java
 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/AuthTokenController.java
index 0432ad6955..4aa3abb954 100644
--- 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/AuthTokenController.java
+++ 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/AuthTokenController.java
@@ -22,22 +22,29 @@ import static 
org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
 import com.usthe.sureness.subject.SubjectSum;
 import com.usthe.sureness.util.SurenessContextHolder;
 import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.hertzbeat.common.entity.dto.Message;
+import org.apache.hertzbeat.common.entity.manager.AuthToken;
 import org.apache.hertzbeat.manager.service.AccountService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 /**
- * Generate TOKEN API
+ * API Token Management Controller
  */
-@Tag(name = "Generate TOKEN API")
+@Tag(name = "API Token Management")
 @RestController
 @RequestMapping(value = "/api/account/token", produces = 
{APPLICATION_JSON_VALUE})
 @Slf4j
@@ -47,8 +54,10 @@ public class AuthTokenController {
     private AccountService accountService;
 
     @PostMapping("/generate")
-    @Operation(summary = "Use refresh TOKEN to re-acquire TOKEN", description 
= "Use refresh TOKEN to re-acquire TOKEN")
-    public ResponseEntity<Message<Map<String, String>>> generateToken() {
+    @Operation(summary = "Generate a new API token", description = "Generate a 
new API token for integrations, optionally with expiration")
+    public ResponseEntity<Message<Map<String, String>>> generateToken(
+            @RequestParam(value = "name", required = false) 
@Parameter(description = "Token name/description") String name,
+            @RequestParam(value = "expireSeconds", required = false) 
@Parameter(description = "Expiration time in seconds, null means never expire") 
Long expireSeconds) {
         SubjectSum subjectSum = SurenessContextHolder.getBindSubject();
         if (subjectSum == null) {
             return ResponseEntity.ok(Message.fail(FAIL_CODE, "No login user"));
@@ -57,12 +66,53 @@ public class AuthTokenController {
             return ResponseEntity.ok(Message.fail(FAIL_CODE, "No permission"));
         }
         try {
-            String token = accountService.generateToken();
+            String token = accountService.generateToken(name, expireSeconds);
             Map<String, String> rep = Collections.singletonMap("token", token);
-            return ResponseEntity.ok(Message.success(rep));   
+            return ResponseEntity.ok(Message.success(rep));
         } catch (Exception e) {
             log.error("generate token error", e);
             return ResponseEntity.ok(Message.fail(FAIL_CODE, "Generate token 
error"));
         }
     }
+
+    @GetMapping
+    @Operation(summary = "List all API tokens", description = "List all active 
non-expiring API tokens")
+    public ResponseEntity<Message<List<AuthToken>>> listTokens() {
+        SubjectSum subjectSum = SurenessContextHolder.getBindSubject();
+        if (subjectSum == null) {
+            return ResponseEntity.ok(Message.fail(FAIL_CODE, "No login user"));
+        }
+        if (!subjectSum.hasRole("admin")) {
+            return ResponseEntity.ok(Message.fail(FAIL_CODE, "No permission"));
+        }
+        try {
+            List<AuthToken> tokens = accountService.listTokens();
+            return ResponseEntity.ok(Message.success(tokens));
+        } catch (Exception e) {
+            log.error("list tokens error", e);
+            return ResponseEntity.ok(Message.fail(FAIL_CODE, "List tokens 
error"));
+        }
+    }
+
+    @DeleteMapping("/{id}")
+    @Operation(summary = "Delete/revoke an API token", description = "Delete 
an API token to revoke its access")
+    public ResponseEntity<Message<Void>> deleteToken(
+            @PathVariable("id") @Parameter(description = "Token ID") Long id) {
+        SubjectSum subjectSum = SurenessContextHolder.getBindSubject();
+        if (subjectSum == null) {
+            return ResponseEntity.ok(Message.fail(FAIL_CODE, "No login user"));
+        }
+        if (!subjectSum.hasRole("admin")) {
+            return ResponseEntity.ok(Message.fail(FAIL_CODE, "No permission"));
+        }
+        try {
+            accountService.deleteToken(id);
+            return ResponseEntity.ok(Message.success(null));
+        } catch (javax.naming.AuthenticationException e) {
+            return ResponseEntity.ok(Message.fail(FAIL_CODE, "No permission"));
+        } catch (Exception e) {
+            log.error("delete token error", e);
+            return ResponseEntity.ok(Message.fail(FAIL_CODE, "Delete token 
error"));
+        }
+    }
 }
diff --git 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/AuthTokenDao.java
 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/AuthTokenDao.java
new file mode 100644
index 0000000000..d185cdaecd
--- /dev/null
+++ 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/AuthTokenDao.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.dao;
+
+import org.apache.hertzbeat.common.entity.manager.AuthToken;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * AuthToken DAO
+ */
+public interface AuthTokenDao extends JpaRepository<AuthToken, Long>, 
JpaSpecificationExecutor<AuthToken> {
+
+    /**
+     * Find all active tokens (status = 0)
+     */
+    List<AuthToken> findByStatus(Byte status);
+
+    /**
+     * Find active tokens created by the given user
+     */
+    List<AuthToken> findByStatusAndCreator(Byte status, String creator);
+
+    /**
+     * Check if an active token exists with the given hash
+     */
+    boolean existsByTokenHashAndStatus(String tokenHash, Byte status);
+
+    /**
+     * Update last used time for a token identified by its hash
+     */
+    @Modifying
+    @Transactional
+    @Query("UPDATE AuthToken t SET t.lastUsedTime = :lastUsedTime WHERE 
t.tokenHash = :tokenHash")
+    void updateLastUsedTime(@Param("tokenHash") String tokenHash, 
@Param("lastUsedTime") LocalDateTime lastUsedTime);
+}
diff --git 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/AccountService.java
 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/AccountService.java
index 6820ad043b..58fe2c1c7e 100644
--- 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/AccountService.java
+++ 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/AccountService.java
@@ -17,8 +17,11 @@
 
 package org.apache.hertzbeat.manager.service;
 
+import java.util.List;
 import java.util.Map;
 import javax.naming.AuthenticationException;
+
+import org.apache.hertzbeat.common.entity.manager.AuthToken;
 import org.apache.hertzbeat.manager.pojo.dto.LoginDto;
 import org.apache.hertzbeat.manager.pojo.dto.RefreshTokenResponse;
 
@@ -28,6 +31,7 @@ import 
org.apache.hertzbeat.manager.pojo.dto.RefreshTokenResponse;
 public interface AccountService {
     /**
      * Account password login to obtain associated user information
+     *
      * @param loginDto loginDto
      * @return token info
      * @throws AuthenticationException when authentication is failed
@@ -36,6 +40,7 @@ public interface AccountService {
 
     /**
      * Use refresh TOKEN to re-acquire TOKEN
+     *
      * @param refreshToken refreshToken
      * @return token and refresh token
      * @throws Exception failed to refresh
@@ -43,8 +48,50 @@ public interface AccountService {
     RefreshTokenResponse refreshToken(String refreshToken) throws Exception;
 
     /**
-     * Generate no expired token
-     * @return token
+     * Generate a managed API token and persist it for lifecycle management
+     *
+     * @param tokenName     optional name/description for the token
+     * @param expireSeconds optional expiration time in seconds, null means 
never expire
+     * @return token string
+     */
+    String generateToken(String tokenName, Long expireSeconds) throws 
AuthenticationException;
+
+    /**
+     * List all API tokens
+     *
+     * @return list of auth tokens
+     */
+    List<AuthToken> listTokens();
+
+    /**
+     * Delete/revoke an API token by id
+     *
+     * @param id token id
+     */
+    void deleteToken(Long id) throws AuthenticationException;
+
+    /**
+     * Check the status of a managed token (revocation + expiration)
+     *
+     * @param tokenValue the raw token string
+     * @return null if the token is valid, otherwise a rejection reason string
+     */
+    String checkTokenStatus(String tokenValue);
+
+    /**
+     * Check whether the current account behind a managed token is still valid
+     * and still owns the roles claimed in that token.
+     *
+     * @param userId        token subject
+     * @param claimedRoles  roles embedded in the token claims
+     * @return null if the token owner is still valid, otherwise a rejection 
reason string
+     */
+    String checkManagedTokenAccess(String userId, List<String> claimedRoles);
+
+    /**
+     * Update the last used time of a managed token
+     *
+     * @param tokenValue the raw token string
      */
-    String generateToken() throws AuthenticationException;
+    void touchTokenLastUsedTime(String tokenValue);
 }
diff --git 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AccountServiceImpl.java
 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AccountServiceImpl.java
index 3b74469764..1cadceb28d 100644
--- 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AccountServiceImpl.java
+++ 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AccountServiceImpl.java
@@ -17,6 +17,8 @@
 
 package org.apache.hertzbeat.manager.service.impl;
 
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
 import com.usthe.sureness.provider.SurenessAccount;
 import com.usthe.sureness.provider.SurenessAccountProvider;
 import com.usthe.sureness.provider.ducument.DocumentAccountProvider;
@@ -25,22 +27,29 @@ import com.usthe.sureness.util.JsonWebTokenUtil;
 import com.usthe.sureness.util.Md5Util;
 import com.usthe.sureness.util.SurenessContextHolder;
 import io.jsonwebtoken.Claims;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import javax.naming.AuthenticationException;
-
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.alert.util.CryptoUtils;
+import org.apache.hertzbeat.common.entity.manager.AuthToken;
 import org.apache.hertzbeat.common.util.JsonUtil;
+import org.apache.hertzbeat.manager.dao.AuthTokenDao;
 import org.apache.hertzbeat.manager.pojo.dto.LoginDto;
 import org.apache.hertzbeat.manager.pojo.dto.RefreshTokenResponse;
 import org.apache.hertzbeat.manager.service.AccountService;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.core.Ordered;
 import org.springframework.core.annotation.Order;
 import org.springframework.stereotype.Service;
 
+import javax.naming.AuthenticationException;
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
 /**
  * Implementation of Account service
  */
@@ -56,6 +65,41 @@ public class AccountServiceImpl implements AccountService {
      */
     private static final long PERIOD_TIME = 3600L;
 
+    /**
+     * Non-expiring managed token duration: 100 years in seconds
+     */
+    private static final long NON_EXPIRING_TOKEN_SECONDS = 365L * 24 * 3600 * 
100;
+
+    private static final byte TOKEN_STATUS_ACTIVE = 0;
+
+    /**
+     * Custom JWT claim key to mark tokens as managed (persisted in DB for 
lifecycle management).
+     * Only tokens with this claim will be validated against the database.
+     * Legacy tokens without this claim are allowed to pass through for 
backward compatibility.
+     */
+    public static final String CLAIM_MANAGED = "managed";
+
+    /**
+     * Minimum interval (in minutes) between lastUsedTime DB updates for the 
same token.
+     * Reduces write pressure under high-frequency requests.
+     */
+    private static final long LAST_USED_UPDATE_INTERVAL_MINUTES = 5;
+
+    /**
+     * Local cache: tokenHash -> isActive.
+     * TTL 60s to reduce DB queries; invalidated on token revocation.
+     */
+    private final Cache<String, Boolean> tokenStatusCache = 
Caffeine.newBuilder()
+        .maximumSize(1000)
+        .expireAfterWrite(60, TimeUnit.SECONDS)
+        .build();
+
+    /**
+     * Tracks when each token's lastUsedTime was last written to DB.
+     * Used to throttle DB writes to at most once per 
LAST_USED_UPDATE_INTERVAL_MINUTES.
+     */
+    private final ConcurrentHashMap<String, LocalDateTime> 
lastUsedWriteTimestamps = new ConcurrentHashMap<>();
+
     /**
      * account data provider
      */
@@ -69,6 +113,9 @@ public class AccountServiceImpl implements AccountService {
         this.accountProvider = accountProvider;
     }
 
+    @Autowired
+    private AuthTokenDao authTokenDao;
+
     @Override
     public Map<String, String> authGetToken(LoginDto loginDto) throws 
AuthenticationException {
         SurenessAccount account = 
accountProvider.loadAccount(loginDto.getIdentifier());
@@ -121,9 +168,9 @@ public class AccountServiceImpl implements AccountService {
     }
 
     @Override
-    public String generateToken() throws AuthenticationException {
-        SubjectSum subjectSum = SurenessContextHolder.getBindSubject();
-        String userId = String.valueOf(subjectSum.getPrincipal());
+    public String generateToken(String tokenName, Long expireSeconds) throws 
AuthenticationException {
+        SubjectSum subjectSum = requireCurrentSubject();
+        String userId = getCurrentUserId(subjectSum);
         SurenessAccount account = accountProvider.loadAccount(userId);
         if (account == null) {
             throw new AuthenticationException("Not Exists This Token Mapping 
Account");
@@ -132,11 +179,114 @@ public class AccountServiceImpl implements 
AccountService {
             throw new AuthenticationException("Expired or Illegal Account");
         }
         List<String> roles = account.getOwnRoles();
-        return issueApiToken(userId, roles);
+        String token = issueApiToken(userId, roles, expireSeconds);
+
+        // Persist token metadata for management
+        String tokenHash = CryptoUtils.sha256Hex(token);
+        String tokenMask = maskToken(token);
+        LocalDateTime expireTime = null;
+        if (expireSeconds != null && expireSeconds > 0) {
+            expireTime = LocalDateTime.now().plusSeconds(expireSeconds);
+        }
+        AuthToken authToken = AuthToken.builder()
+            .name(tokenName)
+            .tokenHash(tokenHash)
+            .tokenMask(tokenMask)
+            .status(TOKEN_STATUS_ACTIVE)
+            .creator(userId)
+            .expireTime(expireTime)
+            .build();
+        authTokenDao.save(authToken);
+
+        return token;
     }
 
-    private String issueAccessToken(String userId, List<String> roles, Long 
expirationMillis) {
-        return JsonWebTokenUtil.issueJwt(userId, expirationMillis, roles, new 
HashMap<>(0));
+    private String issueAccessToken(String userId, List<String> roles, long 
expirationSeconds) {
+        return JsonWebTokenUtil.issueJwt(userId, expirationSeconds, roles, new 
HashMap<>(0));
+    }
+
+    @Override
+    public List<AuthToken> listTokens() {
+        SubjectSum subjectSum = SurenessContextHolder.getBindSubject();
+        if (subjectSum == null) {
+            return List.of();
+        }
+        if (subjectSum.hasRole("admin")) {
+            return authTokenDao.findByStatus(TOKEN_STATUS_ACTIVE);
+        }
+        return authTokenDao.findByStatusAndCreator(TOKEN_STATUS_ACTIVE, 
getCurrentUserId(subjectSum));
+    }
+
+    @Override
+    public void deleteToken(Long id) throws AuthenticationException {
+        SubjectSum subjectSum = requireCurrentSubject();
+        String userId = getCurrentUserId(subjectSum);
+        AuthToken token = authTokenDao.findById(id).orElse(null);
+        if (token == null) {
+            return;
+        }
+        if (!subjectSum.hasRole("admin") && !StringUtils.equals(userId, 
token.getCreator())) {
+            throw new AuthenticationException("No permission");
+        }
+        tokenStatusCache.invalidate(token.getTokenHash());
+        lastUsedWriteTimestamps.remove(token.getTokenHash());
+        authTokenDao.deleteById(id);
+    }
+
+    @Override
+    public String checkTokenStatus(String tokenValue) {
+        String tokenHash = CryptoUtils.sha256Hex(tokenValue);
+        Boolean active = tokenStatusCache.get(tokenHash,
+            hash -> authTokenDao.existsByTokenHashAndStatus(hash, 
TOKEN_STATUS_ACTIVE));
+        if (!Boolean.TRUE.equals(active)) {
+            return "Token has been revoked";
+        }
+        return null;
+    }
+
+    @Override
+    public String checkManagedTokenAccess(String userId, List<String> 
claimedRoles) {
+        if (StringUtils.isBlank(userId)) {
+            return "Token owner account is no longer valid";
+        }
+        SurenessAccount account = accountProvider.loadAccount(userId);
+        if (account == null || account.isDisabledAccount() || 
account.isExcessiveAttempts()) {
+            return "Token owner account is no longer valid";
+        }
+        if (claimedRoles == null || claimedRoles.isEmpty()) {
+            return null;
+        }
+        List<String> currentRoles = account.getOwnRoles();
+        if (currentRoles == null || !new 
HashSet<>(currentRoles).containsAll(claimedRoles)) {
+            return "Token permissions are outdated";
+        }
+        return null;
+    }
+
+    @Override
+    public void touchTokenLastUsedTime(String tokenValue) {
+        String tokenHash = CryptoUtils.sha256Hex(tokenValue);
+        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime lastWrite = lastUsedWriteTimestamps.get(tokenHash);
+        if (lastWrite != null && 
lastWrite.plusMinutes(LAST_USED_UPDATE_INTERVAL_MINUTES).isAfter(now)) {
+            return;
+        }
+        lastUsedWriteTimestamps.put(tokenHash, now);
+        authTokenDao.updateLastUsedTime(tokenHash, now);
+    }
+
+    /**
+     * Issue a api token with the "managed" claim.
+     * This claim distinguishes new managed tokens from legacy unmanaged 
tokens,
+     * allowing the validation filter to only check managed tokens against the 
DB.
+     *
+     * @param expireSeconds optional expiration in seconds, null means never 
expire
+     */
+    private String issueApiToken(String userId, List<String> roles, Long 
expireSeconds) {
+        Map<String, Object> customClaimMap = new HashMap<>(2);
+        customClaimMap.put(CLAIM_MANAGED, true);
+        long effectiveExpire = (expireSeconds != null && expireSeconds > 0) ? 
expireSeconds : NON_EXPIRING_TOKEN_SECONDS;
+        return JsonWebTokenUtil.issueJwt(userId, effectiveExpire, roles, 
customClaimMap);
     }
 
     private String issueRefreshToken(String userId, Long expirationMillis) {
@@ -145,7 +295,20 @@ public class AccountServiceImpl implements AccountService {
         return JsonWebTokenUtil.issueJwt(userId, expirationMillis, 
customClaimMap);
     }
 
-    private String issueApiToken(String userId, List<String> roles) {
-        return issueAccessToken(userId, roles, null);
+    private static String maskToken(String token) {
+        return token.substring(0, 4) + "****" + token.substring(token.length() 
- 4);
+    }
+
+    private SubjectSum requireCurrentSubject() throws AuthenticationException {
+        SubjectSum subjectSum = SurenessContextHolder.getBindSubject();
+        if (subjectSum == null) {
+            throw new AuthenticationException("No login user");
+        }
+        return subjectSum;
+    }
+
+    private String getCurrentUserId(SubjectSum subjectSum) {
+        Object principal = subjectSum.getPrincipal();
+        return principal == null ? null : String.valueOf(principal);
     }
 }
diff --git 
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilterTest.java
 
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilterTest.java
new file mode 100644
index 0000000000..36414d88f2
--- /dev/null
+++ 
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/ApiTokenValidationFilterTest.java
@@ -0,0 +1,263 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.config;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.mockStatic;
+
+import com.usthe.sureness.subject.PrincipalMap;
+import com.usthe.sureness.subject.SubjectSum;
+import com.usthe.sureness.util.SurenessContextHolder;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.List;
+import org.apache.hertzbeat.common.constants.NetworkConstants;
+import org.apache.hertzbeat.manager.service.AccountService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+/**
+ * Test case for {@link ApiTokenValidationFilter}
+ */
+@ExtendWith(MockitoExtension.class)
+class ApiTokenValidationFilterTest {
+
+    private ApiTokenValidationFilter filter;
+
+    @Mock
+    private AccountService accountService;
+
+    @Mock
+    private HttpServletRequest request;
+
+    @Mock
+    private HttpServletResponse response;
+
+    @Mock
+    private PrincipalMap principalMap;
+
+    @BeforeEach
+    void setUp() {
+        filter = new ApiTokenValidationFilter(accountService);
+    }
+
+    @Test
+    void testNoAuthorizationHeaderPassesThrough() throws Exception {
+        
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn(null);
+        SubjectSum subject = mockManagedSubject();
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
+
+            
org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, 
new Object()));
+        }
+    }
+
+    @Test
+    void testNonBearerAuthorizationPassesThrough() throws Exception {
+        
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Basic 
dXNlcjpwYXNz");
+        SubjectSum subject = mockManagedSubject();
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
+
+            
org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, 
new Object()));
+        }
+    }
+
+    @Test
+    void testLegacyTokenPassesThrough() throws Exception {
+        SubjectSum subject = mockSubject();
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
+
+            
org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, 
new Object()));
+            verify(accountService, never()).checkTokenStatus(any());
+        }
+    }
+
+    @Test
+    void testManagedTokenActivePassesThrough() throws Exception {
+        String managedToken = "managed-token";
+        
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + 
managedToken);
+        when(accountService.checkTokenStatus(managedToken)).thenReturn(null);
+        when(accountService.checkManagedTokenAccess("admin", 
List.of("admin"))).thenReturn(null);
+        doNothing().when(accountService).touchTokenLastUsedTime(managedToken);
+        SubjectSum subject = mockManagedSubjectWithClaims();
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
+
+            
org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, 
new Object()));
+            verify(accountService).checkTokenStatus(managedToken);
+            verify(accountService).checkManagedTokenAccess("admin", 
List.of("admin"));
+            verify(accountService).touchTokenLastUsedTime(managedToken);
+        }
+    }
+
+    @Test
+    void testManagedTokenRevokedRejected() throws Exception {
+        String revokedToken = "revoked-token";
+        
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + 
revokedToken);
+        when(accountService.checkTokenStatus(revokedToken)).thenReturn("Token 
has been revoked");
+
+        StringWriter stringWriter = new StringWriter();
+        PrintWriter printWriter = new PrintWriter(stringWriter);
+        when(response.getWriter()).thenReturn(printWriter);
+        SubjectSum subject = mockManagedSubject();
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
+
+            
org.junit.jupiter.api.Assertions.assertFalse(filter.preHandle(request, 
response, new Object()));
+            verify(response).setStatus(401);
+        }
+    }
+
+    @Test
+    void testManagedTokenStatusCheckFailureRejectsRequest() throws Exception {
+        
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer 
managed-token");
+        when(accountService.checkTokenStatus("managed-token")).thenThrow(new 
RuntimeException("DB down"));
+
+        StringWriter stringWriter = new StringWriter();
+        PrintWriter printWriter = new PrintWriter(stringWriter);
+        when(response.getWriter()).thenReturn(printWriter);
+        SubjectSum subject = mockManagedSubject();
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
+
+            
org.junit.jupiter.api.Assertions.assertFalse(filter.preHandle(request, 
response, new Object()));
+            verify(response).setStatus(503);
+            verify(accountService, never()).touchTokenLastUsedTime(any());
+        }
+    }
+
+    @Test
+    void testManagedTokenOutdatedRolesRejected() throws Exception {
+        String managedToken = "managed-token";
+        
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + 
managedToken);
+        when(accountService.checkTokenStatus(managedToken)).thenReturn(null);
+        when(accountService.checkManagedTokenAccess("admin", List.of("admin")))
+                .thenReturn("Token permissions are outdated");
+
+        StringWriter stringWriter = new StringWriter();
+        PrintWriter printWriter = new PrintWriter(stringWriter);
+        when(response.getWriter()).thenReturn(printWriter);
+        SubjectSum subject = mockManagedSubjectWithClaims();
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
+
+            
org.junit.jupiter.api.Assertions.assertFalse(filter.preHandle(request, 
response, new Object()));
+            verify(response).setStatus(401);
+            verify(accountService, 
never()).touchTokenLastUsedTime(managedToken);
+        }
+    }
+
+    @Test
+    void testEmptyBearerTokenPassesThrough() throws Exception {
+        
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer ");
+        SubjectSum subject = mockManagedSubject();
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
+
+            
org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, 
new Object()));
+        }
+    }
+
+    @Test
+    void testTouchLastUsedTimeFailureDoesNotRejectRequest() throws Exception {
+        String managedToken = "managed-token";
+        
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + 
managedToken);
+        when(accountService.checkTokenStatus(managedToken)).thenReturn(null);
+        when(accountService.checkManagedTokenAccess("admin", 
List.of("admin"))).thenReturn(null);
+        // touchTokenLastUsedTime throws exception
+        org.mockito.Mockito.doThrow(new RuntimeException("DB error"))
+                .when(accountService).touchTokenLastUsedTime(managedToken);
+        SubjectSum subject = mockManagedSubjectWithClaims();
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
+
+            
org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, 
new Object()));
+        }
+    }
+
+    @Test
+    void testUnauthenticatedRequestSkipsValidation() throws Exception {
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(null);
+
+            
org.junit.jupiter.api.Assertions.assertTrue(filter.preHandle(request, response, 
new Object()));
+            verify(accountService, never()).checkTokenStatus(any());
+        }
+    }
+
+    @Test
+    void testManagedTokenAccountValidationFailureRejectsRequest() throws 
Exception {
+        String managedToken = "managed-token";
+        
when(request.getHeader(NetworkConstants.AUTHORIZATION)).thenReturn("Bearer " + 
managedToken);
+        when(accountService.checkTokenStatus(managedToken)).thenReturn(null);
+        when(accountService.checkManagedTokenAccess("admin", List.of("admin")))
+                .thenThrow(new RuntimeException("account store unavailable"));
+
+        StringWriter stringWriter = new StringWriter();
+        PrintWriter printWriter = new PrintWriter(stringWriter);
+        when(response.getWriter()).thenReturn(printWriter);
+        SubjectSum subject = mockManagedSubjectWithClaims();
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subject);
+
+            
org.junit.jupiter.api.Assertions.assertFalse(filter.preHandle(request, 
response, new Object()));
+            verify(response).setStatus(503);
+            verify(accountService, 
never()).touchTokenLastUsedTime(managedToken);
+        }
+    }
+
+    private SubjectSum mockSubject() {
+        SubjectSum subjectSum = org.mockito.Mockito.mock(SubjectSum.class);
+        when(subjectSum.getPrincipalMap()).thenReturn(principalMap);
+        return subjectSum;
+    }
+
+    private SubjectSum mockManagedSubject() {
+        SubjectSum subjectSum = mockSubject();
+        when(principalMap.getPrincipal("managed")).thenReturn(true);
+        return subjectSum;
+    }
+
+    private SubjectSum mockManagedSubjectWithClaims() {
+        SubjectSum subjectSum = mockManagedSubject();
+        when(subjectSum.getPrincipal()).thenReturn("admin");
+        when(subjectSum.getRoles()).thenReturn(List.of("admin"));
+        return subjectSum;
+    }
+}
diff --git 
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AuthTokenControllerTest.java
 
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AuthTokenControllerTest.java
new file mode 100644
index 0000000000..58910869dc
--- /dev/null
+++ 
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/AuthTokenControllerTest.java
@@ -0,0 +1,226 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.controller;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import com.usthe.sureness.subject.SubjectSum;
+import com.usthe.sureness.util.SurenessContextHolder;
+import java.time.LocalDateTime;
+import java.util.List;
+import org.apache.hertzbeat.common.constants.CommonConstants;
+import org.apache.hertzbeat.common.entity.manager.AuthToken;
+import org.apache.hertzbeat.manager.service.AccountService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+/**
+ * Test case for {@link AuthTokenController}
+ */
+@ExtendWith(MockitoExtension.class)
+class AuthTokenControllerTest {
+
+    private MockMvc mockMvc;
+
+    @InjectMocks
+    private AuthTokenController authTokenController;
+
+    @Mock
+    private AccountService accountService;
+
+    @BeforeEach
+    void setUp() {
+        this.mockMvc = 
MockMvcBuilders.standaloneSetup(authTokenController).build();
+    }
+
+    @Test
+    void testGenerateTokenSuccess() throws Exception {
+        SubjectSum subjectSum = mockAdminSubject();
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+            when(accountService.generateToken(eq("my-token"), 
eq(3600L))).thenReturn("generated-jwt-token");
+
+            
this.mockMvc.perform(MockMvcRequestBuilders.post("/api/account/token/generate")
+                            .param("name", "my-token")
+                            .param("expireSeconds", "3600"))
+                    .andExpect(status().isOk())
+                    .andExpect(jsonPath("$.code").value((int) 
CommonConstants.SUCCESS_CODE))
+                    
.andExpect(jsonPath("$.data.token").value("generated-jwt-token"));
+        }
+    }
+
+    @Test
+    void testGenerateTokenWithoutOptionalParams() throws Exception {
+        SubjectSum subjectSum = mockAdminSubject();
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+            when(accountService.generateToken(any(), 
any())).thenReturn("generated-jwt-token");
+
+            
this.mockMvc.perform(MockMvcRequestBuilders.post("/api/account/token/generate"))
+                    .andExpect(status().isOk())
+                    .andExpect(jsonPath("$.code").value((int) 
CommonConstants.SUCCESS_CODE))
+                    
.andExpect(jsonPath("$.data.token").value("generated-jwt-token"));
+        }
+    }
+
+    @Test
+    void testGenerateTokenNoLogin() throws Exception {
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(null);
+
+            
this.mockMvc.perform(MockMvcRequestBuilders.post("/api/account/token/generate"))
+                    .andExpect(status().isOk())
+                    .andExpect(jsonPath("$.code").value((int) 
CommonConstants.FAIL_CODE))
+                    .andExpect(jsonPath("$.msg").value("No login user"));
+        }
+    }
+
+    @Test
+    void testGenerateTokenNoPermission() throws Exception {
+        SubjectSum subjectSum = mockUserSubject();
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+            
this.mockMvc.perform(MockMvcRequestBuilders.post("/api/account/token/generate"))
+                    .andExpect(status().isOk())
+                    .andExpect(jsonPath("$.code").value((int) 
CommonConstants.FAIL_CODE))
+                    .andExpect(jsonPath("$.msg").value("No permission"));
+        }
+    }
+
+    @Test
+    void testListTokensSuccess() throws Exception {
+        SubjectSum subjectSum = mockAdminSubject();
+        List<AuthToken> tokens = List.of(
+                
AuthToken.builder().id(1L).name("Token1").tokenHash("hash1").tokenMask("mask1")
+                        .status((byte) 
0).gmtCreate(LocalDateTime.now()).build(),
+                
AuthToken.builder().id(2L).name("Token2").tokenHash("hash2").tokenMask("mask2")
+                        .status((byte) 
0).gmtCreate(LocalDateTime.now()).build()
+        );
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+            when(accountService.listTokens()).thenReturn(tokens);
+
+            
this.mockMvc.perform(MockMvcRequestBuilders.get("/api/account/token"))
+                    .andExpect(status().isOk())
+                    .andExpect(jsonPath("$.code").value((int) 
CommonConstants.SUCCESS_CODE))
+                    .andExpect(jsonPath("$.data.length()").value(2))
+                    .andExpect(jsonPath("$.data[0].name").value("Token1"))
+                    .andExpect(jsonPath("$.data[1].name").value("Token2"));
+        }
+    }
+
+    @Test
+    void testListTokensNoLogin() throws Exception {
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(null);
+
+            
this.mockMvc.perform(MockMvcRequestBuilders.get("/api/account/token"))
+                    .andExpect(status().isOk())
+                    .andExpect(jsonPath("$.code").value((int) 
CommonConstants.FAIL_CODE));
+        }
+    }
+
+    @Test
+    void testListTokensNoPermission() throws Exception {
+        SubjectSum subjectSum = mockUserSubject();
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+            
this.mockMvc.perform(MockMvcRequestBuilders.get("/api/account/token"))
+                    .andExpect(status().isOk())
+                    .andExpect(jsonPath("$.code").value((int) 
CommonConstants.FAIL_CODE))
+                    .andExpect(jsonPath("$.msg").value("No permission"));
+        }
+    }
+
+    @Test
+    void testDeleteTokenSuccess() throws Exception {
+        SubjectSum subjectSum = mockAdminSubject();
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+            doNothing().when(accountService).deleteToken(1L);
+
+            
this.mockMvc.perform(MockMvcRequestBuilders.delete("/api/account/token/1"))
+                    .andExpect(status().isOk())
+                    .andExpect(jsonPath("$.code").value((int) 
CommonConstants.SUCCESS_CODE));
+
+            verify(accountService).deleteToken(1L);
+        }
+    }
+
+    @Test
+    void testDeleteTokenNoLogin() throws Exception {
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(null);
+
+            
this.mockMvc.perform(MockMvcRequestBuilders.delete("/api/account/token/1"))
+                    .andExpect(status().isOk())
+                    .andExpect(jsonPath("$.code").value((int) 
CommonConstants.FAIL_CODE));
+        }
+    }
+
+    @Test
+    void testDeleteTokenNoPermission() throws Exception {
+        SubjectSum subjectSum = mockUserSubject();
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+            
this.mockMvc.perform(MockMvcRequestBuilders.delete("/api/account/token/1"))
+                    .andExpect(status().isOk())
+                    .andExpect(jsonPath("$.code").value((int) 
CommonConstants.FAIL_CODE))
+                    .andExpect(jsonPath("$.msg").value("No permission"));
+
+            verify(accountService, never()).deleteToken(1L);
+        }
+    }
+
+    private SubjectSum mockAdminSubject() {
+        SubjectSum subjectSum = org.mockito.Mockito.mock(SubjectSum.class);
+        when(subjectSum.hasRole("admin")).thenReturn(true);
+        return subjectSum;
+    }
+
+    private SubjectSum mockUserSubject() {
+        SubjectSum subjectSum = org.mockito.Mockito.mock(SubjectSum.class);
+        when(subjectSum.hasRole("admin")).thenReturn(false);
+        return subjectSum;
+    }
+}
diff --git 
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/AccountServiceTest.java
 
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/AccountServiceTest.java
index 3848af58c4..5f1d95d811 100644
--- 
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/AccountServiceTest.java
+++ 
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/AccountServiceTest.java
@@ -17,12 +17,6 @@
 
 package org.apache.hertzbeat.manager.service;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.mockito.Mockito.mockStatic;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
 import com.usthe.sureness.provider.DefaultAccount;
 import com.usthe.sureness.provider.SurenessAccount;
 import com.usthe.sureness.provider.SurenessAccountProvider;
@@ -32,17 +26,36 @@ import com.usthe.sureness.util.Md5Util;
 import com.usthe.sureness.util.SurenessContextHolder;
 import io.jsonwebtoken.Claims;
 import io.jsonwebtoken.MalformedJwtException;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import javax.naming.AuthenticationException;
+import org.apache.hertzbeat.common.entity.manager.AuthToken;
 import org.apache.hertzbeat.common.util.JsonUtil;
+import org.apache.hertzbeat.manager.dao.AuthTokenDao;
 import org.apache.hertzbeat.manager.pojo.dto.LoginDto;
 import org.apache.hertzbeat.manager.pojo.dto.RefreshTokenResponse;
 import org.apache.hertzbeat.manager.service.impl.AccountServiceImpl;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import javax.naming.AuthenticationException;
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 /**
  * test case for {@link AccountServiceImpl}
@@ -54,6 +67,8 @@ class AccountServiceTest {
 
     private SurenessAccountProvider accountProvider;
 
+    private AuthTokenDao authTokenDao;
+
     private final String identifier = "admin";
     private final String password = "hertzbeat";
     private final String salt = "salt1";
@@ -70,7 +85,9 @@ class AccountServiceTest {
     void setUp() {
 
         accountProvider = mock(SurenessAccountProvider.class);
+        authTokenDao = mock(AuthTokenDao.class);
         accountService = new AccountServiceImpl(accountProvider);
+        ReflectionTestUtils.setField(accountService, "authTokenDao", 
authTokenDao);
 
         JsonWebTokenUtil.setDefaultSecretKey(jwt);
     }
@@ -186,13 +203,14 @@ class AccountServiceTest {
                 .setExcessiveAttempts(Boolean.FALSE)
                 .build();
         when(accountProvider.loadAccount(identifier)).thenReturn(account);
+        when(authTokenDao.save(any(AuthToken.class))).thenAnswer(invocation -> 
invocation.getArgument(0));
         SubjectSum subjectSum = mock(SubjectSum.class);
         when(subjectSum.getPrincipal()).thenReturn(identifier);
 
         try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
             
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
 
-            String token = accountService.generateToken();
+            String token = accountService.generateToken(null, null);
 
             assertNull(JsonWebTokenUtil.parseJwt(token).get("refresh", 
Boolean.class));
             Assertions.assertThrows(
@@ -220,7 +238,7 @@ class AccountServiceTest {
 
             Assertions.assertThrows(
                     AuthenticationException.class,
-                    () -> accountService.generateToken()
+                    () -> accountService.generateToken(null, null)
             );
         }
     }
@@ -236,4 +254,305 @@ class AccountServiceTest {
         );
     }
 
+    // ==================== Token Management Tests ====================
+
+    @Test
+    void testGenerateTokenPersistsToDb() throws Exception {
+        SurenessAccount account = buildActiveAccount();
+        when(accountProvider.loadAccount(identifier)).thenReturn(account);
+        when(authTokenDao.save(any(AuthToken.class))).thenAnswer(invocation -> 
invocation.getArgument(0));
+        SubjectSum subjectSum = mockAdminSubject(identifier);
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+            String token = accountService.generateToken("My Token", null);
+
+            assertNotNull(token);
+            ArgumentCaptor<AuthToken> captor = 
ArgumentCaptor.forClass(AuthToken.class);
+            verify(authTokenDao).save(captor.capture());
+            AuthToken saved = captor.getValue();
+            assertEquals("My Token", saved.getName());
+            assertNotNull(saved.getTokenHash());
+            assertNotNull(saved.getTokenMask());
+            assertEquals((byte) 0, saved.getStatus());
+            assertEquals(identifier, saved.getCreator());
+            assertNull(saved.getExpireTime());
+        }
+    }
+
+    @Test
+    void testGenerateTokenNullName() throws Exception {
+        SurenessAccount account = buildActiveAccount();
+        when(accountProvider.loadAccount(identifier)).thenReturn(account);
+        when(authTokenDao.save(any(AuthToken.class))).thenAnswer(invocation -> 
invocation.getArgument(0));
+        SubjectSum subjectSum = mockAdminSubject(identifier);
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+            accountService.generateToken(null, null);
+
+            ArgumentCaptor<AuthToken> captor = 
ArgumentCaptor.forClass(AuthToken.class);
+            verify(authTokenDao).save(captor.capture());
+            assertNull(captor.getValue().getName());
+        }
+    }
+
+    @Test
+    void testGenerateTokenWithExpiration() throws Exception {
+        SurenessAccount account = buildActiveAccount();
+        when(accountProvider.loadAccount(identifier)).thenReturn(account);
+        when(authTokenDao.save(any(AuthToken.class))).thenAnswer(invocation -> 
invocation.getArgument(0));
+        SubjectSum subjectSum = mockAdminSubject(identifier);
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+            LocalDateTime before = LocalDateTime.now();
+            accountService.generateToken("Expiring Token", 7200L);
+
+            ArgumentCaptor<AuthToken> captor = 
ArgumentCaptor.forClass(AuthToken.class);
+            verify(authTokenDao).save(captor.capture());
+            AuthToken saved = captor.getValue();
+            assertNotNull(saved.getExpireTime());
+            
assertTrue(saved.getExpireTime().isAfter(before.plusSeconds(7100)));
+        }
+    }
+
+    @Test
+    void testGenerateTokenHasManagedClaim() throws Exception {
+        SurenessAccount account = buildActiveAccount();
+        when(accountProvider.loadAccount(identifier)).thenReturn(account);
+        when(authTokenDao.save(any(AuthToken.class))).thenAnswer(invocation -> 
invocation.getArgument(0));
+        SubjectSum subjectSum = mockAdminSubject(identifier);
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+            String token = accountService.generateToken("test", null);
+
+            Claims claims = JsonWebTokenUtil.parseJwt(token);
+            assertEquals(Boolean.TRUE, 
claims.get(AccountServiceImpl.CLAIM_MANAGED, Boolean.class));
+        }
+    }
+
+    @Test
+    void testListTokensForAdmin() {
+        List<AuthToken> expected = List.of(
+                AuthToken.builder().id(1L).name("Token1").build(),
+                AuthToken.builder().id(2L).name("Token2").build()
+        );
+        when(authTokenDao.findByStatus((byte) 0)).thenReturn(expected);
+        SubjectSum subjectSum = mockAdminSubject(identifier);
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+            List<AuthToken> result = accountService.listTokens();
+
+            assertEquals(2, result.size());
+            assertEquals("Token1", result.get(0).getName());
+        }
+    }
+
+    @Test
+    void testListTokensForCurrentUser() {
+        List<AuthToken> expected = List.of(
+                
AuthToken.builder().id(1L).name("Token1").creator("tom").build()
+        );
+        when(authTokenDao.findByStatusAndCreator((byte) 0, 
"tom")).thenReturn(expected);
+        SubjectSum subjectSum = mockUserSubject("tom");
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+            List<AuthToken> result = accountService.listTokens();
+
+            assertEquals(1, result.size());
+            assertEquals("Token1", result.get(0).getName());
+            verify(authTokenDao).findByStatusAndCreator((byte) 0, "tom");
+        }
+    }
+
+    @Test
+    void testDeleteTokenInvalidatesCache() throws Exception {
+        AuthToken token = 
AuthToken.builder().id(1L).tokenHash("hash123").creator(identifier).build();
+        when(authTokenDao.findById(1L)).thenReturn(Optional.of(token));
+        SubjectSum subjectSum = mockAdminSubject(identifier);
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+            accountService.deleteToken(1L);
+        }
+
+        verify(authTokenDao).deleteById(1L);
+    }
+
+    @Test
+    void testDeleteTokenRejectsNonOwner() {
+        AuthToken token = 
AuthToken.builder().id(1L).tokenHash("hash123").creator("admin").build();
+        when(authTokenDao.findById(1L)).thenReturn(Optional.of(token));
+        SubjectSum subjectSum = mockUserSubject("tom");
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+            Assertions.assertThrows(AuthenticationException.class, () -> 
accountService.deleteToken(1L));
+        }
+    }
+
+    @Test
+    void testCheckTokenStatusActive() throws Exception {
+        SurenessAccount account = buildActiveAccount();
+        when(accountProvider.loadAccount(identifier)).thenReturn(account);
+        when(authTokenDao.save(any(AuthToken.class))).thenAnswer(invocation -> 
invocation.getArgument(0));
+        SubjectSum subjectSum = mock(SubjectSum.class);
+        when(subjectSum.getPrincipal()).thenReturn(identifier);
+
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+            String token = accountService.generateToken("test", null);
+            when(authTokenDao.existsByTokenHashAndStatus(any(String.class), 
eq((byte) 0))).thenReturn(true);
+
+            String result = accountService.checkTokenStatus(token);
+
+            assertNull(result);
+        }
+    }
+
+    @Test
+    void testCheckTokenStatusRevoked() {
+        when(authTokenDao.existsByTokenHashAndStatus(any(String.class), 
eq((byte) 0))).thenReturn(false);
+
+        String result = accountService.checkTokenStatus("some-revoked-token");
+
+        assertEquals("Token has been revoked", result);
+    }
+
+    @Test
+    void testCheckTokenStatusUsesCache() {
+        when(authTokenDao.existsByTokenHashAndStatus(any(String.class), 
eq((byte) 0))).thenReturn(true);
+
+        // First call - hits DB
+        assertNull(accountService.checkTokenStatus("cached-token"));
+        // Second call - should use cache, no additional DB call
+        assertNull(accountService.checkTokenStatus("cached-token"));
+
+        verify(authTokenDao, 
times(1)).existsByTokenHashAndStatus(any(String.class), eq((byte) 0));
+    }
+
+    @Test
+    void testCheckManagedTokenAccessValid() {
+        
when(accountProvider.loadAccount(identifier)).thenReturn(buildActiveAccount());
+
+        String result = accountService.checkManagedTokenAccess(identifier, 
List.of("admin"));
+
+        assertNull(result);
+    }
+
+    @Test
+    void testCheckManagedTokenAccessRejectsDisabledAccount() {
+        SurenessAccount account = DefaultAccount.builder("app1")
+                .setPassword(Md5Util.md5(password + salt))
+                .setSalt(salt)
+                .setOwnRoles(roles)
+                .setDisabledAccount(Boolean.TRUE)
+                .setExcessiveAttempts(Boolean.FALSE)
+                .build();
+        when(accountProvider.loadAccount(identifier)).thenReturn(account);
+
+        String result = accountService.checkManagedTokenAccess(identifier, 
List.of("admin"));
+
+        assertEquals("Token owner account is no longer valid", result);
+    }
+
+    @Test
+    void testCheckManagedTokenAccessRejectsOutdatedRoles() {
+        SurenessAccount account = DefaultAccount.builder("app1")
+                .setPassword(Md5Util.md5(password + salt))
+                .setSalt(salt)
+                .setOwnRoles(List.of("guest"))
+                .setDisabledAccount(Boolean.FALSE)
+                .setExcessiveAttempts(Boolean.FALSE)
+                .build();
+        when(accountProvider.loadAccount(identifier)).thenReturn(account);
+
+        String result = accountService.checkManagedTokenAccess(identifier, 
List.of("admin"));
+
+        assertEquals("Token permissions are outdated", result);
+    }
+
+    @Test
+    void testTouchTokenLastUsedTime() {
+        accountService.touchTokenLastUsedTime("some-token-value");
+
+        verify(authTokenDao).updateLastUsedTime(any(String.class), 
any(LocalDateTime.class));
+    }
+
+    @Test
+    void testTouchTokenLastUsedTimeThrottled() {
+        // First call - should write to DB
+        accountService.touchTokenLastUsedTime("throttle-token");
+        // Second call within 5 minutes - should be throttled
+        accountService.touchTokenLastUsedTime("throttle-token");
+
+        verify(authTokenDao, times(1)).updateLastUsedTime(any(String.class), 
any(LocalDateTime.class));
+    }
+
+    @Test
+    void testTouchTokenLastUsedTimeDifferentTokensNotThrottled() {
+        accountService.touchTokenLastUsedTime("token-a");
+        accountService.touchTokenLastUsedTime("token-b");
+
+        verify(authTokenDao, times(2)).updateLastUsedTime(any(String.class), 
any(LocalDateTime.class));
+    }
+
+    @Test
+    void testDeleteTokenCacheInvalidation() throws Exception {
+        // Setup: token is active and cached
+        String tokenValue = "token-to-revoke";
+        when(authTokenDao.existsByTokenHashAndStatus(any(String.class), 
eq((byte) 0))).thenReturn(true);
+        assertNull(accountService.checkTokenStatus(tokenValue));
+
+        // Now revoke it
+        AuthToken authToken = 
AuthToken.builder().id(1L).tokenHash("some-hash").creator(identifier).build();
+        when(authTokenDao.findById(1L)).thenReturn(Optional.of(authToken));
+        SubjectSum subjectSum = mockAdminSubject(identifier);
+        try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+            
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+            accountService.deleteToken(1L);
+        }
+
+        // After revoke, the next check should hit DB again (cache invalidated 
for that hash)
+        // Note: the tokenHash in DB differs from sha256(tokenValue), so this 
tests cache invalidation path
+        verify(authTokenDao).findById(1L);
+        verify(authTokenDao).deleteById(1L);
+    }
+
+    private SurenessAccount buildActiveAccount() {
+        return DefaultAccount.builder("app1")
+                .setPassword(Md5Util.md5(password + salt))
+                .setSalt(salt)
+                .setOwnRoles(roles)
+                .setDisabledAccount(Boolean.FALSE)
+                .setExcessiveAttempts(Boolean.FALSE)
+                .build();
+    }
+
+    private SubjectSum mockAdminSubject(String principal) {
+        SubjectSum subjectSum = mock(SubjectSum.class);
+        when(subjectSum.getPrincipal()).thenReturn(principal);
+        when(subjectSum.hasRole("admin")).thenReturn(true);
+        return subjectSum;
+    }
+
+    private SubjectSum mockUserSubject(String principal) {
+        SubjectSum subjectSum = mock(SubjectSum.class);
+        when(subjectSum.getPrincipal()).thenReturn(principal);
+        when(subjectSum.hasRole("admin")).thenReturn(false);
+        return subjectSum;
+    }
 }
diff --git a/hertzbeat-startup/src/main/resources/sureness.yml 
b/hertzbeat-startup/src/main/resources/sureness.yml
index 282e93928a..d366fe8a02 100644
--- a/hertzbeat-startup/src/main/resources/sureness.yml
+++ b/hertzbeat-startup/src/main/resources/sureness.yml
@@ -71,6 +71,9 @@ resourceRole:
   - /api/chat/**===get===[admin,user]
   - /api/chat/**===post===[admin]
   - /api/logs/ingest/**===post===[admin,user]
+  - /api/account/token===get===[admin]
+  - /api/account/token/**===post===[admin]
+  - /api/account/token/**===delete===[admin]
 
 # config the resource restful api that need bypass auth protection
 # rule: api===method 
diff --git 
a/web-app/src/app/routes/alert/alert-integration/alert-integration.component.html
 
b/web-app/src/app/routes/alert/alert-integration/alert-integration.component.html
index f3ae43e0fd..0d6fc7276c 100644
--- 
a/web-app/src/app/routes/alert/alert-integration/alert-integration.component.html
+++ 
b/web-app/src/app/routes/alert/alert-integration/alert-integration.component.html
@@ -43,43 +43,14 @@
   </div>
   <div class="doc-content">
     <ng-container *ngIf="selectedSource">
-      <div style="display: flex; justify-content: space-between">
+      <div style="display: flex; justify-content: space-between; align-items: 
center">
         <h2>{{ selectedSource.name }}</h2>
-        <button [nzLoading]="generateLoading" nz-button nzType="primary" 
(click)="generateToken()" style="margin-right: 16px">{{
-          'alert.integration.token.new' | i18n
-        }}</button>
+        <button nz-button nzType="default" (click)="goToTokenManagement()" 
style="margin-right: 16px">
+          <span nz-icon nzType="setting" nzTheme="outline"></span>
+          {{ 'alert.integration.token.manage' | i18n }}
+        </button>
       </div>
       <markdown [data]="markdownContent"></markdown>
     </ng-container>
   </div>
 </div>
-
-<nz-modal
-  [(nzVisible)]="isModalVisible"
-  nzMaskClosable="false"
-  nzWidth="40%"
-  [nzTitle]="'alert.integration.token.title' | i18n"
-  (nzOnCancel)="handleCancel()"
-  (nzOnOk)="handleOk()"
-  [nzOkText]="'common.button.confirm' | i18n"
-  [nzCancelText]="null"
->
-  <div *nzModalContent class="-inner-content">
-    <div>
-      <p>{{ 'alert.integration.token.desc' | i18n }}</p>
-    </div>
-    <div style="width: 100%">
-      <div style="display: flex; justify-content: flex-end; margin-bottom: 
8px">
-        <button nz-button nzType="primary" (click)="copyToken()">
-          {{ 'common.button.copy' | i18n }}
-        </button>
-      </div>
-      <div style="padding: 12px; border: 1px solid; border-radius: 4px; 
word-wrap: break-word">
-        {{ token }}
-      </div>
-    </div>
-    <div style="text-align: center; font-weight: lighter; margin-top: 16px">
-      <p>{{ 'alert.integration.token.notice' | i18n }}</p>
-    </div>
-  </div>
-</nz-modal>
diff --git 
a/web-app/src/app/routes/alert/alert-integration/alert-integration.component.ts 
b/web-app/src/app/routes/alert/alert-integration/alert-integration.component.ts
index d69174b0c3..57eab71d8f 100644
--- 
a/web-app/src/app/routes/alert/alert-integration/alert-integration.component.ts
+++ 
b/web-app/src/app/routes/alert/alert-integration/alert-integration.component.ts
@@ -25,12 +25,9 @@ import { I18NService } from '@core';
 import { ALAIN_I18N_TOKEN, I18nPipe } from '@delon/theme';
 import { SharedModule } from '@shared';
 import { NzDividerComponent } from 'ng-zorro-antd/divider';
-import { NzModalService } from 'ng-zorro-antd/modal';
 import { NzNotificationService } from 'ng-zorro-antd/notification';
 import { MarkdownModule } from 'ngx-markdown';
 
-import { AuthService } from '../../../service/auth.service';
-
 interface DataSource {
   id: string;
   name: string;
@@ -102,16 +99,11 @@ export class AlertIntegrationComponent implements OnInit {
 
   selectedSource: DataSource | null = null;
   markdownContent: string = '';
-  token: string = '';
-  isModalVisible: boolean = false;
-  generateLoading: boolean = false;
 
   constructor(
     private http: HttpClient,
-    private authSvc: AuthService,
     @Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService,
     private notifySvc: NzNotificationService,
-    private modal: NzModalService,
     private router: Router,
     private route: ActivatedRoute
   ) {}
@@ -147,27 +139,8 @@ export class AlertIntegrationComponent implements OnInit {
     this.router.navigate(['/alert/integration', source.id]);
   }
 
-  public generateToken() {
-    this.generateLoading = true;
-    this.authSvc.generateToken().subscribe(message => {
-      if (message.code === 0) {
-        this.token = message.data?.token;
-        this.isModalVisible = true;
-      } else {
-        this.notifySvc.warning('Failed to generate token', message.msg);
-      }
-      this.generateLoading = false;
-    });
-  }
-
-  handleCancel(): void {
-    this.isModalVisible = false;
-    this.token = '';
-  }
-
-  handleOk(): void {
-    this.isModalVisible = false;
-    this.token = '';
+  goToTokenManagement(): void {
+    this.router.navigateByUrl('/setting/settings/token');
   }
 
   private loadMarkdownContent(source: DataSource) {
@@ -184,14 +157,4 @@ export class AlertIntegrationComponent implements OnInit {
       }
     });
   }
-
-  copyToken() {
-    const el = document.createElement('textarea');
-    el.value = this.token;
-    document.body.appendChild(el);
-    el.select();
-    document.execCommand('copy');
-    document.body.removeChild(el);
-    this.notifySvc.success(this.i18nSvc.fanyi('common.notify.copy-success'), 
this.i18nSvc.fanyi('alert.integration.token.notice'));
-  }
 }
diff --git 
a/web-app/src/app/routes/log/log-integration/log-integration.component.html 
b/web-app/src/app/routes/log/log-integration/log-integration.component.html
index 9e3deba70f..d8525db5cd 100644
--- a/web-app/src/app/routes/log/log-integration/log-integration.component.html
+++ b/web-app/src/app/routes/log/log-integration/log-integration.component.html
@@ -43,43 +43,14 @@
   </div>
   <div class="doc-content">
     <ng-container *ngIf="selectedSource">
-      <div style="display: flex; justify-content: space-between">
+      <div style="display: flex; justify-content: space-between; align-items: 
center">
         <h2>{{ selectedSource.name }}</h2>
-        <button [nzLoading]="generateLoading" nz-button nzType="primary" 
(click)="generateToken()" style="margin-right: 16px">{{
-          'log.integration.token.new' | i18n
-        }}</button>
+        <button nz-button nzType="default" (click)="goToTokenManagement()" 
style="margin-right: 16px">
+          <span nz-icon nzType="setting" nzTheme="outline"></span>
+          {{ 'log.integration.token.manage' | i18n }}
+        </button>
       </div>
       <markdown [data]="markdownContent"></markdown>
     </ng-container>
   </div>
 </div>
-
-<nz-modal
-  [(nzVisible)]="isModalVisible"
-  nzMaskClosable="false"
-  nzWidth="40%"
-  [nzTitle]="'log.integration.token.title' | i18n"
-  (nzOnCancel)="handleCancel()"
-  (nzOnOk)="handleOk()"
-  [nzOkText]="'common.button.confirm' | i18n"
-  [nzCancelText]="null"
->
-  <div *nzModalContent class="-inner-content">
-    <div>
-      <p>{{ 'log.integration.token.desc' | i18n }}</p>
-    </div>
-    <div style="width: 100%">
-      <div style="display: flex; justify-content: flex-end; margin-bottom: 
8px">
-        <button nz-button nzType="primary" (click)="copyToken()">
-          {{ 'common.button.copy' | i18n }}
-        </button>
-      </div>
-      <div style="padding: 12px; border: 1px solid; border-radius: 4px; 
word-wrap: break-word">
-        {{ token }}
-      </div>
-    </div>
-    <div style="text-align: center; font-weight: lighter; margin-top: 16px">
-      <p>{{ 'log.integration.token.notice' | i18n }}</p>
-    </div>
-  </div>
-</nz-modal>
diff --git 
a/web-app/src/app/routes/log/log-integration/log-integration.component.ts 
b/web-app/src/app/routes/log/log-integration/log-integration.component.ts
index 8ca1b9de05..d80408617d 100644
--- a/web-app/src/app/routes/log/log-integration/log-integration.component.ts
+++ b/web-app/src/app/routes/log/log-integration/log-integration.component.ts
@@ -25,12 +25,9 @@ import { I18NService } from '@core';
 import { ALAIN_I18N_TOKEN, I18nPipe } from '@delon/theme';
 import { SharedModule } from '@shared';
 import { NzDividerComponent } from 'ng-zorro-antd/divider';
-import { NzModalService } from 'ng-zorro-antd/modal';
 import { NzNotificationService } from 'ng-zorro-antd/notification';
 import { MarkdownModule } from 'ngx-markdown';
 
-import { AuthService } from '../../../service/auth.service';
-
 interface DataSource {
   id: string;
   name: string;
@@ -57,16 +54,11 @@ export class LogIntegrationComponent implements OnInit {
 
   selectedSource: DataSource | null = null;
   markdownContent: string = '';
-  token: string = '';
-  isModalVisible: boolean = false;
-  generateLoading: boolean = false;
 
   constructor(
     private http: HttpClient,
-    private authSvc: AuthService,
     @Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService,
     private notifySvc: NzNotificationService,
-    private modal: NzModalService,
     private router: Router,
     private route: ActivatedRoute
   ) {}
@@ -102,27 +94,8 @@ export class LogIntegrationComponent implements OnInit {
     this.router.navigate(['/log/integration', source.id]);
   }
 
-  public generateToken() {
-    this.generateLoading = true;
-    this.authSvc.generateToken().subscribe(message => {
-      if (message.code === 0) {
-        this.token = message.data?.token;
-        this.isModalVisible = true;
-      } else {
-        this.notifySvc.warning('Failed to generate token', message.msg);
-      }
-      this.generateLoading = false;
-    });
-  }
-
-  handleCancel(): void {
-    this.isModalVisible = false;
-    this.token = '';
-  }
-
-  handleOk(): void {
-    this.isModalVisible = false;
-    this.token = '';
+  goToTokenManagement(): void {
+    this.router.navigateByUrl('/setting/settings/token');
   }
 
   private loadMarkdownContent(source: DataSource) {
@@ -139,14 +112,4 @@ export class LogIntegrationComponent implements OnInit {
       }
     });
   }
-
-  copyToken() {
-    const el = document.createElement('textarea');
-    el.value = this.token;
-    document.body.appendChild(el);
-    el.select();
-    document.execCommand('copy');
-    document.body.removeChild(el);
-    this.notifySvc.success(this.i18nSvc.fanyi('common.notify.copy-success'), 
this.i18nSvc.fanyi('log.integration.token.notice'));
-  }
 }
diff --git a/web-app/src/app/routes/setting/setting-routing.module.ts 
b/web-app/src/app/routes/setting/setting-routing.module.ts
index 11c1078366..8c17c607dc 100644
--- a/web-app/src/app/routes/setting/setting-routing.module.ts
+++ b/web-app/src/app/routes/setting/setting-routing.module.ts
@@ -28,6 +28,7 @@ import { MessageServerComponent } from 
'./settings/message-server/message-server
 import { ObjectStoreComponent } from 
'./settings/object-store/object-store.component';
 import { SettingsComponent } from './settings/settings.component';
 import { SystemConfigComponent } from 
'./settings/system-config/system-config.component';
+import { TokenManagementComponent } from 
'./settings/token-management/token-management.component';
 import { StatusComponent } from './status/status.component';
 
 const routes: Routes = [
@@ -55,6 +56,11 @@ const routes: Routes = [
         path: 'object-store',
         component: ObjectStoreComponent,
         data: { titleI18n: 'settings.object-store' }
+      },
+      {
+        path: 'token',
+        component: TokenManagementComponent,
+        data: { titleI18n: 'settings.token' }
       }
     ]
   }
diff --git a/web-app/src/app/routes/setting/setting.module.ts 
b/web-app/src/app/routes/setting/setting.module.ts
index 38dbf55bbf..1faa09722c 100644
--- a/web-app/src/app/routes/setting/setting.module.ts
+++ b/web-app/src/app/routes/setting/setting.module.ts
@@ -27,6 +27,7 @@ import { NzCodeEditorModule } from 
'ng-zorro-antd/code-editor';
 import { NzCollapseModule } from 'ng-zorro-antd/collapse';
 import { NzDividerModule } from 'ng-zorro-antd/divider';
 import { NzDropDownModule } from 'ng-zorro-antd/dropdown';
+import { NzEmptyModule } from 'ng-zorro-antd/empty';
 import { NzLayoutModule } from 'ng-zorro-antd/layout';
 import { NzListModule } from 'ng-zorro-antd/list';
 import { NzPaginationComponent } from 'ng-zorro-antd/pagination';
@@ -45,6 +46,7 @@ import { MessageServerComponent } from 
'./settings/message-server/message-server
 import { ObjectStoreComponent } from 
'./settings/object-store/object-store.component';
 import { SettingsComponent } from './settings/settings.component';
 import { SystemConfigComponent } from 
'./settings/system-config/system-config.component';
+import { TokenManagementComponent } from 
'./settings/token-management/token-management.component';
 import { StatusComponent } from './status/status.component';
 
 const COMPONENTS: Array<Type<void>> = [
@@ -54,6 +56,7 @@ const COMPONENTS: Array<Type<void>> = [
   MessageServerComponent,
   SystemConfigComponent,
   ObjectStoreComponent,
+  TokenManagementComponent,
   CollectorComponent,
   StatusComponent,
   SettingPluginsComponent
@@ -78,7 +81,8 @@ const COMPONENTS: Array<Type<void>> = [
     NzRadioModule,
     NzUploadComponent,
     NzPaginationComponent,
-    NzDropDownModule
+    NzDropDownModule,
+    NzEmptyModule
   ],
   declarations: COMPONENTS
 })
diff --git a/web-app/src/app/routes/setting/settings/settings.component.ts 
b/web-app/src/app/routes/setting/settings/settings.component.ts
index 795dd6141b..c86639cc0e 100644
--- a/web-app/src/app/routes/setting/settings/settings.component.ts
+++ b/web-app/src/app/routes/setting/settings/settings.component.ts
@@ -47,6 +47,10 @@ export class SettingsComponent implements AfterViewInit, 
OnDestroy {
     {
       key: 'object-store',
       title: this.i18nSvc.fanyi('settings.object-store')
+    },
+    {
+      key: 'token',
+      title: this.i18nSvc.fanyi('settings.token')
     }
   ];
 
@@ -57,6 +61,7 @@ export class SettingsComponent implements AfterViewInit, 
OnDestroy {
     @Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService
   ) {
     this.router$ = this.router.events.pipe(filter(e => e instanceof 
ActivationEnd)).subscribe(() => this.setActive());
+    this.setActive();
   }
 
   private setActive(): void {
diff --git 
a/web-app/src/app/routes/setting/settings/token-management/token-management.component.html
 
b/web-app/src/app/routes/setting/settings/token-management/token-management.component.html
new file mode 100644
index 0000000000..dd47179286
--- /dev/null
+++ 
b/web-app/src/app/routes/setting/settings/token-management/token-management.component.html
@@ -0,0 +1,143 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+-->
+
+<nz-spin [nzSpinning]="loading">
+  <div class="token-management">
+    <div class="token-header">
+      <p class="token-desc">{{ 'settings.token.desc' | i18n }}</p>
+      <button nz-button nzType="primary" [nzLoading]="generateLoading" 
(click)="showGenerateModal()">
+        <span nz-icon nzType="plus" nzTheme="outline"></span>
+        {{ 'settings.token.generate' | i18n }}
+      </button>
+    </div>
+
+    <nz-table #tokenTable [nzData]="tokens" [nzShowPagination]="false" 
[nzBordered]="true" nzSize="middle">
+      <thead>
+        <tr>
+          <th>{{ 'settings.token.name' | i18n }}</th>
+          <th>{{ 'settings.token.value' | i18n }}</th>
+          <th>{{ 'settings.token.creator' | i18n }}</th>
+          <th>{{ 'settings.token.create-time' | i18n }}</th>
+          <th>{{ 'settings.token.expire-time' | i18n }}</th>
+          <th>{{ 'settings.token.last-used' | i18n }}</th>
+          <th nzWidth="120px">{{ 'common.edit' | i18n }}</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr *ngFor="let token of tokenTable.data">
+          <td>{{ token.name }}</td>
+          <td>
+            <code>{{ token.tokenMask }}</code>
+          </td>
+          <td>{{ token.creator }}</td>
+          <td>{{ token.gmtCreate | date : 'yyyy-MM-dd HH:mm:ss' }}</td>
+          <td>
+            <ng-container *ngIf="token.expireTime; else neverExpire">
+              <span [class.token-expired-text]="isExpired(token)">
+                {{ token.expireTime | date : 'yyyy-MM-dd HH:mm:ss' }}
+              </span>
+              <nz-tag *ngIf="isExpired(token)" nzColor="error" 
style="margin-left: 4px">{{ 'settings.token.expired' | i18n }}</nz-tag>
+            </ng-container>
+            <ng-template #neverExpire>
+              <nz-tag nzColor="success">{{ 'settings.token.expire.never' | 
i18n }}</nz-tag>
+            </ng-template>
+          </td>
+          <td>{{ token.lastUsedTime ? (token.lastUsedTime | date : 'yyyy-MM-dd 
HH:mm:ss') : '-' }}</td>
+          <td>
+            <button nz-button nzType="link" nzDanger 
(click)="confirmDelete(token)">
+              <span nz-icon nzType="delete" nzTheme="outline"></span>
+              {{ 'common.button.delete' | i18n }}
+            </button>
+          </td>
+        </tr>
+      </tbody>
+    </nz-table>
+
+    <div *ngIf="tokens.length === 0 && !loading" class="empty-state">
+      <nz-empty [nzNotFoundContent]="'settings.token.empty' | i18n"></nz-empty>
+    </div>
+  </div>
+</nz-spin>
+
+<!-- Generate Token Name Modal -->
+<nz-modal
+  [(nzVisible)]="isGenerateModalVisible"
+  [nzTitle]="'settings.token.generate-title' | i18n"
+  (nzOnCancel)="handleGenerateCancel()"
+  (nzOnOk)="doGenerateToken()"
+  [nzOkText]="'settings.token.generate' | i18n"
+  [nzCancelText]="'common.button.cancel' | i18n"
+>
+  <div *nzModalContent>
+    <form nz-form nzLayout="vertical">
+      <nz-form-item>
+        <nz-form-label nzRequired>{{ 'settings.token.name' | i18n 
}}</nz-form-label>
+        <nz-form-control [nzErrorTip]="'validation.required' | i18n">
+          <input nz-input [(ngModel)]="newTokenName" name="tokenName" required 
[placeholder]="'settings.token.name-placeholder' | i18n" />
+        </nz-form-control>
+      </nz-form-item>
+      <nz-form-item>
+        <nz-form-label>{{ 'settings.token.expire-time' | i18n 
}}</nz-form-label>
+        <nz-form-control>
+          <nz-select [(ngModel)]="selectedExpire" name="expireTime">
+            <nz-option *ngFor="let opt of expireOptions" 
[nzValue]="opt.seconds" [nzLabel]="opt.label"></nz-option>
+          </nz-select>
+        </nz-form-control>
+      </nz-form-item>
+    </form>
+  </div>
+</nz-modal>
+
+<!-- Token Display Modal -->
+<nz-modal
+  [(nzVisible)]="isTokenModalVisible"
+  [nzMaskClosable]="false"
+  nzWidth="50%"
+  [nzTitle]="'settings.token.generated-title' | i18n"
+  (nzOnCancel)="handleTokenModalClose()"
+  (nzOnOk)="handleTokenModalClose()"
+  [nzOkText]="'common.button.confirm' | i18n"
+  [nzCancelText]="null"
+>
+  <div *nzModalContent>
+    <nz-alert nzType="warning" [nzMessage]="'settings.token.notice' | i18n" 
nzShowIcon></nz-alert>
+    <div style="margin-top: 16px">
+      <div style="display: flex; justify-content: flex-end; margin-bottom: 
8px">
+        <button nz-button nzType="primary" nzSize="small" 
(click)="copyToken()">
+          <span nz-icon nzType="copy" nzTheme="outline"></span>
+          {{ 'common.button.copy' | i18n }}
+        </button>
+      </div>
+      <div
+        style="
+          padding: 12px;
+          border: 1px solid #d9d9d9;
+          border-radius: 4px;
+          word-wrap: break-word;
+          word-break: break-all;
+          font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, 
monospace;
+          font-size: 13px;
+          background: #fafafa;
+        "
+      >
+        {{ generatedToken }}
+      </div>
+    </div>
+  </div>
+</nz-modal>
diff --git 
a/web-app/src/app/routes/setting/settings/token-management/token-management.component.less
 
b/web-app/src/app/routes/setting/settings/token-management/token-management.component.less
new file mode 100644
index 0000000000..7090140c36
--- /dev/null
+++ 
b/web-app/src/app/routes/setting/settings/token-management/token-management.component.less
@@ -0,0 +1,26 @@
+@import '@delon/theme/index';
+
+.token-management {
+  width: 100%;
+
+  .token-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16px;
+
+    .token-desc {
+      margin: 0;
+      color: @text-color-secondary;
+      font-size: 14px;
+    }
+  }
+
+  .empty-state {
+    padding: 48px 0;
+  }
+
+  .token-expired-text {
+    color: @error-color;
+  }
+}
\ No newline at end of file
diff --git 
a/web-app/src/app/routes/setting/settings/token-management/token-management.component.ts
 
b/web-app/src/app/routes/setting/settings/token-management/token-management.component.ts
new file mode 100644
index 0000000000..c0dedf84c4
--- /dev/null
+++ 
b/web-app/src/app/routes/setting/settings/token-management/token-management.component.ts
@@ -0,0 +1,185 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Component, Inject, OnInit } from '@angular/core';
+import { I18NService } from '@core';
+import { ALAIN_I18N_TOKEN } from '@delon/theme';
+import { NzModalService } from 'ng-zorro-antd/modal';
+import { NzNotificationService } from 'ng-zorro-antd/notification';
+
+import { AuthService } from '../../../../service/auth.service';
+
+interface AuthToken {
+  id: number;
+  name: string;
+  tokenMask: string;
+  creator: string;
+  gmtCreate: string;
+  expireTime: string;
+  lastUsedTime: string;
+}
+
+interface ExpireOption {
+  label: string;
+  seconds: number;
+}
+
+@Component({
+  selector: 'app-token-management',
+  templateUrl: './token-management.component.html',
+  styleUrls: ['./token-management.component.less']
+})
+export class TokenManagementComponent implements OnInit {
+  tokens: AuthToken[] = [];
+  loading = false;
+  generateLoading = false;
+  isGenerateModalVisible = false;
+  isTokenModalVisible = false;
+  newTokenName = '';
+  selectedExpire: number = -1;
+  generatedToken = '';
+
+  expireOptions: ExpireOption[] = [];
+
+  constructor(
+    private authSvc: AuthService,
+    private notifySvc: NzNotificationService,
+    private modalSvc: NzModalService,
+    @Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService
+  ) {}
+
+  ngOnInit(): void {
+    this.expireOptions = [
+      { label: this.i18nSvc.fanyi('settings.token.expire.never'), seconds: -1 
},
+      { label: this.i18nSvc.fanyi('settings.token.expire.7d'), seconds: 7 * 24 
* 3600 },
+      { label: this.i18nSvc.fanyi('settings.token.expire.30d'), seconds: 30 * 
24 * 3600 },
+      { label: this.i18nSvc.fanyi('settings.token.expire.90d'), seconds: 90 * 
24 * 3600 },
+      { label: this.i18nSvc.fanyi('settings.token.expire.180d'), seconds: 180 
* 24 * 3600 },
+      { label: this.i18nSvc.fanyi('settings.token.expire.365d'), seconds: 365 
* 24 * 3600 }
+    ];
+    this.loadTokens();
+  }
+
+  loadTokens(): void {
+    this.loading = true;
+    this.authSvc.listTokens().subscribe(
+      message => {
+        if (message.code === 0) {
+          this.tokens = message.data || [];
+        } else {
+          this.notifySvc.error(this.i18nSvc.fanyi('common.notify.query-fail'), 
message.msg);
+        }
+        this.loading = false;
+      },
+      error => {
+        this.loading = false;
+      }
+    );
+  }
+
+  showGenerateModal(): void {
+    this.newTokenName = '';
+    this.selectedExpire = -1;
+    this.isGenerateModalVisible = true;
+  }
+
+  handleGenerateCancel(): void {
+    this.isGenerateModalVisible = false;
+  }
+
+  doGenerateToken(): void {
+    if (!this.newTokenName || !this.newTokenName.trim()) {
+      return;
+    }
+    this.generateLoading = true;
+    this.isGenerateModalVisible = false;
+    this.authSvc.generateToken(this.newTokenName.trim(), 
this.selectedExpire).subscribe(
+      message => {
+        if (message.code === 0) {
+          this.generatedToken = message.data?.token || '';
+          this.isTokenModalVisible = true;
+          this.loadTokens();
+        } else {
+          
this.notifySvc.warning(this.i18nSvc.fanyi('settings.token.generate-fail'), 
message.msg);
+        }
+        this.generateLoading = false;
+      },
+      () => {
+        this.generateLoading = false;
+      }
+    );
+  }
+
+  handleTokenModalClose(): void {
+    this.isTokenModalVisible = false;
+    this.generatedToken = '';
+  }
+
+  confirmDelete(token: AuthToken): void {
+    this.modalSvc.confirm({
+      nzTitle: this.i18nSvc.fanyi('settings.token.delete-confirm'),
+      nzContent: this.i18nSvc.fanyi('settings.token.delete-confirm-content', { 
name: token.name || token.tokenMask }),
+      nzOkDanger: true,
+      nzOkText: this.i18nSvc.fanyi('common.button.delete'),
+      nzOnOk: () => {
+        this.authSvc.deleteToken(token.id).subscribe(
+          message => {
+            if (message.code === 0) {
+              
this.notifySvc.success(this.i18nSvc.fanyi('common.notify.delete-success'), '');
+              this.loadTokens();
+            } else {
+              
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.delete-fail'), 
message.msg);
+            }
+          },
+          () => {
+            
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.delete-fail'), '');
+          }
+        );
+      }
+    });
+  }
+
+  isExpired(token: AuthToken): boolean {
+    if (!token.expireTime) {
+      return false;
+    }
+    return new Date(token.expireTime).getTime() < Date.now();
+  }
+
+  copyToken(): void {
+    navigator.clipboard
+      .writeText(this.generatedToken)
+      .then(() => {
+        
this.notifySvc.success(this.i18nSvc.fanyi('common.notify.copy-success'), '');
+      })
+      .catch(() => {
+        const el = document.createElement('textarea');
+        el.value = this.generatedToken;
+        document.body.appendChild(el);
+        el.select();
+        const success = document.execCommand('copy');
+        document.body.removeChild(el);
+        if (success) {
+          
this.notifySvc.success(this.i18nSvc.fanyi('common.notify.copy-success'), '');
+        } else {
+          
this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.copy-fail'), '');
+        }
+      });
+  }
+}
diff --git a/web-app/src/app/service/auth.service.ts 
b/web-app/src/app/service/auth.service.ts
index fe8350ac3f..ccc8ab20ff 100644
--- a/web-app/src/app/service/auth.service.ts
+++ b/web-app/src/app/service/auth.service.ts
@@ -39,7 +39,22 @@ export class AuthService {
     return this.http.post<Message<any>>(`${account_auth_refresh_uri}`, body);
   }
 
-  public generateToken(): Observable<Message<any>> {
-    return this.http.post<Message<any>>(`${account_token}` + '/generate', {});
+  public generateToken(name?: string, expireSeconds?: number): 
Observable<Message<any>> {
+    let params: any = {};
+    if (name) {
+      params.name = name;
+    }
+    if (expireSeconds != null) {
+      params.expireSeconds = expireSeconds;
+    }
+    return this.http.post<Message<any>>(`${account_token}/generate`, {}, { 
params });
+  }
+
+  public listTokens(): Observable<Message<any>> {
+    return this.http.get<Message<any>>(`${account_token}`);
+  }
+
+  public deleteToken(id: number): Observable<Message<any>> {
+    return this.http.delete<Message<any>>(`${account_token}/${id}`);
   }
 }
diff --git a/web-app/src/assets/i18n/en-US.json 
b/web-app/src/assets/i18n/en-US.json
index ef905fe312..a2407bf068 100644
--- a/web-app/src/assets/i18n/en-US.json
+++ b/web-app/src/assets/i18n/en-US.json
@@ -104,6 +104,7 @@
   "alert.integration.source.huaweicloud-ces": "Huawei Cloud Eye",
   "alert.integration.source.volcengine": "Volcengine Monitoring",
   "alert.integration.token.desc": "Token you generated that can be used to 
access the HertzBeat API.",
+  "alert.integration.token.manage": "Manage API Tokens",
   "alert.integration.token.new": "Click to Generate Token",
   "alert.integration.token.notice": "Token only be displayed once. Please keep 
your token secure. Do not share it with others.",
   "alert.integration.token.title": "Access Token",
@@ -623,6 +624,7 @@
   "log.integration.source": "Integrated Log Source",
   "log.integration.source.otlp": "OTLP Protocol",
   "log.integration.token.desc": "The generated Token can be used to access the 
HertzBeat log access API",
+  "log.integration.token.manage": "Manage API Tokens",
   "log.integration.token.new": "Click to generate Token",
   "log.integration.token.notice": "This content will only be displayed once. 
Please keep your Token safe and do not disclose it to others.",
   "log.integration.token.title": "Access Authentication Token",
@@ -1001,6 +1003,30 @@
   "settings.system-config.theme.dark": "Dark Theme",
   "settings.system-config.theme.default": "Default Theme",
   "settings.system-config.timezone": "System Time Zone",
+  "settings.token": "API Token Management",
+  "settings.token.desc": "Manage long-lived API tokens for integration access. 
Create or revoke tokens at any time.",
+  "settings.token.generate": "Generate Token",
+  "settings.token.generate-title": "Generate New API Token",
+  "settings.token.generate-fail": "Failed to generate token",
+  "settings.token.generated-title": "Token Generated",
+  "settings.token.name": "Token Name",
+  "settings.token.name-placeholder": "Enter token name",
+  "settings.token.value": "Token Value",
+  "settings.token.creator": "Creator",
+  "settings.token.create-time": "Created At",
+  "settings.token.notice": "This token will only be displayed once. Please 
keep it secure and do not share it. You can revoke it by deleting it from this 
page.",
+  "settings.token.delete-confirm": "Confirm Delete Token",
+  "settings.token.delete-confirm-content": "All integrations using this token 
will lose access. Are you sure you want to delete \"{{name}}\"?",
+  "settings.token.empty": "No API tokens yet. Click the button above to 
generate one.",
+  "settings.token.expire-time": "Expiration",
+  "settings.token.expire.never": "Never",
+  "settings.token.expire.7d": "7 Days",
+  "settings.token.expire.30d": "30 Days",
+  "settings.token.expire.90d": "90 Days",
+  "settings.token.expire.180d": "180 Days",
+  "settings.token.expire.365d": "365 Days",
+  "settings.token.expired": "Expired",
+  "settings.token.last-used": "Last Used",
   "status.component": "Component",
   "status.component.config-state": "Configuration State",
   "status.component.config-state.tip": "When the status calculates mode is 
manual, the configured component service status.",
diff --git a/web-app/src/assets/i18n/zh-CN.json 
b/web-app/src/assets/i18n/zh-CN.json
index 5f6e4bcb7c..6f585e092f 100644
--- a/web-app/src/assets/i18n/zh-CN.json
+++ b/web-app/src/assets/i18n/zh-CN.json
@@ -104,6 +104,7 @@
   "alert.integration.source.huaweicloud-ces": "华为云监控服务",
   "alert.integration.source.volcengine": "火山引擎云监控",
   "alert.integration.token.desc": "生成的 Token 可用于访问 HertzBeat API",
+  "alert.integration.token.manage": "管理 API Token",
   "alert.integration.token.new": "点击生成 Token",
   "alert.integration.token.notice": "此内容只会展示一次,请妥善保管您的 Token,不要泄露给他人",
   "alert.integration.token.title": "访问认证 Token",
@@ -626,6 +627,7 @@
   "log.integration.source": "集成日志源",
   "log.integration.source.otlp": "OTLP 协议",
   "log.integration.token.desc": "生成的 Token 可用于访问 HertzBeat 日志接入API",
+  "log.integration.token.manage": "管理 API Token",
   "log.integration.token.new": "点击生成 Token",
   "log.integration.token.notice": "此内容只会展示一次,请妥善保管您的 Token,不要泄露给他人",
   "log.integration.token.title": "访问认证 Token",
@@ -1004,6 +1006,30 @@
   "settings.system-config.theme.dark": "深色主题",
   "settings.system-config.theme.default": "浅色主题",
   "settings.system-config.timezone": "系统时区",
+  "settings.token": "API Token 管理",
+  "settings.token.desc": "管理用于 API 集成访问的长期 Token,可随时创建或撤销。",
+  "settings.token.generate": "生成 Token",
+  "settings.token.generate-title": "生成新的 API Token",
+  "settings.token.generate-fail": "生成 Token 失败",
+  "settings.token.generated-title": "Token 已生成",
+  "settings.token.name": "Token 名称",
+  "settings.token.name-placeholder": "请输入 Token 名称",
+  "settings.token.value": "Token 值",
+  "settings.token.creator": "创建者",
+  "settings.token.create-time": "创建时间",
+  "settings.token.notice": "此 Token 只会展示一次,请妥善保管,不要泄露给他人。如需撤销授权,可在此页面删除对应 
Token。",
+  "settings.token.delete-confirm": "确认删除 Token",
+  "settings.token.delete-confirm-content": "删除后使用此 Token 
的所有集成将无法访问,确认删除「{{name}}」?",
+  "settings.token.empty": "暂无 API Token,点击上方按钮生成",
+  "settings.token.expire-time": "过期时间",
+  "settings.token.expire.never": "永不过期",
+  "settings.token.expire.7d": "7 天",
+  "settings.token.expire.30d": "30 天",
+  "settings.token.expire.90d": "90 天",
+  "settings.token.expire.180d": "180 天",
+  "settings.token.expire.365d": "365 天",
+  "settings.token.expired": "已过期",
+  "settings.token.last-used": "最近使用时间",
   "status.component": "服务组件",
   "status.component.config-state": "配置状态",
   "status.component.config-state.tip": "当状态统计方式为手动时,配置的组件服务状态。",


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to