@hed-hog/core 0.0.347 → 0.0.350

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 (126) hide show
  1. package/dist/auth/auth.controller.d.ts +1 -1
  2. package/dist/auth/auth.service.d.ts +1 -1
  3. package/dist/core.module.d.ts.map +1 -1
  4. package/dist/core.module.js +3 -0
  5. package/dist/core.module.js.map +1 -1
  6. package/dist/dashboard/dashboard-user/dashboard-user.controller.d.ts +3 -3
  7. package/dist/dashboard/dashboard-user/dashboard-user.service.d.ts +3 -3
  8. package/dist/index.d.ts +3 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +4 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp/guards/mcp-auth.guard.d.ts +6 -1
  13. package/dist/mcp/guards/mcp-auth.guard.d.ts.map +1 -1
  14. package/dist/mcp/guards/mcp-auth.guard.js +29 -2
  15. package/dist/mcp/guards/mcp-auth.guard.js.map +1 -1
  16. package/dist/mcp/mcp-api-key.controller.d.ts +32 -0
  17. package/dist/mcp/mcp-api-key.controller.d.ts.map +1 -0
  18. package/dist/mcp/mcp-api-key.controller.js +64 -0
  19. package/dist/mcp/mcp-api-key.controller.js.map +1 -0
  20. package/dist/mcp/mcp-api-key.service.d.ts +27 -0
  21. package/dist/mcp/mcp-api-key.service.d.ts.map +1 -0
  22. package/dist/mcp/mcp-api-key.service.js +74 -0
  23. package/dist/mcp/mcp-api-key.service.js.map +1 -0
  24. package/dist/mcp/mcp-registry.service.d.ts +1 -2
  25. package/dist/mcp/mcp-registry.service.d.ts.map +1 -1
  26. package/dist/mcp/mcp-registry.service.js +6 -2
  27. package/dist/mcp/mcp-registry.service.js.map +1 -1
  28. package/dist/mcp/mcp.module.d.ts.map +1 -1
  29. package/dist/mcp/mcp.module.js +8 -1
  30. package/dist/mcp/mcp.module.js.map +1 -1
  31. package/dist/mcp-chat/mcp-chat.controller.d.ts +4 -0
  32. package/dist/mcp-chat/mcp-chat.controller.d.ts.map +1 -1
  33. package/dist/mcp-chat/mcp-chat.controller.js +46 -0
  34. package/dist/mcp-chat/mcp-chat.controller.js.map +1 -1
  35. package/dist/mcp-chat/mcp-chat.service.d.ts +6 -1
  36. package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -1
  37. package/dist/mcp-chat/mcp-chat.service.js +125 -17
  38. package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
  39. package/dist/notification/dto/create-notification.dto.d.ts +26 -0
  40. package/dist/notification/dto/create-notification.dto.d.ts.map +1 -0
  41. package/dist/notification/dto/create-notification.dto.js +86 -0
  42. package/dist/notification/dto/create-notification.dto.js.map +1 -0
  43. package/dist/notification/dto/update-progress.dto.d.ts +6 -0
  44. package/dist/notification/dto/update-progress.dto.d.ts.map +1 -0
  45. package/dist/notification/dto/update-progress.dto.js +33 -0
  46. package/dist/notification/dto/update-progress.dto.js.map +1 -0
  47. package/dist/notification/notification.controller.d.ts +108 -0
  48. package/dist/notification/notification.controller.d.ts.map +1 -0
  49. package/dist/notification/notification.controller.js +118 -0
  50. package/dist/notification/notification.controller.js.map +1 -0
  51. package/dist/notification/notification.module.d.ts +3 -0
  52. package/dist/notification/notification.module.d.ts.map +1 -0
  53. package/dist/notification/notification.module.js +29 -0
  54. package/dist/notification/notification.module.js.map +1 -0
  55. package/dist/notification/notification.service.d.ts +98 -0
  56. package/dist/notification/notification.service.d.ts.map +1 -0
  57. package/dist/notification/notification.service.js +123 -0
  58. package/dist/notification/notification.service.js.map +1 -0
  59. package/dist/profile/profile.controller.d.ts +1 -1
  60. package/dist/profile/profile.service.d.ts +1 -1
  61. package/dist/setting/dto/update-setting-list.dto.d.ts +9 -0
  62. package/dist/setting/dto/update-setting-list.dto.d.ts.map +1 -0
  63. package/dist/setting/dto/update-setting-list.dto.js +44 -0
  64. package/dist/setting/dto/update-setting-list.dto.js.map +1 -0
  65. package/dist/setting/setting.controller.d.ts +19 -0
  66. package/dist/setting/setting.controller.d.ts.map +1 -1
  67. package/dist/setting/setting.controller.js +12 -0
  68. package/dist/setting/setting.controller.js.map +1 -1
  69. package/dist/setting/setting.service.d.ts +19 -0
  70. package/dist/setting/setting.service.d.ts.map +1 -1
  71. package/dist/setting/setting.service.js +79 -0
  72. package/dist/setting/setting.service.js.map +1 -1
  73. package/dist/task/task.service.d.ts +12 -0
  74. package/dist/task/task.service.d.ts.map +1 -1
  75. package/dist/task/task.service.js +60 -0
  76. package/dist/task/task.service.js.map +1 -1
  77. package/dist/user/user.controller.d.ts +2 -2
  78. package/dist/user/user.service.d.ts +3 -3
  79. package/dist/webhook-integration/webhook-integration.service.d.ts +4 -0
  80. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  81. package/dist/webhook-integration/webhook-integration.service.js +184 -34
  82. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  83. package/dist/webhook-integration/webhook-utils.d.ts +1 -0
  84. package/dist/webhook-integration/webhook-utils.d.ts.map +1 -1
  85. package/dist/webhook-integration/webhook-utils.js +38 -0
  86. package/dist/webhook-integration/webhook-utils.js.map +1 -1
  87. package/hedhog/data/menu.yaml +48 -4
  88. package/hedhog/data/route.yaml +133 -0
  89. package/hedhog/data/setting_group.yaml +45 -0
  90. package/hedhog/data/setting_subgroup.yaml +16 -0
  91. package/hedhog/frontend/app/account/components/mcp-tokens.tsx.ejs +285 -0
  92. package/hedhog/frontend/app/account/layout.tsx.ejs +32 -55
  93. package/hedhog/frontend/app/account/mcp-tokens/page.tsx.ejs +5 -0
  94. package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +2098 -1835
  95. package/hedhog/frontend/app/mcp_chat/chat-components.tsx.ejs +595 -0
  96. package/hedhog/frontend/app/mcp_chat/chat-types.ts.ejs +16 -0
  97. package/hedhog/frontend/app/mcp_chat/page.tsx.ejs +554 -383
  98. package/hedhog/frontend/app/setting-list/page.tsx.ejs +649 -0
  99. package/hedhog/frontend/app/users/page.tsx.ejs +445 -203
  100. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +16 -37
  101. package/hedhog/frontend/messages/en.json +241 -2
  102. package/hedhog/frontend/messages/pt.json +241 -2
  103. package/hedhog/table/mcp_api_key.yaml +30 -0
  104. package/hedhog/table/notification.yaml +72 -0
  105. package/hedhog/table/webhook_action_log.yaml +49 -0
  106. package/package.json +2 -2
  107. package/src/core.module.ts +3 -0
  108. package/src/index.ts +5 -0
  109. package/src/mcp/guards/mcp-auth.guard.ts +64 -32
  110. package/src/mcp/mcp-api-key.controller.ts +42 -0
  111. package/src/mcp/mcp-api-key.service.ts +66 -0
  112. package/src/mcp/mcp-registry.service.ts +75 -69
  113. package/src/mcp/mcp.module.ts +43 -36
  114. package/src/mcp-chat/mcp-chat.controller.ts +113 -60
  115. package/src/mcp-chat/mcp-chat.service.ts +494 -332
  116. package/src/notification/dto/create-notification.dto.ts +71 -0
  117. package/src/notification/dto/update-progress.dto.ts +16 -0
  118. package/src/notification/notification.controller.ts +71 -0
  119. package/src/notification/notification.module.ts +16 -0
  120. package/src/notification/notification.service.ts +124 -0
  121. package/src/setting/dto/update-setting-list.dto.ts +32 -0
  122. package/src/setting/setting.controller.ts +9 -0
  123. package/src/setting/setting.service.ts +91 -0
  124. package/src/task/task.service.ts +72 -0
  125. package/src/webhook-integration/webhook-integration.service.ts +934 -735
  126. package/src/webhook-integration/webhook-utils.ts +136 -79
@@ -1290,6 +1290,15 @@
1290
1290
  slug: admin
1291
1291
  - where:
1292
1292
  slug: admin-access
1293
+ - url: /setting/:settingId/options
1294
+ method: PUT
1295
+ type: HTTP
1296
+ relations:
1297
+ role:
1298
+ - where:
1299
+ slug: admin
1300
+ - where:
1301
+ slug: admin-access
1293
1302
  - url: /setting
1294
1303
  method: PUT
1295
1304
  type: HTTP
@@ -2124,6 +2133,16 @@
2124
2133
  - where:
2125
2134
  slug: admin-access
2126
2135
 
2136
+ - url: /mcp-chat/:id/messages/stream
2137
+ method: POST
2138
+ type: HTTP
2139
+ relations:
2140
+ role:
2141
+ - where:
2142
+ slug: admin
2143
+ - where:
2144
+ slug: admin-access
2145
+
2127
2146
  - url: /core/pdf/example
2128
2147
  method: GET
2129
2148
  type: HTTP
@@ -2131,3 +2150,117 @@
2131
2150
  role:
2132
2151
  - where:
2133
2152
  slug: admin
2153
+
2154
+ - url: /mcp/api-keys
2155
+ method: GET
2156
+ type: HTTP
2157
+ relations:
2158
+ role:
2159
+ - where:
2160
+ slug: admin
2161
+ - where:
2162
+ slug: admin-access
2163
+ - where:
2164
+ slug: user
2165
+
2166
+ - url: /mcp/api-keys
2167
+ method: POST
2168
+ type: HTTP
2169
+ relations:
2170
+ role:
2171
+ - where:
2172
+ slug: admin
2173
+ - where:
2174
+ slug: admin-access
2175
+ - where:
2176
+ slug: user
2177
+
2178
+ - url: /mcp/api-keys/:id
2179
+ method: DELETE
2180
+ type: HTTP
2181
+ relations:
2182
+ role:
2183
+ - where:
2184
+ slug: admin
2185
+ - where:
2186
+ slug: admin-access
2187
+ - where:
2188
+ slug: user
2189
+
2190
+ - url: /notification
2191
+ method: GET
2192
+ type: HTTP
2193
+ relations:
2194
+ role:
2195
+ - where:
2196
+ slug: admin
2197
+ - where:
2198
+ slug: user
2199
+
2200
+ - url: /notification/unread-count
2201
+ method: GET
2202
+ type: HTTP
2203
+ relations:
2204
+ role:
2205
+ - where:
2206
+ slug: admin
2207
+ - where:
2208
+ slug: user
2209
+
2210
+ - url: /notification/read-all
2211
+ method: PATCH
2212
+ type: HTTP
2213
+ relations:
2214
+ role:
2215
+ - where:
2216
+ slug: admin
2217
+ - where:
2218
+ slug: user
2219
+
2220
+ - url: /notification/:id/read
2221
+ method: PATCH
2222
+ type: HTTP
2223
+ relations:
2224
+ role:
2225
+ - where:
2226
+ slug: admin
2227
+ - where:
2228
+ slug: user
2229
+
2230
+ - url: /notification/:id/progress
2231
+ method: PATCH
2232
+ type: HTTP
2233
+ relations:
2234
+ role:
2235
+ - where:
2236
+ slug: admin
2237
+ - where:
2238
+ slug: user
2239
+
2240
+ - url: /notification/:id
2241
+ method: DELETE
2242
+ type: HTTP
2243
+ relations:
2244
+ role:
2245
+ - where:
2246
+ slug: admin
2247
+ - where:
2248
+ slug: user
2249
+
2250
+ - url: /notification
2251
+ method: POST
2252
+ type: HTTP
2253
+ relations:
2254
+ role:
2255
+ - where:
2256
+ slug: admin
2257
+
2258
+ - url: /notification
2259
+ method: DELETE
2260
+ type: HTTP
2261
+ relations:
2262
+ role:
2263
+ - where:
2264
+ slug: admin
2265
+ - where:
2266
+ slug: user
@@ -8,6 +8,17 @@
8
8
  pt: Configurações gerais do sistema e configurações básicas
9
9
  relations:
10
10
  setting:
11
+ - slug: api-body-size-limit
12
+ type: number
13
+ component: input-number
14
+ name:
15
+ en: API Body Size Limit (MB)
16
+ pt: Limite do Corpo da Requisição (MB)
17
+ description:
18
+ en: Maximum size in megabytes for incoming API request bodies. Applies dynamically without restart (50 = 50 MB)
19
+ pt: Tamanho máximo em megabytes para corpos de requisição recebidos pela API. Aplica-se dinamicamente sem reiniciar (50 = 50 MB)
20
+ value: 50
21
+ user_override: false
11
22
  - slug: url
12
23
  type: string
13
24
  name:
@@ -77,6 +88,34 @@
77
88
  subgroup_id:
78
89
  where:
79
90
  slug: session-retention
91
+ - slug: webhook-log-retention-days
92
+ type: number
93
+ component: input-number
94
+ name:
95
+ en: Webhook Log Retention Period
96
+ pt: Período de Retenção de Logs de Webhook
97
+ description:
98
+ en: Number of days to keep incoming webhook execution logs before automatic deletion
99
+ pt: Número de dias para manter logs de execução de webhooks de entrada antes da exclusão automática
100
+ value: 30
101
+ user_override: false
102
+ subgroup_id:
103
+ where:
104
+ slug: webhook-log-retention
105
+ - slug: notification-retention-days
106
+ type: number
107
+ component: input-number
108
+ name:
109
+ en: Notification Retention Period
110
+ pt: Período de Retenção de Notificações
111
+ description:
112
+ en: Number of days to keep user notifications before automatic deletion. Unread notifications are also removed after this period
113
+ pt: Número de dias para manter notificações de usuários antes da exclusão automática. Notificações não lidas também são removidas após este período
114
+ value: 30
115
+ user_override: false
116
+ subgroup_id:
117
+ where:
118
+ slug: notification-retention
80
119
  - slug: authentication
81
120
  icon: lock-password
82
121
  name:
@@ -1469,6 +1508,12 @@
1469
1508
  order: 7
1470
1509
  - value: text/plain
1471
1510
  order: 8
1511
+ - value: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
1512
+ order: 9
1513
+ - value: application/vnd.openxmlformats-officedocument.wordprocessingml.document
1514
+ order: 10
1515
+ - value: application/vnd.openxmlformats-officedocument.presentationml.presentation
1516
+ order: 11
1472
1517
  subgroup_id:
1473
1518
  where:
1474
1519
  slug: storage-general
@@ -14,6 +14,22 @@
14
14
  en: Rules for retaining and removing expired user sessions
15
15
  pt: Regras para retenção e remoção de sessões expiradas de usuários
16
16
 
17
+ - slug: webhook-log-retention
18
+ name:
19
+ en: Webhook Log Retention
20
+ pt: Retenção de Logs de Webhook
21
+ description:
22
+ en: Rules for retaining and removing incoming webhook call logs
23
+ pt: Regras para retenção e remoção de logs de chamadas de webhook de entrada
24
+
25
+ - slug: notification-retention
26
+ name:
27
+ en: Notification Retention
28
+ pt: Retenção de Notificações
29
+ description:
30
+ en: Rules for retaining and automatically removing old user notifications
31
+ pt: Regras para retenção e remoção automática de notificações antigas de usuários
32
+
17
33
  - slug: email-password-auth
18
34
  name:
19
35
  en: Email and Password Authentication
@@ -0,0 +1,285 @@
1
+ 'use client';
2
+
3
+ import { PaginationFooter } from '@/components/entity-list/pagination-footer';
4
+ import {
5
+ AlertDialog,
6
+ AlertDialogAction,
7
+ AlertDialogCancel,
8
+ AlertDialogContent,
9
+ AlertDialogDescription,
10
+ AlertDialogFooter,
11
+ AlertDialogHeader,
12
+ AlertDialogTitle,
13
+ } from '@/components/ui/alert-dialog';
14
+ import { Button } from '@/components/ui/button';
15
+ import {
16
+ Card,
17
+ CardContent,
18
+ CardDescription,
19
+ CardHeader,
20
+ CardTitle,
21
+ } from '@/components/ui/card';
22
+ import {
23
+ Dialog,
24
+ DialogContent,
25
+ DialogDescription,
26
+ DialogFooter,
27
+ DialogHeader,
28
+ DialogTitle,
29
+ } from '@/components/ui/dialog';
30
+ import { Input } from '@/components/ui/input';
31
+ import { Label } from '@/components/ui/label';
32
+ import {
33
+ Table,
34
+ TableBody,
35
+ TableCell,
36
+ TableHead,
37
+ TableHeader,
38
+ TableRow,
39
+ } from '@/components/ui/table';
40
+ import { usePagination } from '@/hooks/use-pagination';
41
+ import { formatDateTime } from '@/lib/format-date';
42
+ import { useApp } from '@hed-hog/next-app-provider';
43
+ import { Check, Copy, KeyRound, Plus, Trash2 } from 'lucide-react';
44
+ import { useTranslations } from 'next-intl';
45
+ import { useState } from 'react';
46
+ import { toast } from 'sonner';
47
+
48
+ type McpApiKey = {
49
+ id: number;
50
+ name: string;
51
+ token_prefix: string;
52
+ last_used_at: string | null;
53
+ expires_at: string | null;
54
+ created_at: string;
55
+ };
56
+
57
+ export function McpTokens() {
58
+ const { request, getSettingValue } = useApp();
59
+ const t = useTranslations('core.McpTokens');
60
+
61
+ const [keyToRevoke, setKeyToRevoke] = useState<number | null>(null);
62
+ const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
63
+ const [newKeyName, setNewKeyName] = useState('');
64
+ const [createdToken, setCreatedToken] = useState<string | null>(null);
65
+ const [isCopied, setIsCopied] = useState(false);
66
+ const [isCopiedUrl, setIsCopiedUrl] = useState(false);
67
+ const {
68
+ items: keys,
69
+ refetch,
70
+ page,
71
+ setPage,
72
+ totalItems,
73
+ pageSize,
74
+ setPageSize,
75
+ } = usePagination({ url: '/mcp/api-keys' });
76
+
77
+ const apiUrl = getSettingValue('api-url') as string | null;
78
+ const mcpUrl = apiUrl ? `${apiUrl.replace(/\/$/, '')}/mcp` : '/mcp';
79
+
80
+ const handleCreate = async () => {
81
+ if (!newKeyName.trim()) return;
82
+ try {
83
+ const res = await request<McpApiKey & { rawToken: string }>({
84
+ url: '/mcp/api-keys',
85
+ method: 'POST',
86
+ data: { name: newKeyName.trim() },
87
+ });
88
+ setCreatedToken(res.data.rawToken);
89
+ setNewKeyName('');
90
+ refetch();
91
+ } catch {
92
+ toast.error(t('createFailure'));
93
+ }
94
+ };
95
+
96
+ const handleRevoke = async (id: number) => {
97
+ try {
98
+ await request({ url: `/mcp/api-keys/${id}`, method: 'DELETE' });
99
+ setKeyToRevoke(null);
100
+ refetch();
101
+ toast.success(t('revokeSuccess'));
102
+ } catch {
103
+ toast.error(t('revokeFailure'));
104
+ }
105
+ };
106
+
107
+ const handleCopyToken = () => {
108
+ if (!createdToken) return;
109
+ navigator.clipboard.writeText(createdToken);
110
+ setIsCopied(true);
111
+ setTimeout(() => setIsCopied(false), 2000);
112
+ };
113
+
114
+ const handleCopyUrl = () => {
115
+ navigator.clipboard.writeText(mcpUrl);
116
+ setIsCopiedUrl(true);
117
+ setTimeout(() => setIsCopiedUrl(false), 2000);
118
+ };
119
+
120
+ const handleCloseCreateDialog = () => {
121
+ setIsCreateDialogOpen(false);
122
+ setCreatedToken(null);
123
+ setNewKeyName('');
124
+ setIsCopied(false);
125
+ };
126
+
127
+ return (
128
+ <>
129
+ <Card>
130
+ <CardHeader className="flex flex-row items-start justify-between">
131
+ <div>
132
+ <CardTitle>{t('title')}</CardTitle>
133
+ <CardDescription className="mt-1">{t('description')}</CardDescription>
134
+ </div>
135
+ <Button size="sm" onClick={() => setIsCreateDialogOpen(true)}>
136
+ <Plus className="size-4 mr-2" />
137
+ {t('createToken')}
138
+ </Button>
139
+ </CardHeader>
140
+ <CardContent className="space-y-4">
141
+ <div className="flex items-center gap-2 rounded-md border bg-muted/40 px-3 py-2">
142
+ <span className="text-sm text-muted-foreground shrink-0">{t('mcpUrlLabel')}:</span>
143
+ <code className="flex-1 text-sm break-all">{mcpUrl}</code>
144
+ <Button variant="ghost" size="icon" className="size-7 shrink-0" onClick={handleCopyUrl}>
145
+ {isCopiedUrl ? <Check className="size-3.5 text-green-500" /> : <Copy className="size-3.5" />}
146
+ </Button>
147
+ </div>
148
+
149
+ {(keys as McpApiKey[]).length === 0 ? (
150
+ <div className="flex flex-col items-center gap-2 py-8 text-muted-foreground">
151
+ <KeyRound className="size-8 opacity-40" />
152
+ <p className="text-sm">{t('noTokens')}</p>
153
+ </div>
154
+ ) : (
155
+ <Table>
156
+ <TableHeader>
157
+ <TableRow>
158
+ <TableHead>{t('name')}</TableHead>
159
+ <TableHead>{t('tokenPrefix')}</TableHead>
160
+ <TableHead>{t('lastUsed')}</TableHead>
161
+ <TableHead>{t('createdAt')}</TableHead>
162
+ <TableHead className="w-15" />
163
+ </TableRow>
164
+ </TableHeader>
165
+ <TableBody>
166
+ {(keys as McpApiKey[]).map((key) => (
167
+ <TableRow key={key.id}>
168
+ <TableCell className="font-medium">{key.name}</TableCell>
169
+ <TableCell>
170
+ <code className="rounded bg-muted px-1.5 py-0.5 text-xs">
171
+ {key.token_prefix}…
172
+ </code>
173
+ </TableCell>
174
+ <TableCell className="text-muted-foreground text-sm">
175
+ {key.last_used_at ? formatDateTime(key.last_used_at, getSettingValue) : t('never')}
176
+ </TableCell>
177
+ <TableCell className="text-muted-foreground text-sm">
178
+ {formatDateTime(key.created_at, getSettingValue)}
179
+ </TableCell>
180
+ <TableCell>
181
+ <Button
182
+ variant="ghost"
183
+ size="icon"
184
+ className="size-8 text-destructive hover:text-destructive"
185
+ onClick={() => setKeyToRevoke(key.id)}
186
+ >
187
+ <Trash2 className="size-4" />
188
+ </Button>
189
+ </TableCell>
190
+ </TableRow>
191
+ ))}
192
+ </TableBody>
193
+ </Table>
194
+ )}
195
+
196
+ <PaginationFooter
197
+ currentPage={page}
198
+ pageSize={pageSize}
199
+ totalItems={totalItems}
200
+ onPageChange={setPage}
201
+ onPageSizeChange={(newSize) => {
202
+ setPageSize(newSize);
203
+ setPage(1);
204
+ }}
205
+ pageSizeOptions={[10]}
206
+ />
207
+ </CardContent>
208
+ </Card>
209
+
210
+ {/* Revoke confirmation */}
211
+ <AlertDialog open={keyToRevoke !== null} onOpenChange={(open) => !open && setKeyToRevoke(null)}>
212
+ <AlertDialogContent>
213
+ <AlertDialogHeader>
214
+ <AlertDialogTitle>{t('revokeTokenTitle')}</AlertDialogTitle>
215
+ <AlertDialogDescription>{t('revokeTokenDescription')}</AlertDialogDescription>
216
+ </AlertDialogHeader>
217
+ <AlertDialogFooter>
218
+ <AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
219
+ <AlertDialogAction
220
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
221
+ onClick={() => keyToRevoke !== null && handleRevoke(keyToRevoke)}
222
+ >
223
+ {t('revokeConfirm')}
224
+ </AlertDialogAction>
225
+ </AlertDialogFooter>
226
+ </AlertDialogContent>
227
+ </AlertDialog>
228
+
229
+ {/* Create token dialog */}
230
+ <Dialog open={isCreateDialogOpen} onOpenChange={(open) => !open && handleCloseCreateDialog()}>
231
+ <DialogContent className="sm:max-w-md">
232
+ {createdToken === null ? (
233
+ <>
234
+ <DialogHeader>
235
+ <DialogTitle>{t('createTokenTitle')}</DialogTitle>
236
+ <DialogDescription>{t('createTokenDescription')}</DialogDescription>
237
+ </DialogHeader>
238
+ <div className="space-y-2 py-2">
239
+ <Label htmlFor="token-name">{t('tokenNameLabel')}</Label>
240
+ <Input
241
+ id="token-name"
242
+ placeholder={t('tokenNamePlaceholder')}
243
+ value={newKeyName}
244
+ onChange={(e) => setNewKeyName(e.target.value)}
245
+ onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
246
+ />
247
+ </div>
248
+ <DialogFooter>
249
+ <Button variant="outline" onClick={handleCloseCreateDialog}>{t('cancel')}</Button>
250
+ <Button onClick={handleCreate} disabled={!newKeyName.trim()}>
251
+ {t('createToken')}
252
+ </Button>
253
+ </DialogFooter>
254
+ </>
255
+ ) : (
256
+ <>
257
+ <DialogHeader>
258
+ <DialogTitle>{t('tokenCreatedTitle')}</DialogTitle>
259
+ </DialogHeader>
260
+ <div className="space-y-3 py-2">
261
+ <div className="rounded-md border border-yellow-300 bg-yellow-50 px-3 py-2 text-sm text-yellow-800 dark:border-yellow-700 dark:bg-yellow-950 dark:text-yellow-300">
262
+ {t('tokenCreatedWarning')}
263
+ </div>
264
+ <div className="space-y-1">
265
+ <Label>{t('tokenCreatedLabel')}</Label>
266
+ <div className="flex items-center gap-2">
267
+ <code className="flex-1 rounded border bg-muted px-3 py-2 text-xs break-all">
268
+ {createdToken}
269
+ </code>
270
+ <Button variant="outline" size="icon" onClick={handleCopyToken} className="shrink-0">
271
+ {isCopied ? <Check className="size-4 text-green-500" /> : <Copy className="size-4" />}
272
+ </Button>
273
+ </div>
274
+ </div>
275
+ </div>
276
+ <DialogFooter>
277
+ <Button onClick={handleCloseCreateDialog}>{t('closeDialog')}</Button>
278
+ </DialogFooter>
279
+ </>
280
+ )}
281
+ </DialogContent>
282
+ </Dialog>
283
+ </>
284
+ );
285
+ }
@@ -1,14 +1,10 @@
1
1
  'use client';
2
2
 
3
3
  import { PageHeader } from '@/components/entity-list';
4
- import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
4
+ import { cn } from '@/lib/utils';
5
5
  import {
6
- Tooltip,
7
- TooltipContent,
8
- TooltipTrigger,
9
- } from '@/components/ui/tooltip';
10
- import {
11
- Link,
6
+ KeyRound,
7
+ Link as LinkIcon,
12
8
  Lock,
13
9
  LucideIcon,
14
10
  Mail,
@@ -17,7 +13,8 @@ import {
17
13
  User,
18
14
  } from 'lucide-react';
19
15
  import { useTranslations } from 'next-intl';
20
- import { usePathname, useRouter } from 'next/navigation';
16
+ import Link from 'next/link';
17
+ import { usePathname } from 'next/navigation';
21
18
  import { ReactNode } from 'react';
22
19
 
23
20
  type LayoutProps = {
@@ -32,7 +29,6 @@ type TabConfig = {
32
29
 
33
30
  export default function AccountLayout({ children }: LayoutProps) {
34
31
  const pathname = usePathname();
35
- const router = useRouter();
36
32
  const t = useTranslations('core.Profile');
37
33
 
38
34
  const tabs: TabConfig[] = [
@@ -40,26 +36,13 @@ export default function AccountLayout({ children }: LayoutProps) {
40
36
  { value: 'password', label: t('tabPassword'), icon: Lock },
41
37
  { value: 'email', label: t('tabEmail'), icon: Mail },
42
38
  { value: '2fa', label: t('tab2fa'), icon: Shield },
43
- { value: 'accounts', label: t('tabAccounts'), icon: Link },
39
+ { value: 'accounts', label: t('tabAccounts'), icon: LinkIcon },
44
40
  { value: 'sessions', label: t('tabSessions'), icon: Smartphone },
41
+ { value: 'mcp-tokens', label: t('tabMcpTokens'), icon: KeyRound },
45
42
  ];
46
43
 
47
- const getCurrentTab = () => {
48
- if (pathname.includes('/profile')) return 'profile';
49
- if (pathname.includes('/password')) return 'password';
50
- if (pathname.includes('/email')) return 'email';
51
- if (pathname.includes('/2fa')) return '2fa';
52
- if (pathname.includes('/accounts')) return 'accounts';
53
- if (pathname.includes('/sessions')) return 'sessions';
54
- return 'profile';
55
- };
56
-
57
- const handleTabChange = (value: string) => {
58
- router.push(`/core/account/${value}`);
59
- };
60
-
61
44
  return (
62
- <div className="flex flex-col pl-4 h-screen">
45
+ <div className="flex min-w-0 flex-col overflow-x-hidden px-4">
63
46
  <PageHeader
64
47
  breadcrumbs={[
65
48
  { label: t('home'), href: '/' },
@@ -68,38 +51,32 @@ export default function AccountLayout({ children }: LayoutProps) {
68
51
  title={t('title')}
69
52
  description={t('description')}
70
53
  />
71
- <div className="py-2">
72
- <Tabs value={getCurrentTab()} onValueChange={handleTabChange}>
73
- <TabsList className="grid w-full grid-cols-6 lg:w-fit gap-1 rounded-sm">
74
- {tabs.map((tab) => {
75
- const Icon = tab.icon;
76
- const isActive = getCurrentTab() === tab.value;
77
54
 
78
- return (
79
- <Tooltip key={tab.value}>
80
- <TooltipTrigger asChild>
81
- <TabsTrigger
82
- value={tab.value}
83
- className={`gap-2 cursor-pointer hover:bg-primary/10 active:bg-primary/20 rounded-sm ${isActive ? 'text-primary' : 'text-primary/50'}`}
84
- >
85
- <Icon className="size-4" />
86
- <span
87
- className={`hidden sm:inline ${isActive ? 'text-primary' : 'text-primary/50'}`}
88
- >
89
- {tab.label}
90
- </span>
91
- </TabsTrigger>
92
- </TooltipTrigger>
93
- <TooltipContent side="bottom" className="block sm:hidden">
94
- {tab.label}
95
- </TooltipContent>
96
- </Tooltip>
97
- );
98
- })}
99
- </TabsList>
100
- <div className="mt-2">{children}</div>
101
- </Tabs>
55
+ <div className="sticky top-0 z-20 -mx-4 mb-6 border-b border-border bg-background/95 px-4 backdrop-blur supports-backdrop-filter:bg-background/80">
56
+ <nav className="flex items-end gap-1 overflow-x-auto" aria-label="Account tabs">
57
+ {tabs.map((tab) => {
58
+ const Icon = tab.icon;
59
+ const isActive = pathname.includes(`/${tab.value}`);
60
+ return (
61
+ <Link
62
+ key={tab.value}
63
+ href={`/core/account/${tab.value}`}
64
+ className={cn(
65
+ 'flex shrink-0 items-center gap-2 border-b-2 px-3 py-3 text-sm font-medium whitespace-nowrap transition-colors',
66
+ isActive
67
+ ? 'border-primary text-primary'
68
+ : 'border-transparent text-muted-foreground hover:border-muted-foreground hover:text-foreground'
69
+ )}
70
+ >
71
+ <Icon className="size-4" />
72
+ <span className="hidden sm:inline">{tab.label}</span>
73
+ </Link>
74
+ );
75
+ })}
76
+ </nav>
102
77
  </div>
78
+
79
+ <div className="min-w-0 pb-25">{children}</div>
103
80
  </div>
104
81
  );
105
82
  }
@@ -0,0 +1,5 @@
1
+ import { McpTokens } from '../components/mcp-tokens';
2
+
3
+ export default function McpTokensPage() {
4
+ return <McpTokens />;
5
+ }