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

jinrongtong pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/master by this push:
     new 4b9ed97  [ISSUE #332] Add configuration options for login information, 
supporting ACL or file storage (#333)
4b9ed97 is described below

commit 4b9ed97f8f0389eeb93478247927980c945b3ac5
Author: Crazylychee <[email protected]>
AuthorDate: Sat Jul 5 20:51:42 2025 +0800

    [ISSUE #332] Add configuration options for login information, supporting 
ACL or file storage (#333)
---
 frontend-new/src/api/remoteApi/remoteApi.js        | 156 ++++++++++-----------
 .../dashboard/controller/AclController.java        |  20 +--
 .../org/apache/rocketmq/dashboard/model/User.java  |   4 +-
 .../dashboard/model/request/UserInfoParam.java     |   2 +
 .../dashboard/service/impl/AbstractFileStore.java  |   4 +-
 .../dashboard/service/impl/LoginServiceImpl.java   |  24 +---
 .../service/strategy/AclUserStrategy.java          |  67 +++++++++
 .../service/strategy/FileUserStrategy.java         | 119 ++++++++++++++++
 .../dashboard/service/strategy/UserContext.java    |  54 +++++++
 .../strategy/UserStrategy.java}                    |  12 +-
 src/main/resources/application.yml                 |   6 +
 11 files changed, 350 insertions(+), 118 deletions(-)

diff --git a/frontend-new/src/api/remoteApi/remoteApi.js 
b/frontend-new/src/api/remoteApi/remoteApi.js
index 048ede9..94cec36 100644
--- a/frontend-new/src/api/remoteApi/remoteApi.js
+++ b/frontend-new/src/api/remoteApi/remoteApi.js
@@ -49,7 +49,7 @@ const remoteApi = {
                 if (_redirectHandler) {
                     _redirectHandler(); // 如果设置了重定向处理函数,则调用它
                 }
-                return { __isRedirectHandled: true };
+                return {__isRedirectHandled: true};
             }
 
             return response;
@@ -77,24 +77,24 @@ const remoteApi = {
     listUsers: async (brokerAddress) => {
         const params = new URLSearchParams();
         if (brokerAddress) params.append('brokerAddress', brokerAddress);
-        const response = await 
remoteApi._fetch(remoteApi.buildUrl(`/acl/listUsers?${params.toString()}`));
+        const response = await 
remoteApi._fetch(remoteApi.buildUrl(`/acl/acls.query?${params.toString()}`));
         return await response.json();
     },
 
     createUser: async (brokerAddress, userInfo) => {
-        const response = await 
remoteApi._fetch(remoteApi.buildUrl('/acl/createUser'), {
+        const response = await 
remoteApi._fetch(remoteApi.buildUrl('/acl/createUser.do'), {
             method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({ brokerAddress, userInfo })
+            headers: {'Content-Type': 'application/json'},
+            body: JSON.stringify({brokerAddress, userInfo})
         });
         return await response.json(); // 返回字符串消息
     },
 
     updateUser: async (brokerAddress, userInfo) => {
-        const response = await 
remoteApi._fetch(remoteApi.buildUrl('/acl/updateUser'), {
+        const response = await 
remoteApi._fetch(remoteApi.buildUrl('/acl/updateUser.do'), {
             method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({ brokerAddress, userInfo })
+            headers: {'Content-Type': 'application/json'},
+            body: JSON.stringify({brokerAddress, userInfo})
         });
         return await response.json();
     },
@@ -103,7 +103,7 @@ const remoteApi = {
         const params = new URLSearchParams();
         if (brokerAddress) params.append('brokerAddress', brokerAddress);
         params.append('username', username);
-        const response = await 
remoteApi._fetch(remoteApi.buildUrl(`/acl/deleteUser?${params.toString()}`), {
+        const response = await 
remoteApi._fetch(remoteApi.buildUrl(`/acl/deleteUser.do?${params.toString()}`), 
{
             method: 'DELETE'
         });
         return await response.json();
@@ -114,24 +114,24 @@ const remoteApi = {
         const params = new URLSearchParams();
         if (brokerAddress) params.append('brokerAddress', brokerAddress);
         if (searchParam) params.append('searchParam', searchParam);
-        const response = await 
remoteApi._fetch(remoteApi.buildUrl(`/acl/listAcls?${params.toString()}`));
+        const response = await 
remoteApi._fetch(remoteApi.buildUrl(`/acl/acls.query?${params.toString()}`));
         return await response.json();
     },
 
     createAcl: async (brokerAddress, subject, policies) => {
-        const response = await 
remoteApi._fetch(remoteApi.buildUrl('/acl/createAcl'), {
+        const response = await 
remoteApi._fetch(remoteApi.buildUrl('/acl/createAcl.do'), {
             method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({ brokerAddress, subject, policies })
+            headers: {'Content-Type': 'application/json'},
+            body: JSON.stringify({brokerAddress, subject, policies})
         });
         return await response.json();
     },
 
     updateAcl: async (brokerAddress, subject, policies) => {
-        const response = await 
remoteApi._fetch(remoteApi.buildUrl('/acl/updateAcl'), {
+        const response = await 
remoteApi._fetch(remoteApi.buildUrl('/acl/updateAcl.do'), {
             method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({ brokerAddress, subject, policies })
+            headers: {'Content-Type': 'application/json'},
+            body: JSON.stringify({brokerAddress, subject, policies})
         });
         return await response.json();
     },
@@ -141,7 +141,7 @@ const remoteApi = {
         if (brokerAddress) params.append('brokerAddress', brokerAddress);
         params.append('subject', subject);
         if (resource) params.append('resource', resource);
-        const response = await 
remoteApi._fetch(remoteApi.buildUrl(`/acl/deleteAcl?${params.toString()}`), {
+        const response = await 
remoteApi._fetch(remoteApi.buildUrl(`/acl/deleteAcl.do?${params.toString()}`), {
             method: 'DELETE'
         });
         return await response.json();
@@ -159,7 +159,7 @@ const remoteApi = {
             return data
         } catch (error) {
             console.error("Error querying message by ID:", error);
-            callback({ status: 1, errMsg: "Failed to query message by ID" });
+            callback({status: 1, errMsg: "Failed to query message by ID"});
         }
     },
 
@@ -197,7 +197,7 @@ const remoteApi = {
             return data;
         } catch (error) {
             console.error("Error querying DLQ messages by consumer group:", 
error);
-            return { status: 1, errMsg: "Failed to query DLQ messages by 
consumer group" };
+            return {status: 1, errMsg: "Failed to query DLQ messages by 
consumer group"};
         }
     },
     resendDlqMessage: async (msgId, consumerGroup, topic) => {
@@ -217,7 +217,7 @@ const remoteApi = {
             return data;
         } catch (error) {
             console.error("Error resending DLQ message:", error);
-            return { status: 1, errMsg: "Failed to resend DLQ message" };
+            return {status: 1, errMsg: "Failed to resend DLQ message"};
         }
     },
     exportDlqMessage: async (msgId, consumerGroup) => {
@@ -236,7 +236,7 @@ const remoteApi = {
 
             if (!newWindow) {
                 // 浏览器可能会阻止弹窗,需要用户允许
-                return { status: 1, errMsg: "Failed to open new window. Please 
allow pop-ups for this site." };
+                return {status: 1, errMsg: "Failed to open new window. Please 
allow pop-ups for this site."};
             }
 
             // 2. 将 JSON 数据格式化后写入新窗口
@@ -247,10 +247,10 @@ const remoteApi = {
             newWindow.document.write('</body></html>');
             newWindow.document.close(); // 关闭文档流,确保内容显示
 
-            return { status: 0, msg: "导出请求成功,内容已在新页面显示" };
+            return {status: 0, msg: "导出请求成功,内容已在新页面显示"};
         } catch (error) {
             console.error("Error exporting DLQ message:", error);
-            return { status: 1, errMsg: "Failed to export DLQ message: " + 
error.message };
+            return {status: 1, errMsg: "Failed to export DLQ message: " + 
error.message};
         }
     },
 
@@ -267,7 +267,7 @@ const remoteApi = {
             return data;
         } catch (error) {
             console.error("Error batch resending DLQ messages:", error);
-            return { status: 1, errMsg: "Failed to batch resend DLQ messages" 
};
+            return {status: 1, errMsg: "Failed to batch resend DLQ messages"};
         }
     },
 
@@ -372,7 +372,7 @@ const remoteApi = {
             callback(data);
         } catch (error) {
             console.error("Error fetching producer connection list:", error);
-            callback({ status: 1, errMsg: "Failed to fetch producer connection 
list" }); // Simulate error response
+            callback({status: 1, errMsg: "Failed to fetch producer connection 
list"}); // Simulate error response
         }
     },
 
@@ -383,7 +383,7 @@ const remoteApi = {
             return data;
         } catch (error) {
             console.error("Error fetching consumer group list:", error);
-            return { status: 1, errMsg: "Failed to fetch consumer group list" 
};
+            return {status: 1, errMsg: "Failed to fetch consumer group list"};
         }
     },
 
@@ -394,7 +394,7 @@ const remoteApi = {
             return data;
         } catch (error) {
             console.error(`Error refreshing consumer group ${consumerGroup}:`, 
error);
-            return { status: 1, errMsg: `Failed to refresh consumer group 
${consumerGroup}` };
+            return {status: 1, errMsg: `Failed to refresh consumer group 
${consumerGroup}`};
         }
     },
 
@@ -405,7 +405,7 @@ const remoteApi = {
             return data;
         } catch (error) {
             console.error("Error refreshing all consumer groups:", error);
-            return { status: 1, errMsg: "Failed to refresh all consumer 
groups" };
+            return {status: 1, errMsg: "Failed to refresh all consumer 
groups"};
         }
     },
 
@@ -416,7 +416,7 @@ const remoteApi = {
             return data;
         } catch (error) {
             console.error(`Error fetching monitor config for 
${consumeGroupName}:`, error);
-            return { status: 1, errMsg: `Failed to fetch monitor config for 
${consumeGroupName}` };
+            return {status: 1, errMsg: `Failed to fetch monitor config for 
${consumeGroupName}`};
         }
     },
 
@@ -428,13 +428,13 @@ const remoteApi = {
                 headers: {
                     'Content-Type': 'application/json'
                 },
-                body: JSON.stringify({ consumeGroupName, minCount, 
maxDiffTotal })
+                body: JSON.stringify({consumeGroupName, minCount, 
maxDiffTotal})
             });
             const data = await response.json();
             return data;
         } catch (error) {
             console.error("Error creating or updating consumer monitor:", 
error);
-            return { status: 1, errMsg: "Failed to create or update consumer 
monitor" };
+            return {status: 1, errMsg: "Failed to create or update consumer 
monitor"};
         }
     },
 
@@ -445,7 +445,7 @@ const remoteApi = {
             return data;
         } catch (error) {
             console.error(`Error fetching broker name list for 
${consumerGroup}:`, error);
-            return { status: 1, errMsg: `Failed to fetch broker name list for 
${consumerGroup}` };
+            return {status: 1, errMsg: `Failed to fetch broker name list for 
${consumerGroup}`};
         }
     },
 
@@ -456,13 +456,13 @@ const remoteApi = {
                 headers: {
                     'Content-Type': 'application/json'
                 },
-                body: JSON.stringify({ groupName, brokerNameList })
+                body: JSON.stringify({groupName, brokerNameList})
             });
             const data = await response.json();
             return data;
         } catch (error) {
             console.error(`Error deleting consumer group ${groupName}:`, 
error);
-            return { status: 1, errMsg: `Failed to delete consumer group 
${groupName}` };
+            return {status: 1, errMsg: `Failed to delete consumer group 
${groupName}`};
         }
     },
 
@@ -473,7 +473,7 @@ const remoteApi = {
             return data;
         } catch (error) {
             console.error(`Error fetching consumer config for 
${consumerGroup}:`, error);
-            return { status: 1, errMsg: `Failed to fetch consumer config for 
${consumerGroup}` };
+            return {status: 1, errMsg: `Failed to fetch consumer config for 
${consumerGroup}`};
         }
     },
 
@@ -490,7 +490,7 @@ const remoteApi = {
             return data;
         } catch (error) {
             console.error("Error creating or updating consumer:", error);
-            return { status: 1, errMsg: "Failed to create or update consumer" 
};
+            return {status: 1, errMsg: "Failed to create or update consumer"};
         }
     },
 
@@ -501,7 +501,7 @@ const remoteApi = {
             return data;
         } catch (error) {
             console.error(`Error fetching topics for consumer group 
${consumerGroup}:`, error);
-            return { status: 1, errMsg: `Failed to fetch topics for consumer 
group ${consumerGroup}` };
+            return {status: 1, errMsg: `Failed to fetch topics for consumer 
group ${consumerGroup}`};
         }
     },
 
@@ -512,7 +512,7 @@ const remoteApi = {
             return data;
         } catch (error) {
             console.error(`Error fetching consumer connections for 
${consumerGroup}:`, error);
-            return { status: 1, errMsg: `Failed to fetch consumer connections 
for ${consumerGroup}` };
+            return {status: 1, errMsg: `Failed to fetch consumer connections 
for ${consumerGroup}`};
         }
     },
 
@@ -523,7 +523,7 @@ const remoteApi = {
             return data;
         } catch (error) {
             console.error(`Error fetching running info for client ${clientId} 
in group ${consumerGroup}:`, error);
-            return { status: 1, errMsg: `Failed to fetch running info for 
client ${clientId} in group ${consumerGroup}` };
+            return {status: 1, errMsg: `Failed to fetch running info for 
client ${clientId} in group ${consumerGroup}`};
         }
     },
     queryTopicList: async () => {
@@ -532,7 +532,7 @@ const remoteApi = {
             return await response.json();
         } catch (error) {
             console.error("Error fetching topic list:", error);
-            return { status: 1, errMsg: "Failed to fetch topic list" };
+            return {status: 1, errMsg: "Failed to fetch topic list"};
         }
     },
 
@@ -549,7 +549,7 @@ const remoteApi = {
             return await response.json();
         } catch (error) {
             console.error("Error deleting topic:", error);
-            return { status: 1, errMsg: "Failed to delete topic" };
+            return {status: 1, errMsg: "Failed to delete topic"};
         }
     },
 
@@ -559,7 +559,7 @@ const remoteApi = {
             return await response.json();
         } catch (error) {
             console.error("Error fetching topic stats:", error);
-            return { status: 1, errMsg: "Failed to fetch topic stats" };
+            return {status: 1, errMsg: "Failed to fetch topic stats"};
         }
     },
 
@@ -569,7 +569,7 @@ const remoteApi = {
             return await response.json();
         } catch (error) {
             console.error("Error fetching topic route:", error);
-            return { status: 1, errMsg: "Failed to fetch topic route" };
+            return {status: 1, errMsg: "Failed to fetch topic route"};
         }
     },
 
@@ -579,7 +579,7 @@ const remoteApi = {
             return await response.json();
         } catch (error) {
             console.error("Error fetching topic consumers:", error);
-            return { status: 1, errMsg: "Failed to fetch topic consumers" };
+            return {status: 1, errMsg: "Failed to fetch topic consumers"};
         }
     },
 
@@ -589,7 +589,7 @@ const remoteApi = {
             return await response.json();
         } catch (error) {
             console.error("Error fetching consumer groups:", error);
-            return { status: 1, errMsg: "Failed to fetch consumer groups" };
+            return {status: 1, errMsg: "Failed to fetch consumer groups"};
         }
     },
 
@@ -599,7 +599,7 @@ const remoteApi = {
             return await response.json();
         } catch (error) {
             console.error("Error fetching topic config:", error);
-            return { status: 1, errMsg: "Failed to fetch topic config" };
+            return {status: 1, errMsg: "Failed to fetch topic config"};
         }
     },
 
@@ -609,7 +609,7 @@ const remoteApi = {
             return await response.json();
         } catch (error) {
             console.error("Error fetching cluster list:", error);
-            return { status: 1, errMsg: "Failed to fetch cluster list" };
+            return {status: 1, errMsg: "Failed to fetch cluster list"};
         }
     },
 
@@ -625,7 +625,7 @@ const remoteApi = {
             return await response.json();
         } catch (error) {
             console.error("Error creating/updating topic:", error);
-            return { status: 1, errMsg: "Failed to create/update topic" };
+            return {status: 1, errMsg: "Failed to create/update topic"};
         }
     },
 
@@ -641,7 +641,7 @@ const remoteApi = {
             return await response.json();
         } catch (error) {
             console.error("Error resetting consumer offset:", error);
-            return { status: 1, errMsg: "Failed to reset consumer offset" };
+            return {status: 1, errMsg: "Failed to reset consumer offset"};
         }
     },
 
@@ -657,7 +657,7 @@ const remoteApi = {
             return await response.json();
         } catch (error) {
             console.error("Error skipping message accumulate:", error);
-            return { status: 1, errMsg: "Failed to skip message accumulate" };
+            return {status: 1, errMsg: "Failed to skip message accumulate"};
         }
     },
 
@@ -673,7 +673,7 @@ const remoteApi = {
             return await response.json();
         } catch (error) {
             console.error("Error sending topic message:", error);
-            return { status: 1, errMsg: "Failed to send topic message" };
+            return {status: 1, errMsg: "Failed to send topic message"};
         }
     },
 
@@ -684,12 +684,12 @@ const remoteApi = {
                 headers: {
                     'Content-Type': 'application/json',
                 },
-                body: JSON.stringify({ brokerName, topic })
+                body: JSON.stringify({brokerName, topic})
             });
             return await response.json();
         } catch (error) {
             console.error("Error deleting topic by broker:", error);
-            return { status: 1, errMsg: "Failed to delete topic by broker" };
+            return {status: 1, errMsg: "Failed to delete topic by broker"};
         }
     },
 
@@ -700,7 +700,7 @@ const remoteApi = {
             return await response.json();
         } catch (error) {
             console.error("Error fetching ops home page data:", error);
-            return { status: 1, errMsg: "Failed to fetch ops home page data" };
+            return {status: 1, errMsg: "Failed to fetch ops home page data"};
         }
     },
 
@@ -712,7 +712,7 @@ const remoteApi = {
             return await response.json();
         } catch (error) {
             console.error("Error updating NameServer address:", error);
-            return { status: 1, errMsg: "Failed to update NameServer address" 
};
+            return {status: 1, errMsg: "Failed to update NameServer address"};
         }
     },
 
@@ -724,7 +724,7 @@ const remoteApi = {
             return await response.json();
         } catch (error) {
             console.error("Error adding NameServer address:", error);
-            return { status: 1, errMsg: "Failed to add NameServer address" };
+            return {status: 1, errMsg: "Failed to add NameServer address"};
         }
     },
 
@@ -736,7 +736,7 @@ const remoteApi = {
             return await response.json();
         } catch (error) {
             console.error("Error updating VIP Channel status:", error);
-            return { status: 1, errMsg: "Failed to update VIP Channel status" 
};
+            return {status: 1, errMsg: "Failed to update VIP Channel status"};
         }
     },
 
@@ -748,7 +748,7 @@ const remoteApi = {
             return await response.json();
         } catch (error) {
             console.error("Error updating TLS status:", error);
-            return { status: 1, errMsg: "Failed to update TLS status" };
+            return {status: 1, errMsg: "Failed to update TLS status"};
         }
     },
 
@@ -759,7 +759,7 @@ const remoteApi = {
             callback(data);
         } catch (error) {
             console.error("Error fetching cluster list:", error);
-            callback({ status: 1, errMsg: "Failed to fetch cluster list" });
+            callback({status: 1, errMsg: "Failed to fetch cluster list"});
         }
     },
 
@@ -767,16 +767,16 @@ const remoteApi = {
         try {
             const url = new URL(remoteApi.buildUrl('/dashboard/broker.query'));
             url.searchParams.append('date', date);
-            const response = await remoteApi._fetch(url.toString(), { signal: 
AbortSignal.timeout(15000) }); // 15s timeout
+            const response = await remoteApi._fetch(url.toString(), {signal: 
AbortSignal.timeout(15000)}); // 15s timeout
             const data = await response.json();
             callback(data);
         } catch (error) {
             if (error.name === 'TimeoutError') {
                 console.error("Broker history data request timed out:", error);
-                callback({ status: 1, errMsg: "Request timed out for broker 
history data" });
+                callback({status: 1, errMsg: "Request timed out for broker 
history data"});
             } else {
                 console.error("Error fetching broker history data:", error);
-                callback({ status: 1, errMsg: "Failed to fetch broker history 
data" });
+                callback({status: 1, errMsg: "Failed to fetch broker history 
data"});
             }
         }
     },
@@ -786,32 +786,32 @@ const remoteApi = {
             const url = new URL(remoteApi.buildUrl('/dashboard/topic.query'));
             url.searchParams.append('date', date);
             url.searchParams.append('topicName', topicName);
-            const response = await remoteApi._fetch(url.toString(), { signal: 
AbortSignal.timeout(15000) }); // 15s timeout
+            const response = await remoteApi._fetch(url.toString(), {signal: 
AbortSignal.timeout(15000)}); // 15s timeout
             const data = await response.json();
             callback(data);
         } catch (error) {
             if (error.name === 'TimeoutError') {
                 console.error("Topic history data request timed out:", error);
-                callback({ status: 1, errMsg: "Request timed out for topic 
history data" });
+                callback({status: 1, errMsg: "Request timed out for topic 
history data"});
             } else {
                 console.error("Error fetching topic history data:", error);
-                callback({ status: 1, errMsg: "Failed to fetch topic history 
data" });
+                callback({status: 1, errMsg: "Failed to fetch topic history 
data"});
             }
         }
     },
 
     queryTopicCurrentData: async (callback) => {
         try {
-            const response = await 
remoteApi._fetch(remoteApi.buildUrl('/dashboard/topicCurrent.query'), { signal: 
AbortSignal.timeout(15000) }); // 15s timeout
+            const response = await 
remoteApi._fetch(remoteApi.buildUrl('/dashboard/topicCurrent.query'), {signal: 
AbortSignal.timeout(15000)}); // 15s timeout
             const data = await response.json();
             callback(data);
         } catch (error) {
             if (error.name === 'TimeoutError') {
                 console.error("Topic current data request timed out:", error);
-                callback({ status: 1, errMsg: "Request timed out for topic 
current data" });
+                callback({status: 1, errMsg: "Request timed out for topic 
current data"});
             } else {
                 console.error("Error fetching topic current data:", error);
-                callback({ status: 1, errMsg: "Failed to fetch topic current 
data" });
+                callback({status: 1, errMsg: "Failed to fetch topic current 
data"});
             }
         }
     },
@@ -825,7 +825,7 @@ const remoteApi = {
             callback(data);
         } catch (error) {
             console.error("Error fetching broker config:", error);
-            callback({ status: 1, errMsg: "Failed to fetch broker config" });
+            callback({status: 1, errMsg: "Failed to fetch broker config"});
         }
     },
 
@@ -839,7 +839,7 @@ const remoteApi = {
             callback(data);
         } catch (error) {
             console.error("Error fetching proxy home page:", error);
-            callback({ status: 1, errMsg: "Failed to fetch proxy home page" });
+            callback({status: 1, errMsg: "Failed to fetch proxy home page"});
         }
     },
 
@@ -850,14 +850,14 @@ const remoteApi = {
         try {
             const response = await 
remoteApi._fetch(remoteApi.buildUrl("/proxy/addProxyAddr.do"), {
                 method: 'POST',
-                headers: { 'Content-Type': 'application/x-www-form-urlencoded' 
},
-                body: new URLSearchParams({ newProxyAddr }).toString()
+                headers: {'Content-Type': 'application/x-www-form-urlencoded'},
+                body: new URLSearchParams({newProxyAddr}).toString()
             });
             const data = await response.json();
             callback(data);
         } catch (error) {
             console.error("Error adding proxy address:", error);
-            callback({ status: 1, errMsg: "Failed to add proxy address" });
+            callback({status: 1, errMsg: "Failed to add proxy address"});
         }
     },
     login: async (username, password) => {
@@ -882,19 +882,19 @@ const remoteApi = {
             return data;
         } catch (error) {
             console.error("Error logging in:", error);
-            return { status: 1, errMsg: "Failed to log in" };
+            return {status: 1, errMsg: "Failed to log in"};
         }
     },
 
     logout: async () => {
         try {
-            const response = await 
remoteApi._fetch(remoteApi.buildUrl("/login/logout.do"),{
+            const response = await 
remoteApi._fetch(remoteApi.buildUrl("/login/logout.do"), {
                 method: 'POST'
             });
             return await response.json()
-        }catch (error) {
+        } catch (error) {
             console.error("Error logging out:", error);
-            return { status: 1, errMsg: "Failed to log out" };
+            return {status: 1, errMsg: "Failed to log out"};
         }
     }
 };
@@ -939,4 +939,4 @@ const tools = {
     }
 };
 
-export { remoteApi, tools };
+export {remoteApi, tools};
diff --git 
a/src/main/java/org/apache/rocketmq/dashboard/controller/AclController.java 
b/src/main/java/org/apache/rocketmq/dashboard/controller/AclController.java
index 6e22958..787667a 100644
--- a/src/main/java/org/apache/rocketmq/dashboard/controller/AclController.java
+++ b/src/main/java/org/apache/rocketmq/dashboard/controller/AclController.java
@@ -23,6 +23,7 @@ import 
org.apache.rocketmq.dashboard.model.request.UserUpdateRequest;
 import org.apache.rocketmq.dashboard.service.impl.AclServiceImpl;
 import org.apache.rocketmq.remoting.protocol.body.UserInfo;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -31,24 +32,23 @@ import 
org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestMethod;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.ResponseBody;
-import org.springframework.web.bind.annotation.RestController;
 
 import java.util.List;
 
-@RestController
+@Controller
 @RequestMapping("/acl")
 public class AclController {
 
     @Autowired
     private AclServiceImpl aclService;
 
-    @GetMapping("/listUsers")
+    @GetMapping("/users.query")
     @ResponseBody
     public List<UserInfo> listUsers(@RequestParam(required = false) String 
brokerAddress) {
         return aclService.listUsers(brokerAddress);
     }
 
-    @GetMapping("/listAcls")
+    @GetMapping("/acls.query")
     @ResponseBody
     public Object listAcls(
             @RequestParam(required = false) String brokerAddress,
@@ -56,34 +56,34 @@ public class AclController {
         return aclService.listAcls(brokerAddress, searchParam);
     }
 
-    @PostMapping("/createAcl")
+    @PostMapping("/createAcl.do")
     @ResponseBody
     public Object createAcl(@RequestBody PolicyRequest request) {
         aclService.createAcl(request);
         return true;
     }
 
-    @DeleteMapping("/deleteUser")
+    @DeleteMapping("/deleteUser.do")
     @ResponseBody
     public Object deleteUser(@RequestParam(required = false) String 
brokerAddress, @RequestParam String username) {
         aclService.deleteUser(brokerAddress, username);
         return true;
     }
 
-    @RequestMapping(value = "/updateUser", method = RequestMethod.POST, 
produces = "application/json;charset=UTF-8")
+    @RequestMapping(value = "/updateUser.do", method = RequestMethod.POST, 
produces = "application/json;charset=UTF-8")
     @ResponseBody
     public Object updateUser(@RequestBody UserUpdateRequest request) {
         aclService.updateUser(request.getBrokerAddress(), 
request.getUserInfo());
         return true;
     }
 
-    @PostMapping("/createUser")
+    @PostMapping("/createUser.do")
     public Object createUser(@RequestBody UserCreateRequest request) {
         aclService.createUser(request.getBrokerAddress(), 
request.getUserInfo());
         return true;
     }
 
-    @DeleteMapping("/deleteAcl")
+    @DeleteMapping("/deleteAcl.do")
     public Object deleteAcl(
             @RequestParam(required = false) String brokerAddress,
             @RequestParam String subject,
@@ -92,7 +92,7 @@ public class AclController {
         return true;
     }
 
-    @RequestMapping(value = "/updateAcl", method = RequestMethod.POST, 
produces = "application/json;charset=UTF-8")
+    @RequestMapping(value = "/updateAcl.do", method = RequestMethod.POST, 
produces = "application/json;charset=UTF-8")
     @ResponseBody
     public Object updateAcl(@RequestBody PolicyRequest request) {
         aclService.updateAcl(request);
diff --git a/src/main/java/org/apache/rocketmq/dashboard/model/User.java 
b/src/main/java/org/apache/rocketmq/dashboard/model/User.java
index e049297..4cb8099 100644
--- a/src/main/java/org/apache/rocketmq/dashboard/model/User.java
+++ b/src/main/java/org/apache/rocketmq/dashboard/model/User.java
@@ -19,8 +19,8 @@ package org.apache.rocketmq.dashboard.model;
 import org.hibernate.validator.constraints.Range;
 
 public class User {
-    public static final int ORDINARY = 0;
-    public static final int ADMIN = 1;
+    public static final int SUPER = 0;
+    public static final int NORMAL = 1;
 
     private long id;
     private String name;
diff --git 
a/src/main/java/org/apache/rocketmq/dashboard/model/request/UserInfoParam.java 
b/src/main/java/org/apache/rocketmq/dashboard/model/request/UserInfoParam.java
index f2dc8ab..a288c35 100644
--- 
a/src/main/java/org/apache/rocketmq/dashboard/model/request/UserInfoParam.java
+++ 
b/src/main/java/org/apache/rocketmq/dashboard/model/request/UserInfoParam.java
@@ -17,9 +17,11 @@
 
 package org.apache.rocketmq.dashboard.model.request;
 
+import lombok.AllArgsConstructor;
 import lombok.Data;
 
 @Data
+@AllArgsConstructor
 public class UserInfoParam {
     private String username;
     private String password;
diff --git 
a/src/main/java/org/apache/rocketmq/dashboard/service/impl/AbstractFileStore.java
 
b/src/main/java/org/apache/rocketmq/dashboard/service/impl/AbstractFileStore.java
index 0d65ae1..ea85e42 100644
--- 
a/src/main/java/org/apache/rocketmq/dashboard/service/impl/AbstractFileStore.java
+++ 
b/src/main/java/org/apache/rocketmq/dashboard/service/impl/AbstractFileStore.java
@@ -58,7 +58,7 @@ public abstract class AbstractFileStore {
         }
     }
 
-    abstract void load(InputStream inputStream);
+    protected abstract void load(InputStream inputStream);
 
     private void load() {
         load(null);
@@ -66,7 +66,7 @@ public abstract class AbstractFileStore {
 
     private boolean watch() {
         try {
-            FileWatchService fileWatchService = new FileWatchService(new 
String[] {filePath}, new FileWatchService.Listener() {
+            FileWatchService fileWatchService = new FileWatchService(new 
String[]{filePath}, new FileWatchService.Listener() {
                 @Override
                 public void onChanged(String path) {
                     log.info("The file changed, reload the context");
diff --git 
a/src/main/java/org/apache/rocketmq/dashboard/service/impl/LoginServiceImpl.java
 
b/src/main/java/org/apache/rocketmq/dashboard/service/impl/LoginServiceImpl.java
index 31f1613..c217a88 100644
--- 
a/src/main/java/org/apache/rocketmq/dashboard/service/impl/LoginServiceImpl.java
+++ 
b/src/main/java/org/apache/rocketmq/dashboard/service/impl/LoginServiceImpl.java
@@ -17,13 +17,10 @@
 
 package org.apache.rocketmq.dashboard.service.impl;
 
-import jakarta.annotation.Resource;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
-import org.apache.rocketmq.dashboard.config.RMQConfigure;
 import org.apache.rocketmq.dashboard.service.LoginService;
-import org.apache.rocketmq.dashboard.service.UserService;
-import org.apache.rocketmq.dashboard.service.provider.UserInfoProvider;
+import org.apache.rocketmq.dashboard.service.strategy.UserContext;
 import org.apache.rocketmq.dashboard.util.UserInfoContext;
 import org.apache.rocketmq.dashboard.util.WebUtil;
 import org.apache.rocketmq.remoting.protocol.body.UserInfo;
@@ -33,34 +30,29 @@ import 
org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.io.IOException;
-import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 
 @Service
 public class LoginServiceImpl implements LoginService {
     private final Logger logger = LoggerFactory.getLogger(this.getClass());
 
-    @Resource
-    private RMQConfigure rmqConfigure;
-
-    @Autowired
-    private UserService userService;
-
     @Autowired
-    private UserInfoProvider userInfoProvider;
+    private UserContext userContext;
 
 
     @Override
     public boolean login(HttpServletRequest request, HttpServletResponse 
response) {
         String username = (String) WebUtil.getValueFromSession(request, 
WebUtil.USER_NAME);
         if (username != null) {
-            UserInfo userInfo = 
userInfoProvider.getUserInfoByUsername(username);
+            UserInfo userInfo = userContext.queryByUsername(username);
             if (userInfo == null) {
                 auth(request, response);
                 return false;
             }
             UserInfoContext.set(WebUtil.USER_NAME, userInfo);
             return true;
+
         }
         auth(request, response);
         return false;
@@ -69,11 +61,7 @@ public class LoginServiceImpl implements LoginService {
     protected void auth(HttpServletRequest request, HttpServletResponse 
response) {
         try {
             String url = WebUtil.getUrl(request);
-            try {
-                url = URLEncoder.encode(url, "UTF-8");
-            } catch (UnsupportedEncodingException e) {
-                logger.error("url encode:{}", url, e);
-            }
+            url = URLEncoder.encode(url, StandardCharsets.UTF_8);
             logger.debug("redirect url : {}", url);
             WebUtil.redirect(response, request, "/#/login?redirect=" + url);
         } catch (IOException e) {
diff --git 
a/src/main/java/org/apache/rocketmq/dashboard/service/strategy/AclUserStrategy.java
 
b/src/main/java/org/apache/rocketmq/dashboard/service/strategy/AclUserStrategy.java
new file mode 100644
index 0000000..4a8f9d5
--- /dev/null
+++ 
b/src/main/java/org/apache/rocketmq/dashboard/service/strategy/AclUserStrategy.java
@@ -0,0 +1,67 @@
+/*
+ * 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.rocketmq.dashboard.service.strategy;
+
+import lombok.AllArgsConstructor;
+import org.apache.rocketmq.dashboard.service.ClusterInfoService;
+import org.apache.rocketmq.remoting.protocol.body.ClusterInfo;
+import org.apache.rocketmq.remoting.protocol.body.UserInfo;
+import org.apache.rocketmq.remoting.protocol.route.BrokerData;
+import org.apache.rocketmq.tools.admin.MQAdminExt;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+@Service
+@AllArgsConstructor
+public class AclUserStrategy implements UserStrategy {
+
+    private static final Logger log = 
LoggerFactory.getLogger(AclUserStrategy.class);
+
+    private final MQAdminExt mqAdminExt;
+
+    private final ClusterInfoService clusterInfoService;
+
+    @Override
+    public UserInfo getUserInfoByUsername(String username) {
+        ClusterInfo clusterInfo = clusterInfoService.get();
+
+        if (clusterInfo == null || clusterInfo.getBrokerAddrTable() == null || 
clusterInfo.getBrokerAddrTable().isEmpty()) {
+            log.warn("Cluster information is not available or has no broker 
addresses.");
+            return null;
+        }
+        for (BrokerData brokerLiveInfo : 
clusterInfo.getBrokerAddrTable().values()) {
+            if (brokerLiveInfo == null || brokerLiveInfo.getBrokerAddrs() == 
null || brokerLiveInfo.getBrokerAddrs().isEmpty()) {
+                continue;
+            }
+            String brokerAddr = brokerLiveInfo.getBrokerAddrs().get(0L); // 
Assuming 0L is the primary address
+            if (brokerAddr == null) {
+                continue;
+            }
+            try {
+                UserInfo userInfo = mqAdminExt.getUser(brokerAddr, username);
+                if (userInfo != null) {
+                    return userInfo;
+                }
+            } catch (Exception e) {
+                log.warn("Failed to get user {} from broker {}. Trying next 
broker if available. Error: {}", username, brokerAddr, e.getMessage());
+            }
+        }
+        return null;
+    }
+}
diff --git 
a/src/main/java/org/apache/rocketmq/dashboard/service/strategy/FileUserStrategy.java
 
b/src/main/java/org/apache/rocketmq/dashboard/service/strategy/FileUserStrategy.java
new file mode 100644
index 0000000..d5dac40
--- /dev/null
+++ 
b/src/main/java/org/apache/rocketmq/dashboard/service/strategy/FileUserStrategy.java
@@ -0,0 +1,119 @@
+/*
+ * 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.rocketmq.dashboard.service.strategy;
+
+import jakarta.annotation.Resource;
+import jakarta.validation.constraints.NotNull;
+import org.apache.rocketmq.dashboard.config.RMQConfigure;
+import org.apache.rocketmq.dashboard.exception.ServiceException;
+import org.apache.rocketmq.dashboard.model.User;
+import org.apache.rocketmq.dashboard.service.impl.AbstractFileStore;
+import org.apache.rocketmq.remoting.protocol.body.UserInfo;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.stereotype.Service;
+
+import java.io.FileReader;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Service
+public class FileUserStrategy implements UserStrategy, InitializingBean {
+    @Resource
+    private RMQConfigure configure;
+
+    private FileBasedUserInfoStore fileBasedUserInfoStore;
+
+
+    @Override
+    public void afterPropertiesSet() throws Exception {
+        if (configure.isLoginRequired()) {
+            fileBasedUserInfoStore = new FileBasedUserInfoStore(configure);
+        }
+    }
+
+    @Override
+    public UserInfo getUserInfoByUsername(String username) {
+        User user = 
fileBasedUserInfoStore.queryByUsernameAndPassword(username);
+        if (user != null) {
+            return UserInfo.of(user.getName(), user.getPassword(), 
user.getType() == 0 ? "normal" : "super");
+        }
+        return null;
+    }
+
+    public static class FileBasedUserInfoStore extends AbstractFileStore {
+        private static final String FILE_NAME = "users.properties";
+
+        private static Map<String, User> userMap = new ConcurrentHashMap<>();
+
+        public FileBasedUserInfoStore(RMQConfigure configure) {
+            super(configure, FILE_NAME);
+        }
+
+        @Override
+        public void load(InputStream inputStream) {
+            Properties prop = new Properties();
+            try {
+                if (inputStream == null) {
+                    prop.load(new FileReader(filePath));
+                } else {
+                    prop.load(inputStream);
+                }
+            } catch (Exception e) {
+                log.error("load user.properties failed", e);
+                throw new ServiceException(0, String.format("Failed to load 
loginUserInfo property file: %s", filePath));
+            }
+
+            Map<String, User> loadUserMap = new HashMap<>();
+            String[] arrs;
+            int role;
+            for (String key : prop.stringPropertyNames()) {
+                String v = prop.getProperty(key);
+                if (v == null)
+                    continue;
+                arrs = v.split(",", 2);
+                if (arrs.length == 0) {
+                    continue;
+                } else if (arrs.length == 1) {
+                    role = 0;
+                } else {
+                    role = Integer.parseInt(arrs[1].trim());
+                }
+
+                loadUserMap.put(key, new User(key, arrs[0].trim(), role));
+            }
+
+            userMap.clear();
+            userMap.putAll(loadUserMap);
+        }
+
+        public User queryByName(String name) {
+            return userMap.get(name);
+        }
+
+        public User queryByUsernameAndPassword(@NotNull String username) {
+            User user = queryByName(username);
+            if (user != null) {
+                return user.cloneOne();
+            }
+            return null;
+        }
+    }
+}
diff --git 
a/src/main/java/org/apache/rocketmq/dashboard/service/strategy/UserContext.java 
b/src/main/java/org/apache/rocketmq/dashboard/service/strategy/UserContext.java
new file mode 100644
index 0000000..f6df1ad
--- /dev/null
+++ 
b/src/main/java/org/apache/rocketmq/dashboard/service/strategy/UserContext.java
@@ -0,0 +1,54 @@
+/*
+ * 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.rocketmq.dashboard.service.strategy;
+
+import jakarta.annotation.PostConstruct;
+import org.apache.rocketmq.remoting.protocol.body.UserInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+@Component
+public class UserContext {
+    private UserStrategy userStrategy;
+
+    @Autowired
+    private Map<String, UserStrategy> userStrategies;
+
+    @Value("${rocketmq.config.authMode}")
+    private String authMode;
+
+    @PostConstruct
+    public void init() {
+        switch (authMode.toLowerCase()) {
+            case "acl":
+                this.userStrategy = userStrategies.get("aclUserStrategy");
+                break;
+            case "file":
+            default:
+                this.userStrategy = userStrategies.get("fileUserStrategy");
+                break;
+        }
+    }
+
+    public UserInfo queryByUsername(String username) {
+        return userStrategy.getUserInfoByUsername(username);
+    }
+}
diff --git 
a/src/main/java/org/apache/rocketmq/dashboard/model/request/UserInfoParam.java 
b/src/main/java/org/apache/rocketmq/dashboard/service/strategy/UserStrategy.java
similarity index 78%
copy from 
src/main/java/org/apache/rocketmq/dashboard/model/request/UserInfoParam.java
copy to 
src/main/java/org/apache/rocketmq/dashboard/service/strategy/UserStrategy.java
index f2dc8ab..1603d7e 100644
--- 
a/src/main/java/org/apache/rocketmq/dashboard/model/request/UserInfoParam.java
+++ 
b/src/main/java/org/apache/rocketmq/dashboard/service/strategy/UserStrategy.java
@@ -15,14 +15,10 @@
  * limitations under the License.
  */
 
-package org.apache.rocketmq.dashboard.model.request;
+package org.apache.rocketmq.dashboard.service.strategy;
 
-import lombok.Data;
+import org.apache.rocketmq.remoting.protocol.body.UserInfo;
 
-@Data
-public class UserInfoParam {
-    private String username;
-    private String password;
-    private String userStatus;
-    private String userType;
+public interface UserStrategy {
+    UserInfo getUserInfoByUsername(String username);
 }
diff --git a/src/main/resources/application.yml 
b/src/main/resources/application.yml
index ddf624a..47117c7 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -58,6 +58,12 @@ rocketmq:
     ticketKey: ticket
     # must create userInfo file: ${rocketmq.config.dataPath}/users.properties 
if the login is required
     loginRequired: false
+    # Authentication mode for RocketMQ Dashboard
+    # Available options:
+    #   - 'file': Use username/password stored in a file (requires 
'auth.file.path')
+    #   - 'acl':  Use credentials from ACL system (requires 'acl.access.key' 
and 'acl.secret.key')
+    # Default: file
+    authMode: file
     useTLS: false
     proxyAddr: 127.0.0.1:8080
     proxyAddrs:


Reply via email to