@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.
- package/README.md +84 -36
- package/dist/auth/auth.controller.d.ts +0 -3
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +4 -7
- package/dist/auth/auth.controller.js.map +1 -1
- package/dist/auth/auth.service.d.ts.map +1 -1
- package/dist/auth/auth.service.js +6 -3
- package/dist/auth/auth.service.js.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +9 -0
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.controller.js +12 -0
- package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +7 -0
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.js +45 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mail-sent/dto/query-mail-sent-list.dto.d.ts +9 -0
- package/dist/mail-sent/dto/query-mail-sent-list.dto.d.ts.map +1 -0
- package/dist/mail-sent/dto/query-mail-sent-list.dto.js +49 -0
- package/dist/mail-sent/dto/query-mail-sent-list.dto.js.map +1 -0
- package/dist/mail-sent/mail-sent.controller.d.ts +24 -2
- package/dist/mail-sent/mail-sent.controller.d.ts.map +1 -1
- package/dist/mail-sent/mail-sent.controller.js +5 -3
- package/dist/mail-sent/mail-sent.controller.js.map +1 -1
- package/dist/mail-sent/mail-sent.service.d.ts +28 -3
- package/dist/mail-sent/mail-sent.service.d.ts.map +1 -1
- package/dist/mail-sent/mail-sent.service.js +108 -3
- package/dist/mail-sent/mail-sent.service.js.map +1 -1
- package/dist/oauth/oauth-callback-coordinator.service.d.ts +18 -0
- package/dist/oauth/oauth-callback-coordinator.service.d.ts.map +1 -0
- package/dist/oauth/oauth-callback-coordinator.service.js +137 -0
- package/dist/oauth/oauth-callback-coordinator.service.js.map +1 -0
- package/dist/oauth/oauth.controller.d.ts +4 -2
- package/dist/oauth/oauth.controller.d.ts.map +1 -1
- package/dist/oauth/oauth.controller.js +39 -11
- package/dist/oauth/oauth.controller.js.map +1 -1
- package/dist/oauth/oauth.errors.d.ts +16 -0
- package/dist/oauth/oauth.errors.d.ts.map +1 -0
- package/dist/oauth/oauth.errors.js +30 -0
- package/dist/oauth/oauth.errors.js.map +1 -0
- package/dist/oauth/oauth.module.d.ts.map +1 -1
- package/dist/oauth/oauth.module.js +10 -1
- package/dist/oauth/oauth.module.js.map +1 -1
- package/dist/oauth/oauth.service.d.ts +9 -1
- package/dist/oauth/oauth.service.d.ts.map +1 -1
- package/dist/oauth/oauth.service.js +97 -24
- package/dist/oauth/oauth.service.js.map +1 -1
- package/dist/oauth/providers/abstract.provider.d.ts +6 -0
- package/dist/oauth/providers/abstract.provider.d.ts.map +1 -1
- package/dist/oauth/providers/abstract.provider.js +34 -0
- package/dist/oauth/providers/abstract.provider.js.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +3 -0
- package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.js +40 -22
- package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
- package/dist/security/security.service.d.ts +1 -0
- package/dist/security/security.service.d.ts.map +1 -1
- package/dist/security/security.service.js +9 -10
- package/dist/security/security.service.js.map +1 -1
- package/dist/session/session.service.d.ts +5 -1
- package/dist/session/session.service.d.ts.map +1 -1
- package/dist/session/session.service.js +76 -10
- package/dist/session/session.service.js.map +1 -1
- package/dist/setting/setting.service.js +4 -4
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/token/token.service.d.ts +1 -0
- package/dist/token/token.service.d.ts.map +1 -1
- package/dist/token/token.service.js +23 -12
- package/dist/token/token.service.js.map +1 -1
- package/dist/user/user.service.d.ts +1 -1
- package/dist/user/user.service.d.ts.map +1 -1
- package/dist/user/user.service.js +3 -0
- package/dist/user/user.service.js.map +1 -1
- package/hedhog/data/dashboard.yaml +2 -2
- package/hedhog/data/mail.yaml +147 -0
- package/hedhog/data/route.yaml +8 -0
- package/hedhog/frontend/app/account/components/change-email-form.tsx.ejs +87 -8
- package/hedhog/frontend/app/account/components/profile-form.tsx.ejs +90 -10
- package/hedhog/frontend/app/ai_agent/page.tsx.ejs +145 -31
- package/hedhog/frontend/app/configurations/layout.tsx.ejs +1 -1
- package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +67 -2
- package/hedhog/frontend/app/dashboard/components/add-widget-selector-dialog.tsx.ejs +13 -9
- package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +250 -45
- package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +111 -15
- package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +110 -15
- package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +113 -15
- package/hedhog/frontend/app/mail/log/page.tsx.ejs +481 -70
- package/hedhog/frontend/app/menu/page.tsx.ejs +202 -30
- package/hedhog/frontend/app/roles/menus.tsx.ejs +1 -1
- package/hedhog/frontend/app/roles/page.tsx.ejs +288 -104
- package/hedhog/frontend/app/users/page.tsx.ejs +168 -23
- package/hedhog/frontend/messages/en.json +45 -6
- package/hedhog/frontend/messages/pt.json +45 -6
- package/package.json +4 -4
- package/src/auth/auth.controller.ts +15 -19
- package/src/auth/auth.service.ts +13 -8
- package/src/dashboard/dashboard-core/dashboard-core.controller.ts +22 -9
- package/src/dashboard/dashboard-core/dashboard-core.service.ts +75 -1
- package/src/index.ts +10 -7
- package/src/mail-sent/dto/query-mail-sent-list.dto.ts +30 -0
- package/src/mail-sent/mail-sent.controller.ts +14 -2
- package/src/mail-sent/mail-sent.service.ts +159 -4
- package/src/oauth/oauth-callback-coordinator.service.ts +158 -0
- package/src/oauth/oauth.controller.ts +58 -1
- package/src/oauth/oauth.errors.ts +32 -0
- package/src/oauth/oauth.module.ts +10 -1
- package/src/oauth/oauth.service.ts +178 -28
- package/src/oauth/providers/abstract.provider.ts +64 -0
- package/src/oauth/providers/microsoft-entra-id.provider.ts +92 -56
- package/src/security/security.service.ts +12 -10
- package/src/session/session.service.ts +84 -9
- package/src/setting/setting.service.ts +10 -10
- package/src/token/token.service.ts +29 -11
- 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(
|
|
30
|
-
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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: [
|
|
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 {}
|