@plyaz/auth 1.0.0

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 (113) hide show
  1. package/.github/pull_request_template.md +71 -0
  2. package/.github/workflows/deploy.yml +9 -0
  3. package/.github/workflows/publish.yml +14 -0
  4. package/.github/workflows/security.yml +20 -0
  5. package/README.md +89 -0
  6. package/commits.txt +5 -0
  7. package/dist/common/index.cjs +48 -0
  8. package/dist/common/index.cjs.map +1 -0
  9. package/dist/common/index.mjs +43 -0
  10. package/dist/common/index.mjs.map +1 -0
  11. package/dist/index.cjs +20411 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.mjs +5139 -0
  14. package/dist/index.mjs.map +1 -0
  15. package/eslint.config.mjs +13 -0
  16. package/index.html +13 -0
  17. package/package.json +141 -0
  18. package/src/adapters/auth-adapter-factory.ts +26 -0
  19. package/src/adapters/auth-adapter.mapper.ts +53 -0
  20. package/src/adapters/base-auth.adapter.ts +119 -0
  21. package/src/adapters/clerk/clerk.adapter.ts +204 -0
  22. package/src/adapters/custom/custom.adapter.ts +119 -0
  23. package/src/adapters/index.ts +4 -0
  24. package/src/adapters/next-auth/authOptions.ts +81 -0
  25. package/src/adapters/next-auth/next-auth.adapter.ts +211 -0
  26. package/src/api/client.ts +37 -0
  27. package/src/audit/audit.logger.ts +52 -0
  28. package/src/client/components/ProtectedRoute.tsx +37 -0
  29. package/src/client/hooks/useAuth.ts +128 -0
  30. package/src/client/hooks/useConnectedAccounts.ts +108 -0
  31. package/src/client/hooks/usePermissions.ts +36 -0
  32. package/src/client/hooks/useRBAC.ts +36 -0
  33. package/src/client/hooks/useSession.ts +18 -0
  34. package/src/client/providers/AuthProvider.tsx +104 -0
  35. package/src/client/store/auth.store.ts +306 -0
  36. package/src/client/utils/storage.ts +70 -0
  37. package/src/common/constants/oauth-providers.ts +49 -0
  38. package/src/common/errors/auth.errors.ts +64 -0
  39. package/src/common/errors/specific-auth-errors.ts +201 -0
  40. package/src/common/index.ts +19 -0
  41. package/src/common/regex/index.ts +27 -0
  42. package/src/common/types/auth.types.ts +641 -0
  43. package/src/common/types/index.ts +297 -0
  44. package/src/common/utils/index.ts +84 -0
  45. package/src/core/blacklist/token.blacklist.ts +60 -0
  46. package/src/core/index.ts +2 -0
  47. package/src/core/jwt/jwt.manager.ts +131 -0
  48. package/src/core/session/session.manager.ts +56 -0
  49. package/src/db/repositories/connected-account.repository.ts +415 -0
  50. package/src/db/repositories/role.repository.ts +519 -0
  51. package/src/db/repositories/session.repository.ts +308 -0
  52. package/src/db/repositories/user.repository.ts +320 -0
  53. package/src/flows/index.ts +2 -0
  54. package/src/flows/sign-in.flow.ts +106 -0
  55. package/src/flows/sign-up.flow.ts +121 -0
  56. package/src/index.ts +54 -0
  57. package/src/libs/clerk.helper.ts +36 -0
  58. package/src/libs/supabase.helper.ts +255 -0
  59. package/src/libs/supabaseClient.ts +6 -0
  60. package/src/providers/base/auth-provider.interface.ts +42 -0
  61. package/src/providers/base/index.ts +1 -0
  62. package/src/providers/index.ts +2 -0
  63. package/src/providers/oauth/facebook.provider.ts +97 -0
  64. package/src/providers/oauth/github.provider.ts +148 -0
  65. package/src/providers/oauth/google.provider.ts +126 -0
  66. package/src/providers/oauth/index.ts +3 -0
  67. package/src/rbac/dynamic-roles.ts +552 -0
  68. package/src/rbac/index.ts +4 -0
  69. package/src/rbac/permission-checker.ts +464 -0
  70. package/src/rbac/role-hierarchy.ts +545 -0
  71. package/src/rbac/role.manager.ts +75 -0
  72. package/src/security/csrf/csrf.protection.ts +37 -0
  73. package/src/security/index.ts +3 -0
  74. package/src/security/rate-limiting/auth/auth.controller.ts +12 -0
  75. package/src/security/rate-limiting/auth/rate-limiting.interface.ts +67 -0
  76. package/src/security/rate-limiting/auth.module.ts +32 -0
  77. package/src/server/auth.module.ts +158 -0
  78. package/src/server/decorators/auth.decorator.ts +43 -0
  79. package/src/server/decorators/auth.decorators.ts +31 -0
  80. package/src/server/decorators/current-user.decorator.ts +49 -0
  81. package/src/server/decorators/permission.decorator.ts +49 -0
  82. package/src/server/guards/auth.guard.ts +56 -0
  83. package/src/server/guards/custom-throttler.guard.ts +46 -0
  84. package/src/server/guards/permissions.guard.ts +115 -0
  85. package/src/server/guards/roles.guard.ts +31 -0
  86. package/src/server/middleware/auth.middleware.ts +46 -0
  87. package/src/server/middleware/index.ts +2 -0
  88. package/src/server/middleware/middleware.ts +11 -0
  89. package/src/server/middleware/session.middleware.ts +255 -0
  90. package/src/server/services/account.service.ts +269 -0
  91. package/src/server/services/auth.service.ts +79 -0
  92. package/src/server/services/brute-force.service.ts +98 -0
  93. package/src/server/services/index.ts +15 -0
  94. package/src/server/services/rate-limiter.service.ts +60 -0
  95. package/src/server/services/session.service.ts +287 -0
  96. package/src/server/services/token.service.ts +262 -0
  97. package/src/session/cookie-store.ts +255 -0
  98. package/src/session/enhanced-session-manager.ts +406 -0
  99. package/src/session/index.ts +14 -0
  100. package/src/session/memory-store.ts +320 -0
  101. package/src/session/redis-store.ts +443 -0
  102. package/src/strategies/oauth.strategy.ts +128 -0
  103. package/src/strategies/traditional-auth.strategy.ts +116 -0
  104. package/src/tokens/index.ts +4 -0
  105. package/src/tokens/refresh-token-manager.ts +448 -0
  106. package/src/tokens/token-validator.ts +311 -0
  107. package/tsconfig.build.json +28 -0
  108. package/tsconfig.json +38 -0
  109. package/tsup.config.mjs +28 -0
  110. package/vitest.config.mjs +16 -0
  111. package/vitest.setup.d.ts +2 -0
  112. package/vitest.setup.d.ts.map +1 -0
  113. package/vitest.setup.ts +1 -0
@@ -0,0 +1,46 @@
1
+ import type { NestMiddleware } from "@nestjs/common";
2
+ import { Injectable } from "@nestjs/common";
3
+ import type { Request, Response, NextFunction } from "express";
4
+
5
+ /**
6
+ * Extends the Express Request object to include `sessionId`.
7
+ */
8
+ export interface AuthRequest extends Request {
9
+ /** Optional session ID extracted from cookies or headers */
10
+ sessionId?: string;
11
+ }
12
+
13
+ /**
14
+ * Middleware to extract the session ID from incoming requests.
15
+ *
16
+ * This middleware does **not validate** the session.
17
+ * It only reads the session ID from:
18
+ * - Cookies (`session_id`)
19
+ * - Headers (`x-session-id`)
20
+ * and attaches it to `req.sessionId` for use in guards or controllers.
21
+ *
22
+ * @example
23
+ * // Access sessionId in a controller or guard
24
+ * const sessionId = req.sessionId;
25
+ */
26
+ @Injectable()
27
+ export class AuthMiddleware implements NestMiddleware {
28
+ /**
29
+ * Extracts session ID from cookies or headers and attaches it to the request object.
30
+ *
31
+ * @param req - Express request object
32
+ * @param res - Express response object
33
+ * @param next - Function to pass control to the next middleware
34
+ */
35
+ use(req: AuthRequest, res: Response, next: NextFunction):void {
36
+ // Read session cookie (or header)
37
+ const sessionId = req.cookies?.session_id ?? req.headers["x-session-id"];
38
+
39
+ // Just attach it — NO validation
40
+ if (sessionId) {
41
+ req.sessionId = String(sessionId);
42
+ }
43
+
44
+ next();
45
+ }
46
+ }
@@ -0,0 +1,2 @@
1
+ export { SessionMiddleware } from "./session.middleware";
2
+ export { AuthMiddleware } from "./auth.middleware";
@@ -0,0 +1,11 @@
1
+ // The `config` object is a Next.js feature used to configure the middleware.
2
+ export const config = {
3
+ // `matcher` is an array of paths that the middleware should run on.
4
+ // It's a powerful tool for specifying exactly which routes need to be
5
+ // protected or have authentication checked.
6
+ matcher: [
7
+ "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
8
+ "/api/(.*)",
9
+ "/trpc/(.*)",
10
+ ],
11
+ };
@@ -0,0 +1,255 @@
1
+ /**
2
+ * @fileoverview Session middleware for @plyaz/auth
3
+ * @module @plyaz/auth/server/middleware/session-middleware
4
+ *
5
+ * @description
6
+ * NestJS middleware for session refresh management. Checks if access token
7
+ * is close to expiry and auto-refreshes if refresh token is valid.
8
+ * Sets new tokens in response cookies for seamless user experience.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { SessionMiddleware } from '@plyaz/auth';
13
+ *
14
+ * @Module({
15
+ * providers: [SessionMiddleware]
16
+ * })
17
+ * export class AppModule implements NestModule {
18
+ * configure(consumer: MiddlewareConsumer) {
19
+ * consumer.apply(SessionMiddleware).forRoutes('*');
20
+ * }
21
+ * }
22
+ * ```
23
+ */
24
+
25
+ import type { NestMiddleware } from "@nestjs/common";
26
+ import { Injectable, Logger } from "@nestjs/common";
27
+ import type { Request, Response, NextFunction } from "express";
28
+ import type { TokenService } from "../services/token.service";
29
+ import type { SessionService } from "../services/session.service";
30
+ import { NUMERIX } from "@plyaz/config";
31
+
32
+ /**
33
+ * Session middleware configuration
34
+ */
35
+ interface SessionMiddlewareConfig {
36
+ /** Refresh threshold in seconds (default: 300 = 5 minutes) */
37
+ refreshThreshold: number;
38
+ /** Cookie options for tokens */
39
+ cookieOptions: {
40
+ httpOnly: boolean;
41
+ secure: boolean;
42
+ sameSite: "strict" | "lax" | "none";
43
+ path: string;
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Session middleware implementation
49
+ * Manages session refresh and token rotation
50
+ */
51
+ @Injectable()
52
+ export class SessionMiddleware implements NestMiddleware {
53
+ private readonly logger = new Logger(SessionMiddleware.name);
54
+ private readonly config: SessionMiddlewareConfig;
55
+
56
+ constructor(
57
+ private tokenService: TokenService,
58
+ private sessionService: SessionService
59
+ ) {
60
+ this.config = {
61
+ refreshThreshold: 300, // 5 minutes
62
+ cookieOptions: {
63
+ httpOnly: true,
64
+ secure: globalThis.process.env.NODE_ENV === "production",
65
+ sameSite: "lax",
66
+ path: "/",
67
+ },
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Middleware function to handle session refresh
73
+ * @param req - HTTP request
74
+ * @param res - HTTP response
75
+ * @param next - Next function
76
+ */
77
+ async use(req: Request, res: Response, next: NextFunction): Promise<void> {
78
+ try {
79
+ const accessToken = this.extractAccessToken(req);
80
+ const refreshToken = this.extractRefreshToken(req);
81
+
82
+ if (!accessToken || !refreshToken) {
83
+ // No tokens present, continue without refresh
84
+ return next();
85
+ }
86
+
87
+ // Check if access token needs refresh
88
+ const shouldRefresh = await this.shouldRefreshToken(accessToken);
89
+
90
+ if (shouldRefresh) {
91
+ await this.refreshTokens(req, res, refreshToken);
92
+ }
93
+
94
+ next();
95
+ } catch (error) {
96
+ this.logger.error("Session middleware error", error);
97
+ // Don't block the request on refresh errors
98
+ next();
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Check if token should be refreshed
104
+ * @param accessToken - Access token to check
105
+ * @returns True if token should be refreshed
106
+ * @private
107
+ */
108
+ private async shouldRefreshToken(accessToken: string): Promise<boolean> {
109
+ try {
110
+ const timeUntilExpiry =
111
+ this.tokenService.getTimeUntilExpiration(accessToken);
112
+
113
+ if (timeUntilExpiry === null) {
114
+ return false; // Invalid token, don't refresh
115
+ }
116
+
117
+ // Refresh if token expires within threshold
118
+ return timeUntilExpiry <= this.config.refreshThreshold;
119
+ } catch (error) {
120
+ this.logger.debug("Error checking token expiry", error);
121
+ return false;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Refresh tokens and update cookies
127
+ * @param req - HTTP request
128
+ * @param res - HTTP response
129
+ * @param refreshToken - Refresh token
130
+ * @private
131
+ */
132
+ private async refreshTokens(
133
+ req: Request,
134
+ res: Response,
135
+ refreshToken: string
136
+ ): Promise<void> {
137
+ try {
138
+ this.logger.debug("Refreshing tokens");
139
+
140
+ // Validate refresh token first
141
+ const validationResult =
142
+ await this.tokenService.validateRefreshToken(refreshToken);
143
+
144
+ if (!validationResult.valid) {
145
+ this.logger.warn("Invalid refresh token, clearing cookies");
146
+ this.clearTokenCookies(res);
147
+ return;
148
+ }
149
+
150
+ // Get user roles and permissions for new tokens
151
+ const payload = validationResult.payload;
152
+ const userRoles = payload?.roles;
153
+ const userPermissions = payload?.permissions;
154
+
155
+ // Generate new token pair
156
+ const tokenPair = await this.tokenService.refreshTokenPair(
157
+ refreshToken,
158
+ userRoles,
159
+ userPermissions
160
+ );
161
+
162
+ // Set new tokens in cookies
163
+ this.setTokenCookies(res, tokenPair.accessToken, tokenPair.refreshToken);
164
+
165
+ // Update request with new access token for downstream handlers
166
+ this.updateRequestToken(req, tokenPair.accessToken);
167
+
168
+ this.logger.log("Tokens refreshed successfully");
169
+ } catch (error) {
170
+ this.logger.error("Failed to refresh tokens", error);
171
+ // Clear invalid tokens
172
+ this.clearTokenCookies(res);
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Extract access token from request
178
+ * @param req - HTTP request
179
+ * @returns Access token or null
180
+ * @private
181
+ */
182
+ private extractAccessToken(req: Request): string | null {
183
+ // Check Authorization header first
184
+ const authHeader = req.headers.authorization;
185
+ if (authHeader?.startsWith("Bearer ")) {
186
+ const seven = 7;
187
+ return authHeader.substring(seven);
188
+ }
189
+
190
+ // Check cookies
191
+ return req.cookies?.accessToken ?? null;
192
+ }
193
+
194
+ /**
195
+ * Extract refresh token from request
196
+ * @param req - HTTP request
197
+ * @returns Refresh token or null
198
+ * @private
199
+ */
200
+ private extractRefreshToken(req: Request): string | null {
201
+ return req.cookies?.refreshToken ?? null;
202
+ }
203
+
204
+ /**
205
+ * Set token cookies in response
206
+ * @param res - HTTP response
207
+ * @param accessToken - Access token
208
+ * @param refreshToken - Refresh token
209
+ * @private
210
+ */
211
+ private setTokenCookies(
212
+ res: Response,
213
+ accessToken: string,
214
+ refreshToken: string
215
+ ): void {
216
+ const fifteen = 15;
217
+ // Set access token cookie (shorter expiry)
218
+ res.cookie("accessToken", accessToken, {
219
+ ...this.config.cookieOptions,
220
+ maxAge: fifteen * NUMERIX.SIXTY * NUMERIX.THOUSAND, // 15 minutes
221
+ });
222
+ const seven = 7;
223
+ // Set refresh token cookie (longer expiry)
224
+ res.cookie("refreshToken", refreshToken, {
225
+ ...this.config.cookieOptions,
226
+ maxAge:
227
+ seven *
228
+ NUMERIX.TWENTY_FOUR *
229
+ NUMERIX.SIXTY *
230
+ NUMERIX.SIXTY *
231
+ NUMERIX.THOUSAND, // 7 days
232
+ });
233
+ }
234
+
235
+ /**
236
+ * Clear token cookies from response
237
+ * @param res - HTTP response
238
+ * @private
239
+ */
240
+ private clearTokenCookies(res: Response): void {
241
+ res.clearCookie("accessToken", this.config.cookieOptions);
242
+ res.clearCookie("refreshToken", this.config.cookieOptions);
243
+ }
244
+
245
+ /**
246
+ * Update request with new access token
247
+ * @param req - HTTP request
248
+ * @param accessToken - New access token
249
+ * @private
250
+ */
251
+ private updateRequestToken(req: Request, accessToken: string): void {
252
+ // Update Authorization header for downstream handlers
253
+ req.headers.authorization = `Bearer ${accessToken}`;
254
+ }
255
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * @fileoverview Account service for @plyaz/auth
3
+ * @module @plyaz/auth/server/services/account-service
4
+ *
5
+ * @description
6
+ * NestJS service for connected account management operations.
7
+ * Provides high-level operations for linking/unlinking provider accounts,
8
+ * managing primary accounts, and handling account-related business logic.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { AccountService } from '@plyaz/auth';
13
+ *
14
+ * @Controller('accounts')
15
+ * export class AccountsController {
16
+ * constructor(private accountService: AccountService) {}
17
+ *
18
+ * @Post('link')
19
+ * async linkAccount(@Body() data, @CurrentUser() user) {
20
+ * return await this.accountService.link(user.id, data.provider, data);
21
+ * }
22
+ * }
23
+ * ```
24
+ */
25
+
26
+ import { Injectable, Logger } from '@nestjs/common';
27
+ import type { ConnectedAccountRepository } from '../../db/repositories/connected-account.repository';
28
+
29
+ import type { ConnectedAccount } from '@plyaz/types';
30
+ import { AUTH_EVENTS } from '@plyaz/types';
31
+
32
+ /**
33
+ * Account service implementation
34
+ * Provides connected account management operations for NestJS applications
35
+ */
36
+ @Injectable()
37
+ export class AccountService {
38
+ private readonly logger = new Logger(AccountService.name);
39
+
40
+ constructor(private accountRepository: ConnectedAccountRepository) {}
41
+
42
+ /**
43
+ * Link account to user
44
+ * INSERT new connected account and emit account.linked event
45
+ * @param userId - User identifier
46
+ * @param provider - Provider name
47
+ * @param providerData - Provider account data
48
+ * @returns Created connected account
49
+ */
50
+
51
+ async link(userId: string, provider: string, providerData: ConnectedAccount): Promise<ConnectedAccount> {
52
+ this.logger.debug(`Linking ${provider} account for user: ${userId}`);
53
+
54
+ try {
55
+
56
+
57
+ // Link the account
58
+ const connectedAccount = await this.accountRepository.linkAccount(
59
+ userId,
60
+ provider,
61
+ providerData
62
+ );
63
+
64
+ this.logger.log(`${provider} account linked for user: ${userId}`);
65
+ this.emitEvent(AUTH_EVENTS.ACCOUNT_LINKED, {
66
+ userId,
67
+ provider,
68
+ accountId: connectedAccount.id,
69
+ timestamp: new Date()
70
+ });
71
+
72
+ return connectedAccount;
73
+ } catch (error) {
74
+ this.logger.error(`Failed to link ${provider} account for user: ${userId}`, error);
75
+ throw error;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Unlink account from user
81
+ * DELETE connected account with validation and emit account.unlinked event
82
+ * @param userId - User identifier
83
+ * @param accountId - Account identifier
84
+ */
85
+ async unlink(userId: string, accountId: string): Promise<void> {
86
+ this.logger.debug(`Unlinking account ${accountId} for user: ${userId}`);
87
+
88
+ try {
89
+ // Get account details
90
+ const account = await this.accountRepository.findById(accountId);
91
+ if (!account) {
92
+ // throw new UserNotFoundError('Connected account not found');
93
+ }
94
+
95
+ // Verify account belongs to user
96
+ if (account?.userId !== userId) {
97
+ throw new Error('Account does not belong to user');
98
+ }
99
+
100
+ // Unlink the account (includes validation for last auth method)
101
+ await this.accountRepository.unlinkAccount(accountId);
102
+
103
+ this.logger.log(`Account ${accountId} unlinked for user: ${userId}`);
104
+ this.emitEvent(AUTH_EVENTS.ACCOUNT_UNLINKED, {
105
+ userId,
106
+ provider: account?.provider,
107
+ accountId,
108
+ timestamp: new Date()
109
+ });
110
+ } catch (error) {
111
+ this.logger.error(`Failed to unlink account ${accountId} for user: ${userId}`, error);
112
+ throw error;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Get all connected accounts for user
118
+ * @param userId - User identifier
119
+ * @returns Array of connected accounts
120
+ */
121
+ async getAll(userId: string): Promise<ConnectedAccount[]> {
122
+ this.logger.debug(`Getting all accounts for user: ${userId}`);
123
+
124
+ try {
125
+ return await this.accountRepository.findByUserId(userId);
126
+ } catch (error) {
127
+ this.logger.error(`Failed to get accounts for user: ${userId}`, error);
128
+ throw error;
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Set primary connected account
134
+ * @param userId - User identifier
135
+ * @param accountId - Account identifier to set as primary
136
+ */
137
+ async setPrimary(userId: string, accountId: string): Promise<void> {
138
+ this.logger.debug(`Setting primary account ${accountId} for user: ${userId}`);
139
+
140
+ try {
141
+ // Verify account belongs to user
142
+ const account = await this.accountRepository.findById(accountId);
143
+ if (account?.userId !== userId) {
144
+ throw new Error('Account not found or does not belong to user');
145
+ }
146
+
147
+ // Set as primary
148
+ await this.accountRepository.setPrimary(userId, accountId);
149
+
150
+ this.logger.log(`Primary account set to ${accountId} for user: ${userId}`);
151
+ } catch (error) {
152
+ this.logger.error(`Failed to set primary account ${accountId} for user: ${userId}`, error);
153
+ throw error;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Get primary connected account for user
159
+ * @param userId - User identifier
160
+ * @returns Primary connected account or null
161
+ */
162
+ async getPrimary(userId: string): Promise<ConnectedAccount | null> {
163
+ this.logger.debug(`Getting primary account for user: ${userId}`);
164
+
165
+ try {
166
+ return await this.accountRepository.findPrimary(userId);
167
+ } catch (error) {
168
+ this.logger.error(`Failed to get primary account for user: ${userId}`, error);
169
+ throw error;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Find account by provider credentials
175
+ * @param provider - Provider name
176
+ * @param providerAccountId - Provider account ID
177
+ * @returns Connected account or null
178
+ */
179
+ async findByProvider(provider: string, providerAccountId: string): Promise<ConnectedAccount | null> {
180
+ this.logger.debug(`Finding account by provider: ${provider}, ID: ${providerAccountId}`);
181
+
182
+ try {
183
+ return await this.accountRepository.findByProvider(provider, providerAccountId);
184
+ } catch (error) {
185
+ this.logger.error(`Failed to find account by provider: ${provider}`, error);
186
+ throw error;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Update account tokens
192
+ * @param accountId - Account identifier
193
+ * @param accessToken - New access token
194
+ * @param refreshToken - New refresh token (optional)
195
+ */
196
+ async updateTokens(accountId: string, accessToken: string, refreshToken?: string): Promise<void> {
197
+ this.logger.debug(`Updating tokens for account: ${accountId}`);
198
+
199
+ try {
200
+ await this.accountRepository.updateTokens(accountId, accessToken, refreshToken);
201
+ this.logger.log(`Tokens updated for account: ${accountId}`);
202
+ } catch (error) {
203
+ this.logger.error(`Failed to update tokens for account: ${accountId}`, error);
204
+ throw error;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Get account by ID
210
+ * @param accountId - Account identifier
211
+ * @returns Connected account or null
212
+ */
213
+ async getById(accountId: string): Promise<ConnectedAccount | null> {
214
+ this.logger.debug(`Getting account by ID: ${accountId}`);
215
+
216
+ try {
217
+ return await this.accountRepository.findById(accountId);
218
+ } catch (error) {
219
+ this.logger.error(`Failed to get account by ID: ${accountId}`, error);
220
+ throw error;
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Check if user has account with provider
226
+ * @param userId - User identifier
227
+ * @param provider - Provider name
228
+ * @returns True if user has account with provider
229
+ */
230
+ async hasProviderAccount(userId: string, provider: string): Promise<boolean> {
231
+ this.logger.debug(`Checking if user ${userId} has ${provider} account`);
232
+
233
+ try {
234
+ const accounts = await this.accountRepository.findByUserId(userId);
235
+ return accounts.some(account => account.provider === provider && account.isActive);
236
+ } catch (error) {
237
+ this.logger.error(`Failed to check provider account for user: ${userId}`, error);
238
+ return false;
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Get account count for user
244
+ * @param userId - User identifier
245
+ * @returns Number of connected accounts
246
+ */
247
+ async getAccountCount(userId: string): Promise<number> {
248
+ this.logger.debug(`Getting account count for user: ${userId}`);
249
+
250
+ try {
251
+ const accounts = await this.accountRepository.findByUserId(userId);
252
+ return accounts.filter(account => account.isActive).length;
253
+ } catch (error) {
254
+ this.logger.error(`Failed to get account count for user: ${userId}`, error);
255
+ return 0;
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Emit event (mock implementation)
261
+ * @param eventType - Event type
262
+ * @param payload - Event payload
263
+ * @private
264
+ */
265
+ private emitEvent(eventType: string, payload: unknown): void {
266
+ // Mock event emission - in real implementation would use event system
267
+ this.logger.debug(`Event: ${eventType}`, payload);
268
+ }
269
+ }
@@ -0,0 +1,79 @@
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';
10
+
11
+
12
+ @Injectable()
13
+ export class AuthService {
14
+ // eslint-disable-next-line max-params
15
+ constructor(
16
+ private jwtManager:JwtManager,
17
+ private sessionManager: SessionManager,
18
+ private traditionalAuth: TraditionalAuthStrategy,
19
+ private oauthAuth: OAuthStrategy,
20
+ private userRepo: UserRepository
21
+ ) {}
22
+
23
+ async signIn(email: string, password: string, deviceInfo: Record<string,unknown>): Promise<{ user: User; tokens: AuthTokens; session: Session }> {
24
+ const user = await this.traditionalAuth.authenticate(email, password);
25
+ const session = await this.sessionManager.createSession(user.id, deviceInfo);
26
+ const tokens = this.jwtManager.generateTokens(user);
27
+
28
+ await this.userRepo.updateLastLogin(user.id);
29
+
30
+ return { user, tokens: { ...tokens, refreshToken: tokens.refreshToken ?? '' }, session };
31
+ }
32
+
33
+ async signUp(userData: Partial<User>, password: string): Promise<User> {
34
+ const passwordHash = await this.traditionalAuth.hashPassword(password);
35
+
36
+ return this.userRepo.create({
37
+ email: userData.email!,
38
+ displayName: userData.displayName!,
39
+ passwordHash,
40
+ authProvider: 'email',
41
+ isActive: true,
42
+ ...userData,
43
+ });
44
+ }
45
+
46
+ async signOut(sessionId: string): Promise<void> {
47
+ await this.sessionManager.invalidateSession(sessionId);
48
+ }
49
+
50
+ async signOutAll(userId: string): Promise<void> {
51
+ await this.sessionManager.invalidateAllUserSessions(userId);
52
+ }
53
+
54
+ async refreshToken(refreshToken: string): Promise<AuthTokens> {
55
+ const payload = this.jwtManager.verifyRefreshToken(refreshToken);
56
+ const user = await this.userRepo.findById(payload.sub);
57
+
58
+ if (!user?.isActive) {
59
+ throw new Error('Invalid refresh token');
60
+ }
61
+
62
+ const tokens = this.jwtManager.generateTokens(user);
63
+ return { ...tokens, refreshToken: tokens.refreshToken ?? '' };
64
+ }
65
+
66
+ async validateUser(userId: string): Promise<User | null> {
67
+ return this.userRepo.findById(userId);
68
+ }
69
+
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 ?? {});
73
+ const tokens = this.jwtManager.generateTokens(user);
74
+
75
+ await this.userRepo.updateLastLogin(user.id);
76
+
77
+ return { user, tokens: { ...tokens, refreshToken: tokens.refreshToken ?? '' }, session };
78
+ }
79
+ }