securepool 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 (126) hide show
  1. package/.dockerignore +7 -0
  2. package/.env.example +20 -0
  3. package/ARCHITECTURE.md +279 -0
  4. package/DEPLOYMENT.md +441 -0
  5. package/README.md +283 -0
  6. package/SETUP.md +388 -0
  7. package/apps/demo-backend/Dockerfile +33 -0
  8. package/apps/demo-backend/package.json +19 -0
  9. package/apps/demo-backend/src/index.ts +71 -0
  10. package/apps/demo-backend/tsconfig.json +8 -0
  11. package/apps/demo-frontend/.env.example +2 -0
  12. package/apps/demo-frontend/README.md +73 -0
  13. package/apps/demo-frontend/eslint.config.js +23 -0
  14. package/apps/demo-frontend/index.html +13 -0
  15. package/apps/demo-frontend/package.json +24 -0
  16. package/apps/demo-frontend/public/favicon.svg +1 -0
  17. package/apps/demo-frontend/public/icons.svg +24 -0
  18. package/apps/demo-frontend/src/App.tsx +33 -0
  19. package/apps/demo-frontend/src/assets/hero.png +0 -0
  20. package/apps/demo-frontend/src/assets/vite.svg +1 -0
  21. package/apps/demo-frontend/src/components/AccountSwitcher.tsx +373 -0
  22. package/apps/demo-frontend/src/components/ChangePasswordModal.tsx +128 -0
  23. package/apps/demo-frontend/src/index.css +272 -0
  24. package/apps/demo-frontend/src/main.tsx +10 -0
  25. package/apps/demo-frontend/src/pages/DashboardPage.tsx +141 -0
  26. package/apps/demo-frontend/src/pages/ForgotPasswordPage.tsx +183 -0
  27. package/apps/demo-frontend/src/pages/LoginPage.tsx +158 -0
  28. package/apps/demo-frontend/src/pages/OtpLoginPage.tsx +114 -0
  29. package/apps/demo-frontend/src/pages/SignupPage.tsx +95 -0
  30. package/apps/demo-frontend/src/pages/VerifyEmailPage.tsx +84 -0
  31. package/apps/demo-frontend/tsconfig.app.json +28 -0
  32. package/apps/demo-frontend/tsconfig.json +7 -0
  33. package/apps/demo-frontend/tsconfig.node.json +26 -0
  34. package/apps/demo-frontend/vite.config.ts +15 -0
  35. package/docs/DATABASE_MONGODB.md +280 -0
  36. package/docs/DATABASE_SQL.md +472 -0
  37. package/package.json +21 -0
  38. package/packages/api/package.json +30 -0
  39. package/packages/api/src/createSecurePool.ts +113 -0
  40. package/packages/api/src/index.ts +8 -0
  41. package/packages/api/src/middleware/authMiddleware.ts +26 -0
  42. package/packages/api/src/middleware/authorize.ts +24 -0
  43. package/packages/api/src/middleware/rateLimiter.ts +25 -0
  44. package/packages/api/src/middleware/tenantMiddleware.ts +12 -0
  45. package/packages/api/src/routes/authRoutes.ts +229 -0
  46. package/packages/api/src/routes/sessionRoutes.ts +30 -0
  47. package/packages/api/src/swagger.ts +529 -0
  48. package/packages/api/tsconfig.json +8 -0
  49. package/packages/application/package.json +16 -0
  50. package/packages/application/src/index.ts +17 -0
  51. package/packages/application/src/interfaces/IAuditLogRepository.ts +6 -0
  52. package/packages/application/src/interfaces/IAuthPlugin.ts +4 -0
  53. package/packages/application/src/interfaces/IEmailService.ts +3 -0
  54. package/packages/application/src/interfaces/IGoogleAuthService.ts +3 -0
  55. package/packages/application/src/interfaces/IOtpRepository.ts +8 -0
  56. package/packages/application/src/interfaces/IOtpService.ts +4 -0
  57. package/packages/application/src/interfaces/IPasswordHasher.ts +4 -0
  58. package/packages/application/src/interfaces/IRoleRepository.ts +8 -0
  59. package/packages/application/src/interfaces/ISessionRepository.ts +8 -0
  60. package/packages/application/src/interfaces/ITokenRepository.ts +9 -0
  61. package/packages/application/src/interfaces/ITokenService.ts +5 -0
  62. package/packages/application/src/interfaces/IUserRepository.ts +8 -0
  63. package/packages/application/src/services/AuthService.ts +323 -0
  64. package/packages/application/src/services/RefreshTokenService.ts +53 -0
  65. package/packages/application/tsconfig.json +8 -0
  66. package/packages/core/package.json +13 -0
  67. package/packages/core/src/entities/AuditLog.ts +11 -0
  68. package/packages/core/src/entities/OtpCode.ts +10 -0
  69. package/packages/core/src/entities/RefreshToken.ts +9 -0
  70. package/packages/core/src/entities/Role.ts +6 -0
  71. package/packages/core/src/entities/Session.ts +10 -0
  72. package/packages/core/src/entities/Tenant.ts +7 -0
  73. package/packages/core/src/entities/User.ts +10 -0
  74. package/packages/core/src/entities/UserRole.ts +6 -0
  75. package/packages/core/src/enums/index.ts +22 -0
  76. package/packages/core/src/index.ts +10 -0
  77. package/packages/core/tsconfig.json +8 -0
  78. package/packages/infrastructure/package.json +24 -0
  79. package/packages/infrastructure/src/email/NodemailerEmailService.ts +55 -0
  80. package/packages/infrastructure/src/google/GoogleAuthServiceImpl.ts +28 -0
  81. package/packages/infrastructure/src/hashing/BcryptHasher.ts +18 -0
  82. package/packages/infrastructure/src/index.ts +6 -0
  83. package/packages/infrastructure/src/jwt/JwtTokenService.ts +32 -0
  84. package/packages/infrastructure/src/otp/OtpServiceImpl.ts +50 -0
  85. package/packages/infrastructure/tsconfig.json +8 -0
  86. package/packages/persistence/package.json +22 -0
  87. package/packages/persistence/prisma/schema.prisma +88 -0
  88. package/packages/persistence/src/factory.ts +48 -0
  89. package/packages/persistence/src/index.ts +30 -0
  90. package/packages/persistence/src/mongo/connection.ts +9 -0
  91. package/packages/persistence/src/mongo/models/AuditLogModel.ts +21 -0
  92. package/packages/persistence/src/mongo/models/OtpModel.ts +19 -0
  93. package/packages/persistence/src/mongo/models/RefreshTokenModel.ts +17 -0
  94. package/packages/persistence/src/mongo/models/RoleModel.ts +11 -0
  95. package/packages/persistence/src/mongo/models/SessionModel.ts +19 -0
  96. package/packages/persistence/src/mongo/models/UserModel.ts +21 -0
  97. package/packages/persistence/src/mongo/models/UserRoleModel.ts +15 -0
  98. package/packages/persistence/src/mongo/repositories/MongoAuditLogRepository.ts +29 -0
  99. package/packages/persistence/src/mongo/repositories/MongoOtpRepository.ts +34 -0
  100. package/packages/persistence/src/mongo/repositories/MongoRoleRepository.ts +32 -0
  101. package/packages/persistence/src/mongo/repositories/MongoSessionRepository.ts +29 -0
  102. package/packages/persistence/src/mongo/repositories/MongoTokenRepository.ts +34 -0
  103. package/packages/persistence/src/mongo/repositories/MongoUserRepository.ts +37 -0
  104. package/packages/persistence/src/prisma/repositories/PrismaAuditLogRepository.ts +37 -0
  105. package/packages/persistence/src/prisma/repositories/PrismaOtpRepository.ts +43 -0
  106. package/packages/persistence/src/prisma/repositories/PrismaRoleRepository.ts +36 -0
  107. package/packages/persistence/src/prisma/repositories/PrismaSessionRepository.ts +39 -0
  108. package/packages/persistence/src/prisma/repositories/PrismaTokenRepository.ts +50 -0
  109. package/packages/persistence/src/prisma/repositories/PrismaUserRepository.ts +45 -0
  110. package/packages/persistence/tsconfig.json +8 -0
  111. package/packages/react-sdk/package.json +23 -0
  112. package/packages/react-sdk/src/components/GoogleLoginButton.tsx +54 -0
  113. package/packages/react-sdk/src/components/LoginForm.tsx +67 -0
  114. package/packages/react-sdk/src/components/OTPVerification.tsx +104 -0
  115. package/packages/react-sdk/src/components/SessionList.tsx +64 -0
  116. package/packages/react-sdk/src/components/SignupForm.tsx +95 -0
  117. package/packages/react-sdk/src/context/AuthContext.ts +4 -0
  118. package/packages/react-sdk/src/context/SecurePoolProvider.tsx +492 -0
  119. package/packages/react-sdk/src/hooks/useAuth.ts +11 -0
  120. package/packages/react-sdk/src/index.ts +22 -0
  121. package/packages/react-sdk/src/types.ts +53 -0
  122. package/packages/react-sdk/tsconfig.json +12 -0
  123. package/scripts/setup.js +285 -0
  124. package/scripts/setup.sh +309 -0
  125. package/tsconfig.base.json +16 -0
  126. package/turbo.json +16 -0
@@ -0,0 +1,323 @@
1
+ import crypto from "crypto";
2
+ import { User, AuditLog, AuditAction, RefreshToken } from "@securepool/core";
3
+ import { IUserRepository } from "../interfaces/IUserRepository";
4
+ import { IPasswordHasher } from "../interfaces/IPasswordHasher";
5
+ import { ITokenService } from "../interfaces/ITokenService";
6
+ import { ITokenRepository } from "../interfaces/ITokenRepository";
7
+ import { IOtpService } from "../interfaces/IOtpService";
8
+ import { IAuditLogRepository } from "../interfaces/IAuditLogRepository";
9
+ import { IEmailService } from "../interfaces/IEmailService";
10
+
11
+ function normalizeEmail(raw: string): string {
12
+ return raw.trim().toLowerCase();
13
+ }
14
+
15
+ export class AuthService {
16
+ constructor(
17
+ private readonly userRepository: IUserRepository,
18
+ private readonly passwordHasher: IPasswordHasher,
19
+ private readonly tokenService: ITokenService,
20
+ private readonly tokenRepository: ITokenRepository,
21
+ private readonly otpService?: IOtpService,
22
+ private readonly auditLogRepository?: IAuditLogRepository,
23
+ private readonly emailService?: IEmailService,
24
+ ) {}
25
+
26
+ // Helper: generate tokens and save refresh token to DB
27
+ private async generateAndSaveTokens(
28
+ userId: string,
29
+ tenantId: string,
30
+ ): Promise<{ accessToken: string; refreshToken: string }> {
31
+ const accessToken = await this.tokenService.generateAccessToken(userId, tenantId);
32
+ const refreshToken = await this.tokenService.generateRefreshToken(userId);
33
+
34
+ // Save refresh token to DB so it can be looked up on refresh
35
+ await this.tokenRepository.save(new RefreshToken(
36
+ crypto.randomUUID(),
37
+ userId,
38
+ refreshToken, // stored as-is; looked up by exact match
39
+ new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
40
+ false,
41
+ ));
42
+
43
+ return { accessToken, refreshToken };
44
+ }
45
+
46
+ // Step 1: Validate + send OTP (does NOT create user yet)
47
+ async register(
48
+ rawEmail: string,
49
+ password: string,
50
+ tenantId: string,
51
+ ): Promise<{ email: string }> {
52
+ const email = normalizeEmail(rawEmail);
53
+
54
+ const existing = await this.userRepository.findByEmail(email, tenantId);
55
+ if (existing) {
56
+ throw new Error("User with this email already exists");
57
+ }
58
+
59
+ if (!this.otpService) throw new Error("OTP service is not configured");
60
+
61
+ const passwordHash = await this.passwordHasher.hash(password);
62
+
63
+ const pendingKey = `pending:${tenantId}:${email}`;
64
+ const code = await this.otpService.generate(pendingKey, {
65
+ email,
66
+ passwordHash,
67
+ tenantId,
68
+ });
69
+
70
+ if (this.emailService) {
71
+ await this.emailService.sendOtp(email, code);
72
+ }
73
+
74
+ return { email };
75
+ }
76
+
77
+ // Step 2: Verify OTP → create user → return tokens
78
+ async verifyEmail(
79
+ rawEmail: string,
80
+ code: string,
81
+ tenantId: string,
82
+ ): Promise<{ accessToken: string; refreshToken: string }> {
83
+ const email = normalizeEmail(rawEmail);
84
+
85
+ if (!this.otpService) throw new Error("OTP service is not configured");
86
+
87
+ const existing = await this.userRepository.findByEmail(email, tenantId);
88
+ if (existing) {
89
+ throw new Error("User already exists. Please login instead.");
90
+ }
91
+
92
+ const pendingKey = `pending:${tenantId}:${email}`;
93
+ const result = await this.otpService.verify(pendingKey, code);
94
+
95
+ if (!result.metadata?.passwordHash) {
96
+ throw new Error("Registration data not found. Please register again.");
97
+ }
98
+
99
+ const user = new User(
100
+ crypto.randomUUID(),
101
+ tenantId,
102
+ email,
103
+ result.metadata.passwordHash,
104
+ true,
105
+ new Date(),
106
+ );
107
+ await this.userRepository.create(user);
108
+
109
+ const tokens = await this.generateAndSaveTokens(user.id, tenantId);
110
+
111
+ if (this.auditLogRepository) {
112
+ await this.auditLogRepository.log(new AuditLog(
113
+ crypto.randomUUID(), user.id, tenantId,
114
+ AuditAction.REGISTER, "unknown", { email }, new Date(),
115
+ ));
116
+ }
117
+
118
+ return tokens;
119
+ }
120
+
121
+ // Login
122
+ async login(
123
+ rawEmail: string,
124
+ password: string,
125
+ tenantId: string,
126
+ ip: string,
127
+ ): Promise<{ accessToken: string; refreshToken: string }> {
128
+ const email = normalizeEmail(rawEmail);
129
+
130
+ const user = await this.userRepository.findByEmail(email, tenantId);
131
+ if (!user || !user.passwordHash) {
132
+ throw new Error("Invalid email or password");
133
+ }
134
+
135
+ const isValid = await this.passwordHasher.compare(password, user.passwordHash);
136
+ if (!isValid) {
137
+ if (this.auditLogRepository) {
138
+ await this.auditLogRepository.log(new AuditLog(
139
+ crypto.randomUUID(), user.id, tenantId,
140
+ AuditAction.LOGIN_FAILED, ip, { email }, new Date(),
141
+ ));
142
+ }
143
+ throw new Error("Invalid email or password");
144
+ }
145
+
146
+ if (!user.isVerified) {
147
+ throw new Error("Email not verified. Please verify your email first.");
148
+ }
149
+
150
+ const tokens = await this.generateAndSaveTokens(user.id, tenantId);
151
+
152
+ if (this.auditLogRepository) {
153
+ await this.auditLogRepository.log(new AuditLog(
154
+ crypto.randomUUID(), user.id, tenantId,
155
+ AuditAction.LOGIN_SUCCESS, ip, { email }, new Date(),
156
+ ));
157
+ }
158
+
159
+ return tokens;
160
+ }
161
+
162
+ // Google SSO login
163
+ async loginWithGoogle(
164
+ googleUser: { email: string; name: string; googleId: string },
165
+ tenantId: string,
166
+ ip: string,
167
+ ): Promise<{ accessToken: string; refreshToken: string }> {
168
+ const email = normalizeEmail(googleUser.email);
169
+
170
+ let user = await this.userRepository.findByEmail(email, tenantId);
171
+
172
+ if (!user) {
173
+ user = new User(
174
+ crypto.randomUUID(), tenantId, email,
175
+ null, true, new Date(),
176
+ );
177
+ await this.userRepository.create(user);
178
+ }
179
+
180
+ const tokens = await this.generateAndSaveTokens(user.id, tenantId);
181
+
182
+ if (this.auditLogRepository) {
183
+ await this.auditLogRepository.log(new AuditLog(
184
+ crypto.randomUUID(), user.id, tenantId,
185
+ AuditAction.LOGIN_SUCCESS, ip,
186
+ { provider: "google", googleId: googleUser.googleId }, new Date(),
187
+ ));
188
+ }
189
+
190
+ return tokens;
191
+ }
192
+
193
+ // Request OTP (for OTP login)
194
+ async requestOtp(rawEmail: string, tenantId: string): Promise<string> {
195
+ const email = normalizeEmail(rawEmail);
196
+
197
+ if (!this.otpService) throw new Error("OTP service is not configured");
198
+
199
+ const user = await this.userRepository.findByEmail(email, tenantId);
200
+ if (!user) throw new Error("User not found");
201
+
202
+ const code = await this.otpService.generate(user.id);
203
+
204
+ if (this.emailService) {
205
+ await this.emailService.sendOtp(email, code);
206
+ }
207
+
208
+ if (this.auditLogRepository) {
209
+ await this.auditLogRepository.log(new AuditLog(
210
+ crypto.randomUUID(), user.id, tenantId,
211
+ AuditAction.OTP_GENERATED, "unknown", { email }, new Date(),
212
+ ));
213
+ }
214
+
215
+ return code;
216
+ }
217
+
218
+ // Verify OTP (for OTP login)
219
+ async verifyOtp(
220
+ rawEmail: string, code: string, tenantId: string, ip: string,
221
+ ): Promise<{ accessToken: string; refreshToken: string }> {
222
+ const email = normalizeEmail(rawEmail);
223
+
224
+ if (!this.otpService) throw new Error("OTP service is not configured");
225
+
226
+ const user = await this.userRepository.findByEmail(email, tenantId);
227
+ if (!user) throw new Error("User not found");
228
+
229
+ await this.otpService.verify(user.id, code);
230
+
231
+ // OTP proves email ownership - ensure user is verified
232
+ if (!user.isVerified) {
233
+ user.isVerified = true;
234
+ await this.userRepository.update(user);
235
+ }
236
+
237
+ const tokens = await this.generateAndSaveTokens(user.id, tenantId);
238
+
239
+ if (this.auditLogRepository) {
240
+ await this.auditLogRepository.log(new AuditLog(
241
+ crypto.randomUUID(), user.id, tenantId,
242
+ AuditAction.OTP_VERIFIED, ip, { email }, new Date(),
243
+ ));
244
+ }
245
+
246
+ return tokens;
247
+ }
248
+
249
+ // Forgot password - sends OTP to email
250
+ async forgotPassword(rawEmail: string, tenantId: string): Promise<void> {
251
+ const email = normalizeEmail(rawEmail);
252
+
253
+ if (!this.otpService) throw new Error("OTP service is not configured");
254
+
255
+ const user = await this.userRepository.findByEmail(email, tenantId);
256
+ if (!user) throw new Error("User not found");
257
+
258
+ const code = await this.otpService.generate(user.id);
259
+
260
+ if (this.emailService) {
261
+ await this.emailService.sendOtp(email, code);
262
+ }
263
+
264
+ if (this.auditLogRepository) {
265
+ await this.auditLogRepository.log(new AuditLog(
266
+ crypto.randomUUID(), user.id, tenantId,
267
+ AuditAction.PASSWORD_RESET, "unknown",
268
+ { email, step: "otp_sent" }, new Date(),
269
+ ));
270
+ }
271
+ }
272
+
273
+ // Reset password - verify OTP + set new password
274
+ async resetPassword(
275
+ rawEmail: string, code: string, newPassword: string, tenantId: string,
276
+ ): Promise<void> {
277
+ const email = normalizeEmail(rawEmail);
278
+
279
+ if (!this.otpService) throw new Error("OTP service is not configured");
280
+
281
+ const user = await this.userRepository.findByEmail(email, tenantId);
282
+ if (!user) throw new Error("User not found");
283
+
284
+ await this.otpService.verify(user.id, code);
285
+
286
+ user.passwordHash = await this.passwordHasher.hash(newPassword);
287
+ await this.userRepository.update(user);
288
+
289
+ if (this.auditLogRepository) {
290
+ await this.auditLogRepository.log(new AuditLog(
291
+ crypto.randomUUID(), user.id, tenantId,
292
+ AuditAction.PASSWORD_RESET, "unknown",
293
+ { email, step: "password_changed" }, new Date(),
294
+ ));
295
+ }
296
+ }
297
+
298
+ // Change password (authenticated, requires old password)
299
+ async changePassword(
300
+ userId: string, oldPassword: string, newPassword: string, tenantId: string,
301
+ ): Promise<void> {
302
+ const user = await this.userRepository.findById(userId);
303
+ if (!user || !user.passwordHash) {
304
+ throw new Error("User not found");
305
+ }
306
+
307
+ const isValid = await this.passwordHasher.compare(oldPassword, user.passwordHash);
308
+ if (!isValid) {
309
+ throw new Error("Current password is incorrect");
310
+ }
311
+
312
+ user.passwordHash = await this.passwordHasher.hash(newPassword);
313
+ await this.userRepository.update(user);
314
+
315
+ if (this.auditLogRepository) {
316
+ await this.auditLogRepository.log(new AuditLog(
317
+ crypto.randomUUID(), user.id, tenantId,
318
+ AuditAction.PASSWORD_RESET, "unknown",
319
+ { step: "password_changed_by_user" }, new Date(),
320
+ ));
321
+ }
322
+ }
323
+ }
@@ -0,0 +1,53 @@
1
+ import crypto from "crypto";
2
+ import { RefreshToken } from "@securepool/core";
3
+ import { ITokenRepository } from "../interfaces/ITokenRepository";
4
+ import { ITokenService } from "../interfaces/ITokenService";
5
+
6
+ export class RefreshTokenService {
7
+ constructor(
8
+ private readonly tokenRepository: ITokenRepository,
9
+ private readonly tokenService: ITokenService,
10
+ ) {}
11
+
12
+ async refresh(
13
+ oldToken: string,
14
+ ): Promise<{ accessToken: string; refreshToken: string }> {
15
+ // Find the refresh token by exact match (tokenHash stores the raw token)
16
+ const existingToken = await this.tokenRepository.findByTokenHash(oldToken);
17
+
18
+ if (!existingToken) {
19
+ throw new Error("Invalid refresh token");
20
+ }
21
+
22
+ if (existingToken.isRevoked) {
23
+ throw new Error("Refresh token has been revoked");
24
+ }
25
+
26
+ if (existingToken.expiresAt < new Date()) {
27
+ throw new Error("Refresh token has expired");
28
+ }
29
+
30
+ // Revoke old token (rotation)
31
+ await this.tokenRepository.revoke(existingToken.id);
32
+
33
+ // Generate new token pair
34
+ const accessToken = await this.tokenService.generateAccessToken(
35
+ existingToken.userId,
36
+ "default", // tenantId from the original token
37
+ );
38
+ const newRefreshToken = await this.tokenService.generateRefreshToken(
39
+ existingToken.userId,
40
+ );
41
+
42
+ // Save new refresh token to DB
43
+ await this.tokenRepository.save(new RefreshToken(
44
+ crypto.randomUUID(),
45
+ existingToken.userId,
46
+ newRefreshToken,
47
+ new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
48
+ false,
49
+ ));
50
+
51
+ return { accessToken, refreshToken: newRefreshToken };
52
+ }
53
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist"
6
+ },
7
+ "include": ["src"]
8
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@securepool/core",
3
+ "version": "1.0.0",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "scripts": {
7
+ "build": "tsc",
8
+ "clean": "rm -rf dist"
9
+ },
10
+ "devDependencies": {
11
+ "typescript": "^5.5.0"
12
+ }
13
+ }
@@ -0,0 +1,11 @@
1
+ export class AuditLog {
2
+ constructor(
3
+ public id: string,
4
+ public userId: string,
5
+ public tenantId: string,
6
+ public action: string,
7
+ public ip: string,
8
+ public metadata: Record<string, unknown>,
9
+ public timestamp: Date,
10
+ ) {}
11
+ }
@@ -0,0 +1,10 @@
1
+ export class OtpCode {
2
+ constructor(
3
+ public id: string,
4
+ public userId: string,
5
+ public code: string,
6
+ public expiresAt: Date,
7
+ public attempts: number,
8
+ public metadata?: Record<string, string>,
9
+ ) {}
10
+ }
@@ -0,0 +1,9 @@
1
+ export class RefreshToken {
2
+ constructor(
3
+ public id: string,
4
+ public userId: string,
5
+ public tokenHash: string,
6
+ public expiresAt: Date,
7
+ public isRevoked: boolean,
8
+ ) {}
9
+ }
@@ -0,0 +1,6 @@
1
+ export class Role {
2
+ constructor(
3
+ public id: string,
4
+ public name: string,
5
+ ) {}
6
+ }
@@ -0,0 +1,10 @@
1
+ export class Session {
2
+ constructor(
3
+ public id: string,
4
+ public userId: string,
5
+ public device: string,
6
+ public ip: string,
7
+ public createdAt: Date,
8
+ public isActive: boolean,
9
+ ) {}
10
+ }
@@ -0,0 +1,7 @@
1
+ export class Tenant {
2
+ constructor(
3
+ public id: string,
4
+ public name: string,
5
+ public createdAt: Date,
6
+ ) {}
7
+ }
@@ -0,0 +1,10 @@
1
+ export class User {
2
+ constructor(
3
+ public id: string,
4
+ public tenantId: string,
5
+ public email: string,
6
+ public passwordHash: string | null,
7
+ public isVerified: boolean,
8
+ public createdAt: Date,
9
+ ) {}
10
+ }
@@ -0,0 +1,6 @@
1
+ export class UserRole {
2
+ constructor(
3
+ public userId: string,
4
+ public roleId: string,
5
+ ) {}
6
+ }
@@ -0,0 +1,22 @@
1
+ export enum RoleType {
2
+ ADMIN = "ADMIN",
3
+ USER = "USER",
4
+ MODERATOR = "MODERATOR",
5
+ }
6
+
7
+ export enum AuthProvider {
8
+ EMAIL = "EMAIL",
9
+ GOOGLE = "GOOGLE",
10
+ OTP = "OTP",
11
+ }
12
+
13
+ export enum AuditAction {
14
+ LOGIN_SUCCESS = "LOGIN_SUCCESS",
15
+ LOGIN_FAILED = "LOGIN_FAILED",
16
+ REGISTER = "REGISTER",
17
+ PASSWORD_RESET = "PASSWORD_RESET",
18
+ TOKEN_REFRESH = "TOKEN_REFRESH",
19
+ LOGOUT = "LOGOUT",
20
+ OTP_GENERATED = "OTP_GENERATED",
21
+ OTP_VERIFIED = "OTP_VERIFIED",
22
+ }
@@ -0,0 +1,10 @@
1
+ export { User } from "./entities/User";
2
+ export { Role } from "./entities/Role";
3
+ export { UserRole } from "./entities/UserRole";
4
+ export { OtpCode } from "./entities/OtpCode";
5
+ export { RefreshToken } from "./entities/RefreshToken";
6
+ export { Session } from "./entities/Session";
7
+ export { Tenant } from "./entities/Tenant";
8
+ export { AuditLog } from "./entities/AuditLog";
9
+
10
+ export { RoleType, AuthProvider, AuditAction } from "./enums";
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist"
6
+ },
7
+ "include": ["src"]
8
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@securepool/infrastructure",
3
+ "version": "1.0.0",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "scripts": {
7
+ "build": "tsc",
8
+ "clean": "rm -rf dist"
9
+ },
10
+ "dependencies": {
11
+ "@securepool/core": "1.0.0",
12
+ "@securepool/application": "1.0.0",
13
+ "bcrypt": "^5.1.1",
14
+ "jsonwebtoken": "^9.0.2",
15
+ "google-auth-library": "^9.14.0",
16
+ "nodemailer": "^6.9.0"
17
+ },
18
+ "devDependencies": {
19
+ "typescript": "^5.5.0",
20
+ "@types/bcrypt": "^5.0.2",
21
+ "@types/jsonwebtoken": "^9.0.7",
22
+ "@types/nodemailer": "^6.4.0"
23
+ }
24
+ }
@@ -0,0 +1,55 @@
1
+ import nodemailer, { Transporter } from "nodemailer";
2
+ import { IEmailService } from "@securepool/application";
3
+
4
+ export interface EmailConfig {
5
+ host: string;
6
+ port: number;
7
+ secure: boolean;
8
+ auth: {
9
+ user: string;
10
+ pass: string;
11
+ };
12
+ from: string;
13
+ }
14
+
15
+ export class NodemailerEmailService implements IEmailService {
16
+ private transporter: Transporter;
17
+ private from: string;
18
+
19
+ constructor(config: EmailConfig) {
20
+ this.from = config.from;
21
+ this.transporter = nodemailer.createTransport({
22
+ host: config.host,
23
+ port: config.port,
24
+ secure: config.secure,
25
+ auth: {
26
+ user: config.auth.user,
27
+ pass: config.auth.pass,
28
+ },
29
+ });
30
+ }
31
+
32
+ async sendOtp(to: string, code: string): Promise<void> {
33
+ await this.transporter.sendMail({
34
+ from: `"SecurePool Auth" <${this.from}>`,
35
+ to,
36
+ subject: "Your SecurePool OTP Code",
37
+ text: `Your OTP code is: ${code}\n\nThis code expires in 5 minutes. Do not share it with anyone.`,
38
+ html: `
39
+ <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 480px; margin: 0 auto; padding: 40px 20px;">
40
+ <div style="background: #1e293b; border-radius: 12px; padding: 40px; text-align: center;">
41
+ <h2 style="color: #f1f5f9; margin: 0 0 8px;">SecurePool</h2>
42
+ <p style="color: #94a3b8; font-size: 14px; margin: 0 0 32px;">Your one-time verification code</p>
43
+ <div style="background: #0f172a; border-radius: 8px; padding: 20px; margin: 0 0 24px;">
44
+ <span style="font-size: 36px; font-weight: 700; letter-spacing: 8px; color: #3b82f6;">${code}</span>
45
+ </div>
46
+ <p style="color: #64748b; font-size: 13px; margin: 0;">
47
+ This code expires in <strong style="color: #94a3b8;">5 minutes</strong>.<br/>
48
+ If you didn't request this, ignore this email.
49
+ </p>
50
+ </div>
51
+ </div>
52
+ `,
53
+ });
54
+ }
55
+ }
@@ -0,0 +1,28 @@
1
+ import { OAuth2Client } from "google-auth-library";
2
+ import { IGoogleAuthService } from "@securepool/application";
3
+
4
+ export class GoogleAuthServiceImpl implements IGoogleAuthService {
5
+ private client: OAuth2Client;
6
+
7
+ constructor(private clientId: string) {
8
+ this.client = new OAuth2Client(clientId);
9
+ }
10
+
11
+ async verifyToken(token: string): Promise<{ email: string; name: string; googleId: string }> {
12
+ const ticket = await this.client.verifyIdToken({
13
+ idToken: token,
14
+ audience: this.clientId,
15
+ });
16
+
17
+ const payload = ticket.getPayload();
18
+ if (!payload || !payload.email) {
19
+ throw new Error("Invalid Google token");
20
+ }
21
+
22
+ return {
23
+ email: payload.email,
24
+ name: payload.name || "",
25
+ googleId: payload.sub,
26
+ };
27
+ }
28
+ }
@@ -0,0 +1,18 @@
1
+ import bcrypt from "bcrypt";
2
+ import { IPasswordHasher } from "@securepool/application";
3
+
4
+ export class BcryptHasher implements IPasswordHasher {
5
+ private saltRounds: number;
6
+
7
+ constructor(saltRounds: number = 12) {
8
+ this.saltRounds = saltRounds;
9
+ }
10
+
11
+ async hash(password: string): Promise<string> {
12
+ return bcrypt.hash(password, this.saltRounds);
13
+ }
14
+
15
+ async compare(password: string, hash: string): Promise<boolean> {
16
+ return bcrypt.compare(password, hash);
17
+ }
18
+ }
@@ -0,0 +1,6 @@
1
+ export { JwtTokenService } from "./jwt/JwtTokenService";
2
+ export { BcryptHasher } from "./hashing/BcryptHasher";
3
+ export { OtpServiceImpl } from "./otp/OtpServiceImpl";
4
+ export { GoogleAuthServiceImpl } from "./google/GoogleAuthServiceImpl";
5
+ export { NodemailerEmailService } from "./email/NodemailerEmailService";
6
+ export type { EmailConfig } from "./email/NodemailerEmailService";