@hed-hog/core 0.0.321 → 0.0.325

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/dist/auth/auth.controller.d.ts +3 -3
  2. package/dist/auth/auth.service.d.ts +8 -8
  3. package/dist/auth/guards/auth.guard.d.ts.map +1 -1
  4. package/dist/auth/guards/auth.guard.js +0 -1
  5. package/dist/auth/guards/auth.guard.js.map +1 -1
  6. package/dist/core.module.d.ts.map +1 -1
  7. package/dist/core.module.js +3 -0
  8. package/dist/core.module.js.map +1 -1
  9. package/dist/dashboard/dashboard-user/dashboard-user.controller.d.ts +3 -3
  10. package/dist/dashboard/dashboard-user/dashboard-user.service.d.ts +3 -3
  11. package/dist/file/file.controller.d.ts +2 -2
  12. package/dist/file/file.service.d.ts +4 -4
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/mail-profile/dto/create.dto.d.ts +23 -0
  18. package/dist/mail-profile/dto/create.dto.d.ts.map +1 -0
  19. package/dist/mail-profile/dto/create.dto.js +139 -0
  20. package/dist/mail-profile/dto/create.dto.js.map +1 -0
  21. package/dist/mail-profile/dto/update.dto.d.ts +6 -0
  22. package/dist/mail-profile/dto/update.dto.d.ts.map +1 -0
  23. package/dist/mail-profile/dto/update.dto.js +9 -0
  24. package/dist/mail-profile/dto/update.dto.js.map +1 -0
  25. package/dist/mail-profile/mail-profile.controller.d.ts +94 -0
  26. package/dist/mail-profile/mail-profile.controller.d.ts.map +1 -0
  27. package/dist/mail-profile/mail-profile.controller.js +95 -0
  28. package/dist/mail-profile/mail-profile.controller.js.map +1 -0
  29. package/dist/mail-profile/mail-profile.module.d.ts +3 -0
  30. package/dist/mail-profile/mail-profile.module.d.ts.map +1 -0
  31. package/dist/mail-profile/mail-profile.module.js +31 -0
  32. package/dist/mail-profile/mail-profile.module.js.map +1 -0
  33. package/dist/mail-profile/mail-profile.service.d.ts +109 -0
  34. package/dist/mail-profile/mail-profile.service.d.ts.map +1 -0
  35. package/dist/mail-profile/mail-profile.service.js +278 -0
  36. package/dist/mail-profile/mail-profile.service.js.map +1 -0
  37. package/dist/session/session.controller.d.ts +1 -1
  38. package/dist/session/session.service.d.ts +3 -3
  39. package/dist/user/user.controller.d.ts +18 -3
  40. package/dist/user/user.controller.d.ts.map +1 -1
  41. package/dist/user/user.controller.js +22 -0
  42. package/dist/user/user.controller.js.map +1 -1
  43. package/dist/user/user.service.d.ts +22 -7
  44. package/dist/user/user.service.d.ts.map +1 -1
  45. package/dist/user/user.service.js +99 -0
  46. package/dist/user/user.service.js.map +1 -1
  47. package/hedhog/data/menu.yaml +35 -3
  48. package/hedhog/data/route.yaml +64 -0
  49. package/hedhog/frontend/app/mail/page.tsx.ejs +5 -0
  50. package/hedhog/frontend/app/mail/profiles/page.tsx.ejs +825 -0
  51. package/hedhog/frontend/app/users/page.tsx.ejs +25 -1383
  52. package/hedhog/frontend/app/users/permissions.tsx.ejs +109 -55
  53. package/hedhog/frontend/app/users/user-access-sheet.tsx.ejs +304 -0
  54. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +1385 -0
  55. package/hedhog/frontend/messages/en.json +91 -0
  56. package/hedhog/frontend/messages/pt.json +91 -0
  57. package/hedhog/table/mail_profile.yaml +75 -0
  58. package/package.json +4 -4
  59. package/src/auth/guards/auth.guard.ts +0 -2
  60. package/src/core.module.ts +3 -0
  61. package/src/index.ts +2 -0
  62. package/src/mail-profile/dto/create.dto.ts +145 -0
  63. package/src/mail-profile/dto/update.dto.ts +4 -0
  64. package/src/mail-profile/mail-profile.controller.ts +56 -0
  65. package/src/mail-profile/mail-profile.module.ts +18 -0
  66. package/src/mail-profile/mail-profile.service.ts +308 -0
  67. package/src/user/user.controller.ts +13 -0
  68. package/src/user/user.service.ts +98 -5
@@ -1,15 +1,18 @@
1
1
  'use client';
2
2
 
3
3
  import { EmptyState } from '@/components/entity-list';
4
+ import { Button } from '@/components/ui/button';
4
5
  import { Card, CardContent } from '@/components/ui/card';
6
+ import { Input } from '@/components/ui/input';
5
7
  import { Label } from '@/components/ui/label';
6
8
  import { Switch } from '@/components/ui/switch';
7
9
  import { Role } from '@hed-hog/api-types';
8
10
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
9
- import { Loader2, ShieldCheck } from 'lucide-react';
11
+ import { Eye, Loader2, Search, ShieldCheck } from 'lucide-react';
10
12
  import { useTranslations } from 'next-intl';
11
13
  import { useState } from 'react';
12
14
  import { toast } from 'sonner';
15
+ import { UserAccessSheet } from './user-access-sheet';
13
16
 
14
17
  type CustomRole = Role & {
15
18
  isAssigned: boolean;
@@ -17,16 +20,20 @@ type CustomRole = Role & {
17
20
 
18
21
  type PermissionsSectionProps = {
19
22
  userId: number;
23
+ userName?: string;
20
24
  onRoleChange?: () => void;
21
25
  };
22
26
 
23
27
  export function PermissionsSection({
24
28
  userId,
29
+ userName,
25
30
  onRoleChange,
26
31
  }: PermissionsSectionProps) {
27
32
  const t = useTranslations('core.UserPage');
28
33
  const { request, currentLocaleCode } = useApp();
29
34
  const [togglingRoleId, setTogglingRoleId] = useState<number | null>(null);
35
+ const [search, setSearch] = useState('');
36
+ const [accessSheetOpen, setAccessSheetOpen] = useState(false);
30
37
 
31
38
  const {
32
39
  data: userRoles = [],
@@ -71,6 +78,16 @@ export function PermissionsSection({
71
78
  }
72
79
  };
73
80
 
81
+ const filteredRoles = userRoles.filter((role) => {
82
+ if (!search.trim()) return true;
83
+ const q = search.toLowerCase();
84
+ return (
85
+ role.name?.toLowerCase().includes(q) ||
86
+ role.slug?.toLowerCase().includes(q) ||
87
+ role.description?.toLowerCase().includes(q)
88
+ );
89
+ });
90
+
74
91
  if (isLoadingUserRoles) {
75
92
  return (
76
93
  <div className="flex items-center justify-center py-8">
@@ -93,61 +110,98 @@ export function PermissionsSection({
93
110
  }
94
111
 
95
112
  return (
96
- <div className="space-y-2">
97
- {userRoles.map((role, index) => {
98
- const isToggling = togglingRoleId === role.id;
113
+ <>
114
+ <div className="flex items-center gap-2 mb-3">
115
+ <div className="relative flex-1">
116
+ <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
117
+ <Input
118
+ placeholder={t('filterRolesPlaceholder')}
119
+ value={search}
120
+ onChange={(e) => setSearch(e.target.value)}
121
+ className="pl-8 h-8 text-sm"
122
+ />
123
+ </div>
124
+ <Button
125
+ variant="outline"
126
+ size="sm"
127
+ className="shrink-0 gap-1.5"
128
+ onClick={() => setAccessSheetOpen(true)}
129
+ >
130
+ <Eye className="h-4 w-4" />
131
+ {t('viewAccessButton')}
132
+ </Button>
133
+ </div>
134
+
135
+ {filteredRoles.length === 0 ? (
136
+ <div className="flex flex-col items-center justify-center py-10 text-center">
137
+ <Search className="h-8 w-8 text-muted-foreground/40 mb-2" />
138
+ <p className="text-sm text-muted-foreground">{t('noRolesMatch')}</p>
139
+ </div>
140
+ ) : (
141
+ <div className="space-y-2">
142
+ {filteredRoles.map((role, index) => {
143
+ const isToggling = togglingRoleId === role.id;
99
144
 
100
- return (
101
- <Card
102
- key={role.id ?? `role-${index}`}
103
- className={`transition-all py-4 ${
104
- role.isAssigned
105
- ? 'border-primary bg-primary/5'
106
- : 'border-border hover:border-primary/50'
107
- }`}
108
- >
109
- <CardContent className="flex items-center justify-between px-4">
110
- <div className="flex items-center gap-2 flex-1">
111
- <div
112
- className={`rounded-md p-2 ${
113
- role.isAssigned ? 'bg-primary/10' : 'bg-muted'
114
- }`}
115
- >
116
- <ShieldCheck
117
- className={`h-5 w-5 ${
118
- role.isAssigned ? 'text-primary' : 'text-muted-foreground'
119
- }`}
145
+ return (
146
+ <Card
147
+ key={role.id ?? `role-${index}`}
148
+ className={`transition-all py-4 ${
149
+ role.isAssigned
150
+ ? 'border-primary bg-primary/5'
151
+ : 'border-border hover:border-primary/50'
152
+ }`}
153
+ >
154
+ <CardContent className="flex items-center justify-between px-4">
155
+ <div className="flex items-center gap-2 flex-1">
156
+ <div
157
+ className={`rounded-md p-2 ${
158
+ role.isAssigned ? 'bg-primary/10' : 'bg-muted'
159
+ }`}
160
+ >
161
+ <ShieldCheck
162
+ className={`h-5 w-5 ${
163
+ role.isAssigned
164
+ ? 'text-primary'
165
+ : 'text-muted-foreground'
166
+ }`}
167
+ />
168
+ </div>
169
+ <div className="flex-1">
170
+ <Label
171
+ htmlFor={`role-${role.id}`}
172
+ className="text-sm font-medium cursor-pointer"
173
+ >
174
+ {role.name} ({role.slug})
175
+ </Label>
176
+ {role.description && (
177
+ <p className="text-xs text-muted-foreground">
178
+ {role.description}
179
+ </p>
180
+ )}
181
+ </div>
182
+ </div>
183
+ <Switch
184
+ id={`role-${role.id}`}
185
+ checked={role.isAssigned}
186
+ disabled={isToggling}
187
+ onCheckedChange={() =>
188
+ handleToggleRole(role.id!, role.isAssigned)
189
+ }
190
+ className="ml-4"
120
191
  />
121
- </div>
122
- <div className="flex-1">
123
- <Label
124
- htmlFor={`role-${role.id}`}
125
- className={`text-sm font-medium cursor-pointer ${
126
- role.isAssigned ? 'text-foreground' : 'text-foreground'
127
- }`}
128
- >
129
- {role.name} ({role.slug})
130
- </Label>
131
- {role.description && (
132
- <p className="text-xs text-muted-foreground">
133
- {role.description}
134
- </p>
135
- )}
136
- </div>
137
- </div>
138
- <Switch
139
- id={`role-${role.id}`}
140
- checked={role.isAssigned}
141
- disabled={isToggling}
142
- onCheckedChange={() =>
143
- handleToggleRole(role.id!, role.isAssigned)
144
- }
145
- className="ml-4"
146
- />
147
- </CardContent>
148
- </Card>
149
- );
150
- })}
151
- </div>
192
+ </CardContent>
193
+ </Card>
194
+ );
195
+ })}
196
+ </div>
197
+ )}
198
+
199
+ <UserAccessSheet
200
+ open={accessSheetOpen}
201
+ onOpenChange={setAccessSheetOpen}
202
+ userId={userId}
203
+ userName={userName}
204
+ />
205
+ </>
152
206
  );
153
207
  }
@@ -0,0 +1,304 @@
1
+ 'use client';
2
+
3
+ import { Badge } from '@/components/ui/badge';
4
+ import {
5
+ Sheet,
6
+ SheetContent,
7
+ SheetDescription,
8
+ SheetHeader,
9
+ SheetTitle,
10
+ } from '@/components/ui/sheet';
11
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
12
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
13
+ import { Globe, Loader2, Lock, Menu } from 'lucide-react';
14
+ import { useTranslations } from 'next-intl';
15
+
16
+ type MenuRole = { name: string; slug: string };
17
+
18
+ type UserMenu = {
19
+ id: number;
20
+ menuId: number | null;
21
+ name: string;
22
+ url?: string;
23
+ icon?: string;
24
+ order?: number;
25
+ roles?: MenuRole[];
26
+ };
27
+
28
+ type MenuNode = UserMenu & { children: MenuNode[] };
29
+
30
+ type UserRoute = {
31
+ id: number;
32
+ url: string;
33
+ method: string;
34
+ roles?: MenuRole[];
35
+ };
36
+
37
+ type UserAccessSheetProps = {
38
+ open: boolean;
39
+ onOpenChange: (open: boolean) => void;
40
+ userId: number;
41
+ userName?: string;
42
+ };
43
+
44
+ const METHOD_COLORS: Record<string, string> = {
45
+ GET: 'bg-green-50 text-green-700 border-green-200',
46
+ POST: 'bg-blue-50 text-blue-700 border-blue-200',
47
+ PATCH: 'bg-amber-50 text-amber-700 border-amber-200',
48
+ PUT: 'bg-orange-50 text-orange-700 border-orange-200',
49
+ DELETE: 'bg-rose-50 text-rose-700 border-rose-200',
50
+ };
51
+
52
+ function buildTree(flat: UserMenu[]): MenuNode[] {
53
+ const map = new Map<number, MenuNode>();
54
+ flat.forEach((m) => map.set(m.id, { ...m, children: [] }));
55
+
56
+ const roots: MenuNode[] = [];
57
+ map.forEach((node) => {
58
+ if (node.menuId != null && map.has(node.menuId)) {
59
+ map.get(node.menuId)!.children.push(node);
60
+ } else {
61
+ roots.push(node);
62
+ }
63
+ });
64
+
65
+ const sortNodes = (nodes: MenuNode[]) => {
66
+ nodes.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
67
+ nodes.forEach((n) => sortNodes(n.children));
68
+ };
69
+ sortNodes(roots);
70
+
71
+ return roots;
72
+ }
73
+
74
+ function MenuIcon({ name, className }: { name?: string; className?: string }) {
75
+ if (name) {
76
+ try {
77
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
78
+ const { [name]: DynamicIcon } = require('lucide-react');
79
+ if (DynamicIcon) return <DynamicIcon className={className} />;
80
+ } catch {
81
+ // fallback below
82
+ }
83
+ }
84
+ return <Menu className={className} />;
85
+ }
86
+
87
+ function MenuTreeNode({ node, depth }: { node: MenuNode; depth: number }) {
88
+ const isLeaf = node.children.length === 0;
89
+ const indent = depth * 16;
90
+
91
+ return (
92
+ <>
93
+ <div
94
+ className="flex items-center gap-2 rounded py-1 px-1 hover:bg-muted/50 transition-colors group"
95
+ style={{ paddingLeft: `${indent + 4}px` }}
96
+ >
97
+ {/* tree line connector */}
98
+ {depth > 0 && (
99
+ <span className="shrink-0 w-3 h-px bg-border" aria-hidden />
100
+ )}
101
+
102
+ <span
103
+ className={`shrink-0 flex items-center justify-center rounded p-1 ${
104
+ isLeaf ? 'bg-muted/60' : 'bg-primary/10'
105
+ }`}
106
+ >
107
+ <MenuIcon
108
+ name={node.icon}
109
+ className={`h-3.5 w-3.5 ${isLeaf ? 'text-muted-foreground' : 'text-primary'}`}
110
+ />
111
+ </span>
112
+
113
+ <div className="flex-1 min-w-0">
114
+ <span className="text-sm leading-tight truncate block">
115
+ {node.name}
116
+ </span>
117
+ {node.url && (
118
+ <span className="text-xs text-muted-foreground truncate block font-mono">
119
+ {node.url}
120
+ </span>
121
+ )}
122
+ {node.roles && node.roles.length > 0 && (
123
+ <div className="flex flex-wrap gap-1 mt-0.5">
124
+ {node.roles.map((role) => (
125
+ <span
126
+ key={role.slug}
127
+ className="inline-block rounded-full bg-muted px-1.5 py-0 text-[10px] font-medium text-muted-foreground leading-4"
128
+ title={role.slug}
129
+ >
130
+ {role.name || role.slug}
131
+ </span>
132
+ ))}
133
+ </div>
134
+ )}
135
+ </div>
136
+ </div>
137
+
138
+ {node.children.map((child) => (
139
+ <MenuTreeNode key={child.id} node={child} depth={depth + 1} />
140
+ ))}
141
+ </>
142
+ );
143
+ }
144
+
145
+ export function UserAccessSheet({
146
+ open,
147
+ onOpenChange,
148
+ userId,
149
+ userName,
150
+ }: UserAccessSheetProps) {
151
+ const t = useTranslations('core.UserPage');
152
+ const { request, currentLocaleCode } = useApp();
153
+
154
+ const { data: menus = [], isLoading: loadingMenus } = useQuery<UserMenu[]>({
155
+ queryKey: ['user-menus', userId, currentLocaleCode],
156
+ queryFn: async () => {
157
+ const res = await request<UserMenu[]>({
158
+ url: `/user/${userId}/menu`,
159
+ method: 'GET',
160
+ });
161
+ return res.data || [];
162
+ },
163
+ enabled: open && !!userId,
164
+ });
165
+
166
+ const { data: routes = [], isLoading: loadingRoutes } = useQuery<UserRoute[]>(
167
+ {
168
+ queryKey: ['user-routes', userId],
169
+ queryFn: async () => {
170
+ const res = await request<UserRoute[]>({
171
+ url: `/user/${userId}/route`,
172
+ method: 'GET',
173
+ });
174
+ return res.data || [];
175
+ },
176
+ enabled: open && !!userId,
177
+ }
178
+ );
179
+
180
+ const menuTree = buildTree(menus);
181
+
182
+ return (
183
+ <Sheet open={open} onOpenChange={onOpenChange}>
184
+ <SheetContent className="w-full sm:max-w-xl overflow-y-auto gap-0">
185
+ <SheetHeader className="px-6 pt-6 pb-4">
186
+ <SheetTitle className="flex items-center gap-2">
187
+ <Lock className="h-5 w-5 text-primary" />
188
+ {t('accessViewTitle')}
189
+ </SheetTitle>
190
+ <SheetDescription>
191
+ {userName
192
+ ? t('accessViewDescriptionUser', { name: userName })
193
+ : t('accessViewDescription')}
194
+ </SheetDescription>
195
+ </SheetHeader>
196
+
197
+ <div className="w-full border-t" />
198
+
199
+ <Tabs defaultValue="menu" className="px-6 pt-4">
200
+ <TabsList className="w-full mb-4">
201
+ <TabsTrigger
202
+ value="menu"
203
+ className="flex-1 flex items-center gap-2"
204
+ >
205
+ <Menu className="h-4 w-4" />
206
+ {t('accessTabMenu')}
207
+ {!loadingMenus && (
208
+ <Badge variant="secondary" className="ml-1 text-xs">
209
+ {menus.length}
210
+ </Badge>
211
+ )}
212
+ </TabsTrigger>
213
+ <TabsTrigger
214
+ value="routes"
215
+ className="flex-1 flex items-center gap-2"
216
+ >
217
+ <Globe className="h-4 w-4" />
218
+ {t('accessTabRoutes')}
219
+ {!loadingRoutes && (
220
+ <Badge variant="secondary" className="ml-1 text-xs">
221
+ {routes.length}
222
+ </Badge>
223
+ )}
224
+ </TabsTrigger>
225
+ </TabsList>
226
+
227
+ <TabsContent value="menu" className="mt-0">
228
+ {loadingMenus ? (
229
+ <div className="flex items-center justify-center py-12">
230
+ <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
231
+ <span className="ml-2 text-sm text-muted-foreground">
232
+ {t('loading')}
233
+ </span>
234
+ </div>
235
+ ) : menuTree.length === 0 ? (
236
+ <div className="flex flex-col items-center justify-center py-12 text-center">
237
+ <Menu className="h-10 w-10 text-muted-foreground/40 mb-3" />
238
+ <p className="text-sm text-muted-foreground">
239
+ {t('accessNoMenus')}
240
+ </p>
241
+ </div>
242
+ ) : (
243
+ <div className="rounded-lg border border-border/60 bg-card px-2 py-2">
244
+ {menuTree.map((node) => (
245
+ <MenuTreeNode key={node.id} node={node} depth={0} />
246
+ ))}
247
+ </div>
248
+ )}
249
+ </TabsContent>
250
+
251
+ <TabsContent value="routes" className="space-y-2 mt-0">
252
+ {loadingRoutes ? (
253
+ <div className="flex items-center justify-center py-12">
254
+ <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
255
+ <span className="ml-2 text-sm text-muted-foreground">
256
+ {t('loading')}
257
+ </span>
258
+ </div>
259
+ ) : routes.length === 0 ? (
260
+ <div className="flex flex-col items-center justify-center py-12 text-center">
261
+ <Globe className="h-10 w-10 text-muted-foreground/40 mb-3" />
262
+ <p className="text-sm text-muted-foreground">
263
+ {t('accessNoRoutes')}
264
+ </p>
265
+ </div>
266
+ ) : (
267
+ routes.map((route) => (
268
+ <div
269
+ key={route.id}
270
+ className="flex items-start gap-3 rounded-lg border border-border/60 bg-card px-4 py-2.5 hover:border-primary/40 transition-colors"
271
+ >
272
+ <span
273
+ className={`shrink-0 rounded border px-2 py-0.5 text-xs font-mono font-semibold ${
274
+ METHOD_COLORS[route.method] ??
275
+ 'bg-muted text-muted-foreground border-border'
276
+ }`}
277
+ >
278
+ {route.method}
279
+ </span>
280
+ <div className="flex-1 min-w-0">
281
+ <p className="text-sm font-mono truncate">{route.url}</p>
282
+ {route.roles && route.roles.length > 0 && (
283
+ <div className="flex flex-wrap gap-1 mt-0.5">
284
+ {route.roles.map((role) => (
285
+ <span
286
+ key={role.slug}
287
+ className="inline-block rounded-full bg-muted px-1.5 py-0 text-[10px] font-medium text-muted-foreground leading-4"
288
+ title={role.slug}
289
+ >
290
+ {role.name || role.slug}
291
+ </span>
292
+ ))}
293
+ </div>
294
+ )}
295
+ </div>
296
+ </div>
297
+ ))
298
+ )}
299
+ </TabsContent>
300
+ </Tabs>
301
+ </SheetContent>
302
+ </Sheet>
303
+ );
304
+ }