@plyaz/auth 1.0.0 → 1.0.2

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 (99) hide show
  1. package/commits.txt +3 -3
  2. package/dist/common/index.cjs +3 -1
  3. package/dist/common/index.cjs.map +1 -1
  4. package/dist/common/index.mjs +3 -1
  5. package/dist/common/index.mjs.map +1 -1
  6. package/dist/index.cjs +424 -154
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.mjs +421 -152
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +2 -1
  11. package/release_message.txt +28 -0
  12. package/src/adapters/auth-adapter-factory.ts +4 -3
  13. package/src/adapters/auth-adapter.mapper.ts +2 -2
  14. package/src/adapters/base-auth.adapter.ts +17 -9
  15. package/src/adapters/clerk/clerk.adapter.ts +9 -12
  16. package/src/adapters/custom/custom.adapter.ts +19 -10
  17. package/src/adapters/index.ts +0 -1
  18. package/src/adapters/next-auth/authOptions.ts +20 -16
  19. package/src/adapters/next-auth/next-auth.adapter.ts +13 -15
  20. package/src/api/client.ts +4 -6
  21. package/src/audit/audit.logger.ts +19 -10
  22. package/src/client/components/ProtectedRoute.tsx +15 -11
  23. package/src/client/hooks/useAuth.ts +23 -21
  24. package/src/client/hooks/useConnectedAccounts.ts +57 -45
  25. package/src/client/hooks/usePermissions.ts +1 -1
  26. package/src/client/hooks/useRBAC.ts +6 -6
  27. package/src/client/hooks/useSession.ts +5 -5
  28. package/src/client/providers/AuthProvider.tsx +23 -17
  29. package/src/client/store/auth.store.ts +71 -62
  30. package/src/client/utils/storage.ts +45 -18
  31. package/src/common/constants/oauth-providers.ts +10 -7
  32. package/src/common/errors/auth.errors.ts +4 -4
  33. package/src/common/errors/specific-auth-errors.ts +5 -9
  34. package/src/common/regex/index.ts +6 -4
  35. package/src/common/types/auth.types.ts +47 -38
  36. package/src/common/types/index.ts +12 -6
  37. package/src/common/utils/index.ts +15 -11
  38. package/src/core/blacklist/token.blacklist.ts +13 -7
  39. package/src/core/index.ts +2 -2
  40. package/src/core/jwt/jwt.manager.ts +47 -22
  41. package/src/core/session/session.manager.ts +17 -14
  42. package/src/db/repositories/connected-account.repository.ts +120 -78
  43. package/src/db/repositories/role.repository.ts +41 -26
  44. package/src/db/repositories/session.repository.ts +9 -10
  45. package/src/db/repositories/user.repository.ts +105 -91
  46. package/src/flows/index.ts +2 -2
  47. package/src/flows/sign-in.flow.ts +28 -14
  48. package/src/flows/sign-up.flow.ts +31 -20
  49. package/src/index.ts +36 -37
  50. package/src/libs/clerk.helper.ts +6 -7
  51. package/src/libs/supabase.helper.ts +79 -61
  52. package/src/libs/supabaseClient.ts +3 -3
  53. package/src/providers/base/auth-provider.interface.ts +13 -11
  54. package/src/providers/base/index.ts +1 -1
  55. package/src/providers/index.ts +1 -1
  56. package/src/providers/oauth/facebook.provider.ts +63 -39
  57. package/src/providers/oauth/github.provider.ts +14 -10
  58. package/src/providers/oauth/google.provider.ts +39 -28
  59. package/src/providers/oauth/index.ts +1 -1
  60. package/src/rbac/dynamic-roles.ts +88 -54
  61. package/src/rbac/index.ts +4 -4
  62. package/src/rbac/permission-checker.ts +147 -75
  63. package/src/rbac/role-hierarchy.ts +8 -8
  64. package/src/rbac/role.manager.ts +11 -8
  65. package/src/security/csrf/csrf.protection.ts +9 -7
  66. package/src/security/index.ts +2 -2
  67. package/src/security/rate-limiting/auth/auth.controller.ts +2 -4
  68. package/src/security/rate-limiting/auth/rate-limiting.interface.ts +26 -6
  69. package/src/security/rate-limiting/auth.module.ts +1 -2
  70. package/src/server/auth.module.ts +55 -52
  71. package/src/server/decorators/auth.decorator.ts +9 -11
  72. package/src/server/decorators/auth.decorators.ts +8 -9
  73. package/src/server/decorators/current-user.decorator.ts +6 -6
  74. package/src/server/decorators/permission.decorator.ts +17 -9
  75. package/src/server/guards/auth.guard.ts +21 -16
  76. package/src/server/guards/custom-throttler.guard.ts +4 -9
  77. package/src/server/guards/permissions.guard.ts +32 -23
  78. package/src/server/guards/roles.guard.ts +14 -12
  79. package/src/server/middleware/auth.middleware.ts +4 -4
  80. package/src/server/middleware/session.middleware.ts +4 -4
  81. package/src/server/services/account.service.ts +96 -48
  82. package/src/server/services/auth.service.ts +57 -28
  83. package/src/server/services/brute-force.service.ts +24 -19
  84. package/src/server/services/index.ts +1 -1
  85. package/src/server/services/rate-limiter.service.ts +9 -4
  86. package/src/server/services/session.service.ts +84 -48
  87. package/src/server/services/token.service.ts +71 -51
  88. package/src/session/cookie-store.ts +47 -34
  89. package/src/session/enhanced-session-manager.ts +69 -48
  90. package/src/session/index.ts +5 -5
  91. package/src/session/memory-store.ts +37 -30
  92. package/src/session/redis-store.ts +105 -72
  93. package/src/strategies/oauth.strategy.ts +10 -9
  94. package/src/strategies/traditional-auth.strategy.ts +41 -29
  95. package/src/tokens/index.ts +4 -4
  96. package/src/tokens/refresh-token-manager.ts +70 -55
  97. package/src/tokens/token-validator.ts +109 -53
  98. package/vitest.setup.d.ts +2 -2
  99. package/vitest.setup.ts +1 -1
@@ -1,31 +1,33 @@
1
- import type { CanActivate, ExecutionContext } from '@nestjs/common';
2
- import { Injectable } from '@nestjs/common';
3
- import type { Reflector } from '@nestjs/core';
4
- import type { RoleManager } from '../../rbac/role.manager';
5
- import { AuthenticationError } from '@plyaz/errors';
6
-
1
+ import type { CanActivate, ExecutionContext } from "@nestjs/common";
2
+ import { Injectable } from "@nestjs/common";
3
+ import type { Reflector } from "@nestjs/core";
4
+ import type { RoleManager } from "../../rbac/role.manager";
5
+ import { AuthenticationError } from "@plyaz/errors";
7
6
 
8
7
  @Injectable()
9
8
  export class RolesGuard implements CanActivate {
10
9
  constructor(
11
10
  private roleManager: RoleManager,
12
- private reflector: Reflector
11
+ private reflector: Reflector,
13
12
  ) {}
14
13
 
15
14
  async canActivate(context: ExecutionContext): Promise<boolean> {
16
- const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
15
+ const requiredRoles = this.reflector.get<string[]>(
16
+ "roles",
17
+ context.getHandler(),
18
+ );
17
19
  if (!requiredRoles) return true;
18
20
 
19
21
  const request = context.switchToHttp().getRequest();
20
22
  const user = request.user;
21
-
22
- if (!user) throw new AuthenticationError('AUTH_INVALID_CREDENTIALS');
23
+
24
+ if (!user) throw new AuthenticationError("AUTH_INVALID_CREDENTIALS");
23
25
 
24
26
  const hasRole = await this.roleManager.hasAnyRole(user.sub, requiredRoles);
25
27
  if (!hasRole) {
26
- throw new AuthenticationError('AUTH_ROLE_REQUIRED');
28
+ throw new AuthenticationError("AUTH_ROLE_REQUIRED");
27
29
  }
28
30
 
29
31
  return true;
30
32
  }
31
- }
33
+ }
@@ -13,10 +13,10 @@ export interface AuthRequest extends Request {
13
13
  /**
14
14
  * Middleware to extract the session ID from incoming requests.
15
15
  *
16
- * This middleware does **not validate** the session.
16
+ * This middleware does **not validate** the session.
17
17
  * It only reads the session ID from:
18
- * - Cookies (`session_id`)
19
- * - Headers (`x-session-id`)
18
+ * - Cookies (`session_id`)
19
+ * - Headers (`x-session-id`)
20
20
  * and attaches it to `req.sessionId` for use in guards or controllers.
21
21
  *
22
22
  * @example
@@ -32,7 +32,7 @@ export class AuthMiddleware implements NestMiddleware {
32
32
  * @param res - Express response object
33
33
  * @param next - Function to pass control to the next middleware
34
34
  */
35
- use(req: AuthRequest, res: Response, next: NextFunction):void {
35
+ use(req: AuthRequest, res: Response, next: NextFunction): void {
36
36
  // Read session cookie (or header)
37
37
  const sessionId = req.cookies?.session_id ?? req.headers["x-session-id"];
38
38
 
@@ -55,7 +55,7 @@ export class SessionMiddleware implements NestMiddleware {
55
55
 
56
56
  constructor(
57
57
  private tokenService: TokenService,
58
- private sessionService: SessionService
58
+ private sessionService: SessionService,
59
59
  ) {
60
60
  this.config = {
61
61
  refreshThreshold: 300, // 5 minutes
@@ -132,7 +132,7 @@ export class SessionMiddleware implements NestMiddleware {
132
132
  private async refreshTokens(
133
133
  req: Request,
134
134
  res: Response,
135
- refreshToken: string
135
+ refreshToken: string,
136
136
  ): Promise<void> {
137
137
  try {
138
138
  this.logger.debug("Refreshing tokens");
@@ -156,7 +156,7 @@ export class SessionMiddleware implements NestMiddleware {
156
156
  const tokenPair = await this.tokenService.refreshTokenPair(
157
157
  refreshToken,
158
158
  userRoles,
159
- userPermissions
159
+ userPermissions,
160
160
  );
161
161
 
162
162
  // Set new tokens in cookies
@@ -211,7 +211,7 @@ export class SessionMiddleware implements NestMiddleware {
211
211
  private setTokenCookies(
212
212
  res: Response,
213
213
  accessToken: string,
214
- refreshToken: string
214
+ refreshToken: string,
215
215
  ): void {
216
216
  const fifteen = 15;
217
217
  // Set access token cookie (shorter expiry)
@@ -1,20 +1,20 @@
1
1
  /**
2
2
  * @fileoverview Account service for @plyaz/auth
3
3
  * @module @plyaz/auth/server/services/account-service
4
- *
4
+ *
5
5
  * @description
6
6
  * NestJS service for connected account management operations.
7
7
  * Provides high-level operations for linking/unlinking provider accounts,
8
8
  * managing primary accounts, and handling account-related business logic.
9
- *
9
+ *
10
10
  * @example
11
11
  * ```typescript
12
12
  * import { AccountService } from '@plyaz/auth';
13
- *
13
+ *
14
14
  * @Controller('accounts')
15
15
  * export class AccountsController {
16
16
  * constructor(private accountService: AccountService) {}
17
- *
17
+ *
18
18
  * @Post('link')
19
19
  * async linkAccount(@Body() data, @CurrentUser() user) {
20
20
  * return await this.accountService.link(user.id, data.provider, data);
@@ -23,11 +23,11 @@
23
23
  * ```
24
24
  */
25
25
 
26
- import { Injectable, Logger } from '@nestjs/common';
27
- import type { ConnectedAccountRepository } from '../../db/repositories/connected-account.repository';
26
+ import { Injectable, Logger } from "@nestjs/common";
27
+ import type { ConnectedAccountRepository } from "../../db/repositories/connected-account.repository";
28
28
 
29
- import type { ConnectedAccount } from '@plyaz/types';
30
- import { AUTH_EVENTS } from '@plyaz/types';
29
+ import type { ConnectedAccount } from "@plyaz/types";
30
+ import { AUTH_EVENTS } from "@plyaz/types";
31
31
 
32
32
  /**
33
33
  * Account service implementation
@@ -48,30 +48,35 @@ export class AccountService {
48
48
  * @returns Created connected account
49
49
  */
50
50
 
51
- async link(userId: string, provider: string, providerData: ConnectedAccount): Promise<ConnectedAccount> {
51
+ async link(
52
+ userId: string,
53
+ provider: string,
54
+ providerData: ConnectedAccount,
55
+ ): Promise<ConnectedAccount> {
52
56
  this.logger.debug(`Linking ${provider} account for user: ${userId}`);
53
-
54
- try {
55
-
56
57
 
58
+ try {
57
59
  // Link the account
58
60
  const connectedAccount = await this.accountRepository.linkAccount(
59
61
  userId,
60
62
  provider,
61
- providerData
63
+ providerData,
62
64
  );
63
-
65
+
64
66
  this.logger.log(`${provider} account linked for user: ${userId}`);
65
67
  this.emitEvent(AUTH_EVENTS.ACCOUNT_LINKED, {
66
68
  userId,
67
69
  provider,
68
70
  accountId: connectedAccount.id,
69
- timestamp: new Date()
71
+ timestamp: new Date(),
70
72
  });
71
-
73
+
72
74
  return connectedAccount;
73
75
  } catch (error) {
74
- this.logger.error(`Failed to link ${provider} account for user: ${userId}`, error);
76
+ this.logger.error(
77
+ `Failed to link ${provider} account for user: ${userId}`,
78
+ error,
79
+ );
75
80
  throw error;
76
81
  }
77
82
  }
@@ -84,7 +89,7 @@ export class AccountService {
84
89
  */
85
90
  async unlink(userId: string, accountId: string): Promise<void> {
86
91
  this.logger.debug(`Unlinking account ${accountId} for user: ${userId}`);
87
-
92
+
88
93
  try {
89
94
  // Get account details
90
95
  const account = await this.accountRepository.findById(accountId);
@@ -94,21 +99,24 @@ export class AccountService {
94
99
 
95
100
  // Verify account belongs to user
96
101
  if (account?.userId !== userId) {
97
- throw new Error('Account does not belong to user');
102
+ throw new Error("Account does not belong to user");
98
103
  }
99
104
 
100
105
  // Unlink the account (includes validation for last auth method)
101
106
  await this.accountRepository.unlinkAccount(accountId);
102
-
107
+
103
108
  this.logger.log(`Account ${accountId} unlinked for user: ${userId}`);
104
109
  this.emitEvent(AUTH_EVENTS.ACCOUNT_UNLINKED, {
105
110
  userId,
106
111
  provider: account?.provider,
107
112
  accountId,
108
- timestamp: new Date()
113
+ timestamp: new Date(),
109
114
  });
110
115
  } catch (error) {
111
- this.logger.error(`Failed to unlink account ${accountId} for user: ${userId}`, error);
116
+ this.logger.error(
117
+ `Failed to unlink account ${accountId} for user: ${userId}`,
118
+ error,
119
+ );
112
120
  throw error;
113
121
  }
114
122
  }
@@ -120,7 +128,7 @@ export class AccountService {
120
128
  */
121
129
  async getAll(userId: string): Promise<ConnectedAccount[]> {
122
130
  this.logger.debug(`Getting all accounts for user: ${userId}`);
123
-
131
+
124
132
  try {
125
133
  return await this.accountRepository.findByUserId(userId);
126
134
  } catch (error) {
@@ -135,21 +143,28 @@ export class AccountService {
135
143
  * @param accountId - Account identifier to set as primary
136
144
  */
137
145
  async setPrimary(userId: string, accountId: string): Promise<void> {
138
- this.logger.debug(`Setting primary account ${accountId} for user: ${userId}`);
139
-
146
+ this.logger.debug(
147
+ `Setting primary account ${accountId} for user: ${userId}`,
148
+ );
149
+
140
150
  try {
141
151
  // Verify account belongs to user
142
152
  const account = await this.accountRepository.findById(accountId);
143
153
  if (account?.userId !== userId) {
144
- throw new Error('Account not found or does not belong to user');
154
+ throw new Error("Account not found or does not belong to user");
145
155
  }
146
156
 
147
157
  // Set as primary
148
158
  await this.accountRepository.setPrimary(userId, accountId);
149
-
150
- this.logger.log(`Primary account set to ${accountId} for user: ${userId}`);
159
+
160
+ this.logger.log(
161
+ `Primary account set to ${accountId} for user: ${userId}`,
162
+ );
151
163
  } catch (error) {
152
- this.logger.error(`Failed to set primary account ${accountId} for user: ${userId}`, error);
164
+ this.logger.error(
165
+ `Failed to set primary account ${accountId} for user: ${userId}`,
166
+ error,
167
+ );
153
168
  throw error;
154
169
  }
155
170
  }
@@ -161,11 +176,14 @@ export class AccountService {
161
176
  */
162
177
  async getPrimary(userId: string): Promise<ConnectedAccount | null> {
163
178
  this.logger.debug(`Getting primary account for user: ${userId}`);
164
-
179
+
165
180
  try {
166
181
  return await this.accountRepository.findPrimary(userId);
167
182
  } catch (error) {
168
- this.logger.error(`Failed to get primary account for user: ${userId}`, error);
183
+ this.logger.error(
184
+ `Failed to get primary account for user: ${userId}`,
185
+ error,
186
+ );
169
187
  throw error;
170
188
  }
171
189
  }
@@ -176,13 +194,24 @@ export class AccountService {
176
194
  * @param providerAccountId - Provider account ID
177
195
  * @returns Connected account or null
178
196
  */
179
- async findByProvider(provider: string, providerAccountId: string): Promise<ConnectedAccount | null> {
180
- this.logger.debug(`Finding account by provider: ${provider}, ID: ${providerAccountId}`);
181
-
197
+ async findByProvider(
198
+ provider: string,
199
+ providerAccountId: string,
200
+ ): Promise<ConnectedAccount | null> {
201
+ this.logger.debug(
202
+ `Finding account by provider: ${provider}, ID: ${providerAccountId}`,
203
+ );
204
+
182
205
  try {
183
- return await this.accountRepository.findByProvider(provider, providerAccountId);
206
+ return await this.accountRepository.findByProvider(
207
+ provider,
208
+ providerAccountId,
209
+ );
184
210
  } catch (error) {
185
- this.logger.error(`Failed to find account by provider: ${provider}`, error);
211
+ this.logger.error(
212
+ `Failed to find account by provider: ${provider}`,
213
+ error,
214
+ );
186
215
  throw error;
187
216
  }
188
217
  }
@@ -193,14 +222,25 @@ export class AccountService {
193
222
  * @param accessToken - New access token
194
223
  * @param refreshToken - New refresh token (optional)
195
224
  */
196
- async updateTokens(accountId: string, accessToken: string, refreshToken?: string): Promise<void> {
225
+ async updateTokens(
226
+ accountId: string,
227
+ accessToken: string,
228
+ refreshToken?: string,
229
+ ): Promise<void> {
197
230
  this.logger.debug(`Updating tokens for account: ${accountId}`);
198
-
231
+
199
232
  try {
200
- await this.accountRepository.updateTokens(accountId, accessToken, refreshToken);
233
+ await this.accountRepository.updateTokens(
234
+ accountId,
235
+ accessToken,
236
+ refreshToken,
237
+ );
201
238
  this.logger.log(`Tokens updated for account: ${accountId}`);
202
239
  } catch (error) {
203
- this.logger.error(`Failed to update tokens for account: ${accountId}`, error);
240
+ this.logger.error(
241
+ `Failed to update tokens for account: ${accountId}`,
242
+ error,
243
+ );
204
244
  throw error;
205
245
  }
206
246
  }
@@ -212,7 +252,7 @@ export class AccountService {
212
252
  */
213
253
  async getById(accountId: string): Promise<ConnectedAccount | null> {
214
254
  this.logger.debug(`Getting account by ID: ${accountId}`);
215
-
255
+
216
256
  try {
217
257
  return await this.accountRepository.findById(accountId);
218
258
  } catch (error) {
@@ -229,12 +269,17 @@ export class AccountService {
229
269
  */
230
270
  async hasProviderAccount(userId: string, provider: string): Promise<boolean> {
231
271
  this.logger.debug(`Checking if user ${userId} has ${provider} account`);
232
-
272
+
233
273
  try {
234
274
  const accounts = await this.accountRepository.findByUserId(userId);
235
- return accounts.some(account => account.provider === provider && account.isActive);
275
+ return accounts.some(
276
+ (account) => account.provider === provider && account.isActive,
277
+ );
236
278
  } catch (error) {
237
- this.logger.error(`Failed to check provider account for user: ${userId}`, error);
279
+ this.logger.error(
280
+ `Failed to check provider account for user: ${userId}`,
281
+ error,
282
+ );
238
283
  return false;
239
284
  }
240
285
  }
@@ -246,12 +291,15 @@ export class AccountService {
246
291
  */
247
292
  async getAccountCount(userId: string): Promise<number> {
248
293
  this.logger.debug(`Getting account count for user: ${userId}`);
249
-
294
+
250
295
  try {
251
296
  const accounts = await this.accountRepository.findByUserId(userId);
252
- return accounts.filter(account => account.isActive).length;
297
+ return accounts.filter((account) => account.isActive).length;
253
298
  } catch (error) {
254
- this.logger.error(`Failed to get account count for user: ${userId}`, error);
299
+ this.logger.error(
300
+ `Failed to get account count for user: ${userId}`,
301
+ error,
302
+ );
255
303
  return 0;
256
304
  }
257
305
  }
@@ -266,4 +314,4 @@ export class AccountService {
266
314
  // Mock event emission - in real implementation would use event system
267
315
  this.logger.debug(`Event: ${eventType}`, payload);
268
316
  }
269
- }
317
+ }
@@ -1,43 +1,56 @@
1
- import { Injectable } from '@nestjs/common';
2
-
3
- import type { SessionManager } from '../../core/session/session.manager';
4
- import type { TraditionalAuthStrategy } from '../../strategies/traditional-auth.strategy';
5
- import type { OAuthStrategy, OAuthProfile } from '../../strategies/oauth.strategy';
6
- import type { UserRepository } from '../../db/repositories/user.repository';
7
- import type { User, AuthTokens } from '../../common/types/auth.types';
8
- import type { Session } from '@plyaz/types';
9
- import type { JwtManager } from '@/core/jwt/jwt.manager';
1
+ import { Injectable } from "@nestjs/common";
10
2
 
3
+ import type { SessionManager } from "../../core/session/session.manager";
4
+ import type { TraditionalAuthStrategy } from "../../strategies/traditional-auth.strategy";
5
+ import type {
6
+ OAuthStrategy,
7
+ OAuthProfile,
8
+ } from "../../strategies/oauth.strategy";
9
+ import type { UserRepository } from "../../db/repositories/user.repository";
10
+ import type { User, AuthTokens } from "../../common/types/auth.types";
11
+ import type { Session } from "@plyaz/types";
12
+ import type { JwtManager } from "@/core/jwt/jwt.manager";
11
13
 
12
14
  @Injectable()
13
15
  export class AuthService {
14
16
  // eslint-disable-next-line max-params
15
17
  constructor(
16
- private jwtManager:JwtManager,
18
+ private jwtManager: JwtManager,
17
19
  private sessionManager: SessionManager,
18
20
  private traditionalAuth: TraditionalAuthStrategy,
19
21
  private oauthAuth: OAuthStrategy,
20
- private userRepo: UserRepository
22
+ private userRepo: UserRepository,
21
23
  ) {}
22
24
 
23
- async signIn(email: string, password: string, deviceInfo: Record<string,unknown>): Promise<{ user: User; tokens: AuthTokens; session: Session }> {
25
+ async signIn(
26
+ email: string,
27
+ password: string,
28
+ deviceInfo: Record<string, unknown>,
29
+ ): Promise<{ user: User; tokens: AuthTokens; session: Session }> {
24
30
  const user = await this.traditionalAuth.authenticate(email, password);
25
- const session = await this.sessionManager.createSession(user.id, deviceInfo);
31
+ const session = await this.sessionManager.createSession(
32
+ user.id,
33
+ deviceInfo,
34
+ );
26
35
  const tokens = this.jwtManager.generateTokens(user);
27
-
36
+
28
37
  await this.userRepo.updateLastLogin(user.id);
29
-
30
- return { user, tokens: { ...tokens, refreshToken: tokens.refreshToken ?? '' }, session };
38
+
39
+ return {
40
+ user,
41
+ tokens: { ...tokens, refreshToken: tokens.refreshToken ?? "" },
42
+ session,
43
+ };
31
44
  }
32
45
 
33
46
  async signUp(userData: Partial<User>, password: string): Promise<User> {
34
47
  const passwordHash = await this.traditionalAuth.hashPassword(password);
35
-
48
+
36
49
  return this.userRepo.create({
37
50
  email: userData.email!,
38
51
  displayName: userData.displayName!,
39
52
  passwordHash,
40
- authProvider: 'email',
53
+ authProvider: "email",
41
54
  isActive: true,
42
55
  ...userData,
43
56
  });
@@ -54,26 +67,42 @@ export class AuthService {
54
67
  async refreshToken(refreshToken: string): Promise<AuthTokens> {
55
68
  const payload = this.jwtManager.verifyRefreshToken(refreshToken);
56
69
  const user = await this.userRepo.findById(payload.sub);
57
-
70
+
58
71
  if (!user?.isActive) {
59
- throw new Error('Invalid refresh token');
72
+ throw new Error("Invalid refresh token");
60
73
  }
61
74
 
62
75
  const tokens = this.jwtManager.generateTokens(user);
63
- return { ...tokens, refreshToken: tokens.refreshToken ?? '' };
76
+ return { ...tokens, refreshToken: tokens.refreshToken ?? "" };
64
77
  }
65
78
 
66
79
  async validateUser(userId: string): Promise<User | null> {
67
80
  return this.userRepo.findById(userId);
68
81
  }
69
82
 
70
- async oauthSignIn(profile: OAuthProfile, accessToken: string, refreshToken?: string, deviceInfo?: Record<string,unknown>): Promise<{ user: User; tokens: AuthTokens; session: Session }> {
71
- const user = await this.oauthAuth.authenticate(profile, accessToken, refreshToken);
72
- const session = await this.sessionManager.createSession(user.id, deviceInfo ?? {});
83
+ async oauthSignIn(
84
+ profile: OAuthProfile,
85
+ accessToken: string,
86
+ refreshToken?: string,
87
+ deviceInfo?: Record<string, unknown>,
88
+ ): Promise<{ user: User; tokens: AuthTokens; session: Session }> {
89
+ const user = await this.oauthAuth.authenticate(
90
+ profile,
91
+ accessToken,
92
+ refreshToken,
93
+ );
94
+ const session = await this.sessionManager.createSession(
95
+ user.id,
96
+ deviceInfo ?? {},
97
+ );
73
98
  const tokens = this.jwtManager.generateTokens(user);
74
-
99
+
75
100
  await this.userRepo.updateLastLogin(user.id);
76
-
77
- return { user, tokens: { ...tokens, refreshToken: tokens.refreshToken ?? '' }, session };
101
+
102
+ return {
103
+ user,
104
+ tokens: { ...tokens, refreshToken: tokens.refreshToken ?? "" },
105
+ session,
106
+ };
78
107
  }
79
- }
108
+ }
@@ -3,26 +3,32 @@ import { Injectable, BadRequestException } from "@nestjs/common";
3
3
  import { NUMERIX } from "@plyaz/config";
4
4
  import type { Redis } from "ioredis";
5
5
 
6
- const thirty = 30;
6
+ const thirty = 30;
7
7
  @Injectable()
8
8
  export class BruteForceService {
9
9
  private static readonly USER_HARD_LIMIT = 6;
10
10
 
11
- private static readonly USER_HARD_BLOCK_MS = thirty * NUMERIX.SIXTY * NUMERIX.THOUSAND; // 30 min
12
- private static readonly USER_PROGRESSIVE_DELAYS: Record<number, number> = { 4: 5000, 5: 5000 };
11
+ private static readonly USER_HARD_BLOCK_MS =
12
+ thirty * NUMERIX.SIXTY * NUMERIX.THOUSAND; // 30 min
13
+ private static readonly USER_PROGRESSIVE_DELAYS: Record<number, number> = {
14
+ 4: 5000,
15
+ 5: 5000,
16
+ };
13
17
 
14
18
  private static readonly IP_HARD_LIMIT = 10;
15
- private static readonly IP_HARD_BLOCK_MS = NUMERIX.SIXTY * NUMERIX.SIXTY * NUMERIX.THOUSAND; // 1 hour
19
+ private static readonly IP_HARD_BLOCK_MS =
20
+ NUMERIX.SIXTY * NUMERIX.SIXTY * NUMERIX.THOUSAND; // 1 hour
16
21
 
17
22
  private static readonly PROVIDER_HARD_LIMIT = 5;
18
- private static readonly PROVIDER_HARD_BLOCK_MS = thirty * NUMERIX.SIXTY * NUMERIX.THOUSAND; // 30 min
23
+ private static readonly PROVIDER_HARD_BLOCK_MS =
24
+ thirty * NUMERIX.SIXTY * NUMERIX.THOUSAND; // 30 min
19
25
 
20
26
  constructor(private readonly redis: Redis) {}
21
27
 
22
28
  async trackFailedLogin(
23
29
  userId: string,
24
30
  ip: string,
25
- providerId?: string
31
+ providerId?: string,
26
32
  ): Promise<void> {
27
33
  // User-level progressive delay + hard block
28
34
  await this.trackEntity(
@@ -30,7 +36,7 @@ export class BruteForceService {
30
36
  `bf:user:block:${userId}`,
31
37
  BruteForceService.USER_HARD_LIMIT,
32
38
  BruteForceService.USER_HARD_BLOCK_MS,
33
- BruteForceService.USER_PROGRESSIVE_DELAYS
39
+ BruteForceService.USER_PROGRESSIVE_DELAYS,
34
40
  );
35
41
 
36
42
  // IP-level block
@@ -38,7 +44,7 @@ export class BruteForceService {
38
44
  `bf:ip:${ip}`,
39
45
  `bf:ip:block:${ip}`,
40
46
  BruteForceService.IP_HARD_LIMIT,
41
- BruteForceService.IP_HARD_BLOCK_MS
47
+ BruteForceService.IP_HARD_BLOCK_MS,
42
48
  );
43
49
 
44
50
  // Provider-level block (optional)
@@ -47,7 +53,7 @@ export class BruteForceService {
47
53
  `bf:provider:${providerId}`,
48
54
  `bf:provider:block:${providerId}`,
49
55
  BruteForceService.PROVIDER_HARD_LIMIT,
50
- BruteForceService.PROVIDER_HARD_BLOCK_MS
56
+ BruteForceService.PROVIDER_HARD_BLOCK_MS,
51
57
  );
52
58
  }
53
59
  }
@@ -58,30 +64,29 @@ export class BruteForceService {
58
64
  blockKey: string,
59
65
  limit: number,
60
66
  blockMs: number,
61
- progressiveDelays?: Record<number, number>
67
+ progressiveDelays?: Record<number, number>,
62
68
  ): Promise<void> {
63
69
  const blockedUntil = await this.redis.get(blockKey);
64
70
  if (blockedUntil && Date.now() < Number(blockedUntil)) {
65
71
  throw new BadRequestException(
66
- "Account temporarily blocked due to repeated failed attempts."
72
+ "Account temporarily blocked due to repeated failed attempts.",
67
73
  );
68
74
  }
69
75
 
70
76
  const failures = await this.redis.incr(entityKey);
71
77
  if (failures === 1) await this.redis.pexpire(entityKey, blockMs);
72
78
 
73
- if (progressiveDelays?.[failures]) {
74
- await new Promise<void>((resolve) => {
75
- globalThis.setTimeout(resolve, progressiveDelays[failures]);
76
- });
77
- }
78
-
79
+ if (progressiveDelays?.[failures]) {
80
+ await new Promise<void>((resolve) => {
81
+ globalThis.setTimeout(resolve, progressiveDelays[failures]);
82
+ });
83
+ }
79
84
 
80
85
  if (failures >= limit) {
81
86
  await this.redis.set(blockKey, Date.now() + blockMs, "PX", blockMs);
82
87
  await this.redis.del(entityKey); // reset failures
83
88
  throw new BadRequestException(
84
- "Account locked due to too many failed login attempts."
89
+ "Account locked due to too many failed login attempts.",
85
90
  );
86
91
  }
87
92
  }
@@ -92,7 +97,7 @@ export class BruteForceService {
92
97
  if (providerId)
93
98
  await this.redis.del(
94
99
  `bf:provider:${providerId}`,
95
- `bf:provider:block:${providerId}`
100
+ `bf:provider:block:${providerId}`,
96
101
  );
97
102
  }
98
103
  }
@@ -12,4 +12,4 @@ export * from "./session.service";
12
12
  export * from "./token.service";
13
13
  export * from "./account.service";
14
14
  export * from "./brute-force.service";
15
- export * from "./rate-limiter.service"
15
+ export * from "./rate-limiter.service";