@hed-hog/core 0.0.356 → 0.0.359

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/dist/auth/guards/auth.guard.d.ts +6 -1
  2. package/dist/auth/guards/auth.guard.d.ts.map +1 -1
  3. package/dist/auth/guards/auth.guard.js +33 -2
  4. package/dist/auth/guards/auth.guard.js.map +1 -1
  5. package/dist/file/file.controller.d.ts +4 -2
  6. package/dist/file/file.controller.d.ts.map +1 -1
  7. package/dist/file/file.controller.js +44 -38
  8. package/dist/file/file.controller.js.map +1 -1
  9. package/dist/file/file.service.d.ts +2 -2
  10. package/dist/file/file.service.d.ts.map +1 -1
  11. package/dist/file/file.service.js +25 -42
  12. package/dist/file/file.service.js.map +1 -1
  13. package/dist/file/provider/azure.provider.d.ts.map +1 -1
  14. package/dist/file/provider/azure.provider.js +10 -15
  15. package/dist/file/provider/azure.provider.js.map +1 -1
  16. package/dist/file/provider/gcs.provider.d.ts.map +1 -1
  17. package/dist/file/provider/gcs.provider.js +8 -9
  18. package/dist/file/provider/gcs.provider.js.map +1 -1
  19. package/dist/file/provider/local.provider.d.ts.map +1 -1
  20. package/dist/file/provider/local.provider.js +11 -10
  21. package/dist/file/provider/local.provider.js.map +1 -1
  22. package/dist/file/provider/provider.factory.d.ts +1 -1
  23. package/dist/file/provider/provider.factory.d.ts.map +1 -1
  24. package/dist/file/provider/provider.factory.js +16 -6
  25. package/dist/file/provider/provider.factory.js.map +1 -1
  26. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  27. package/dist/file/provider/s3.provider.js +4 -20
  28. package/dist/file/provider/s3.provider.js.map +1 -1
  29. package/dist/file/provider/spaces.provider.d.ts.map +1 -1
  30. package/dist/file/provider/spaces.provider.js +4 -19
  31. package/dist/file/provider/spaces.provider.js.map +1 -1
  32. package/dist/integration-profile/integration-profile.controller.d.ts +1 -1
  33. package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
  34. package/dist/integration-profile/integration-profile.controller.js +4 -3
  35. package/dist/integration-profile/integration-profile.controller.js.map +1 -1
  36. package/dist/integration-profile/integration-profile.service.d.ts +1 -1
  37. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
  38. package/dist/integration-profile/integration-profile.service.js +7 -4
  39. package/dist/integration-profile/integration-profile.service.js.map +1 -1
  40. package/dist/integration-profile/integration-profile.utils.d.ts +6 -0
  41. package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
  42. package/dist/integration-profile/integration-profile.utils.js +9 -0
  43. package/dist/integration-profile/integration-profile.utils.js.map +1 -1
  44. package/dist/mail/mail.module.d.ts.map +1 -1
  45. package/dist/mail/mail.module.js +9 -4
  46. package/dist/mail/mail.module.js.map +1 -1
  47. package/dist/mail/mail.service.d.ts +2 -1
  48. package/dist/mail/mail.service.d.ts.map +1 -1
  49. package/dist/mail/mail.service.js +11 -7
  50. package/dist/mail/mail.service.js.map +1 -1
  51. package/dist/mcp/guards/mcp-auth.guard.js +1 -1
  52. package/dist/mcp/guards/mcp-auth.guard.js.map +1 -1
  53. package/dist/mcp/mcp-api-key.controller.d.ts +2 -0
  54. package/dist/mcp/mcp-api-key.controller.d.ts.map +1 -1
  55. package/dist/mcp/mcp-api-key.controller.js +1 -1
  56. package/dist/mcp/mcp-api-key.controller.js.map +1 -1
  57. package/dist/mcp/mcp-api-key.service.d.ts +2 -1
  58. package/dist/mcp/mcp-api-key.service.d.ts.map +1 -1
  59. package/dist/mcp/mcp-api-key.service.js +6 -2
  60. package/dist/mcp/mcp-api-key.service.js.map +1 -1
  61. package/dist/mcp/mcp.module.d.ts.map +1 -1
  62. package/dist/mcp/mcp.module.js +1 -3
  63. package/dist/mcp/mcp.module.js.map +1 -1
  64. package/dist/oauth/interfaces/OAuthProvider.d.ts +1 -1
  65. package/dist/oauth/interfaces/OAuthProvider.d.ts.map +1 -1
  66. package/dist/oauth/oauth.controller.d.ts +4 -1
  67. package/dist/oauth/oauth.controller.d.ts.map +1 -1
  68. package/dist/oauth/oauth.controller.js +31 -5
  69. package/dist/oauth/oauth.controller.js.map +1 -1
  70. package/dist/oauth/oauth.service.d.ts +12 -1
  71. package/dist/oauth/oauth.service.d.ts.map +1 -1
  72. package/dist/oauth/oauth.service.js +125 -23
  73. package/dist/oauth/oauth.service.js.map +1 -1
  74. package/dist/oauth/providers/abstract.provider.d.ts +1 -1
  75. package/dist/oauth/providers/abstract.provider.d.ts.map +1 -1
  76. package/dist/oauth/providers/facebook.provider.d.ts +5 -2
  77. package/dist/oauth/providers/facebook.provider.d.ts.map +1 -1
  78. package/dist/oauth/providers/facebook.provider.js +34 -19
  79. package/dist/oauth/providers/facebook.provider.js.map +1 -1
  80. package/dist/oauth/providers/github.provider.d.ts +6 -2
  81. package/dist/oauth/providers/github.provider.d.ts.map +1 -1
  82. package/dist/oauth/providers/github.provider.js +43 -20
  83. package/dist/oauth/providers/github.provider.js.map +1 -1
  84. package/dist/oauth/providers/google.provider.d.ts +5 -2
  85. package/dist/oauth/providers/google.provider.d.ts.map +1 -1
  86. package/dist/oauth/providers/google.provider.js +34 -19
  87. package/dist/oauth/providers/google.provider.js.map +1 -1
  88. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +5 -2
  89. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
  90. package/dist/oauth/providers/microsoft-entra-id.provider.js +43 -40
  91. package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
  92. package/dist/oauth/providers/microsoft.provider.d.ts +5 -2
  93. package/dist/oauth/providers/microsoft.provider.d.ts.map +1 -1
  94. package/dist/oauth/providers/microsoft.provider.js +34 -19
  95. package/dist/oauth/providers/microsoft.provider.js.map +1 -1
  96. package/dist/profile/profile.controller.js +1 -1
  97. package/dist/profile/profile.controller.js.map +1 -1
  98. package/dist/security/security.service.d.ts +2 -0
  99. package/dist/security/security.service.d.ts.map +1 -1
  100. package/dist/security/security.service.js +21 -0
  101. package/dist/security/security.service.js.map +1 -1
  102. package/dist/setting/setting.service.d.ts +26 -1
  103. package/dist/setting/setting.service.d.ts.map +1 -1
  104. package/dist/setting/setting.service.js +240 -5
  105. package/dist/setting/setting.service.js.map +1 -1
  106. package/dist/user/user.controller.d.ts +2 -1
  107. package/dist/user/user.controller.d.ts.map +1 -1
  108. package/dist/user/user.controller.js +5 -4
  109. package/dist/user/user.controller.js.map +1 -1
  110. package/dist/user/user.service.d.ts +4 -2
  111. package/dist/user/user.service.d.ts.map +1 -1
  112. package/dist/user/user.service.js +19 -4
  113. package/dist/user/user.service.js.map +1 -1
  114. package/dist/webhook-integration/dto/webhook-action.dto.d.ts +17 -2
  115. package/dist/webhook-integration/dto/webhook-action.dto.d.ts.map +1 -1
  116. package/dist/webhook-integration/dto/webhook-action.dto.js +93 -2
  117. package/dist/webhook-integration/dto/webhook-action.dto.js.map +1 -1
  118. package/dist/webhook-integration/webhook-command.registry.d.ts +15 -0
  119. package/dist/webhook-integration/webhook-command.registry.d.ts.map +1 -0
  120. package/dist/webhook-integration/webhook-command.registry.js +36 -0
  121. package/dist/webhook-integration/webhook-command.registry.js.map +1 -0
  122. package/dist/webhook-integration/webhook-integration.controller.d.ts +19 -0
  123. package/dist/webhook-integration/webhook-integration.controller.d.ts.map +1 -1
  124. package/dist/webhook-integration/webhook-integration.controller.js +38 -0
  125. package/dist/webhook-integration/webhook-integration.controller.js.map +1 -1
  126. package/dist/webhook-integration/webhook-integration.module.d.ts.map +1 -1
  127. package/dist/webhook-integration/webhook-integration.module.js +5 -0
  128. package/dist/webhook-integration/webhook-integration.module.js.map +1 -1
  129. package/dist/webhook-integration/webhook-integration.service.d.ts +25 -1
  130. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  131. package/dist/webhook-integration/webhook-integration.service.js +332 -23
  132. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  133. package/hedhog/data/integration_provider.yaml +16 -0
  134. package/hedhog/data/menu.yaml +0 -16
  135. package/hedhog/data/route.yaml +32 -383
  136. package/hedhog/data/setting_group.yaml +179 -174
  137. package/hedhog/data/setting_subgroup.yaml +16 -0
  138. package/hedhog/frontend/app/account/components/tokens.tsx.ejs +421 -0
  139. package/hedhog/frontend/app/account/layout.tsx.ejs +6 -3
  140. package/hedhog/frontend/app/account/mcp-tokens/page.tsx.ejs +2 -2
  141. package/hedhog/frontend/app/account/tokens/page.tsx.ejs +5 -0
  142. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +414 -76
  143. package/hedhog/frontend/app/configurations/layout.tsx.ejs +1 -1
  144. package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +3 -1
  145. package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +186 -18
  146. package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +597 -21
  147. package/hedhog/frontend/app/mail/profiles/page.tsx.ejs +2 -2
  148. package/hedhog/frontend/app/setting-list/page.tsx.ejs +229 -395
  149. package/hedhog/frontend/messages/en.json +67 -3
  150. package/hedhog/frontend/messages/pt.json +67 -3
  151. package/hedhog/table/file.yaml +1 -1
  152. package/hedhog/table/mcp_api_key.yaml +4 -0
  153. package/hedhog/table/webhook_action.yaml +57 -1
  154. package/package.json +5 -4
  155. package/src/auth/guards/auth.guard.ts +32 -0
  156. package/src/file/file.controller.ts +51 -42
  157. package/src/file/file.service.ts +31 -52
  158. package/src/file/provider/azure.provider.ts +14 -28
  159. package/src/file/provider/gcs.provider.ts +9 -9
  160. package/src/file/provider/local.provider.ts +10 -13
  161. package/src/file/provider/provider.factory.ts +19 -7
  162. package/src/file/provider/s3.provider.ts +4 -20
  163. package/src/file/provider/spaces.provider.ts +4 -19
  164. package/src/integration-profile/integration-profile.controller.ts +2 -0
  165. package/src/integration-profile/integration-profile.service.ts +7 -2
  166. package/src/integration-profile/integration-profile.utils.ts +17 -0
  167. package/src/mail/mail.module.ts +14 -4
  168. package/src/mail/mail.service.ts +12 -7
  169. package/src/mcp/guards/mcp-auth.guard.ts +1 -1
  170. package/src/mcp/mcp-api-key.controller.ts +2 -2
  171. package/src/mcp/mcp-api-key.service.ts +6 -2
  172. package/src/mcp/mcp.module.ts +1 -3
  173. package/src/oauth/interfaces/OAuthProvider.ts +1 -1
  174. package/src/oauth/oauth.controller.ts +30 -2
  175. package/src/oauth/oauth.service.ts +194 -35
  176. package/src/oauth/providers/abstract.provider.ts +1 -1
  177. package/src/oauth/providers/facebook.provider.ts +36 -20
  178. package/src/oauth/providers/github.provider.ts +46 -23
  179. package/src/oauth/providers/google.provider.ts +36 -20
  180. package/src/oauth/providers/microsoft-entra-id.provider.ts +42 -46
  181. package/src/oauth/providers/microsoft.provider.ts +36 -21
  182. package/src/profile/profile.controller.ts +2 -2
  183. package/src/security/security.service.ts +34 -1
  184. package/src/setting/setting.service.ts +307 -7
  185. package/src/user/user.controller.ts +4 -1
  186. package/src/user/user.service.ts +20 -2
  187. package/src/webhook-integration/dto/webhook-action.dto.ts +85 -4
  188. package/src/webhook-integration/webhook-command.registry.ts +42 -0
  189. package/src/webhook-integration/webhook-integration.controller.ts +20 -0
  190. package/src/webhook-integration/webhook-integration.module.ts +50 -45
  191. package/src/webhook-integration/webhook-integration.service.ts +374 -11
  192. package/dist/mail-profile/dto/create.dto.d.ts +0 -23
  193. package/dist/mail-profile/dto/create.dto.d.ts.map +0 -1
  194. package/dist/mail-profile/dto/create.dto.js +0 -139
  195. package/dist/mail-profile/dto/create.dto.js.map +0 -1
  196. package/dist/mail-profile/dto/update.dto.d.ts +0 -6
  197. package/dist/mail-profile/dto/update.dto.d.ts.map +0 -1
  198. package/dist/mail-profile/dto/update.dto.js +0 -9
  199. package/dist/mail-profile/dto/update.dto.js.map +0 -1
  200. package/dist/mail-profile/mail-profile.controller.d.ts +0 -94
  201. package/dist/mail-profile/mail-profile.controller.d.ts.map +0 -1
  202. package/dist/mail-profile/mail-profile.controller.js +0 -95
  203. package/dist/mail-profile/mail-profile.controller.js.map +0 -1
  204. package/dist/mail-profile/mail-profile.module.d.ts +0 -3
  205. package/dist/mail-profile/mail-profile.module.d.ts.map +0 -1
  206. package/dist/mail-profile/mail-profile.module.js +0 -31
  207. package/dist/mail-profile/mail-profile.module.js.map +0 -1
  208. package/dist/mail-profile/mail-profile.service.d.ts +0 -109
  209. package/dist/mail-profile/mail-profile.service.d.ts.map +0 -1
  210. package/dist/mail-profile/mail-profile.service.js +0 -278
  211. package/dist/mail-profile/mail-profile.service.js.map +0 -1
  212. package/dist/mcp/tools/core-mail-profile.mcp-tools.d.ts +0 -20
  213. package/dist/mcp/tools/core-mail-profile.mcp-tools.d.ts.map +0 -1
  214. package/dist/mcp/tools/core-mail-profile.mcp-tools.js +0 -96
  215. package/dist/mcp/tools/core-mail-profile.mcp-tools.js.map +0 -1
  216. package/dist/whatsapp-profile/dto/create.dto.d.ts +0 -11
  217. package/dist/whatsapp-profile/dto/create.dto.d.ts.map +0 -1
  218. package/dist/whatsapp-profile/dto/create.dto.js +0 -55
  219. package/dist/whatsapp-profile/dto/create.dto.js.map +0 -1
  220. package/dist/whatsapp-profile/dto/update.dto.d.ts +0 -6
  221. package/dist/whatsapp-profile/dto/update.dto.d.ts.map +0 -1
  222. package/dist/whatsapp-profile/dto/update.dto.js +0 -9
  223. package/dist/whatsapp-profile/dto/update.dto.js.map +0 -1
  224. package/dist/whatsapp-profile/whatsapp-profile.controller.d.ts +0 -54
  225. package/dist/whatsapp-profile/whatsapp-profile.controller.d.ts.map +0 -1
  226. package/dist/whatsapp-profile/whatsapp-profile.controller.js +0 -85
  227. package/dist/whatsapp-profile/whatsapp-profile.controller.js.map +0 -1
  228. package/dist/whatsapp-profile/whatsapp-profile.module.d.ts +0 -3
  229. package/dist/whatsapp-profile/whatsapp-profile.module.d.ts.map +0 -1
  230. package/dist/whatsapp-profile/whatsapp-profile.module.js +0 -26
  231. package/dist/whatsapp-profile/whatsapp-profile.module.js.map +0 -1
  232. package/dist/whatsapp-profile/whatsapp-profile.service.d.ts +0 -63
  233. package/dist/whatsapp-profile/whatsapp-profile.service.d.ts.map +0 -1
  234. package/dist/whatsapp-profile/whatsapp-profile.service.js +0 -138
  235. package/dist/whatsapp-profile/whatsapp-profile.service.js.map +0 -1
  236. package/hedhog/data/mail_profile.yaml +0 -18
  237. package/hedhog/data/whatsapp_profile.yaml +0 -6
  238. package/hedhog/table/mail_profile.yaml +0 -75
  239. package/hedhog/table/whatsapp_profile.yaml +0 -26
  240. package/src/mail-profile/dto/create.dto.ts +0 -145
  241. package/src/mail-profile/dto/update.dto.ts +0 -4
  242. package/src/mail-profile/mail-profile.controller.ts +0 -56
  243. package/src/mail-profile/mail-profile.module.ts +0 -18
  244. package/src/mail-profile/mail-profile.service.ts +0 -308
  245. package/src/mcp/tools/core-mail-profile.mcp-tools.ts +0 -69
  246. package/src/whatsapp-profile/dto/create.dto.ts +0 -36
  247. package/src/whatsapp-profile/dto/update.dto.ts +0 -4
  248. package/src/whatsapp-profile/whatsapp-profile.controller.ts +0 -51
  249. package/src/whatsapp-profile/whatsapp-profile.module.ts +0 -13
  250. package/src/whatsapp-profile/whatsapp-profile.service.ts +0 -160
@@ -1,16 +1,22 @@
1
+ import { PrismaService } from '@hed-hog/api-prisma';
1
2
  import { HttpService } from '@nestjs/axios';
2
- import { forwardRef, Inject, Injectable } from '@nestjs/common';
3
+ import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common';
4
+ import { buildOAuthConfigFromIntegration } from '../../integration-profile/integration-profile.utils';
3
5
  import { SettingService } from '../../setting/setting.service';
4
6
  import { BaseOAuthProvider } from './abstract.provider';
5
7
 
6
8
  @Injectable()
7
- export class GithubProvider
8
- extends BaseOAuthProvider {
9
+ export class GithubProvider extends BaseOAuthProvider {
10
+
11
+ private isAbsoluteCallbackPath(value: string) {
12
+ return /^[a-z][a-z\d+.-]*:/i.test(String(value || '').trim());
13
+ }
9
14
 
10
15
  constructor(
11
16
  http: HttpService,
12
17
  @Inject(forwardRef(() => SettingService))
13
18
  private readonly setting: SettingService,
19
+ private readonly prisma: PrismaService,
14
20
  ) {
15
21
  super(http);
16
22
  }
@@ -19,21 +25,41 @@ export class GithubProvider
19
25
  return 'GITHUB';
20
26
  }
21
27
 
22
- async getAuthUrl(callbackPath: string) {
23
-
28
+ private async loadConfig() {
24
29
  const settings = await this.setting.getSettingValues([
25
- 'github_client_id',
26
- 'github_client_secret',
27
- 'github_scopes',
30
+ 'oauth-github-profile-id',
28
31
  'api-url',
29
32
  ]);
33
+ const profileId = Number(settings['oauth-github-profile-id']);
34
+ if (!profileId) throw new BadRequestException('GitHub OAuth profile not configured.');
35
+
36
+ const profile = await this.prisma.integration_profile.findUnique({
37
+ where: { id: profileId },
38
+ include: { integration_provider: { select: { slug: true } } },
39
+ });
40
+ if (!profile) throw new BadRequestException('GitHub OAuth profile not found.');
41
+
42
+ const { clientId, clientSecret } = buildOAuthConfigFromIntegration(
43
+ profile.integration_provider.slug,
44
+ profile.config,
45
+ );
46
+ return { clientId, clientSecret, apiUrl: String(settings['api-url'] ?? '') };
47
+ }
48
+
49
+ async getAuthUrl(callbackPath: string) {
50
+ const { clientId, apiUrl } = await this.loadConfig();
51
+ const settings = await this.setting.getSettingValues(['oauth-github-scopes']);
52
+ const scopes: string[] = settings['oauth-github-scopes'] ?? ['read:user', 'user:email'];
30
53
 
31
54
  const fixedCallbackPath = '/oauth/github/callback';
32
- const redirectURI = new URL(fixedCallbackPath, settings['api-url']).toString();
33
- const scopes = settings['github_scopes'];
34
- const type = callbackPath.split('/').pop() || 'login';
55
+ const redirectURI = this.isAbsoluteCallbackPath(callbackPath)
56
+ ? callbackPath
57
+ : new URL(fixedCallbackPath, apiUrl).toString();
58
+ const type = this.isAbsoluteCallbackPath(callbackPath)
59
+ ? 'login'
60
+ : callbackPath.split('/').pop() || 'login';
35
61
  const params = new URLSearchParams({
36
- client_id: settings['github_client_id'],
62
+ client_id: clientId,
37
63
  redirect_uri: redirectURI,
38
64
  scope: scopes.join(' '),
39
65
  allow_signup: 'true',
@@ -43,21 +69,18 @@ export class GithubProvider
43
69
  return `https://github.com/login/oauth/authorize?${params.toString()}`;
44
70
  }
45
71
 
46
- async getProfile(code: string) {
47
- const settings = await this.setting.getSettingValues([
48
- 'github_client_id',
49
- 'github_client_secret',
50
- 'github_scopes',
51
- 'api-url',
52
- ]);
72
+ async getProfile(code: string, _type?: string, redirectUri?: string) {
73
+ const { clientId, clientSecret, apiUrl } = await this.loadConfig();
53
74
 
54
75
  const fixedCallbackPath = '/oauth/github/callback';
55
- const redirectURI = new URL(fixedCallbackPath, settings['api-url'].replace(":3200", ":3100")).toString();
76
+ const redirectURI = redirectUri?.trim()
77
+ ? redirectUri.trim()
78
+ : new URL(fixedCallbackPath, apiUrl.replace(':3200', ':3100')).toString();
56
79
  const tokenResponse = await this.http.axiosRef.post(
57
80
  'https://github.com/login/oauth/access_token',
58
81
  {
59
- client_id: settings['github_client_id'],
60
- client_secret: settings['github_client_secret'],
82
+ client_id: clientId,
83
+ client_secret: clientSecret,
61
84
  code,
62
85
  redirect_uri: redirectURI,
63
86
  },
@@ -114,4 +137,4 @@ export class GithubProvider
114
137
  },
115
138
  };
116
139
  }
117
- }
140
+ }
@@ -1,5 +1,7 @@
1
+ import { PrismaService } from '@hed-hog/api-prisma';
1
2
  import { HttpService } from '@nestjs/axios';
2
- import { forwardRef, Inject, Injectable } from '@nestjs/common';
3
+ import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common';
4
+ import { buildOAuthConfigFromIntegration } from '../../integration-profile/integration-profile.utils';
3
5
  import { SettingService } from '../../setting/setting.service';
4
6
  import { BaseOAuthProvider } from './abstract.provider';
5
7
 
@@ -10,6 +12,7 @@ export class GoogleProvider extends BaseOAuthProvider {
10
12
  http: HttpService,
11
13
  @Inject(forwardRef(() => SettingService))
12
14
  private readonly setting: SettingService,
15
+ private readonly prisma: PrismaService,
13
16
  ) {
14
17
  super(http);
15
18
  }
@@ -18,22 +21,38 @@ export class GoogleProvider extends BaseOAuthProvider {
18
21
  return 'GOOGLE';
19
22
  }
20
23
 
21
- async getAuthUrl(callbackPath: string) {
22
-
24
+ private async loadConfig() {
23
25
  const settings = await this.setting.getSettingValues([
24
- 'google_client_id',
25
- 'google_client_secret',
26
- 'google_scopes',
26
+ 'oauth-google-profile-id',
27
27
  'url',
28
28
  ]);
29
+ const profileId = Number(settings['oauth-google-profile-id']);
30
+ if (!profileId) throw new BadRequestException('Google OAuth profile not configured.');
31
+
32
+ const profile = await this.prisma.integration_profile.findUnique({
33
+ where: { id: profileId },
34
+ include: { integration_provider: { select: { slug: true } } },
35
+ });
36
+ if (!profile) throw new BadRequestException('Google OAuth profile not found.');
37
+
38
+ const { clientId, clientSecret } = buildOAuthConfigFromIntegration(
39
+ profile.integration_provider.slug,
40
+ profile.config,
41
+ );
42
+ return { clientId, clientSecret, baseUrl: String(settings['url'] ?? '') };
43
+ }
29
44
 
30
- const redirectURI = new URL(callbackPath, settings['url']).toString();
45
+ async getAuthUrl(callbackPath: string) {
46
+ const { clientId, baseUrl } = await this.loadConfig();
47
+ const settings = await this.setting.getSettingValues(['oauth-google-scopes']);
48
+ const scopes: string[] = settings['oauth-google-scopes'] ?? ['email', 'profile'];
49
+ const redirectURI = new URL(callbackPath, baseUrl).toString();
31
50
 
32
51
  const params = new URLSearchParams({
33
- client_id: settings['google_client_id'],
52
+ client_id: clientId,
34
53
  redirect_uri: redirectURI,
35
54
  response_type: 'code',
36
- scope: (settings['google_scopes'] ?? []).join(' '),
55
+ scope: scopes.join(' '),
37
56
  access_type: 'offline',
38
57
  prompt: 'consent',
39
58
  });
@@ -41,21 +60,18 @@ export class GoogleProvider extends BaseOAuthProvider {
41
60
  return `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
42
61
  }
43
62
 
44
- async getProfile(code: string, type: string) {
63
+ async getProfile(code: string, type: string, redirectUri?: string) {
64
+ const { clientId, clientSecret, baseUrl } = await this.loadConfig();
45
65
 
46
- const settings = await this.setting.getSettingValues([
47
- 'google_client_id',
48
- 'google_client_secret',
49
- 'google_scopes',
50
- 'url',
51
- ]);
66
+ const resolvedRedirectUri =
67
+ redirectUri?.trim() || `${baseUrl}/callback/google/${type}`;
52
68
 
53
69
  const token = await this.fetchToken({
54
70
  code,
55
71
  url: 'https://oauth2.googleapis.com/token',
56
- clientId: settings['google_client_id'],
57
- clientSecret: settings['google_client_secret'],
58
- redirectUri: `${settings['url']}/callback/google/${type}`,
72
+ clientId,
73
+ clientSecret,
74
+ redirectUri: resolvedRedirectUri,
59
75
  });
60
76
 
61
77
  const basicProfile = await this.fetchProfile(
@@ -66,7 +82,7 @@ export class GoogleProvider extends BaseOAuthProvider {
66
82
  const peopleProfile = await this.getGoogleProfileFromAccessToken(
67
83
  token.access_token,
68
84
  );
69
-
85
+
70
86
  return {
71
87
  ...basicProfile,
72
88
  gender: peopleProfile.gender,
@@ -1,5 +1,7 @@
1
+ import { PrismaService } from '@hed-hog/api-prisma';
1
2
  import { HttpService } from '@nestjs/axios';
2
- import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
3
+ import { BadRequestException, forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
4
+ import { buildOAuthConfigFromIntegration } from '../../integration-profile/integration-profile.utils';
3
5
  import { SettingService } from '../../setting/setting.service';
4
6
  import { BaseOAuthProvider } from './abstract.provider';
5
7
 
@@ -11,6 +13,7 @@ export class MicrosoftEntraIdProvider extends BaseOAuthProvider {
11
13
  http: HttpService,
12
14
  @Inject(forwardRef(() => SettingService))
13
15
  private readonly setting: SettingService,
16
+ private readonly prisma: PrismaService,
14
17
  ) {
15
18
  super(http);
16
19
  }
@@ -32,39 +35,42 @@ export class MicrosoftEntraIdProvider extends BaseOAuthProvider {
32
35
  return 'MICROSOFT-ENTRA-ID';
33
36
  }
34
37
 
35
- async getAuthUrl(callbackPath: string) {
38
+ private async loadConfig() {
36
39
  const settings = await this.setting.getSettingValues([
37
- 'microsoft_entra_id_client_id',
38
- 'microsoft_entra_id_client_secret',
39
- 'microsoft_entra_id_scopes',
40
- 'microsoft_entra_id_tenant_id',
40
+ 'oauth-microsoft-entra-id-profile-id',
41
41
  'url',
42
42
  ]);
43
+ const profileId = Number(settings['oauth-microsoft-entra-id-profile-id']);
44
+ if (!profileId) throw new BadRequestException('Microsoft Entra ID OAuth profile not configured.');
43
45
 
44
- if (
45
- !settings['microsoft_entra_id_client_id'] ||
46
- !settings['microsoft_entra_id_client_secret'] ||
47
- !settings['microsoft_entra_id_scopes'] ||
48
- !settings['microsoft_entra_id_tenant_id'] ||
49
- !settings['url']
50
- ) {
51
- this.logger.error(
52
- 'Microsoft Entra ID OAuth is missing required settings. Verify client id, client secret, scopes, tenant id, and url.',
53
- );
54
- throw new Error('Microsoft Entra ID OAuth is not configured properly. Please configure microsoft_entra_id_client_id, microsoft_entra_id_client_secret, microsoft_entra_id_scopes, microsoft_entra_id_tenant_id, and url settings.');
46
+ const profile = await this.prisma.integration_profile.findUnique({
47
+ where: { id: profileId },
48
+ include: { integration_provider: { select: { slug: true } } },
49
+ });
50
+ if (!profile) throw new BadRequestException('Microsoft Entra ID OAuth profile not found.');
51
+
52
+ const { clientId, clientSecret, tenantId } = buildOAuthConfigFromIntegration(
53
+ profile.integration_provider.slug,
54
+ profile.config,
55
+ );
56
+ if (!tenantId) {
57
+ throw new BadRequestException('Microsoft Entra ID OAuth profile is missing tenant_id.');
55
58
  }
59
+ return { clientId, clientSecret, tenantId, baseUrl: String(settings['url'] ?? '') };
60
+ }
56
61
 
57
- const tenantId = settings['microsoft_entra_id_tenant_id'];
58
- const redirectURI = this.buildRedirectUri(settings['url'], callbackPath);
59
- const baseScopes = Array.isArray(settings['microsoft_entra_id_scopes'])
60
- ? settings['microsoft_entra_id_scopes']
61
- : [];
62
+ async getAuthUrl(callbackPath: string) {
63
+ const { clientId, tenantId, baseUrl } = await this.loadConfig();
64
+ const settings = await this.setting.getSettingValues(['oauth-microsoft-entra-id-scopes']);
65
+ const baseScopes: string[] = settings['oauth-microsoft-entra-id-scopes'] ?? ['openid', 'profile', 'email'];
62
66
  const scopes = baseScopes.includes('offline_access')
63
67
  ? baseScopes
64
68
  : [...baseScopes, 'offline_access'];
69
+
70
+ const redirectURI = this.buildRedirectUri(baseUrl, callbackPath);
65
71
  const isConnectFlow = callbackPath.includes('/connect');
66
72
  const params = new URLSearchParams({
67
- client_id: settings['microsoft_entra_id_client_id'],
73
+ client_id: clientId,
68
74
  redirect_uri: redirectURI,
69
75
  response_type: 'code',
70
76
  scope: scopes.join(' '),
@@ -75,43 +81,33 @@ export class MicrosoftEntraIdProvider extends BaseOAuthProvider {
75
81
  `OAuth auth URL requested: provider=MICROSOFT-ENTRA-ID redirectUri=${redirectURI} hasOfflineAccess=${scopes.includes('offline_access') ? 'yes' : 'no'} connectFlow=${isConnectFlow ? 'yes' : 'no'} tenantConfigured=${tenantId ? 'yes' : 'no'}`,
76
82
  );
77
83
 
78
- // Force consent only for explicit account connect, not for routine login.
79
84
  if (isConnectFlow) {
80
85
  params.set('prompt', 'consent');
81
86
  }
82
87
 
83
- const authUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?${params.toString()}`;
84
- return authUrl;
88
+ return `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?${params.toString()}`;
85
89
  }
86
90
 
87
- async getProfile(code: string, type: string): Promise<any> {
88
- let redirectUri = '';
91
+ async getProfile(code: string, type: string, redirectUri?: string): Promise<any> {
92
+ let resolvedRedirectUri = '';
89
93
 
90
94
  try {
91
- const settings = await this.setting.getSettingValues([
92
- 'microsoft_entra_id_client_id',
93
- 'microsoft_entra_id_client_secret',
94
- 'microsoft_entra_id_scopes',
95
- 'microsoft_entra_id_tenant_id',
96
- 'url',
97
- ]);
98
- const tenantId = settings['microsoft_entra_id_tenant_id'];
99
- const scopes = settings['microsoft_entra_id_scopes'] ?? [];
100
- redirectUri = this.buildRedirectUri(
101
- settings['url'],
102
- `/callback/microsoft-entra-id/${type}`,
103
- );
95
+ const { clientId, clientSecret, tenantId, baseUrl } = await this.loadConfig();
96
+ const settings = await this.setting.getSettingValues(['oauth-microsoft-entra-id-scopes']);
97
+ const scopes: string[] = settings['oauth-microsoft-entra-id-scopes'] ?? [];
98
+ resolvedRedirectUri =
99
+ redirectUri?.trim() || this.buildRedirectUri(baseUrl, `/callback/microsoft-entra-id/${type}`);
104
100
 
105
101
  this.logger.log(
106
- `OAuth token exchange started: provider=MICROSOFT-ENTRA-ID type=${type} redirectUri=${redirectUri} hasCode=${code ? 'yes' : 'no'} codeLength=${code?.length ?? 0}`,
102
+ `OAuth token exchange started: provider=MICROSOFT-ENTRA-ID type=${type} redirectUri=${resolvedRedirectUri} hasCode=${code ? 'yes' : 'no'} codeLength=${code?.length ?? 0}`,
107
103
  );
108
104
 
109
105
  const token = await this.fetchToken({
110
106
  code,
111
107
  url: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
112
- clientId: settings['microsoft_entra_id_client_id'],
113
- clientSecret: settings['microsoft_entra_id_client_secret'],
114
- redirectUri,
108
+ clientId,
109
+ clientSecret,
110
+ redirectUri: resolvedRedirectUri,
115
111
  });
116
112
 
117
113
  const profile = await this.fetchProfile(
@@ -141,7 +137,7 @@ export class MicrosoftEntraIdProvider extends BaseOAuthProvider {
141
137
  const mappedError = this.mapProviderError('Microsoft Entra ID', error);
142
138
 
143
139
  this.logger.warn(
144
- `OAuth callback failed: provider=MICROSOFT-ENTRA-ID status=${mappedError.status ?? 'unknown'} error=${mappedError.providerError ?? 'unknown'} providerCode=${mappedError.providerErrorCode ?? 'n/a'} reason=${mappedError.reason} redirectUri=${redirectUri || 'unresolved'}`,
140
+ `OAuth callback failed: provider=MICROSOFT-ENTRA-ID status=${mappedError.status ?? 'unknown'} error=${mappedError.providerError ?? 'unknown'} providerCode=${mappedError.providerErrorCode ?? 'n/a'} reason=${mappedError.reason} redirectUri=${resolvedRedirectUri || 'unresolved'}`,
145
141
  );
146
142
 
147
143
  throw mappedError;
@@ -1,16 +1,18 @@
1
+ import { PrismaService } from '@hed-hog/api-prisma';
1
2
  import { HttpService } from '@nestjs/axios';
2
- import { forwardRef, Inject, Injectable } from '@nestjs/common';
3
+ import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common';
4
+ import { buildOAuthConfigFromIntegration } from '../../integration-profile/integration-profile.utils';
3
5
  import { SettingService } from '../../setting/setting.service';
4
6
  import { BaseOAuthProvider } from './abstract.provider';
5
7
 
6
8
  @Injectable()
7
- export class MicrosoftProvider
8
- extends BaseOAuthProvider {
9
+ export class MicrosoftProvider extends BaseOAuthProvider {
9
10
 
10
11
  constructor(
11
12
  http: HttpService,
12
13
  @Inject(forwardRef(() => SettingService))
13
14
  private readonly setting: SettingService,
15
+ private readonly prisma: PrismaService,
14
16
  ) {
15
17
  super(http);
16
18
  }
@@ -19,19 +21,35 @@ export class MicrosoftProvider
19
21
  return 'MICROSOFT';
20
22
  }
21
23
 
22
- async getAuthUrl(callbackPath: string) {
23
-
24
+ private async loadConfig() {
24
25
  const settings = await this.setting.getSettingValues([
25
- 'microsoft_client_id',
26
- 'microsoft_client_secret',
27
- 'microsoft_scopes',
26
+ 'oauth-microsoft-profile-id',
28
27
  'url',
29
28
  ]);
29
+ const profileId = Number(settings['oauth-microsoft-profile-id']);
30
+ if (!profileId) throw new BadRequestException('Microsoft OAuth profile not configured.');
31
+
32
+ const profile = await this.prisma.integration_profile.findUnique({
33
+ where: { id: profileId },
34
+ include: { integration_provider: { select: { slug: true } } },
35
+ });
36
+ if (!profile) throw new BadRequestException('Microsoft OAuth profile not found.');
37
+
38
+ const { clientId, clientSecret, tenantId } = buildOAuthConfigFromIntegration(
39
+ profile.integration_provider.slug,
40
+ profile.config,
41
+ );
42
+ return { clientId, clientSecret, tenantId, baseUrl: String(settings['url'] ?? '') };
43
+ }
44
+
45
+ async getAuthUrl(callbackPath: string) {
46
+ const { clientId, baseUrl } = await this.loadConfig();
47
+ const settings = await this.setting.getSettingValues(['oauth-microsoft-scopes']);
48
+ const scopes: string[] = settings['oauth-microsoft-scopes'] ?? ['openid', 'profile', 'email'];
49
+ const redirectURI = new URL(callbackPath, baseUrl).toString();
30
50
 
31
- const redirectURI = new URL(callbackPath, settings['url']).toString();
32
- const scopes = settings['microsoft_scopes']
33
51
  const params = new URLSearchParams({
34
- client_id: settings['microsoft_client_id'],
52
+ client_id: clientId,
35
53
  redirect_uri: redirectURI,
36
54
  response_type: 'code',
37
55
  scope: scopes.join(' '),
@@ -42,21 +60,18 @@ export class MicrosoftProvider
42
60
  return `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?${params}`;
43
61
  }
44
62
 
45
- async getProfile(code: string, type: string): Promise<any> {
63
+ async getProfile(code: string, type: string, redirectUri?: string): Promise<any> {
64
+ const { clientId, clientSecret, baseUrl } = await this.loadConfig();
46
65
 
47
- const settings = await this.setting.getSettingValues([
48
- 'microsoft_client_id',
49
- 'microsoft_client_secret',
50
- 'microsoft_scopes',
51
- 'url',
52
- ]);
66
+ const resolvedRedirectUri =
67
+ redirectUri?.trim() || `${baseUrl}/callback/microsoft/${type}`;
53
68
 
54
69
  const token = await this.fetchToken({
55
70
  code,
56
71
  url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
57
- clientId: settings['microsoft_client_id'],
58
- clientSecret: settings['microsoft_client_secret'],
59
- redirectUri: `${settings['url']}/callback/microsoft/${type}`,
72
+ clientId,
73
+ clientSecret,
74
+ redirectUri: resolvedRedirectUri,
60
75
  });
61
76
 
62
77
  const profile = await this.fetchProfile(
@@ -1,4 +1,4 @@
1
- import { Role, User } from '@hed-hog/api';
1
+ import { NoRole, User } from '@hed-hog/api';
2
2
  import { Locale } from '@hed-hog/api-locale';
3
3
  import { Body, Controller, Delete, Get, Headers, Ip, Param, ParseIntPipe, Post, Put, UploadedFile, UseInterceptors } from '@nestjs/common';
4
4
  import { FileInterceptor } from '@nestjs/platform-express';
@@ -19,7 +19,7 @@ import { VerifyMfaTotpDto } from './dto/verify-mfa-totp.dto';
19
19
  import { VerifyWebAuthnRegistrationDto } from './dto/verify-webauthn-registration.dto';
20
20
  import { VerifyWebAuthnDto } from './dto/verify-webauthn.dto';
21
21
  import { ProfileService } from './profile.service';
22
- @Role()
22
+ @NoRole()
23
23
  @Controller('profile')
24
24
  export class ProfileController {
25
25
 
@@ -2,7 +2,15 @@ import { getLocaleText } from '@hed-hog/api-locale';
2
2
  import { BadRequestException, Injectable } from '@nestjs/common';
3
3
  import { ConfigService } from '@nestjs/config';
4
4
  import * as argon2 from 'argon2';
5
- import { createCipheriv, createDecipheriv, createHash, randomBytes, scryptSync } from 'crypto';
5
+ import {
6
+ createCipheriv,
7
+ createDecipheriv,
8
+ createHash,
9
+ createHmac,
10
+ randomBytes,
11
+ scryptSync,
12
+ timingSafeEqual,
13
+ } from 'crypto';
6
14
 
7
15
  @Injectable()
8
16
  export class SecurityService {
@@ -31,6 +39,31 @@ export class SecurityService {
31
39
  return createHash('sha256').update(value).digest('base64url');
32
40
  }
33
41
 
42
+ signHmacSha256(value: string, secret?: string): string {
43
+ const signingSecret = secret || this.getJwtSecret();
44
+ return createHmac('sha256', signingSecret).update(value).digest('base64url');
45
+ }
46
+
47
+ verifyHmacSha256(value: string, signature: string, secret?: string): boolean {
48
+ if (!signature) {
49
+ return false;
50
+ }
51
+
52
+ try {
53
+ const expected = this.signHmacSha256(value, secret);
54
+ const expectedBuffer = Buffer.from(expected);
55
+ const providedBuffer = Buffer.from(signature);
56
+
57
+ if (expectedBuffer.length !== providedBuffer.length) {
58
+ return false;
59
+ }
60
+
61
+ return timingSafeEqual(expectedBuffer, providedBuffer);
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+
34
67
  hashArgon2(value: string, pepper?: string): Promise<string> {
35
68
  return argon2.hash(this.getValueWithPepper(value, pepper));
36
69
  }