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,32 @@
|
|
|
1
|
+
import jwt, { SignOptions, Algorithm } from "jsonwebtoken";
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
import { ITokenService } from "@securepool/application";
|
|
4
|
+
|
|
5
|
+
export class JwtTokenService implements ITokenService {
|
|
6
|
+
private algorithm: Algorithm = "RS256";
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
private privateKey: string,
|
|
10
|
+
private publicKey: string,
|
|
11
|
+
private accessTokenExpirySeconds: number = 900 // 15 minutes
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
async generateAccessToken(userId: string, tenantId: string): Promise<string> {
|
|
15
|
+
const options: SignOptions = {
|
|
16
|
+
algorithm: this.algorithm,
|
|
17
|
+
expiresIn: this.accessTokenExpirySeconds,
|
|
18
|
+
};
|
|
19
|
+
return jwt.sign({ sub: userId, tenantId }, this.privateKey, options);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async generateRefreshToken(_userId: string): Promise<string> {
|
|
23
|
+
return crypto.randomBytes(64).toString("hex");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async verifyAccessToken(token: string): Promise<{ sub: string; tenantId: string }> {
|
|
27
|
+
const payload = jwt.verify(token, this.publicKey, {
|
|
28
|
+
algorithms: [this.algorithm],
|
|
29
|
+
}) as any;
|
|
30
|
+
return { sub: payload.sub, tenantId: payload.tenantId };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import { IOtpService, IOtpRepository } from "@securepool/application";
|
|
3
|
+
import { OtpCode } from "@securepool/core";
|
|
4
|
+
|
|
5
|
+
export class OtpServiceImpl implements IOtpService {
|
|
6
|
+
private maxAttempts: number;
|
|
7
|
+
private expiryMinutes: number;
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
private otpRepo: IOtpRepository,
|
|
11
|
+
options?: { maxAttempts?: number; expiryMinutes?: number }
|
|
12
|
+
) {
|
|
13
|
+
this.maxAttempts = options?.maxAttempts ?? 5;
|
|
14
|
+
this.expiryMinutes = options?.expiryMinutes ?? 5;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async generate(userId: string, metadata?: Record<string, string>): Promise<string> {
|
|
18
|
+
await this.otpRepo.deleteAllForUser(userId);
|
|
19
|
+
|
|
20
|
+
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
|
21
|
+
const otp = new OtpCode(
|
|
22
|
+
crypto.randomUUID(),
|
|
23
|
+
userId,
|
|
24
|
+
code,
|
|
25
|
+
new Date(Date.now() + this.expiryMinutes * 60 * 1000),
|
|
26
|
+
0,
|
|
27
|
+
metadata,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
await this.otpRepo.save(otp);
|
|
31
|
+
return code;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async verify(userId: string, code: string): Promise<{ valid: boolean; metadata?: Record<string, string> }> {
|
|
35
|
+
const record = await this.otpRepo.findLatest(userId);
|
|
36
|
+
if (!record) throw new Error("OTP not found");
|
|
37
|
+
if (record.expiresAt < new Date()) throw new Error("OTP expired");
|
|
38
|
+
if (record.attempts >= this.maxAttempts) throw new Error("Too many attempts");
|
|
39
|
+
|
|
40
|
+
if (record.code !== code) {
|
|
41
|
+
record.attempts++;
|
|
42
|
+
await this.otpRepo.update(record);
|
|
43
|
+
throw new Error("Invalid OTP");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const metadata = record.metadata;
|
|
47
|
+
await this.otpRepo.deleteAllForUser(userId);
|
|
48
|
+
return { valid: true, metadata };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@securepool/persistence",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "npx prisma generate && tsc",
|
|
8
|
+
"clean": "rm -rf dist",
|
|
9
|
+
"prisma:generate": "npx prisma generate",
|
|
10
|
+
"prisma:migrate": "npx prisma migrate dev"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@securepool/core": "1.0.0",
|
|
14
|
+
"@securepool/application": "1.0.0",
|
|
15
|
+
"mongoose": "^8.5.0",
|
|
16
|
+
"@prisma/client": "^5.18.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"typescript": "^5.5.0",
|
|
20
|
+
"prisma": "^5.18.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
generator client {
|
|
2
|
+
provider = "prisma-client-js"
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
datasource db {
|
|
6
|
+
provider = "postgresql"
|
|
7
|
+
url = env("DATABASE_URL")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
model User {
|
|
11
|
+
id String @id @default(uuid())
|
|
12
|
+
tenantId String
|
|
13
|
+
email String
|
|
14
|
+
passwordHash String?
|
|
15
|
+
isVerified Boolean @default(false)
|
|
16
|
+
createdAt DateTime @default(now())
|
|
17
|
+
|
|
18
|
+
roles UserRole[]
|
|
19
|
+
sessions Session[]
|
|
20
|
+
auditLogs AuditLog[]
|
|
21
|
+
|
|
22
|
+
@@unique([email, tenantId])
|
|
23
|
+
@@index([tenantId])
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
model Role {
|
|
27
|
+
id String @id @default(uuid())
|
|
28
|
+
name String @unique
|
|
29
|
+
users UserRole[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
model UserRole {
|
|
33
|
+
userId String
|
|
34
|
+
roleId String
|
|
35
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
36
|
+
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
|
37
|
+
|
|
38
|
+
@@id([userId, roleId])
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
model RefreshToken {
|
|
42
|
+
id String @id @default(uuid())
|
|
43
|
+
userId String
|
|
44
|
+
tokenHash String
|
|
45
|
+
expiresAt DateTime
|
|
46
|
+
isRevoked Boolean @default(false)
|
|
47
|
+
|
|
48
|
+
@@index([userId])
|
|
49
|
+
@@index([tokenHash])
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
model OtpCode {
|
|
53
|
+
id String @id @default(uuid())
|
|
54
|
+
userId String
|
|
55
|
+
code String
|
|
56
|
+
expiresAt DateTime
|
|
57
|
+
attempts Int @default(0)
|
|
58
|
+
|
|
59
|
+
@@index([userId])
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
model Session {
|
|
63
|
+
id String @id @default(uuid())
|
|
64
|
+
userId String
|
|
65
|
+
device String
|
|
66
|
+
ip String
|
|
67
|
+
createdAt DateTime @default(now())
|
|
68
|
+
isActive Boolean @default(true)
|
|
69
|
+
|
|
70
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
71
|
+
|
|
72
|
+
@@index([userId])
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
model AuditLog {
|
|
76
|
+
id String @id @default(uuid())
|
|
77
|
+
userId String
|
|
78
|
+
tenantId String
|
|
79
|
+
action String
|
|
80
|
+
ip String
|
|
81
|
+
metadata Json @default("{}")
|
|
82
|
+
timestamp DateTime @default(now())
|
|
83
|
+
|
|
84
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
85
|
+
|
|
86
|
+
@@index([userId])
|
|
87
|
+
@@index([tenantId])
|
|
88
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { PrismaClient } from "@prisma/client";
|
|
2
|
+
import { IUserRepository, ITokenRepository, IOtpRepository, ISessionRepository, IAuditLogRepository, IRoleRepository } from "@securepool/application";
|
|
3
|
+
import { connectMongo } from "./mongo/connection";
|
|
4
|
+
import { MongoUserRepository } from "./mongo/repositories/MongoUserRepository";
|
|
5
|
+
import { MongoTokenRepository } from "./mongo/repositories/MongoTokenRepository";
|
|
6
|
+
import { MongoOtpRepository } from "./mongo/repositories/MongoOtpRepository";
|
|
7
|
+
import { MongoSessionRepository } from "./mongo/repositories/MongoSessionRepository";
|
|
8
|
+
import { MongoAuditLogRepository } from "./mongo/repositories/MongoAuditLogRepository";
|
|
9
|
+
import { MongoRoleRepository } from "./mongo/repositories/MongoRoleRepository";
|
|
10
|
+
import { PrismaUserRepository } from "./prisma/repositories/PrismaUserRepository";
|
|
11
|
+
import { PrismaTokenRepository } from "./prisma/repositories/PrismaTokenRepository";
|
|
12
|
+
import { PrismaOtpRepository } from "./prisma/repositories/PrismaOtpRepository";
|
|
13
|
+
import { PrismaSessionRepository } from "./prisma/repositories/PrismaSessionRepository";
|
|
14
|
+
import { PrismaAuditLogRepository } from "./prisma/repositories/PrismaAuditLogRepository";
|
|
15
|
+
import { PrismaRoleRepository } from "./prisma/repositories/PrismaRoleRepository";
|
|
16
|
+
|
|
17
|
+
export interface Repositories {
|
|
18
|
+
userRepo: IUserRepository;
|
|
19
|
+
tokenRepo: ITokenRepository;
|
|
20
|
+
otpRepo: IOtpRepository;
|
|
21
|
+
sessionRepo: ISessionRepository;
|
|
22
|
+
auditLogRepo: IAuditLogRepository;
|
|
23
|
+
roleRepo: IRoleRepository;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function createRepositories(config: { type: "mongo" | "postgres"; url: string }): Promise<Repositories> {
|
|
27
|
+
if (config.type === "mongo") {
|
|
28
|
+
await connectMongo(config.url);
|
|
29
|
+
return {
|
|
30
|
+
userRepo: new MongoUserRepository(),
|
|
31
|
+
tokenRepo: new MongoTokenRepository(),
|
|
32
|
+
otpRepo: new MongoOtpRepository(),
|
|
33
|
+
sessionRepo: new MongoSessionRepository(),
|
|
34
|
+
auditLogRepo: new MongoAuditLogRepository(),
|
|
35
|
+
roleRepo: new MongoRoleRepository(),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const prisma = new PrismaClient({ datasourceUrl: config.url });
|
|
40
|
+
return {
|
|
41
|
+
userRepo: new PrismaUserRepository(prisma),
|
|
42
|
+
tokenRepo: new PrismaTokenRepository(prisma),
|
|
43
|
+
otpRepo: new PrismaOtpRepository(prisma),
|
|
44
|
+
sessionRepo: new PrismaSessionRepository(prisma),
|
|
45
|
+
auditLogRepo: new PrismaAuditLogRepository(prisma),
|
|
46
|
+
roleRepo: new PrismaRoleRepository(prisma),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Factory
|
|
2
|
+
export { createRepositories, Repositories } from "./factory";
|
|
3
|
+
|
|
4
|
+
// MongoDB connection helpers
|
|
5
|
+
export { connectMongo, disconnectMongo } from "./mongo/connection";
|
|
6
|
+
|
|
7
|
+
// MongoDB models
|
|
8
|
+
export { UserModel } from "./mongo/models/UserModel";
|
|
9
|
+
export { RefreshTokenModel } from "./mongo/models/RefreshTokenModel";
|
|
10
|
+
export { OtpModel } from "./mongo/models/OtpModel";
|
|
11
|
+
export { SessionModel } from "./mongo/models/SessionModel";
|
|
12
|
+
export { AuditLogModel } from "./mongo/models/AuditLogModel";
|
|
13
|
+
export { RoleModel } from "./mongo/models/RoleModel";
|
|
14
|
+
export { UserRoleModel } from "./mongo/models/UserRoleModel";
|
|
15
|
+
|
|
16
|
+
// MongoDB repositories
|
|
17
|
+
export { MongoUserRepository } from "./mongo/repositories/MongoUserRepository";
|
|
18
|
+
export { MongoTokenRepository } from "./mongo/repositories/MongoTokenRepository";
|
|
19
|
+
export { MongoOtpRepository } from "./mongo/repositories/MongoOtpRepository";
|
|
20
|
+
export { MongoSessionRepository } from "./mongo/repositories/MongoSessionRepository";
|
|
21
|
+
export { MongoAuditLogRepository } from "./mongo/repositories/MongoAuditLogRepository";
|
|
22
|
+
export { MongoRoleRepository } from "./mongo/repositories/MongoRoleRepository";
|
|
23
|
+
|
|
24
|
+
// Prisma repositories
|
|
25
|
+
export { PrismaUserRepository } from "./prisma/repositories/PrismaUserRepository";
|
|
26
|
+
export { PrismaTokenRepository } from "./prisma/repositories/PrismaTokenRepository";
|
|
27
|
+
export { PrismaOtpRepository } from "./prisma/repositories/PrismaOtpRepository";
|
|
28
|
+
export { PrismaSessionRepository } from "./prisma/repositories/PrismaSessionRepository";
|
|
29
|
+
export { PrismaAuditLogRepository } from "./prisma/repositories/PrismaAuditLogRepository";
|
|
30
|
+
export { PrismaRoleRepository } from "./prisma/repositories/PrismaRoleRepository";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import mongoose from "mongoose";
|
|
2
|
+
|
|
3
|
+
export async function connectMongo(url: string): Promise<typeof mongoose> {
|
|
4
|
+
return mongoose.connect(url, { retryWrites: true });
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function disconnectMongo(): Promise<void> {
|
|
8
|
+
await mongoose.disconnect();
|
|
9
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import mongoose, { Schema, Document } from "mongoose";
|
|
2
|
+
|
|
3
|
+
export interface IAuditLogDocument extends Document {
|
|
4
|
+
userId: string;
|
|
5
|
+
tenantId: string;
|
|
6
|
+
action: string;
|
|
7
|
+
ip: string;
|
|
8
|
+
metadata: Record<string, unknown>;
|
|
9
|
+
timestamp: Date;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const AuditLogSchema = new Schema<IAuditLogDocument>({
|
|
13
|
+
userId: { type: String, required: true, index: true },
|
|
14
|
+
tenantId: { type: String, required: true, index: true },
|
|
15
|
+
action: { type: String, required: true },
|
|
16
|
+
ip: { type: String, default: "unknown" },
|
|
17
|
+
metadata: { type: Schema.Types.Mixed, default: {} },
|
|
18
|
+
timestamp: { type: Date, default: Date.now },
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const AuditLogModel = mongoose.model<IAuditLogDocument>("AuditLog", AuditLogSchema);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import mongoose, { Schema, Document } from "mongoose";
|
|
2
|
+
|
|
3
|
+
export interface IOtpDocument extends Document {
|
|
4
|
+
userId: string;
|
|
5
|
+
code: string;
|
|
6
|
+
expiresAt: Date;
|
|
7
|
+
attempts: number;
|
|
8
|
+
metadata?: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const OtpSchema = new Schema<IOtpDocument>({
|
|
12
|
+
userId: { type: String, required: true, index: true },
|
|
13
|
+
code: { type: String, required: true },
|
|
14
|
+
expiresAt: { type: Date, required: true },
|
|
15
|
+
attempts: { type: Number, default: 0 },
|
|
16
|
+
metadata: { type: Schema.Types.Mixed, default: undefined },
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const OtpModel = mongoose.model<IOtpDocument>("OtpCode", OtpSchema);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import mongoose, { Schema, Document } from "mongoose";
|
|
2
|
+
|
|
3
|
+
export interface IRefreshTokenDocument extends Document {
|
|
4
|
+
userId: string;
|
|
5
|
+
tokenHash: string;
|
|
6
|
+
expiresAt: Date;
|
|
7
|
+
isRevoked: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const RefreshTokenSchema = new Schema<IRefreshTokenDocument>({
|
|
11
|
+
userId: { type: String, required: true, index: true },
|
|
12
|
+
tokenHash: { type: String, required: true, index: true },
|
|
13
|
+
expiresAt: { type: Date, required: true },
|
|
14
|
+
isRevoked: { type: Boolean, default: false },
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const RefreshTokenModel = mongoose.model<IRefreshTokenDocument>("RefreshToken", RefreshTokenSchema);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import mongoose, { Schema, Document } from "mongoose";
|
|
2
|
+
|
|
3
|
+
export interface IRoleDocument extends Document {
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const RoleSchema = new Schema<IRoleDocument>({
|
|
8
|
+
name: { type: String, required: true, unique: true },
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const RoleModel = mongoose.model<IRoleDocument>("Role", RoleSchema);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import mongoose, { Schema, Document } from "mongoose";
|
|
2
|
+
|
|
3
|
+
export interface ISessionDocument extends Document {
|
|
4
|
+
userId: string;
|
|
5
|
+
device: string;
|
|
6
|
+
ip: string;
|
|
7
|
+
createdAt: Date;
|
|
8
|
+
isActive: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const SessionSchema = new Schema<ISessionDocument>({
|
|
12
|
+
userId: { type: String, required: true, index: true },
|
|
13
|
+
device: { type: String, required: true },
|
|
14
|
+
ip: { type: String, required: true },
|
|
15
|
+
createdAt: { type: Date, default: Date.now },
|
|
16
|
+
isActive: { type: Boolean, default: true },
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const SessionModel = mongoose.model<ISessionDocument>("Session", SessionSchema);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import mongoose, { Schema, Document } from "mongoose";
|
|
2
|
+
|
|
3
|
+
export interface IUserDocument extends Document {
|
|
4
|
+
tenantId: string;
|
|
5
|
+
email: string;
|
|
6
|
+
passwordHash: string | null;
|
|
7
|
+
isVerified: boolean;
|
|
8
|
+
createdAt: Date;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const UserSchema = new Schema<IUserDocument>({
|
|
12
|
+
tenantId: { type: String, required: true, index: true },
|
|
13
|
+
email: { type: String, required: true },
|
|
14
|
+
passwordHash: { type: String, default: null },
|
|
15
|
+
isVerified: { type: Boolean, default: false },
|
|
16
|
+
createdAt: { type: Date, default: Date.now },
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
UserSchema.index({ email: 1, tenantId: 1 }, { unique: true });
|
|
20
|
+
|
|
21
|
+
export const UserModel = mongoose.model<IUserDocument>("User", UserSchema);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import mongoose, { Schema, Document } from "mongoose";
|
|
2
|
+
|
|
3
|
+
export interface IUserRoleDocument extends Document {
|
|
4
|
+
userId: string;
|
|
5
|
+
roleId: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const UserRoleSchema = new Schema<IUserRoleDocument>({
|
|
9
|
+
userId: { type: String, required: true },
|
|
10
|
+
roleId: { type: String, required: true },
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
UserRoleSchema.index({ userId: 1, roleId: 1 }, { unique: true });
|
|
14
|
+
|
|
15
|
+
export const UserRoleModel = mongoose.model<IUserRoleDocument>("UserRole", UserRoleSchema);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { AuditLog } from "@securepool/core";
|
|
2
|
+
import { IAuditLogRepository } from "@securepool/application";
|
|
3
|
+
import { AuditLogModel } from "../models/AuditLogModel";
|
|
4
|
+
|
|
5
|
+
export class MongoAuditLogRepository implements IAuditLogRepository {
|
|
6
|
+
async log(entry: AuditLog): Promise<void> {
|
|
7
|
+
await AuditLogModel.create({
|
|
8
|
+
userId: entry.userId,
|
|
9
|
+
tenantId: entry.tenantId,
|
|
10
|
+
action: entry.action,
|
|
11
|
+
ip: entry.ip,
|
|
12
|
+
metadata: entry.metadata,
|
|
13
|
+
timestamp: entry.timestamp,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async findByUserId(userId: string): Promise<AuditLog[]> {
|
|
18
|
+
const docs = await AuditLogModel.find({ userId }).sort({ timestamp: -1 });
|
|
19
|
+
return docs.map(doc => new AuditLog(
|
|
20
|
+
doc._id.toString(),
|
|
21
|
+
doc.userId,
|
|
22
|
+
doc.tenantId,
|
|
23
|
+
doc.action,
|
|
24
|
+
doc.ip,
|
|
25
|
+
doc.metadata,
|
|
26
|
+
doc.timestamp,
|
|
27
|
+
));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { OtpCode } from "@securepool/core";
|
|
2
|
+
import { IOtpRepository } from "@securepool/application";
|
|
3
|
+
import { OtpModel } from "../models/OtpModel";
|
|
4
|
+
|
|
5
|
+
export class MongoOtpRepository implements IOtpRepository {
|
|
6
|
+
async save(otp: OtpCode): Promise<void> {
|
|
7
|
+
const doc = await OtpModel.create({
|
|
8
|
+
userId: otp.userId,
|
|
9
|
+
code: otp.code,
|
|
10
|
+
expiresAt: otp.expiresAt,
|
|
11
|
+
attempts: otp.attempts,
|
|
12
|
+
metadata: otp.metadata,
|
|
13
|
+
});
|
|
14
|
+
otp.id = doc._id.toString();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async findLatest(userId: string): Promise<OtpCode | null> {
|
|
18
|
+
const doc = await OtpModel.findOne({ userId }).sort({ expiresAt: -1 });
|
|
19
|
+
if (!doc) return null;
|
|
20
|
+
return new OtpCode(doc._id.toString(), doc.userId, doc.code, doc.expiresAt, doc.attempts, doc.metadata as Record<string, string> | undefined);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async update(otp: OtpCode): Promise<void> {
|
|
24
|
+
await OtpModel.findByIdAndUpdate(otp.id, {
|
|
25
|
+
code: otp.code,
|
|
26
|
+
expiresAt: otp.expiresAt,
|
|
27
|
+
attempts: otp.attempts,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async deleteAllForUser(userId: string): Promise<void> {
|
|
32
|
+
await OtpModel.deleteMany({ userId });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Role, UserRole } from "@securepool/core";
|
|
2
|
+
import { IRoleRepository } from "@securepool/application";
|
|
3
|
+
import { RoleModel } from "../models/RoleModel";
|
|
4
|
+
import { UserRoleModel } from "../models/UserRoleModel";
|
|
5
|
+
|
|
6
|
+
export class MongoRoleRepository implements IRoleRepository {
|
|
7
|
+
async findById(id: string): Promise<Role | null> {
|
|
8
|
+
const doc = await RoleModel.findById(id);
|
|
9
|
+
if (!doc) return null;
|
|
10
|
+
return new Role(doc._id.toString(), doc.name);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async findByName(name: string): Promise<Role | null> {
|
|
14
|
+
const doc = await RoleModel.findOne({ name });
|
|
15
|
+
if (!doc) return null;
|
|
16
|
+
return new Role(doc._id.toString(), doc.name);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async assignRole(userRole: UserRole): Promise<void> {
|
|
20
|
+
await UserRoleModel.create({
|
|
21
|
+
userId: userRole.userId,
|
|
22
|
+
roleId: userRole.roleId,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async getUserRoles(userId: string): Promise<Role[]> {
|
|
27
|
+
const userRoleDocs = await UserRoleModel.find({ userId });
|
|
28
|
+
const roleIds = userRoleDocs.map(doc => doc.roleId);
|
|
29
|
+
const roleDocs = await RoleModel.find({ _id: { $in: roleIds } });
|
|
30
|
+
return roleDocs.map(doc => new Role(doc._id.toString(), doc.name));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Session } from "@securepool/core";
|
|
2
|
+
import { ISessionRepository } from "@securepool/application";
|
|
3
|
+
import { SessionModel } from "../models/SessionModel";
|
|
4
|
+
|
|
5
|
+
export class MongoSessionRepository implements ISessionRepository {
|
|
6
|
+
async create(session: Session): Promise<void> {
|
|
7
|
+
const doc = await SessionModel.create({
|
|
8
|
+
userId: session.userId,
|
|
9
|
+
device: session.device,
|
|
10
|
+
ip: session.ip,
|
|
11
|
+
createdAt: session.createdAt,
|
|
12
|
+
isActive: session.isActive,
|
|
13
|
+
});
|
|
14
|
+
session.id = doc._id.toString();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async findByUserId(userId: string): Promise<Session[]> {
|
|
18
|
+
const docs = await SessionModel.find({ userId, isActive: true });
|
|
19
|
+
return docs.map(doc => new Session(doc._id.toString(), doc.userId, doc.device, doc.ip, doc.createdAt, doc.isActive));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async deactivate(sessionId: string): Promise<void> {
|
|
23
|
+
await SessionModel.findByIdAndUpdate(sessionId, { isActive: false });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async deactivateAllForUser(userId: string): Promise<void> {
|
|
27
|
+
await SessionModel.updateMany({ userId }, { isActive: false });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { RefreshToken } from "@securepool/core";
|
|
2
|
+
import { ITokenRepository } from "@securepool/application";
|
|
3
|
+
import { RefreshTokenModel } from "../models/RefreshTokenModel";
|
|
4
|
+
|
|
5
|
+
export class MongoTokenRepository implements ITokenRepository {
|
|
6
|
+
async save(token: RefreshToken): Promise<void> {
|
|
7
|
+
const doc = await RefreshTokenModel.create({
|
|
8
|
+
userId: token.userId,
|
|
9
|
+
tokenHash: token.tokenHash,
|
|
10
|
+
expiresAt: token.expiresAt,
|
|
11
|
+
isRevoked: token.isRevoked,
|
|
12
|
+
});
|
|
13
|
+
token.id = doc._id.toString();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async findByTokenHash(tokenHash: string): Promise<RefreshToken | null> {
|
|
17
|
+
const doc = await RefreshTokenModel.findOne({ tokenHash });
|
|
18
|
+
if (!doc) return null;
|
|
19
|
+
return new RefreshToken(doc._id.toString(), doc.userId, doc.tokenHash, doc.expiresAt, doc.isRevoked);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async findActiveByUserId(userId: string): Promise<RefreshToken[]> {
|
|
23
|
+
const docs = await RefreshTokenModel.find({ userId, isRevoked: false, expiresAt: { $gt: new Date() } });
|
|
24
|
+
return docs.map(doc => new RefreshToken(doc._id.toString(), doc.userId, doc.tokenHash, doc.expiresAt, doc.isRevoked));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async revoke(tokenId: string): Promise<void> {
|
|
28
|
+
await RefreshTokenModel.findByIdAndUpdate(tokenId, { isRevoked: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async revokeAllForUser(userId: string): Promise<void> {
|
|
32
|
+
await RefreshTokenModel.updateMany({ userId }, { isRevoked: true });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { User } from "@securepool/core";
|
|
2
|
+
import { IUserRepository } from "@securepool/application";
|
|
3
|
+
import { UserModel } from "../models/UserModel";
|
|
4
|
+
|
|
5
|
+
export class MongoUserRepository implements IUserRepository {
|
|
6
|
+
async findById(id: string): Promise<User | null> {
|
|
7
|
+
const doc = await UserModel.findById(id);
|
|
8
|
+
if (!doc) return null;
|
|
9
|
+
return new User(doc._id.toString(), doc.tenantId, doc.email, doc.passwordHash, doc.isVerified, doc.createdAt);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async findByEmail(email: string, tenantId: string): Promise<User | null> {
|
|
13
|
+
const doc = await UserModel.findOne({ email, tenantId });
|
|
14
|
+
if (!doc) return null;
|
|
15
|
+
return new User(doc._id.toString(), doc.tenantId, doc.email, doc.passwordHash, doc.isVerified, doc.createdAt);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async create(user: User): Promise<void> {
|
|
19
|
+
const doc = await UserModel.create({
|
|
20
|
+
tenantId: user.tenantId,
|
|
21
|
+
email: user.email,
|
|
22
|
+
passwordHash: user.passwordHash,
|
|
23
|
+
isVerified: user.isVerified,
|
|
24
|
+
createdAt: user.createdAt,
|
|
25
|
+
});
|
|
26
|
+
// Update the user's id with the Mongo-generated ObjectId
|
|
27
|
+
user.id = doc._id.toString();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async update(user: User): Promise<void> {
|
|
31
|
+
await UserModel.findByIdAndUpdate(user.id, {
|
|
32
|
+
email: user.email,
|
|
33
|
+
passwordHash: user.passwordHash,
|
|
34
|
+
isVerified: user.isVerified,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { PrismaClient } from "@prisma/client";
|
|
2
|
+
import { AuditLog } from "@securepool/core";
|
|
3
|
+
import { IAuditLogRepository } from "@securepool/application";
|
|
4
|
+
|
|
5
|
+
export class PrismaAuditLogRepository implements IAuditLogRepository {
|
|
6
|
+
constructor(private readonly prisma: PrismaClient) {}
|
|
7
|
+
|
|
8
|
+
async log(entry: AuditLog): Promise<void> {
|
|
9
|
+
await this.prisma.auditLog.create({
|
|
10
|
+
data: {
|
|
11
|
+
id: entry.id,
|
|
12
|
+
userId: entry.userId,
|
|
13
|
+
tenantId: entry.tenantId,
|
|
14
|
+
action: entry.action,
|
|
15
|
+
ip: entry.ip,
|
|
16
|
+
metadata: entry.metadata as any,
|
|
17
|
+
timestamp: entry.timestamp,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async findByUserId(userId: string): Promise<AuditLog[]> {
|
|
23
|
+
const records = await this.prisma.auditLog.findMany({
|
|
24
|
+
where: { userId },
|
|
25
|
+
orderBy: { timestamp: "desc" },
|
|
26
|
+
});
|
|
27
|
+
return records.map((record: any) => new AuditLog(
|
|
28
|
+
record.id,
|
|
29
|
+
record.userId,
|
|
30
|
+
record.tenantId,
|
|
31
|
+
record.action,
|
|
32
|
+
record.ip,
|
|
33
|
+
record.metadata as Record<string, unknown>,
|
|
34
|
+
record.timestamp,
|
|
35
|
+
));
|
|
36
|
+
}
|
|
37
|
+
}
|