strapi-plugin-magic-sessionmanager 4.0.0 → 4.0.2

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.
Files changed (68) hide show
  1. package/README.md +2 -2
  2. package/admin/src/components/LicenseGuard.jsx +6 -6
  3. package/admin/src/components/OnlineUsersWidget.jsx +11 -7
  4. package/admin/src/components/SessionDetailModal.jsx +45 -41
  5. package/admin/src/components/SessionInfoCard.jsx +3 -3
  6. package/admin/src/components/SessionInfoPanel.jsx +31 -21
  7. package/admin/src/index.js +9 -0
  8. package/admin/src/pages/Analytics.jsx +2 -2
  9. package/admin/src/pages/HomePage.jsx +129 -165
  10. package/admin/src/pages/License.jsx +5 -5
  11. package/admin/src/pages/Settings.jsx +148 -144
  12. package/admin/src/pages/SettingsNew.jsx +21 -21
  13. package/admin/src/pages/UpgradePage.jsx +448 -0
  14. package/admin/src/pluginId.js +1 -0
  15. package/admin/src/translations/de.json +294 -15
  16. package/admin/src/translations/en.json +293 -14
  17. package/admin/src/translations/es.json +284 -18
  18. package/admin/src/translations/fr.json +284 -18
  19. package/admin/src/translations/pt.json +284 -18
  20. package/admin/src/utils/parseUserAgent.js +6 -6
  21. package/admin/src/utils/theme.js +85 -0
  22. package/dist/_chunks/{Analytics-mYu_uGwU.mjs → Analytics-DTE_zmRV.mjs} +4 -4
  23. package/dist/_chunks/{Analytics-ioaeEh-E.js → Analytics-lw_JaOVy.js} +4 -4
  24. package/dist/_chunks/{App-DdnUYWbC.js → App-DDKYCjKw.js} +221 -216
  25. package/dist/_chunks/{App-BXpIS12l.mjs → App-DJW1ZNl5.mjs} +221 -216
  26. package/dist/_chunks/{License-C03C2j9P.mjs → License-DaOFuImm.mjs} +6 -10
  27. package/dist/_chunks/{License-DZYrOgcx.js → License-Tk-6UfPl.js} +6 -10
  28. package/dist/_chunks/{OnlineUsersWidget-B8JS1xZu.js → OnlineUsersWidget-C1qTpsws.js} +11 -7
  29. package/dist/_chunks/{OnlineUsersWidget-ArMl0nen.mjs → OnlineUsersWidget-CADphbXG.mjs} +11 -7
  30. package/dist/_chunks/{Settings-0ocB3qHk.mjs → Settings-C9xvckgq.mjs} +200 -188
  31. package/dist/_chunks/{Settings-C6_CqpCC.js → Settings-DyEAuTNQ.js} +200 -188
  32. package/dist/_chunks/UpgradePage-Dssk8A0Z.js +354 -0
  33. package/dist/_chunks/UpgradePage-cINvE9zY.mjs +352 -0
  34. package/dist/_chunks/de-CDA1V0rF.mjs +292 -0
  35. package/dist/_chunks/de-I-Q-pWqu.js +292 -0
  36. package/dist/_chunks/en-Bd7_h-4e.js +292 -0
  37. package/dist/_chunks/en-DzmOCyzQ.mjs +292 -0
  38. package/dist/_chunks/es-BcAx18XG.js +277 -0
  39. package/dist/_chunks/es-Cx-SN6qV.mjs +277 -0
  40. package/dist/_chunks/fr-DCzYMuJ-.js +277 -0
  41. package/dist/_chunks/fr-DXlXE5Eo.mjs +277 -0
  42. package/dist/_chunks/{index-DC8Y0qxx.js → index-CWcvrfXc.js} +52 -49
  43. package/dist/_chunks/{index-DBRS3kt5.mjs → index-DQO9bNP7.mjs} +52 -49
  44. package/dist/_chunks/pt-21-MAb72.js +277 -0
  45. package/dist/_chunks/pt-zsdTSjba.mjs +277 -0
  46. package/dist/_chunks/{useLicense-qgGfMvse.js → useLicense-DtvJOszr.js} +1 -1
  47. package/dist/_chunks/{useLicense-DSLL9n3Y.mjs → useLicense-DxbD4Wf8.mjs} +1 -1
  48. package/dist/admin/index.js +1 -1
  49. package/dist/admin/index.mjs +1 -1
  50. package/dist/server/index.js +142 -33
  51. package/dist/server/index.mjs +142 -33
  52. package/package.json +1 -1
  53. package/server/src/bootstrap.js +76 -4
  54. package/server/src/controllers/session.js +59 -9
  55. package/server/src/middlewares/last-seen.js +5 -4
  56. package/server/src/routes/content-api.js +11 -2
  57. package/server/src/services/notifications.js +10 -10
  58. package/server/src/services/session.js +24 -4
  59. package/dist/_chunks/de-BxFx1pwE.js +0 -23
  60. package/dist/_chunks/de-CdO3s01z.mjs +0 -23
  61. package/dist/_chunks/en-CsPpPJL3.mjs +0 -23
  62. package/dist/_chunks/en-RqmpDHdS.js +0 -23
  63. package/dist/_chunks/es-CuLHazN1.js +0 -23
  64. package/dist/_chunks/es-Dkmjhy9c.mjs +0 -23
  65. package/dist/_chunks/fr-BAJp2yhI.js +0 -23
  66. package/dist/_chunks/fr-Bssg_3UF.mjs +0 -23
  67. package/dist/_chunks/pt-BAP9cKs3.js +0 -23
  68. package/dist/_chunks/pt-BVNoNcuY.mjs +0 -23
package/README.md CHANGED
@@ -136,7 +136,7 @@ npm run develop
136
136
  3. Click it!
137
137
  4. You'll see the dashboard (like the screenshot above)
138
138
 
139
- **That's it! You're done!** 🎉
139
+ **That's it! You're done!**
140
140
 
141
141
  ---
142
142
 
@@ -621,4 +621,4 @@ Language automatically follows your Strapi admin interface setting.
621
621
 
622
622
  ---
623
623
 
624
- **Made with ❤️ for Strapi v5**
624
+ **Made for Strapi v5**
@@ -407,12 +407,12 @@ const LicenseGuard = ({ children }) => {
407
407
  >
408
408
  <Typography variant="omega" style={{ fontSize: '13px', lineHeight: '1.6' }}>
409
409
  {useExistingKey
410
- ? '🔑 Enter your email and license key to activate.'
410
+ ? 'Enter your email and license key to activate.'
411
411
  : useAutoCreate && adminUser && adminUser.email
412
- ? `✨ Click "Activate" to auto-create a license with your account (${adminUser.email})`
412
+ ? `Click "Activate" to auto-create a license with your account (${adminUser.email})`
413
413
  : useAutoCreate
414
- ? 'Click "Activate" to auto-create a license with your admin account'
415
- : '💡 A license will be created with the details below.'
414
+ ? 'Click "Activate" to auto-create a license with your admin account'
415
+ : 'A license will be created with the details below.'
416
416
  }
417
417
  </Typography>
418
418
  </Box>
@@ -476,10 +476,10 @@ const LicenseGuard = ({ children }) => {
476
476
  Ready to activate with your account:
477
477
  </Typography>
478
478
  <Typography variant="pi" style={{ marginBottom: '4px', display: 'block' }}>
479
- 👤 {adminUser.firstname || 'Admin'} {adminUser.lastname || 'User'}
479
+ {adminUser.firstname || 'Admin'} {adminUser.lastname || 'User'}
480
480
  </Typography>
481
481
  <Typography variant="pi" textColor="neutral600">
482
- 📧 {adminUser.email || 'Loading...'}
482
+ {adminUser.email || 'Loading...'}
483
483
  </Typography>
484
484
  </Box>
485
485
  ) : (
@@ -1,14 +1,18 @@
1
1
  import { useState, useEffect, useCallback } from 'react';
2
+ import { useIntl } from 'react-intl';
2
3
  import { Box, Typography, Flex, Grid } from '@strapi/design-system';
3
4
  import { Check, Cross, Clock, User, Monitor } from '@strapi/icons';
4
5
  import { useFetchClient } from '@strapi/strapi/admin';
6
+ import { getTranslation } from '../utils/getTranslation';
5
7
 
6
8
  /**
7
9
  * Online Users Widget - Dashboard widget showing user activity statistics
8
10
  * Styled exactly like Project Statistics
9
11
  */
10
12
  const OnlineUsersWidget = () => {
13
+ const { formatMessage } = useIntl();
11
14
  const { get } = useFetchClient();
15
+ const t = (id, defaultMessage, values) => formatMessage({ id: getTranslation(id), defaultMessage }, values);
12
16
  const [stats, setStats] = useState({
13
17
  onlineNow: 0,
14
18
  offline: 0,
@@ -137,7 +141,7 @@ const OnlineUsersWidget = () => {
137
141
  if (loading) {
138
142
  return (
139
143
  <Box padding={4}>
140
- <Typography variant="pi" textColor="neutral600">Loading...</Typography>
144
+ <Typography variant="pi" textColor="neutral600">{t('common.loading', 'Loading...')}</Typography>
141
145
  </Box>
142
146
  );
143
147
  }
@@ -149,7 +153,7 @@ const OnlineUsersWidget = () => {
149
153
  <Grid.Item col={1}>
150
154
  <StatCard
151
155
  icon={Check}
152
- label="Online Now"
156
+ label={t('widget.stats.onlineNow', 'Online Now')}
153
157
  value={stats.onlineNow}
154
158
  color="success"
155
159
  />
@@ -158,7 +162,7 @@ const OnlineUsersWidget = () => {
158
162
  <Grid.Item col={1}>
159
163
  <StatCard
160
164
  icon={Cross}
161
- label="Offline"
165
+ label={t('widget.stats.offline', 'Offline')}
162
166
  value={stats.offline}
163
167
  color="neutral"
164
168
  />
@@ -167,7 +171,7 @@ const OnlineUsersWidget = () => {
167
171
  <Grid.Item col={1}>
168
172
  <StatCard
169
173
  icon={Clock}
170
- label="Last 15 min"
174
+ label={t('widget.stats.last15min', 'Last 15 min')}
171
175
  value={stats.last15min}
172
176
  color="primary"
173
177
  />
@@ -176,7 +180,7 @@ const OnlineUsersWidget = () => {
176
180
  <Grid.Item col={1}>
177
181
  <StatCard
178
182
  icon={Clock}
179
- label="Last 30 min"
183
+ label={t('widget.stats.last30min', 'Last 30 min')}
180
184
  value={stats.last30min}
181
185
  color="secondary"
182
186
  />
@@ -185,7 +189,7 @@ const OnlineUsersWidget = () => {
185
189
  <Grid.Item col={1}>
186
190
  <StatCard
187
191
  icon={User}
188
- label="Total Users"
192
+ label={t('widget.stats.totalUsers', 'Total Users')}
189
193
  value={stats.totalUsers}
190
194
  color="neutral"
191
195
  />
@@ -194,7 +198,7 @@ const OnlineUsersWidget = () => {
194
198
  <Grid.Item col={1}>
195
199
  <StatCard
196
200
  icon={Cross}
197
- label="Blocked"
201
+ label={t('widget.stats.blocked', 'Blocked')}
198
202
  value={stats.blocked}
199
203
  color="danger"
200
204
  />
@@ -1,4 +1,5 @@
1
1
  import { useState, useEffect } from 'react';
2
+ import { useIntl } from 'react-intl';
2
3
  import styled from 'styled-components';
3
4
  import {
4
5
  Modal,
@@ -25,6 +26,7 @@ import { useFetchClient, useNotification } from '@strapi/strapi/admin';
25
26
  import parseUserAgent from '../utils/parseUserAgent';
26
27
  import pluginId from '../pluginId';
27
28
  import { useLicense } from '../hooks/useLicense';
29
+ import { getTranslation } from '../utils/getTranslation';
28
30
 
29
31
  const TwoColumnGrid = styled.div`
30
32
  display: grid;
@@ -53,6 +55,7 @@ const Section = styled(Box)`
53
55
  `;
54
56
 
55
57
  const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
58
+ const { formatMessage } = useIntl();
56
59
  const { get, post } = useFetchClient();
57
60
  const { toggleNotification } = useNotification();
58
61
  const { isPremium, loading: licenseLoading } = useLicense();
@@ -60,6 +63,7 @@ const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
60
63
  const [showUserAgent, setShowUserAgent] = useState(false);
61
64
  const [geoData, setGeoData] = useState(null);
62
65
  const [geoLoading, setGeoLoading] = useState(false);
66
+ const t = (id, defaultMessage, values) => formatMessage({ id: getTranslation(id), defaultMessage }, values);
63
67
 
64
68
  if (!session) return null;
65
69
 
@@ -82,7 +86,7 @@ const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
82
86
  console.error('[SessionDetailModal] Error fetching geolocation:', err);
83
87
  // Fallback to mock data if API fails
84
88
  setGeoData({
85
- country_flag: '🌍',
89
+ country_flag: '',
86
90
  country: 'Unknown',
87
91
  city: 'Unknown',
88
92
  timezone: 'Unknown',
@@ -98,7 +102,7 @@ const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
98
102
 
99
103
  // Use real data if available, otherwise fallback
100
104
  const premiumData = geoData || {
101
- country_flag: '🌍',
105
+ country_flag: '',
102
106
  country: 'Loading...',
103
107
  city: 'Loading...',
104
108
  timezone: 'Loading...',
@@ -117,7 +121,7 @@ const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
117
121
  const DeviceIcon = getDeviceIcon(deviceInfo.device);
118
122
 
119
123
  const handleTerminate = async () => {
120
- if (!confirm('Are you sure you want to terminate this session?')) {
124
+ if (!confirm(t('modal.confirm.terminate', 'Are you sure you want to terminate this session?'))) {
121
125
  return;
122
126
  }
123
127
 
@@ -127,7 +131,7 @@ const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
127
131
 
128
132
  toggleNotification({
129
133
  type: 'success',
130
- message: 'Session terminated successfully',
134
+ message: t('notifications.success.terminated', 'Session terminated successfully'),
131
135
  });
132
136
 
133
137
  onSessionTerminated();
@@ -136,7 +140,7 @@ const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
136
140
  console.error('[SessionDetailModal] Error:', err);
137
141
  toggleNotification({
138
142
  type: 'danger',
139
- message: 'Failed to terminate session',
143
+ message: t('notifications.error.terminate', 'Failed to terminate session'),
140
144
  });
141
145
  } finally {
142
146
  setTerminating(false);
@@ -188,10 +192,10 @@ const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
188
192
  </Box>
189
193
  <Flex direction="column" alignItems="flex-start">
190
194
  <Typography variant="beta" fontWeight="bold">
191
- Session Details
195
+ {t('modal.title', 'Session Details')}
192
196
  </Typography>
193
197
  <Typography variant="pi" textColor="neutral600">
194
- ID: {session.id}
198
+ {t('modal.id', 'ID: {id}', { id: session.id })}
195
199
  </Typography>
196
200
  </Flex>
197
201
  </Flex>
@@ -207,7 +211,7 @@ const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
207
211
  size="M"
208
212
  style={{ fontSize: '14px', padding: '8px 20px', fontWeight: '600' }}
209
213
  >
210
- {isOnline ? '🟢 ONLINE' : ' OFFLINE'}
214
+ {isOnline ? t('modal.status.online', 'ONLINE') : t('modal.status.offline', 'OFFLINE')}
211
215
  </Badge>
212
216
  </Flex>
213
217
 
@@ -220,24 +224,24 @@ const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
220
224
  {/* User Information */}
221
225
  <Section>
222
226
  <SectionTitle>
223
- 👤 User
227
+ {t('modal.section.user', 'User')}
224
228
  </SectionTitle>
225
229
 
226
- <DetailRow compact icon={Check} label="Username" value={session.user?.username || 'N/A'} />
227
- <DetailRow compact icon={Information} label="Email" value={session.user?.email || 'N/A'} />
228
- <DetailRow compact icon={Information} label="User ID" value={session.user?.id || 'N/A'} />
230
+ <DetailRow compact icon={Check} label={t('modal.user.username', 'Username')} value={session.user?.username || t('modal.user.na', 'N/A')} />
231
+ <DetailRow compact icon={Information} label={t('modal.user.email', 'Email')} value={session.user?.email || t('modal.user.na', 'N/A')} />
232
+ <DetailRow compact icon={Information} label={t('modal.user.id', 'User ID')} value={session.user?.id || t('modal.user.na', 'N/A')} />
229
233
  </Section>
230
234
 
231
235
  {/* Device Information */}
232
236
  <Section>
233
237
  <SectionTitle>
234
- 💻 Device
238
+ {t('modal.section.device', 'Device')}
235
239
  </SectionTitle>
236
240
 
237
- <DetailRow compact icon={DeviceIcon} label="Device" value={deviceInfo.device} />
238
- <DetailRow compact icon={Monitor} label="Browser" value={`${deviceInfo.browser} ${deviceInfo.browserVersion || ''}`} />
239
- <DetailRow compact icon={Server} label="OS" value={deviceInfo.os} />
240
- <DetailRow compact icon={Information} label="IP" value={session.ipAddress} />
241
+ <DetailRow compact icon={DeviceIcon} label={t('modal.device.device', 'Device')} value={deviceInfo.device} />
242
+ <DetailRow compact icon={Monitor} label={t('modal.device.browser', 'Browser')} value={`${deviceInfo.browser} ${deviceInfo.browserVersion || ''}`} />
243
+ <DetailRow compact icon={Server} label={t('modal.device.os', 'OS')} value={deviceInfo.os} />
244
+ <DetailRow compact icon={Information} label={t('modal.device.ip', 'IP')} value={session.ipAddress} />
241
245
  </Section>
242
246
  </Box>
243
247
 
@@ -245,13 +249,13 @@ const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
245
249
  <Box>
246
250
  <Section>
247
251
  <SectionTitle>
248
- ⏱️ Timeline
252
+ {t('modal.section.timeline', 'Timeline')}
249
253
  </SectionTitle>
250
254
 
251
255
  <DetailRow
252
256
  compact
253
257
  icon={Clock}
254
- label="Login"
258
+ label={t('modal.timeline.login', 'Login')}
255
259
  value={new Date(session.loginTime).toLocaleString('de-DE', {
256
260
  day: '2-digit',
257
261
  month: 'short',
@@ -262,7 +266,7 @@ const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
262
266
  <DetailRow
263
267
  compact
264
268
  icon={Clock}
265
- label="Last Active"
269
+ label={t('modal.timeline.lastActive', 'Last Active')}
266
270
  value={new Date(session.lastActive || session.loginTime).toLocaleString('de-DE', {
267
271
  day: '2-digit',
268
272
  month: 'short',
@@ -273,14 +277,14 @@ const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
273
277
  <DetailRow
274
278
  compact
275
279
  icon={Clock}
276
- label="Idle Time"
277
- value={`${session.minutesSinceActive} min`}
280
+ label={t('modal.timeline.idleTime', 'Idle Time')}
281
+ value={t('modal.timeline.minutes', '{minutes} min', { minutes: session.minutesSinceActive })}
278
282
  />
279
283
  {session.logoutTime && (
280
284
  <DetailRow
281
285
  compact
282
286
  icon={Cross}
283
- label="Logout"
287
+ label={t('modal.timeline.logout', 'Logout')}
284
288
  value={new Date(session.logoutTime).toLocaleString('de-DE', {
285
289
  day: '2-digit',
286
290
  month: 'short',
@@ -297,13 +301,13 @@ const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
297
301
  {isPremium ? (
298
302
  <Section>
299
303
  <SectionTitle>
300
- 🌍 Location & Security
304
+ {t('modal.section.security', 'Location and Security')}
301
305
  </SectionTitle>
302
306
 
303
307
  {geoLoading ? (
304
308
  <Box padding={4} style={{ textAlign: 'center' }}>
305
309
  <Typography variant="pi" textColor="neutral600">
306
- Loading location data...
310
+ {t('modal.security.loading', 'Loading location data...')}
307
311
  </Typography>
308
312
  </Box>
309
313
  ) : (
@@ -312,30 +316,30 @@ const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
312
316
  <DetailRow
313
317
  compact
314
318
  icon={Earth}
315
- label="Country"
316
- value={`${premiumData.country_flag || '🌍'} ${premiumData.country}`}
319
+ label={t('modal.security.country', 'Country')}
320
+ value={`${premiumData.country_flag || ''} ${premiumData.country}`.trim()}
317
321
  />
318
- <DetailRow compact icon={Earth} label="City" value={premiumData.city} />
319
- <DetailRow compact icon={Clock} label="Timezone" value={premiumData.timezone} />
322
+ <DetailRow compact icon={Earth} label={t('modal.security.city', 'City')} value={premiumData.city} />
323
+ <DetailRow compact icon={Clock} label={t('modal.security.timezone', 'Timezone')} value={premiumData.timezone} />
320
324
  </Box>
321
325
  <Box>
322
326
  <DetailRow
323
327
  compact
324
328
  icon={Shield}
325
- label="Security"
329
+ label={t('modal.security.score', 'Security')}
326
330
  value={`${premiumData.securityScore}/100 (${premiumData.riskLevel})`}
327
331
  />
328
332
  <DetailRow
329
333
  compact
330
334
  icon={Shield}
331
- label="VPN"
332
- value={premiumData.isVpn ? '⚠️ Yes' : ' No'}
335
+ label={t('modal.security.vpn', 'VPN')}
336
+ value={premiumData.isVpn ? t('modal.security.vpnWarning', '[WARNING] Yes') : t('modal.security.no', 'No')}
333
337
  />
334
338
  <DetailRow
335
339
  compact
336
340
  icon={Shield}
337
- label="Proxy"
338
- value={premiumData.isProxy ? '⚠️ Yes' : ' No'}
341
+ label={t('modal.security.proxy', 'Proxy')}
342
+ value={premiumData.isProxy ? t('modal.security.vpnWarning', '[WARNING] Yes') : t('modal.security.no', 'No')}
339
343
  />
340
344
  </Box>
341
345
  </TwoColumnGrid>
@@ -355,10 +359,10 @@ const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
355
359
  <Flex direction="column" alignItems="center" gap={3}>
356
360
  <Crown style={{ width: '40px', height: '40px', color: '#d97706' }} />
357
361
  <Typography variant="beta" style={{ color: '#92400e', fontWeight: '700' }}>
358
- 🌍 Location & Security Analysis
362
+ {t('modal.premium.title', 'Location and Security Analysis')}
359
363
  </Typography>
360
364
  <Typography variant="omega" style={{ color: '#78350f', fontSize: '14px', lineHeight: '1.6' }}>
361
- Unlock premium features to get IP geolocation, security scoring, and VPN/Proxy detection for every session
365
+ {t('modal.premium.description', 'Unlock premium features to get IP geolocation, security scoring, and VPN/Proxy detection for every session')}
362
366
  </Typography>
363
367
  <Button
364
368
  variant="secondary"
@@ -373,7 +377,7 @@ const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
373
377
  boxShadow: '0 4px 12px rgba(245, 158, 11, 0.3)',
374
378
  }}
375
379
  >
376
- Upgrade to Premium
380
+ {t('modal.premium.upgrade', 'Upgrade to Premium')}
377
381
  </Button>
378
382
  </Flex>
379
383
  </Box>
@@ -384,7 +388,7 @@ const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
384
388
  <Section>
385
389
  <Flex justifyContent="space-between" alignItems="center" style={{ marginBottom: '12px' }}>
386
390
  <SectionTitle style={{ marginBottom: 0, paddingBottom: 0, border: 'none' }}>
387
- 🔧 Technical Details
391
+ {t('modal.section.technical', 'Technical Details')}
388
392
  </SectionTitle>
389
393
  <Button
390
394
  variant="tertiary"
@@ -392,7 +396,7 @@ const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
392
396
  onClick={() => setShowUserAgent(!showUserAgent)}
393
397
  style={{ fontSize: '12px' }}
394
398
  >
395
- {showUserAgent ? ' Hide Details' : ' Show Details'}
399
+ {showUserAgent ? `▲ ${t('modal.technical.hide', 'Hide Details')}` : `▼ ${t('modal.technical.show', 'Show Details')}`}
396
400
  </Button>
397
401
  </Flex>
398
402
 
@@ -423,7 +427,7 @@ const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
423
427
  <Modal.Footer>
424
428
  <Flex justifyContent="space-between" style={{ width: '100%' }}>
425
429
  <Button onClick={onClose} variant="tertiary">
426
- Close
430
+ {t('modal.actions.close', 'Close')}
427
431
  </Button>
428
432
  <Button
429
433
  onClick={handleTerminate}
@@ -432,7 +436,7 @@ const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
432
436
  loading={terminating}
433
437
  startIcon={<Cross />}
434
438
  >
435
- Terminate Session
439
+ {t('modal.actions.terminate', 'Terminate Session')}
436
440
  </Button>
437
441
  </Flex>
438
442
  </Modal.Footer>
@@ -76,11 +76,11 @@ const SessionInfoCard = ({ id, model }) => {
76
76
  </Typography>
77
77
  {isOnline ? (
78
78
  <Badge active backgroundColor="success500">
79
- 🟢 Online
79
+ Online
80
80
  </Badge>
81
81
  ) : (
82
82
  <Badge backgroundColor="neutral150">
83
- Offline
83
+ Offline
84
84
  </Badge>
85
85
  )}
86
86
  </Flex>
@@ -110,7 +110,7 @@ const SessionInfoCard = ({ id, model }) => {
110
110
  marginBottom={2}
111
111
  >
112
112
  <Typography variant="pi" textColor="neutral800">
113
- 📱 {session.ipAddress}
113
+ IP: {session.ipAddress}
114
114
  </Typography>
115
115
  <Typography variant="pi" textColor="neutral600" fontSize="11px">
116
116
  {new Date(session.loginTime).toLocaleString()}
@@ -1,22 +1,27 @@
1
1
  import { useState, useEffect } from 'react';
2
+ import { useIntl } from 'react-intl';
2
3
  import { Box, Typography, Flex, Button, Badge, Divider } from '@strapi/design-system';
3
4
  import { Check, Cross, Monitor, Phone, Server, Clock } from '@strapi/icons';
4
5
  import { useFetchClient, useNotification } from '@strapi/strapi/admin';
5
6
  import parseUserAgent from '../utils/parseUserAgent';
7
+ import { getTranslation } from '../utils/getTranslation';
6
8
 
7
9
  /**
8
10
  * Session Info Panel - Native Strapi design
9
11
  * Clean, professional sidebar panel for Content Manager
10
12
  */
11
13
  const SessionInfoPanel = ({ documentId, model, document }) => {
14
+ const { formatMessage } = useIntl();
12
15
  const [sessions, setSessions] = useState([]);
13
16
  const [loading, setLoading] = useState(true);
14
17
  const [isBlocked, setIsBlocked] = useState(false);
15
18
  const [actionLoading, setActionLoading] = useState(false);
16
19
  const { get, post: postRequest } = useFetchClient();
17
20
  const { toggleNotification } = useNotification();
21
+ const t = (id, defaultMessage, values) => formatMessage({ id: getTranslation(id), defaultMessage }, values);
18
22
 
19
- const userId = document?.id || documentId;
23
+ // Strapi v5: Use documentId (string UUID) instead of numeric id
24
+ const userId = document?.documentId || documentId;
20
25
 
21
26
  useEffect(() => {
22
27
  if (model !== 'plugin::users-permissions.user' || !userId) {
@@ -52,14 +57,14 @@ const SessionInfoPanel = ({ documentId, model, document }) => {
52
57
  if (response.data?.success) {
53
58
  toggleNotification({
54
59
  type: 'success',
55
- message: 'All sessions terminated successfully',
60
+ message: t('notifications.success.terminatedAll', 'All sessions terminated successfully'),
56
61
  });
57
62
  setSessions([]);
58
63
  }
59
64
  } catch (error) {
60
65
  toggleNotification({
61
66
  type: 'warning',
62
- message: 'Failed to terminate sessions',
67
+ message: t('notifications.error.terminateAll', 'Failed to terminate sessions'),
63
68
  });
64
69
  console.error('[SessionInfoPanel] Logout all error:', error);
65
70
  } finally {
@@ -80,7 +85,9 @@ const SessionInfoPanel = ({ documentId, model, document }) => {
80
85
 
81
86
  toggleNotification({
82
87
  type: 'success',
83
- message: newBlockedStatus ? 'User blocked successfully' : 'User unblocked successfully',
88
+ message: newBlockedStatus
89
+ ? t('notifications.success.blocked', 'User blocked successfully')
90
+ : t('notifications.success.unblocked', 'User unblocked successfully'),
84
91
  });
85
92
 
86
93
  if (newBlockedStatus) {
@@ -90,7 +97,7 @@ const SessionInfoPanel = ({ documentId, model, document }) => {
90
97
  } catch (error) {
91
98
  toggleNotification({
92
99
  type: 'warning',
93
- message: 'Failed to update user status',
100
+ message: t('notifications.error.block', 'Failed to update user status'),
94
101
  });
95
102
  console.error('[SessionInfoPanel] Toggle block error:', error);
96
103
  } finally {
@@ -111,10 +118,10 @@ const SessionInfoPanel = ({ documentId, model, document }) => {
111
118
 
112
119
  if (loading) {
113
120
  return {
114
- title: 'Session Info',
121
+ title: t('panel.title', 'Session Info'),
115
122
  content: (
116
123
  <Box padding={4} background="neutral0">
117
- <Typography variant="pi" textColor="neutral600">Loading...</Typography>
124
+ <Typography variant="pi" textColor="neutral600">{t('panel.loading', 'Loading...')}</Typography>
118
125
  </Box>
119
126
  ),
120
127
  };
@@ -123,7 +130,7 @@ const SessionInfoPanel = ({ documentId, model, document }) => {
123
130
  const isOnline = sessions.length > 0;
124
131
 
125
132
  return {
126
- title: 'Session Info',
133
+ title: t('panel.title', 'Session Info'),
127
134
  content: (
128
135
  <Box style={{ width: '100%' }}>
129
136
  <Flex direction="column" gap={4} alignItems="stretch">
@@ -144,10 +151,10 @@ const SessionInfoPanel = ({ documentId, model, document }) => {
144
151
  size="M"
145
152
  style={{ fontSize: '14px', padding: '6px 12px' }}
146
153
  >
147
- {isOnline ? '🟢 ACTIVE' : ' OFFLINE'}
154
+ {isOnline ? t('panel.status.active', 'ACTIVE') : t('panel.status.offline', 'OFFLINE')}
148
155
  </Badge>
149
156
  <Typography variant="omega" fontWeight="semiBold" textColor={isOnline ? 'success700' : 'neutral700'}>
150
- {sessions.length} active session{sessions.length !== 1 ? 's' : ''}
157
+ {t('panel.sessions.count', '{count} active session{count, plural, one {} other {s}}', { count: sessions.length })}
151
158
  </Typography>
152
159
  </Flex>
153
160
  </Box>
@@ -160,10 +167,10 @@ const SessionInfoPanel = ({ documentId, model, document }) => {
160
167
  hasRadius
161
168
  >
162
169
  <Typography variant="omega" fontWeight="semiBold" textColor="danger700" marginBottom={1}>
163
- User is blocked
170
+ {t('panel.blocked.title', 'User is blocked')}
164
171
  </Typography>
165
172
  <Typography variant="pi" textColor="danger600">
166
- Authentication disabled
173
+ {t('panel.blocked.description', 'Authentication disabled')}
167
174
  </Typography>
168
175
  </Box>
169
176
  )}
@@ -176,7 +183,7 @@ const SessionInfoPanel = ({ documentId, model, document }) => {
176
183
  letterSpacing: '0.5px',
177
184
  fontSize: '12px'
178
185
  }}>
179
- Active Sessions
186
+ {t('panel.sessions.title', 'Active Sessions')}
180
187
  </Typography>
181
188
 
182
189
  {sessions.slice(0, 5).map((session) => {
@@ -218,7 +225,7 @@ const SessionInfoPanel = ({ documentId, model, document }) => {
218
225
  textColor="neutral0"
219
226
  size="S"
220
227
  >
221
- Active
228
+ {t('panel.sessions.active', 'Active')}
222
229
  </Badge>
223
230
 
224
231
  {/* Browser & OS */}
@@ -252,7 +259,10 @@ const SessionInfoPanel = ({ documentId, model, document }) => {
252
259
  {/* Show minutes since last activity */}
253
260
  {session.minutesSinceActive !== undefined && session.minutesSinceActive < 60 && (
254
261
  <Typography variant="pi" textColor="success600" fontWeight="semiBold">
255
- Active {session.minutesSinceActive === 0 ? 'now' : `${session.minutesSinceActive} min ago`}
262
+ {session.minutesSinceActive === 0
263
+ ? t('panel.sessions.activeNow', 'Active now')
264
+ : t('panel.sessions.activeAgo', 'Active {minutes} min ago', { minutes: session.minutesSinceActive })
265
+ }
256
266
  </Typography>
257
267
  )}
258
268
  </Flex>
@@ -263,7 +273,7 @@ const SessionInfoPanel = ({ documentId, model, document }) => {
263
273
  {sessions.length > 5 && (
264
274
  <Box padding={3} background="primary100" hasRadius textAlign="center">
265
275
  <Typography variant="pi" textColor="primary600" fontWeight="semiBold">
266
- +{sessions.length - 5} more session{sessions.length - 5 !== 1 ? 's' : ''}
276
+ {t('panel.sessions.more', '+{count} more session{count, plural, one {} other {s}}', { count: sessions.length - 5 })}
267
277
  </Typography>
268
278
  </Box>
269
279
  )}
@@ -287,10 +297,10 @@ const SessionInfoPanel = ({ documentId, model, document }) => {
287
297
  💤
288
298
  </Typography>
289
299
  <Typography variant="omega" fontWeight="semiBold" textColor="neutral700">
290
- No active sessions
300
+ {t('panel.empty.title', 'No active sessions')}
291
301
  </Typography>
292
302
  <Typography variant="pi" textColor="neutral500" style={{ fontSize: '13px' }}>
293
- User has not logged in yet
303
+ {t('panel.empty.description', 'User has not logged in yet')}
294
304
  </Typography>
295
305
  </Flex>
296
306
  </Box>
@@ -305,7 +315,7 @@ const SessionInfoPanel = ({ documentId, model, document }) => {
305
315
  letterSpacing: '0.5px',
306
316
  fontSize: '12px'
307
317
  }}>
308
- Actions
318
+ {t('panel.actions.title', 'Actions')}
309
319
  </Typography>
310
320
 
311
321
  <Button
@@ -334,7 +344,7 @@ const SessionInfoPanel = ({ documentId, model, document }) => {
334
344
  }
335
345
  }}
336
346
  >
337
- Terminate All Sessions
347
+ {t('panel.actions.terminateAll', 'Terminate All Sessions')}
338
348
  </Button>
339
349
 
340
350
  <Button
@@ -363,7 +373,7 @@ const SessionInfoPanel = ({ documentId, model, document }) => {
363
373
  }
364
374
  }}
365
375
  >
366
- {isBlocked ? 'Unblock User' : 'Block User'}
376
+ {isBlocked ? t('panel.actions.unblockUser', 'Unblock User') : t('panel.actions.blockUser', 'Block User')}
367
377
  </Button>
368
378
  </Flex>
369
379
  </Flex>
@@ -34,6 +34,15 @@ export default {
34
34
  to: `/settings/${pluginId}`,
35
35
  },
36
36
  [
37
+ {
38
+ intlLabel: {
39
+ id: `${pluginId}.settings.upgrade`,
40
+ defaultMessage: 'Upgrade',
41
+ },
42
+ id: 'upgrade',
43
+ to: `/settings/${pluginId}/upgrade`,
44
+ Component: () => import('./pages/UpgradePage'),
45
+ },
37
46
  {
38
47
  intlLabel: {
39
48
  id: `${pluginId}.settings.general`,