@hed-hog/core 0.0.141 → 0.0.150
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/dist/auth/guards/auth.guard.d.ts.map +1 -1
- package/dist/auth/guards/auth.guard.js +10 -0
- package/dist/auth/guards/auth.guard.js.map +1 -1
- package/dist/oauth/oauth.controller.d.ts +7 -8
- package/dist/oauth/oauth.controller.d.ts.map +1 -1
- package/dist/oauth/oauth.controller.js +4 -5
- package/dist/oauth/oauth.controller.js.map +1 -1
- package/dist/oauth/oauth.module.d.ts.map +1 -1
- package/dist/oauth/oauth.module.js +2 -1
- package/dist/oauth/oauth.module.js.map +1 -1
- package/dist/oauth/oauth.service.d.ts +3 -2
- package/dist/oauth/oauth.service.d.ts.map +1 -1
- package/dist/oauth/oauth.service.js +6 -3
- package/dist/oauth/oauth.service.js.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +11 -0
- package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -0
- package/dist/oauth/providers/microsoft-entra-id.provider.js +88 -0
- package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -0
- package/dist/session/session.controller.d.ts +11 -2
- package/dist/session/session.controller.d.ts.map +1 -1
- package/dist/session/session.controller.js +21 -11
- package/dist/session/session.controller.js.map +1 -1
- package/dist/session/session.service.d.ts +9 -2
- package/dist/session/session.service.d.ts.map +1 -1
- package/dist/session/session.service.js +61 -10
- package/dist/session/session.service.js.map +1 -1
- package/dist/token/token.module.d.ts.map +1 -1
- package/dist/token/token.module.js +2 -0
- package/dist/token/token.module.js.map +1 -1
- package/dist/token/token.service.d.ts +2 -2
- package/dist/token/token.service.d.ts.map +1 -1
- package/dist/token/token.service.js +26 -17
- package/dist/token/token.service.js.map +1 -1
- package/hedhog/data/route.yaml +10 -0
- package/hedhog/data/setting_group.yaml +51 -0
- package/package.json +4 -4
- package/src/auth/guards/auth.guard.ts +18 -5
- package/src/language/en.json +2 -1
- package/src/language/pt.json +2 -1
- package/src/oauth/oauth.controller.ts +21 -14
- package/src/oauth/oauth.module.ts +2 -1
- package/src/oauth/oauth.service.ts +7 -4
- package/src/oauth/providers/microsoft-entra-id.provider.ts +76 -0
- package/src/session/session.controller.ts +19 -10
- package/src/session/session.service.ts +80 -11
- package/src/token/token.module.ts +2 -0
- package/src/token/token.service.ts +22 -13
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { HttpService } from '@nestjs/axios';
|
|
2
|
+
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
|
3
|
+
import { SettingService } from '../../setting/setting.service';
|
|
4
|
+
import { BaseOAuthProvider } from './abstract.provider';
|
|
5
|
+
|
|
6
|
+
@Injectable()
|
|
7
|
+
export class MicrosoftEntraIdProvider extends BaseOAuthProvider {
|
|
8
|
+
constructor(
|
|
9
|
+
http: HttpService,
|
|
10
|
+
@Inject(forwardRef(() => SettingService))
|
|
11
|
+
private readonly setting: SettingService,
|
|
12
|
+
) {
|
|
13
|
+
super(http);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getProviderType() {
|
|
17
|
+
return 'MICROSOFT_ENTRA_ID';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async getAuthUrl(callbackPath: string) {
|
|
21
|
+
const settings = await this.setting.getSettingValues([
|
|
22
|
+
'microsoft_client_id',
|
|
23
|
+
'microsoft_client_secret',
|
|
24
|
+
'microsoft_scopes',
|
|
25
|
+
'microsoft_tenant_id',
|
|
26
|
+
'url',
|
|
27
|
+
]);
|
|
28
|
+
const tenantId = settings['microsoft_tenant_id'];
|
|
29
|
+
const redirectURI = new URL(callbackPath, settings['url']).toString();
|
|
30
|
+
const scopes = settings['microsoft_scopes'];
|
|
31
|
+
const params = new URLSearchParams({
|
|
32
|
+
client_id: settings['microsoft_client_id'],
|
|
33
|
+
redirect_uri: redirectURI,
|
|
34
|
+
response_type: 'code',
|
|
35
|
+
scope: scopes.join(' '),
|
|
36
|
+
response_mode: 'query',
|
|
37
|
+
prompt: 'consent',
|
|
38
|
+
});
|
|
39
|
+
return `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?${params}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async getProfile(code: string, type: string): Promise<any> {
|
|
43
|
+
const settings = await this.setting.getSettingValues([
|
|
44
|
+
'microsoft_client_id',
|
|
45
|
+
'microsoft_client_secret',
|
|
46
|
+
'microsoft_scopes',
|
|
47
|
+
'microsoft_tenant_id',
|
|
48
|
+
'url',
|
|
49
|
+
]);
|
|
50
|
+
const tenantId = settings['microsoft_tenant_id'];
|
|
51
|
+
const token = await this.fetchToken({
|
|
52
|
+
code,
|
|
53
|
+
url: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
|
|
54
|
+
clientId: settings['microsoft_client_id'],
|
|
55
|
+
clientSecret: settings['microsoft_client_secret'],
|
|
56
|
+
redirectUri: `${settings['url']}/callback/microsoft-entra-id/${type}`,
|
|
57
|
+
});
|
|
58
|
+
const profile = await this.fetchProfile(
|
|
59
|
+
token.access_token,
|
|
60
|
+
'https://graph.microsoft.com/v1.0/me',
|
|
61
|
+
);
|
|
62
|
+
const pictureUrl = 'https://graph.microsoft.com/v1.0/me/photo/$value';
|
|
63
|
+
return {
|
|
64
|
+
id: profile.id,
|
|
65
|
+
email: profile.mail || profile.userPrincipalName,
|
|
66
|
+
name: profile.displayName,
|
|
67
|
+
picture: pictureUrl,
|
|
68
|
+
oauth_tokens: {
|
|
69
|
+
access_token: token.access_token,
|
|
70
|
+
refresh_token: token.refresh_token,
|
|
71
|
+
expires_in: token.expires_in,
|
|
72
|
+
token_type: token.token_type,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Role, User } from '@hed-hog/api';
|
|
1
|
+
import { Role, Session, User } from '@hed-hog/api';
|
|
2
2
|
import { Locale } from '@hed-hog/api-locale';
|
|
3
3
|
import { Pagination, PaginationDTO } from '@hed-hog/api-pagination';
|
|
4
|
-
import { Controller, Delete, Get, Param } from '@nestjs/common';
|
|
4
|
+
import { Controller, Delete, Get, Param, ParseIntPipe } from '@nestjs/common';
|
|
5
5
|
import { SessionService } from './session.service';
|
|
6
6
|
@Role()
|
|
7
7
|
@Controller('sessions')
|
|
@@ -10,7 +10,15 @@ export class SessionController {
|
|
|
10
10
|
private readonly sessionService: SessionService
|
|
11
11
|
) {}
|
|
12
12
|
|
|
13
|
-
@
|
|
13
|
+
@Get('active')
|
|
14
|
+
async getUserSessionsActive(
|
|
15
|
+
@Pagination() paginationParams: PaginationDTO,
|
|
16
|
+
@User() { id },
|
|
17
|
+
@Locale() locale: string
|
|
18
|
+
) {
|
|
19
|
+
return this.sessionService.getUserSessionsActive(paginationParams, id,locale)
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
@Get('user')
|
|
15
23
|
async getUserSessions(
|
|
16
24
|
@Pagination() paginationParams: PaginationDTO,
|
|
@@ -20,21 +28,22 @@ export class SessionController {
|
|
|
20
28
|
return this.sessionService.getUserSessions(paginationParams, id,locale)
|
|
21
29
|
}
|
|
22
30
|
|
|
23
|
-
@Role()
|
|
24
31
|
@Delete('revoke-all-other')
|
|
25
|
-
async revokeAllOtherSessions(@User() { id }){
|
|
26
|
-
return this.sessionService.revokeAllOtherSessions(id)
|
|
32
|
+
async revokeAllOtherSessions(@User() { id }, @Session() sessionId: number){
|
|
33
|
+
return this.sessionService.revokeAllOtherSessions(id, sessionId)
|
|
27
34
|
}
|
|
28
35
|
|
|
29
|
-
@Role()
|
|
30
36
|
@Delete('revoke-all')
|
|
31
37
|
async revokeAllSessions(@User() { id }){
|
|
32
38
|
return this.sessionService.revokeAllSessions(id)
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
@Role()
|
|
36
41
|
@Delete(':sessionId/revoke')
|
|
37
|
-
async revokeSession(
|
|
38
|
-
|
|
42
|
+
async revokeSession(
|
|
43
|
+
@User() { id: userId },
|
|
44
|
+
@Param('sessionId', ParseIntPipe) sessionId: number,
|
|
45
|
+
@Locale() locale: string
|
|
46
|
+
){
|
|
47
|
+
return this.sessionService.revokeUserSession(userId, sessionId, locale)
|
|
39
48
|
}
|
|
40
49
|
}
|
|
@@ -2,7 +2,7 @@ import { getLocaleText } from '@hed-hog/api-locale';
|
|
|
2
2
|
import { PaginationDTO, PaginationService } from '@hed-hog/api-pagination';
|
|
3
3
|
import { PrismaService } from '@hed-hog/api-prisma';
|
|
4
4
|
import { HttpService } from '@nestjs/axios';
|
|
5
|
-
import { BadRequestException, forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
|
|
5
|
+
import { BadRequestException, forwardRef, HttpException, HttpStatus, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
|
6
6
|
import { firstValueFrom } from 'rxjs';
|
|
7
7
|
import { SecurityService } from '../security/security.service';
|
|
8
8
|
import { SettingService } from '../setting/setting.service';
|
|
@@ -157,7 +157,59 @@ export class SessionService {
|
|
|
157
157
|
pageSize: paginate.pageSize ?? paginationParams.pageSize ?? 10,
|
|
158
158
|
};
|
|
159
159
|
} catch (err) {
|
|
160
|
-
throw new HttpException(
|
|
160
|
+
throw new HttpException(
|
|
161
|
+
getLocaleText('session.errorFetchingSessions', locale, 'Error fetching user sessions'),
|
|
162
|
+
HttpStatus.SERVICE_UNAVAILABLE
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async getUserSessionsActive(paginationParams: PaginationDTO, userId: number, locale: string) {
|
|
168
|
+
|
|
169
|
+
const userExists = await this.prisma.user.findUnique({
|
|
170
|
+
where: { id: userId },
|
|
171
|
+
select: { id: true },
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (!userExists) {
|
|
175
|
+
throw new BadRequestException(getLocaleText('session.userNotFound', locale, 'User not found.'));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const paginate = await this.paginationService.paginatePrismaModel(this.prisma.user_session, {
|
|
180
|
+
...paginationParams,
|
|
181
|
+
where: { user_id: userId, revoked_at: null, expires_at: { gt: new Date() } },
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const itemsWithLocation = await Promise.all(
|
|
185
|
+
paginate.data.map(async (s) => {
|
|
186
|
+
const ip = s.ip_address || s.ip || null;
|
|
187
|
+
let location: GeoIpResult | null = null;
|
|
188
|
+
if (ip && ip !== '127.0.0.1' && ip !== '::1') {
|
|
189
|
+
try {
|
|
190
|
+
location = await this.fetchGeoByIp(ip);
|
|
191
|
+
} catch {
|
|
192
|
+
location = { ip, raw: null };
|
|
193
|
+
}
|
|
194
|
+
} else if (ip) {
|
|
195
|
+
location = { ip: '127.0.0.1', country: 'Localhost', region: '', city: '' };
|
|
196
|
+
}
|
|
197
|
+
return { ...s, location };
|
|
198
|
+
})
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
data: itemsWithLocation,
|
|
203
|
+
total: paginate.total || 0,
|
|
204
|
+
lastPage: Math.ceil((paginate.total || 0) / (paginate.pageSize || 1)),
|
|
205
|
+
page: paginate.page ?? 1,
|
|
206
|
+
pageSize: paginate.pageSize ?? paginationParams.pageSize ?? 10,
|
|
207
|
+
};
|
|
208
|
+
} catch (err) {
|
|
209
|
+
throw new HttpException(
|
|
210
|
+
getLocaleText('session.errorFetchingSessions', locale, 'Error fetching user sessions'),
|
|
211
|
+
HttpStatus.SERVICE_UNAVAILABLE
|
|
212
|
+
);
|
|
161
213
|
}
|
|
162
214
|
}
|
|
163
215
|
|
|
@@ -188,27 +240,44 @@ export class SessionService {
|
|
|
188
240
|
});
|
|
189
241
|
}
|
|
190
242
|
|
|
191
|
-
async revokeAllOtherSessions(userId: number) {
|
|
192
|
-
const latestSession = await this.prisma.user_session.
|
|
193
|
-
where: {
|
|
194
|
-
|
|
243
|
+
async revokeAllOtherSessions(userId: number, sessionId: number) {
|
|
244
|
+
const latestSession = await this.prisma.user_session.findUnique({
|
|
245
|
+
where: {
|
|
246
|
+
id: sessionId
|
|
247
|
+
},
|
|
195
248
|
select: { id: true },
|
|
196
249
|
});
|
|
197
250
|
|
|
198
251
|
if (!latestSession) { return { count: 0 } }
|
|
199
|
-
return this.markRevokedByFilter(userId, {
|
|
252
|
+
return this.markRevokedByFilter(userId, {
|
|
253
|
+
NOT: { id: latestSession.id },
|
|
254
|
+
revoked_at: null
|
|
255
|
+
}, 'revokeAllOtherSessions');
|
|
200
256
|
}
|
|
201
257
|
|
|
202
258
|
async revokeAllSessions(userId: number) {
|
|
203
|
-
return this.markRevokedByFilter(userId, {}, 'revokeAllSessions');
|
|
259
|
+
return this.markRevokedByFilter(userId, { revoked_at: null }, 'revokeAllSessions');
|
|
204
260
|
}
|
|
205
261
|
|
|
206
|
-
async revokeUserSession(userId: number, sessionId: number){
|
|
207
|
-
await this.
|
|
208
|
-
return this.prisma.user_session.update({
|
|
262
|
+
async revokeUserSession(userId: number, sessionId: number, locale: string){
|
|
263
|
+
const session = await this.prisma.user_session.findFirst({
|
|
209
264
|
where: {
|
|
210
265
|
id: sessionId,
|
|
211
266
|
user_id: userId
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (!session) {
|
|
271
|
+
throw new NotFoundException(
|
|
272
|
+
getLocaleText('session.notFound', locale, 'Session not found or does not belong to user')
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
await this.user.registerUserActivity(userId, "revokeSession");
|
|
277
|
+
|
|
278
|
+
return this.prisma.user_session.update({
|
|
279
|
+
where: {
|
|
280
|
+
id: sessionId
|
|
212
281
|
},
|
|
213
282
|
data: {
|
|
214
283
|
revoked_at: new Date()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PrismaModule } from "@hed-hog/api-prisma";
|
|
1
2
|
import { forwardRef, Module } from "@nestjs/common";
|
|
2
3
|
import { SecurityModule } from "../security/security.module";
|
|
3
4
|
import { SettingModule } from "../setting/setting.module";
|
|
@@ -7,6 +8,7 @@ import { TokenService } from "./token.service";
|
|
|
7
8
|
providers: [TokenService],
|
|
8
9
|
exports: [TokenService],
|
|
9
10
|
imports: [
|
|
11
|
+
forwardRef(() => PrismaModule),
|
|
10
12
|
forwardRef(() => SettingModule),
|
|
11
13
|
forwardRef(() => SecurityModule),
|
|
12
14
|
]
|
|
@@ -9,33 +9,42 @@ import { SettingService } from "../setting/setting.service";
|
|
|
9
9
|
export class TokenService {
|
|
10
10
|
|
|
11
11
|
constructor(
|
|
12
|
+
private readonly prisma: PrismaService,
|
|
12
13
|
@Inject(forwardRef(() => JwtService))
|
|
13
14
|
private readonly jwt: JwtService,
|
|
14
15
|
@Inject(forwardRef(() => SecurityService))
|
|
15
16
|
private readonly security: SecurityService,
|
|
16
17
|
@Inject(forwardRef(() => SettingService))
|
|
17
18
|
private readonly setting: SettingService,
|
|
18
|
-
@Inject(forwardRef(() => PrismaService))
|
|
19
|
-
private readonly prisma: PrismaService,
|
|
20
19
|
) { }
|
|
21
20
|
|
|
22
21
|
async verify(locale: string, token: string) {
|
|
23
22
|
try {
|
|
23
|
+
|
|
24
24
|
const payload = await this.jwt.verifyAsync(token, {
|
|
25
25
|
secret: this.security.getJwtSecret(),
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
// Verify session is not revoked
|
|
29
|
-
if (payload.sessionId) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
// Verify session is not revoked (only if prisma is available)
|
|
29
|
+
if (payload.sessionId && this.prisma) {
|
|
30
|
+
try {
|
|
31
|
+
const session = await this.prisma.user_session.findUnique({
|
|
32
|
+
where: { id: payload.sessionId },
|
|
33
|
+
select: { revoked_at: true, expires_at: true }
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (!session || session.revoked_at !== null || session.expires_at <= new Date()) {
|
|
37
|
+
throw new UnauthorizedException(
|
|
38
|
+
getLocaleText('sessionRevoked', locale, 'Session has been revoked.')
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
} catch (sessionError) {
|
|
42
|
+
// If it's an Unauthorized error from revoked session, rethrow it
|
|
43
|
+
if (sessionError instanceof UnauthorizedException) {
|
|
44
|
+
throw sessionError;
|
|
45
|
+
}
|
|
46
|
+
// Otherwise, log the error but allow auth to continue
|
|
47
|
+
console.error('Session validation error:', sessionError);
|
|
39
48
|
}
|
|
40
49
|
}
|
|
41
50
|
|