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,272 @@
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
9
+ background: #0f172a;
10
+ color: #e2e8f0;
11
+ min-height: 100vh;
12
+ }
13
+
14
+ a {
15
+ color: #60a5fa;
16
+ text-decoration: none;
17
+ }
18
+
19
+ a:hover {
20
+ text-decoration: underline;
21
+ }
22
+
23
+ /* Auth Layout */
24
+ .auth-layout {
25
+ display: flex;
26
+ justify-content: center;
27
+ align-items: center;
28
+ min-height: 100vh;
29
+ padding: 20px;
30
+ }
31
+
32
+ .auth-card {
33
+ background: #1e293b;
34
+ border: 1px solid #334155;
35
+ border-radius: 12px;
36
+ padding: 40px;
37
+ width: 100%;
38
+ max-width: 420px;
39
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
40
+ }
41
+
42
+ .auth-card h1 {
43
+ font-size: 24px;
44
+ margin-bottom: 8px;
45
+ color: #f1f5f9;
46
+ }
47
+
48
+ .auth-card p.subtitle {
49
+ color: #94a3b8;
50
+ margin-bottom: 24px;
51
+ font-size: 14px;
52
+ }
53
+
54
+ /* Form Styles */
55
+ .form-group {
56
+ margin-bottom: 16px;
57
+ }
58
+
59
+ .form-group label {
60
+ display: block;
61
+ margin-bottom: 6px;
62
+ font-size: 14px;
63
+ color: #94a3b8;
64
+ font-weight: 500;
65
+ }
66
+
67
+ .form-group input {
68
+ width: 100%;
69
+ padding: 10px 14px;
70
+ background: #0f172a;
71
+ border: 1px solid #334155;
72
+ border-radius: 8px;
73
+ color: #e2e8f0;
74
+ font-size: 14px;
75
+ outline: none;
76
+ transition: border-color 0.2s;
77
+ }
78
+
79
+ .form-group input:focus {
80
+ border-color: #3b82f6;
81
+ }
82
+
83
+ .form-group input:disabled {
84
+ opacity: 0.5;
85
+ }
86
+
87
+ /* Buttons */
88
+ .btn {
89
+ width: 100%;
90
+ padding: 12px;
91
+ border: none;
92
+ border-radius: 8px;
93
+ font-size: 14px;
94
+ font-weight: 600;
95
+ cursor: pointer;
96
+ transition: all 0.2s;
97
+ margin-top: 8px;
98
+ }
99
+
100
+ .btn-primary {
101
+ background: #3b82f6;
102
+ color: white;
103
+ }
104
+
105
+ .btn-primary:hover:not(:disabled) {
106
+ background: #2563eb;
107
+ }
108
+
109
+ .btn-outline {
110
+ background: transparent;
111
+ color: #60a5fa;
112
+ border: 1px solid #334155;
113
+ }
114
+
115
+ .btn-outline:hover:not(:disabled) {
116
+ background: #1e293b;
117
+ }
118
+
119
+ .btn-danger {
120
+ background: #ef4444;
121
+ color: white;
122
+ }
123
+
124
+ .btn-danger:hover:not(:disabled) {
125
+ background: #dc2626;
126
+ }
127
+
128
+ .btn:disabled {
129
+ opacity: 0.5;
130
+ cursor: not-allowed;
131
+ }
132
+
133
+ /* Messages */
134
+ .error-msg {
135
+ background: #451a1a;
136
+ border: 1px solid #7f1d1d;
137
+ color: #fca5a5;
138
+ padding: 10px 14px;
139
+ border-radius: 8px;
140
+ font-size: 13px;
141
+ margin-bottom: 16px;
142
+ }
143
+
144
+ .success-msg {
145
+ background: #14532d;
146
+ border: 1px solid #166534;
147
+ color: #86efac;
148
+ padding: 10px 14px;
149
+ border-radius: 8px;
150
+ font-size: 13px;
151
+ margin-bottom: 16px;
152
+ }
153
+
154
+ /* Divider */
155
+ .divider {
156
+ display: flex;
157
+ align-items: center;
158
+ margin: 20px 0;
159
+ color: #475569;
160
+ font-size: 13px;
161
+ }
162
+
163
+ .divider::before,
164
+ .divider::after {
165
+ content: '';
166
+ flex: 1;
167
+ border-bottom: 1px solid #334155;
168
+ }
169
+
170
+ .divider span {
171
+ padding: 0 12px;
172
+ }
173
+
174
+ /* Links */
175
+ .auth-links {
176
+ text-align: center;
177
+ margin-top: 20px;
178
+ font-size: 14px;
179
+ color: #94a3b8;
180
+ }
181
+
182
+ /* Dashboard */
183
+ .dashboard {
184
+ max-width: 800px;
185
+ margin: 0 auto;
186
+ padding: 40px 20px;
187
+ }
188
+
189
+ .dashboard-header {
190
+ display: flex;
191
+ justify-content: space-between;
192
+ align-items: center;
193
+ margin-bottom: 32px;
194
+ padding-bottom: 16px;
195
+ border-bottom: 1px solid #334155;
196
+ }
197
+
198
+ .dashboard-header h1 {
199
+ font-size: 24px;
200
+ }
201
+
202
+ .user-info {
203
+ background: #1e293b;
204
+ border: 1px solid #334155;
205
+ border-radius: 12px;
206
+ padding: 24px;
207
+ margin-bottom: 24px;
208
+ }
209
+
210
+ .user-info h2 {
211
+ font-size: 18px;
212
+ margin-bottom: 12px;
213
+ color: #f1f5f9;
214
+ }
215
+
216
+ .user-info p {
217
+ color: #94a3b8;
218
+ font-size: 14px;
219
+ margin-bottom: 4px;
220
+ }
221
+
222
+ /* Sessions */
223
+ .sessions-card {
224
+ background: #1e293b;
225
+ border: 1px solid #334155;
226
+ border-radius: 12px;
227
+ padding: 24px;
228
+ }
229
+
230
+ .sessions-card h2 {
231
+ font-size: 18px;
232
+ margin-bottom: 16px;
233
+ color: #f1f5f9;
234
+ }
235
+
236
+ .session-item {
237
+ display: flex;
238
+ justify-content: space-between;
239
+ align-items: center;
240
+ padding: 12px 0;
241
+ border-bottom: 1px solid #334155;
242
+ }
243
+
244
+ .session-item:last-child {
245
+ border-bottom: none;
246
+ }
247
+
248
+ .session-device {
249
+ font-weight: 500;
250
+ color: #e2e8f0;
251
+ }
252
+
253
+ .session-meta {
254
+ font-size: 12px;
255
+ color: #64748b;
256
+ margin-top: 2px;
257
+ }
258
+
259
+ .btn-sm {
260
+ padding: 6px 12px;
261
+ font-size: 12px;
262
+ width: auto;
263
+ margin-top: 0;
264
+ }
265
+
266
+ /* OTP input */
267
+ .otp-input {
268
+ letter-spacing: 8px;
269
+ text-align: center;
270
+ font-size: 24px !important;
271
+ font-weight: 600;
272
+ }
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import "./index.css";
4
+ import App from "./App";
5
+
6
+ createRoot(document.getElementById("root")!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>
10
+ );
@@ -0,0 +1,141 @@
1
+ import { useEffect, useMemo } from "react";
2
+ import { useAuth } from "@securepool/react-sdk";
3
+ import { useNavigate } from "react-router-dom";
4
+ import AccountSwitcher from "../components/AccountSwitcher";
5
+
6
+ export default function DashboardPage() {
7
+ const {
8
+ user,
9
+ isAuthenticated,
10
+ isInitialized,
11
+ logout,
12
+ sessions,
13
+ fetchSessions,
14
+ revokeSession,
15
+ revokeAllSessions,
16
+ isLoading,
17
+ } = useAuth();
18
+ const navigate = useNavigate();
19
+
20
+ useEffect(() => {
21
+ if (!isInitialized) return;
22
+ if (!isAuthenticated) {
23
+ navigate("/login");
24
+ return;
25
+ }
26
+ fetchSessions();
27
+ }, [isAuthenticated, isInitialized, navigate, fetchSessions]);
28
+
29
+ const currentSessionId = useMemo(() => {
30
+ if (sessions.length === 0) return null;
31
+ const sorted = [...sessions].sort(
32
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
33
+ );
34
+ return sorted[0].id;
35
+ }, [sessions]);
36
+
37
+ const handleRevokeSession = async (sessionId: string) => {
38
+ await revokeSession(sessionId);
39
+ if (sessionId === currentSessionId) {
40
+ logout();
41
+ navigate("/login");
42
+ }
43
+ };
44
+
45
+ const handleRevokeAll = async () => {
46
+ await revokeAllSessions();
47
+ logout();
48
+ navigate("/login");
49
+ };
50
+
51
+ if (!isAuthenticated) return null;
52
+
53
+ return (
54
+ <div className="dashboard">
55
+ <div className="dashboard-header">
56
+ <h1>SecurePool Dashboard</h1>
57
+ <AccountSwitcher />
58
+ </div>
59
+
60
+ <div className="user-info">
61
+ <h2>User Profile</h2>
62
+ <p><strong>ID:</strong> {user?.id}</p>
63
+ <p><strong>Email:</strong> {user?.email || "N/A"}</p>
64
+ <p><strong>Verified:</strong> {user?.isVerified ? "Yes" : "No"}</p>
65
+ </div>
66
+
67
+ <div className="sessions-card">
68
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
69
+ <h2>Active Sessions</h2>
70
+ <button
71
+ className="btn btn-outline btn-sm"
72
+ onClick={() => fetchSessions()}
73
+ disabled={isLoading}
74
+ >
75
+ Refresh
76
+ </button>
77
+ </div>
78
+
79
+ {sessions.length === 0 && (
80
+ <p style={{ color: "#64748b", fontSize: 14 }}>No active sessions found</p>
81
+ )}
82
+
83
+ {sessions.map((session) => {
84
+ const isCurrent = session.id === currentSessionId;
85
+ return (
86
+ <div
87
+ key={session.id}
88
+ className="session-item"
89
+ style={{
90
+ borderLeft: isCurrent ? "3px solid #22c55e" : "3px solid transparent",
91
+ paddingLeft: 12,
92
+ background: isCurrent ? "rgba(34, 197, 94, 0.08)" : "transparent",
93
+ borderRadius: isCurrent ? 8 : 0,
94
+ }}
95
+ >
96
+ <div>
97
+ <div className="session-device" style={{ display: "flex", alignItems: "center", gap: 8 }}>
98
+ {session.device}
99
+ {isCurrent && (
100
+ <span style={{
101
+ fontSize: 11,
102
+ fontWeight: 600,
103
+ color: "#22c55e",
104
+ background: "rgba(34, 197, 94, 0.15)",
105
+ padding: "2px 8px",
106
+ borderRadius: 4,
107
+ }}>
108
+ Current
109
+ </span>
110
+ )}
111
+ </div>
112
+ <div className="session-meta">
113
+ IP: {session.ip} &middot; {new Date(session.createdAt).toLocaleString()}
114
+ </div>
115
+ </div>
116
+ <button
117
+ className={isCurrent ? "btn btn-outline btn-sm" : "btn btn-danger btn-sm"}
118
+ onClick={() => handleRevokeSession(session.id)}
119
+ disabled={isLoading}
120
+ style={isCurrent ? { borderColor: "#ef4444", color: "#ef4444" } : {}}
121
+ >
122
+ {isCurrent ? "Logout this device" : "Revoke"}
123
+ </button>
124
+ </div>
125
+ );
126
+ })}
127
+
128
+ {sessions.length > 1 && (
129
+ <button
130
+ className="btn btn-danger"
131
+ style={{ marginTop: 16 }}
132
+ onClick={handleRevokeAll}
133
+ disabled={isLoading}
134
+ >
135
+ Revoke All Sessions
136
+ </button>
137
+ )}
138
+ </div>
139
+ </div>
140
+ );
141
+ }
@@ -0,0 +1,183 @@
1
+ import { useState, useEffect, type FormEvent } from "react";
2
+ import { useAuth } from "@securepool/react-sdk";
3
+ import { useNavigate, Link } from "react-router-dom";
4
+
5
+ type Step = "email" | "otp" | "newPassword";
6
+
7
+ export default function ForgotPasswordPage() {
8
+ const { forgotPassword, resetPassword, isLoading, error, clearError } = useAuth();
9
+ const navigate = useNavigate();
10
+
11
+ useEffect(() => { clearError(); }, [clearError]);
12
+
13
+ const [step, setStep] = useState<Step>("email");
14
+ const [email, setEmail] = useState("");
15
+ const [code, setCode] = useState("");
16
+ const [newPassword, setNewPassword] = useState("");
17
+ const [confirmPassword, setConfirmPassword] = useState("");
18
+ const [localError, setLocalError] = useState<string | null>(null);
19
+ const [success, setSuccess] = useState(false);
20
+
21
+ const handleSendOtp = async (e: FormEvent) => {
22
+ e.preventDefault();
23
+ try {
24
+ await forgotPassword(email);
25
+ setStep("otp");
26
+ } catch {
27
+ // error in context
28
+ }
29
+ };
30
+
31
+ const handleVerifyAndReset = async (e: FormEvent) => {
32
+ e.preventDefault();
33
+ setLocalError(null);
34
+
35
+ if (step === "otp") {
36
+ setStep("newPassword");
37
+ return;
38
+ }
39
+
40
+ if (newPassword !== confirmPassword) {
41
+ setLocalError("Passwords do not match");
42
+ return;
43
+ }
44
+ if (newPassword.length < 8) {
45
+ setLocalError("Password must be at least 8 characters");
46
+ return;
47
+ }
48
+
49
+ try {
50
+ await resetPassword(email, code, newPassword);
51
+ setSuccess(true);
52
+ setTimeout(() => navigate("/login"), 2000);
53
+ } catch {
54
+ // error in context
55
+ }
56
+ };
57
+
58
+ const displayError = localError || error;
59
+
60
+ // Step 1: Enter email
61
+ if (step === "email") {
62
+ return (
63
+ <div className="auth-layout">
64
+ <div className="auth-card">
65
+ <h1>Forgot Password</h1>
66
+ <p className="subtitle">Enter your email and we'll send you a reset code</p>
67
+
68
+ {displayError && <div className="error-msg">{displayError}</div>}
69
+
70
+ <form onSubmit={handleSendOtp}>
71
+ <div className="form-group">
72
+ <label htmlFor="email">Email</label>
73
+ <input
74
+ id="email"
75
+ type="email"
76
+ value={email}
77
+ onChange={(e) => setEmail(e.target.value)}
78
+ placeholder="you@example.com"
79
+ required
80
+ disabled={isLoading}
81
+ />
82
+ </div>
83
+ <button type="submit" className="btn btn-primary" disabled={isLoading}>
84
+ {isLoading ? "Sending..." : "Send Reset Code"}
85
+ </button>
86
+ </form>
87
+
88
+ <div className="auth-links">
89
+ <Link to="/login">Back to login</Link>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ );
94
+ }
95
+
96
+ // Step 2: Enter OTP
97
+ if (step === "otp") {
98
+ return (
99
+ <div className="auth-layout">
100
+ <div className="auth-card">
101
+ <h1>Enter Reset Code</h1>
102
+ <p className="subtitle">
103
+ Code sent to <strong>{email}</strong>
104
+ </p>
105
+
106
+ {displayError && <div className="error-msg">{displayError}</div>}
107
+
108
+ <form onSubmit={handleVerifyAndReset}>
109
+ <div className="form-group">
110
+ <label htmlFor="otp">6-digit Code</label>
111
+ <input
112
+ id="otp"
113
+ type="text"
114
+ className="otp-input"
115
+ value={code}
116
+ onChange={(e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
117
+ placeholder="000000"
118
+ maxLength={6}
119
+ required
120
+ disabled={isLoading}
121
+ />
122
+ </div>
123
+ <button type="submit" className="btn btn-primary" disabled={isLoading || code.length !== 6}>
124
+ Next
125
+ </button>
126
+ </form>
127
+
128
+ <button
129
+ type="button"
130
+ className="btn btn-outline"
131
+ style={{ marginTop: 12 }}
132
+ onClick={() => setStep("email")}
133
+ >
134
+ Use different email
135
+ </button>
136
+ </div>
137
+ </div>
138
+ );
139
+ }
140
+
141
+ // Step 3: Set new password
142
+ return (
143
+ <div className="auth-layout">
144
+ <div className="auth-card">
145
+ <h1>Set New Password</h1>
146
+ <p className="subtitle">Create a new password for your account</p>
147
+
148
+ {success && <div className="success-msg">Password reset! Redirecting to login...</div>}
149
+ {displayError && <div className="error-msg">{displayError}</div>}
150
+
151
+ <form onSubmit={handleVerifyAndReset}>
152
+ <div className="form-group">
153
+ <label htmlFor="newPass">New Password</label>
154
+ <input
155
+ id="newPass"
156
+ type="password"
157
+ value={newPassword}
158
+ onChange={(e) => setNewPassword(e.target.value)}
159
+ placeholder="Min 8 characters"
160
+ required
161
+ disabled={isLoading || success}
162
+ />
163
+ </div>
164
+ <div className="form-group">
165
+ <label htmlFor="confirmPass">Confirm Password</label>
166
+ <input
167
+ id="confirmPass"
168
+ type="password"
169
+ value={confirmPassword}
170
+ onChange={(e) => setConfirmPassword(e.target.value)}
171
+ placeholder="Re-enter password"
172
+ required
173
+ disabled={isLoading || success}
174
+ />
175
+ </div>
176
+ <button type="submit" className="btn btn-primary" disabled={isLoading || success}>
177
+ {isLoading ? "Resetting..." : "Reset Password"}
178
+ </button>
179
+ </form>
180
+ </div>
181
+ </div>
182
+ );
183
+ }