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,24 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg">
2
+ <symbol id="bluesky-icon" viewBox="0 0 16 17">
3
+ <g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
4
+ <defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
5
+ </symbol>
6
+ <symbol id="discord-icon" viewBox="0 0 20 19">
7
+ <path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
8
+ </symbol>
9
+ <symbol id="documentation-icon" viewBox="0 0 21 20">
10
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
11
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
12
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
13
+ </symbol>
14
+ <symbol id="github-icon" viewBox="0 0 19 19">
15
+ <path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
16
+ </symbol>
17
+ <symbol id="social-icon" viewBox="0 0 20 20">
18
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
19
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
20
+ </symbol>
21
+ <symbol id="x-icon" viewBox="0 0 19 19">
22
+ <path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
23
+ </symbol>
24
+ </svg>
@@ -0,0 +1,33 @@
1
+ import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
2
+ import { SecurePoolProvider } from "@securepool/react-sdk";
3
+ import LoginPage from "./pages/LoginPage";
4
+ import SignupPage from "./pages/SignupPage";
5
+ import VerifyEmailPage from "./pages/VerifyEmailPage";
6
+ import OtpLoginPage from "./pages/OtpLoginPage";
7
+ import ForgotPasswordPage from "./pages/ForgotPasswordPage";
8
+ import DashboardPage from "./pages/DashboardPage";
9
+
10
+ const config = {
11
+ apiBaseUrl: import.meta.env.VITE_API_URL || "http://localhost:5001",
12
+ tenantId: import.meta.env.VITE_TENANT_ID || "default",
13
+ };
14
+
15
+ function App() {
16
+ return (
17
+ <SecurePoolProvider config={config}>
18
+ <BrowserRouter>
19
+ <Routes>
20
+ <Route path="/login" element={<LoginPage />} />
21
+ <Route path="/signup" element={<SignupPage />} />
22
+ <Route path="/verify-email" element={<VerifyEmailPage />} />
23
+ <Route path="/otp-login" element={<OtpLoginPage />} />
24
+ <Route path="/forgot-password" element={<ForgotPasswordPage />} />
25
+ <Route path="/dashboard" element={<DashboardPage />} />
26
+ <Route path="*" element={<Navigate to="/login" replace />} />
27
+ </Routes>
28
+ </BrowserRouter>
29
+ </SecurePoolProvider>
30
+ );
31
+ }
32
+
33
+ export default App;
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="77" height="47" fill="none" aria-labelledby="vite-logo-title" viewBox="0 0 77 47"><title id="vite-logo-title">Vite</title><style>.parenthesis{fill:#000}@media (prefers-color-scheme:dark){.parenthesis{fill:#fff}}</style><path fill="#9135ff" d="M40.151 45.71c-.663.844-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.493c-.92 0-1.457-1.04-.92-1.788l7.479-10.471c1.07-1.498 0-3.578-1.842-3.578H15.443c-.92 0-1.456-1.04-.92-1.788l9.696-13.576c.213-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.472c-1.07 1.497 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.087.89 1.83L40.153 45.712z"/><mask id="a" width="48" height="47" x="14" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M40.047 45.71c-.663.843-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.389c-.92 0-1.457-1.04-.92-1.788l7.479-10.472c1.07-1.497 0-3.578-1.842-3.578H15.34c-.92 0-1.456-1.04-.92-1.788l9.696-13.575c.213-.297.556-.474.92-.474H53.93c.92 0 1.456 1.04.92 1.788L47.37 13.03c-1.07 1.498 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.088.89 1.831L40.049 45.712z"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#eee6ff" rx="5.508" ry="14.704" transform="rotate(269.814 20.96 11.29)scale(-1 1)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#eee6ff" rx="10.399" ry="29.851" transform="rotate(89.814 -16.902 -8.275)scale(1 -1)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#8900ff" rx="5.508" ry="30.487" transform="rotate(89.814 -19.197 -7.127)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.928 4.177)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.738 5.52)scale(1 -1)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#eee6ff" rx="14.072" ry="22.078" transform="rotate(93.35 31.245 55.578)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx="14.592" cy="9.743" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(39.51 14.592 9.743)"/></g><g filter="url(#k)"><ellipse cx="61.728" cy="-5.321" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 61.728 -5.32)"/></g><g filter="url(#l)"><ellipse cx="55.618" cy="7.104" fill="#00c2ff" rx="5.971" ry="9.665" transform="rotate(37.892 55.618 7.104)"/></g><g filter="url(#m)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#n)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#o)"><ellipse cx="49.857" cy="30.678" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 49.857 30.678)"/></g><g filter="url(#p)"><ellipse cx="52.623" cy="33.171" fill="#00c2ff" rx="5.971" ry="15.297" transform="rotate(37.892 52.623 33.17)"/></g></g><path d="M6.919 0c-9.198 13.166-9.252 33.575 0 46.789h6.215c-9.25-13.214-9.196-33.623 0-46.789zm62.424 0h-6.215c9.198 13.166 9.252 33.575 0 46.789h6.215c9.25-13.214 9.196-33.623 0-46.789" class="parenthesis"/><defs><filter id="b" width="60.045" height="41.654" x="-5.564" y="16.92" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-40.407" y="-6.762" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-35.435" y="2.801" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-30.84" y="20.8" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-29.307" y="21.949" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="29.961" y="-17.13" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-13.43" y="-22.082" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="34.321" y="-37.644" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="38.847" y="-10.552" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="22.45" y="-1.645" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="32.919" y="11.36" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter></defs></svg>
@@ -0,0 +1,373 @@
1
+ import { useState, useRef, useEffect } from "react";
2
+ import { useAuth } from "@securepool/react-sdk";
3
+ import { useNavigate } from "react-router-dom";
4
+ import ChangePasswordModal from "./ChangePasswordModal";
5
+
6
+ function getGravatarUrl(email: string, size = 80): string {
7
+ // Simple hash for gravatar - use md5 in production
8
+ // For now, use UI Avatars as a fallback that always works
9
+ const initials = email
10
+ .split("@")[0]
11
+ .slice(0, 2)
12
+ .toUpperCase();
13
+ return `https://ui-avatars.com/api/?name=${encodeURIComponent(initials)}&size=${size}&background=3b82f6&color=fff&bold=true&format=svg`;
14
+ }
15
+
16
+ export default function AccountSwitcher() {
17
+ const { user, accounts, switchAccount, logoutAccount, logout } = useAuth();
18
+ const navigate = useNavigate();
19
+ const [open, setOpen] = useState(false);
20
+ const [showChangePassword, setShowChangePassword] = useState(false);
21
+ const ref = useRef<HTMLDivElement>(null);
22
+
23
+ // Close dropdown on outside click
24
+ useEffect(() => {
25
+ const handler = (e: MouseEvent) => {
26
+ if (ref.current && !ref.current.contains(e.target as Node)) {
27
+ setOpen(false);
28
+ }
29
+ };
30
+ document.addEventListener("mousedown", handler);
31
+ return () => document.removeEventListener("mousedown", handler);
32
+ }, []);
33
+
34
+ const otherAccounts = accounts.filter((a) => a.id !== user?.id);
35
+
36
+ const handleSwitch = (accountId: string) => {
37
+ switchAccount(accountId);
38
+ setOpen(false);
39
+ navigate("/dashboard");
40
+ window.location.reload(); // Reload to re-fetch sessions for new account
41
+ };
42
+
43
+ const handleLogoutOther = (accountId: string) => {
44
+ logoutAccount(accountId);
45
+ };
46
+
47
+ const handleAddAccount = () => {
48
+ setOpen(false);
49
+ // Don't logout current - just go to login to add another
50
+ navigate("/login");
51
+ };
52
+
53
+ const handleLogoutCurrent = () => {
54
+ logout();
55
+ setOpen(false);
56
+ navigate("/login");
57
+ };
58
+
59
+ return (
60
+ <div ref={ref} style={{ position: "relative" }}>
61
+ {/* Avatar Button */}
62
+ <button
63
+ onClick={() => setOpen(!open)}
64
+ style={{
65
+ width: 40,
66
+ height: 40,
67
+ borderRadius: "50%",
68
+ border: "2px solid #334155",
69
+ padding: 0,
70
+ cursor: "pointer",
71
+ overflow: "hidden",
72
+ background: "#3b82f6",
73
+ display: "flex",
74
+ alignItems: "center",
75
+ justifyContent: "center",
76
+ transition: "border-color 0.2s",
77
+ }}
78
+ onMouseEnter={(e) => (e.currentTarget.style.borderColor = "#3b82f6")}
79
+ onMouseLeave={(e) => (e.currentTarget.style.borderColor = "#334155")}
80
+ >
81
+ <img
82
+ src={getGravatarUrl(user?.email || "", 80)}
83
+ alt="avatar"
84
+ style={{ width: 36, height: 36, borderRadius: "50%" }}
85
+ />
86
+ </button>
87
+
88
+ {/* Dropdown */}
89
+ {open && (
90
+ <div
91
+ style={{
92
+ position: "absolute",
93
+ top: 50,
94
+ right: 0,
95
+ width: 320,
96
+ background: "#1e293b",
97
+ border: "1px solid #334155",
98
+ borderRadius: 12,
99
+ boxShadow: "0 8px 32px rgba(0,0,0,0.4)",
100
+ zIndex: 100,
101
+ overflow: "hidden",
102
+ }}
103
+ >
104
+ {/* Current User */}
105
+ <div
106
+ style={{
107
+ padding: "16px 16px 12px",
108
+ borderBottom: "1px solid #334155",
109
+ }}
110
+ >
111
+ <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
112
+ <img
113
+ src={getGravatarUrl(user?.email || "", 80)}
114
+ alt="avatar"
115
+ style={{
116
+ width: 44,
117
+ height: 44,
118
+ borderRadius: "50%",
119
+ border: "2px solid #22c55e",
120
+ }}
121
+ />
122
+ <div style={{ flex: 1, minWidth: 0 }}>
123
+ <div
124
+ style={{
125
+ fontSize: 14,
126
+ fontWeight: 600,
127
+ color: "#f1f5f9",
128
+ overflow: "hidden",
129
+ textOverflow: "ellipsis",
130
+ whiteSpace: "nowrap",
131
+ }}
132
+ >
133
+ {user?.email}
134
+ </div>
135
+ <div
136
+ style={{
137
+ fontSize: 11,
138
+ color: "#22c55e",
139
+ fontWeight: 500,
140
+ marginTop: 2,
141
+ }}
142
+ >
143
+ Active
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </div>
148
+
149
+ {/* Other Accounts */}
150
+ {otherAccounts.length > 0 && (
151
+ <div style={{ borderBottom: "1px solid #334155" }}>
152
+ <div
153
+ style={{
154
+ padding: "8px 16px 4px",
155
+ fontSize: 11,
156
+ color: "#64748b",
157
+ textTransform: "uppercase",
158
+ letterSpacing: 0.5,
159
+ }}
160
+ >
161
+ Other accounts
162
+ </div>
163
+ {otherAccounts.map((account) => (
164
+ <div
165
+ key={account.id}
166
+ style={{
167
+ display: "flex",
168
+ alignItems: "center",
169
+ gap: 12,
170
+ padding: "10px 16px",
171
+ cursor: "pointer",
172
+ transition: "background 0.15s",
173
+ }}
174
+ onMouseEnter={(e) =>
175
+ (e.currentTarget.style.background = "rgba(59,130,246,0.1)")
176
+ }
177
+ onMouseLeave={(e) =>
178
+ (e.currentTarget.style.background = "transparent")
179
+ }
180
+ onClick={() => handleSwitch(account.id)}
181
+ >
182
+ <img
183
+ src={getGravatarUrl(account.email, 80)}
184
+ alt="avatar"
185
+ style={{
186
+ width: 36,
187
+ height: 36,
188
+ borderRadius: "50%",
189
+ border: "1px solid #334155",
190
+ }}
191
+ />
192
+ <div style={{ flex: 1, minWidth: 0 }}>
193
+ <div
194
+ style={{
195
+ fontSize: 13,
196
+ color: "#e2e8f0",
197
+ overflow: "hidden",
198
+ textOverflow: "ellipsis",
199
+ whiteSpace: "nowrap",
200
+ }}
201
+ >
202
+ {account.email}
203
+ </div>
204
+ <div style={{ fontSize: 11, color: "#64748b" }}>
205
+ Click to switch
206
+ </div>
207
+ </div>
208
+ <button
209
+ onClick={(e) => {
210
+ e.stopPropagation();
211
+ handleLogoutOther(account.id);
212
+ }}
213
+ style={{
214
+ background: "transparent",
215
+ border: "1px solid #475569",
216
+ color: "#94a3b8",
217
+ borderRadius: 6,
218
+ padding: "4px 8px",
219
+ fontSize: 11,
220
+ cursor: "pointer",
221
+ transition: "all 0.15s",
222
+ }}
223
+ onMouseEnter={(e) => {
224
+ e.currentTarget.style.borderColor = "#ef4444";
225
+ e.currentTarget.style.color = "#ef4444";
226
+ }}
227
+ onMouseLeave={(e) => {
228
+ e.currentTarget.style.borderColor = "#475569";
229
+ e.currentTarget.style.color = "#94a3b8";
230
+ }}
231
+ >
232
+ Logout
233
+ </button>
234
+ </div>
235
+ ))}
236
+ </div>
237
+ )}
238
+
239
+ {/* Actions */}
240
+ <div style={{ padding: 8 }}>
241
+ <button
242
+ onClick={() => { setShowChangePassword(true); setOpen(false); }}
243
+ style={{
244
+ width: "100%",
245
+ padding: "10px 12px",
246
+ background: "transparent",
247
+ border: "none",
248
+ color: "#e2e8f0",
249
+ fontSize: 13,
250
+ fontWeight: 500,
251
+ cursor: "pointer",
252
+ borderRadius: 8,
253
+ textAlign: "left",
254
+ display: "flex",
255
+ alignItems: "center",
256
+ gap: 10,
257
+ transition: "background 0.15s",
258
+ }}
259
+ onMouseEnter={(e) =>
260
+ (e.currentTarget.style.background = "rgba(255,255,255,0.05)")
261
+ }
262
+ onMouseLeave={(e) =>
263
+ (e.currentTarget.style.background = "transparent")
264
+ }
265
+ >
266
+ <span
267
+ style={{
268
+ width: 28,
269
+ height: 28,
270
+ borderRadius: "50%",
271
+ background: "rgba(148,163,184,0.15)",
272
+ display: "flex",
273
+ alignItems: "center",
274
+ justifyContent: "center",
275
+ fontSize: 14,
276
+ }}
277
+ >
278
+ 🔒
279
+ </span>
280
+ Change password
281
+ </button>
282
+ <button
283
+ onClick={handleAddAccount}
284
+ style={{
285
+ width: "100%",
286
+ padding: "10px 12px",
287
+ background: "transparent",
288
+ border: "none",
289
+ color: "#60a5fa",
290
+ fontSize: 13,
291
+ fontWeight: 500,
292
+ cursor: "pointer",
293
+ borderRadius: 8,
294
+ textAlign: "left",
295
+ display: "flex",
296
+ alignItems: "center",
297
+ gap: 10,
298
+ transition: "background 0.15s",
299
+ }}
300
+ onMouseEnter={(e) =>
301
+ (e.currentTarget.style.background = "rgba(59,130,246,0.1)")
302
+ }
303
+ onMouseLeave={(e) =>
304
+ (e.currentTarget.style.background = "transparent")
305
+ }
306
+ >
307
+ <span
308
+ style={{
309
+ width: 28,
310
+ height: 28,
311
+ borderRadius: "50%",
312
+ border: "2px dashed #475569",
313
+ display: "flex",
314
+ alignItems: "center",
315
+ justifyContent: "center",
316
+ fontSize: 16,
317
+ color: "#64748b",
318
+ }}
319
+ >
320
+ +
321
+ </span>
322
+ Add another account
323
+ </button>
324
+ <button
325
+ onClick={handleLogoutCurrent}
326
+ style={{
327
+ width: "100%",
328
+ padding: "10px 12px",
329
+ background: "transparent",
330
+ border: "none",
331
+ color: "#ef4444",
332
+ fontSize: 13,
333
+ fontWeight: 500,
334
+ cursor: "pointer",
335
+ borderRadius: 8,
336
+ textAlign: "left",
337
+ display: "flex",
338
+ alignItems: "center",
339
+ gap: 10,
340
+ transition: "background 0.15s",
341
+ }}
342
+ onMouseEnter={(e) =>
343
+ (e.currentTarget.style.background = "rgba(239,68,68,0.1)")
344
+ }
345
+ onMouseLeave={(e) =>
346
+ (e.currentTarget.style.background = "transparent")
347
+ }
348
+ >
349
+ <span
350
+ style={{
351
+ width: 28,
352
+ height: 28,
353
+ borderRadius: "50%",
354
+ background: "rgba(239,68,68,0.15)",
355
+ display: "flex",
356
+ alignItems: "center",
357
+ justifyContent: "center",
358
+ fontSize: 14,
359
+ }}
360
+ >
361
+
362
+ </span>
363
+ Sign out
364
+ </button>
365
+ </div>
366
+ </div>
367
+ )}
368
+ {showChangePassword && (
369
+ <ChangePasswordModal onClose={() => setShowChangePassword(false)} />
370
+ )}
371
+ </div>
372
+ );
373
+ }
@@ -0,0 +1,128 @@
1
+ import { useState, type FormEvent } from "react";
2
+ import { useAuth } from "@securepool/react-sdk";
3
+
4
+ interface ChangePasswordModalProps {
5
+ onClose: () => void;
6
+ }
7
+
8
+ export default function ChangePasswordModal({ onClose }: ChangePasswordModalProps) {
9
+ const { changePassword, isLoading, error } = useAuth();
10
+ const [oldPassword, setOldPassword] = useState("");
11
+ const [newPassword, setNewPassword] = useState("");
12
+ const [confirmPassword, setConfirmPassword] = useState("");
13
+ const [localError, setLocalError] = useState<string | null>(null);
14
+ const [success, setSuccess] = useState(false);
15
+
16
+ const handleSubmit = async (e: FormEvent) => {
17
+ e.preventDefault();
18
+ setLocalError(null);
19
+
20
+ if (newPassword !== confirmPassword) {
21
+ setLocalError("Passwords do not match");
22
+ return;
23
+ }
24
+ if (newPassword.length < 8) {
25
+ setLocalError("Password must be at least 8 characters");
26
+ return;
27
+ }
28
+ if (oldPassword === newPassword) {
29
+ setLocalError("New password must be different from old password");
30
+ return;
31
+ }
32
+
33
+ try {
34
+ await changePassword(oldPassword, newPassword);
35
+ setSuccess(true);
36
+ setTimeout(onClose, 1500);
37
+ } catch {
38
+ // error in context
39
+ }
40
+ };
41
+
42
+ const displayError = localError || error;
43
+
44
+ return (
45
+ <div
46
+ style={{
47
+ position: "fixed",
48
+ inset: 0,
49
+ background: "rgba(0,0,0,0.6)",
50
+ display: "flex",
51
+ justifyContent: "center",
52
+ alignItems: "center",
53
+ zIndex: 200,
54
+ padding: 20,
55
+ }}
56
+ onClick={(e) => {
57
+ if (e.target === e.currentTarget) onClose();
58
+ }}
59
+ >
60
+ <div
61
+ style={{
62
+ background: "#1e293b",
63
+ border: "1px solid #334155",
64
+ borderRadius: 12,
65
+ padding: 32,
66
+ width: "100%",
67
+ maxWidth: 400,
68
+ boxShadow: "0 8px 32px rgba(0,0,0,0.4)",
69
+ }}
70
+ >
71
+ <h2 style={{ fontSize: 20, color: "#f1f5f9", marginBottom: 4 }}>Change Password</h2>
72
+ <p style={{ fontSize: 13, color: "#94a3b8", marginBottom: 20 }}>
73
+ Enter your current password and choose a new one
74
+ </p>
75
+
76
+ {success && <div className="success-msg">Password changed!</div>}
77
+ {displayError && <div className="error-msg">{displayError}</div>}
78
+
79
+ <form onSubmit={handleSubmit}>
80
+ <div className="form-group">
81
+ <label htmlFor="cp-old">Current Password</label>
82
+ <input
83
+ id="cp-old"
84
+ type="password"
85
+ value={oldPassword}
86
+ onChange={(e) => setOldPassword(e.target.value)}
87
+ placeholder="Enter current password"
88
+ required
89
+ disabled={isLoading || success}
90
+ />
91
+ </div>
92
+ <div className="form-group">
93
+ <label htmlFor="cp-new">New Password</label>
94
+ <input
95
+ id="cp-new"
96
+ type="password"
97
+ value={newPassword}
98
+ onChange={(e) => setNewPassword(e.target.value)}
99
+ placeholder="Min 8 characters"
100
+ required
101
+ disabled={isLoading || success}
102
+ />
103
+ </div>
104
+ <div className="form-group">
105
+ <label htmlFor="cp-confirm">Confirm New Password</label>
106
+ <input
107
+ id="cp-confirm"
108
+ type="password"
109
+ value={confirmPassword}
110
+ onChange={(e) => setConfirmPassword(e.target.value)}
111
+ placeholder="Re-enter new password"
112
+ required
113
+ disabled={isLoading || success}
114
+ />
115
+ </div>
116
+ <div style={{ display: "flex", gap: 8, marginTop: 8 }}>
117
+ <button type="button" className="btn btn-outline" onClick={onClose} disabled={isLoading}>
118
+ Cancel
119
+ </button>
120
+ <button type="submit" className="btn btn-primary" disabled={isLoading || success}>
121
+ {isLoading ? "Changing..." : "Change Password"}
122
+ </button>
123
+ </div>
124
+ </form>
125
+ </div>
126
+ </div>
127
+ );
128
+ }