@seedgrid/fe-security 2026.7.0 → 2026.7.1

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 (124) hide show
  1. package/dist/client/index.d.ts +4 -0
  2. package/dist/client/index.d.ts.map +1 -0
  3. package/dist/client/index.js +5 -0
  4. package/dist/client/permission-provider.d.ts +18 -0
  5. package/dist/client/permission-provider.d.ts.map +1 -0
  6. package/dist/client/permission-provider.js +57 -0
  7. package/dist/client/pick-list-field.d.ts +15 -0
  8. package/dist/client/pick-list-field.d.ts.map +1 -0
  9. package/dist/client/pick-list-field.js +30 -0
  10. package/dist/client/status-badge.d.ts +6 -0
  11. package/dist/client/status-badge.d.ts.map +1 -0
  12. package/dist/client/status-badge.js +6 -0
  13. package/dist/config.d.ts +34 -0
  14. package/dist/config.d.ts.map +1 -0
  15. package/dist/config.js +4 -0
  16. package/dist/dtos.d.ts +88 -0
  17. package/dist/dtos.d.ts.map +1 -0
  18. package/dist/dtos.js +3 -0
  19. package/dist/index.d.ts +8 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +10 -0
  22. package/dist/menu.d.ts +19 -0
  23. package/dist/menu.d.ts.map +1 -0
  24. package/dist/menu.js +41 -0
  25. package/dist/messages.d.ts +1858 -0
  26. package/dist/messages.d.ts.map +1 -0
  27. package/dist/messages.js +990 -0
  28. package/dist/module.d.ts +3 -0
  29. package/dist/module.d.ts.map +1 -0
  30. package/dist/module.js +37 -0
  31. package/dist/module.json +21 -0
  32. package/dist/paths.d.ts +1 -0
  33. package/dist/paths.d.ts.map +1 -1
  34. package/dist/paths.js +3 -0
  35. package/dist/permissions.d.ts +14 -0
  36. package/dist/permissions.d.ts.map +1 -0
  37. package/dist/permissions.js +39 -0
  38. package/dist/server/action.d.ts +3 -0
  39. package/dist/server/action.d.ts.map +1 -0
  40. package/dist/server/action.js +21 -0
  41. package/dist/server/create-security-server.d.ts +4 -0
  42. package/dist/server/create-security-server.d.ts.map +1 -0
  43. package/dist/server/create-security-server.js +166 -0
  44. package/dist/server/errors.d.ts +13 -0
  45. package/dist/server/errors.d.ts.map +1 -0
  46. package/dist/server/errors.js +26 -0
  47. package/dist/server/index.d.ts +8 -0
  48. package/dist/server/index.d.ts.map +1 -0
  49. package/dist/server/index.js +8 -0
  50. package/dist/server/problem-response.d.ts +3 -0
  51. package/dist/server/problem-response.d.ts.map +1 -0
  52. package/dist/server/problem-response.js +28 -0
  53. package/dist/server/route.d.ts +3 -0
  54. package/dist/server/route.d.ts.map +1 -0
  55. package/dist/server/route.js +22 -0
  56. package/dist/server/to-problem.d.ts +3 -0
  57. package/dist/server/to-problem.d.ts.map +1 -0
  58. package/dist/server/to-problem.js +22 -0
  59. package/dist/server-contract.d.ts +23 -0
  60. package/dist/server-contract.d.ts.map +1 -0
  61. package/dist/server-contract.js +4 -0
  62. package/dist/session-contract.d.ts +36 -0
  63. package/dist/session-contract.d.ts.map +1 -0
  64. package/dist/session-contract.js +8 -0
  65. package/dist/session-result.d.ts +22 -0
  66. package/dist/session-result.d.ts.map +1 -0
  67. package/dist/session-result.js +14 -0
  68. package/package.json +38 -3
  69. package/scaffold/README.md +97 -0
  70. package/scaffold/src/app/api/security/session/2fa/resend/route.ts +8 -0
  71. package/scaffold/src/app/api/security/session/2fa/verify/route.ts +8 -0
  72. package/scaffold/src/app/api/security/session/login/route.ts +7 -0
  73. package/scaffold/src/app/api/security/session/logout/route.ts +5 -0
  74. package/scaffold/src/app/api/security/session/refresh/route.ts +7 -0
  75. package/scaffold/src/app/api/security/users/[publicId]/avatar/route.ts +15 -0
  76. package/scaffold/src/app/auth/2fa/verify/page.tsx +286 -0
  77. package/scaffold/src/app/auth/forgot-password/confirm/page.tsx +96 -0
  78. package/scaffold/src/app/auth/forgot-password/page.tsx +165 -0
  79. package/scaffold/src/app/auth/forgot-password/success/page.tsx +31 -0
  80. package/scaffold/src/app/auth/login/page.tsx +386 -0
  81. package/scaffold/src/app/auth/welcome/page.tsx +90 -0
  82. package/scaffold/src/app/login/page.tsx +7 -0
  83. package/scaffold/src/app/page.tsx +12 -0
  84. package/scaffold/src/app/security/2fa/page.tsx +370 -0
  85. package/scaffold/src/app/security/companies/(create)/new/page.tsx +12 -0
  86. package/scaffold/src/app/security/companies/(edit)/[publicId]/page.tsx +18 -0
  87. package/scaffold/src/app/security/companies/(list)/page.tsx +298 -0
  88. package/scaffold/src/app/security/companies/editors/CompanyEditor.tsx +754 -0
  89. package/scaffold/src/app/security/profile/page.tsx +492 -0
  90. package/scaffold/src/app/security/roles/(create)/new/page.tsx +11 -0
  91. package/scaffold/src/app/security/roles/(edit)/[publicId]/page.tsx +17 -0
  92. package/scaffold/src/app/security/roles/(list)/page.tsx +269 -0
  93. package/scaffold/src/app/security/roles/editors/RoleEditor.tsx +328 -0
  94. package/scaffold/src/app/security/users/(create)/new/page.tsx +10 -0
  95. package/scaffold/src/app/security/users/(edit)/[publicId]/page.tsx +16 -0
  96. package/scaffold/src/app/security/users/(list)/page.tsx +289 -0
  97. package/scaffold/src/app/security/users/editors/UserEditor.tsx +478 -0
  98. package/scaffold/src/app/users/change-password/page.tsx +172 -0
  99. package/scaffold/src/modules/security/AuthProvider.tsx +405 -0
  100. package/scaffold/src/modules/security/components/AppShell.tsx.hbs +889 -0
  101. package/scaffold/src/modules/security/components/CompanyManagementGuard.tsx +59 -0
  102. package/scaffold/src/modules/security/components/PageFrame.tsx +105 -0
  103. package/scaffold/src/modules/security/components/RouteGuard.tsx +114 -0
  104. package/scaffold/src/modules/security/components/SecurityDashboard.tsx.hbs +315 -0
  105. package/scaffold/src/modules/security/config.ts +55 -0
  106. package/scaffold/src/modules/security/hooks/useAuth.ts +7 -0
  107. package/scaffold/src/modules/security/hooks/useDebouncedSearch.ts +13 -0
  108. package/scaffold/src/modules/security/hooks/usePermission.ts +35 -0
  109. package/scaffold/src/modules/security/host-access.ts +59 -0
  110. package/scaffold/src/modules/security/index.ts +44 -0
  111. package/scaffold/src/modules/security/navigation.ts +41 -0
  112. package/scaffold/src/modules/security/server/avatar-routes.ts +86 -0
  113. package/scaffold/src/modules/security/server/public-api-base-url.ts +26 -0
  114. package/scaffold/src/modules/security/server/session-routes.ts.hbs +322 -0
  115. package/scaffold/src/modules/security/server/session-server.ts.hbs +30 -0
  116. package/scaffold/src/modules/security/services/auth-service.ts +332 -0
  117. package/scaffold/src/modules/security/services/security-api.test.ts +178 -0
  118. package/scaffold/src/modules/security/services/security-api.ts +281 -0
  119. package/scaffold/src/modules/security/services/security-service.test.ts +71 -0
  120. package/scaffold/src/modules/security/services/security-service.ts +727 -0
  121. package/scaffold/src/modules/security/services/session-storage.ts +80 -0
  122. package/scaffold/src/modules/security/types.ts +295 -0
  123. package/scaffold/src/modules/security/useSecurityAppShellSections.tsx.hbs +180 -0
  124. package/scaffold/src/modules/security/utils.ts +194 -0
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+
5
+ import { SgButton } from "@seedgrid/fe-components";
6
+ import { useRouter } from "next/navigation";
7
+
8
+ import { useI18n } from "@/i18n";
9
+
10
+ import { securityConfig } from "../config";
11
+ import { useCompanyManagementEnabledForCurrentHost } from "../host-access";
12
+ import { InlineNotice, PageFrame } from "./PageFrame";
13
+
14
+ export function CompanyManagementGuard({
15
+ children,
16
+ }: {
17
+ children: ReactNode;
18
+ }) {
19
+ const router = useRouter();
20
+ const { t } = useI18n();
21
+ const companiesEnabled = useCompanyManagementEnabledForCurrentHost();
22
+
23
+ if (companiesEnabled == null) {
24
+ return (
25
+ <PageFrame
26
+ title={t("security.guard.loading.title")}
27
+ description={t("security.guard.loading.description")}
28
+ >
29
+ <InlineNotice>{t("security.guard.loading.message")}</InlineNotice>
30
+ </PageFrame>
31
+ );
32
+ }
33
+
34
+ if (!companiesEnabled) {
35
+ return (
36
+ <PageFrame
37
+ title={t("security.guard.unauthorized.title")}
38
+ description={t("security.guard.unauthorized.description")}
39
+ actions={
40
+ <SgButton
41
+ appearance="outline"
42
+ severity="primary"
43
+ shape="rounded"
44
+ label={t("security.guard.unauthorized.action")}
45
+ onClick={() => {
46
+ router.push(securityConfig.routes.dashboard);
47
+ }}
48
+ />
49
+ }
50
+ >
51
+ <InlineNotice tone="warning">
52
+ {t("security.guard.unauthorized.message")}
53
+ </InlineNotice>
54
+ </PageFrame>
55
+ );
56
+ }
57
+
58
+ return <>{children}</>;
59
+ }
@@ -0,0 +1,105 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+
5
+ import { SgBadge, SgCard, SgStack, SgWhistleHost } from "@seedgrid/fe-components";
6
+ import { useSgTheme } from "@/theme";
7
+
8
+ type PageFrameProps = {
9
+ title: string;
10
+ description?: string;
11
+ actions?: ReactNode;
12
+ children: ReactNode;
13
+ readonlyLabel?: string | null;
14
+ };
15
+
16
+ export function PageFrame({
17
+ title,
18
+ description,
19
+ actions,
20
+ children,
21
+ readonlyLabel,
22
+ }: PageFrameProps) {
23
+ const { currentMode } = useSgTheme();
24
+ const isDarkMode = currentMode === "dark";
25
+
26
+ return (
27
+ <section className="w-full">
28
+ <div className="mx-auto w-full max-w-none">
29
+ <SgCard
30
+ cardStyle="elevated"
31
+ size="lg"
32
+ className={`rounded-[2rem] border shadow-xl backdrop-blur ${
33
+ isDarkMode
34
+ ? "border-slate-800 bg-slate-900/95 shadow-black/20"
35
+ : "border-white/70 bg-white/92 shadow-slate-900/5"
36
+ }`}
37
+ title={
38
+ <SgStack gap={8}>
39
+ {readonlyLabel ? (
40
+ <SgBadge
41
+ value={readonlyLabel}
42
+ severity="warning"
43
+ badgeStyle="soft"
44
+ rounded
45
+ />
46
+ ) : null}
47
+ <div
48
+ className={`text-2xl font-semibold sm:text-3xl ${
49
+ isDarkMode ? "text-slate-50" : "text-slate-950"
50
+ }`}
51
+ >
52
+ {title}
53
+ </div>
54
+ </SgStack>
55
+ }
56
+ description={description}
57
+ actions={actions}
58
+ headerClassName={
59
+ isDarkMode
60
+ ? "border-b border-slate-800 bg-slate-950/70"
61
+ : "border-b border-slate-200/80 bg-slate-50/80"
62
+ }
63
+ >
64
+ <SgStack gap={16}>
65
+ <SgWhistleHost className="w-full" newestOnTop max={2} />
66
+ {children}
67
+ </SgStack>
68
+ </SgCard>
69
+ </div>
70
+ </section>
71
+ );
72
+ }
73
+
74
+ export function InlineNotice({
75
+ tone = "info",
76
+ children,
77
+ }: {
78
+ tone?: "danger" | "info" | "success" | "warning";
79
+ children: ReactNode;
80
+ }) {
81
+ const { currentMode } = useSgTheme();
82
+ const isDarkMode = currentMode === "dark";
83
+ const className =
84
+ tone === "danger"
85
+ ? isDarkMode
86
+ ? "border-rose-900/70 bg-rose-950/40 text-rose-100"
87
+ : "border-rose-200 bg-rose-50 text-rose-950"
88
+ : tone === "success"
89
+ ? isDarkMode
90
+ ? "border-emerald-900/70 bg-emerald-950/40 text-emerald-100"
91
+ : "border-emerald-200 bg-emerald-50 text-emerald-950"
92
+ : tone === "warning"
93
+ ? isDarkMode
94
+ ? "border-amber-900/70 bg-amber-950/35 text-amber-100"
95
+ : "border-amber-200 bg-amber-50 text-amber-950"
96
+ : isDarkMode
97
+ ? "border-sky-900/70 bg-sky-950/35 text-sky-100"
98
+ : "border-sky-200 bg-sky-50 text-sky-950";
99
+
100
+ return (
101
+ <div className={`rounded-2xl border p-4 text-sm leading-6 ${className}`}>
102
+ {children}
103
+ </div>
104
+ );
105
+ }
@@ -0,0 +1,114 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import { useEffect } from "react";
5
+
6
+ import { SgButton } from "@seedgrid/fe-components";
7
+ import { usePathname, useRouter } from "next/navigation";
8
+
9
+ import { useI18n } from "@/i18n";
10
+
11
+ import { securityConfig } from "../config";
12
+ import { useAuth } from "../hooks/useAuth";
13
+ import { InlineNotice, PageFrame } from "./PageFrame";
14
+
15
+ type RouteGuardProps = {
16
+ children: ReactNode;
17
+ permissions?: string[];
18
+ permissionMode?: "all" | "any";
19
+ };
20
+
21
+ export function RouteGuard({
22
+ children,
23
+ permissions,
24
+ permissionMode = "all",
25
+ }: RouteGuardProps) {
26
+ const router = useRouter();
27
+ const pathname = usePathname();
28
+ const { t } = useI18n();
29
+ const auth = useAuth();
30
+
31
+ useEffect(() => {
32
+ if (!auth.isInitializing && !auth.isAuthenticated) {
33
+ router.replace(securityConfig.routes.login);
34
+ return;
35
+ }
36
+
37
+ if (
38
+ !auth.isInitializing &&
39
+ auth.isAuthenticated &&
40
+ auth.session?.changePasswordRequired &&
41
+ pathname !== securityConfig.routes.changePassword
42
+ ) {
43
+ router.replace(securityConfig.routes.changePassword);
44
+ }
45
+ }, [auth.isAuthenticated, auth.isInitializing, auth.session?.changePasswordRequired, pathname, router]);
46
+
47
+ if (auth.isInitializing) {
48
+ return (
49
+ <PageFrame
50
+ title={t("security.guard.loading.title")}
51
+ description={t("security.guard.loading.description")}
52
+ >
53
+ <InlineNotice>{t("security.guard.loading.message")}</InlineNotice>
54
+ </PageFrame>
55
+ );
56
+ }
57
+
58
+ if (!auth.isAuthenticated) {
59
+ return (
60
+ <PageFrame
61
+ title={t("security.guard.redirecting.title")}
62
+ description={t("security.guard.redirecting.description")}
63
+ >
64
+ <InlineNotice>{t("security.guard.redirecting.message")}</InlineNotice>
65
+ </PageFrame>
66
+ );
67
+ }
68
+
69
+ if (
70
+ auth.session?.changePasswordRequired &&
71
+ pathname !== securityConfig.routes.changePassword
72
+ ) {
73
+ return (
74
+ <PageFrame
75
+ title={t("security.guard.redirecting.title")}
76
+ description={t("security.guard.redirecting.description")}
77
+ >
78
+ <InlineNotice>{t("security.guard.redirecting.message")}</InlineNotice>
79
+ </PageFrame>
80
+ );
81
+ }
82
+
83
+ const isAllowed = !permissions?.length
84
+ ? true
85
+ : permissionMode === "any"
86
+ ? auth.hasAnyPermission(permissions)
87
+ : auth.hasAllPermissions(permissions);
88
+
89
+ if (!isAllowed) {
90
+ return (
91
+ <PageFrame
92
+ title={t("security.guard.unauthorized.title")}
93
+ description={t("security.guard.unauthorized.description")}
94
+ actions={
95
+ <SgButton
96
+ appearance="outline"
97
+ severity="primary"
98
+ shape="rounded"
99
+ label={t("security.guard.unauthorized.action")}
100
+ onClick={() => {
101
+ router.push(securityConfig.routes.login);
102
+ }}
103
+ />
104
+ }
105
+ >
106
+ <InlineNotice tone="warning">
107
+ {t("security.guard.unauthorized.message")}
108
+ </InlineNotice>
109
+ </PageFrame>
110
+ );
111
+ }
112
+
113
+ return <>{children}</>;
114
+ }
@@ -0,0 +1,315 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+
5
+ import { SgBadge, SgButton, SgCard, SgGrid, SgStack } from "@seedgrid/fe-components";
6
+ import {
7
+ ArrowRight,
8
+ Building2,
9
+ KeyRound,
10
+ LockKeyhole,
11
+ Shield,
12
+ ShieldCheck,
13
+ Users,
14
+ } from "lucide-react";
15
+ import { useRouter } from "next/navigation";
16
+
17
+ import { useI18n } from "@/i18n";
18
+ import { useSgTheme } from "@/theme";
19
+
20
+ import { securityConfig } from "../config";
21
+ import { useCompanyManagementEnabledForCurrentHost } from "../host-access";
22
+ import { useAuth } from "../hooks/useAuth";
23
+ import { useCrudPermission, usePermission } from "../hooks/usePermission";
24
+
25
+ type DashboardModule = {
26
+ id: string;
27
+ title: string;
28
+ description: string;
29
+ route: string;
30
+ readOnly: boolean;
31
+ icon: ReactNode;
32
+ };
33
+
34
+ export function SecurityDashboard() {
35
+ const router = useRouter();
36
+ const { t } = useI18n();
37
+ const auth = useAuth();
38
+ const { currentMode } = useSgTheme();
39
+ const isDarkMode = currentMode === "dark";
40
+ const companiesEnabled = useCompanyManagementEnabledForCurrentHost();
41
+ const userPermissions = useCrudPermission("USER");
42
+ const companyPermissions = useCrudPermission("COMPANY");
43
+ const rolePermissions = useCrudPermission("ROLE");
44
+ {{#if security_api_keys_enabled}}
45
+ const canReadApiKeys = usePermission("APIKEY_READ");
46
+ const canCreateApiKeys = usePermission("APIKEY_CREATE");
47
+ const canRevokeApiKeys = usePermission("APIKEY_REVOKE");
48
+ {{/if}}
49
+ const canChangePassword = usePermission("USER_CHANGE_PASSWORD");
50
+ const currentUser = auth.session?.login?.trim() || t("common.brand");
51
+ const currentRole = auth.roles[0] || t("security.dashboard.user.subtitle");
52
+ const accessibleModules: DashboardModule[] = [];
53
+
54
+ if (userPermissions.canRead) {
55
+ accessibleModules.push({
56
+ id: "users",
57
+ title: t("security.users.list.title"),
58
+ description: t("security.users.list.description"),
59
+ route: securityConfig.routes.users,
60
+ readOnly: userPermissions.isReadOnly,
61
+ icon: <Users size={18} />,
62
+ });
63
+ }
64
+
65
+ if (companiesEnabled && companyPermissions.canRead) {
66
+ accessibleModules.push({
67
+ id: "companies",
68
+ title: t("security.companies.list.title"),
69
+ description: t("security.companies.list.description"),
70
+ route: securityConfig.routes.companies,
71
+ readOnly: companyPermissions.isReadOnly,
72
+ icon: <Building2 size={18} />,
73
+ });
74
+ }
75
+
76
+ {{#if security_api_keys_enabled}}
77
+ if (canReadApiKeys) {
78
+ accessibleModules.push({
79
+ id: "api-keys",
80
+ title: t("security.api_keys.title"),
81
+ description: t("security.api_keys.description"),
82
+ route: securityConfig.routes.apiKeys,
83
+ readOnly: !canCreateApiKeys && !canRevokeApiKeys,
84
+ icon: <KeyRound size={18} />,
85
+ });
86
+ }
87
+ {{/if}}
88
+
89
+ if (rolePermissions.canRead) {
90
+ accessibleModules.push({
91
+ id: "roles",
92
+ title: t("security.roles.list.title"),
93
+ description: t("security.roles.list.description"),
94
+ route: securityConfig.routes.roles,
95
+ readOnly: rolePermissions.isReadOnly,
96
+ icon: <Shield size={18} />,
97
+ });
98
+ }
99
+
100
+ const statCards = [
101
+ {
102
+ id: "modules",
103
+ label: "Modulos liberados",
104
+ value: String(accessibleModules.length),
105
+ icon: <ShieldCheck size={18} />,
106
+ },
107
+ {
108
+ id: "profile",
109
+ label: "Perfil atual",
110
+ value: currentRole,
111
+ icon: <LockKeyhole size={18} />,
112
+ },
113
+ ];
114
+
115
+ return (
116
+ <SgStack gap={24}>
117
+ <SgGrid
118
+ minItemWidth={360}
119
+ gap={16}
120
+ >
121
+ <SgCard
122
+ cardStyle="elevated"
123
+ size="lg"
124
+ className={`rounded-[2rem] border shadow-lg ${
125
+ isDarkMode
126
+ ? "border-slate-800 bg-slate-900 shadow-black/20"
127
+ : "border-slate-200 bg-white shadow-slate-900/5"
128
+ }`}
129
+ title={
130
+ <SgStack gap={12}>
131
+ <div className="text-xs font-semibold uppercase tracking-[0.24em] text-blue-500">
132
+ {t("security.dashboard.title")}
133
+ </div>
134
+ <div
135
+ className={`text-3xl font-semibold ${
136
+ isDarkMode ? "text-slate-50" : "text-slate-950"
137
+ }`}
138
+ >
139
+ {currentUser}
140
+ </div>
141
+ </SgStack>
142
+ }
143
+ description={t("security.dashboard.description")}
144
+ >
145
+ <SgStack gap={24}>
146
+ <SgGrid columns={2} gap={12}>
147
+ {statCards.map((item) => (
148
+ <div
149
+ key={item.id}
150
+ className={`rounded-[1.5rem] border p-4 ${
151
+ isDarkMode
152
+ ? "border-slate-800 bg-slate-950/70"
153
+ : "border-slate-200 bg-slate-50"
154
+ }`}
155
+ >
156
+ <div
157
+ className={`flex items-center gap-3 ${
158
+ isDarkMode ? "text-slate-400" : "text-slate-500"
159
+ }`}
160
+ >
161
+ <span
162
+ className={`flex size-10 items-center justify-center rounded-2xl text-blue-600 shadow-sm ${
163
+ isDarkMode ? "bg-slate-900" : "bg-white"
164
+ }`}
165
+ >
166
+ {item.icon}
167
+ </span>
168
+ <span className="text-sm font-medium">{item.label}</span>
169
+ </div>
170
+ <div
171
+ className={`mt-4 text-xl font-semibold ${
172
+ isDarkMode ? "text-slate-50" : "text-slate-950"
173
+ }`}
174
+ >
175
+ {item.value}
176
+ </div>
177
+ </div>
178
+ ))}
179
+ </SgGrid>
180
+
181
+ <SgStack direction="row" wrap gap={12}>
182
+ {canChangePassword ? (
183
+ <SgButton
184
+ appearance="outline"
185
+ severity="secondary"
186
+ shape="rounded"
187
+ label={t("security.change_password.title")}
188
+ onClick={() => {
189
+ router.push(securityConfig.routes.changePassword);
190
+ }}
191
+ />
192
+ ) : null}
193
+
194
+ <SgButton
195
+ appearance="solid"
196
+ severity="primary"
197
+ shape="rounded"
198
+ label={t("security.two_factor.title")}
199
+ onClick={() => {
200
+ router.push(securityConfig.routes.twoFactor);
201
+ }}
202
+ />
203
+ </SgStack>
204
+ </SgStack>
205
+ </SgCard>
206
+
207
+ <SgCard
208
+ cardStyle="elevated"
209
+ size="lg"
210
+ className={`rounded-[2rem] border shadow-lg ${
211
+ isDarkMode
212
+ ? "border-slate-800 bg-slate-900 shadow-black/20"
213
+ : "border-slate-200 bg-white shadow-slate-900/5"
214
+ }`}
215
+ title={
216
+ <SgStack direction="row" justify="between" align="center" gap={16}>
217
+ <div>
218
+ <div
219
+ className={`text-lg font-semibold ${
220
+ isDarkMode ? "text-slate-50" : "text-slate-950"
221
+ }`}
222
+ >
223
+ Atalhos de acesso
224
+ </div>
225
+ <div
226
+ className={`text-sm ${
227
+ isDarkMode ? "text-slate-400" : "text-slate-500"
228
+ }`}
229
+ >
230
+ Rotas disponiveis para o perfil autenticado.
231
+ </div>
232
+ </div>
233
+ <SgBadge
234
+ value={String(accessibleModules.length)}
235
+ severity="info"
236
+ badgeStyle="soft"
237
+ rounded
238
+ />
239
+ </SgStack>
240
+ }
241
+ >
242
+ <SgStack gap={12}>
243
+ {accessibleModules.length ? (
244
+ accessibleModules.map((moduleItem) => (
245
+ <SgButton
246
+ key={moduleItem.id}
247
+ appearance="ghost"
248
+ severity="info"
249
+ onClick={() => {
250
+ router.push(moduleItem.route);
251
+ }}
252
+ className={`w-full rounded-[1.5rem] border px-4 py-4 text-left transition hover:-translate-y-0.5 ${
253
+ isDarkMode
254
+ ? "border-slate-800 bg-slate-950/70 hover:border-blue-700 hover:bg-slate-800"
255
+ : "border-slate-200 bg-slate-50 hover:border-blue-200 hover:bg-blue-50"
256
+ }`}
257
+ >
258
+ <span className="flex w-full items-center gap-4">
259
+ <span
260
+ className={`flex size-11 items-center justify-center rounded-2xl text-blue-600 shadow-sm ${
261
+ isDarkMode ? "bg-slate-900" : "bg-white"
262
+ }`}
263
+ >
264
+ {moduleItem.icon}
265
+ </span>
266
+ <span className="min-w-0 flex-1">
267
+ <span
268
+ className={`block truncate text-base font-semibold ${
269
+ isDarkMode ? "text-slate-50" : "text-slate-950"
270
+ }`}
271
+ >
272
+ {moduleItem.title}
273
+ </span>
274
+ <span
275
+ className={`mt-1 block text-sm ${
276
+ isDarkMode ? "text-slate-400" : "text-slate-500"
277
+ }`}
278
+ >
279
+ {moduleItem.description}
280
+ </span>
281
+ </span>
282
+ <span className="flex items-center gap-3">
283
+ {moduleItem.readOnly ? (
284
+ <SgBadge
285
+ value={t("security.common.readonly")}
286
+ severity="warning"
287
+ badgeStyle="soft"
288
+ rounded
289
+ />
290
+ ) : null}
291
+ <ArrowRight
292
+ size={18}
293
+ className={isDarkMode ? "text-slate-500" : "text-slate-400"}
294
+ />
295
+ </span>
296
+ </span>
297
+ </SgButton>
298
+ ))
299
+ ) : (
300
+ <div
301
+ className={`rounded-[1.5rem] border border-dashed px-4 py-5 text-sm ${
302
+ isDarkMode
303
+ ? "border-slate-700 bg-slate-950/70 text-slate-400"
304
+ : "border-slate-300 bg-slate-50 text-slate-500"
305
+ }`}
306
+ >
307
+ Nenhum modulo administrativo foi liberado para o usuario atual.
308
+ </div>
309
+ )}
310
+ </SgStack>
311
+ </SgCard>
312
+ </SgGrid>
313
+ </SgStack>
314
+ );
315
+ }
@@ -0,0 +1,55 @@
1
+ import { appEnvironment } from "@/config/environment";
2
+
3
+ function resolveApiBaseUrl() {
4
+ // Client-side: route through Next.js proxy to avoid CORS on error responses.
5
+ // The proxy at /api/[[...path]] forwards server-side to the backend.
6
+ if (globalThis.window !== undefined) {
7
+ return "/api";
8
+ }
9
+
10
+ return appEnvironment.hosts.apiOrigin;
11
+ }
12
+
13
+ function resolveBooleanFlag(value: string | undefined) {
14
+ return value?.trim().toLowerCase() === "true";
15
+ }
16
+
17
+ export const securityConfig = {
18
+ apiBaseUrl: resolveApiBaseUrl(),
19
+ pagination: {
20
+ defaultPageSize: 10,
21
+ pageSizeOptions: [10, 20, 50],
22
+ },
23
+ search: {
24
+ debounceMs: 1000,
25
+ minLength: 3,
26
+ },
27
+ storage: {
28
+ sessionKey: `${appEnvironment.persistence.scope}:security:session`,
29
+ pendingTwoFactorKey: `${appEnvironment.persistence.scope}:security:pending-2fa`,
30
+ },
31
+ features: {
32
+ selfEnterEnabled: resolveBooleanFlag(
33
+ process.env.NEXT_PUBLIC_SECURITY_SELF_ENTER_ENABLED
34
+ ),
35
+ },
36
+ routes: {
37
+ dashboard: "/",
38
+ profile: "/security/profile",
39
+ login: "/auth/login",
40
+ welcome: "/auth/welcome",
41
+ forgotPassword: "/auth/forgot-password",
42
+ forgotPasswordConfirm: "/auth/forgot-password/confirm",
43
+ forgotPasswordSuccess: "/auth/forgot-password/success",
44
+ verifyTwoFactor: "/auth/2fa/verify",
45
+ users: "/security/users",
46
+ companies: "/security/companies",
47
+ roles: "/security/roles",
48
+ twoFactor: "/security/2fa",
49
+ apiKeys: "/security/api-keys",
50
+ changePassword: "/users/change-password",
51
+ selfEnterPublic: "/auth/self-enter",
52
+ selfEnterApproval: "/self-enter/requests/to-approve",
53
+ selfEnterInvitations: "/self-enter/invitations",
54
+ },
55
+ } as const;
@@ -0,0 +1,7 @@
1
+ // seedgrid:managed
2
+
3
+ import { useAuthContext } from "../AuthProvider";
4
+
5
+ export function useAuth() {
6
+ return useAuthContext();
7
+ }
@@ -0,0 +1,13 @@
1
+ "use client";
2
+
3
+ import { useDebouncedValue } from "@/modules/core";
4
+
5
+ import { securityConfig } from "../config";
6
+ import { normalizeSearchTerm } from "../utils";
7
+
8
+ export function useDebouncedSearch(
9
+ value: string,
10
+ delayMs = securityConfig.search.debounceMs
11
+ ) {
12
+ return useDebouncedValue(value, delayMs, normalizeSearchTerm);
13
+ }