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,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
|
+
}
|