@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.
Files changed (47) hide show
  1. package/dist/auth/guards/auth.guard.d.ts.map +1 -1
  2. package/dist/auth/guards/auth.guard.js +10 -0
  3. package/dist/auth/guards/auth.guard.js.map +1 -1
  4. package/dist/oauth/oauth.controller.d.ts +7 -8
  5. package/dist/oauth/oauth.controller.d.ts.map +1 -1
  6. package/dist/oauth/oauth.controller.js +4 -5
  7. package/dist/oauth/oauth.controller.js.map +1 -1
  8. package/dist/oauth/oauth.module.d.ts.map +1 -1
  9. package/dist/oauth/oauth.module.js +2 -1
  10. package/dist/oauth/oauth.module.js.map +1 -1
  11. package/dist/oauth/oauth.service.d.ts +3 -2
  12. package/dist/oauth/oauth.service.d.ts.map +1 -1
  13. package/dist/oauth/oauth.service.js +6 -3
  14. package/dist/oauth/oauth.service.js.map +1 -1
  15. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +11 -0
  16. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -0
  17. package/dist/oauth/providers/microsoft-entra-id.provider.js +88 -0
  18. package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -0
  19. package/dist/session/session.controller.d.ts +11 -2
  20. package/dist/session/session.controller.d.ts.map +1 -1
  21. package/dist/session/session.controller.js +21 -11
  22. package/dist/session/session.controller.js.map +1 -1
  23. package/dist/session/session.service.d.ts +9 -2
  24. package/dist/session/session.service.d.ts.map +1 -1
  25. package/dist/session/session.service.js +61 -10
  26. package/dist/session/session.service.js.map +1 -1
  27. package/dist/token/token.module.d.ts.map +1 -1
  28. package/dist/token/token.module.js +2 -0
  29. package/dist/token/token.module.js.map +1 -1
  30. package/dist/token/token.service.d.ts +2 -2
  31. package/dist/token/token.service.d.ts.map +1 -1
  32. package/dist/token/token.service.js +26 -17
  33. package/dist/token/token.service.js.map +1 -1
  34. package/hedhog/data/route.yaml +10 -0
  35. package/hedhog/data/setting_group.yaml +51 -0
  36. package/package.json +4 -4
  37. package/src/auth/guards/auth.guard.ts +18 -5
  38. package/src/language/en.json +2 -1
  39. package/src/language/pt.json +2 -1
  40. package/src/oauth/oauth.controller.ts +21 -14
  41. package/src/oauth/oauth.module.ts +2 -1
  42. package/src/oauth/oauth.service.ts +7 -4
  43. package/src/oauth/providers/microsoft-entra-id.provider.ts +76 -0
  44. package/src/session/session.controller.ts +19 -10
  45. package/src/session/session.service.ts +80 -11
  46. package/src/token/token.module.ts +2 -0
  47. 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
- @Role()
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(@User() { id: userId }, @Param('sessionId') sessionId: number){
38
- return this.sessionService.revokeUserSession(userId, sessionId)
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('Erro ao buscar sessões do usuário', HttpStatus.SERVICE_UNAVAILABLE);
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.findFirst({
193
- where: { user_id: userId },
194
- orderBy: { created_at: 'desc' },
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, { NOT: { id: latestSession.id } }, 'revokeAllOtherSessions');
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.user.registerUserActivity(userId, "revokeSession")
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
- const session = await this.prisma.session.findUnique({
31
- where: { id: payload.sessionId },
32
- select: { revoked_at: true }
33
- });
34
-
35
- if (!session || session.revoked_at !== null) {
36
- throw new UnauthorizedException(
37
- getLocaleText('sessionRevoked', locale, 'Session has been revoked.')
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