@hed-hog/core 0.0.370 → 0.0.373
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/LICENSE.md +21 -0
- package/README.md +2 -0
- package/dist/ai/ai.service.d.ts.map +1 -1
- package/dist/ai/ai.service.js +10 -7
- package/dist/ai/ai.service.js.map +1 -1
- package/dist/app-command/app-command.module.d.ts +3 -0
- package/dist/app-command/app-command.module.d.ts.map +1 -0
- package/dist/app-command/app-command.module.js +22 -0
- package/dist/app-command/app-command.module.js.map +1 -0
- package/dist/app-command/app-command.registry.d.ts +22 -0
- package/dist/app-command/app-command.registry.d.ts.map +1 -0
- package/dist/{webhook-integration/webhook-command.registry.js → app-command/app-command.registry.js} +6 -6
- package/dist/app-command/app-command.registry.js.map +1 -0
- package/dist/auth/auth.controller.d.ts +2 -2
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +20 -7
- package/dist/auth/auth.controller.js.map +1 -1
- package/dist/auth/auth.service.d.ts +3 -3
- package/dist/auth/auth.service.d.ts.map +1 -1
- package/dist/auth/auth.service.js +10 -36
- package/dist/auth/auth.service.js.map +1 -1
- package/dist/auth/dto/forget.dto.d.ts +1 -0
- package/dist/auth/dto/forget.dto.d.ts.map +1 -1
- package/dist/auth/dto/forget.dto.js +5 -0
- package/dist/auth/dto/forget.dto.js.map +1 -1
- package/dist/auth/guards/auth.guard.d.ts.map +1 -1
- package/dist/auth/guards/auth.guard.js +4 -1
- package/dist/auth/guards/auth.guard.js.map +1 -1
- package/dist/challenge/challenge.service.d.ts +8 -7
- package/dist/challenge/challenge.service.d.ts.map +1 -1
- package/dist/challenge/challenge.service.js +27 -19
- package/dist/challenge/challenge.service.js.map +1 -1
- package/dist/file/dto/upload.dto.d.ts +3 -0
- package/dist/file/dto/upload.dto.d.ts.map +1 -1
- package/dist/file/dto/upload.dto.js +22 -0
- package/dist/file/dto/upload.dto.js.map +1 -1
- package/dist/file/file.controller.d.ts +2 -1
- package/dist/file/file.controller.d.ts.map +1 -1
- package/dist/file/file.controller.js +29 -16
- package/dist/file/file.controller.js.map +1 -1
- package/dist/file/file.service.d.ts +51 -1
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +250 -18
- package/dist/file/file.service.js.map +1 -1
- package/dist/file/image-optimization.service.d.ts +17 -4
- package/dist/file/image-optimization.service.d.ts.map +1 -1
- package/dist/file/image-optimization.service.js +101 -46
- package/dist/file/image-optimization.service.js.map +1 -1
- package/dist/file/provider/s3.provider.d.ts.map +1 -1
- package/dist/file/provider/s3.provider.js +15 -7
- package/dist/file/provider/s3.provider.js.map +1 -1
- package/dist/file/provider/spaces.provider.d.ts.map +1 -1
- package/dist/file/provider/spaces.provider.js +18 -8
- package/dist/file/provider/spaces.provider.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/integration/services/event-subscriber.registry.d.ts +5 -0
- package/dist/integration/services/event-subscriber.registry.d.ts.map +1 -1
- package/dist/integration/services/event-subscriber.registry.js +15 -0
- package/dist/integration/services/event-subscriber.registry.js.map +1 -1
- package/dist/integration/services/outbox.processor.d.ts.map +1 -1
- package/dist/integration/services/outbox.processor.js +2 -4
- package/dist/integration/services/outbox.processor.js.map +1 -1
- package/dist/integration/services/outbox.service.d.ts +4 -2
- package/dist/integration/services/outbox.service.d.ts.map +1 -1
- package/dist/integration/services/outbox.service.js +26 -15
- package/dist/integration/services/outbox.service.js.map +1 -1
- package/dist/integration-profile/integration-credential-crypto.service.d.ts +43 -0
- package/dist/integration-profile/integration-credential-crypto.service.d.ts.map +1 -0
- package/dist/integration-profile/integration-credential-crypto.service.js +142 -0
- package/dist/integration-profile/integration-credential-crypto.service.js.map +1 -0
- package/dist/integration-profile/integration-profile.controller.d.ts +6 -7
- package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.controller.js +4 -3
- package/dist/integration-profile/integration-profile.controller.js.map +1 -1
- package/dist/integration-profile/integration-profile.module.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.module.js +5 -2
- package/dist/integration-profile/integration-profile.module.js.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.d.ts +16 -0
- package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -0
- package/dist/integration-profile/integration-profile.secrets.js +56 -0
- package/dist/integration-profile/integration-profile.secrets.js.map +1 -0
- package/dist/integration-profile/integration-profile.service.d.ts +125 -8
- package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.service.js +257 -68
- package/dist/integration-profile/integration-profile.service.js.map +1 -1
- package/dist/integration-profile/integration-profile.utils.d.ts +17 -0
- package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.utils.js +31 -0
- package/dist/integration-profile/integration-profile.utils.js.map +1 -1
- package/dist/language/en.json +178 -0
- package/dist/language/pt.json +178 -0
- package/dist/mail/mail.controller.d.ts.map +1 -1
- package/dist/mail/mail.controller.js +5 -28
- package/dist/mail/mail.controller.js.map +1 -1
- package/dist/mail/mail.module.js +18 -18
- package/dist/mail/mail.module.js.map +1 -1
- package/dist/mail/mail.service.d.ts +13 -2
- package/dist/mail/mail.service.d.ts.map +1 -1
- package/dist/mail/mail.service.js +25 -15
- package/dist/mail/mail.service.js.map +1 -1
- package/dist/mcp-chat/mcp-chat.service.js +6 -5
- package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
- package/dist/oauth/providers/facebook.provider.js +5 -5
- package/dist/oauth/providers/facebook.provider.js.map +1 -1
- package/dist/oauth/providers/github.provider.js +5 -5
- package/dist/oauth/providers/github.provider.js.map +1 -1
- package/dist/oauth/providers/google.provider.js +5 -5
- package/dist/oauth/providers/google.provider.js.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.js +5 -5
- package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
- package/dist/oauth/providers/microsoft.provider.js +5 -5
- package/dist/oauth/providers/microsoft.provider.js.map +1 -1
- package/dist/pdf/pdf.service.d.ts +4 -0
- package/dist/pdf/pdf.service.d.ts.map +1 -1
- package/dist/pdf/pdf.service.js +16 -9
- package/dist/pdf/pdf.service.js.map +1 -1
- package/dist/profile/profile.controller.d.ts +7 -7
- package/dist/profile/profile.controller.d.ts.map +1 -1
- package/dist/profile/profile.controller.js +28 -21
- package/dist/profile/profile.controller.js.map +1 -1
- package/dist/profile/profile.service.d.ts +7 -7
- package/dist/profile/profile.service.d.ts.map +1 -1
- package/dist/profile/profile.service.js +21 -25
- package/dist/profile/profile.service.js.map +1 -1
- package/dist/setting/setting.service.d.ts +1 -0
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +15 -2
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/token/token.service.d.ts.map +1 -1
- package/dist/token/token.service.js +2 -1
- package/dist/token/token.service.js.map +1 -1
- package/dist/user/user.controller.d.ts +9 -0
- package/dist/user/user.controller.d.ts.map +1 -1
- package/dist/user/user.controller.js +13 -0
- package/dist/user/user.controller.js.map +1 -1
- package/dist/user/user.service.d.ts +7 -0
- package/dist/user/user.service.d.ts.map +1 -1
- package/dist/user/user.service.js +19 -0
- package/dist/user/user.service.js.map +1 -1
- package/dist/utils/webauthn-origin.d.ts +11 -0
- package/dist/utils/webauthn-origin.d.ts.map +1 -0
- package/dist/utils/webauthn-origin.js +37 -0
- package/dist/utils/webauthn-origin.js.map +1 -0
- package/dist/webhook-integration/webhook-integration.controller.d.ts +13 -0
- package/dist/webhook-integration/webhook-integration.controller.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.controller.js +29 -0
- package/dist/webhook-integration/webhook-integration.controller.js.map +1 -1
- package/dist/webhook-integration/webhook-integration.module.js +3 -3
- package/dist/webhook-integration/webhook-integration.module.js.map +1 -1
- package/dist/webhook-integration/webhook-integration.service.d.ts +18 -2
- package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.service.js +110 -6
- package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
- package/hedhog/data/integration_provider.yaml +9 -0
- package/hedhog/data/integration_type.yaml +6 -0
- package/hedhog/data/menu.yaml +6 -0
- package/hedhog/data/role.yaml +7 -0
- package/hedhog/data/route.yaml +36 -0
- package/hedhog/data/setting_group.yaml +119 -13
- package/hedhog/frontend/app/access-log/page.tsx.ejs +35 -3
- package/hedhog/frontend/app/account/components/active-sessions.tsx.ejs +36 -3
- package/hedhog/frontend/app/account/components/mcp-tokens.tsx.ejs +39 -4
- package/hedhog/frontend/app/account/components/tokens.tsx.ejs +39 -4
- package/hedhog/frontend/app/ai_agent/page.tsx.ejs +35 -2
- package/hedhog/frontend/app/configurations/[slug]/components/byte-size-input.tsx.ejs +160 -0
- package/hedhog/frontend/app/configurations/[slug]/components/duration-input.tsx.ejs +157 -0
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +244 -63
- package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +33 -1
- package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +35 -3
- package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +36 -4
- package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +35 -3
- package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +36 -4
- package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +35 -3
- package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +156 -38
- package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +118 -11
- package/hedhog/frontend/app/mail/log/page.tsx.ejs +37 -4
- package/hedhog/frontend/app/menu/page.tsx.ejs +34 -2
- package/hedhog/frontend/app/preferences/page.tsx.ejs +77 -0
- package/hedhog/frontend/app/roles/menus.tsx.ejs +35 -3
- package/hedhog/frontend/app/roles/page.tsx.ejs +34 -2
- package/hedhog/frontend/app/roles/routes.tsx.ejs +36 -4
- package/hedhog/frontend/app/roles/users.tsx.ejs +36 -4
- package/hedhog/frontend/app/routes/page.tsx.ejs +34 -2
- package/hedhog/frontend/app/setting-list/page.tsx.ejs +34 -2
- package/hedhog/frontend/app/users/page.tsx.ejs +51 -21
- package/hedhog/frontend/messages/en.json +69 -4
- package/hedhog/frontend/messages/pt.json +69 -4
- package/hedhog/table/setting.yaml +2 -0
- package/hedhog/table/webhook_call_log.yaml +49 -46
- package/package.json +9 -4
- package/src/ai/ai.service.ts +9 -6
- package/src/app-command/app-command.module.ts +9 -0
- package/src/{webhook-integration/webhook-command.registry.ts → app-command/app-command.registry.ts} +9 -9
- package/src/auth/auth.controller.ts +20 -4
- package/src/auth/auth.service.ts +10 -36
- package/src/auth/dto/forget.dto.ts +5 -1
- package/src/auth/guards/auth.guard.ts +4 -1
- package/src/challenge/challenge.service.ts +30 -19
- package/src/file/dto/upload.dto.ts +25 -1
- package/src/file/file.controller.ts +31 -18
- package/src/file/file.service.ts +341 -16
- package/src/file/image-optimization.service.ts +111 -46
- package/src/file/provider/s3.provider.ts +16 -7
- package/src/file/provider/spaces.provider.ts +15 -5
- package/src/index.ts +4 -1
- package/src/integration/services/event-subscriber.registry.ts +15 -0
- package/src/integration/services/outbox.processor.ts +2 -5
- package/src/integration/services/outbox.service.ts +35 -17
- package/src/integration-profile/integration-credential-crypto.service.ts +146 -0
- package/src/integration-profile/integration-profile.controller.ts +2 -0
- package/src/integration-profile/integration-profile.module.ts +5 -2
- package/src/integration-profile/integration-profile.secrets.ts +51 -0
- package/src/integration-profile/integration-profile.service.ts +335 -54
- package/src/integration-profile/integration-profile.utils.ts +56 -0
- package/src/language/en.json +5 -1
- package/src/language/pt.json +5 -1
- package/src/mail/mail.controller.ts +5 -29
- package/src/mail/mail.module.ts +5 -5
- package/src/mail/mail.service.ts +37 -13
- package/src/mcp-chat/mcp-chat.service.ts +5 -5
- package/src/oauth/oauth-callback-coordinator.service.spec.ts +159 -0
- package/src/oauth/providers/facebook.provider.ts +3 -3
- package/src/oauth/providers/github.provider.ts +3 -3
- package/src/oauth/providers/google.provider.ts +3 -3
- package/src/oauth/providers/microsoft-entra-id.provider.ts +3 -3
- package/src/oauth/providers/microsoft.provider.ts +3 -3
- package/src/pdf/pdf.service.ts +19 -6
- package/src/profile/profile.controller.ts +21 -14
- package/src/profile/profile.service.ts +23 -20
- package/src/security/security.service.spec.ts +222 -0
- package/src/setting/setting.service.ts +15 -2
- package/src/token/token.service.ts +2 -1
- package/src/user/user.controller.ts +26 -16
- package/src/user/user.service.ts +25 -0
- package/src/utils/webauthn-origin.ts +38 -0
- package/src/webhook-integration/webhook-integration.controller.ts +23 -0
- package/src/webhook-integration/webhook-integration.module.ts +3 -3
- package/src/webhook-integration/webhook-integration.service.ts +115 -5
- package/dist/webhook-integration/webhook-command.registry.d.ts +0 -22
- package/dist/webhook-integration/webhook-command.registry.d.ts.map +0 -1
- package/dist/webhook-integration/webhook-command.registry.js.map +0 -1
- package/src/menu/menu.service.spec.ts +0 -247
- package/src/route/route.service.spec.ts +0 -300
- package/src/screen/screen.service.spec.ts +0 -298
- package/src/user/user.service.spec.ts +0 -294
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.373",
|
|
4
|
+
"license": "MIT",
|
|
4
5
|
"main": "dist/index.js",
|
|
5
6
|
"types": "dist/index.d.ts",
|
|
6
7
|
"dependencies": {
|
|
7
8
|
"@aws-sdk/client-lambda": "3.1014.0",
|
|
8
9
|
"@aws-sdk/client-s3": "^3.1014.0",
|
|
9
10
|
"@aws-sdk/client-sts": "3.1014.0",
|
|
11
|
+
"@aws-sdk/lib-storage": "3.1014.0",
|
|
10
12
|
"@aws-sdk/s3-request-presigner": "^3.1014.0",
|
|
11
13
|
"@azure/storage-blob": "^12.27.0",
|
|
12
14
|
"@google-cloud/storage": "^7.16.0",
|
|
@@ -19,6 +21,7 @@
|
|
|
19
21
|
"@nestjs/jwt": "^11",
|
|
20
22
|
"@nestjs/mapped-types": "*",
|
|
21
23
|
"@nestjs/schedule": "^6.0.1",
|
|
24
|
+
"@nestjs/throttler": "^6.4.0",
|
|
22
25
|
"@simplewebauthn/server": "^13.2.2",
|
|
23
26
|
"argon2": "^0.44.0",
|
|
24
27
|
"axios": "^1.9.0",
|
|
@@ -40,10 +43,10 @@
|
|
|
40
43
|
"uuid": "^11.1.0",
|
|
41
44
|
"@hed-hog/api": "0.0.8",
|
|
42
45
|
"@hed-hog/api-locale": "0.0.14",
|
|
46
|
+
"@hed-hog/api-mail": "0.0.9",
|
|
43
47
|
"@hed-hog/api-pagination": "0.0.7",
|
|
44
|
-
"@hed-hog/api-prisma": "0.0.6",
|
|
45
48
|
"@hed-hog/api-types": "0.0.1",
|
|
46
|
-
"@hed-hog/api-
|
|
49
|
+
"@hed-hog/api-prisma": "0.0.6"
|
|
47
50
|
},
|
|
48
51
|
"exports": {
|
|
49
52
|
".": {
|
|
@@ -85,10 +88,12 @@
|
|
|
85
88
|
"scripts": {
|
|
86
89
|
"_dev": "pnpm build --watch",
|
|
87
90
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
|
|
91
|
+
"test": "jest --config jest.config.ts --runInBand --passWithNoTests",
|
|
88
92
|
"clean": "rimraf dist tsconfig.production.tsbuildinfo",
|
|
89
93
|
"prebuild": "pnpm run clean && pnpm --dir ../.. exec ts-node ./scripts/build-dependencies.ts libraries/core",
|
|
90
|
-
"build": "tsc --project tsconfig.production.json && pnpm run copy:templates",
|
|
94
|
+
"build": "tsc --project tsconfig.production.json && pnpm run copy:templates && pnpm run copy:language",
|
|
91
95
|
"copy:templates": "node -e \"require('fs').cpSync('src/file/templates', 'dist/file/templates', {recursive: true})\"",
|
|
96
|
+
"copy:language": "node -e \"const fs=require('fs');if(fs.existsSync('src/language'))fs.cpSync('src/language','dist/language',{recursive:true})\"",
|
|
92
97
|
"patch": "pnpm exec ts-node ../../scripts/patch.ts libraries/core",
|
|
93
98
|
"prod": "pnpm run patch && pnpm run build && pnpm publish --access public --no-git-checks"
|
|
94
99
|
}
|
package/src/ai/ai.service.ts
CHANGED
|
@@ -62,7 +62,10 @@ export class AiService implements OnModuleInit {
|
|
|
62
62
|
'ai-openai-profile-id',
|
|
63
63
|
'ai-gemini-profile-id',
|
|
64
64
|
]);
|
|
65
|
-
if (
|
|
65
|
+
if (
|
|
66
|
+
!String(s['ai-openai-profile-id'] ?? '').trim() &&
|
|
67
|
+
!String(s['ai-gemini-profile-id'] ?? '').trim()
|
|
68
|
+
) {
|
|
66
69
|
this.logger.warn('No AI profiles configured. Set profiles in Settings → AI.');
|
|
67
70
|
}
|
|
68
71
|
} catch { /* settings not available yet */ }
|
|
@@ -442,21 +445,21 @@ export class AiService implements OnModuleInit {
|
|
|
442
445
|
private async getAiApiKey(provider: AiProvider): Promise<string> {
|
|
443
446
|
const slug = provider === 'openai' ? 'ai-openai-profile-id' : 'ai-gemini-profile-id';
|
|
444
447
|
const settings = await this.settingService.getSettingValues([slug]);
|
|
445
|
-
const
|
|
448
|
+
const profileSlug = String(settings[slug] ?? '').trim();
|
|
446
449
|
|
|
447
|
-
if (!
|
|
450
|
+
if (!profileSlug) {
|
|
448
451
|
throw new BadRequestException(
|
|
449
452
|
`No ${provider} AI profile configured. Set one in Settings → AI.`,
|
|
450
453
|
);
|
|
451
454
|
}
|
|
452
455
|
|
|
453
456
|
const profile = await this.prismaService.integration_profile.findUnique({
|
|
454
|
-
where: {
|
|
457
|
+
where: { slug: profileSlug },
|
|
455
458
|
include: { integration_provider: { select: { slug: true } } },
|
|
456
459
|
});
|
|
457
460
|
|
|
458
461
|
if (!profile) {
|
|
459
|
-
throw new BadRequestException(`AI profile ${
|
|
462
|
+
throw new BadRequestException(`AI profile "${profileSlug}" not found.`);
|
|
460
463
|
}
|
|
461
464
|
|
|
462
465
|
const { apiKey } = buildAiConfigFromIntegration(
|
|
@@ -465,7 +468,7 @@ export class AiService implements OnModuleInit {
|
|
|
465
468
|
);
|
|
466
469
|
|
|
467
470
|
if (!apiKey) {
|
|
468
|
-
throw new BadRequestException(`AI profile ${
|
|
471
|
+
throw new BadRequestException(`AI profile "${profileSlug}" has no API key configured.`);
|
|
469
472
|
}
|
|
470
473
|
|
|
471
474
|
return apiKey;
|
package/src/{webhook-integration/webhook-command.registry.ts → app-command/app-command.registry.ts}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Injectable } from '@nestjs/common';
|
|
2
2
|
|
|
3
|
-
export interface
|
|
3
|
+
export interface AppCommandContext extends Record<string, unknown> {
|
|
4
4
|
webhookBody?: unknown;
|
|
5
5
|
webhookHeaders?: Record<string, unknown>;
|
|
6
6
|
remoteIp?: string | null;
|
|
@@ -8,37 +8,37 @@ export interface WebhookCommandContext extends Record<string, unknown> {
|
|
|
8
8
|
integrationId?: number;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export interface
|
|
11
|
+
export interface AppCommand {
|
|
12
12
|
slug: string;
|
|
13
13
|
name: string;
|
|
14
14
|
description: string;
|
|
15
15
|
inputSchema: Record<string, unknown>;
|
|
16
16
|
handler: (
|
|
17
17
|
params: Record<string, unknown>,
|
|
18
|
-
context?:
|
|
18
|
+
context?: AppCommandContext,
|
|
19
19
|
) => Promise<unknown>;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
@Injectable()
|
|
23
|
-
export class
|
|
24
|
-
private readonly commands = new Map<string,
|
|
23
|
+
export class AppCommandRegistry {
|
|
24
|
+
private readonly commands = new Map<string, AppCommand>();
|
|
25
25
|
|
|
26
|
-
register(command:
|
|
26
|
+
register(command: AppCommand): void {
|
|
27
27
|
this.commands.set(command.slug, command);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
find(slug: string):
|
|
30
|
+
find(slug: string): AppCommand | undefined {
|
|
31
31
|
return this.commands.get(slug);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
list():
|
|
34
|
+
list(): AppCommand[] {
|
|
35
35
|
return Array.from(this.commands.values());
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
async dispatch(
|
|
39
39
|
slug: string,
|
|
40
40
|
params: Record<string, unknown>,
|
|
41
|
-
context?:
|
|
41
|
+
context?: AppCommandContext,
|
|
42
42
|
): Promise<unknown> {
|
|
43
43
|
const command = this.find(slug);
|
|
44
44
|
if (!command) {
|
|
@@ -12,8 +12,10 @@ import {
|
|
|
12
12
|
Post,
|
|
13
13
|
Req,
|
|
14
14
|
Res,
|
|
15
|
-
UnauthorizedException
|
|
15
|
+
UnauthorizedException,
|
|
16
|
+
UseGuards
|
|
16
17
|
} from '@nestjs/common';
|
|
18
|
+
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
|
|
17
19
|
import { TokenService } from '../token/token.service';
|
|
18
20
|
import { CreateWithEmailAndPasswordDTO } from '../user/dto/create-with-email-and-password.dto';
|
|
19
21
|
import { UserService } from '../user/user.service';
|
|
@@ -87,7 +89,11 @@ export class AuthController {
|
|
|
87
89
|
};
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
// Rate-limit contra brute-force de credenciais: 10 tentativas/min por IP
|
|
93
|
+
// (ajustável; valor acomoda a suíte E2E de auth que faz vários logins seguidos).
|
|
90
94
|
@Public()
|
|
95
|
+
@UseGuards(ThrottlerGuard)
|
|
96
|
+
@Throttle({ default: { limit: 10, ttl: 60_000 } })
|
|
91
97
|
@Post('login')
|
|
92
98
|
async login(
|
|
93
99
|
@Locale() locale: string,
|
|
@@ -101,6 +107,8 @@ export class AuthController {
|
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
@Public()
|
|
110
|
+
@UseGuards(ThrottlerGuard)
|
|
111
|
+
@Throttle({ default: { limit: 10, ttl: 60_000 } })
|
|
104
112
|
@Post('login-email-verification')
|
|
105
113
|
async emailVerificationLogin(
|
|
106
114
|
@Locale() locale: string,
|
|
@@ -132,6 +140,8 @@ export class AuthController {
|
|
|
132
140
|
}
|
|
133
141
|
|
|
134
142
|
@Public()
|
|
143
|
+
@UseGuards(ThrottlerGuard)
|
|
144
|
+
@Throttle({ default: { limit: 10, ttl: 60_000 } })
|
|
135
145
|
@Post('login-code')
|
|
136
146
|
async loginCode(
|
|
137
147
|
@Locale() locale: string,
|
|
@@ -156,6 +166,8 @@ export class AuthController {
|
|
|
156
166
|
}
|
|
157
167
|
|
|
158
168
|
@Public()
|
|
169
|
+
@UseGuards(ThrottlerGuard)
|
|
170
|
+
@Throttle({ default: { limit: 10, ttl: 60_000 } })
|
|
159
171
|
@Post('login-recovery-code')
|
|
160
172
|
async loginRecoveryCode(
|
|
161
173
|
@Locale() locale: string,
|
|
@@ -191,9 +203,10 @@ export class AuthController {
|
|
|
191
203
|
@Post('webauthn/generate')
|
|
192
204
|
async generateWebAuthnOptions(
|
|
193
205
|
@Locale() locale: string,
|
|
194
|
-
@Body() { mfaToken }: { mfaToken: string }
|
|
206
|
+
@Body() { mfaToken }: { mfaToken: string },
|
|
207
|
+
@Headers('origin') origin: string
|
|
195
208
|
) {
|
|
196
|
-
return this.service.generateWebAuthnAuthenticationOptions(locale, mfaToken);
|
|
209
|
+
return this.service.generateWebAuthnAuthenticationOptions(locale, mfaToken, origin);
|
|
197
210
|
}
|
|
198
211
|
|
|
199
212
|
@Public()
|
|
@@ -203,6 +216,7 @@ export class AuthController {
|
|
|
203
216
|
@Body() { mfaToken, assertionResponse }: { mfaToken: string; assertionResponse: any },
|
|
204
217
|
@Ip() ipAddress: string,
|
|
205
218
|
@Headers('user-agent') userAgent: string,
|
|
219
|
+
@Headers('origin') origin: string,
|
|
206
220
|
@Res({ passthrough: true }) res,
|
|
207
221
|
) {
|
|
208
222
|
const { accessToken, refreshToken, session, requiresPasswordReset } = await this.service.verifyWebAuthnAuthentication(
|
|
@@ -210,7 +224,9 @@ export class AuthController {
|
|
|
210
224
|
mfaToken,
|
|
211
225
|
assertionResponse,
|
|
212
226
|
ipAddress,
|
|
213
|
-
userAgent
|
|
227
|
+
userAgent,
|
|
228
|
+
undefined,
|
|
229
|
+
origin,
|
|
214
230
|
);
|
|
215
231
|
|
|
216
232
|
await this.token.setRefreshTokenCookie(locale, res, refreshToken, session.expires_at);
|
package/src/auth/auth.service.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { SessionService } from '../session/session.service';
|
|
|
17
17
|
import { SettingService } from '../setting/setting.service';
|
|
18
18
|
import { TokenService } from '../token/token.service';
|
|
19
19
|
import { UserService } from '../user/user.service';
|
|
20
|
+
import { resolveWebAuthnOrigin } from '../utils/webauthn-origin';
|
|
20
21
|
import { ForgetDTO } from './dto/forget.dto';
|
|
21
22
|
import { LoginDTO } from './dto/login.dto';
|
|
22
23
|
|
|
@@ -508,12 +509,12 @@ export class AuthService {
|
|
|
508
509
|
|
|
509
510
|
}
|
|
510
511
|
|
|
511
|
-
async forgotPassword(locale: string, { email }: ForgetDTO) {
|
|
512
|
+
async forgotPassword(locale: string, { email, app }: ForgetDTO) {
|
|
512
513
|
const user = await this.user.findUserByEmail(locale, email);
|
|
513
514
|
if (user){
|
|
514
515
|
await this.user.registerUserActivity(user?.id, "forgotPassword")
|
|
515
516
|
}
|
|
516
|
-
await this.challenge.forgotEmail(locale, email);
|
|
517
|
+
await this.challenge.forgotEmail(locale, email, app);
|
|
517
518
|
return { success: true };
|
|
518
519
|
}
|
|
519
520
|
|
|
@@ -530,41 +531,17 @@ export class AuthService {
|
|
|
530
531
|
|
|
531
532
|
async forgotResetPassword(locale: string, password: string, code: string, ipAddress: string, userAgent: string) {
|
|
532
533
|
const challenge = await this.challenge.verifyCodePassword(locale, code);
|
|
534
|
+
const userIdentifier = challenge.user_identifier;
|
|
533
535
|
const passwordHash = await this.security.hashArgon2(password);
|
|
534
536
|
|
|
535
537
|
await this.prisma.user_credential.updateMany({
|
|
536
538
|
where: {
|
|
537
539
|
type: 'password',
|
|
538
|
-
|
|
539
|
-
user_mfa: {
|
|
540
|
-
some: {
|
|
541
|
-
user_mfa_challenge: {
|
|
542
|
-
some: { id: challenge.id }
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
}
|
|
540
|
+
user_id: userIdentifier.user_id,
|
|
547
541
|
},
|
|
548
542
|
data: { hash: passwordHash, requires_reset: false },
|
|
549
543
|
});
|
|
550
544
|
|
|
551
|
-
const userIdentifier = await this.prisma.user_identifier.findFirst({
|
|
552
|
-
where: {
|
|
553
|
-
type: 'email',
|
|
554
|
-
user_id: challenge.user_mfa.user_id,
|
|
555
|
-
enabled: true
|
|
556
|
-
},
|
|
557
|
-
include: {
|
|
558
|
-
user: {
|
|
559
|
-
select: { name: true }
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
if (!userIdentifier) {
|
|
565
|
-
throw new BadRequestException(getLocaleText('userIdentifierNotFound', locale, 'User identifier not found.'));
|
|
566
|
-
}
|
|
567
|
-
|
|
568
545
|
this.mail.sendTemplatedMail(
|
|
569
546
|
locale,
|
|
570
547
|
{
|
|
@@ -576,7 +553,7 @@ export class AuthService {
|
|
|
576
553
|
}
|
|
577
554
|
);
|
|
578
555
|
|
|
579
|
-
const { accessToken, refreshToken, session } = await this.getAuthenticationPayload(locale,
|
|
556
|
+
const { accessToken, refreshToken, session } = await this.getAuthenticationPayload(locale, userIdentifier.user_id, ipAddress, userAgent);
|
|
580
557
|
await this.user.registerUserActivity(userIdentifier.id, "resetPassword")
|
|
581
558
|
return { accessToken, refreshToken, session }
|
|
582
559
|
}
|
|
@@ -780,7 +757,7 @@ export class AuthService {
|
|
|
780
757
|
};
|
|
781
758
|
}
|
|
782
759
|
|
|
783
|
-
async generateWebAuthnAuthenticationOptions(locale: string, mfaToken: string) {
|
|
760
|
+
async generateWebAuthnAuthenticationOptions(locale: string, mfaToken: string, requestOrigin?: string) {
|
|
784
761
|
const { generateAuthenticationOptions } = await import('@simplewebauthn/server');
|
|
785
762
|
const payload = await this.token.verifyMfaChallengeToken(mfaToken);
|
|
786
763
|
|
|
@@ -798,8 +775,7 @@ export class AuthService {
|
|
|
798
775
|
throw new BadRequestException(getLocaleText('webauthnNotConfigured', locale, 'WebAuthn not configured.'));
|
|
799
776
|
}
|
|
800
777
|
|
|
801
|
-
const
|
|
802
|
-
const rpID = new URL(primaryOrigin).hostname;
|
|
778
|
+
const { rpID } = resolveWebAuthnOrigin(requestOrigin);
|
|
803
779
|
|
|
804
780
|
const allowCredentials = user.user_mfa
|
|
805
781
|
.filter(mfa => mfa.user_mfa_webauthn && mfa.user_mfa_webauthn.length > 0)
|
|
@@ -825,7 +801,7 @@ export class AuthService {
|
|
|
825
801
|
return options;
|
|
826
802
|
}
|
|
827
803
|
|
|
828
|
-
async verifyWebAuthnAuthentication(locale: string, mfaToken: string, assertionResponse: any, ipAddress: string, userAgent: string, browserId?: string | null) {
|
|
804
|
+
async verifyWebAuthnAuthentication(locale: string, mfaToken: string, assertionResponse: any, ipAddress: string, userAgent: string, browserId?: string | null, requestOrigin?: string) {
|
|
829
805
|
const { verifyAuthenticationResponse } = await import('@simplewebauthn/server');
|
|
830
806
|
const payload = await this.token.verifyMfaChallengeToken(mfaToken);
|
|
831
807
|
const userId = payload.userId;
|
|
@@ -853,9 +829,7 @@ export class AuthService {
|
|
|
853
829
|
}
|
|
854
830
|
|
|
855
831
|
const authenticator = challengeRecord.user_mfa.user_mfa_webauthn[0];
|
|
856
|
-
const
|
|
857
|
-
const rpID = new URL(primaryOrigin).hostname;
|
|
858
|
-
const expectedOrigin = primaryOrigin;
|
|
832
|
+
const { rpID, expectedOrigin } = resolveWebAuthnOrigin(requestOrigin);
|
|
859
833
|
|
|
860
834
|
const verification = await verifyAuthenticationResponse({
|
|
861
835
|
response: assertionResponse,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getLocaleText } from '@hed-hog/api-locale';
|
|
2
|
-
import { IsNotEmpty } from 'class-validator';
|
|
2
|
+
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
|
3
3
|
import { IsEmailWithSettings } from '../../validators/is-email-with-settings.validator';
|
|
4
4
|
|
|
5
5
|
export class ForgetDTO {
|
|
@@ -12,4 +12,8 @@ export class ForgetDTO {
|
|
|
12
12
|
return this._email;
|
|
13
13
|
}
|
|
14
14
|
private _email: string;
|
|
15
|
+
|
|
16
|
+
@IsOptional()
|
|
17
|
+
@IsString()
|
|
18
|
+
app?: string;
|
|
15
19
|
}
|
|
@@ -83,7 +83,10 @@ export class AuthGuard implements CanActivate {
|
|
|
83
83
|
} else {
|
|
84
84
|
const errName = (error as any)?.name ?? 'UnknownError';
|
|
85
85
|
const message = (error as any)?.message || errName || 'Unauthorized';
|
|
86
|
-
|
|
86
|
+
// Token expirado é rotineiro (cliente renova); só alerta para rejeições genuínas.
|
|
87
|
+
if (!/expired/i.test(message)) {
|
|
88
|
+
this.logger.warn(`[AuthGuard] Token rejected ${request.method} ${request.url} error=${errName}: ${message}`);
|
|
89
|
+
}
|
|
87
90
|
throw new UnauthorizedException(message);
|
|
88
91
|
}
|
|
89
92
|
}
|
|
@@ -185,13 +185,12 @@ export class ChallengeService {
|
|
|
185
185
|
return { codeHash };
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
async forgotEmail(locale: string, email: string) {
|
|
189
|
-
const openChallengesCount = await this.prisma.
|
|
188
|
+
async forgotEmail(locale: string, email: string, app?: string) {
|
|
189
|
+
const openChallengesCount = await this.prisma.user_identifier_challenge.count({
|
|
190
190
|
where: {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
191
|
+
user_identifier: {
|
|
192
|
+
type: 'email',
|
|
193
|
+
value: email,
|
|
195
194
|
},
|
|
196
195
|
expires_at: { gt: new Date() }
|
|
197
196
|
}
|
|
@@ -201,27 +200,26 @@ export class ChallengeService {
|
|
|
201
200
|
throw new BadRequestException(getLocaleText('forgot.rateLimit', locale, 'Too many password reset requests. Please try again later.'));
|
|
202
201
|
}
|
|
203
202
|
|
|
204
|
-
const
|
|
205
|
-
where: { email },
|
|
206
|
-
include: {
|
|
207
|
-
user_mfa: { include: { user: true } },
|
|
208
|
-
},
|
|
203
|
+
const userIdentifier = await this.prisma.user_identifier.findFirst({
|
|
204
|
+
where: { type: 'email', value: email, enabled: true },
|
|
205
|
+
include: { user: true },
|
|
209
206
|
});
|
|
210
207
|
|
|
211
|
-
if (!
|
|
208
|
+
if (!userIdentifier) throw new BadRequestException(getLocaleText('forgot.userNotFound', locale, 'User not found'));
|
|
212
209
|
|
|
213
210
|
const settings = await this.setting.getSettingValues([
|
|
214
211
|
'mfa-challenge-expiration-minutes',
|
|
215
|
-
'url'
|
|
212
|
+
'url',
|
|
213
|
+
'app-urls'
|
|
216
214
|
]);
|
|
217
215
|
|
|
218
216
|
const token = await this.token.createOpaqueToken(16);
|
|
219
217
|
const hash = this.security.hashWithPepper(token);
|
|
220
|
-
const challenge = await this.prisma.
|
|
218
|
+
const challenge = await this.prisma.user_identifier_challenge.create({
|
|
221
219
|
data: {
|
|
222
220
|
hash,
|
|
223
221
|
expires_at: new Date(Date.now() + (settings['mfa-challenge-expiration-minutes'] || 15) * 60000),
|
|
224
|
-
|
|
222
|
+
user_identifier_id: userIdentifier.id,
|
|
225
223
|
},
|
|
226
224
|
});
|
|
227
225
|
|
|
@@ -232,8 +230,8 @@ export class ChallengeService {
|
|
|
232
230
|
email,
|
|
233
231
|
slug: 'auth-forget-password',
|
|
234
232
|
variables: {
|
|
235
|
-
link: `${settings
|
|
236
|
-
name:
|
|
233
|
+
link: `${this.resolveAppUrl(settings, app)}/reset-password?token=${tokenBase64}`,
|
|
234
|
+
name: userIdentifier.user.name,
|
|
237
235
|
}
|
|
238
236
|
}
|
|
239
237
|
);
|
|
@@ -241,15 +239,28 @@ export class ChallengeService {
|
|
|
241
239
|
return challenge;
|
|
242
240
|
}
|
|
243
241
|
|
|
242
|
+
private resolveAppUrl(settings: Record<string, any>, app?: string): string {
|
|
243
|
+
const appUrls: string[] = Array.isArray(settings['app-urls']) ? settings['app-urls'] : [];
|
|
244
|
+
|
|
245
|
+
if (app) {
|
|
246
|
+
const entry = appUrls.find((item) => item.split('=')[0]?.trim() === app);
|
|
247
|
+
if (entry) {
|
|
248
|
+
return entry.slice(entry.indexOf('=') + 1).trim();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return settings['url'];
|
|
253
|
+
}
|
|
254
|
+
|
|
244
255
|
async verifyCodePassword(locale: string, code: string) {
|
|
245
256
|
const hash = this.security.hashWithPepper(code);
|
|
246
|
-
const challenge = await this.prisma.
|
|
257
|
+
const challenge = await this.prisma.user_identifier_challenge.findFirst({
|
|
247
258
|
where: {
|
|
248
259
|
hash,
|
|
249
260
|
expires_at: { gt: new Date() }
|
|
250
261
|
},
|
|
251
262
|
include: {
|
|
252
|
-
|
|
263
|
+
user_identifier: { include: { user: true } },
|
|
253
264
|
},
|
|
254
265
|
});
|
|
255
266
|
|
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
import { getLocaleText } from '@hed-hog/api-locale';
|
|
2
|
-
import {
|
|
2
|
+
import { Type } from 'class-transformer';
|
|
3
|
+
import { IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
|
|
3
4
|
|
|
4
5
|
export class UploadFileDTO {
|
|
5
6
|
@IsString({ message: (args) => getLocaleText('validation.stringRequired', args.value) })
|
|
6
7
|
@IsOptional()
|
|
7
8
|
destination?: string;
|
|
9
|
+
|
|
10
|
+
// Optional informational tag for the kind of image being uploaded
|
|
11
|
+
// (e.g. "course-banner", "course-logo", "course-square").
|
|
12
|
+
@IsString()
|
|
13
|
+
@IsOptional()
|
|
14
|
+
imageType?: string;
|
|
15
|
+
|
|
16
|
+
// Target dimensions for a precise resize on upload. When provided, the image
|
|
17
|
+
// is resized to fit these bounds before compression/WebP conversion. Sent via
|
|
18
|
+
// multipart form fields, so they arrive as strings and are coerced to numbers.
|
|
19
|
+
@Type(() => Number)
|
|
20
|
+
@IsInt()
|
|
21
|
+
@Min(1)
|
|
22
|
+
@Max(8000)
|
|
23
|
+
@IsOptional()
|
|
24
|
+
maxWidth?: number;
|
|
25
|
+
|
|
26
|
+
@Type(() => Number)
|
|
27
|
+
@IsInt()
|
|
28
|
+
@Min(1)
|
|
29
|
+
@Max(8000)
|
|
30
|
+
@IsOptional()
|
|
31
|
+
maxHeight?: number;
|
|
8
32
|
}
|
|
@@ -147,10 +147,32 @@ export class FileController {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
@Public()
|
|
151
|
+
@Get('image/:id')
|
|
152
|
+
async openImage(
|
|
153
|
+
@Param('id', ParseIntPipe) id: number,
|
|
154
|
+
@Req() req: Request,
|
|
155
|
+
@Res() res: Response,
|
|
156
|
+
@Locale() locale: string,
|
|
157
|
+
) {
|
|
158
|
+
try {
|
|
159
|
+
await this.fileService.serveImageById(
|
|
160
|
+
locale,
|
|
161
|
+
id,
|
|
162
|
+
req.headers['if-none-match'],
|
|
163
|
+
res,
|
|
164
|
+
);
|
|
165
|
+
} catch (error: any) {
|
|
166
|
+
const html = this.renderDownloadErrorHtml(locale, error);
|
|
167
|
+
res.status(400).set('Content-Type', 'text/html').send(html);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
150
171
|
@Public()
|
|
151
172
|
@Get('open/:token')
|
|
152
173
|
async open(
|
|
153
174
|
@Param('token') token: string,
|
|
175
|
+
@Req() req: Request,
|
|
154
176
|
@Res() res: Response,
|
|
155
177
|
@Locale() locale: string,
|
|
156
178
|
) {
|
|
@@ -161,23 +183,12 @@ export class FileController {
|
|
|
161
183
|
const isNumericId = Number.isInteger(numericId) && String(numericId) === token;
|
|
162
184
|
|
|
163
185
|
if (isNumericId && numericId > 0) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
'Content-Type': mimetype || 'application/octet-stream',
|
|
171
|
-
'Content-Disposition':
|
|
172
|
-
`inline; filename="${filename}"; ` +
|
|
173
|
-
`filename*=UTF-8''${encodedFilename}`,
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
if (typeof size === 'number') {
|
|
177
|
-
res.set('Content-Length', `${size}`);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
stream.pipe(res);
|
|
186
|
+
await this.fileService.serveFileById(
|
|
187
|
+
locale,
|
|
188
|
+
numericId,
|
|
189
|
+
req.headers['if-none-match'],
|
|
190
|
+
res,
|
|
191
|
+
);
|
|
181
192
|
return;
|
|
182
193
|
}
|
|
183
194
|
|
|
@@ -215,7 +226,9 @@ export class FileController {
|
|
|
215
226
|
@Body() body: UploadFileDTO,
|
|
216
227
|
) {
|
|
217
228
|
const destination = body.destination || 'files';
|
|
218
|
-
|
|
229
|
+
const maxWidth = Number(body.maxWidth) || undefined;
|
|
230
|
+
const maxHeight = Number(body.maxHeight) || undefined;
|
|
231
|
+
return this.fileService.upload(destination, file, { maxWidth, maxHeight });
|
|
219
232
|
}
|
|
220
233
|
|
|
221
234
|
@Delete()
|