@hed-hog/core 0.0.370 → 0.0.371
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 +141 -29
- 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 +8 -3
- 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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DeleteDTO, Role } from '@hed-hog/api';
|
|
2
2
|
import { Locale } from '@hed-hog/api-locale';
|
|
3
|
+
import { renderMailTemplate } from '@hed-hog/api-mail';
|
|
3
4
|
import { Pagination } from '@hed-hog/api-pagination';
|
|
4
5
|
import {
|
|
5
6
|
Body,
|
|
@@ -56,35 +57,10 @@ export class MailController {
|
|
|
56
57
|
) {
|
|
57
58
|
const locale = queryLocale || decoratorLocale;
|
|
58
59
|
const mail = await this.mailService.get(locale, id);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<meta charset="UTF-8">
|
|
64
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
65
|
-
<title>${mail.subject}</title>
|
|
66
|
-
<style>
|
|
67
|
-
body {
|
|
68
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
69
|
-
line-height: 1.6;
|
|
70
|
-
color: #333;
|
|
71
|
-
max-width: 600px;
|
|
72
|
-
margin: 0 auto;
|
|
73
|
-
padding: 20px;
|
|
74
|
-
}
|
|
75
|
-
a {
|
|
76
|
-
color: #0066cc;
|
|
77
|
-
text-decoration: none;
|
|
78
|
-
}
|
|
79
|
-
a:hover {
|
|
80
|
-
text-decoration: underline;
|
|
81
|
-
}
|
|
82
|
-
</style>
|
|
83
|
-
</head>
|
|
84
|
-
<body>
|
|
85
|
-
${mail.body}
|
|
86
|
-
</body>
|
|
87
|
-
</html>`;
|
|
60
|
+
const html = renderMailTemplate({
|
|
61
|
+
subject: mail.subject,
|
|
62
|
+
body: mail.body,
|
|
63
|
+
});
|
|
88
64
|
|
|
89
65
|
res.setHeader('Content-Disposition', `attachment; filename="${mail.slug}-${locale}.html"`);
|
|
90
66
|
res.send(html);
|
package/src/mail/mail.module.ts
CHANGED
|
@@ -28,16 +28,16 @@ import { MailService } from './mail.service';
|
|
|
28
28
|
profileSettings.map((item) => [item.slug, item.value]),
|
|
29
29
|
);
|
|
30
30
|
|
|
31
|
-
const
|
|
32
|
-
profileSettingsMap.get('mail-integration-profile-id'),
|
|
33
|
-
);
|
|
31
|
+
const profileSlug = String(
|
|
32
|
+
profileSettingsMap.get('mail-integration-profile-id') ?? '',
|
|
33
|
+
).trim();
|
|
34
34
|
|
|
35
|
-
if (!
|
|
35
|
+
if (!profileSlug) {
|
|
36
36
|
return { global: true, type: 'SMTP', host: 'localhost', port: 1025, secure: false, username: '', password: '' } as MailModuleOptions;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const profile = await (prisma as any).integration_profile.findUnique({
|
|
40
|
-
where: {
|
|
40
|
+
where: { slug: profileSlug },
|
|
41
41
|
include: {
|
|
42
42
|
integration_provider: { select: { slug: true } },
|
|
43
43
|
integration_type: { select: { slug: true } },
|
package/src/mail/mail.service.ts
CHANGED
|
@@ -76,20 +76,22 @@ export class MailService implements OnModuleInit {
|
|
|
76
76
|
const settings = await this.setting.getSettingValues([
|
|
77
77
|
this.integrationProfileSettingSlug,
|
|
78
78
|
]);
|
|
79
|
-
const
|
|
79
|
+
const profileSlug = String(
|
|
80
|
+
settings[this.integrationProfileSettingSlug] ?? '',
|
|
81
|
+
).trim();
|
|
80
82
|
|
|
81
|
-
if (!
|
|
83
|
+
if (!profileSlug) {
|
|
82
84
|
await this.setting.setValue('configured-mail', 'false');
|
|
83
85
|
return;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
const { from
|
|
88
|
+
const { from } = await this.reloadConfig();
|
|
87
89
|
|
|
88
90
|
await this.mailMainService.send({
|
|
89
91
|
to: from,
|
|
90
92
|
from,
|
|
91
93
|
subject: '[HedHog] Mail configuration validation',
|
|
92
|
-
body: `This is an automatic validation email for the configured profile (
|
|
94
|
+
body: `This is an automatic validation email for the configured profile (slug: ${profileSlug}).`,
|
|
93
95
|
});
|
|
94
96
|
|
|
95
97
|
await this.setting.setValue('configured-mail', 'true');
|
|
@@ -103,27 +105,29 @@ export class MailService implements OnModuleInit {
|
|
|
103
105
|
|
|
104
106
|
/**
|
|
105
107
|
* Reload mail configuration from database and update MailMainService.
|
|
106
|
-
* When
|
|
108
|
+
* When profileSlug is provided, that profile is used directly.
|
|
107
109
|
* Otherwise the global integration profile setting is read.
|
|
108
110
|
* Returns the active from address and the built config object.
|
|
109
111
|
*/
|
|
110
|
-
async reloadConfig(
|
|
112
|
+
async reloadConfig(profileSlug?: string): Promise<{ success: boolean; provider: string; from: string; config: any }> {
|
|
111
113
|
try {
|
|
112
|
-
let
|
|
114
|
+
let resolvedProfileSlug = String(profileSlug ?? '').trim();
|
|
113
115
|
|
|
114
|
-
if (!
|
|
116
|
+
if (!resolvedProfileSlug) {
|
|
115
117
|
const settings = await this.setting.getSettingValues([
|
|
116
118
|
this.integrationProfileSettingSlug,
|
|
117
119
|
]);
|
|
118
|
-
|
|
120
|
+
resolvedProfileSlug = String(
|
|
121
|
+
settings[this.integrationProfileSettingSlug] ?? '',
|
|
122
|
+
).trim();
|
|
119
123
|
}
|
|
120
124
|
|
|
121
|
-
if (!
|
|
125
|
+
if (!resolvedProfileSlug) {
|
|
122
126
|
throw new BadRequestException('No mail profile configured. Set a mail profile in the Mail settings.');
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
const profile = await this.prismaService.integration_profile.findUnique({
|
|
126
|
-
where: {
|
|
130
|
+
where: { slug: resolvedProfileSlug },
|
|
127
131
|
include: {
|
|
128
132
|
integration_provider: { select: { slug: true } },
|
|
129
133
|
integration_type: { select: { slug: true } },
|
|
@@ -131,12 +135,12 @@ export class MailService implements OnModuleInit {
|
|
|
131
135
|
});
|
|
132
136
|
|
|
133
137
|
if (!profile) {
|
|
134
|
-
throw new NotFoundException(`Mail profile with
|
|
138
|
+
throw new NotFoundException(`Mail profile with slug "${resolvedProfileSlug}" not found.`);
|
|
135
139
|
}
|
|
136
140
|
|
|
137
141
|
if (profile.integration_type?.slug !== 'email') {
|
|
138
142
|
throw new BadRequestException(
|
|
139
|
-
`Integration profile ${
|
|
143
|
+
`Integration profile "${resolvedProfileSlug}" is not an email profile.`,
|
|
140
144
|
);
|
|
141
145
|
}
|
|
142
146
|
|
|
@@ -155,6 +159,26 @@ export class MailService implements OnModuleInit {
|
|
|
155
159
|
}
|
|
156
160
|
}
|
|
157
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Sends an ad-hoc HTML email using the configured (or given) mail profile,
|
|
164
|
+
* without requiring a stored template. Used by other modules (e.g. agent
|
|
165
|
+
* human-approval notifications).
|
|
166
|
+
*/
|
|
167
|
+
async sendRawMail({
|
|
168
|
+
to,
|
|
169
|
+
subject,
|
|
170
|
+
html,
|
|
171
|
+
profileSlug,
|
|
172
|
+
}: {
|
|
173
|
+
to: string;
|
|
174
|
+
subject: string;
|
|
175
|
+
html: string;
|
|
176
|
+
profileSlug?: string;
|
|
177
|
+
}) {
|
|
178
|
+
const { from } = await this.reloadConfig(profileSlug);
|
|
179
|
+
return this.mailMainService.send({ to, from, subject, body: html } as any);
|
|
180
|
+
}
|
|
181
|
+
|
|
158
182
|
|
|
159
183
|
async list(locale: string, paginationParams: PaginationDTO) {
|
|
160
184
|
const result = await this.localeService.listModelWithLocale(
|
|
@@ -860,28 +860,28 @@ export class McpChatService {
|
|
|
860
860
|
|
|
861
861
|
private async getProviderKey(): Promise<{ provider: 'openai' | 'gemini'; apiKey: string }> {
|
|
862
862
|
const settings = await this.setting.getSettingValues(['mcp-ai-profile-id']);
|
|
863
|
-
const
|
|
863
|
+
const profileSlug = String(settings['mcp-ai-profile-id'] ?? '').trim();
|
|
864
864
|
|
|
865
|
-
if (!
|
|
865
|
+
if (!profileSlug) {
|
|
866
866
|
throw new BadRequestException(
|
|
867
867
|
'No MCP AI profile configured. Select one in Settings → MCP.',
|
|
868
868
|
);
|
|
869
869
|
}
|
|
870
870
|
|
|
871
871
|
const profile = await this.prisma.integration_profile.findUnique({
|
|
872
|
-
where: {
|
|
872
|
+
where: { slug: profileSlug },
|
|
873
873
|
include: { integration_provider: { select: { slug: true } } },
|
|
874
874
|
});
|
|
875
875
|
|
|
876
876
|
if (!profile) {
|
|
877
|
-
throw new BadRequestException(`MCP AI profile ${
|
|
877
|
+
throw new BadRequestException(`MCP AI profile "${profileSlug}" not found.`);
|
|
878
878
|
}
|
|
879
879
|
|
|
880
880
|
const providerSlug = profile.integration_provider.slug;
|
|
881
881
|
const { apiKey } = buildAiConfigFromIntegration(providerSlug, profile.config);
|
|
882
882
|
|
|
883
883
|
if (!apiKey) {
|
|
884
|
-
throw new BadRequestException(`MCP AI profile ${
|
|
884
|
+
throw new BadRequestException(`MCP AI profile "${profileSlug}" has no API key configured.`);
|
|
885
885
|
}
|
|
886
886
|
|
|
887
887
|
return { provider: providerSlug === 'gemini' ? 'gemini' : 'openai', apiKey };
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from '@jest/globals';
|
|
2
|
+
import { OAuthCallbackCoordinatorService } from './oauth-callback-coordinator.service';
|
|
3
|
+
import {
|
|
4
|
+
OAuthCallbackAlreadyProcessedError,
|
|
5
|
+
OAuthCallbackInProgressError,
|
|
6
|
+
OAuthProviderException,
|
|
7
|
+
} from './oauth.errors';
|
|
8
|
+
|
|
9
|
+
const makeService = (prisma: any) => new OAuthCallbackCoordinatorService(prisma as any);
|
|
10
|
+
|
|
11
|
+
const noLockPrisma = (overrides: any = {}) => ({
|
|
12
|
+
isPostgres: jest.fn().mockReturnValue(false),
|
|
13
|
+
isMysql: jest.fn().mockReturnValue(false),
|
|
14
|
+
getProvider: jest.fn().mockReturnValue('sqlite'),
|
|
15
|
+
...overrides,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('OAuthCallbackCoordinatorService', () => {
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
jest.clearAllMocks();
|
|
21
|
+
jest.useRealTimers();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('execute (no distributed lock provider)', () => {
|
|
25
|
+
it('runs the callback and marks the key completed', async () => {
|
|
26
|
+
const service = makeService(noLockPrisma());
|
|
27
|
+
const callback = jest.fn().mockResolvedValue('ok');
|
|
28
|
+
|
|
29
|
+
await expect(service.execute('key-a', callback)).resolves.toBe('ok');
|
|
30
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
31
|
+
expect((service as any).isCompleted('key-a')).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('rejects a second call with the same key as already processed', async () => {
|
|
35
|
+
const service = makeService(noLockPrisma());
|
|
36
|
+
await service.execute('key-b', jest.fn().mockResolvedValue(1));
|
|
37
|
+
|
|
38
|
+
await expect(
|
|
39
|
+
service.execute('key-b', jest.fn().mockResolvedValue(2)),
|
|
40
|
+
).rejects.toThrow(OAuthCallbackAlreadyProcessedError);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('does not mark completed when the callback throws a generic error', async () => {
|
|
44
|
+
const service = makeService(noLockPrisma());
|
|
45
|
+
await expect(
|
|
46
|
+
service.execute('key-c', jest.fn().mockRejectedValue(new Error('boom'))),
|
|
47
|
+
).rejects.toThrow('boom');
|
|
48
|
+
expect((service as any).isCompleted('key-c')).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('marks completed when the callback throws a callback_consumed provider error', async () => {
|
|
52
|
+
const service = makeService(noLockPrisma());
|
|
53
|
+
const err = new OAuthProviderException('google', 'callback_consumed', 'consumed');
|
|
54
|
+
await expect(service.execute('key-d', jest.fn().mockRejectedValue(err))).rejects.toBe(err);
|
|
55
|
+
expect((service as any).isCompleted('key-d')).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('execute (postgres advisory lock)', () => {
|
|
60
|
+
it('acquires the advisory lock and runs the callback', async () => {
|
|
61
|
+
const tx = { $queryRaw: jest.fn().mockResolvedValue([{ locked: true }]) };
|
|
62
|
+
const prisma = {
|
|
63
|
+
isPostgres: jest.fn().mockReturnValue(true),
|
|
64
|
+
isMysql: jest.fn().mockReturnValue(false),
|
|
65
|
+
getProvider: jest.fn().mockReturnValue('postgresql'),
|
|
66
|
+
$transaction: jest.fn(async (cb: any) => cb(tx)),
|
|
67
|
+
};
|
|
68
|
+
const service = makeService(prisma);
|
|
69
|
+
const callback = jest.fn().mockResolvedValue('done');
|
|
70
|
+
|
|
71
|
+
await expect(service.execute('pg-key', callback)).resolves.toBe('done');
|
|
72
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
73
|
+
expect((service as any).isCompleted('pg-key')).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('throws InProgress and does not run the callback when the lock is held', async () => {
|
|
77
|
+
const tx = { $queryRaw: jest.fn().mockResolvedValue([{ locked: false }]) };
|
|
78
|
+
const prisma = {
|
|
79
|
+
isPostgres: jest.fn().mockReturnValue(true),
|
|
80
|
+
isMysql: jest.fn().mockReturnValue(false),
|
|
81
|
+
getProvider: jest.fn().mockReturnValue('postgresql'),
|
|
82
|
+
$transaction: jest.fn(async (cb: any) => cb(tx)),
|
|
83
|
+
};
|
|
84
|
+
const service = makeService(prisma);
|
|
85
|
+
const callback = jest.fn();
|
|
86
|
+
|
|
87
|
+
await expect(service.execute('pg-busy', callback)).rejects.toThrow(OAuthCallbackInProgressError);
|
|
88
|
+
expect(callback).not.toHaveBeenCalled();
|
|
89
|
+
expect((service as any).isCompleted('pg-busy')).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('completed-callback bookkeeping', () => {
|
|
94
|
+
it('expires completed entries after the TTL window', () => {
|
|
95
|
+
jest.useFakeTimers();
|
|
96
|
+
jest.setSystemTime(new Date('2026-01-01T00:00:00.000Z'));
|
|
97
|
+
const service = makeService(noLockPrisma());
|
|
98
|
+
|
|
99
|
+
(service as any).markCompleted('ttl-key');
|
|
100
|
+
expect((service as any).isCompleted('ttl-key')).toBe(true);
|
|
101
|
+
|
|
102
|
+
// Advance beyond the 10 minute TTL.
|
|
103
|
+
jest.setSystemTime(new Date('2026-01-01T00:11:00.000Z'));
|
|
104
|
+
expect((service as any).isCompleted('ttl-key')).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('prunes expired entries from the internal map', () => {
|
|
108
|
+
jest.useFakeTimers();
|
|
109
|
+
jest.setSystemTime(new Date('2026-01-01T00:00:00.000Z'));
|
|
110
|
+
const service = makeService(noLockPrisma());
|
|
111
|
+
(service as any).markCompleted('old');
|
|
112
|
+
|
|
113
|
+
jest.setSystemTime(new Date('2026-01-01T00:11:00.000Z'));
|
|
114
|
+
(service as any).markCompleted('fresh');
|
|
115
|
+
(service as any).pruneCompletedCallbacks();
|
|
116
|
+
|
|
117
|
+
expect((service as any).completedCallbacks.has('old')).toBe(false);
|
|
118
|
+
expect((service as any).completedCallbacks.has('fresh')).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('shouldMarkCompleted', () => {
|
|
123
|
+
it('is true for already-processed and callback_consumed errors only', () => {
|
|
124
|
+
const service = makeService(noLockPrisma());
|
|
125
|
+
expect((service as any).shouldMarkCompleted(new OAuthCallbackAlreadyProcessedError())).toBe(true);
|
|
126
|
+
expect(
|
|
127
|
+
(service as any).shouldMarkCompleted(
|
|
128
|
+
new OAuthProviderException('g', 'callback_consumed', 'x'),
|
|
129
|
+
),
|
|
130
|
+
).toBe(true);
|
|
131
|
+
expect(
|
|
132
|
+
(service as any).shouldMarkCompleted(
|
|
133
|
+
new OAuthProviderException('g', 'upstream_failure', 'x'),
|
|
134
|
+
),
|
|
135
|
+
).toBe(false);
|
|
136
|
+
expect((service as any).shouldMarkCompleted(new Error('other'))).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('lock key derivation', () => {
|
|
141
|
+
it('derives a deterministic signed bigint within int64 bounds', () => {
|
|
142
|
+
const service = makeService(noLockPrisma());
|
|
143
|
+
const a = (service as any).toSignedBigInt('same');
|
|
144
|
+
const b = (service as any).toSignedBigInt('same');
|
|
145
|
+
expect(a).toBe(b);
|
|
146
|
+
expect(typeof a).toBe('bigint');
|
|
147
|
+
expect(a >= -(BigInt(1) << BigInt(63))).toBe(true);
|
|
148
|
+
expect(a < BigInt(1) << BigInt(63)).toBe(true);
|
|
149
|
+
expect((service as any).toSignedBigInt('other')).not.toBe(a);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('derives a prefixed 48-char mysql lock name', () => {
|
|
153
|
+
const service = makeService(noLockPrisma());
|
|
154
|
+
const name = (service as any).toMysqlLockName('key');
|
|
155
|
+
expect(name.startsWith('oauth:')).toBe(true);
|
|
156
|
+
expect(name.slice('oauth:'.length)).toHaveLength(48);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -26,11 +26,11 @@ export class FacebookProvider extends BaseOAuthProvider {
|
|
|
26
26
|
'oauth-facebook-profile-id',
|
|
27
27
|
'url',
|
|
28
28
|
]);
|
|
29
|
-
const
|
|
30
|
-
if (!
|
|
29
|
+
const profileSlug = String(settings['oauth-facebook-profile-id'] ?? '').trim();
|
|
30
|
+
if (!profileSlug) throw new BadRequestException('Facebook OAuth profile not configured.');
|
|
31
31
|
|
|
32
32
|
const profile = await this.prisma.integration_profile.findUnique({
|
|
33
|
-
where: {
|
|
33
|
+
where: { slug: profileSlug },
|
|
34
34
|
include: { integration_provider: { select: { slug: true } } },
|
|
35
35
|
});
|
|
36
36
|
if (!profile) throw new BadRequestException('Facebook OAuth profile not found.');
|
|
@@ -30,11 +30,11 @@ export class GithubProvider extends BaseOAuthProvider {
|
|
|
30
30
|
'oauth-github-profile-id',
|
|
31
31
|
'api-url',
|
|
32
32
|
]);
|
|
33
|
-
const
|
|
34
|
-
if (!
|
|
33
|
+
const profileSlug = String(settings['oauth-github-profile-id'] ?? '').trim();
|
|
34
|
+
if (!profileSlug) throw new BadRequestException('GitHub OAuth profile not configured.');
|
|
35
35
|
|
|
36
36
|
const profile = await this.prisma.integration_profile.findUnique({
|
|
37
|
-
where: {
|
|
37
|
+
where: { slug: profileSlug },
|
|
38
38
|
include: { integration_provider: { select: { slug: true } } },
|
|
39
39
|
});
|
|
40
40
|
if (!profile) throw new BadRequestException('GitHub OAuth profile not found.');
|
|
@@ -26,11 +26,11 @@ export class GoogleProvider extends BaseOAuthProvider {
|
|
|
26
26
|
'oauth-google-profile-id',
|
|
27
27
|
'url',
|
|
28
28
|
]);
|
|
29
|
-
const
|
|
30
|
-
if (!
|
|
29
|
+
const profileSlug = String(settings['oauth-google-profile-id'] ?? '').trim();
|
|
30
|
+
if (!profileSlug) throw new BadRequestException('Google OAuth profile not configured.');
|
|
31
31
|
|
|
32
32
|
const profile = await this.prisma.integration_profile.findUnique({
|
|
33
|
-
where: {
|
|
33
|
+
where: { slug: profileSlug },
|
|
34
34
|
include: { integration_provider: { select: { slug: true } } },
|
|
35
35
|
});
|
|
36
36
|
if (!profile) throw new BadRequestException('Google OAuth profile not found.');
|
|
@@ -40,11 +40,11 @@ export class MicrosoftEntraIdProvider extends BaseOAuthProvider {
|
|
|
40
40
|
'oauth-microsoft-entra-id-profile-id',
|
|
41
41
|
'url',
|
|
42
42
|
]);
|
|
43
|
-
const
|
|
44
|
-
if (!
|
|
43
|
+
const profileSlug = String(settings['oauth-microsoft-entra-id-profile-id'] ?? '').trim();
|
|
44
|
+
if (!profileSlug) throw new BadRequestException('Microsoft Entra ID OAuth profile not configured.');
|
|
45
45
|
|
|
46
46
|
const profile = await this.prisma.integration_profile.findUnique({
|
|
47
|
-
where: {
|
|
47
|
+
where: { slug: profileSlug },
|
|
48
48
|
include: { integration_provider: { select: { slug: true } } },
|
|
49
49
|
});
|
|
50
50
|
if (!profile) throw new BadRequestException('Microsoft Entra ID OAuth profile not found.');
|
|
@@ -26,11 +26,11 @@ export class MicrosoftProvider extends BaseOAuthProvider {
|
|
|
26
26
|
'oauth-microsoft-profile-id',
|
|
27
27
|
'url',
|
|
28
28
|
]);
|
|
29
|
-
const
|
|
30
|
-
if (!
|
|
29
|
+
const profileSlug = String(settings['oauth-microsoft-profile-id'] ?? '').trim();
|
|
30
|
+
if (!profileSlug) throw new BadRequestException('Microsoft OAuth profile not configured.');
|
|
31
31
|
|
|
32
32
|
const profile = await this.prisma.integration_profile.findUnique({
|
|
33
|
-
where: {
|
|
33
|
+
where: { slug: profileSlug },
|
|
34
34
|
include: { integration_provider: { select: { slug: true } } },
|
|
35
35
|
});
|
|
36
36
|
if (!profile) throw new BadRequestException('Microsoft OAuth profile not found.');
|
package/src/pdf/pdf.service.ts
CHANGED
|
@@ -15,6 +15,10 @@ export interface PdfGenerateOptions {
|
|
|
15
15
|
margin?: { top?: string; right?: string; bottom?: string; left?: string };
|
|
16
16
|
printBackground?: boolean;
|
|
17
17
|
landscape?: boolean;
|
|
18
|
+
/** Explicit CSS page size (e.g. '1600px'). Takes precedence over `format`/`landscape` when set. */
|
|
19
|
+
width?: string;
|
|
20
|
+
/** Explicit CSS page size (e.g. '1131px'). Takes precedence over `format`/`landscape` when set. */
|
|
21
|
+
height?: string;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -161,12 +165,21 @@ export class PdfService {
|
|
|
161
165
|
await page.setViewportSize({ width: 794, height: 1123 });
|
|
162
166
|
await page.setContent(html, { waitUntil: 'networkidle' });
|
|
163
167
|
|
|
164
|
-
const pdfBuffer = await page.pdf(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
const pdfBuffer = await page.pdf(
|
|
169
|
+
options.width && options.height
|
|
170
|
+
? {
|
|
171
|
+
width: options.width,
|
|
172
|
+
height: options.height,
|
|
173
|
+
printBackground: options.printBackground ?? true,
|
|
174
|
+
margin: options.margin ?? { top: '0', right: '0', bottom: '0', left: '0' },
|
|
175
|
+
}
|
|
176
|
+
: {
|
|
177
|
+
format: options.format ?? 'A4',
|
|
178
|
+
printBackground: options.printBackground ?? true,
|
|
179
|
+
landscape: options.landscape ?? false,
|
|
180
|
+
margin: options.margin ?? { top: '0', right: '0', bottom: '0', left: '0' },
|
|
181
|
+
},
|
|
182
|
+
);
|
|
170
183
|
|
|
171
184
|
return Buffer.from(pdfBuffer);
|
|
172
185
|
} catch (error) {
|
|
@@ -44,9 +44,10 @@ export class ProfileController {
|
|
|
44
44
|
async verifyBeforeAddMfa(
|
|
45
45
|
@User() { id },
|
|
46
46
|
@Body() data: VerifyBeforeAddMfaDto,
|
|
47
|
-
@Locale() locale: string
|
|
47
|
+
@Locale() locale: string,
|
|
48
|
+
@Headers('origin') origin: string
|
|
48
49
|
) {
|
|
49
|
-
return this.profileService.verifyBeforeAddMfa(locale, id, data);
|
|
50
|
+
return this.profileService.verifyBeforeAddMfa(locale, id, data, origin);
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
@Post('totp/generate')
|
|
@@ -130,9 +131,10 @@ export class ProfileController {
|
|
|
130
131
|
@User() { id },
|
|
131
132
|
@Param('mfaId', ParseIntPipe) mfaId: number,
|
|
132
133
|
@Body() data: RemoveMfaDto,
|
|
133
|
-
@Locale() locale: string
|
|
134
|
+
@Locale() locale: string,
|
|
135
|
+
@Headers('origin') origin: string
|
|
134
136
|
) {
|
|
135
|
-
return this.profileService.removeMfa(locale, id, mfaId, data);
|
|
137
|
+
return this.profileService.removeMfa(locale, id, mfaId, data, origin);
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
@Post('recovery-codes/send-verification')
|
|
@@ -147,43 +149,48 @@ export class ProfileController {
|
|
|
147
149
|
async regenerateRecoveryCodes(
|
|
148
150
|
@User() { id },
|
|
149
151
|
@Body() data: RegenerateRecoveryCodesDto,
|
|
150
|
-
@Locale() locale: string
|
|
152
|
+
@Locale() locale: string,
|
|
153
|
+
@Headers('origin') origin: string
|
|
151
154
|
) {
|
|
152
|
-
return this.profileService.regenerateRecoveryCodes(locale, id, data);
|
|
155
|
+
return this.profileService.regenerateRecoveryCodes(locale, id, data, origin);
|
|
153
156
|
}
|
|
154
157
|
|
|
155
158
|
@Post('webauthn/generate')
|
|
156
159
|
async generateWebAuthnRegistrationOptions(
|
|
157
160
|
@User() { id },
|
|
158
161
|
@Body() { name }: GenerateWebAuthnRegistrationOptionsDto,
|
|
159
|
-
@Locale() locale: string
|
|
162
|
+
@Locale() locale: string,
|
|
163
|
+
@Headers('origin') origin: string
|
|
160
164
|
) {
|
|
161
|
-
return this.profileService.generateWebAuthnRegistrationOptions(locale, id, name);
|
|
165
|
+
return this.profileService.generateWebAuthnRegistrationOptions(locale, id, name, origin);
|
|
162
166
|
}
|
|
163
167
|
|
|
164
168
|
@Post('webauthn/verify')
|
|
165
169
|
async verifyWebAuthnRegistration(
|
|
166
170
|
@User() { id },
|
|
167
171
|
@Body() { name, attestationResponse }: VerifyWebAuthnRegistrationDto,
|
|
168
|
-
@Locale() locale: string
|
|
172
|
+
@Locale() locale: string,
|
|
173
|
+
@Headers('origin') origin: string
|
|
169
174
|
) {
|
|
170
|
-
return this.profileService.verifyWebAuthnRegistration(locale, id, name, attestationResponse);
|
|
175
|
+
return this.profileService.verifyWebAuthnRegistration(locale, id, name, attestationResponse, origin);
|
|
171
176
|
}
|
|
172
177
|
|
|
173
178
|
@Post('webauthn/authenticate/generate')
|
|
174
179
|
async generateWebAuthnAuthenticationOptions(
|
|
175
180
|
@User() { id },
|
|
176
|
-
@Locale() locale: string
|
|
181
|
+
@Locale() locale: string,
|
|
182
|
+
@Headers('origin') origin: string
|
|
177
183
|
) {
|
|
178
|
-
return this.profileService.generateWebAuthnAuthenticationOptions(locale, id);
|
|
184
|
+
return this.profileService.generateWebAuthnAuthenticationOptions(locale, id, origin);
|
|
179
185
|
}
|
|
180
186
|
|
|
181
187
|
@Post('webauthn/authenticate/verify')
|
|
182
188
|
async verifyWebAuthnAuthentication(
|
|
183
189
|
@User() { id },
|
|
184
|
-
@Body() { assertionResponse }: VerifyWebAuthnDto
|
|
190
|
+
@Body() { assertionResponse }: VerifyWebAuthnDto,
|
|
191
|
+
@Headers('origin') origin: string
|
|
185
192
|
) {
|
|
186
|
-
return this.profileService.verifyWebAuthnAuthentication(id, assertionResponse);
|
|
193
|
+
return this.profileService.verifyWebAuthnAuthentication(id, assertionResponse, origin);
|
|
187
194
|
}
|
|
188
195
|
|
|
189
196
|
@Delete('webauthn/:mfaId/remove')
|