@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.
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +5 -0
- package/dist/client/permission-provider.d.ts +18 -0
- package/dist/client/permission-provider.d.ts.map +1 -0
- package/dist/client/permission-provider.js +57 -0
- package/dist/client/pick-list-field.d.ts +15 -0
- package/dist/client/pick-list-field.d.ts.map +1 -0
- package/dist/client/pick-list-field.js +30 -0
- package/dist/client/status-badge.d.ts +6 -0
- package/dist/client/status-badge.d.ts.map +1 -0
- package/dist/client/status-badge.js +6 -0
- package/dist/config.d.ts +34 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +4 -0
- package/dist/dtos.d.ts +88 -0
- package/dist/dtos.d.ts.map +1 -0
- package/dist/dtos.js +3 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/menu.d.ts +19 -0
- package/dist/menu.d.ts.map +1 -0
- package/dist/menu.js +41 -0
- package/dist/messages.d.ts +1858 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +990 -0
- package/dist/module.d.ts +3 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +37 -0
- package/dist/module.json +21 -0
- package/dist/paths.d.ts +1 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +3 -0
- package/dist/permissions.d.ts +14 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +39 -0
- package/dist/server/action.d.ts +3 -0
- package/dist/server/action.d.ts.map +1 -0
- package/dist/server/action.js +21 -0
- package/dist/server/create-security-server.d.ts +4 -0
- package/dist/server/create-security-server.d.ts.map +1 -0
- package/dist/server/create-security-server.js +166 -0
- package/dist/server/errors.d.ts +13 -0
- package/dist/server/errors.d.ts.map +1 -0
- package/dist/server/errors.js +26 -0
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +8 -0
- package/dist/server/problem-response.d.ts +3 -0
- package/dist/server/problem-response.d.ts.map +1 -0
- package/dist/server/problem-response.js +28 -0
- package/dist/server/route.d.ts +3 -0
- package/dist/server/route.d.ts.map +1 -0
- package/dist/server/route.js +22 -0
- package/dist/server/to-problem.d.ts +3 -0
- package/dist/server/to-problem.d.ts.map +1 -0
- package/dist/server/to-problem.js +22 -0
- package/dist/server-contract.d.ts +23 -0
- package/dist/server-contract.d.ts.map +1 -0
- package/dist/server-contract.js +4 -0
- package/dist/session-contract.d.ts +36 -0
- package/dist/session-contract.d.ts.map +1 -0
- package/dist/session-contract.js +8 -0
- package/dist/session-result.d.ts +22 -0
- package/dist/session-result.d.ts.map +1 -0
- package/dist/session-result.js +14 -0
- package/package.json +38 -3
- package/scaffold/README.md +97 -0
- package/scaffold/src/app/api/security/session/2fa/resend/route.ts +8 -0
- package/scaffold/src/app/api/security/session/2fa/verify/route.ts +8 -0
- package/scaffold/src/app/api/security/session/login/route.ts +7 -0
- package/scaffold/src/app/api/security/session/logout/route.ts +5 -0
- package/scaffold/src/app/api/security/session/refresh/route.ts +7 -0
- package/scaffold/src/app/api/security/users/[publicId]/avatar/route.ts +15 -0
- package/scaffold/src/app/auth/2fa/verify/page.tsx +286 -0
- package/scaffold/src/app/auth/forgot-password/confirm/page.tsx +96 -0
- package/scaffold/src/app/auth/forgot-password/page.tsx +165 -0
- package/scaffold/src/app/auth/forgot-password/success/page.tsx +31 -0
- package/scaffold/src/app/auth/login/page.tsx +386 -0
- package/scaffold/src/app/auth/welcome/page.tsx +90 -0
- package/scaffold/src/app/login/page.tsx +7 -0
- package/scaffold/src/app/page.tsx +12 -0
- package/scaffold/src/app/security/2fa/page.tsx +370 -0
- package/scaffold/src/app/security/companies/(create)/new/page.tsx +12 -0
- package/scaffold/src/app/security/companies/(edit)/[publicId]/page.tsx +18 -0
- package/scaffold/src/app/security/companies/(list)/page.tsx +298 -0
- package/scaffold/src/app/security/companies/editors/CompanyEditor.tsx +754 -0
- package/scaffold/src/app/security/profile/page.tsx +492 -0
- package/scaffold/src/app/security/roles/(create)/new/page.tsx +11 -0
- package/scaffold/src/app/security/roles/(edit)/[publicId]/page.tsx +17 -0
- package/scaffold/src/app/security/roles/(list)/page.tsx +269 -0
- package/scaffold/src/app/security/roles/editors/RoleEditor.tsx +328 -0
- package/scaffold/src/app/security/users/(create)/new/page.tsx +10 -0
- package/scaffold/src/app/security/users/(edit)/[publicId]/page.tsx +16 -0
- package/scaffold/src/app/security/users/(list)/page.tsx +289 -0
- package/scaffold/src/app/security/users/editors/UserEditor.tsx +478 -0
- package/scaffold/src/app/users/change-password/page.tsx +172 -0
- package/scaffold/src/modules/security/AuthProvider.tsx +405 -0
- package/scaffold/src/modules/security/components/AppShell.tsx.hbs +889 -0
- package/scaffold/src/modules/security/components/CompanyManagementGuard.tsx +59 -0
- package/scaffold/src/modules/security/components/PageFrame.tsx +105 -0
- package/scaffold/src/modules/security/components/RouteGuard.tsx +114 -0
- package/scaffold/src/modules/security/components/SecurityDashboard.tsx.hbs +315 -0
- package/scaffold/src/modules/security/config.ts +55 -0
- package/scaffold/src/modules/security/hooks/useAuth.ts +7 -0
- package/scaffold/src/modules/security/hooks/useDebouncedSearch.ts +13 -0
- package/scaffold/src/modules/security/hooks/usePermission.ts +35 -0
- package/scaffold/src/modules/security/host-access.ts +59 -0
- package/scaffold/src/modules/security/index.ts +44 -0
- package/scaffold/src/modules/security/navigation.ts +41 -0
- package/scaffold/src/modules/security/server/avatar-routes.ts +86 -0
- package/scaffold/src/modules/security/server/public-api-base-url.ts +26 -0
- package/scaffold/src/modules/security/server/session-routes.ts.hbs +322 -0
- package/scaffold/src/modules/security/server/session-server.ts.hbs +30 -0
- package/scaffold/src/modules/security/services/auth-service.ts +332 -0
- package/scaffold/src/modules/security/services/security-api.test.ts +178 -0
- package/scaffold/src/modules/security/services/security-api.ts +281 -0
- package/scaffold/src/modules/security/services/security-service.test.ts +71 -0
- package/scaffold/src/modules/security/services/security-service.ts +727 -0
- package/scaffold/src/modules/security/services/session-storage.ts +80 -0
- package/scaffold/src/modules/security/types.ts +295 -0
- package/scaffold/src/modules/security/useSecurityAppShellSections.tsx.hbs +180 -0
- package/scaffold/src/modules/security/utils.ts +194 -0
|
@@ -0,0 +1,889 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
SgAvatar,
|
|
8
|
+
SgButton,
|
|
9
|
+
SgCombobox,
|
|
10
|
+
SgMenu,
|
|
11
|
+
type SgMenuNode,
|
|
12
|
+
SgStack,
|
|
13
|
+
SgToggleSwitch,
|
|
14
|
+
} from "@seedgrid/fe-components";
|
|
15
|
+
import {
|
|
16
|
+
Bell,
|
|
17
|
+
ChevronDown,
|
|
18
|
+
KeyRound,
|
|
19
|
+
LogOut,
|
|
20
|
+
Mail,
|
|
21
|
+
Menu,
|
|
22
|
+
Moon,
|
|
23
|
+
Palette,
|
|
24
|
+
PencilLine,
|
|
25
|
+
ShieldCheck,
|
|
26
|
+
Sun,
|
|
27
|
+
UserCog,
|
|
28
|
+
} from "lucide-react";
|
|
29
|
+
import { usePathname, useRouter } from "next/navigation";
|
|
30
|
+
|
|
31
|
+
import { useAppShellSections } from "@/components/layout/app-shell-navigation";
|
|
32
|
+
import { appEnvironment } from "@/config/environment";
|
|
33
|
+
import { locales } from "@/config/i18n";
|
|
34
|
+
import { useI18n } from "@/i18n";
|
|
35
|
+
import { useSgTheme } from "@/theme";
|
|
36
|
+
|
|
37
|
+
import { securityConfig } from "../config";
|
|
38
|
+
import { useAuth } from "../hooks/useAuth";
|
|
39
|
+
import { usePermission } from "../hooks/usePermission";
|
|
40
|
+
import { securityService } from "../services/security-service";
|
|
41
|
+
|
|
42
|
+
type LocaleOption = "pt-BR" | "pt-PT" | "en" | "es" | "fr";
|
|
43
|
+
|
|
44
|
+
const languageOptions: Array<{
|
|
45
|
+
id: LocaleOption;
|
|
46
|
+
value: LocaleOption;
|
|
47
|
+
label: string;
|
|
48
|
+
flag: string;
|
|
49
|
+
}> = [
|
|
50
|
+
{
|
|
51
|
+
id: "pt-BR",
|
|
52
|
+
value: "pt-BR",
|
|
53
|
+
label: "Portugues (Brasil)",
|
|
54
|
+
flag: "\uD83C\uDDE7\uD83C\uDDF7",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "pt-PT",
|
|
58
|
+
value: "pt-PT",
|
|
59
|
+
label: "Portugues (Portugal)",
|
|
60
|
+
flag: "\uD83C\uDDF5\uD83C\uDDF9",
|
|
61
|
+
},
|
|
62
|
+
{ id: "en", value: "en", label: "English", flag: "\uD83C\uDDFA\uD83C\uDDF8" },
|
|
63
|
+
{ id: "es", value: "es", label: "Espanol", flag: "\uD83C\uDDEA\uD83C\uDDF8" },
|
|
64
|
+
{ id: "fr", value: "fr", label: "Francais", flag: "\uD83C\uDDEB\uD83C\uDDF7" },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
// `locales` e uma tupla `as const` (so os idiomas configurados no projeto);
|
|
68
|
+
// `option.id` percorre todos os LocaleOption. O check e de pertinencia de
|
|
69
|
+
// string, entao alargamos `locales` pra `readonly string[]` (senao o TS
|
|
70
|
+
// recusa comparar um LocaleOption fora da uniao configurada — TS2345).
|
|
71
|
+
const enabledLanguageOptions = languageOptions.filter((option) =>
|
|
72
|
+
(locales as readonly string[]).includes(option.id)
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
function LocaleFlag({ locale }: { locale: LocaleOption }) {
|
|
76
|
+
if (locale === "pt-BR") {
|
|
77
|
+
return (
|
|
78
|
+
<svg viewBox="0 0 20 14" className="h-3.5 w-5 rounded-[3px] shadow-sm" aria-hidden="true">
|
|
79
|
+
<rect width="20" height="14" rx="2" fill="#1f8f4d" />
|
|
80
|
+
<path d="M10 2.1 16.3 7 10 11.9 3.7 7Z" fill="#f3c942" />
|
|
81
|
+
<circle cx="10" cy="7" r="2.15" fill="#21468b" />
|
|
82
|
+
</svg>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (locale === "pt-PT") {
|
|
87
|
+
return (
|
|
88
|
+
<svg viewBox="0 0 20 14" className="h-3.5 w-5 rounded-[3px] shadow-sm" aria-hidden="true">
|
|
89
|
+
<rect width="8" height="14" fill="#1f8f4d" />
|
|
90
|
+
<rect x="8" width="12" height="14" fill="#d0392f" />
|
|
91
|
+
<circle cx="8" cy="7" r="2.2" fill="#f3c942" />
|
|
92
|
+
</svg>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (locale === "en") {
|
|
97
|
+
return (
|
|
98
|
+
<svg viewBox="0 0 20 14" className="h-3.5 w-5 rounded-[3px] shadow-sm" aria-hidden="true">
|
|
99
|
+
<rect width="20" height="14" rx="2" fill="#fff" />
|
|
100
|
+
<rect width="20" height="2" y="0" fill="#c53030" />
|
|
101
|
+
<rect width="20" height="2" y="4" fill="#c53030" />
|
|
102
|
+
<rect width="20" height="2" y="8" fill="#c53030" />
|
|
103
|
+
<rect width="20" height="2" y="12" fill="#c53030" />
|
|
104
|
+
<rect width="9" height="7.5" fill="#21468b" />
|
|
105
|
+
</svg>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (locale === "es") {
|
|
110
|
+
return (
|
|
111
|
+
<svg viewBox="0 0 20 14" className="h-3.5 w-5 rounded-[3px] shadow-sm" aria-hidden="true">
|
|
112
|
+
<rect width="20" height="14" rx="2" fill="#f0c419" />
|
|
113
|
+
<rect width="20" height="3" y="0" fill="#c53030" />
|
|
114
|
+
<rect width="20" height="3" y="11" fill="#c53030" />
|
|
115
|
+
</svg>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<svg viewBox="0 0 20 14" className="h-3.5 w-5 rounded-[3px] shadow-sm" aria-hidden="true">
|
|
121
|
+
<rect width="20" height="14" rx="2" fill="#1f4aa8" />
|
|
122
|
+
<rect width="20" height="4.6" y="4.7" fill="#fff" />
|
|
123
|
+
<rect width="20" height="4.6" y="9.4" fill="#c53030" />
|
|
124
|
+
</svg>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function toMenuNodes(sections: ReturnType<typeof useAppShellSections>): SgMenuNode[] {
|
|
129
|
+
return sections.flatMap((section) =>
|
|
130
|
+
section.items.map(function mapItem(item): SgMenuNode {
|
|
131
|
+
return {
|
|
132
|
+
id: item.id,
|
|
133
|
+
label: item.label,
|
|
134
|
+
url: item.href,
|
|
135
|
+
icon: item.icon,
|
|
136
|
+
badge: item.badge,
|
|
137
|
+
disabled: item.disabled,
|
|
138
|
+
children: item.children?.map(mapItem),
|
|
139
|
+
};
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const COLOR_PRESETS = [
|
|
145
|
+
{ label: "Verde", value: "#16a34a" },
|
|
146
|
+
{ label: "Azul", value: "#2563eb" },
|
|
147
|
+
{ label: "Roxo", value: "#7c3aed" },
|
|
148
|
+
{ label: "Vermelho", value: "#dc2626" },
|
|
149
|
+
{ label: "Laranja", value: "#ea580c" },
|
|
150
|
+
{ label: "Rosa", value: "#db2777" },
|
|
151
|
+
{ label: "Ciano", value: "#0891b2" },
|
|
152
|
+
{ label: "Ardósia", value: "#475569" },
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
export function AppShell({ children }: { children: ReactNode }) {
|
|
156
|
+
const router = useRouter();
|
|
157
|
+
const pathname = usePathname();
|
|
158
|
+
const { locale, setLocale, t } = useI18n();
|
|
159
|
+
const { currentMode, currentTheme, setMode, setTheme } = useSgTheme();
|
|
160
|
+
const auth = useAuth();
|
|
161
|
+
const sections = useAppShellSections();
|
|
162
|
+
const canChangePassword = usePermission("USER_CHANGE_PASSWORD");
|
|
163
|
+
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
|
|
164
|
+
const [isDesktopViewport, setIsDesktopViewport] = useState(false);
|
|
165
|
+
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
|
|
166
|
+
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
|
|
167
|
+
const [currentUserName, setCurrentUserName] = useState("");
|
|
168
|
+
const [currentUserAvatarStorageKey, setCurrentUserAvatarStorageKey] = useState("");
|
|
169
|
+
const [currentUserAvatarVersion, setCurrentUserAvatarVersion] = useState("");
|
|
170
|
+
const [currentUserAvatarObjectUrl, setCurrentUserAvatarObjectUrl] = useState("");
|
|
171
|
+
const [isTwoFactorEnabled, setIsTwoFactorEnabled] = useState(false);
|
|
172
|
+
const [profileRefreshToken, setProfileRefreshToken] = useState(0);
|
|
173
|
+
const userMenuRef = useRef<HTMLDivElement | null>(null);
|
|
174
|
+
const colorPickerRef = useRef<HTMLDivElement | null>(null);
|
|
175
|
+
const isDarkMode = currentMode === "dark";
|
|
176
|
+
const primaryColor = currentTheme.seed;
|
|
177
|
+
const selectedLanguage = enabledLanguageOptions.find(
|
|
178
|
+
(option) => option.id === locale
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const currentUserLogin = auth.session?.login?.trim() ?? "";
|
|
182
|
+
const currentUserPublicId = auth.session?.publicId?.trim() ?? "";
|
|
183
|
+
const resolvedCurrentUserName =
|
|
184
|
+
currentUserName.trim() || auth.session?.name?.trim() || "";
|
|
185
|
+
const currentUser = resolvedCurrentUserName || currentUserLogin || t("common.brand");
|
|
186
|
+
const currentUserSubtitle =
|
|
187
|
+
resolvedCurrentUserName && currentUserLogin
|
|
188
|
+
? currentUserLogin
|
|
189
|
+
: t("security.dashboard.user.subtitle");
|
|
190
|
+
const currentUserAvatarUrl =
|
|
191
|
+
currentUserAvatarObjectUrl ||
|
|
192
|
+
(currentUserPublicId
|
|
193
|
+
? securityService.buildUserAvatarUrl(
|
|
194
|
+
currentUserPublicId,
|
|
195
|
+
currentUserAvatarVersion || currentUserAvatarStorageKey
|
|
196
|
+
)
|
|
197
|
+
: undefined);
|
|
198
|
+
const menuNodes = useMemo(() => toMenuNodes(sections), [sections]);
|
|
199
|
+
const menuSelection = {
|
|
200
|
+
activeUrl: pathname ?? undefined,
|
|
201
|
+
};
|
|
202
|
+
const menuBrand = {
|
|
203
|
+
title: appEnvironment.branding.productName.trim() || "SeedGrid CRM",
|
|
204
|
+
imageSrc: appEnvironment.branding.masterLogoPath.trim() || undefined,
|
|
205
|
+
onClick: () => handleNavigate(securityConfig.routes.dashboard),
|
|
206
|
+
};
|
|
207
|
+
const menuSearch = {
|
|
208
|
+
enabled: true,
|
|
209
|
+
placeholder: t("security.dashboard.menu.search_placeholder"),
|
|
210
|
+
};
|
|
211
|
+
const menuUser = {
|
|
212
|
+
name: currentUser,
|
|
213
|
+
subtitle: currentUserSubtitle,
|
|
214
|
+
avatarSrc: currentUserAvatarUrl,
|
|
215
|
+
avatar: (
|
|
216
|
+
<SgAvatar
|
|
217
|
+
src={currentUserAvatarUrl}
|
|
218
|
+
alt={currentUser}
|
|
219
|
+
label={currentUser.slice(0, 2).toUpperCase()}
|
|
220
|
+
onImageError={handleCurrentUserAvatarError}
|
|
221
|
+
/>
|
|
222
|
+
),
|
|
223
|
+
};
|
|
224
|
+
const shellUserMenu: SgMenuNode[] = [
|
|
225
|
+
{
|
|
226
|
+
id: "edit-profile",
|
|
227
|
+
label: t("security.profile.edit_account"),
|
|
228
|
+
url: securityConfig.routes.profile,
|
|
229
|
+
icon: <PencilLine size={16} />,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: "change-password",
|
|
233
|
+
label: t("security.change_password.title"),
|
|
234
|
+
url: securityConfig.routes.changePassword,
|
|
235
|
+
icon: <KeyRound size={16} />,
|
|
236
|
+
disabled: !canChangePassword,
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
id: "toggle-two-factor",
|
|
240
|
+
label: t(
|
|
241
|
+
isTwoFactorEnabled
|
|
242
|
+
? "security.profile.disable_two_factor"
|
|
243
|
+
: "security.profile.enable_two_factor"
|
|
244
|
+
),
|
|
245
|
+
url: securityConfig.routes.twoFactor,
|
|
246
|
+
icon: <ShieldCheck size={16} />,
|
|
247
|
+
},
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
if (typeof window === "undefined") {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const mediaQuery = window.matchMedia("(min-width: 1024px)");
|
|
256
|
+
const syncViewport = () => setIsDesktopViewport(mediaQuery.matches);
|
|
257
|
+
|
|
258
|
+
syncViewport();
|
|
259
|
+
mediaQuery.addEventListener("change", syncViewport);
|
|
260
|
+
|
|
261
|
+
return () => {
|
|
262
|
+
mediaQuery.removeEventListener("change", syncViewport);
|
|
263
|
+
};
|
|
264
|
+
}, []);
|
|
265
|
+
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
setCurrentUserAvatarObjectUrl((current) => {
|
|
268
|
+
if (current.startsWith("blob:")) {
|
|
269
|
+
URL.revokeObjectURL(current);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return "";
|
|
273
|
+
});
|
|
274
|
+
}, [currentUserAvatarStorageKey, currentUserAvatarVersion, currentUserPublicId]);
|
|
275
|
+
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
return () => {
|
|
278
|
+
if (currentUserAvatarObjectUrl.startsWith("blob:")) {
|
|
279
|
+
URL.revokeObjectURL(currentUserAvatarObjectUrl);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}, [currentUserAvatarObjectUrl]);
|
|
283
|
+
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
let active = true;
|
|
286
|
+
|
|
287
|
+
void (async () => {
|
|
288
|
+
if (auth.session?.name?.trim()) {
|
|
289
|
+
setCurrentUserName(auth.session.name.trim());
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!currentUserPublicId && !currentUserLogin) {
|
|
293
|
+
setCurrentUserName("");
|
|
294
|
+
setCurrentUserAvatarStorageKey("");
|
|
295
|
+
setCurrentUserAvatarVersion("");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
let resolvedName = "";
|
|
301
|
+
|
|
302
|
+
if (currentUserPublicId) {
|
|
303
|
+
const user = await securityService.getUser(currentUserPublicId);
|
|
304
|
+
resolvedName = user?.name?.trim() ?? "";
|
|
305
|
+
if (active) {
|
|
306
|
+
const nextAvatarStorageKey = user?.avatarStorageKey?.trim() ?? "";
|
|
307
|
+
setCurrentUserAvatarStorageKey(nextAvatarStorageKey);
|
|
308
|
+
setCurrentUserAvatarVersion(nextAvatarStorageKey);
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
const response = await securityService.listUsersPage({
|
|
312
|
+
page: 0,
|
|
313
|
+
size: 20,
|
|
314
|
+
search: currentUserLogin,
|
|
315
|
+
sort: {
|
|
316
|
+
sortField: "email",
|
|
317
|
+
sortOrder: 1,
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const matchedUser =
|
|
322
|
+
response.items.find(
|
|
323
|
+
(user) => user.email?.toLowerCase() === currentUserLogin.toLowerCase()
|
|
324
|
+
) ??
|
|
325
|
+
response.items.find(
|
|
326
|
+
(user) => user.name?.toLowerCase() === currentUserLogin.toLowerCase()
|
|
327
|
+
) ??
|
|
328
|
+
null;
|
|
329
|
+
|
|
330
|
+
resolvedName = matchedUser?.name?.trim() ?? "";
|
|
331
|
+
if (active) {
|
|
332
|
+
const nextAvatarStorageKey =
|
|
333
|
+
matchedUser?.avatarStorageKey?.trim() ?? "";
|
|
334
|
+
setCurrentUserAvatarStorageKey(nextAvatarStorageKey);
|
|
335
|
+
setCurrentUserAvatarVersion(nextAvatarStorageKey);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (!active) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
setCurrentUserName(resolvedName);
|
|
344
|
+
} catch {
|
|
345
|
+
if (active) {
|
|
346
|
+
setCurrentUserName("");
|
|
347
|
+
setCurrentUserAvatarStorageKey("");
|
|
348
|
+
setCurrentUserAvatarVersion("");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
})();
|
|
352
|
+
|
|
353
|
+
return () => {
|
|
354
|
+
active = false;
|
|
355
|
+
};
|
|
356
|
+
}, [auth.session?.name, currentUserLogin, currentUserPublicId, profileRefreshToken]);
|
|
357
|
+
|
|
358
|
+
async function handleCurrentUserAvatarError() {
|
|
359
|
+
if (!currentUserPublicId || currentUserAvatarObjectUrl) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const fallbackObjectUrl = await securityService.fetchUserAvatarObjectUrl(
|
|
364
|
+
currentUserPublicId,
|
|
365
|
+
currentUserAvatarVersion || currentUserAvatarStorageKey
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
if (!fallbackObjectUrl) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
setCurrentUserAvatarObjectUrl((current) => {
|
|
373
|
+
if (current.startsWith("blob:")) {
|
|
374
|
+
URL.revokeObjectURL(current);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return fallbackObjectUrl;
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
useEffect(() => {
|
|
382
|
+
let active = true;
|
|
383
|
+
|
|
384
|
+
void (async () => {
|
|
385
|
+
try {
|
|
386
|
+
const status = await securityService.getTwoFactorStatus();
|
|
387
|
+
|
|
388
|
+
if (active) {
|
|
389
|
+
setIsTwoFactorEnabled(Boolean(status.enabled));
|
|
390
|
+
}
|
|
391
|
+
} catch {
|
|
392
|
+
if (active) {
|
|
393
|
+
setIsTwoFactorEnabled(false);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
})();
|
|
397
|
+
|
|
398
|
+
return () => {
|
|
399
|
+
active = false;
|
|
400
|
+
};
|
|
401
|
+
}, [pathname]);
|
|
402
|
+
|
|
403
|
+
useEffect(() => {
|
|
404
|
+
function handlePointerDown(event: MouseEvent) {
|
|
405
|
+
if (!userMenuRef.current?.contains(event.target as Node)) {
|
|
406
|
+
setIsUserMenuOpen(false);
|
|
407
|
+
}
|
|
408
|
+
if (!colorPickerRef.current?.contains(event.target as Node)) {
|
|
409
|
+
setIsColorPickerOpen(false);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
414
|
+
if (event.key === "Escape") {
|
|
415
|
+
setIsMobileSidebarOpen(false);
|
|
416
|
+
setIsUserMenuOpen(false);
|
|
417
|
+
setIsColorPickerOpen(false);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function handleProfileUpdated(event: Event) {
|
|
422
|
+
const detail =
|
|
423
|
+
event instanceof CustomEvent && event.detail && typeof event.detail === "object"
|
|
424
|
+
? (event.detail as {
|
|
425
|
+
publicId?: string;
|
|
426
|
+
avatarStorageKey?: string;
|
|
427
|
+
avatarVersion?: string;
|
|
428
|
+
name?: string;
|
|
429
|
+
})
|
|
430
|
+
: null;
|
|
431
|
+
|
|
432
|
+
if (detail?.publicId && detail.publicId !== currentUserPublicId) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (typeof detail?.name === "string") {
|
|
437
|
+
setCurrentUserName(detail.name.trim());
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (typeof detail?.avatarStorageKey === "string") {
|
|
441
|
+
setCurrentUserAvatarStorageKey(detail.avatarStorageKey.trim());
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (typeof detail?.avatarVersion === "string") {
|
|
445
|
+
setCurrentUserAvatarVersion(detail.avatarVersion.trim());
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
setProfileRefreshToken((current) => current + 1);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
document.addEventListener("mousedown", handlePointerDown);
|
|
452
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
453
|
+
window.addEventListener("seedgrid:profile-updated", handleProfileUpdated);
|
|
454
|
+
|
|
455
|
+
return () => {
|
|
456
|
+
document.removeEventListener("mousedown", handlePointerDown);
|
|
457
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
458
|
+
window.removeEventListener("seedgrid:profile-updated", handleProfileUpdated);
|
|
459
|
+
};
|
|
460
|
+
}, [currentUserPublicId]);
|
|
461
|
+
|
|
462
|
+
useEffect(() => {
|
|
463
|
+
setIsMobileSidebarOpen(false);
|
|
464
|
+
setIsUserMenuOpen(false);
|
|
465
|
+
setIsColorPickerOpen(false);
|
|
466
|
+
}, [pathname]);
|
|
467
|
+
|
|
468
|
+
async function handleSignOut() {
|
|
469
|
+
await auth.logout();
|
|
470
|
+
router.replace(securityConfig.routes.login);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function handleNavigate(href: string) {
|
|
474
|
+
setIsMobileSidebarOpen(false);
|
|
475
|
+
router.push(href);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function handleMenuNode(node: SgMenuNode) {
|
|
479
|
+
if (node.url) {
|
|
480
|
+
handleNavigate(node.url);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
node.onClick?.();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return (
|
|
487
|
+
<div
|
|
488
|
+
className={`min-h-screen ${
|
|
489
|
+
isDarkMode ? "bg-slate-950 text-slate-100" : "bg-[#f4f7fb] text-slate-900"
|
|
490
|
+
}`}
|
|
491
|
+
>
|
|
492
|
+
<div className="lg:grid lg:min-h-screen lg:grid-cols-[18.5rem_minmax(0,1fr)]">
|
|
493
|
+
<div
|
|
494
|
+
className={`fixed inset-0 z-40 bg-slate-950/40 backdrop-blur-sm transition lg:hidden ${
|
|
495
|
+
isMobileSidebarOpen ? "opacity-100" : "pointer-events-none opacity-0"
|
|
496
|
+
}`}
|
|
497
|
+
onClick={() => setIsMobileSidebarOpen(false)}
|
|
498
|
+
aria-hidden="true"
|
|
499
|
+
/>
|
|
500
|
+
|
|
501
|
+
{!isDesktopViewport ? (
|
|
502
|
+
<aside
|
|
503
|
+
className={`fixed inset-y-0 left-0 z-50 w-[18.5rem] max-w-[88vw] border-r shadow-2xl transition-transform lg:hidden ${
|
|
504
|
+
isDarkMode ? "border-slate-800 bg-slate-900" : "border-slate-200 bg-white"
|
|
505
|
+
} ${isMobileSidebarOpen ? "translate-x-0" : "-translate-x-full"}`}
|
|
506
|
+
>
|
|
507
|
+
<SgMenu
|
|
508
|
+
id={`app-shell-mobile-menu-${pathname ?? "root"}`}
|
|
509
|
+
menu={menuNodes}
|
|
510
|
+
selection={menuSelection}
|
|
511
|
+
brand={menuBrand}
|
|
512
|
+
user={menuUser}
|
|
513
|
+
userMenu={shellUserMenu}
|
|
514
|
+
menuStyle="inline"
|
|
515
|
+
menuVariantStyle="tiered"
|
|
516
|
+
mode="multiple"
|
|
517
|
+
closeOnNavigate
|
|
518
|
+
search={menuSearch}
|
|
519
|
+
onNavigate={handleMenuNode}
|
|
520
|
+
onAction={handleMenuNode}
|
|
521
|
+
onItemClick={handleMenuNode}
|
|
522
|
+
className="h-full overflow-y-auto border-0"
|
|
523
|
+
/>
|
|
524
|
+
</aside>
|
|
525
|
+
) : null}
|
|
526
|
+
|
|
527
|
+
{isDesktopViewport ? (
|
|
528
|
+
<aside
|
|
529
|
+
className={`hidden border-r lg:block ${
|
|
530
|
+
isDarkMode ? "border-slate-800 bg-slate-900" : "border-slate-200 bg-white"
|
|
531
|
+
}`}
|
|
532
|
+
>
|
|
533
|
+
<SgMenu
|
|
534
|
+
id={`app-shell-sidebar-menu-${pathname ?? "root"}`}
|
|
535
|
+
menu={menuNodes}
|
|
536
|
+
selection={menuSelection}
|
|
537
|
+
brand={menuBrand}
|
|
538
|
+
user={menuUser}
|
|
539
|
+
userMenu={shellUserMenu}
|
|
540
|
+
menuStyle="sidebar"
|
|
541
|
+
menuVariantStyle="tiered"
|
|
542
|
+
mode="multiple"
|
|
543
|
+
showCollapseButton
|
|
544
|
+
closeOnNavigate
|
|
545
|
+
search={menuSearch}
|
|
546
|
+
onNavigate={handleMenuNode}
|
|
547
|
+
onAction={handleMenuNode}
|
|
548
|
+
onItemClick={handleMenuNode}
|
|
549
|
+
className="h-full border-0"
|
|
550
|
+
/>
|
|
551
|
+
</aside>
|
|
552
|
+
) : null}
|
|
553
|
+
|
|
554
|
+
<div className="flex min-h-screen min-w-0 flex-col">
|
|
555
|
+
<header
|
|
556
|
+
className={`sticky top-0 z-30 border-b backdrop-blur ${
|
|
557
|
+
isDarkMode
|
|
558
|
+
? "border-slate-800 bg-slate-950/92"
|
|
559
|
+
: "border-slate-200 bg-white/92"
|
|
560
|
+
}`}
|
|
561
|
+
>
|
|
562
|
+
<div className="flex h-[4.5rem] items-center gap-3 px-4 sm:px-6 lg:px-8">
|
|
563
|
+
<SgButton
|
|
564
|
+
appearance="ghost"
|
|
565
|
+
severity="secondary"
|
|
566
|
+
iconOnly
|
|
567
|
+
leftIcon={<Menu size={20} />}
|
|
568
|
+
onClick={() => setIsMobileSidebarOpen(true)}
|
|
569
|
+
className={`border transition lg:hidden ${
|
|
570
|
+
isDarkMode
|
|
571
|
+
? "border-slate-700 bg-slate-900 text-slate-300 hover:border-slate-600 hover:text-slate-50"
|
|
572
|
+
: "border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:text-slate-950"
|
|
573
|
+
}`}
|
|
574
|
+
aria-label="Abrir menu"
|
|
575
|
+
/>
|
|
576
|
+
|
|
577
|
+
<div className="ml-auto flex items-center gap-2 sm:gap-3">
|
|
578
|
+
<div className="hidden sm:block">
|
|
579
|
+
<SgCombobox
|
|
580
|
+
id="shell-language"
|
|
581
|
+
width={188}
|
|
582
|
+
placeholder="Idioma"
|
|
583
|
+
source={enabledLanguageOptions}
|
|
584
|
+
value={selectedLanguage?.id ?? "pt-BR"}
|
|
585
|
+
mapItem={(option) => ({
|
|
586
|
+
id: option.id,
|
|
587
|
+
value: option.value,
|
|
588
|
+
label: option.label,
|
|
589
|
+
data: {
|
|
590
|
+
flag: option.flag,
|
|
591
|
+
locale: option.id,
|
|
592
|
+
label: option.label,
|
|
593
|
+
},
|
|
594
|
+
})}
|
|
595
|
+
onValueChange={(value) =>
|
|
596
|
+
setLocale((value as LocaleOption) ?? "pt-BR")
|
|
597
|
+
}
|
|
598
|
+
renderItem={(item) => {
|
|
599
|
+
const itemData = item.data as
|
|
600
|
+
| { flag?: string; label?: string; locale?: LocaleOption }
|
|
601
|
+
| undefined;
|
|
602
|
+
|
|
603
|
+
return (
|
|
604
|
+
<div className="flex items-center gap-3">
|
|
605
|
+
<LocaleFlag locale={itemData?.locale ?? "pt-BR"} />
|
|
606
|
+
<span>{itemData?.label ?? item.label}</span>
|
|
607
|
+
</div>
|
|
608
|
+
);
|
|
609
|
+
}}
|
|
610
|
+
renderValue={(item) => {
|
|
611
|
+
const itemData = item?.data as
|
|
612
|
+
| { flag?: string; label?: string; locale?: LocaleOption }
|
|
613
|
+
| undefined;
|
|
614
|
+
|
|
615
|
+
if (!item) {
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return (
|
|
620
|
+
<div className="flex items-center gap-2">
|
|
621
|
+
<LocaleFlag locale={itemData?.locale ?? "pt-BR"} />
|
|
622
|
+
<span className="truncate">{itemData?.label ?? item.label}</span>
|
|
623
|
+
</div>
|
|
624
|
+
);
|
|
625
|
+
}}
|
|
626
|
+
/>
|
|
627
|
+
</div>
|
|
628
|
+
|
|
629
|
+
<div
|
|
630
|
+
className={`rounded-full border px-1 py-1 ${
|
|
631
|
+
isDarkMode
|
|
632
|
+
? "border-slate-700 bg-slate-900"
|
|
633
|
+
: "border-slate-200 bg-slate-50"
|
|
634
|
+
}`}
|
|
635
|
+
>
|
|
636
|
+
<SgToggleSwitch
|
|
637
|
+
id="shell-theme"
|
|
638
|
+
checked={isDarkMode}
|
|
639
|
+
onChange={(checked) => setMode(checked ? "dark" : "light")}
|
|
640
|
+
onIcon={<Sun size={12} />}
|
|
641
|
+
offIcon={<Moon size={12} />}
|
|
642
|
+
/>
|
|
643
|
+
</div>
|
|
644
|
+
|
|
645
|
+
<div className="relative" ref={colorPickerRef}>
|
|
646
|
+
<SgButton
|
|
647
|
+
appearance="ghost"
|
|
648
|
+
severity="secondary"
|
|
649
|
+
iconOnly
|
|
650
|
+
leftIcon={<Palette size={18} />}
|
|
651
|
+
onClick={() => setIsColorPickerOpen((current) => !current)}
|
|
652
|
+
className={`size-11 flex items-center justify-center rounded-full border transition ${
|
|
653
|
+
isDarkMode
|
|
654
|
+
? "border-slate-700 bg-slate-900 text-slate-400 hover:border-slate-600 hover:text-slate-50"
|
|
655
|
+
: "border-slate-200 bg-white text-slate-500 hover:border-slate-300 hover:text-slate-950"
|
|
656
|
+
}`}
|
|
657
|
+
aria-label="Cor do tema"
|
|
658
|
+
/>
|
|
659
|
+
|
|
660
|
+
{isColorPickerOpen ? (
|
|
661
|
+
<div
|
|
662
|
+
className={`absolute right-0 top-full z-20 mt-3 w-52 overflow-hidden rounded-3xl border p-3 shadow-2xl ${
|
|
663
|
+
isDarkMode
|
|
664
|
+
? "border-slate-800 bg-slate-900 shadow-black/30"
|
|
665
|
+
: "border-slate-200 bg-white shadow-slate-900/10"
|
|
666
|
+
}`}
|
|
667
|
+
>
|
|
668
|
+
<p
|
|
669
|
+
className={`mb-2.5 px-1 text-xs font-semibold uppercase tracking-wide ${
|
|
670
|
+
isDarkMode ? "text-slate-400" : "text-slate-500"
|
|
671
|
+
}`}
|
|
672
|
+
>
|
|
673
|
+
Cor do tema
|
|
674
|
+
</p>
|
|
675
|
+
<div className="grid grid-cols-4 gap-2">
|
|
676
|
+
{COLOR_PRESETS.map((preset) => (
|
|
677
|
+
<SgButton
|
|
678
|
+
key={preset.value}
|
|
679
|
+
appearance="ghost"
|
|
680
|
+
severity="secondary"
|
|
681
|
+
title={preset.label}
|
|
682
|
+
onClick={() => {
|
|
683
|
+
setTheme({ seed: preset.value });
|
|
684
|
+
setIsColorPickerOpen(false);
|
|
685
|
+
}}
|
|
686
|
+
className="relative flex h-9 w-full items-center justify-center rounded-xl transition hover:scale-110"
|
|
687
|
+
style=\{{ backgroundColor: preset.value, border: "none" }}
|
|
688
|
+
aria-label={preset.label}
|
|
689
|
+
>
|
|
690
|
+
{primaryColor === preset.value ? (
|
|
691
|
+
<span className="block h-2.5 w-2.5 rounded-full bg-white shadow" />
|
|
692
|
+
) : null}
|
|
693
|
+
</SgButton>
|
|
694
|
+
))}
|
|
695
|
+
</div>
|
|
696
|
+
</div>
|
|
697
|
+
) : null}
|
|
698
|
+
</div>
|
|
699
|
+
|
|
700
|
+
<SgButton
|
|
701
|
+
appearance="ghost"
|
|
702
|
+
severity="secondary"
|
|
703
|
+
iconOnly
|
|
704
|
+
leftIcon={<Mail size={18} />}
|
|
705
|
+
className={`hidden size-11 items-center justify-center rounded-full border transition sm:flex ${
|
|
706
|
+
isDarkMode
|
|
707
|
+
? "border-slate-700 bg-slate-900 text-slate-400 hover:border-slate-600 hover:text-slate-50"
|
|
708
|
+
: "border-slate-200 bg-white text-slate-500 hover:border-slate-300 hover:text-slate-950"
|
|
709
|
+
}`}
|
|
710
|
+
aria-label="Mensagens"
|
|
711
|
+
/>
|
|
712
|
+
|
|
713
|
+
<SgButton
|
|
714
|
+
appearance="ghost"
|
|
715
|
+
severity="secondary"
|
|
716
|
+
iconOnly
|
|
717
|
+
leftIcon={<Bell size={18} />}
|
|
718
|
+
className={`hidden size-11 items-center justify-center rounded-full border transition sm:flex ${
|
|
719
|
+
isDarkMode
|
|
720
|
+
? "border-slate-700 bg-slate-900 text-slate-400 hover:border-slate-600 hover:text-slate-50"
|
|
721
|
+
: "border-slate-200 bg-white text-slate-500 hover:border-slate-300 hover:text-slate-950"
|
|
722
|
+
}`}
|
|
723
|
+
aria-label="Notificacoes"
|
|
724
|
+
/>
|
|
725
|
+
|
|
726
|
+
<div className="relative" ref={userMenuRef}>
|
|
727
|
+
<SgButton
|
|
728
|
+
appearance="ghost"
|
|
729
|
+
severity="secondary"
|
|
730
|
+
aria-haspopup="menu"
|
|
731
|
+
aria-expanded={isUserMenuOpen}
|
|
732
|
+
onClick={() => setIsUserMenuOpen((current) => !current)}
|
|
733
|
+
className={`rounded-full border pl-2 pr-3 py-1.5 transition ${
|
|
734
|
+
isDarkMode
|
|
735
|
+
? "border-slate-700 bg-slate-900 hover:border-slate-600"
|
|
736
|
+
: "border-slate-200 bg-white hover:border-slate-300"
|
|
737
|
+
}`}
|
|
738
|
+
>
|
|
739
|
+
<span className="flex items-center gap-3">
|
|
740
|
+
<SgAvatar
|
|
741
|
+
src={currentUserAvatarUrl}
|
|
742
|
+
alt={currentUser}
|
|
743
|
+
label={currentUser.slice(0, 2).toUpperCase()}
|
|
744
|
+
onImageError={handleCurrentUserAvatarError}
|
|
745
|
+
/>
|
|
746
|
+
<span className="hidden text-left sm:block">
|
|
747
|
+
<span
|
|
748
|
+
className={`block max-w-36 truncate text-sm font-semibold ${
|
|
749
|
+
isDarkMode ? "text-slate-50" : "text-slate-950"
|
|
750
|
+
}`}
|
|
751
|
+
>
|
|
752
|
+
{currentUser}
|
|
753
|
+
</span>
|
|
754
|
+
<span
|
|
755
|
+
className={`block max-w-36 truncate text-xs ${
|
|
756
|
+
isDarkMode ? "text-slate-400" : "text-slate-500"
|
|
757
|
+
}`}
|
|
758
|
+
>
|
|
759
|
+
{currentUserSubtitle}
|
|
760
|
+
</span>
|
|
761
|
+
</span>
|
|
762
|
+
<ChevronDown
|
|
763
|
+
size={16}
|
|
764
|
+
className={`hidden sm:block ${
|
|
765
|
+
isDarkMode ? "text-slate-500" : "text-slate-400"
|
|
766
|
+
}`}
|
|
767
|
+
/>
|
|
768
|
+
</span>
|
|
769
|
+
</SgButton>
|
|
770
|
+
|
|
771
|
+
{isUserMenuOpen ? (
|
|
772
|
+
<div
|
|
773
|
+
className={`absolute right-0 top-full z-20 mt-3 w-64 overflow-hidden rounded-3xl border p-2 shadow-2xl ${
|
|
774
|
+
isDarkMode
|
|
775
|
+
? "border-slate-800 bg-slate-900 shadow-black/30"
|
|
776
|
+
: "border-slate-200 bg-white shadow-slate-900/10"
|
|
777
|
+
}`}
|
|
778
|
+
>
|
|
779
|
+
<div
|
|
780
|
+
className={`rounded-2xl px-4 py-3 ${
|
|
781
|
+
isDarkMode ? "bg-slate-950/80" : "bg-slate-50"
|
|
782
|
+
}`}
|
|
783
|
+
>
|
|
784
|
+
<div
|
|
785
|
+
className={`text-sm font-semibold ${
|
|
786
|
+
isDarkMode ? "text-slate-50" : "text-slate-950"
|
|
787
|
+
}`}
|
|
788
|
+
>
|
|
789
|
+
{currentUser}
|
|
790
|
+
</div>
|
|
791
|
+
<div
|
|
792
|
+
className={`text-xs ${
|
|
793
|
+
isDarkMode ? "text-slate-400" : "text-slate-500"
|
|
794
|
+
}`}
|
|
795
|
+
>
|
|
796
|
+
{currentUserSubtitle}
|
|
797
|
+
</div>
|
|
798
|
+
</div>
|
|
799
|
+
|
|
800
|
+
<SgStack gap={4} className="mt-2">
|
|
801
|
+
<SgButton
|
|
802
|
+
appearance="ghost"
|
|
803
|
+
severity="secondary"
|
|
804
|
+
leftIcon={<UserCog size={16} />}
|
|
805
|
+
onClick={() => {
|
|
806
|
+
setIsUserMenuOpen(false);
|
|
807
|
+
router.push(securityConfig.routes.profile);
|
|
808
|
+
}}
|
|
809
|
+
className={`w-full justify-start rounded-2xl px-4 py-3 text-left text-sm transition ${
|
|
810
|
+
isDarkMode
|
|
811
|
+
? "text-slate-300 hover:bg-slate-800 hover:text-slate-50"
|
|
812
|
+
: "text-slate-600 hover:bg-slate-100 hover:text-slate-950"
|
|
813
|
+
}`}
|
|
814
|
+
>
|
|
815
|
+
{t("security.profile.edit_account")}
|
|
816
|
+
</SgButton>
|
|
817
|
+
|
|
818
|
+
{canChangePassword ? (
|
|
819
|
+
<SgButton
|
|
820
|
+
appearance="ghost"
|
|
821
|
+
severity="secondary"
|
|
822
|
+
leftIcon={<UserCog size={16} />}
|
|
823
|
+
onClick={() => {
|
|
824
|
+
setIsUserMenuOpen(false);
|
|
825
|
+
router.push(securityConfig.routes.changePassword);
|
|
826
|
+
}}
|
|
827
|
+
className={`w-full justify-start rounded-2xl px-4 py-3 text-left text-sm transition ${
|
|
828
|
+
isDarkMode
|
|
829
|
+
? "text-slate-300 hover:bg-slate-800 hover:text-slate-50"
|
|
830
|
+
: "text-slate-600 hover:bg-slate-100 hover:text-slate-950"
|
|
831
|
+
}`}
|
|
832
|
+
>
|
|
833
|
+
{t("security.change_password.title")}
|
|
834
|
+
</SgButton>
|
|
835
|
+
) : null}
|
|
836
|
+
|
|
837
|
+
<SgButton
|
|
838
|
+
appearance="ghost"
|
|
839
|
+
severity="secondary"
|
|
840
|
+
leftIcon={<ShieldCheck size={16} />}
|
|
841
|
+
onClick={() => {
|
|
842
|
+
setIsUserMenuOpen(false);
|
|
843
|
+
router.push(securityConfig.routes.twoFactor);
|
|
844
|
+
}}
|
|
845
|
+
className={`w-full justify-start rounded-2xl px-4 py-3 text-left text-sm transition ${
|
|
846
|
+
isDarkMode
|
|
847
|
+
? "text-slate-300 hover:bg-slate-800 hover:text-slate-50"
|
|
848
|
+
: "text-slate-600 hover:bg-slate-100 hover:text-slate-950"
|
|
849
|
+
}`}
|
|
850
|
+
>
|
|
851
|
+
{t(
|
|
852
|
+
isTwoFactorEnabled
|
|
853
|
+
? "security.profile.disable_two_factor"
|
|
854
|
+
: "security.profile.enable_two_factor"
|
|
855
|
+
)}
|
|
856
|
+
</SgButton>
|
|
857
|
+
|
|
858
|
+
<SgButton
|
|
859
|
+
appearance="ghost"
|
|
860
|
+
severity="danger"
|
|
861
|
+
leftIcon={<LogOut size={16} />}
|
|
862
|
+
onClick={() => {
|
|
863
|
+
setIsUserMenuOpen(false);
|
|
864
|
+
void handleSignOut();
|
|
865
|
+
}}
|
|
866
|
+
className={`w-full justify-start rounded-2xl px-4 py-3 text-left text-sm text-rose-600 transition ${
|
|
867
|
+
isDarkMode ? "hover:bg-rose-950/40" : "hover:bg-rose-50"
|
|
868
|
+
}`}
|
|
869
|
+
>
|
|
870
|
+
{t("security.login.signout")}
|
|
871
|
+
</SgButton>
|
|
872
|
+
</SgStack>
|
|
873
|
+
</div>
|
|
874
|
+
) : null}
|
|
875
|
+
</div>
|
|
876
|
+
</div>
|
|
877
|
+
</div>
|
|
878
|
+
</header>
|
|
879
|
+
|
|
880
|
+
<main className="min-h-0 flex-1 overflow-x-hidden">
|
|
881
|
+
<div className="mx-auto flex min-h-full w-full max-w-[1600px] flex-col px-4 py-4 sm:px-6 sm:py-6 lg:px-8 lg:py-8">
|
|
882
|
+
<div className="min-h-0 flex-1">{children}</div>
|
|
883
|
+
</div>
|
|
884
|
+
</main>
|
|
885
|
+
</div>
|
|
886
|
+
</div>
|
|
887
|
+
</div>
|
|
888
|
+
);
|
|
889
|
+
}
|