@hed-hog/core 0.0.361 → 0.0.362
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/access-log/access-log.controller.d.ts +1 -1
- package/dist/access-log/access-log.service.d.ts +1 -1
- package/dist/access-log/access-log.service.d.ts.map +1 -1
- package/dist/access-log/access-log.service.js +43 -1
- package/dist/access-log/access-log.service.js.map +1 -1
- package/dist/auth/auth.controller.d.ts +1 -0
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +6 -2
- package/dist/auth/auth.controller.js.map +1 -1
- package/dist/auth/auth.service.d.ts +1 -0
- package/dist/auth/auth.service.d.ts.map +1 -1
- package/dist/auth/auth.service.js +15 -5
- package/dist/auth/auth.service.js.map +1 -1
- package/dist/auth/guards/auth.guard.d.ts +1 -0
- package/dist/auth/guards/auth.guard.d.ts.map +1 -1
- package/dist/auth/guards/auth.guard.js +11 -4
- package/dist/auth/guards/auth.guard.js.map +1 -1
- package/dist/file/file.module.d.ts.map +1 -1
- package/dist/file/file.module.js +3 -2
- package/dist/file/file.module.js.map +1 -1
- package/dist/file/file.service.d.ts +71 -1
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +180 -32
- package/dist/file/file.service.js.map +1 -1
- package/dist/file/image-optimization.service.d.ts +20 -0
- package/dist/file/image-optimization.service.d.ts.map +1 -0
- package/dist/file/image-optimization.service.js +126 -0
- package/dist/file/image-optimization.service.js.map +1 -0
- package/dist/file/provider/local.provider.d.ts +1 -1
- package/dist/file/provider/local.provider.d.ts.map +1 -1
- package/dist/file/provider/local.provider.js +2 -9
- package/dist/file/provider/local.provider.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/tools/core-users.mcp-tools.d.ts +1 -1
- package/dist/mcp/tools/core-users.mcp-tools.d.ts.map +1 -1
- package/dist/mcp/tools/core-users.mcp-tools.js +2 -2
- package/dist/mcp/tools/core-users.mcp-tools.js.map +1 -1
- package/dist/profile/profile.service.d.ts.map +1 -1
- package/dist/profile/profile.service.js +44 -23
- package/dist/profile/profile.service.js.map +1 -1
- package/dist/session/session.service.d.ts.map +1 -1
- package/dist/session/session.service.js +7 -2
- package/dist/session/session.service.js.map +1 -1
- package/dist/token/token.service.d.ts +1 -0
- package/dist/token/token.service.d.ts.map +1 -1
- package/dist/token/token.service.js +23 -6
- package/dist/token/token.service.js.map +1 -1
- package/dist/user/user.controller.d.ts +1 -1
- package/dist/user/user.controller.d.ts.map +1 -1
- package/dist/user/user.controller.js +4 -3
- package/dist/user/user.controller.js.map +1 -1
- package/dist/user/user.service.d.ts +1 -1
- package/dist/user/user.service.d.ts.map +1 -1
- package/dist/user/user.service.js +6 -6
- package/dist/user/user.service.js.map +1 -1
- package/hedhog/data/route.yaml +2 -0
- package/hedhog/data/setting_group.yaml +117 -0
- package/hedhog/data/setting_subgroup.yaml +8 -0
- package/hedhog/frontend/app/access-log/page.tsx.ejs +170 -11
- package/hedhog/frontend/app/account/components/remove-mfa-dialog.tsx.ejs +1 -1
- package/hedhog/frontend/app/account/components/verify-before-add-dialog.tsx.ejs +1 -1
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +112 -5
- package/hedhog/frontend/app/users/page.tsx.ejs +7 -7
- package/hedhog/frontend/app/users/permissions.tsx.ejs +137 -83
- package/hedhog/frontend/app/users/user-access-sheet.tsx.ejs +28 -26
- package/hedhog/frontend/app/users/user-sheet.tsx.ejs +207 -75
- package/hedhog/frontend/messages/en.json +9 -2
- package/hedhog/frontend/messages/pt.json +9 -2
- package/package.json +11 -6
- package/src/access-log/access-log.service.ts +19 -1
- package/src/auth/auth.controller.ts +5 -0
- package/src/auth/auth.service.ts +14 -4
- package/src/auth/guards/auth.guard.ts +10 -3
- package/src/file/file.module.ts +3 -2
- package/src/file/file.service.ts +361 -47
- package/src/file/image-optimization.service.ts +123 -0
- package/src/file/provider/local.provider.ts +2 -9
- package/src/index.ts +1 -0
- package/src/mcp/tools/core-users.mcp-tools.ts +267 -267
- package/src/profile/profile.service.ts +40 -22
- package/src/session/session.service.ts +10 -2
- package/src/token/token.service.ts +26 -6
- package/src/user/user.controller.ts +5 -2
- package/src/user/user.service.ts +11 -8
- package/dist/menu/menu.service.spec.d.ts +0 -1
- package/dist/menu/menu.service.spec.d.ts.map +0 -1
- package/dist/menu/menu.service.spec.js +0 -208
- package/dist/menu/menu.service.spec.js.map +0 -1
- package/dist/route/route.service.spec.d.ts +0 -1
- package/dist/route/route.service.spec.d.ts.map +0 -1
- package/dist/route/route.service.spec.js +0 -262
- package/dist/route/route.service.spec.js.map +0 -1
- package/dist/screen/screen.service.spec.d.ts +0 -1
- package/dist/screen/screen.service.spec.d.ts.map +0 -1
- package/dist/screen/screen.service.spec.js +0 -259
- package/dist/screen/screen.service.spec.js.map +0 -1
- package/dist/user/user.service.spec.d.ts +0 -1
- package/dist/user/user.service.spec.d.ts.map +0 -1
- package/dist/user/user.service.spec.js +0 -261
- package/dist/user/user.service.spec.js.map +0 -1
|
@@ -29,6 +29,12 @@ import {
|
|
|
29
29
|
} from '@/components/ui/form';
|
|
30
30
|
import { FormActions } from '@/components/ui/form-actions';
|
|
31
31
|
import { Input } from '@/components/ui/input';
|
|
32
|
+
import {
|
|
33
|
+
formatIntegrationProviderName,
|
|
34
|
+
IntegrationLogo,
|
|
35
|
+
normalizeIntegrationProvider,
|
|
36
|
+
resolveOAuthProvider,
|
|
37
|
+
} from '@/components/ui/integration-logo';
|
|
32
38
|
import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
|
|
33
39
|
import {
|
|
34
40
|
Sheet,
|
|
@@ -36,6 +42,7 @@ import {
|
|
|
36
42
|
SheetHeader,
|
|
37
43
|
SheetTitle,
|
|
38
44
|
} from '@/components/ui/sheet';
|
|
45
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
39
46
|
import { useFormDraft } from '@/hooks/use-form-draft';
|
|
40
47
|
import { formatDateTime } from '@/lib/format-date';
|
|
41
48
|
import { getPhotoUrl } from '@/lib/get-photo-url';
|
|
@@ -44,7 +51,6 @@ import { User, UserMfa } from '@hed-hog/api-types';
|
|
|
44
51
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
45
52
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
46
53
|
import { Avatar, AvatarFallback, AvatarImage } from '@radix-ui/react-avatar';
|
|
47
|
-
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
48
54
|
import { formatDistanceToNow } from 'date-fns';
|
|
49
55
|
import { enUS, ptBR } from 'date-fns/locale';
|
|
50
56
|
import {
|
|
@@ -103,10 +109,42 @@ type UserEditDraftPayload = {
|
|
|
103
109
|
|
|
104
110
|
const USER_CREATE_DRAFT_STORAGE_KEY = 'core-user-create-draft';
|
|
105
111
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
112
|
+
const OAUTH_PROVIDER_TOGGLE_MAP: Array<{
|
|
113
|
+
settingSlug: string;
|
|
114
|
+
provider: {
|
|
115
|
+
id: string;
|
|
116
|
+
slug: string;
|
|
117
|
+
name: string;
|
|
118
|
+
};
|
|
119
|
+
}> = [
|
|
120
|
+
{
|
|
121
|
+
settingSlug: 'oauth-facebook-enabled',
|
|
122
|
+
provider: { id: 'facebook', slug: 'facebook-oauth', name: 'Facebook' },
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
settingSlug: 'oauth-github-enabled',
|
|
126
|
+
provider: { id: 'github', slug: 'github-oauth', name: 'GitHub' },
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
settingSlug: 'oauth-google-enabled',
|
|
130
|
+
provider: { id: 'google', slug: 'google-oauth', name: 'Google' },
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
settingSlug: 'oauth-microsoft-enabled',
|
|
134
|
+
provider: {
|
|
135
|
+
id: 'microsoft',
|
|
136
|
+
slug: 'microsoft-oauth',
|
|
137
|
+
name: 'Microsoft',
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
settingSlug: 'oauth-microsoft-entra-id-enabled',
|
|
142
|
+
provider: {
|
|
143
|
+
id: 'microsoft-entra-id',
|
|
144
|
+
slug: 'microsoft-entra-id-oauth',
|
|
145
|
+
name: 'Microsoft Entra ID',
|
|
146
|
+
},
|
|
147
|
+
},
|
|
110
148
|
];
|
|
111
149
|
|
|
112
150
|
function generateRandomPassword(length = 16) {
|
|
@@ -368,6 +406,13 @@ function UserEditSheet({
|
|
|
368
406
|
const userActivityT = useTranslations('core.UserActivity');
|
|
369
407
|
const { request, currentLocaleCode, getSettingValue } = useApp();
|
|
370
408
|
const minPasswordLength = Number(getSettingValue('password-min-length')) || 6;
|
|
409
|
+
const enabledOAuthProviders = useMemo(
|
|
410
|
+
() =>
|
|
411
|
+
OAUTH_PROVIDER_TOGGLE_MAP.filter(
|
|
412
|
+
({ settingSlug }) => getSettingValue(settingSlug) === true
|
|
413
|
+
).map(({ provider }) => provider),
|
|
414
|
+
[getSettingValue]
|
|
415
|
+
);
|
|
371
416
|
|
|
372
417
|
const [activeTab, setActiveTab] = useState('overview');
|
|
373
418
|
const prevUserIdRef = useRef<number | null>(null);
|
|
@@ -419,6 +464,41 @@ function UserEditSheet({
|
|
|
419
464
|
defaultValues: { password: '' },
|
|
420
465
|
});
|
|
421
466
|
|
|
467
|
+
const passwordCredential = useMemo(() => {
|
|
468
|
+
const credentials = (editingUser as any)?.user_credential;
|
|
469
|
+
if (!Array.isArray(credentials) || credentials.length === 0) return null;
|
|
470
|
+
|
|
471
|
+
return (
|
|
472
|
+
credentials.find(
|
|
473
|
+
(credential) =>
|
|
474
|
+
String(credential?.type ?? '').toLowerCase() === 'password'
|
|
475
|
+
) ?? credentials[0]
|
|
476
|
+
);
|
|
477
|
+
}, [editingUser]);
|
|
478
|
+
|
|
479
|
+
const passwordLastChangedStatus = useMemo(() => {
|
|
480
|
+
const changedAt =
|
|
481
|
+
passwordCredential?.updated_at ?? passwordCredential?.created_at ?? null;
|
|
482
|
+
if (!changedAt) return null;
|
|
483
|
+
|
|
484
|
+
const changedDate = new Date(changedAt);
|
|
485
|
+
if (Number.isNaN(changedDate.getTime())) return null;
|
|
486
|
+
|
|
487
|
+
const locale = currentLocaleCode === 'en' ? enUS : ptBR;
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
relativeLabel: formatDistanceToNow(changedDate, {
|
|
491
|
+
addSuffix: true,
|
|
492
|
+
locale,
|
|
493
|
+
}),
|
|
494
|
+
absoluteLabel: formatDateTime(
|
|
495
|
+
changedDate,
|
|
496
|
+
getSettingValue,
|
|
497
|
+
currentLocaleCode
|
|
498
|
+
),
|
|
499
|
+
};
|
|
500
|
+
}, [currentLocaleCode, getSettingValue, passwordCredential]);
|
|
501
|
+
|
|
422
502
|
const watchedEditValues = useWatch({ control: editForm.control });
|
|
423
503
|
|
|
424
504
|
const editDraftStorageKey = `core-user-edit-draft-${userId}`;
|
|
@@ -655,36 +735,38 @@ function UserEditSheet({
|
|
|
655
735
|
</SheetHeader>
|
|
656
736
|
|
|
657
737
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
|
658
|
-
<
|
|
659
|
-
<
|
|
660
|
-
<
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
<
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
<
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
<
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
<
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
<
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
<
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
738
|
+
<div className="sticky top-0 z-20 bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/80">
|
|
739
|
+
<TabsList className="flex-wrap overflow-visible px-2 py-1.5">
|
|
740
|
+
<TabsTrigger value="overview">
|
|
741
|
+
<UserCircle className="h-4 w-4" />
|
|
742
|
+
<span>{t('tabOverview')}</span>
|
|
743
|
+
</TabsTrigger>
|
|
744
|
+
<TabsTrigger value="edit">
|
|
745
|
+
<Save className="h-4 w-4" />
|
|
746
|
+
<span>{t('tabEdit')}</span>
|
|
747
|
+
</TabsTrigger>
|
|
748
|
+
<TabsTrigger value="credentials">
|
|
749
|
+
<Key className="h-4 w-4" />
|
|
750
|
+
<span>{t('tabCredentials')}</span>
|
|
751
|
+
</TabsTrigger>
|
|
752
|
+
<TabsTrigger value="identifiers">
|
|
753
|
+
<Fingerprint className="h-4 w-4" />
|
|
754
|
+
<span>{t('tabIdentifiers')}</span>
|
|
755
|
+
</TabsTrigger>
|
|
756
|
+
<TabsTrigger value="permissions">
|
|
757
|
+
<ShieldCheck className="h-4 w-4" />
|
|
758
|
+
<span>{t('tabPermissions')}</span>
|
|
759
|
+
</TabsTrigger>
|
|
760
|
+
<TabsTrigger value="mfa">
|
|
761
|
+
<Shield className="h-4 w-4" />
|
|
762
|
+
<span>{t('tabMfa')}</span>
|
|
763
|
+
</TabsTrigger>
|
|
764
|
+
<TabsTrigger value="sessions">
|
|
765
|
+
<Monitor className="h-4 w-4" />
|
|
766
|
+
<span>{t('tabSessions')}</span>
|
|
767
|
+
</TabsTrigger>
|
|
768
|
+
</TabsList>
|
|
769
|
+
</div>
|
|
688
770
|
|
|
689
771
|
{/* Overview Tab */}
|
|
690
772
|
<TabsContent value="overview" className="space-y-4 mt-4 p-4 pt-0">
|
|
@@ -1044,10 +1126,28 @@ function UserEditSheet({
|
|
|
1044
1126
|
<h4 className="text-sm font-semibold">
|
|
1045
1127
|
{t('passwordResetTitle')}
|
|
1046
1128
|
</h4>
|
|
1047
|
-
<p className="text-xs text-muted-foreground
|
|
1129
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
1048
1130
|
{t('passwordResetDescription')}
|
|
1049
1131
|
</p>
|
|
1050
1132
|
</div>
|
|
1133
|
+
{passwordLastChangedStatus ? (
|
|
1134
|
+
<div className="rounded-md border border-border/60 bg-muted/30 px-3 py-2">
|
|
1135
|
+
<p className="text-xs font-medium text-foreground">
|
|
1136
|
+
{t('passwordLastChangedHumanized', {
|
|
1137
|
+
relativeLabel: passwordLastChangedStatus.relativeLabel,
|
|
1138
|
+
})}
|
|
1139
|
+
</p>
|
|
1140
|
+
<p className="mt-1 text-[11px] text-muted-foreground">
|
|
1141
|
+
{t('passwordLastChangedAbsolute', {
|
|
1142
|
+
absoluteLabel: passwordLastChangedStatus.absoluteLabel,
|
|
1143
|
+
})}
|
|
1144
|
+
</p>
|
|
1145
|
+
</div>
|
|
1146
|
+
) : (
|
|
1147
|
+
<p className="text-xs text-muted-foreground">
|
|
1148
|
+
{t('passwordLastChangedUnavailable')}
|
|
1149
|
+
</p>
|
|
1150
|
+
)}
|
|
1051
1151
|
<p className="text-xs text-muted-foreground">
|
|
1052
1152
|
{t('passwordResetNotice')}
|
|
1053
1153
|
</p>
|
|
@@ -1057,7 +1157,7 @@ function UserEditSheet({
|
|
|
1057
1157
|
onClick={openResetPasswordDialog}
|
|
1058
1158
|
className="w-full sm:w-fit"
|
|
1059
1159
|
>
|
|
1060
|
-
<RefreshCcw className="h-4 w-4
|
|
1160
|
+
<RefreshCcw className="mr-2 h-4 w-4" />
|
|
1061
1161
|
{t('buttonResetPassword')}
|
|
1062
1162
|
</Button>
|
|
1063
1163
|
</div>
|
|
@@ -1068,30 +1168,44 @@ function UserEditSheet({
|
|
|
1068
1168
|
{t('connectedAccountsTitle')}
|
|
1069
1169
|
</h4>
|
|
1070
1170
|
<div className="space-y-2">
|
|
1071
|
-
{
|
|
1171
|
+
{enabledOAuthProviders.map((provider) => {
|
|
1172
|
+
const normalizedProviderId = resolveOAuthProvider(
|
|
1173
|
+
provider.id
|
|
1174
|
+
);
|
|
1072
1175
|
const isConnected = editingUser.user_account?.some(
|
|
1073
1176
|
(account) =>
|
|
1074
|
-
|
|
1075
|
-
|
|
1177
|
+
resolveOAuthProvider(
|
|
1178
|
+
normalizeIntegrationProvider(
|
|
1179
|
+
String(account.provider ?? '')
|
|
1180
|
+
)
|
|
1181
|
+
) === normalizedProviderId
|
|
1076
1182
|
);
|
|
1183
|
+
|
|
1077
1184
|
return (
|
|
1078
1185
|
<div
|
|
1079
|
-
key={provider.
|
|
1186
|
+
key={provider.slug}
|
|
1080
1187
|
className={`flex items-center justify-between rounded-lg border p-3 ${isConnected ? '' : 'border-dashed'}`}
|
|
1081
1188
|
>
|
|
1082
1189
|
<div className="flex items-center gap-3">
|
|
1083
1190
|
<div className="rounded-md bg-slate-50 p-2">
|
|
1084
|
-
<
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1191
|
+
<IntegrationLogo
|
|
1192
|
+
provider={provider.slug}
|
|
1193
|
+
size={20}
|
|
1194
|
+
decorative={false}
|
|
1195
|
+
title={provider.name}
|
|
1196
|
+
className={
|
|
1197
|
+
isConnected ? 'opacity-100' : 'opacity-50'
|
|
1198
|
+
}
|
|
1088
1199
|
/>
|
|
1089
1200
|
</div>
|
|
1090
1201
|
<div>
|
|
1091
1202
|
<p
|
|
1092
1203
|
className={`text-sm font-medium ${isConnected ? '' : 'text-muted-foreground'}`}
|
|
1093
1204
|
>
|
|
1094
|
-
{
|
|
1205
|
+
{formatIntegrationProviderName(
|
|
1206
|
+
provider.slug,
|
|
1207
|
+
provider.name
|
|
1208
|
+
)}
|
|
1095
1209
|
</p>
|
|
1096
1210
|
<p className="text-xs text-muted-foreground">
|
|
1097
1211
|
{isConnected ? t('connected') : t('notConnected')}
|
|
@@ -1101,28 +1215,39 @@ function UserEditSheet({
|
|
|
1101
1215
|
</div>
|
|
1102
1216
|
);
|
|
1103
1217
|
})}
|
|
1218
|
+
{enabledOAuthProviders.length === 0 && (
|
|
1219
|
+
<div className="rounded-lg border border-dashed p-3 text-sm text-muted-foreground">
|
|
1220
|
+
{t('noConnectedAccounts')}
|
|
1221
|
+
</div>
|
|
1222
|
+
)}
|
|
1104
1223
|
</div>
|
|
1105
1224
|
</div>
|
|
1106
1225
|
|
|
1107
|
-
<
|
|
1226
|
+
<Sheet
|
|
1108
1227
|
open={isResetPasswordDialogOpen}
|
|
1109
1228
|
onOpenChange={(open) => {
|
|
1110
1229
|
setIsResetPasswordDialogOpen(open);
|
|
1111
1230
|
if (!open) setResetFormError(null);
|
|
1112
1231
|
}}
|
|
1113
1232
|
>
|
|
1114
|
-
<
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1233
|
+
<ResizableSheetContent
|
|
1234
|
+
sheetId="core-users-reset-password-sheet"
|
|
1235
|
+
defaultWidth={520}
|
|
1236
|
+
minWidth={420}
|
|
1237
|
+
maxWidth={760}
|
|
1238
|
+
className="w-full sm:max-w-lg overflow-y-auto gap-0"
|
|
1239
|
+
>
|
|
1240
|
+
<SheetHeader>
|
|
1241
|
+
<SheetTitle>{t('passwordResetDialogTitle')}</SheetTitle>
|
|
1242
|
+
<SheetDescription>
|
|
1120
1243
|
{t('passwordResetDialogDescription')}
|
|
1121
|
-
</
|
|
1122
|
-
</
|
|
1244
|
+
</SheetDescription>
|
|
1245
|
+
</SheetHeader>
|
|
1246
|
+
|
|
1247
|
+
<div className="w-full border-t" />
|
|
1123
1248
|
|
|
1124
1249
|
<Form {...resetPasswordForm}>
|
|
1125
|
-
<form className="
|
|
1250
|
+
<form className="flex flex-col gap-4 p-4">
|
|
1126
1251
|
<FormField
|
|
1127
1252
|
control={resetPasswordForm.control}
|
|
1128
1253
|
name="password"
|
|
@@ -1159,6 +1284,7 @@ function UserEditSheet({
|
|
|
1159
1284
|
<Button
|
|
1160
1285
|
type="button"
|
|
1161
1286
|
variant="outline"
|
|
1287
|
+
className="w-full sm:w-fit"
|
|
1162
1288
|
onClick={() => {
|
|
1163
1289
|
resetPasswordForm.setValue(
|
|
1164
1290
|
'password',
|
|
@@ -1186,27 +1312,33 @@ function UserEditSheet({
|
|
|
1186
1312
|
<AlertDescription>{resetFormError}</AlertDescription>
|
|
1187
1313
|
</Alert>
|
|
1188
1314
|
)}
|
|
1315
|
+
|
|
1316
|
+
<div className="mt-auto flex flex-col gap-2 border-t pt-4 sm:flex-row sm:justify-end">
|
|
1317
|
+
<Button
|
|
1318
|
+
type="button"
|
|
1319
|
+
variant="outline"
|
|
1320
|
+
onClick={() => setIsResetPasswordDialogOpen(false)}
|
|
1321
|
+
>
|
|
1322
|
+
{t('cancel')}
|
|
1323
|
+
</Button>
|
|
1324
|
+
<Button
|
|
1325
|
+
type="button"
|
|
1326
|
+
disabled={isResettingPassword}
|
|
1327
|
+
onClick={async () => {
|
|
1328
|
+
const valid = await resetPasswordForm.trigger();
|
|
1329
|
+
if (!valid) return;
|
|
1330
|
+
await onResetPasswordSubmit();
|
|
1331
|
+
}}
|
|
1332
|
+
>
|
|
1333
|
+
{isResettingPassword
|
|
1334
|
+
? t('passwordResetSubmitting')
|
|
1335
|
+
: t('passwordResetConfirm')}
|
|
1336
|
+
</Button>
|
|
1337
|
+
</div>
|
|
1189
1338
|
</form>
|
|
1190
1339
|
</Form>
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
|
|
1194
|
-
<AlertDialogAction
|
|
1195
|
-
disabled={isResettingPassword}
|
|
1196
|
-
onClick={async (event) => {
|
|
1197
|
-
event.preventDefault();
|
|
1198
|
-
const valid = await resetPasswordForm.trigger();
|
|
1199
|
-
if (!valid) return;
|
|
1200
|
-
await onResetPasswordSubmit();
|
|
1201
|
-
}}
|
|
1202
|
-
>
|
|
1203
|
-
{isResettingPassword
|
|
1204
|
-
? t('passwordResetSubmitting')
|
|
1205
|
-
: t('passwordResetConfirm')}
|
|
1206
|
-
</AlertDialogAction>
|
|
1207
|
-
</AlertDialogFooter>
|
|
1208
|
-
</AlertDialogContent>
|
|
1209
|
-
</AlertDialog>
|
|
1340
|
+
</ResizableSheetContent>
|
|
1341
|
+
</Sheet>
|
|
1210
1342
|
|
|
1211
1343
|
<AlertDialog
|
|
1212
1344
|
open={isResetResultDialogOpen}
|
|
@@ -338,6 +338,9 @@
|
|
|
338
338
|
"errorBulkRemovingRoles": "Error removing all roles",
|
|
339
339
|
"passwordResetTitle": "Password Reset",
|
|
340
340
|
"passwordResetDescription": "Generate a new password for this user.",
|
|
341
|
+
"passwordLastChangedHumanized": "This password has not been changed {relativeLabel}.",
|
|
342
|
+
"passwordLastChangedAbsolute": "Last recorded update: {absoluteLabel}",
|
|
343
|
+
"passwordLastChangedUnavailable": "The system could not determine when this password was last updated.",
|
|
341
344
|
"passwordResetNotice": "After this reset, the user must change the password on the next login.",
|
|
342
345
|
"buttonResetPassword": "Reset Password",
|
|
343
346
|
"passwordResetDialogTitle": "Reset User Password",
|
|
@@ -1230,6 +1233,7 @@
|
|
|
1230
1233
|
"fileUrlPlaceholder": "File URL",
|
|
1231
1234
|
"selectOption": "Select an option",
|
|
1232
1235
|
"selectMailProfile": "Select a mail profile",
|
|
1236
|
+
"mailProfileEmptyState": "No mail profiles found.",
|
|
1233
1237
|
"selectWhatsAppProfile": "Select a WhatsApp profile",
|
|
1234
1238
|
"selectStorageProfile": "Select a storage profile",
|
|
1235
1239
|
"storageProfileEmptyState": "No storage profiles found.",
|
|
@@ -2226,7 +2230,10 @@
|
|
|
2226
2230
|
"id": "ID",
|
|
2227
2231
|
"actions": "Actions",
|
|
2228
2232
|
"noUserAgent": "—",
|
|
2229
|
-
"detailsDescription": "Full details of this access log entry"
|
|
2233
|
+
"detailsDescription": "Full details of this access log entry",
|
|
2234
|
+
"os": "Operating System",
|
|
2235
|
+
"location": "Location",
|
|
2236
|
+
"rawUserAgent": "User Agent"
|
|
2230
2237
|
},
|
|
2231
2238
|
"IntegrationProfilePage": {
|
|
2232
2239
|
"breadcrumbHome": "Home",
|
|
@@ -2347,4 +2354,4 @@
|
|
|
2347
2354
|
"showPassword": "Show value",
|
|
2348
2355
|
"hidePassword": "Hide value"
|
|
2349
2356
|
}
|
|
2350
|
-
}
|
|
2357
|
+
}
|
|
@@ -338,6 +338,9 @@
|
|
|
338
338
|
"errorBulkRemovingRoles": "Erro ao remover todos os cargos",
|
|
339
339
|
"passwordResetTitle": "Redefinição de senha",
|
|
340
340
|
"passwordResetDescription": "Gere uma nova senha para este usuário.",
|
|
341
|
+
"passwordLastChangedHumanized": "A senha não é alterada {relativeLabel}.",
|
|
342
|
+
"passwordLastChangedAbsolute": "Última atualização registrada: {absoluteLabel}",
|
|
343
|
+
"passwordLastChangedUnavailable": "Ainda não foi possível identificar quando esta senha foi atualizada pela última vez.",
|
|
341
344
|
"passwordResetNotice": "Após essa redefinição, o usuário será obrigado a alterar a senha no próximo login.",
|
|
342
345
|
"buttonResetPassword": "Redefinir senha",
|
|
343
346
|
"passwordResetDialogTitle": "Redefinir senha do usuário",
|
|
@@ -1233,6 +1236,7 @@
|
|
|
1233
1236
|
"fileUrlPlaceholder": "URL do arquivo",
|
|
1234
1237
|
"selectOption": "Selecione uma opção",
|
|
1235
1238
|
"selectMailProfile": "Selecione um perfil de email",
|
|
1239
|
+
"mailProfileEmptyState": "Nenhum perfil de email encontrado.",
|
|
1236
1240
|
"selectWhatsAppProfile": "Selecione um perfil de WhatsApp",
|
|
1237
1241
|
"selectStorageProfile": "Selecione um perfil de armazenamento",
|
|
1238
1242
|
"storageProfileEmptyState": "Nenhum perfil de armazenamento encontrado.",
|
|
@@ -2281,7 +2285,10 @@
|
|
|
2281
2285
|
"id": "ID",
|
|
2282
2286
|
"actions": "Ações",
|
|
2283
2287
|
"noUserAgent": "—",
|
|
2284
|
-
"detailsDescription": "Detalhes completos desta entrada de log de acesso"
|
|
2288
|
+
"detailsDescription": "Detalhes completos desta entrada de log de acesso",
|
|
2289
|
+
"os": "Sistema Operacional",
|
|
2290
|
+
"location": "Localização",
|
|
2291
|
+
"rawUserAgent": "User Agent"
|
|
2285
2292
|
},
|
|
2286
2293
|
"IntegrationProfilePage": {
|
|
2287
2294
|
"breadcrumbHome": "Início",
|
|
@@ -2402,4 +2409,4 @@
|
|
|
2402
2409
|
"showPassword": "Mostrar valor",
|
|
2403
2410
|
"hidePassword": "Ocultar valor"
|
|
2404
2411
|
}
|
|
2405
|
-
}
|
|
2412
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.362",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"dependencies": {
|
|
7
|
+
"@aws-sdk/client-lambda": "3.1014.0",
|
|
7
8
|
"@aws-sdk/client-s3": "^3.1014.0",
|
|
8
9
|
"@aws-sdk/client-sts": "3.1014.0",
|
|
9
10
|
"@aws-sdk/s3-request-presigner": "^3.1014.0",
|
|
10
11
|
"@azure/storage-blob": "^12.27.0",
|
|
11
12
|
"@google-cloud/storage": "^7.16.0",
|
|
13
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
12
14
|
"@nestjs/axios": "*",
|
|
13
15
|
"@nestjs/common": "*",
|
|
14
16
|
"@nestjs/config": "^4.0.2",
|
|
@@ -20,6 +22,7 @@
|
|
|
20
22
|
"argon2": "^0.44.0",
|
|
21
23
|
"axios": "^1.9.0",
|
|
22
24
|
"cookie-parser": "^1.4.7",
|
|
25
|
+
"geoip-lite": "^2.0.2",
|
|
23
26
|
"handlebars": "^4.7.8",
|
|
24
27
|
"jsonwebtoken": "^9.0.2",
|
|
25
28
|
"multer": "^2.0.1",
|
|
@@ -32,14 +35,14 @@
|
|
|
32
35
|
"rxjs": "^7.8.2",
|
|
33
36
|
"sharp": "^0.34.2",
|
|
34
37
|
"speakeasy": "^2.0.0",
|
|
38
|
+
"ua-parser-js": "^2.0.10",
|
|
35
39
|
"uuid": "^11.1.0",
|
|
36
|
-
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
37
|
-
"@hed-hog/api-locale": "0.0.14",
|
|
38
|
-
"@hed-hog/api-mail": "0.0.9",
|
|
39
40
|
"@hed-hog/api": "0.0.8",
|
|
41
|
+
"@hed-hog/api-locale": "0.0.14",
|
|
40
42
|
"@hed-hog/api-prisma": "0.0.6",
|
|
41
|
-
"@hed-hog/api-
|
|
42
|
-
"@hed-hog/api-types": "0.0.1"
|
|
43
|
+
"@hed-hog/api-mail": "0.0.9",
|
|
44
|
+
"@hed-hog/api-types": "0.0.1",
|
|
45
|
+
"@hed-hog/api-pagination": "0.0.7"
|
|
43
46
|
},
|
|
44
47
|
"exports": {
|
|
45
48
|
".": {
|
|
@@ -72,8 +75,10 @@
|
|
|
72
75
|
"devDependencies": {
|
|
73
76
|
"@types/cookie-parser": "^1.4.10",
|
|
74
77
|
"@types/express": "^4.17.24",
|
|
78
|
+
"@types/geoip-lite": "^1.4.4",
|
|
75
79
|
"@types/multer": "^2.0.0",
|
|
76
80
|
"@types/pako": "^2.0.4",
|
|
81
|
+
"@types/ua-parser-js": "^0.7.39",
|
|
77
82
|
"rimraf": "^6.1.0"
|
|
78
83
|
},
|
|
79
84
|
"scripts": {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { PageOrderDirection, PaginationService } from '@hed-hog/api-pagination';
|
|
2
2
|
import { Prisma, PrismaService } from '@hed-hog/api-prisma';
|
|
3
3
|
import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
|
|
4
|
+
import * as geoip from 'geoip-lite';
|
|
5
|
+
import { UAParser } from 'ua-parser-js';
|
|
4
6
|
|
|
5
7
|
interface RecordHttpData {
|
|
6
8
|
userId?: number;
|
|
@@ -100,7 +102,7 @@ export class AccessLogService {
|
|
|
100
102
|
where.mcp_tool = { not: null };
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
|
|
105
|
+
const result = await this.pagination.paginate(this.prisma.access_log, {
|
|
104
106
|
...params,
|
|
105
107
|
fields: params.fields ?? '',
|
|
106
108
|
}, {
|
|
@@ -110,6 +112,22 @@ export class AccessLogService {
|
|
|
110
112
|
user: { select: { id: true, name: true, photo_id: true } },
|
|
111
113
|
},
|
|
112
114
|
});
|
|
115
|
+
|
|
116
|
+
const data = (result.data as any[]).map((log) => {
|
|
117
|
+
const ua = log.user_agent ? new UAParser(log.user_agent).getResult() : null;
|
|
118
|
+
const geo = log.ip ? geoip.lookup(log.ip) : null;
|
|
119
|
+
return {
|
|
120
|
+
...log,
|
|
121
|
+
browser_name: ua?.browser?.name ?? null,
|
|
122
|
+
browser_version: ua?.browser?.version ?? null,
|
|
123
|
+
os_name: ua?.os?.name ?? null,
|
|
124
|
+
os_version: ua?.os?.version ?? null,
|
|
125
|
+
country: geo?.country ?? null,
|
|
126
|
+
city: geo?.city ?? null,
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return { ...result, data };
|
|
113
131
|
}
|
|
114
132
|
|
|
115
133
|
async clearOld(retentionDays: number, batchLimit: number): Promise<void> {
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
Headers,
|
|
9
9
|
Inject,
|
|
10
10
|
Ip,
|
|
11
|
+
Logger,
|
|
11
12
|
Post,
|
|
12
13
|
Req,
|
|
13
14
|
Res,
|
|
@@ -28,6 +29,8 @@ import { ResetDTO } from './dto/reset.dto';
|
|
|
28
29
|
|
|
29
30
|
@Controller('auth')
|
|
30
31
|
export class AuthController {
|
|
32
|
+
private readonly logger = new Logger(AuthController.name);
|
|
33
|
+
|
|
31
34
|
constructor(
|
|
32
35
|
@Inject(forwardRef(() => AuthService))
|
|
33
36
|
private readonly service: AuthService,
|
|
@@ -60,6 +63,8 @@ export class AuthController {
|
|
|
60
63
|
@Headers('user-agent') userAgent: string,
|
|
61
64
|
@Ip() ipAddress: string
|
|
62
65
|
) {
|
|
66
|
+
const tokenSource = req.cookies['rt'] ? 'cookie' : (refreshTokenFromBody ? 'body' : 'none');
|
|
67
|
+
this.logger.debug(`[AuthController] POST /auth/refresh tokenSource=${tokenSource} ip=${ipAddress}`);
|
|
63
68
|
const currentRefreshToken = req.cookies['rt'] || refreshTokenFromBody;
|
|
64
69
|
const { session, refreshToken, accessToken } = await this.service.refreshAccessToken(locale, currentRefreshToken, ipAddress, userAgent);
|
|
65
70
|
await this.token.setRefreshTokenCookie(locale, res, refreshToken, session.expires_at);
|
package/src/auth/auth.service.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
forwardRef,
|
|
7
7
|
Inject,
|
|
8
8
|
Injectable,
|
|
9
|
+
Logger,
|
|
9
10
|
NotFoundException,
|
|
10
11
|
UnauthorizedException,
|
|
11
12
|
} from '@nestjs/common';
|
|
@@ -21,6 +22,7 @@ import { LoginDTO } from './dto/login.dto';
|
|
|
21
22
|
|
|
22
23
|
@Injectable()
|
|
23
24
|
export class AuthService {
|
|
25
|
+
private readonly logger = new Logger(AuthService.name);
|
|
24
26
|
|
|
25
27
|
constructor(
|
|
26
28
|
@Inject(forwardRef(() => PrismaService))
|
|
@@ -46,7 +48,8 @@ export class AuthService {
|
|
|
46
48
|
const payload = { sub: userId, sessionId: session.id };
|
|
47
49
|
const accessToken = await this.token.createAccessToken(payload);
|
|
48
50
|
await this.prisma.user.update({ where: { id: userId }, data: { last_login_at: new Date() } });
|
|
49
|
-
|
|
51
|
+
this.logger.debug(`[AuthService] Session created userId=${userId} sessionId=${session.id}`);
|
|
52
|
+
|
|
50
53
|
return {
|
|
51
54
|
accessToken,
|
|
52
55
|
refreshToken,
|
|
@@ -474,9 +477,12 @@ export class AuthService {
|
|
|
474
477
|
async refreshAccessToken(locale: string, refreshToken: string, ipAddress: string, userAgent: string) {
|
|
475
478
|
|
|
476
479
|
if (!refreshToken) {
|
|
480
|
+
this.logger.warn(`[AuthService] refreshAccessToken called without token ip=${ipAddress}`);
|
|
477
481
|
throw new UnauthorizedException(getLocaleText('accessDenied', locale, 'Access denied.'));
|
|
478
482
|
}
|
|
479
483
|
|
|
484
|
+
this.logger.debug(`[AuthService] refreshAccessToken ip=${ipAddress} ua=${userAgent?.slice(0, 40)}`);
|
|
485
|
+
|
|
480
486
|
const { session, token } = await this.session.refresh(
|
|
481
487
|
locale,
|
|
482
488
|
refreshToken,
|
|
@@ -488,6 +494,8 @@ export class AuthService {
|
|
|
488
494
|
const payload = { sub: user.id, sessionId: session.id };
|
|
489
495
|
const accessToken = await this.token.createAccessToken(payload);
|
|
490
496
|
|
|
497
|
+
this.logger.debug(`[AuthService] refreshAccessToken success userId=${user.id} newSessionId=${session.id}`);
|
|
498
|
+
|
|
491
499
|
return { accessToken, refreshToken: token, session };
|
|
492
500
|
|
|
493
501
|
}
|
|
@@ -780,7 +788,8 @@ export class AuthService {
|
|
|
780
788
|
throw new BadRequestException(getLocaleText('webauthnNotConfigured', locale, 'WebAuthn not configured.'));
|
|
781
789
|
}
|
|
782
790
|
|
|
783
|
-
const
|
|
791
|
+
const primaryOrigin = (process.env.CORS_ALLOWED_ORIGINS ?? '').split(/[\n,;]+/).map(u => u.trim()).filter(Boolean)[0] ?? 'http://localhost:3200';
|
|
792
|
+
const rpID = new URL(primaryOrigin).hostname;
|
|
784
793
|
|
|
785
794
|
const allowCredentials = user.user_mfa
|
|
786
795
|
.filter(mfa => mfa.user_mfa_webauthn && mfa.user_mfa_webauthn.length > 0)
|
|
@@ -834,8 +843,9 @@ export class AuthService {
|
|
|
834
843
|
}
|
|
835
844
|
|
|
836
845
|
const authenticator = challengeRecord.user_mfa.user_mfa_webauthn[0];
|
|
837
|
-
const
|
|
838
|
-
const
|
|
846
|
+
const primaryOrigin = (process.env.CORS_ALLOWED_ORIGINS ?? '').split(/[\n,;]+/).map(u => u.trim()).filter(Boolean)[0] ?? 'http://localhost:3200';
|
|
847
|
+
const rpID = new URL(primaryOrigin).hostname;
|
|
848
|
+
const expectedOrigin = primaryOrigin;
|
|
839
849
|
|
|
840
850
|
const verification = await verifyAuthenticationResponse({
|
|
841
851
|
response: assertionResponse,
|