@hed-hog/core 0.0.303 → 0.0.305

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 (118) hide show
  1. package/README.md +84 -36
  2. package/dist/auth/auth.controller.d.ts +0 -3
  3. package/dist/auth/auth.controller.d.ts.map +1 -1
  4. package/dist/auth/auth.controller.js +4 -7
  5. package/dist/auth/auth.controller.js.map +1 -1
  6. package/dist/auth/auth.service.d.ts.map +1 -1
  7. package/dist/auth/auth.service.js +6 -3
  8. package/dist/auth/auth.service.js.map +1 -1
  9. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +9 -0
  10. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
  11. package/dist/dashboard/dashboard-core/dashboard-core.controller.js +12 -0
  12. package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
  13. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +7 -0
  14. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
  15. package/dist/dashboard/dashboard-core/dashboard-core.service.js +45 -1
  16. package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/mail-sent/dto/query-mail-sent-list.dto.d.ts +9 -0
  22. package/dist/mail-sent/dto/query-mail-sent-list.dto.d.ts.map +1 -0
  23. package/dist/mail-sent/dto/query-mail-sent-list.dto.js +49 -0
  24. package/dist/mail-sent/dto/query-mail-sent-list.dto.js.map +1 -0
  25. package/dist/mail-sent/mail-sent.controller.d.ts +24 -2
  26. package/dist/mail-sent/mail-sent.controller.d.ts.map +1 -1
  27. package/dist/mail-sent/mail-sent.controller.js +5 -3
  28. package/dist/mail-sent/mail-sent.controller.js.map +1 -1
  29. package/dist/mail-sent/mail-sent.service.d.ts +28 -3
  30. package/dist/mail-sent/mail-sent.service.d.ts.map +1 -1
  31. package/dist/mail-sent/mail-sent.service.js +108 -3
  32. package/dist/mail-sent/mail-sent.service.js.map +1 -1
  33. package/dist/oauth/oauth-callback-coordinator.service.d.ts +18 -0
  34. package/dist/oauth/oauth-callback-coordinator.service.d.ts.map +1 -0
  35. package/dist/oauth/oauth-callback-coordinator.service.js +137 -0
  36. package/dist/oauth/oauth-callback-coordinator.service.js.map +1 -0
  37. package/dist/oauth/oauth.controller.d.ts +4 -2
  38. package/dist/oauth/oauth.controller.d.ts.map +1 -1
  39. package/dist/oauth/oauth.controller.js +39 -11
  40. package/dist/oauth/oauth.controller.js.map +1 -1
  41. package/dist/oauth/oauth.errors.d.ts +16 -0
  42. package/dist/oauth/oauth.errors.d.ts.map +1 -0
  43. package/dist/oauth/oauth.errors.js +30 -0
  44. package/dist/oauth/oauth.errors.js.map +1 -0
  45. package/dist/oauth/oauth.module.d.ts.map +1 -1
  46. package/dist/oauth/oauth.module.js +10 -1
  47. package/dist/oauth/oauth.module.js.map +1 -1
  48. package/dist/oauth/oauth.service.d.ts +9 -1
  49. package/dist/oauth/oauth.service.d.ts.map +1 -1
  50. package/dist/oauth/oauth.service.js +97 -24
  51. package/dist/oauth/oauth.service.js.map +1 -1
  52. package/dist/oauth/providers/abstract.provider.d.ts +6 -0
  53. package/dist/oauth/providers/abstract.provider.d.ts.map +1 -1
  54. package/dist/oauth/providers/abstract.provider.js +34 -0
  55. package/dist/oauth/providers/abstract.provider.js.map +1 -1
  56. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +3 -0
  57. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
  58. package/dist/oauth/providers/microsoft-entra-id.provider.js +40 -22
  59. package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
  60. package/dist/security/security.service.d.ts +1 -0
  61. package/dist/security/security.service.d.ts.map +1 -1
  62. package/dist/security/security.service.js +9 -10
  63. package/dist/security/security.service.js.map +1 -1
  64. package/dist/session/session.service.d.ts +5 -1
  65. package/dist/session/session.service.d.ts.map +1 -1
  66. package/dist/session/session.service.js +76 -10
  67. package/dist/session/session.service.js.map +1 -1
  68. package/dist/setting/setting.service.js +4 -4
  69. package/dist/setting/setting.service.js.map +1 -1
  70. package/dist/token/token.service.d.ts +1 -0
  71. package/dist/token/token.service.d.ts.map +1 -1
  72. package/dist/token/token.service.js +23 -12
  73. package/dist/token/token.service.js.map +1 -1
  74. package/dist/user/user.service.d.ts +1 -1
  75. package/dist/user/user.service.d.ts.map +1 -1
  76. package/dist/user/user.service.js +3 -0
  77. package/dist/user/user.service.js.map +1 -1
  78. package/hedhog/data/dashboard.yaml +2 -2
  79. package/hedhog/data/mail.yaml +147 -0
  80. package/hedhog/data/route.yaml +8 -0
  81. package/hedhog/frontend/app/account/components/change-email-form.tsx.ejs +87 -8
  82. package/hedhog/frontend/app/account/components/profile-form.tsx.ejs +90 -10
  83. package/hedhog/frontend/app/ai_agent/page.tsx.ejs +145 -31
  84. package/hedhog/frontend/app/configurations/layout.tsx.ejs +1 -1
  85. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +67 -2
  86. package/hedhog/frontend/app/dashboard/components/add-widget-selector-dialog.tsx.ejs +13 -9
  87. package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +250 -45
  88. package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +111 -15
  89. package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +110 -15
  90. package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +113 -15
  91. package/hedhog/frontend/app/mail/log/page.tsx.ejs +481 -70
  92. package/hedhog/frontend/app/menu/page.tsx.ejs +202 -30
  93. package/hedhog/frontend/app/roles/menus.tsx.ejs +1 -1
  94. package/hedhog/frontend/app/roles/page.tsx.ejs +288 -104
  95. package/hedhog/frontend/app/users/page.tsx.ejs +168 -23
  96. package/hedhog/frontend/messages/en.json +45 -6
  97. package/hedhog/frontend/messages/pt.json +45 -6
  98. package/package.json +4 -4
  99. package/src/auth/auth.controller.ts +15 -19
  100. package/src/auth/auth.service.ts +13 -8
  101. package/src/dashboard/dashboard-core/dashboard-core.controller.ts +22 -9
  102. package/src/dashboard/dashboard-core/dashboard-core.service.ts +75 -1
  103. package/src/index.ts +10 -7
  104. package/src/mail-sent/dto/query-mail-sent-list.dto.ts +30 -0
  105. package/src/mail-sent/mail-sent.controller.ts +14 -2
  106. package/src/mail-sent/mail-sent.service.ts +159 -4
  107. package/src/oauth/oauth-callback-coordinator.service.ts +158 -0
  108. package/src/oauth/oauth.controller.ts +58 -1
  109. package/src/oauth/oauth.errors.ts +32 -0
  110. package/src/oauth/oauth.module.ts +10 -1
  111. package/src/oauth/oauth.service.ts +178 -28
  112. package/src/oauth/providers/abstract.provider.ts +64 -0
  113. package/src/oauth/providers/microsoft-entra-id.provider.ts +92 -56
  114. package/src/security/security.service.ts +12 -10
  115. package/src/session/session.service.ts +84 -9
  116. package/src/setting/setting.service.ts +10 -10
  117. package/src/token/token.service.ts +29 -11
  118. package/src/user/user.service.ts +10 -6
package/src/index.ts CHANGED
@@ -19,13 +19,13 @@ export * from './screen/screen.service';
19
19
  export * from './user/constants/user.constants';
20
20
  export * from './user/user.service';
21
21
 
22
- export * from './mail/mail.module';
23
- export * from './mail/mail.service';
24
-
25
- export * from './file/file.module';
26
- export * from './file/file.service';
27
- export * from './setting/setting.module';
28
- export * from './setting/setting.service';
22
+ export * from './mail/mail.module';
23
+ export * from './mail/mail.service';
24
+
25
+ export * from './file/file.module';
26
+ export * from './file/file.service';
27
+ export * from './setting/setting.module';
28
+ export * from './setting/setting.service';
29
29
 
30
30
  export * from './ai/ai.module';
31
31
  export * from './ai/ai.service';
@@ -50,3 +50,6 @@ export * from './validators/is-email-with-settings.validator';
50
50
  export * from './validators/is-pin-code-with-setting.validator';
51
51
  export * from './validators/is-strong-password-with-settings.validator';
52
52
 
53
+ // Utilities
54
+ export * from './utils/locale-context';
55
+
@@ -0,0 +1,30 @@
1
+ import { PaginationDTO } from '@hed-hog/api-pagination';
2
+ import { Transform } from 'class-transformer';
3
+ import { IsIn, IsOptional, IsString } from 'class-validator';
4
+
5
+ export class QueryMailSentListDTO extends PaginationDTO {
6
+ @IsOptional()
7
+ @Transform(({ value }) => String(value ?? '').trim().toLowerCase())
8
+ @IsIn(['all', 'received', 'read', 'error'])
9
+ status?: 'all' | 'received' | 'read' | 'error';
10
+
11
+ @IsOptional()
12
+ @Transform(({ value }) => String(value ?? '').trim().toLowerCase())
13
+ @IsString()
14
+ hasError?: string;
15
+
16
+ @IsOptional()
17
+ @Transform(({ value }) => String(value ?? '').trim())
18
+ @IsString()
19
+ recipientEmail?: string;
20
+
21
+ @IsOptional()
22
+ @Transform(({ value }) => String(value ?? '').trim())
23
+ @IsString()
24
+ createdAtFrom?: string;
25
+
26
+ @IsOptional()
27
+ @Transform(({ value }) => String(value ?? '').trim())
28
+ @IsString()
29
+ createdAtTo?: string;
30
+ }
@@ -11,9 +11,11 @@ import {
11
11
  ParseIntPipe,
12
12
  Patch,
13
13
  Post,
14
+ Query,
14
15
  forwardRef,
15
16
  } from '@nestjs/common';
16
17
  import { CreateDTO } from './dto/create.dto';
18
+ import { QueryMailSentListDTO } from './dto/query-mail-sent-list.dto';
17
19
  import { UpdateDTO } from './dto/update.dto';
18
20
  import { MailSentService } from './mail-sent.service';
19
21
 
@@ -26,8 +28,18 @@ export class MailSentController {
26
28
  ) {}
27
29
 
28
30
  @Get()
29
- async list(@Pagination() paginationParams) {
30
- return this.mailSentService.list(paginationParams);
31
+ async list(
32
+ @Pagination() paginationParams,
33
+ @Query() query: QueryMailSentListDTO,
34
+ ) {
35
+ return this.mailSentService.list({
36
+ ...paginationParams,
37
+ status: query.status,
38
+ hasError: query.hasError,
39
+ recipientEmail: query.recipientEmail,
40
+ createdAtFrom: query.createdAtFrom,
41
+ createdAtTo: query.createdAtTo,
42
+ });
31
43
  }
32
44
 
33
45
  @Get(':id')
@@ -1,6 +1,6 @@
1
1
  import { DeleteDTO } from '@hed-hog/api';
2
2
  import { getLocaleText } from '@hed-hog/api-locale';
3
- import { PaginationDTO, PaginationService } from '@hed-hog/api-pagination';
3
+ import { PaginationService } from '@hed-hog/api-pagination';
4
4
  import { PrismaService } from '@hed-hog/api-prisma';
5
5
  import {
6
6
  BadRequestException,
@@ -10,8 +10,28 @@ import {
10
10
  forwardRef,
11
11
  } from '@nestjs/common';
12
12
  import { CreateDTO } from './dto/create.dto';
13
+ import { QueryMailSentListDTO } from './dto/query-mail-sent-list.dto';
13
14
  import { UpdateDTO } from './dto/update.dto';
14
15
 
16
+ type MailSentUserStatus = 'received' | 'read' | 'error';
17
+
18
+ type MailSentUserLog = {
19
+ id: number;
20
+ recipient_email: string;
21
+ status: MailSentUserStatus;
22
+ read_at: Date | null;
23
+ error_code: string | null;
24
+ error_message: string | null;
25
+ user_id: number;
26
+ user_identifier_id: number | null;
27
+ created_at: Date;
28
+ updated_at: Date;
29
+ };
30
+
31
+ type MailSentWithUsers = {
32
+ mail_sent_user?: MailSentUserLog[];
33
+ };
34
+
15
35
  @Injectable()
16
36
  export class MailSentService {
17
37
  constructor(
@@ -21,26 +41,161 @@ export class MailSentService {
21
41
  private readonly paginationService: PaginationService,
22
42
  ) { }
23
43
 
24
- async list(paginationParams: PaginationDTO) {
44
+ async list(paginationParams: QueryMailSentListDTO) {
25
45
  const fields = ['subject', 'from', 'to', 'cc', 'bcc', 'body'];
26
46
  const OR: any[] = this.prismaService.createInsensitiveSearch(
27
47
  fields,
28
48
  paginationParams,
29
49
  );
30
50
 
51
+ const AND: any[] = [];
52
+
53
+ if (
54
+ paginationParams.status &&
55
+ paginationParams.status !== 'all' &&
56
+ ['received', 'read', 'error'].includes(paginationParams.status)
57
+ ) {
58
+ AND.push({
59
+ mail_sent_user: {
60
+ some: {
61
+ status: paginationParams.status,
62
+ },
63
+ },
64
+ });
65
+ }
66
+
67
+ const recipientEmail = paginationParams.recipientEmail?.trim();
68
+ if (recipientEmail) {
69
+ AND.push({
70
+ mail_sent_user: {
71
+ some: {
72
+ recipient_email: {
73
+ contains: recipientEmail,
74
+ mode: 'insensitive',
75
+ },
76
+ },
77
+ },
78
+ });
79
+ }
80
+
81
+ const hasErrorRaw = paginationParams.hasError?.trim().toLowerCase();
82
+ if (hasErrorRaw === 'true' || hasErrorRaw === '1') {
83
+ AND.push({
84
+ mail_sent_user: {
85
+ some: {
86
+ status: 'error',
87
+ },
88
+ },
89
+ });
90
+ }
91
+
92
+ if (hasErrorRaw === 'false' || hasErrorRaw === '0') {
93
+ AND.push({
94
+ mail_sent_user: {
95
+ none: {
96
+ status: 'error',
97
+ },
98
+ },
99
+ });
100
+ }
101
+
102
+ const createdAt: { gte?: Date; lte?: Date } = {};
103
+ const createdAtFrom = paginationParams.createdAtFrom?.trim();
104
+ const createdAtTo = paginationParams.createdAtTo?.trim();
105
+
106
+ if (createdAtFrom) {
107
+ const parsedFrom = new Date(createdAtFrom);
108
+ if (!isNaN(parsedFrom.getTime())) {
109
+ createdAt.gte = parsedFrom;
110
+ }
111
+ }
112
+
113
+ if (createdAtTo) {
114
+ const parsedTo = new Date(createdAtTo);
115
+ if (!isNaN(parsedTo.getTime())) {
116
+ parsedTo.setHours(23, 59, 59, 999);
117
+ createdAt.lte = parsedTo;
118
+ }
119
+ }
120
+
31
121
  if (paginationParams.search && !isNaN(+paginationParams.search)) {
32
122
  OR.push({ id: { equals: +paginationParams.search } });
33
123
  }
34
124
 
35
- return this.paginationService.paginate(
125
+ const result = await this.paginationService.paginate(
36
126
  this.prismaService.mail_sent,
37
127
  paginationParams,
38
128
  {
39
129
  where: {
40
- OR,
130
+ ...(OR.length > 0 ? { OR } : {}),
131
+ ...(AND.length > 0 ? { AND } : {}),
132
+ ...(Object.keys(createdAt).length > 0 ? { created_at: createdAt } : {}),
133
+ },
134
+ include: {
135
+ mail_sent_user: {
136
+ select: {
137
+ id: true,
138
+ recipient_email: true,
139
+ status: true,
140
+ read_at: true,
141
+ error_code: true,
142
+ error_message: true,
143
+ user_id: true,
144
+ user_identifier_id: true,
145
+ created_at: true,
146
+ updated_at: true,
147
+ },
148
+ },
41
149
  },
42
150
  },
43
151
  );
152
+
153
+ const data = (result.data as MailSentWithUsers[]).map((item) => {
154
+ const recipientLogs = item.mail_sent_user ?? [];
155
+ const summary = recipientLogs.reduce(
156
+ (acc, recipient) => {
157
+ if (recipient.status === 'received') {
158
+ acc.receivedCount += 1;
159
+ }
160
+
161
+ if (recipient.status === 'read') {
162
+ acc.readCount += 1;
163
+ if (
164
+ recipient.read_at &&
165
+ (!acc.lastReadAt || recipient.read_at > acc.lastReadAt)
166
+ ) {
167
+ acc.lastReadAt = recipient.read_at;
168
+ }
169
+ }
170
+
171
+ if (recipient.status === 'error') {
172
+ acc.errorCount += 1;
173
+ }
174
+
175
+ return acc;
176
+ },
177
+ {
178
+ totalRecipients: recipientLogs.length,
179
+ receivedCount: 0,
180
+ readCount: 0,
181
+ errorCount: 0,
182
+ hasError: false,
183
+ lastReadAt: null as Date | null,
184
+ },
185
+ );
186
+
187
+ summary.hasError = summary.errorCount > 0;
188
+
189
+ return {
190
+ ...item,
191
+ deliverySummary: summary,
192
+ };
193
+ });
194
+
195
+ return {
196
+ ...result,
197
+ data,
198
+ };
44
199
  }
45
200
 
46
201
  async get(locale:string, id: number) {
@@ -0,0 +1,158 @@
1
+ import { Prisma, PrismaService } from '@hed-hog/api-prisma';
2
+ import { Injectable, Logger } from '@nestjs/common';
3
+ import { createHash } from 'crypto';
4
+ import {
5
+ OAuthCallbackAlreadyProcessedError,
6
+ OAuthCallbackInProgressError,
7
+ OAuthProviderException,
8
+ } from './oauth.errors';
9
+
10
+ @Injectable()
11
+ export class OAuthCallbackCoordinatorService {
12
+ private readonly logger = new Logger(OAuthCallbackCoordinatorService.name);
13
+ private readonly completedCallbacks = new Map<string, number>();
14
+ private readonly completedTtlMs = 10 * 60 * 1000;
15
+
16
+ constructor(private readonly prisma: PrismaService) {}
17
+
18
+ async execute<T>(callbackKey: string, callback: () => Promise<T>): Promise<T> {
19
+ this.pruneCompletedCallbacks();
20
+
21
+ if (this.isCompleted(callbackKey)) {
22
+ throw new OAuthCallbackAlreadyProcessedError();
23
+ }
24
+
25
+ try {
26
+ if (this.prisma.isPostgres()) {
27
+ return await this.executeWithPostgresLock(callbackKey, callback);
28
+ }
29
+
30
+ if (this.prisma.isMysql()) {
31
+ return await this.executeWithMysqlLock(callbackKey, callback);
32
+ }
33
+
34
+ this.logger.warn(
35
+ `OAuth callback coordination is running without distributed lock support for provider ${this.prisma.getProvider()}.`,
36
+ );
37
+
38
+ const result = await callback();
39
+ this.markCompleted(callbackKey);
40
+ return result;
41
+ } catch (error) {
42
+ if (this.shouldMarkCompleted(error)) {
43
+ this.markCompleted(callbackKey);
44
+ }
45
+
46
+ throw error;
47
+ }
48
+ }
49
+
50
+ private async executeWithPostgresLock<T>(
51
+ callbackKey: string,
52
+ callback: () => Promise<T>,
53
+ ): Promise<T> {
54
+ const lockKey = this.toSignedBigInt(callbackKey);
55
+
56
+ return this.prisma.$transaction(
57
+ async (tx) => {
58
+ const rows = await tx.$queryRaw<Array<{ locked: boolean }>>`
59
+ SELECT pg_try_advisory_xact_lock(${lockKey}) AS locked
60
+ `;
61
+
62
+ if (!rows[0]?.locked) {
63
+ throw new OAuthCallbackInProgressError();
64
+ }
65
+
66
+ const result = await callback();
67
+ this.markCompleted(callbackKey);
68
+ return result;
69
+ },
70
+ {
71
+ maxWait: 5_000,
72
+ timeout: 60_000,
73
+ },
74
+ );
75
+ }
76
+
77
+ private async executeWithMysqlLock<T>(
78
+ callbackKey: string,
79
+ callback: () => Promise<T>,
80
+ ): Promise<T> {
81
+ const lockName = this.toMysqlLockName(callbackKey);
82
+
83
+ return this.prisma.$transaction(
84
+ async (tx) => {
85
+ const rows = await tx.$queryRaw<Array<{ locked: 0 | 1 | null }>>(Prisma.sql`
86
+ SELECT GET_LOCK(${lockName}, 0) AS locked
87
+ `);
88
+
89
+ if (rows[0]?.locked !== 1) {
90
+ throw new OAuthCallbackInProgressError();
91
+ }
92
+
93
+ try {
94
+ const result = await callback();
95
+ this.markCompleted(callbackKey);
96
+ return result;
97
+ } finally {
98
+ await tx.$queryRaw(Prisma.sql`
99
+ SELECT RELEASE_LOCK(${lockName}) AS released
100
+ `);
101
+ }
102
+ },
103
+ {
104
+ maxWait: 5_000,
105
+ timeout: 60_000,
106
+ },
107
+ );
108
+ }
109
+
110
+ private shouldMarkCompleted(error: unknown) {
111
+ return (
112
+ error instanceof OAuthCallbackAlreadyProcessedError ||
113
+ (error instanceof OAuthProviderException &&
114
+ error.reason === 'callback_consumed')
115
+ );
116
+ }
117
+
118
+ private isCompleted(callbackKey: string) {
119
+ const expiresAt = this.completedCallbacks.get(callbackKey);
120
+ if (!expiresAt) {
121
+ return false;
122
+ }
123
+
124
+ if (expiresAt <= Date.now()) {
125
+ this.completedCallbacks.delete(callbackKey);
126
+ return false;
127
+ }
128
+
129
+ return true;
130
+ }
131
+
132
+ private markCompleted(callbackKey: string) {
133
+ this.completedCallbacks.set(callbackKey, Date.now() + this.completedTtlMs);
134
+ }
135
+
136
+ private pruneCompletedCallbacks() {
137
+ const now = Date.now();
138
+ for (const [callbackKey, expiresAt] of this.completedCallbacks.entries()) {
139
+ if (expiresAt <= now) {
140
+ this.completedCallbacks.delete(callbackKey);
141
+ }
142
+ }
143
+ }
144
+
145
+ private toSignedBigInt(callbackKey: string) {
146
+ const hex = createHash('sha256').update(callbackKey).digest('hex').slice(0, 16);
147
+ const unsigned = BigInt(`0x${hex}`);
148
+ const signedBoundary = BigInt(1) << BigInt(63);
149
+ return unsigned >= signedBoundary
150
+ ? unsigned - (BigInt(1) << BigInt(64))
151
+ : unsigned;
152
+ }
153
+
154
+ private toMysqlLockName(callbackKey: string) {
155
+ const suffix = createHash('sha256').update(callbackKey).digest('hex').slice(0, 48);
156
+ return `oauth:${suffix}`;
157
+ }
158
+ }
@@ -9,6 +9,7 @@ import {
9
9
  Get,
10
10
  Headers,
11
11
  Ip,
12
+ Logger,
12
13
  Param,
13
14
  Query,
14
15
  Res,
@@ -18,8 +19,42 @@ import { OAuthService } from './oauth.service';
18
19
 
19
20
  @Controller('oauth')
20
21
  export class OAuthController {
22
+ private readonly logger = new Logger(OAuthController.name);
23
+
21
24
  constructor(private readonly service: OAuthService, private readonly settingService: SettingService) {}
22
25
 
26
+ private logOAuthRequest(
27
+ action: string,
28
+ provider: string,
29
+ {
30
+ locale,
31
+ origin,
32
+ referer,
33
+ host,
34
+ ipAddress,
35
+ userAgent,
36
+ code,
37
+ }: {
38
+ locale?: string;
39
+ origin?: string;
40
+ referer?: string;
41
+ host?: string;
42
+ ipAddress?: string;
43
+ userAgent?: string;
44
+ code?: string;
45
+ },
46
+ ) {
47
+ const summarizedUserAgent = userAgent
48
+ ? userAgent.length > 120
49
+ ? `${userAgent.slice(0, 117)}...`
50
+ : userAgent
51
+ : 'n/a';
52
+
53
+ this.logger.log(
54
+ `OAuth ${action}: provider=${provider} locale=${locale ?? 'n/a'} origin=${origin ?? 'n/a'} referer=${referer ?? 'n/a'} host=${host ?? 'n/a'} ip=${ipAddress ?? 'n/a'} hasCode=${code ? 'yes' : 'no'} codeLength=${code?.length ?? 0} userAgent="${summarizedUserAgent}"`,
55
+ );
56
+ }
57
+
23
58
  @Public()
24
59
  @Get('github/callback')
25
60
  async githubCallback(
@@ -35,7 +70,15 @@ export class OAuthController {
35
70
 
36
71
  @Public()
37
72
  @Get(':provider/login')
38
- async login(@Param('provider') provider: string, @Res() res) {
73
+ async login(
74
+ @Headers('origin') origin: string,
75
+ @Headers('referer') referer: string,
76
+ @Headers('host') host: string,
77
+ @Param('provider') provider: string,
78
+ @Res() res,
79
+ ) {
80
+ this.logOAuthRequest('login-init', provider, { origin, referer, host });
81
+
39
82
  const url = await this.service.getAuthUrl(
40
83
  provider as user_account_provider_52222e2ecb_enum,
41
84
  `/callback/${provider}/login`,
@@ -72,10 +115,24 @@ export class OAuthController {
72
115
  @Locale() locale: string,
73
116
  @Ip() ipAddress: string,
74
117
  @Headers('user-agent') userAgent: string,
118
+ @Headers('origin') origin: string,
119
+ @Headers('referer') referer: string,
120
+ @Headers('host') host: string,
121
+ @Headers('x-forwarded-host') forwardedHost: string,
75
122
  @Param('provider') provider: string,
76
123
  @Query('code') code: string,
77
124
  @Res({ passthrough: true }) res,
78
125
  ) {
126
+ this.logOAuthRequest('callback-login', provider, {
127
+ locale,
128
+ origin,
129
+ referer,
130
+ host: forwardedHost || host,
131
+ ipAddress,
132
+ userAgent,
133
+ code,
134
+ });
135
+
79
136
  return this.service.handleCallback({ res, locale, ipAddress, userAgent, provider: provider as user_account_provider_52222e2ecb_enum, code, type: 'login' });
80
137
  }
81
138
 
@@ -0,0 +1,32 @@
1
+ export type OAuthProviderFailureReason =
2
+ | 'callback_consumed'
3
+ | 'invalid_callback'
4
+ | 'upstream_failure';
5
+
6
+ export class OAuthProviderException extends Error {
7
+ constructor(
8
+ public readonly provider: string,
9
+ public readonly reason: OAuthProviderFailureReason,
10
+ message: string,
11
+ public readonly status?: number,
12
+ public readonly providerError?: string,
13
+ public readonly providerErrorCode?: string,
14
+ ) {
15
+ super(message);
16
+ this.name = 'OAuthProviderException';
17
+ }
18
+ }
19
+
20
+ export class OAuthCallbackAlreadyProcessedError extends Error {
21
+ constructor() {
22
+ super('OAuth callback already processed.');
23
+ this.name = 'OAuthCallbackAlreadyProcessedError';
24
+ }
25
+ }
26
+
27
+ export class OAuthCallbackInProgressError extends Error {
28
+ constructor() {
29
+ super('OAuth callback is already in progress.');
30
+ this.name = 'OAuthCallbackInProgressError';
31
+ }
32
+ }
@@ -9,6 +9,7 @@ import { MailModule } from '../mail/mail.module';
9
9
  import { SecurityModule } from '../security/security.module';
10
10
  import { TokenModule } from '../token/token.module';
11
11
  import { UserModule } from '../user/user.module';
12
+ import { OAuthCallbackCoordinatorService } from './oauth-callback-coordinator.service';
12
13
  import { OAuthController } from './oauth.controller';
13
14
  import { OAuthService } from './oauth.service';
14
15
  import { FacebookProvider } from './providers/facebook.provider';
@@ -31,7 +32,15 @@ import { MicrosoftProvider } from './providers/microsoft.provider';
31
32
  forwardRef(() => UserModule),
32
33
  ],
33
34
  controllers: [OAuthController],
34
- providers: [OAuthService, GoogleProvider, FacebookProvider, GithubProvider, MicrosoftProvider, MicrosoftEntraIdProvider],
35
+ providers: [
36
+ OAuthCallbackCoordinatorService,
37
+ OAuthService,
38
+ GoogleProvider,
39
+ FacebookProvider,
40
+ GithubProvider,
41
+ MicrosoftProvider,
42
+ MicrosoftEntraIdProvider,
43
+ ],
35
44
  exports: [OAuthService],
36
45
  })
37
46
  export class OAuthModule {}