@dashadmin/dash-admin 1.3.42 → 1.3.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -178,7 +178,11 @@ const CollapsableSidebarMenu = ({
178
178
  primary: /* @__PURE__ */ jsx(Typography, { children: item.label })
179
179
  }
180
180
  ),
181
- navExpanded && /* @__PURE__ */ jsx("div", { onClick: handleExpandClick, children: sidebarPosition === "bottom" ? localOpen ? /* @__PURE__ */ jsx(ExpandMoreOutlinedIcon, { className: "expand-icon more" }) : /* @__PURE__ */ jsx(ExpandLessOutlinedIcon, { className: "expand-icon less" }) : localOpen ? /* @__PURE__ */ jsx(ExpandLessOutlinedIcon, { className: "expand-icon less" }) : /* @__PURE__ */ jsx(ExpandMoreOutlinedIcon, { className: "expand-icon more" }) })
181
+ navExpanded && // "expand-icon" is ALSO on the wrapper (not just the inner svg): the row's
182
+ // onClick above does closest('.expand-icon') to tell "clicked the arrow"
183
+ // from "clicked the row" — if only the svg had it, clicking the circle's
184
+ // padding (outside the svg's own box) would miss the check and navigate.
185
+ /* @__PURE__ */ jsx("div", { className: "expand-icon expand-icon-wrapper", onClick: handleExpandClick, children: sidebarPosition === "bottom" ? localOpen ? /* @__PURE__ */ jsx(ExpandMoreOutlinedIcon, { className: "expand-icon more" }) : /* @__PURE__ */ jsx(ExpandLessOutlinedIcon, { className: "expand-icon less" }) : localOpen ? /* @__PURE__ */ jsx(ExpandLessOutlinedIcon, { className: "expand-icon less" }) : /* @__PURE__ */ jsx(ExpandMoreOutlinedIcon, { className: "expand-icon more" }) })
182
186
  ]
183
187
  }
184
188
  )
@@ -26,9 +26,14 @@ const getSubmenuBaseStyle = (sidebarPosition, renderAtTop = false) => ({
26
26
  overflowY: "auto",
27
27
  width: sidebarPosition === "top" || sidebarPosition === "bottom" ? "auto" : "260px",
28
28
  minWidth: "200px",
29
- background: "var(--module-bg, #252526)",
30
- boxShadow: "0 4px 12px rgba(0,0,0,0.25)",
31
- borderRadius: 8
29
+ // The wrapper is the SINGLE owner of the panel visuals (surface, border,
30
+ // shadow, radius, padding). The inner ul.dropdown must stay transparent —
31
+ // if both layers draw a border/background, their edges show as a seam line.
32
+ background: "var(--primary-contrast, #252526)",
33
+ border: "1px solid rgba(255,255,255,0.08)",
34
+ boxShadow: "0 10px 15px -3px rgba(0,0,0,0.25), 0 4px 6px -4px rgba(0,0,0,0.4)",
35
+ borderRadius: 12,
36
+ padding: 6
32
37
  });
33
38
  const calculateSubmenuPosition = (rect, sidebarPosition, renderAtTop = false) => {
34
39
  switch (sidebarPosition) {
@@ -1,6 +1,39 @@
1
+ var __async = (__this, __arguments, generator) => {
2
+ return new Promise((resolve, reject) => {
3
+ var fulfilled = (value) => {
4
+ try {
5
+ step(generator.next(value));
6
+ } catch (e) {
7
+ reject(e);
8
+ }
9
+ };
10
+ var rejected = (value) => {
11
+ try {
12
+ step(generator.throw(value));
13
+ } catch (e) {
14
+ reject(e);
15
+ }
16
+ };
17
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
18
+ step((generator = generator.apply(__this, __arguments)).next());
19
+ });
20
+ };
1
21
  import axios from "axios";
2
22
  import constants from "dash-constants/src/DASHAdminSystemConstants";
3
23
  import { dashStorage } from "dash-utils";
24
+ let isRefreshing = false;
25
+ let failedQueue = [];
26
+ const processQueue = (error, token) => {
27
+ failedQueue.forEach((prom) => {
28
+ if (error) {
29
+ prom.reject(error);
30
+ } else {
31
+ prom.resolve(token);
32
+ }
33
+ });
34
+ isRefreshing = false;
35
+ failedQueue = [];
36
+ };
4
37
  const initAxios = () => {
5
38
  let CSRFAuth = false;
6
39
  let options = {
@@ -28,17 +61,76 @@ const initAxios = () => {
28
61
  (response) => {
29
62
  return response;
30
63
  },
31
- (error) => {
32
- var _a;
33
- window.dispatchEvent(new MessageEvent("GlobalError", { data: { error } }));
64
+ (error) => __async(null, null, function* () {
65
+ var _a, _b, _c;
66
+ const originalRequest = error.config;
67
+ const authEndpoints = ["/login", "/auth/refresh", "/logout", "/register"];
68
+ const isAuthEndpoint = authEndpoints.some(
69
+ (endpoint) => {
70
+ var _a2;
71
+ return (_a2 = originalRequest.url) == null ? void 0 : _a2.includes(endpoint);
72
+ }
73
+ );
74
+ if (((_a = error.response) == null ? void 0 : _a.status) === 401 && !isAuthEndpoint && !originalRequest._retry) {
75
+ if (isRefreshing) {
76
+ return new Promise((resolve, reject) => {
77
+ failedQueue.push({ resolve, reject });
78
+ }).then((token) => {
79
+ originalRequest.headers.Authorization = "Bearer " + token;
80
+ return instance(originalRequest);
81
+ }).catch((err) => {
82
+ return Promise.reject(err);
83
+ });
84
+ }
85
+ originalRequest._retry = true;
86
+ isRefreshing = true;
87
+ try {
88
+ const refreshToken = dashStorage.getItem("refreshToken");
89
+ if (!refreshToken) {
90
+ yield handleAuthFailure();
91
+ return Promise.reject(error);
92
+ }
93
+ const refreshResponse = yield axios.post(
94
+ `${constants.system.ADMIN_API_URL}/auth/refresh`,
95
+ { refresh_token: refreshToken }
96
+ );
97
+ const { token, refresh_token } = refreshResponse.data;
98
+ dashStorage.setItem("token", token);
99
+ dashStorage.setItem("refreshToken", refresh_token);
100
+ originalRequest.headers.Authorization = "Bearer " + token;
101
+ processQueue(null, token);
102
+ return instance(originalRequest);
103
+ } catch (refreshError) {
104
+ yield handleAuthFailure();
105
+ processQueue(refreshError, null);
106
+ return Promise.reject(refreshError);
107
+ }
108
+ }
109
+ if (((_b = error.response) == null ? void 0 : _b.status) !== 401) {
110
+ window.dispatchEvent(new MessageEvent("GlobalError", { data: { error } }));
111
+ }
34
112
  if (error.response && error.response.status === 422) {
35
- return Promise.reject(((_a = error.response.data) == null ? void 0 : _a.errors) || error);
113
+ return Promise.reject(((_c = error.response.data) == null ? void 0 : _c.errors) || error);
36
114
  }
37
115
  return Promise.reject(error);
38
- }
116
+ })
39
117
  );
40
118
  return instance;
41
119
  };
120
+ function handleAuthFailure() {
121
+ return __async(this, null, function* () {
122
+ dashStorage.removeItem("token");
123
+ dashStorage.removeItem("refreshToken");
124
+ dashStorage.removeItem("user");
125
+ dashStorage.removeItem("auth");
126
+ dashStorage.removeItem("authenticated");
127
+ dashStorage.removeItem("roles");
128
+ window.dispatchEvent(new MessageEvent("auth:logout"));
129
+ if (!window.location.pathname.includes("/login")) {
130
+ window.location.href = "/login";
131
+ }
132
+ });
133
+ }
42
134
  const useAxios = () => {
43
135
  const axios2 = initAxios();
44
136
  return {
@@ -28,6 +28,8 @@ const logoutFromStorage = () => __async(null, null, function* () {
28
28
  removeCookie("token");
29
29
  dashStorage.removeItem("user");
30
30
  dashStorage.removeItem("auth");
31
+ dashStorage.removeItem("token");
32
+ dashStorage.removeItem("refreshToken");
31
33
  dashStorage.setItem("authenticated", "false");
32
34
  dashStorage.setItem("roles", null);
33
35
  yield clearDeviceStoreAuth();
@@ -46,6 +48,9 @@ var authProvider_default = {
46
48
  if (loginResponse.status >= 200 && loginResponse.status <= 299) {
47
49
  setCookie("token", loginResponse.data.token);
48
50
  dashStorage.setItem("token", loginResponse.data.token);
51
+ if (loginResponse.data.refresh_token) {
52
+ dashStorage.setItem("refreshToken", loginResponse.data.refresh_token);
53
+ }
49
54
  try {
50
55
  let { data: auth } = yield axios.get("/auth/getauth");
51
56
  dashStorage.setItem("roles", JSON.stringify(auth.user.roles));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dashadmin/dash-admin",
3
- "version": "1.3.42",
3
+ "version": "1.3.44",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -390,6 +390,7 @@ const AppMaterialMenu: React.FC<IAppMenuExtended> = props => {
390
390
  const isHorizontal = sidebarPosition === "top" || sidebarPosition === "bottom";
391
391
 
392
392
  return <>
393
+
393
394
  <Box className='sidebar-header'
394
395
 
395
396
  sx={{
@@ -227,7 +227,11 @@ const CollapsableSidebarMenu = ({
227
227
  />
228
228
  )}
229
229
  {navExpanded && (
230
- <div onClick={handleExpandClick}>
230
+ // "expand-icon" is ALSO on the wrapper (not just the inner svg): the row's
231
+ // onClick above does closest('.expand-icon') to tell "clicked the arrow"
232
+ // from "clicked the row" — if only the svg had it, clicking the circle's
233
+ // padding (outside the svg's own box) would miss the check and navigate.
234
+ <div className="expand-icon expand-icon-wrapper" onClick={handleExpandClick}>
231
235
  {/* For bottom position, invert icons since submenu opens upward */}
232
236
  {sidebarPosition === "bottom" ? (
233
237
  localOpen ?
@@ -20,9 +20,14 @@ export const getSubmenuBaseStyle = (
20
20
  overflowY: 'auto',
21
21
  width: sidebarPosition === "top" || sidebarPosition === "bottom" ? 'auto' : '260px',
22
22
  minWidth: '200px',
23
- background: 'var(--module-bg, #252526)',
24
- boxShadow: '0 4px 12px rgba(0,0,0,0.25)',
25
- borderRadius: 8,
23
+ // The wrapper is the SINGLE owner of the panel visuals (surface, border,
24
+ // shadow, radius, padding). The inner ul.dropdown must stay transparent —
25
+ // if both layers draw a border/background, their edges show as a seam line.
26
+ background: 'var(--primary-contrast, #252526)',
27
+ border: '1px solid rgba(255,255,255,0.08)',
28
+ boxShadow: '0 10px 15px -3px rgba(0,0,0,0.25), 0 4px 6px -4px rgba(0,0,0,0.4)',
29
+ borderRadius: 12,
30
+ padding: 6,
26
31
  });
27
32
 
28
33
  /**
@@ -305,7 +305,7 @@ const AppSidebarMaterial: React.FC<AppSidebarMaterialProps> = (props) => {
305
305
 
306
306
  return (
307
307
  <Box sx={{ display: 'flex' }}>
308
-
308
+
309
309
  <MuiDrawer
310
310
  variant={localNavSize === "small" ? 'temporary' : 'permanent'}
311
311
  anchor={sidebarPosition}
@@ -2,6 +2,23 @@ import axios from 'axios';
2
2
  import constants from 'dash-constants/src/DASHAdminSystemConstants';
3
3
  import { dashStorage } from 'dash-utils';
4
4
 
5
+ // Track refresh state to prevent multiple simultaneous refresh attempts
6
+ let isRefreshing = false;
7
+ let failedQueue: Array<{ resolve: (token: string) => void; reject: (error: any) => void }> = [];
8
+
9
+ const processQueue = (error: any, token?: string) => {
10
+ failedQueue.forEach(prom => {
11
+ if (error) {
12
+ prom.reject(error);
13
+ } else {
14
+ prom.resolve(token!);
15
+ }
16
+ });
17
+
18
+ isRefreshing = false;
19
+ failedQueue = [];
20
+ };
21
+
5
22
  export const initAxios = () => {
6
23
  let CSRFAuth = false;
7
24
 
@@ -36,8 +53,74 @@ export const initAxios = () => {
36
53
  //console.log("Axios Success", response);
37
54
  return response;
38
55
  },
39
- (error) => {
40
- window.dispatchEvent(new MessageEvent('GlobalError', { data: {error:error} }));
56
+ async (error) => {
57
+ const originalRequest = error.config;
58
+
59
+ // Skip refresh for auth-related endpoints to prevent infinite loops
60
+ const authEndpoints = ['/login', '/auth/refresh', '/logout', '/register'];
61
+ const isAuthEndpoint = authEndpoints.some(endpoint =>
62
+ originalRequest.url?.includes(endpoint)
63
+ );
64
+
65
+ // Handle 401 Unauthorized
66
+ if (error.response?.status === 401 && !isAuthEndpoint && !originalRequest._retry) {
67
+ if (isRefreshing) {
68
+ // If already refreshing, queue this request
69
+ return new Promise((resolve, reject) => {
70
+ failedQueue.push({ resolve, reject });
71
+ }).then(token => {
72
+ originalRequest.headers.Authorization = 'Bearer ' + token;
73
+ return instance(originalRequest);
74
+ }).catch(err => {
75
+ return Promise.reject(err);
76
+ });
77
+ }
78
+
79
+ originalRequest._retry = true;
80
+ isRefreshing = true;
81
+
82
+ try {
83
+ const refreshToken = dashStorage.getItem('refreshToken');
84
+
85
+ if (!refreshToken) {
86
+ // No refresh token available, logout user
87
+ await handleAuthFailure();
88
+ return Promise.reject(error);
89
+ }
90
+
91
+ // Attempt to refresh the access token
92
+ const refreshResponse = await axios.post(
93
+ `${constants.system.ADMIN_API_URL}/auth/refresh`,
94
+ { refresh_token: refreshToken }
95
+ );
96
+
97
+ const { token, refresh_token } = refreshResponse.data;
98
+
99
+ // Store new tokens
100
+ dashStorage.setItem('token', token);
101
+ dashStorage.setItem('refreshToken', refresh_token);
102
+
103
+ // Update original request with new token
104
+ originalRequest.headers.Authorization = 'Bearer ' + token;
105
+
106
+ // Process queued requests
107
+ processQueue(null, token);
108
+
109
+ // Retry original request
110
+ return instance(originalRequest);
111
+ } catch (refreshError) {
112
+ // Refresh failed, logout user
113
+ await handleAuthFailure();
114
+ processQueue(refreshError, null);
115
+ return Promise.reject(refreshError);
116
+ }
117
+ }
118
+
119
+ // Dispatch global error event for other errors
120
+ if (error.response?.status !== 401) {
121
+ window.dispatchEvent(new MessageEvent('GlobalError', { data: { error } }));
122
+ }
123
+
41
124
  if (error.response && error.response.status === 422) {
42
125
  /**
43
126
  * Error Handling This is important for the system to parse Form field errors within AutoAdmin.
@@ -45,6 +128,7 @@ export const initAxios = () => {
45
128
  **/
46
129
  return Promise.reject(error.response.data?.errors || error);
47
130
  }
131
+
48
132
  return Promise.reject(error);
49
133
  },
50
134
  );
@@ -52,6 +136,25 @@ export const initAxios = () => {
52
136
  return instance;
53
137
  };
54
138
 
139
+ // Helper function to handle authentication failure
140
+ async function handleAuthFailure() {
141
+ // Clear auth data
142
+ dashStorage.removeItem('token');
143
+ dashStorage.removeItem('refreshToken');
144
+ dashStorage.removeItem('user');
145
+ dashStorage.removeItem('auth');
146
+ dashStorage.removeItem('authenticated');
147
+ dashStorage.removeItem('roles');
148
+
149
+ // Dispatch logout event
150
+ window.dispatchEvent(new MessageEvent('auth:logout'));
151
+
152
+ // Redirect to login if not already there
153
+ if (!window.location.pathname.includes('/login')) {
154
+ window.location.href = '/login';
155
+ }
156
+ }
157
+
55
158
  const useAxios = () => {
56
159
  const axios = initAxios();
57
160
  return {
@@ -9,9 +9,11 @@ export const logoutFromStorage = async () => {
9
9
  removeCookie('token');
10
10
  dashStorage.removeItem('user');
11
11
  dashStorage.removeItem('auth');
12
+ dashStorage.removeItem('token');
13
+ dashStorage.removeItem('refreshToken'); // Clear refresh token for security
12
14
  dashStorage.setItem('authenticated', 'false');
13
15
  dashStorage.setItem('roles', null); // before it was guest role.
14
-
16
+
15
17
  // Clear auth data from device store (Electron/Capacitor)
16
18
  await clearDeviceStoreAuth();
17
19
  };
@@ -40,6 +42,11 @@ export default {
40
42
 
41
43
  dashStorage.setItem('token', loginResponse.data.token);
42
44
 
45
+ // Store refresh token for automatic token refresh
46
+ if (loginResponse.data.refresh_token) {
47
+ dashStorage.setItem('refreshToken', loginResponse.data.refresh_token);
48
+ }
49
+
43
50
  try {
44
51
  let { data: auth } = await axios.get('/auth/getauth');
45
52