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.
- package/.dockerignore +7 -0
- package/.env.example +20 -0
- package/ARCHITECTURE.md +279 -0
- package/DEPLOYMENT.md +441 -0
- package/README.md +283 -0
- package/SETUP.md +388 -0
- package/apps/demo-backend/Dockerfile +33 -0
- package/apps/demo-backend/package.json +19 -0
- package/apps/demo-backend/src/index.ts +71 -0
- package/apps/demo-backend/tsconfig.json +8 -0
- package/apps/demo-frontend/.env.example +2 -0
- package/apps/demo-frontend/README.md +73 -0
- package/apps/demo-frontend/eslint.config.js +23 -0
- package/apps/demo-frontend/index.html +13 -0
- package/apps/demo-frontend/package.json +24 -0
- package/apps/demo-frontend/public/favicon.svg +1 -0
- package/apps/demo-frontend/public/icons.svg +24 -0
- package/apps/demo-frontend/src/App.tsx +33 -0
- package/apps/demo-frontend/src/assets/hero.png +0 -0
- package/apps/demo-frontend/src/assets/vite.svg +1 -0
- package/apps/demo-frontend/src/components/AccountSwitcher.tsx +373 -0
- package/apps/demo-frontend/src/components/ChangePasswordModal.tsx +128 -0
- package/apps/demo-frontend/src/index.css +272 -0
- package/apps/demo-frontend/src/main.tsx +10 -0
- package/apps/demo-frontend/src/pages/DashboardPage.tsx +141 -0
- package/apps/demo-frontend/src/pages/ForgotPasswordPage.tsx +183 -0
- package/apps/demo-frontend/src/pages/LoginPage.tsx +158 -0
- package/apps/demo-frontend/src/pages/OtpLoginPage.tsx +114 -0
- package/apps/demo-frontend/src/pages/SignupPage.tsx +95 -0
- package/apps/demo-frontend/src/pages/VerifyEmailPage.tsx +84 -0
- package/apps/demo-frontend/tsconfig.app.json +28 -0
- package/apps/demo-frontend/tsconfig.json +7 -0
- package/apps/demo-frontend/tsconfig.node.json +26 -0
- package/apps/demo-frontend/vite.config.ts +15 -0
- package/docs/DATABASE_MONGODB.md +280 -0
- package/docs/DATABASE_SQL.md +472 -0
- package/package.json +21 -0
- package/packages/api/package.json +30 -0
- package/packages/api/src/createSecurePool.ts +113 -0
- package/packages/api/src/index.ts +8 -0
- package/packages/api/src/middleware/authMiddleware.ts +26 -0
- package/packages/api/src/middleware/authorize.ts +24 -0
- package/packages/api/src/middleware/rateLimiter.ts +25 -0
- package/packages/api/src/middleware/tenantMiddleware.ts +12 -0
- package/packages/api/src/routes/authRoutes.ts +229 -0
- package/packages/api/src/routes/sessionRoutes.ts +30 -0
- package/packages/api/src/swagger.ts +529 -0
- package/packages/api/tsconfig.json +8 -0
- package/packages/application/package.json +16 -0
- package/packages/application/src/index.ts +17 -0
- package/packages/application/src/interfaces/IAuditLogRepository.ts +6 -0
- package/packages/application/src/interfaces/IAuthPlugin.ts +4 -0
- package/packages/application/src/interfaces/IEmailService.ts +3 -0
- package/packages/application/src/interfaces/IGoogleAuthService.ts +3 -0
- package/packages/application/src/interfaces/IOtpRepository.ts +8 -0
- package/packages/application/src/interfaces/IOtpService.ts +4 -0
- package/packages/application/src/interfaces/IPasswordHasher.ts +4 -0
- package/packages/application/src/interfaces/IRoleRepository.ts +8 -0
- package/packages/application/src/interfaces/ISessionRepository.ts +8 -0
- package/packages/application/src/interfaces/ITokenRepository.ts +9 -0
- package/packages/application/src/interfaces/ITokenService.ts +5 -0
- package/packages/application/src/interfaces/IUserRepository.ts +8 -0
- package/packages/application/src/services/AuthService.ts +323 -0
- package/packages/application/src/services/RefreshTokenService.ts +53 -0
- package/packages/application/tsconfig.json +8 -0
- package/packages/core/package.json +13 -0
- package/packages/core/src/entities/AuditLog.ts +11 -0
- package/packages/core/src/entities/OtpCode.ts +10 -0
- package/packages/core/src/entities/RefreshToken.ts +9 -0
- package/packages/core/src/entities/Role.ts +6 -0
- package/packages/core/src/entities/Session.ts +10 -0
- package/packages/core/src/entities/Tenant.ts +7 -0
- package/packages/core/src/entities/User.ts +10 -0
- package/packages/core/src/entities/UserRole.ts +6 -0
- package/packages/core/src/enums/index.ts +22 -0
- package/packages/core/src/index.ts +10 -0
- package/packages/core/tsconfig.json +8 -0
- package/packages/infrastructure/package.json +24 -0
- package/packages/infrastructure/src/email/NodemailerEmailService.ts +55 -0
- package/packages/infrastructure/src/google/GoogleAuthServiceImpl.ts +28 -0
- package/packages/infrastructure/src/hashing/BcryptHasher.ts +18 -0
- package/packages/infrastructure/src/index.ts +6 -0
- package/packages/infrastructure/src/jwt/JwtTokenService.ts +32 -0
- package/packages/infrastructure/src/otp/OtpServiceImpl.ts +50 -0
- package/packages/infrastructure/tsconfig.json +8 -0
- package/packages/persistence/package.json +22 -0
- package/packages/persistence/prisma/schema.prisma +88 -0
- package/packages/persistence/src/factory.ts +48 -0
- package/packages/persistence/src/index.ts +30 -0
- package/packages/persistence/src/mongo/connection.ts +9 -0
- package/packages/persistence/src/mongo/models/AuditLogModel.ts +21 -0
- package/packages/persistence/src/mongo/models/OtpModel.ts +19 -0
- package/packages/persistence/src/mongo/models/RefreshTokenModel.ts +17 -0
- package/packages/persistence/src/mongo/models/RoleModel.ts +11 -0
- package/packages/persistence/src/mongo/models/SessionModel.ts +19 -0
- package/packages/persistence/src/mongo/models/UserModel.ts +21 -0
- package/packages/persistence/src/mongo/models/UserRoleModel.ts +15 -0
- package/packages/persistence/src/mongo/repositories/MongoAuditLogRepository.ts +29 -0
- package/packages/persistence/src/mongo/repositories/MongoOtpRepository.ts +34 -0
- package/packages/persistence/src/mongo/repositories/MongoRoleRepository.ts +32 -0
- package/packages/persistence/src/mongo/repositories/MongoSessionRepository.ts +29 -0
- package/packages/persistence/src/mongo/repositories/MongoTokenRepository.ts +34 -0
- package/packages/persistence/src/mongo/repositories/MongoUserRepository.ts +37 -0
- package/packages/persistence/src/prisma/repositories/PrismaAuditLogRepository.ts +37 -0
- package/packages/persistence/src/prisma/repositories/PrismaOtpRepository.ts +43 -0
- package/packages/persistence/src/prisma/repositories/PrismaRoleRepository.ts +36 -0
- package/packages/persistence/src/prisma/repositories/PrismaSessionRepository.ts +39 -0
- package/packages/persistence/src/prisma/repositories/PrismaTokenRepository.ts +50 -0
- package/packages/persistence/src/prisma/repositories/PrismaUserRepository.ts +45 -0
- package/packages/persistence/tsconfig.json +8 -0
- package/packages/react-sdk/package.json +23 -0
- package/packages/react-sdk/src/components/GoogleLoginButton.tsx +54 -0
- package/packages/react-sdk/src/components/LoginForm.tsx +67 -0
- package/packages/react-sdk/src/components/OTPVerification.tsx +104 -0
- package/packages/react-sdk/src/components/SessionList.tsx +64 -0
- package/packages/react-sdk/src/components/SignupForm.tsx +95 -0
- package/packages/react-sdk/src/context/AuthContext.ts +4 -0
- package/packages/react-sdk/src/context/SecurePoolProvider.tsx +492 -0
- package/packages/react-sdk/src/hooks/useAuth.ts +11 -0
- package/packages/react-sdk/src/index.ts +22 -0
- package/packages/react-sdk/src/types.ts +53 -0
- package/packages/react-sdk/tsconfig.json +12 -0
- package/scripts/setup.js +285 -0
- package/scripts/setup.sh +309 -0
- package/tsconfig.base.json +16 -0
- package/turbo.json +16 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import React, { useState, useCallback, useEffect } from "react";
|
|
2
|
+
import { AuthContext } from "./AuthContext";
|
|
3
|
+
import { SecurePoolConfig, AuthUser, AuthSession, StoredAccount } from "../types";
|
|
4
|
+
|
|
5
|
+
interface SecurePoolProviderProps {
|
|
6
|
+
config: SecurePoolConfig;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// ---- LocalStorage helpers ----
|
|
11
|
+
|
|
12
|
+
function getAccounts(): StoredAccount[] {
|
|
13
|
+
if (typeof window === "undefined") return [];
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(localStorage.getItem("sp_accounts") || "[]");
|
|
16
|
+
} catch {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function saveAccounts(accounts: StoredAccount[]): void {
|
|
22
|
+
localStorage.setItem("sp_accounts", JSON.stringify(accounts));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getActiveAccountId(): string | null {
|
|
26
|
+
return typeof window !== "undefined"
|
|
27
|
+
? localStorage.getItem("sp_active_account")
|
|
28
|
+
: null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function setActiveAccountId(id: string): void {
|
|
32
|
+
localStorage.setItem("sp_active_account", id);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function addOrUpdateAccount(account: StoredAccount): void {
|
|
36
|
+
const accounts = getAccounts();
|
|
37
|
+
const idx = accounts.findIndex((a) => a.id === account.id);
|
|
38
|
+
if (idx >= 0) {
|
|
39
|
+
accounts[idx] = account;
|
|
40
|
+
} else {
|
|
41
|
+
accounts.push(account);
|
|
42
|
+
}
|
|
43
|
+
saveAccounts(accounts);
|
|
44
|
+
setActiveAccountId(account.id);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function removeAccount(accountId: string): void {
|
|
48
|
+
const accounts = getAccounts().filter((a) => a.id !== accountId);
|
|
49
|
+
saveAccounts(accounts);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getActiveAccount(): StoredAccount | null {
|
|
53
|
+
const activeId = getActiveAccountId();
|
|
54
|
+
if (!activeId) return null;
|
|
55
|
+
return getAccounts().find((a) => a.id === activeId) || null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getAccessToken(): string | null {
|
|
59
|
+
return getActiveAccount()?.accessToken || null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getRefreshTokenValue(): string | null {
|
|
63
|
+
return getActiveAccount()?.refreshToken || null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function setTokensForAccount(
|
|
67
|
+
userId: string,
|
|
68
|
+
email: string,
|
|
69
|
+
accessToken: string,
|
|
70
|
+
refreshToken: string
|
|
71
|
+
): void {
|
|
72
|
+
addOrUpdateAccount({ id: userId, email, accessToken, refreshToken });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function clearActiveAccount(): void {
|
|
76
|
+
const activeId = getActiveAccountId();
|
|
77
|
+
if (activeId) removeAccount(activeId);
|
|
78
|
+
localStorage.removeItem("sp_active_account");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---- Provider ----
|
|
82
|
+
|
|
83
|
+
export const SecurePoolProvider: React.FC<SecurePoolProviderProps> = ({
|
|
84
|
+
config,
|
|
85
|
+
children,
|
|
86
|
+
}) => {
|
|
87
|
+
// Restore active user synchronously
|
|
88
|
+
const [user, setUser] = useState<AuthUser | null>(() => {
|
|
89
|
+
const account = getActiveAccount();
|
|
90
|
+
if (!account) return null;
|
|
91
|
+
try {
|
|
92
|
+
const payload = JSON.parse(atob(account.accessToken.split(".")[1]));
|
|
93
|
+
if (payload.exp * 1000 > Date.now()) {
|
|
94
|
+
return { id: account.id, email: account.email, isVerified: true };
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// invalid
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
});
|
|
101
|
+
const [accounts, setAccountsState] = useState<StoredAccount[]>(getAccounts);
|
|
102
|
+
const [isInitialized] = useState(true);
|
|
103
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
104
|
+
const [error, setError] = useState<string | null>(null);
|
|
105
|
+
const [sessions, setSessions] = useState<AuthSession[]>([]);
|
|
106
|
+
|
|
107
|
+
const clearError = useCallback(() => setError(null), []);
|
|
108
|
+
|
|
109
|
+
const refreshAccountsList = useCallback(() => {
|
|
110
|
+
setAccountsState(getAccounts());
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
const buildHeaders = useCallback((): Record<string, string> => {
|
|
114
|
+
const headers: Record<string, string> = {
|
|
115
|
+
"Content-Type": "application/json",
|
|
116
|
+
"x-tenant-id": config.tenantId,
|
|
117
|
+
};
|
|
118
|
+
const token = getAccessToken();
|
|
119
|
+
if (token) {
|
|
120
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
121
|
+
}
|
|
122
|
+
return headers;
|
|
123
|
+
}, [config.tenantId]);
|
|
124
|
+
|
|
125
|
+
const apiCall = useCallback(
|
|
126
|
+
async (path: string, options: RequestInit = {}) => {
|
|
127
|
+
const res = await fetch(`${config.apiBaseUrl}${path}`, {
|
|
128
|
+
...options,
|
|
129
|
+
headers: {
|
|
130
|
+
...buildHeaders(),
|
|
131
|
+
...((options.headers as Record<string, string>) || {}),
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
const contentType = res.headers.get("content-type") || "";
|
|
135
|
+
if (!contentType.includes("application/json")) {
|
|
136
|
+
throw new Error(res.ok ? "Unexpected response" : `Request failed (${res.status})`);
|
|
137
|
+
}
|
|
138
|
+
const data = await res.json();
|
|
139
|
+
if (!res.ok) throw new Error(data.error || "Request failed");
|
|
140
|
+
return data;
|
|
141
|
+
},
|
|
142
|
+
[config.apiBaseUrl, buildHeaders]
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// ---- Helper to handle login response ----
|
|
146
|
+
|
|
147
|
+
const handleLoginResponse = useCallback(
|
|
148
|
+
(data: { accessToken: string; refreshToken: string }, email: string) => {
|
|
149
|
+
const payload = JSON.parse(atob(data.accessToken.split(".")[1]));
|
|
150
|
+
setTokensForAccount(payload.sub, email, data.accessToken, data.refreshToken);
|
|
151
|
+
setUser({ id: payload.sub, email, isVerified: true });
|
|
152
|
+
refreshAccountsList();
|
|
153
|
+
},
|
|
154
|
+
[refreshAccountsList]
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// ---- Auth Actions ----
|
|
158
|
+
|
|
159
|
+
const register = useCallback(
|
|
160
|
+
async (email: string, password: string) => {
|
|
161
|
+
setIsLoading(true);
|
|
162
|
+
setError(null);
|
|
163
|
+
try {
|
|
164
|
+
const data = await apiCall("/auth/register", {
|
|
165
|
+
method: "POST",
|
|
166
|
+
body: JSON.stringify({ email, password }),
|
|
167
|
+
});
|
|
168
|
+
return data;
|
|
169
|
+
} catch (err: any) {
|
|
170
|
+
setError(err.message);
|
|
171
|
+
throw err;
|
|
172
|
+
} finally {
|
|
173
|
+
setIsLoading(false);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
[apiCall]
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const login = useCallback(
|
|
180
|
+
async (email: string, password: string) => {
|
|
181
|
+
setIsLoading(true);
|
|
182
|
+
setError(null);
|
|
183
|
+
try {
|
|
184
|
+
const data = await apiCall("/auth/login", {
|
|
185
|
+
method: "POST",
|
|
186
|
+
body: JSON.stringify({ email, password }),
|
|
187
|
+
});
|
|
188
|
+
handleLoginResponse(data, email);
|
|
189
|
+
} catch (err: any) {
|
|
190
|
+
setError(err.message);
|
|
191
|
+
throw err;
|
|
192
|
+
} finally {
|
|
193
|
+
setIsLoading(false);
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
[apiCall, handleLoginResponse]
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const loginWithGoogle = useCallback(
|
|
200
|
+
async (googleToken: string) => {
|
|
201
|
+
setIsLoading(true);
|
|
202
|
+
setError(null);
|
|
203
|
+
try {
|
|
204
|
+
const data = await apiCall("/auth/google", {
|
|
205
|
+
method: "POST",
|
|
206
|
+
body: JSON.stringify({ token: googleToken }),
|
|
207
|
+
});
|
|
208
|
+
handleLoginResponse(data, data.email || "");
|
|
209
|
+
} catch (err: any) {
|
|
210
|
+
setError(err.message);
|
|
211
|
+
throw err;
|
|
212
|
+
} finally {
|
|
213
|
+
setIsLoading(false);
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
[apiCall, handleLoginResponse]
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const requestOtp = useCallback(
|
|
220
|
+
async (email: string) => {
|
|
221
|
+
setIsLoading(true);
|
|
222
|
+
setError(null);
|
|
223
|
+
try {
|
|
224
|
+
const data = await apiCall("/auth/otp/request", {
|
|
225
|
+
method: "POST",
|
|
226
|
+
body: JSON.stringify({ email }),
|
|
227
|
+
});
|
|
228
|
+
return data;
|
|
229
|
+
} catch (err: any) {
|
|
230
|
+
setError(err.message);
|
|
231
|
+
throw err;
|
|
232
|
+
} finally {
|
|
233
|
+
setIsLoading(false);
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
[apiCall]
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const verifyOtp = useCallback(
|
|
240
|
+
async (email: string, code: string) => {
|
|
241
|
+
setIsLoading(true);
|
|
242
|
+
setError(null);
|
|
243
|
+
try {
|
|
244
|
+
const data = await apiCall("/auth/otp/verify", {
|
|
245
|
+
method: "POST",
|
|
246
|
+
body: JSON.stringify({ email, code }),
|
|
247
|
+
});
|
|
248
|
+
handleLoginResponse(data, email);
|
|
249
|
+
} catch (err: any) {
|
|
250
|
+
setError(err.message);
|
|
251
|
+
throw err;
|
|
252
|
+
} finally {
|
|
253
|
+
setIsLoading(false);
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
[apiCall, handleLoginResponse]
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// Verify email after registration
|
|
260
|
+
const verifyEmail = useCallback(
|
|
261
|
+
async (email: string, code: string) => {
|
|
262
|
+
setIsLoading(true);
|
|
263
|
+
setError(null);
|
|
264
|
+
try {
|
|
265
|
+
const data = await apiCall("/auth/verify-email", {
|
|
266
|
+
method: "POST",
|
|
267
|
+
body: JSON.stringify({ email, code }),
|
|
268
|
+
});
|
|
269
|
+
handleLoginResponse(data, email);
|
|
270
|
+
} catch (err: any) {
|
|
271
|
+
setError(err.message);
|
|
272
|
+
throw err;
|
|
273
|
+
} finally {
|
|
274
|
+
setIsLoading(false);
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
[apiCall, handleLoginResponse]
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
// Forgot password - sends OTP
|
|
281
|
+
const forgotPassword = useCallback(
|
|
282
|
+
async (email: string) => {
|
|
283
|
+
setIsLoading(true);
|
|
284
|
+
setError(null);
|
|
285
|
+
try {
|
|
286
|
+
await apiCall("/auth/forgot-password", {
|
|
287
|
+
method: "POST",
|
|
288
|
+
body: JSON.stringify({ email }),
|
|
289
|
+
});
|
|
290
|
+
} catch (err: any) {
|
|
291
|
+
setError(err.message);
|
|
292
|
+
throw err;
|
|
293
|
+
} finally {
|
|
294
|
+
setIsLoading(false);
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
[apiCall]
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// Reset password with OTP
|
|
301
|
+
const resetPassword = useCallback(
|
|
302
|
+
async (email: string, code: string, newPassword: string) => {
|
|
303
|
+
setIsLoading(true);
|
|
304
|
+
setError(null);
|
|
305
|
+
try {
|
|
306
|
+
await apiCall("/auth/reset-password", {
|
|
307
|
+
method: "POST",
|
|
308
|
+
body: JSON.stringify({ email, code, newPassword }),
|
|
309
|
+
});
|
|
310
|
+
} catch (err: any) {
|
|
311
|
+
setError(err.message);
|
|
312
|
+
throw err;
|
|
313
|
+
} finally {
|
|
314
|
+
setIsLoading(false);
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
[apiCall]
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
// Change password (authenticated)
|
|
321
|
+
const changePassword = useCallback(
|
|
322
|
+
async (oldPassword: string, newPassword: string) => {
|
|
323
|
+
setIsLoading(true);
|
|
324
|
+
setError(null);
|
|
325
|
+
try {
|
|
326
|
+
await apiCall("/auth/change-password", {
|
|
327
|
+
method: "POST",
|
|
328
|
+
body: JSON.stringify({ oldPassword, newPassword }),
|
|
329
|
+
});
|
|
330
|
+
} catch (err: any) {
|
|
331
|
+
setError(err.message);
|
|
332
|
+
throw err;
|
|
333
|
+
} finally {
|
|
334
|
+
setIsLoading(false);
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
[apiCall]
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const refreshTokenFn = useCallback(async () => {
|
|
341
|
+
const rt = getRefreshTokenValue();
|
|
342
|
+
if (!rt) return;
|
|
343
|
+
try {
|
|
344
|
+
const data = await apiCall("/auth/refresh", {
|
|
345
|
+
method: "POST",
|
|
346
|
+
body: JSON.stringify({ refreshToken: rt }),
|
|
347
|
+
});
|
|
348
|
+
// Update tokens for the active account
|
|
349
|
+
const account = getActiveAccount();
|
|
350
|
+
if (account) {
|
|
351
|
+
setTokensForAccount(account.id, account.email, data.accessToken, data.refreshToken);
|
|
352
|
+
refreshAccountsList();
|
|
353
|
+
}
|
|
354
|
+
} catch {
|
|
355
|
+
clearActiveAccount();
|
|
356
|
+
setUser(null);
|
|
357
|
+
refreshAccountsList();
|
|
358
|
+
}
|
|
359
|
+
}, [apiCall, refreshAccountsList]);
|
|
360
|
+
|
|
361
|
+
// Logout current account (removes it from the accounts list)
|
|
362
|
+
const logout = useCallback(() => {
|
|
363
|
+
clearActiveAccount();
|
|
364
|
+
setUser(null);
|
|
365
|
+
setSessions([]);
|
|
366
|
+
refreshAccountsList();
|
|
367
|
+
}, [refreshAccountsList]);
|
|
368
|
+
|
|
369
|
+
// Logout a specific account (remove from list, not necessarily current)
|
|
370
|
+
const logoutAccount = useCallback(
|
|
371
|
+
(accountId: string) => {
|
|
372
|
+
const activeId = getActiveAccountId();
|
|
373
|
+
removeAccount(accountId);
|
|
374
|
+
refreshAccountsList();
|
|
375
|
+
// If we removed the current account, clear user state
|
|
376
|
+
if (accountId === activeId) {
|
|
377
|
+
localStorage.removeItem("sp_active_account");
|
|
378
|
+
setUser(null);
|
|
379
|
+
setSessions([]);
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
[refreshAccountsList]
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
// Switch to another stored account
|
|
386
|
+
const switchAccount = useCallback(
|
|
387
|
+
(accountId: string) => {
|
|
388
|
+
const accts = getAccounts();
|
|
389
|
+
const target = accts.find((a) => a.id === accountId);
|
|
390
|
+
if (!target) return;
|
|
391
|
+
|
|
392
|
+
setActiveAccountId(target.id);
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
const payload = JSON.parse(atob(target.accessToken.split(".")[1]));
|
|
396
|
+
if (payload.exp * 1000 > Date.now()) {
|
|
397
|
+
setUser({ id: target.id, email: target.email, isVerified: true });
|
|
398
|
+
setSessions([]);
|
|
399
|
+
} else {
|
|
400
|
+
// Token expired, need to refresh
|
|
401
|
+
setUser({ id: target.id, email: target.email, isVerified: true });
|
|
402
|
+
setSessions([]);
|
|
403
|
+
}
|
|
404
|
+
} catch {
|
|
405
|
+
removeAccount(accountId);
|
|
406
|
+
refreshAccountsList();
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
[refreshAccountsList]
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
// ---- Session Management ----
|
|
413
|
+
|
|
414
|
+
const fetchSessions = useCallback(async () => {
|
|
415
|
+
try {
|
|
416
|
+
const data = await apiCall("/sessions");
|
|
417
|
+
setSessions(data.sessions || []);
|
|
418
|
+
} catch (err: any) {
|
|
419
|
+
setError(err.message);
|
|
420
|
+
}
|
|
421
|
+
}, [apiCall]);
|
|
422
|
+
|
|
423
|
+
const revokeSession = useCallback(
|
|
424
|
+
async (sessionId: string) => {
|
|
425
|
+
await apiCall(`/sessions/${sessionId}`, { method: "DELETE" });
|
|
426
|
+
await fetchSessions();
|
|
427
|
+
},
|
|
428
|
+
[apiCall, fetchSessions]
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
const revokeAllSessions = useCallback(async () => {
|
|
432
|
+
await apiCall("/sessions", { method: "DELETE" });
|
|
433
|
+
setSessions([]);
|
|
434
|
+
}, [apiCall]);
|
|
435
|
+
|
|
436
|
+
// ---- Handle expired token on mount ----
|
|
437
|
+
|
|
438
|
+
useEffect(() => {
|
|
439
|
+
const account = getActiveAccount();
|
|
440
|
+
if (account && !user) {
|
|
441
|
+
refreshTokenFn().then(() => {
|
|
442
|
+
const updated = getActiveAccount();
|
|
443
|
+
if (updated) {
|
|
444
|
+
try {
|
|
445
|
+
JSON.parse(atob(updated.accessToken.split(".")[1]));
|
|
446
|
+
setUser({
|
|
447
|
+
id: updated.id,
|
|
448
|
+
email: updated.email,
|
|
449
|
+
isVerified: true,
|
|
450
|
+
});
|
|
451
|
+
} catch {
|
|
452
|
+
clearActiveAccount();
|
|
453
|
+
refreshAccountsList();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}, []);
|
|
459
|
+
|
|
460
|
+
return (
|
|
461
|
+
<AuthContext.Provider
|
|
462
|
+
value={{
|
|
463
|
+
user,
|
|
464
|
+
isAuthenticated: !!user,
|
|
465
|
+
isInitialized,
|
|
466
|
+
isLoading,
|
|
467
|
+
error,
|
|
468
|
+
clearError,
|
|
469
|
+
login,
|
|
470
|
+
register,
|
|
471
|
+
verifyEmail,
|
|
472
|
+
loginWithGoogle,
|
|
473
|
+
requestOtp,
|
|
474
|
+
verifyOtp,
|
|
475
|
+
forgotPassword,
|
|
476
|
+
resetPassword,
|
|
477
|
+
changePassword,
|
|
478
|
+
logout,
|
|
479
|
+
logoutAccount,
|
|
480
|
+
switchAccount,
|
|
481
|
+
accounts,
|
|
482
|
+
refreshToken: refreshTokenFn,
|
|
483
|
+
sessions,
|
|
484
|
+
fetchSessions,
|
|
485
|
+
revokeSession,
|
|
486
|
+
revokeAllSessions,
|
|
487
|
+
}}
|
|
488
|
+
>
|
|
489
|
+
{children}
|
|
490
|
+
</AuthContext.Provider>
|
|
491
|
+
);
|
|
492
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
import { AuthContext } from "../context/AuthContext";
|
|
3
|
+
import { AuthContextValue } from "../types";
|
|
4
|
+
|
|
5
|
+
export function useAuth(): AuthContextValue {
|
|
6
|
+
const context = useContext(AuthContext);
|
|
7
|
+
if (!context) {
|
|
8
|
+
throw new Error("useAuth must be used within a SecurePoolProvider");
|
|
9
|
+
}
|
|
10
|
+
return context;
|
|
11
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Context & Provider
|
|
2
|
+
export { SecurePoolProvider } from "./context/SecurePoolProvider";
|
|
3
|
+
export { AuthContext } from "./context/AuthContext";
|
|
4
|
+
|
|
5
|
+
// Hooks
|
|
6
|
+
export { useAuth } from "./hooks/useAuth";
|
|
7
|
+
|
|
8
|
+
// Components
|
|
9
|
+
export { LoginForm } from "./components/LoginForm";
|
|
10
|
+
export { SignupForm } from "./components/SignupForm";
|
|
11
|
+
export { OTPVerification } from "./components/OTPVerification";
|
|
12
|
+
export { GoogleLoginButton } from "./components/GoogleLoginButton";
|
|
13
|
+
export { SessionList } from "./components/SessionList";
|
|
14
|
+
|
|
15
|
+
// Types
|
|
16
|
+
export type {
|
|
17
|
+
SecurePoolConfig,
|
|
18
|
+
AuthUser,
|
|
19
|
+
AuthSession,
|
|
20
|
+
AuthContextValue,
|
|
21
|
+
StoredAccount,
|
|
22
|
+
} from "./types";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface SecurePoolConfig {
|
|
2
|
+
apiBaseUrl: string;
|
|
3
|
+
tenantId: string;
|
|
4
|
+
authType?: "jwt" | "otp" | "google";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface AuthUser {
|
|
8
|
+
id: string;
|
|
9
|
+
email: string;
|
|
10
|
+
isVerified: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface StoredAccount {
|
|
14
|
+
id: string;
|
|
15
|
+
email: string;
|
|
16
|
+
accessToken: string;
|
|
17
|
+
refreshToken: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AuthSession {
|
|
21
|
+
id: string;
|
|
22
|
+
device: string;
|
|
23
|
+
ip: string;
|
|
24
|
+
createdAt: string;
|
|
25
|
+
isActive: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AuthContextValue {
|
|
29
|
+
user: AuthUser | null;
|
|
30
|
+
isAuthenticated: boolean;
|
|
31
|
+
isInitialized: boolean;
|
|
32
|
+
isLoading: boolean;
|
|
33
|
+
error: string | null;
|
|
34
|
+
clearError: () => void;
|
|
35
|
+
login: (email: string, password: string) => Promise<void>;
|
|
36
|
+
register: (email: string, password: string) => Promise<any>;
|
|
37
|
+
verifyEmail: (email: string, code: string) => Promise<void>;
|
|
38
|
+
loginWithGoogle: (googleToken: string) => Promise<void>;
|
|
39
|
+
requestOtp: (email: string) => Promise<any>;
|
|
40
|
+
verifyOtp: (email: string, code: string) => Promise<void>;
|
|
41
|
+
forgotPassword: (email: string) => Promise<void>;
|
|
42
|
+
resetPassword: (email: string, code: string, newPassword: string) => Promise<void>;
|
|
43
|
+
changePassword: (oldPassword: string, newPassword: string) => Promise<void>;
|
|
44
|
+
logout: () => void;
|
|
45
|
+
logoutAccount: (accountId: string) => void;
|
|
46
|
+
switchAccount: (accountId: string) => void;
|
|
47
|
+
accounts: StoredAccount[];
|
|
48
|
+
refreshToken: () => Promise<void>;
|
|
49
|
+
sessions: AuthSession[];
|
|
50
|
+
fetchSessions: () => Promise<void>;
|
|
51
|
+
revokeSession: (sessionId: string) => Promise<void>;
|
|
52
|
+
revokeAllSessions: () => Promise<void>;
|
|
53
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "src",
|
|
5
|
+
"outDir": "dist",
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"]
|
|
10
|
+
},
|
|
11
|
+
"include": ["src"]
|
|
12
|
+
}
|