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,43 @@
|
|
|
1
|
+
import { PrismaClient } from "@prisma/client";
|
|
2
|
+
import { OtpCode } from "@securepool/core";
|
|
3
|
+
import { IOtpRepository } from "@securepool/application";
|
|
4
|
+
|
|
5
|
+
export class PrismaOtpRepository implements IOtpRepository {
|
|
6
|
+
constructor(private readonly prisma: PrismaClient) {}
|
|
7
|
+
|
|
8
|
+
async save(otp: OtpCode): Promise<void> {
|
|
9
|
+
await this.prisma.otpCode.create({
|
|
10
|
+
data: {
|
|
11
|
+
id: otp.id,
|
|
12
|
+
userId: otp.userId,
|
|
13
|
+
code: otp.code,
|
|
14
|
+
expiresAt: otp.expiresAt,
|
|
15
|
+
attempts: otp.attempts,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async findLatest(userId: string): Promise<OtpCode | null> {
|
|
21
|
+
const record = await this.prisma.otpCode.findFirst({
|
|
22
|
+
where: { userId },
|
|
23
|
+
orderBy: { expiresAt: "desc" },
|
|
24
|
+
});
|
|
25
|
+
if (!record) return null;
|
|
26
|
+
return new OtpCode(record.id, record.userId, record.code, record.expiresAt, record.attempts);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async update(otp: OtpCode): Promise<void> {
|
|
30
|
+
await this.prisma.otpCode.update({
|
|
31
|
+
where: { id: otp.id },
|
|
32
|
+
data: {
|
|
33
|
+
code: otp.code,
|
|
34
|
+
expiresAt: otp.expiresAt,
|
|
35
|
+
attempts: otp.attempts,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async deleteAllForUser(userId: string): Promise<void> {
|
|
41
|
+
await this.prisma.otpCode.deleteMany({ where: { userId } });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { PrismaClient } from "@prisma/client";
|
|
2
|
+
import { Role, UserRole } from "@securepool/core";
|
|
3
|
+
import { IRoleRepository } from "@securepool/application";
|
|
4
|
+
|
|
5
|
+
export class PrismaRoleRepository implements IRoleRepository {
|
|
6
|
+
constructor(private readonly prisma: PrismaClient) {}
|
|
7
|
+
|
|
8
|
+
async findById(id: string): Promise<Role | null> {
|
|
9
|
+
const record = await this.prisma.role.findUnique({ where: { id } });
|
|
10
|
+
if (!record) return null;
|
|
11
|
+
return new Role(record.id, record.name);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async findByName(name: string): Promise<Role | null> {
|
|
15
|
+
const record = await this.prisma.role.findUnique({ where: { name } });
|
|
16
|
+
if (!record) return null;
|
|
17
|
+
return new Role(record.id, record.name);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async assignRole(userRole: UserRole): Promise<void> {
|
|
21
|
+
await this.prisma.userRole.create({
|
|
22
|
+
data: {
|
|
23
|
+
userId: userRole.userId,
|
|
24
|
+
roleId: userRole.roleId,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async getUserRoles(userId: string): Promise<Role[]> {
|
|
30
|
+
const userRoles = await this.prisma.userRole.findMany({
|
|
31
|
+
where: { userId },
|
|
32
|
+
include: { role: true },
|
|
33
|
+
});
|
|
34
|
+
return userRoles.map((ur: any) => new Role(ur.role.id, ur.role.name));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { PrismaClient } from "@prisma/client";
|
|
2
|
+
import { Session } from "@securepool/core";
|
|
3
|
+
import { ISessionRepository } from "@securepool/application";
|
|
4
|
+
|
|
5
|
+
export class PrismaSessionRepository implements ISessionRepository {
|
|
6
|
+
constructor(private readonly prisma: PrismaClient) {}
|
|
7
|
+
|
|
8
|
+
async create(session: Session): Promise<void> {
|
|
9
|
+
await this.prisma.session.create({
|
|
10
|
+
data: {
|
|
11
|
+
id: session.id,
|
|
12
|
+
userId: session.userId,
|
|
13
|
+
device: session.device,
|
|
14
|
+
ip: session.ip,
|
|
15
|
+
createdAt: session.createdAt,
|
|
16
|
+
isActive: session.isActive,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async findByUserId(userId: string): Promise<Session[]> {
|
|
22
|
+
const records = await this.prisma.session.findMany({ where: { userId } });
|
|
23
|
+
return records.map((record: any) => new Session(record.id, record.userId, record.device, record.ip, record.createdAt, record.isActive));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async deactivate(sessionId: string): Promise<void> {
|
|
27
|
+
await this.prisma.session.update({
|
|
28
|
+
where: { id: sessionId },
|
|
29
|
+
data: { isActive: false },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async deactivateAllForUser(userId: string): Promise<void> {
|
|
34
|
+
await this.prisma.session.updateMany({
|
|
35
|
+
where: { userId },
|
|
36
|
+
data: { isActive: false },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { PrismaClient } from "@prisma/client";
|
|
2
|
+
import { RefreshToken } from "@securepool/core";
|
|
3
|
+
import { ITokenRepository } from "@securepool/application";
|
|
4
|
+
|
|
5
|
+
export class PrismaTokenRepository implements ITokenRepository {
|
|
6
|
+
constructor(private readonly prisma: PrismaClient) {}
|
|
7
|
+
|
|
8
|
+
async save(token: RefreshToken): Promise<void> {
|
|
9
|
+
await this.prisma.refreshToken.create({
|
|
10
|
+
data: {
|
|
11
|
+
id: token.id,
|
|
12
|
+
userId: token.userId,
|
|
13
|
+
tokenHash: token.tokenHash,
|
|
14
|
+
expiresAt: token.expiresAt,
|
|
15
|
+
isRevoked: token.isRevoked,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async findByTokenHash(tokenHash: string): Promise<RefreshToken | null> {
|
|
21
|
+
const record = await this.prisma.refreshToken.findFirst({ where: { tokenHash } });
|
|
22
|
+
if (!record) return null;
|
|
23
|
+
return new RefreshToken(record.id, record.userId, record.tokenHash, record.expiresAt, record.isRevoked);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async findActiveByUserId(userId: string): Promise<RefreshToken[]> {
|
|
27
|
+
const records = await this.prisma.refreshToken.findMany({
|
|
28
|
+
where: {
|
|
29
|
+
userId,
|
|
30
|
+
isRevoked: false,
|
|
31
|
+
expiresAt: { gt: new Date() },
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
return records.map((record: any) => new RefreshToken(record.id, record.userId, record.tokenHash, record.expiresAt, record.isRevoked));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async revoke(tokenId: string): Promise<void> {
|
|
38
|
+
await this.prisma.refreshToken.update({
|
|
39
|
+
where: { id: tokenId },
|
|
40
|
+
data: { isRevoked: true },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async revokeAllForUser(userId: string): Promise<void> {
|
|
45
|
+
await this.prisma.refreshToken.updateMany({
|
|
46
|
+
where: { userId },
|
|
47
|
+
data: { isRevoked: true },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { PrismaClient } from "@prisma/client";
|
|
2
|
+
import { User } from "@securepool/core";
|
|
3
|
+
import { IUserRepository } from "@securepool/application";
|
|
4
|
+
|
|
5
|
+
export class PrismaUserRepository implements IUserRepository {
|
|
6
|
+
constructor(private readonly prisma: PrismaClient) {}
|
|
7
|
+
|
|
8
|
+
async findById(id: string): Promise<User | null> {
|
|
9
|
+
const record = await this.prisma.user.findUnique({ where: { id } });
|
|
10
|
+
if (!record) return null;
|
|
11
|
+
return new User(record.id, record.tenantId, record.email, record.passwordHash, record.isVerified, record.createdAt);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async findByEmail(email: string, tenantId: string): Promise<User | null> {
|
|
15
|
+
const record = await this.prisma.user.findUnique({
|
|
16
|
+
where: { email_tenantId: { email, tenantId } },
|
|
17
|
+
});
|
|
18
|
+
if (!record) return null;
|
|
19
|
+
return new User(record.id, record.tenantId, record.email, record.passwordHash, record.isVerified, record.createdAt);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async create(user: User): Promise<void> {
|
|
23
|
+
await this.prisma.user.create({
|
|
24
|
+
data: {
|
|
25
|
+
id: user.id,
|
|
26
|
+
tenantId: user.tenantId,
|
|
27
|
+
email: user.email,
|
|
28
|
+
passwordHash: user.passwordHash,
|
|
29
|
+
isVerified: user.isVerified,
|
|
30
|
+
createdAt: user.createdAt,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async update(user: User): Promise<void> {
|
|
36
|
+
await this.prisma.user.update({
|
|
37
|
+
where: { id: user.id },
|
|
38
|
+
data: {
|
|
39
|
+
email: user.email,
|
|
40
|
+
passwordHash: user.passwordHash,
|
|
41
|
+
isVerified: user.isVerified,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@securepool/react-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"clean": "rm -rf dist"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"typescript": "^5.5.0",
|
|
14
|
+
"@types/react": "^19.0.0",
|
|
15
|
+
"@types/react-dom": "^19.0.0",
|
|
16
|
+
"react": "^19.0.0",
|
|
17
|
+
"react-dom": "^19.0.0"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"react": ">=18.0.0",
|
|
21
|
+
"react-dom": ">=18.0.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useAuth } from "../hooks/useAuth";
|
|
3
|
+
|
|
4
|
+
interface GoogleLoginButtonProps {
|
|
5
|
+
onSuccess?: () => void;
|
|
6
|
+
onError?: (error: string) => void;
|
|
7
|
+
className?: string;
|
|
8
|
+
style?: React.CSSProperties;
|
|
9
|
+
label?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const GoogleLoginButton: React.FC<GoogleLoginButtonProps> = ({
|
|
13
|
+
onSuccess,
|
|
14
|
+
onError,
|
|
15
|
+
className,
|
|
16
|
+
style,
|
|
17
|
+
label = "Sign in with Google",
|
|
18
|
+
}) => {
|
|
19
|
+
const { loginWithGoogle, isLoading } = useAuth();
|
|
20
|
+
|
|
21
|
+
const handleClick = async () => {
|
|
22
|
+
try {
|
|
23
|
+
// In production, integrate with Google Identity Services SDK
|
|
24
|
+
// to obtain the credential token, then pass it here.
|
|
25
|
+
const google = (window as any).google;
|
|
26
|
+
if (google?.accounts?.id) {
|
|
27
|
+
google.accounts.id.prompt((response: any) => {
|
|
28
|
+
if (response.credential) {
|
|
29
|
+
loginWithGoogle(response.credential).then(onSuccess).catch((err: any) => {
|
|
30
|
+
onError?.(err.message);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
} else {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"Google Identity Services SDK not loaded. Add the script to your HTML."
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
} catch (err: any) {
|
|
40
|
+
onError?.(err.message);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<button
|
|
46
|
+
onClick={handleClick}
|
|
47
|
+
disabled={isLoading}
|
|
48
|
+
className={className}
|
|
49
|
+
style={style}
|
|
50
|
+
>
|
|
51
|
+
{isLoading ? "Signing in..." : label}
|
|
52
|
+
</button>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React, { useState, FormEvent } from "react";
|
|
2
|
+
import { useAuth } from "../hooks/useAuth";
|
|
3
|
+
|
|
4
|
+
interface LoginFormProps {
|
|
5
|
+
onSuccess?: () => void;
|
|
6
|
+
onError?: (error: string) => void;
|
|
7
|
+
className?: string;
|
|
8
|
+
style?: React.CSSProperties;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const LoginForm: React.FC<LoginFormProps> = ({
|
|
12
|
+
onSuccess,
|
|
13
|
+
onError,
|
|
14
|
+
className,
|
|
15
|
+
style,
|
|
16
|
+
}) => {
|
|
17
|
+
const { login, isLoading, error } = useAuth();
|
|
18
|
+
const [email, setEmail] = useState("");
|
|
19
|
+
const [password, setPassword] = useState("");
|
|
20
|
+
|
|
21
|
+
const handleSubmit = async (e: FormEvent) => {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
try {
|
|
24
|
+
await login(email, password);
|
|
25
|
+
onSuccess?.();
|
|
26
|
+
} catch (err: any) {
|
|
27
|
+
onError?.(err.message);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<form onSubmit={handleSubmit} className={className} style={style}>
|
|
33
|
+
<div>
|
|
34
|
+
<label htmlFor="sp-login-email">Email</label>
|
|
35
|
+
<input
|
|
36
|
+
id="sp-login-email"
|
|
37
|
+
type="email"
|
|
38
|
+
value={email}
|
|
39
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
40
|
+
placeholder="Enter your email"
|
|
41
|
+
required
|
|
42
|
+
disabled={isLoading}
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
<div>
|
|
46
|
+
<label htmlFor="sp-login-password">Password</label>
|
|
47
|
+
<input
|
|
48
|
+
id="sp-login-password"
|
|
49
|
+
type="password"
|
|
50
|
+
value={password}
|
|
51
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
52
|
+
placeholder="Enter your password"
|
|
53
|
+
required
|
|
54
|
+
disabled={isLoading}
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
{error && (
|
|
58
|
+
<div role="alert" style={{ color: "red" }}>
|
|
59
|
+
{error}
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
<button type="submit" disabled={isLoading}>
|
|
63
|
+
{isLoading ? "Logging in..." : "Login"}
|
|
64
|
+
</button>
|
|
65
|
+
</form>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React, { useState, FormEvent } from "react";
|
|
2
|
+
import { useAuth } from "../hooks/useAuth";
|
|
3
|
+
|
|
4
|
+
interface OTPVerificationProps {
|
|
5
|
+
onSuccess?: () => void;
|
|
6
|
+
onError?: (error: string) => void;
|
|
7
|
+
className?: string;
|
|
8
|
+
style?: React.CSSProperties;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const OTPVerification: React.FC<OTPVerificationProps> = ({
|
|
12
|
+
onSuccess,
|
|
13
|
+
onError,
|
|
14
|
+
className,
|
|
15
|
+
style,
|
|
16
|
+
}) => {
|
|
17
|
+
const { requestOtp, verifyOtp, isLoading, error } = useAuth();
|
|
18
|
+
const [email, setEmail] = useState("");
|
|
19
|
+
const [code, setCode] = useState("");
|
|
20
|
+
const [otpSent, setOtpSent] = useState(false);
|
|
21
|
+
|
|
22
|
+
const handleRequestOtp = async (e: FormEvent) => {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
try {
|
|
25
|
+
await requestOtp(email);
|
|
26
|
+
setOtpSent(true);
|
|
27
|
+
} catch (err: any) {
|
|
28
|
+
onError?.(err.message);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const handleVerifyOtp = async (e: FormEvent) => {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
try {
|
|
35
|
+
await verifyOtp(email, code);
|
|
36
|
+
onSuccess?.();
|
|
37
|
+
} catch (err: any) {
|
|
38
|
+
onError?.(err.message);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (!otpSent) {
|
|
43
|
+
return (
|
|
44
|
+
<form onSubmit={handleRequestOtp} className={className} style={style}>
|
|
45
|
+
<div>
|
|
46
|
+
<label htmlFor="sp-otp-email">Email</label>
|
|
47
|
+
<input
|
|
48
|
+
id="sp-otp-email"
|
|
49
|
+
type="email"
|
|
50
|
+
value={email}
|
|
51
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
52
|
+
placeholder="Enter your email"
|
|
53
|
+
required
|
|
54
|
+
disabled={isLoading}
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
{error && (
|
|
58
|
+
<div role="alert" style={{ color: "red" }}>
|
|
59
|
+
{error}
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
<button type="submit" disabled={isLoading}>
|
|
63
|
+
{isLoading ? "Sending OTP..." : "Send OTP"}
|
|
64
|
+
</button>
|
|
65
|
+
</form>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<form onSubmit={handleVerifyOtp} className={className} style={style}>
|
|
71
|
+
<div>
|
|
72
|
+
<label htmlFor="sp-otp-code">Enter OTP</label>
|
|
73
|
+
<input
|
|
74
|
+
id="sp-otp-code"
|
|
75
|
+
type="text"
|
|
76
|
+
value={code}
|
|
77
|
+
onChange={(e) => setCode(e.target.value)}
|
|
78
|
+
placeholder="Enter 6-digit OTP"
|
|
79
|
+
maxLength={6}
|
|
80
|
+
required
|
|
81
|
+
disabled={isLoading}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
{error && (
|
|
85
|
+
<div role="alert" style={{ color: "red" }}>
|
|
86
|
+
{error}
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
<button type="submit" disabled={isLoading}>
|
|
90
|
+
{isLoading ? "Verifying..." : "Verify OTP"}
|
|
91
|
+
</button>
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
onClick={() => {
|
|
95
|
+
setOtpSent(false);
|
|
96
|
+
setCode("");
|
|
97
|
+
}}
|
|
98
|
+
disabled={isLoading}
|
|
99
|
+
>
|
|
100
|
+
Back
|
|
101
|
+
</button>
|
|
102
|
+
</form>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
import { useAuth } from "../hooks/useAuth";
|
|
3
|
+
|
|
4
|
+
interface SessionListProps {
|
|
5
|
+
className?: string;
|
|
6
|
+
style?: React.CSSProperties;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const SessionList: React.FC<SessionListProps> = ({
|
|
10
|
+
className,
|
|
11
|
+
style,
|
|
12
|
+
}) => {
|
|
13
|
+
const {
|
|
14
|
+
sessions,
|
|
15
|
+
fetchSessions,
|
|
16
|
+
revokeSession,
|
|
17
|
+
revokeAllSessions,
|
|
18
|
+
isLoading,
|
|
19
|
+
} = useAuth();
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
fetchSessions();
|
|
23
|
+
}, [fetchSessions]);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className={className} style={style}>
|
|
27
|
+
<h3>Active Sessions</h3>
|
|
28
|
+
{sessions.length === 0 && <p>No active sessions</p>}
|
|
29
|
+
<ul style={{ listStyle: "none", padding: 0 }}>
|
|
30
|
+
{sessions.map((session) => (
|
|
31
|
+
<li
|
|
32
|
+
key={session.id}
|
|
33
|
+
style={{
|
|
34
|
+
display: "flex",
|
|
35
|
+
justifyContent: "space-between",
|
|
36
|
+
alignItems: "center",
|
|
37
|
+
padding: "8px 0",
|
|
38
|
+
borderBottom: "1px solid #eee",
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<div>
|
|
42
|
+
<strong>{session.device}</strong>
|
|
43
|
+
<br />
|
|
44
|
+
<small>
|
|
45
|
+
IP: {session.ip} | {new Date(session.createdAt).toLocaleString()}
|
|
46
|
+
</small>
|
|
47
|
+
</div>
|
|
48
|
+
<button
|
|
49
|
+
onClick={() => revokeSession(session.id)}
|
|
50
|
+
disabled={isLoading}
|
|
51
|
+
>
|
|
52
|
+
Revoke
|
|
53
|
+
</button>
|
|
54
|
+
</li>
|
|
55
|
+
))}
|
|
56
|
+
</ul>
|
|
57
|
+
{sessions.length > 0 && (
|
|
58
|
+
<button onClick={revokeAllSessions} disabled={isLoading}>
|
|
59
|
+
Revoke All Sessions
|
|
60
|
+
</button>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React, { useState, FormEvent } from "react";
|
|
2
|
+
import { useAuth } from "../hooks/useAuth";
|
|
3
|
+
|
|
4
|
+
interface SignupFormProps {
|
|
5
|
+
onSuccess?: () => void;
|
|
6
|
+
onError?: (error: string) => void;
|
|
7
|
+
className?: string;
|
|
8
|
+
style?: React.CSSProperties;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const SignupForm: React.FC<SignupFormProps> = ({
|
|
12
|
+
onSuccess,
|
|
13
|
+
onError,
|
|
14
|
+
className,
|
|
15
|
+
style,
|
|
16
|
+
}) => {
|
|
17
|
+
const { register, isLoading, error } = useAuth();
|
|
18
|
+
const [email, setEmail] = useState("");
|
|
19
|
+
const [password, setPassword] = useState("");
|
|
20
|
+
const [confirmPassword, setConfirmPassword] = useState("");
|
|
21
|
+
const [localError, setLocalError] = useState<string | null>(null);
|
|
22
|
+
|
|
23
|
+
const handleSubmit = async (e: FormEvent) => {
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
setLocalError(null);
|
|
26
|
+
|
|
27
|
+
if (password !== confirmPassword) {
|
|
28
|
+
setLocalError("Passwords do not match");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (password.length < 8) {
|
|
33
|
+
setLocalError("Password must be at least 8 characters");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
await register(email, password);
|
|
39
|
+
onSuccess?.();
|
|
40
|
+
} catch (err: any) {
|
|
41
|
+
onError?.(err.message);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const displayError = localError || error;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<form onSubmit={handleSubmit} className={className} style={style}>
|
|
49
|
+
<div>
|
|
50
|
+
<label htmlFor="sp-signup-email">Email</label>
|
|
51
|
+
<input
|
|
52
|
+
id="sp-signup-email"
|
|
53
|
+
type="email"
|
|
54
|
+
value={email}
|
|
55
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
56
|
+
placeholder="Enter your email"
|
|
57
|
+
required
|
|
58
|
+
disabled={isLoading}
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
<div>
|
|
62
|
+
<label htmlFor="sp-signup-password">Password</label>
|
|
63
|
+
<input
|
|
64
|
+
id="sp-signup-password"
|
|
65
|
+
type="password"
|
|
66
|
+
value={password}
|
|
67
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
68
|
+
placeholder="Create a password"
|
|
69
|
+
required
|
|
70
|
+
disabled={isLoading}
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
<div>
|
|
74
|
+
<label htmlFor="sp-signup-confirm">Confirm Password</label>
|
|
75
|
+
<input
|
|
76
|
+
id="sp-signup-confirm"
|
|
77
|
+
type="password"
|
|
78
|
+
value={confirmPassword}
|
|
79
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
80
|
+
placeholder="Confirm your password"
|
|
81
|
+
required
|
|
82
|
+
disabled={isLoading}
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
{displayError && (
|
|
86
|
+
<div role="alert" style={{ color: "red" }}>
|
|
87
|
+
{displayError}
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
<button type="submit" disabled={isLoading}>
|
|
91
|
+
{isLoading ? "Creating account..." : "Sign Up"}
|
|
92
|
+
</button>
|
|
93
|
+
</form>
|
|
94
|
+
);
|
|
95
|
+
};
|