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,25 @@
1
+ import rateLimit from "express-rate-limit";
2
+
3
+ export const loginRateLimiter = rateLimit({
4
+ windowMs: 15 * 60 * 1000,
5
+ max: 10,
6
+ message: { error: "Too many login attempts. Please try again later." },
7
+ standardHeaders: true,
8
+ legacyHeaders: false,
9
+ });
10
+
11
+ export const apiRateLimiter = rateLimit({
12
+ windowMs: 15 * 60 * 1000,
13
+ max: 100,
14
+ message: { error: "Too many requests. Please try again later." },
15
+ standardHeaders: true,
16
+ legacyHeaders: false,
17
+ });
18
+
19
+ export const otpRateLimiter = rateLimit({
20
+ windowMs: 15 * 60 * 1000,
21
+ max: 5,
22
+ message: { error: "Too many OTP requests. Please try again later." },
23
+ standardHeaders: true,
24
+ legacyHeaders: false,
25
+ });
@@ -0,0 +1,12 @@
1
+ import { Request, Response, NextFunction } from "express";
2
+ import { AuthenticatedRequest } from "./authMiddleware";
3
+
4
+ export function tenantMiddleware(req: AuthenticatedRequest, res: Response, next: NextFunction): void {
5
+ const tenantId = req.headers["x-tenant-id"] as string;
6
+ if (!tenantId) {
7
+ res.status(400).json({ error: "x-tenant-id header is required" });
8
+ return;
9
+ }
10
+ req.tenantId = tenantId;
11
+ next();
12
+ }
@@ -0,0 +1,229 @@
1
+ import { Router } from "express";
2
+ import { AuthService, RefreshTokenService } from "@securepool/application";
3
+ import { AuthenticatedRequest } from "../middleware/authMiddleware";
4
+ import { loginRateLimiter, otpRateLimiter } from "../middleware/rateLimiter";
5
+ import UAParser from "ua-parser-js";
6
+ import { ISessionRepository, IAuditLogRepository } from "@securepool/application";
7
+ import { Session } from "@securepool/core";
8
+ import crypto from "crypto";
9
+ import { ITokenService } from "@securepool/application";
10
+
11
+ export function createAuthRoutes(
12
+ authService: AuthService,
13
+ refreshTokenService: RefreshTokenService,
14
+ sessionRepo: ISessionRepository,
15
+ auditLogRepo: IAuditLogRepository,
16
+ tokenService: ITokenService,
17
+ authMiddleware: (req: any, res: any, next: any) => void,
18
+ ): Router {
19
+ const router = Router();
20
+
21
+ // POST /auth/register - creates user + sends verification OTP
22
+ router.post("/register", async (req: AuthenticatedRequest, res) => {
23
+ try {
24
+ const { email, password } = req.body;
25
+ const tenantId = req.tenantId || req.headers["x-tenant-id"] as string;
26
+ if (!email || !password || !tenantId) {
27
+ res.status(400).json({ error: "email, password, and tenant are required" });
28
+ return;
29
+ }
30
+ await authService.register(email, password, tenantId);
31
+ res.status(200).json({ message: "OTP sent to your email. Verify to complete registration.", email });
32
+ } catch (err: any) {
33
+ res.status(400).json({ error: err.message });
34
+ }
35
+ });
36
+
37
+ // POST /auth/verify-email - verify OTP after registration
38
+ router.post("/verify-email", async (req: AuthenticatedRequest, res) => {
39
+ try {
40
+ const { email, code } = req.body;
41
+ const tenantId = req.tenantId || req.headers["x-tenant-id"] as string;
42
+ const ip = req.ip || "unknown";
43
+ if (!email || !code || !tenantId) {
44
+ res.status(400).json({ error: "email, code, and tenant are required" });
45
+ return;
46
+ }
47
+ const result = await authService.verifyEmail(email, code, tenantId);
48
+
49
+ // Create session
50
+ const tokenPayload = JSON.parse(
51
+ Buffer.from(result.accessToken.split(".")[1], "base64").toString()
52
+ );
53
+ const parser = new UAParser(req.headers["user-agent"]);
54
+ const device = parser.getResult();
55
+ const session = new Session(
56
+ crypto.randomUUID(), tokenPayload.sub,
57
+ `${device.browser.name || "Unknown"} on ${device.os.name || "Unknown"}`,
58
+ ip, new Date(), true
59
+ );
60
+ await sessionRepo.create(session);
61
+
62
+ res.json(result);
63
+ } catch (err: any) {
64
+ res.status(400).json({ error: err.message });
65
+ }
66
+ });
67
+
68
+ // POST /auth/login
69
+ router.post("/login", loginRateLimiter, async (req: AuthenticatedRequest, res) => {
70
+ try {
71
+ const { email, password } = req.body;
72
+ const tenantId = req.tenantId || req.headers["x-tenant-id"] as string;
73
+ const ip = req.ip || "unknown";
74
+ if (!email || !password || !tenantId) {
75
+ res.status(400).json({ error: "email, password, and tenant are required" });
76
+ return;
77
+ }
78
+ const result = await authService.login(email, password, tenantId, ip);
79
+
80
+ const tokenPayload = JSON.parse(
81
+ Buffer.from(result.accessToken.split(".")[1], "base64").toString()
82
+ );
83
+ const parser = new UAParser(req.headers["user-agent"]);
84
+ const device = parser.getResult();
85
+ const session = new Session(
86
+ crypto.randomUUID(), tokenPayload.sub,
87
+ `${device.browser.name || "Unknown"} on ${device.os.name || "Unknown"}`,
88
+ ip, new Date(), true
89
+ );
90
+ await sessionRepo.create(session);
91
+
92
+ res.json({ accessToken: result.accessToken, refreshToken: result.refreshToken });
93
+ } catch (err: any) {
94
+ res.status(401).json({ error: err.message });
95
+ }
96
+ });
97
+
98
+ // POST /auth/refresh
99
+ router.post("/refresh", async (req, res) => {
100
+ try {
101
+ const { refreshToken } = req.body;
102
+ if (!refreshToken) {
103
+ res.status(400).json({ error: "refreshToken is required" });
104
+ return;
105
+ }
106
+ const tokens = await refreshTokenService.refresh(refreshToken);
107
+ res.json(tokens);
108
+ } catch (err: any) {
109
+ res.status(401).json({ error: err.message });
110
+ }
111
+ });
112
+
113
+ // POST /auth/google
114
+ router.post("/google", async (req: AuthenticatedRequest, res) => {
115
+ try {
116
+ const { token } = req.body;
117
+ const tenantId = req.tenantId || req.headers["x-tenant-id"] as string;
118
+ const ip = req.ip || "unknown";
119
+ if (!token || !tenantId) {
120
+ res.status(400).json({ error: "token and tenant are required" });
121
+ return;
122
+ }
123
+ const result = await authService.loginWithGoogle({ email: "", name: "", googleId: token }, tenantId, ip);
124
+ res.json(result);
125
+ } catch (err: any) {
126
+ res.status(401).json({ error: err.message });
127
+ }
128
+ });
129
+
130
+ // POST /auth/otp/request
131
+ router.post("/otp/request", otpRateLimiter, async (req: AuthenticatedRequest, res) => {
132
+ try {
133
+ const { email } = req.body;
134
+ const tenantId = req.tenantId || req.headers["x-tenant-id"] as string;
135
+ if (!email || !tenantId) {
136
+ res.status(400).json({ error: "email and tenant are required" });
137
+ return;
138
+ }
139
+ await authService.requestOtp(email, tenantId);
140
+ res.json({ message: "OTP sent to your email" });
141
+ } catch (err: any) {
142
+ res.status(400).json({ error: err.message });
143
+ }
144
+ });
145
+
146
+ // POST /auth/otp/verify
147
+ router.post("/otp/verify", async (req: AuthenticatedRequest, res) => {
148
+ try {
149
+ const { email, code } = req.body;
150
+ const tenantId = req.tenantId || req.headers["x-tenant-id"] as string;
151
+ const ip = req.ip || "unknown";
152
+ if (!email || !code || !tenantId) {
153
+ res.status(400).json({ error: "email, code, and tenant are required" });
154
+ return;
155
+ }
156
+ const result = await authService.verifyOtp(email, code, tenantId, ip);
157
+
158
+ // Create session
159
+ const tokenPayload = JSON.parse(
160
+ Buffer.from(result.accessToken.split(".")[1], "base64").toString()
161
+ );
162
+ const parser = new UAParser(req.headers["user-agent"]);
163
+ const device = parser.getResult();
164
+ const session = new Session(
165
+ crypto.randomUUID(), tokenPayload.sub,
166
+ `${device.browser.name || "Unknown"} on ${device.os.name || "Unknown"}`,
167
+ ip, new Date(), true
168
+ );
169
+ await sessionRepo.create(session);
170
+
171
+ res.json(result);
172
+ } catch (err: any) {
173
+ res.status(401).json({ error: err.message });
174
+ }
175
+ });
176
+
177
+ // POST /auth/forgot-password - sends OTP
178
+ router.post("/forgot-password", otpRateLimiter, async (req: AuthenticatedRequest, res) => {
179
+ try {
180
+ const { email } = req.body;
181
+ const tenantId = req.tenantId || req.headers["x-tenant-id"] as string;
182
+ if (!email || !tenantId) {
183
+ res.status(400).json({ error: "email and tenant are required" });
184
+ return;
185
+ }
186
+ await authService.forgotPassword(email, tenantId);
187
+ res.json({ message: "OTP sent to your email" });
188
+ } catch (err: any) {
189
+ res.status(400).json({ error: err.message });
190
+ }
191
+ });
192
+
193
+ // POST /auth/reset-password - verify OTP + set new password
194
+ router.post("/reset-password", async (req: AuthenticatedRequest, res) => {
195
+ try {
196
+ const { email, code, newPassword } = req.body;
197
+ const tenantId = req.tenantId || req.headers["x-tenant-id"] as string;
198
+ if (!email || !code || !newPassword || !tenantId) {
199
+ res.status(400).json({ error: "email, code, newPassword, and tenant are required" });
200
+ return;
201
+ }
202
+ await authService.resetPassword(email, code, newPassword, tenantId);
203
+ res.json({ message: "Password reset successfully" });
204
+ } catch (err: any) {
205
+ res.status(400).json({ error: err.message });
206
+ }
207
+ });
208
+
209
+ // POST /auth/change-password - authenticated, requires old password
210
+ router.post("/change-password", authMiddleware, async (req: AuthenticatedRequest, res) => {
211
+ try {
212
+ const { oldPassword, newPassword } = req.body;
213
+ if (!req.user) {
214
+ res.status(401).json({ error: "Not authenticated" });
215
+ return;
216
+ }
217
+ if (!oldPassword || !newPassword) {
218
+ res.status(400).json({ error: "oldPassword and newPassword are required" });
219
+ return;
220
+ }
221
+ await authService.changePassword(req.user.userId, oldPassword, newPassword, req.user.tenantId);
222
+ res.json({ message: "Password changed successfully" });
223
+ } catch (err: any) {
224
+ res.status(400).json({ error: err.message });
225
+ }
226
+ });
227
+
228
+ return router;
229
+ }
@@ -0,0 +1,30 @@
1
+ import { Router } from "express";
2
+ import { ISessionRepository } from "@securepool/application";
3
+ import { AuthenticatedRequest } from "../middleware/authMiddleware";
4
+
5
+ export function createSessionRoutes(sessionRepo: ISessionRepository): Router {
6
+ const router = Router();
7
+
8
+ // GET /sessions - list user's sessions
9
+ router.get("/", async (req: AuthenticatedRequest, res) => {
10
+ if (!req.user) { res.status(401).json({ error: "Not authenticated" }); return; }
11
+ const sessions = await sessionRepo.findByUserId(req.user.userId);
12
+ res.json({ sessions });
13
+ });
14
+
15
+ // DELETE /sessions/:id - deactivate a session
16
+ router.delete("/:id", async (req: AuthenticatedRequest, res) => {
17
+ if (!req.user) { res.status(401).json({ error: "Not authenticated" }); return; }
18
+ await sessionRepo.deactivate(req.params.id);
19
+ res.json({ message: "Session deactivated" });
20
+ });
21
+
22
+ // DELETE /sessions - logout all
23
+ router.delete("/", async (req: AuthenticatedRequest, res) => {
24
+ if (!req.user) { res.status(401).json({ error: "Not authenticated" }); return; }
25
+ await sessionRepo.deactivateAllForUser(req.user.userId);
26
+ res.json({ message: "All sessions deactivated" });
27
+ });
28
+
29
+ return router;
30
+ }