@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.
- package/commits.txt +3 -3
- package/dist/common/index.cjs +3 -1
- package/dist/common/index.cjs.map +1 -1
- package/dist/common/index.mjs +3 -1
- package/dist/common/index.mjs.map +1 -1
- package/dist/index.cjs +424 -154
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +421 -152
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/release_message.txt +28 -0
- package/src/adapters/auth-adapter-factory.ts +4 -3
- package/src/adapters/auth-adapter.mapper.ts +2 -2
- package/src/adapters/base-auth.adapter.ts +17 -9
- package/src/adapters/clerk/clerk.adapter.ts +9 -12
- package/src/adapters/custom/custom.adapter.ts +19 -10
- package/src/adapters/index.ts +0 -1
- package/src/adapters/next-auth/authOptions.ts +20 -16
- package/src/adapters/next-auth/next-auth.adapter.ts +13 -15
- package/src/api/client.ts +4 -6
- package/src/audit/audit.logger.ts +19 -10
- package/src/client/components/ProtectedRoute.tsx +15 -11
- package/src/client/hooks/useAuth.ts +23 -21
- package/src/client/hooks/useConnectedAccounts.ts +57 -45
- package/src/client/hooks/usePermissions.ts +1 -1
- package/src/client/hooks/useRBAC.ts +6 -6
- package/src/client/hooks/useSession.ts +5 -5
- package/src/client/providers/AuthProvider.tsx +23 -17
- package/src/client/store/auth.store.ts +71 -62
- package/src/client/utils/storage.ts +45 -18
- package/src/common/constants/oauth-providers.ts +10 -7
- package/src/common/errors/auth.errors.ts +4 -4
- package/src/common/errors/specific-auth-errors.ts +5 -9
- package/src/common/regex/index.ts +6 -4
- package/src/common/types/auth.types.ts +47 -38
- package/src/common/types/index.ts +12 -6
- package/src/common/utils/index.ts +15 -11
- package/src/core/blacklist/token.blacklist.ts +13 -7
- package/src/core/index.ts +2 -2
- package/src/core/jwt/jwt.manager.ts +47 -22
- package/src/core/session/session.manager.ts +17 -14
- package/src/db/repositories/connected-account.repository.ts +120 -78
- package/src/db/repositories/role.repository.ts +41 -26
- package/src/db/repositories/session.repository.ts +9 -10
- package/src/db/repositories/user.repository.ts +105 -91
- package/src/flows/index.ts +2 -2
- package/src/flows/sign-in.flow.ts +28 -14
- package/src/flows/sign-up.flow.ts +31 -20
- package/src/index.ts +36 -37
- package/src/libs/clerk.helper.ts +6 -7
- package/src/libs/supabase.helper.ts +79 -61
- package/src/libs/supabaseClient.ts +3 -3
- package/src/providers/base/auth-provider.interface.ts +13 -11
- package/src/providers/base/index.ts +1 -1
- package/src/providers/index.ts +1 -1
- package/src/providers/oauth/facebook.provider.ts +63 -39
- package/src/providers/oauth/github.provider.ts +14 -10
- package/src/providers/oauth/google.provider.ts +39 -28
- package/src/providers/oauth/index.ts +1 -1
- package/src/rbac/dynamic-roles.ts +88 -54
- package/src/rbac/index.ts +4 -4
- package/src/rbac/permission-checker.ts +147 -75
- package/src/rbac/role-hierarchy.ts +8 -8
- package/src/rbac/role.manager.ts +11 -8
- package/src/security/csrf/csrf.protection.ts +9 -7
- package/src/security/index.ts +2 -2
- package/src/security/rate-limiting/auth/auth.controller.ts +2 -4
- package/src/security/rate-limiting/auth/rate-limiting.interface.ts +26 -6
- package/src/security/rate-limiting/auth.module.ts +1 -2
- package/src/server/auth.module.ts +55 -52
- package/src/server/decorators/auth.decorator.ts +9 -11
- package/src/server/decorators/auth.decorators.ts +8 -9
- package/src/server/decorators/current-user.decorator.ts +6 -6
- package/src/server/decorators/permission.decorator.ts +17 -9
- package/src/server/guards/auth.guard.ts +21 -16
- package/src/server/guards/custom-throttler.guard.ts +4 -9
- package/src/server/guards/permissions.guard.ts +32 -23
- package/src/server/guards/roles.guard.ts +14 -12
- package/src/server/middleware/auth.middleware.ts +4 -4
- package/src/server/middleware/session.middleware.ts +4 -4
- package/src/server/services/account.service.ts +96 -48
- package/src/server/services/auth.service.ts +57 -28
- package/src/server/services/brute-force.service.ts +24 -19
- package/src/server/services/index.ts +1 -1
- package/src/server/services/rate-limiter.service.ts +9 -4
- package/src/server/services/session.service.ts +84 -48
- package/src/server/services/token.service.ts +71 -51
- package/src/session/cookie-store.ts +47 -34
- package/src/session/enhanced-session-manager.ts +69 -48
- package/src/session/index.ts +5 -5
- package/src/session/memory-store.ts +37 -30
- package/src/session/redis-store.ts +105 -72
- package/src/strategies/oauth.strategy.ts +10 -9
- package/src/strategies/traditional-auth.strategy.ts +41 -29
- package/src/tokens/index.ts +4 -4
- package/src/tokens/refresh-token-manager.ts +70 -55
- package/src/tokens/token-validator.ts +109 -53
- package/vitest.setup.d.ts +2 -2
- package/vitest.setup.ts +1 -1
|
@@ -1,31 +1,33 @@
|
|
|
1
|
-
import type { CanActivate, ExecutionContext } from
|
|
2
|
-
import { Injectable } from
|
|
3
|
-
import type { Reflector } from
|
|
4
|
-
import type { RoleManager } from
|
|
5
|
-
import { AuthenticationError } from
|
|
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[]>(
|
|
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(
|
|
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(
|
|
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
|
|
27
|
-
import type { ConnectedAccountRepository } from
|
|
26
|
+
import { Injectable, Logger } from "@nestjs/common";
|
|
27
|
+
import type { ConnectedAccountRepository } from "../../db/repositories/connected-account.repository";
|
|
28
28
|
|
|
29
|
-
import type { ConnectedAccount } from
|
|
30
|
-
import { AUTH_EVENTS } from
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
159
|
+
|
|
160
|
+
this.logger.log(
|
|
161
|
+
`Primary account set to ${accountId} for user: ${userId}`,
|
|
162
|
+
);
|
|
151
163
|
} catch (error) {
|
|
152
|
-
this.logger.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(
|
|
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(
|
|
180
|
-
|
|
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(
|
|
206
|
+
return await this.accountRepository.findByProvider(
|
|
207
|
+
provider,
|
|
208
|
+
providerAccountId,
|
|
209
|
+
);
|
|
184
210
|
} catch (error) {
|
|
185
|
-
this.logger.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(
|
|
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(
|
|
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(
|
|
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(
|
|
275
|
+
return accounts.some(
|
|
276
|
+
(account) => account.provider === provider && account.isActive,
|
|
277
|
+
);
|
|
236
278
|
} catch (error) {
|
|
237
|
-
this.logger.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(
|
|
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
|
|
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(
|
|
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(
|
|
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 {
|
|
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:
|
|
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(
|
|
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(
|
|
71
|
-
|
|
72
|
-
|
|
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 {
|
|
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
|
|
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 =
|
|
12
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
}
|