securepool 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/.dockerignore +7 -0
  2. package/.env.example +20 -0
  3. package/ARCHITECTURE.md +279 -0
  4. package/DEPLOYMENT.md +441 -0
  5. package/README.md +283 -0
  6. package/SETUP.md +388 -0
  7. package/apps/demo-backend/Dockerfile +33 -0
  8. package/apps/demo-backend/package.json +19 -0
  9. package/apps/demo-backend/src/index.ts +71 -0
  10. package/apps/demo-backend/tsconfig.json +8 -0
  11. package/apps/demo-frontend/.env.example +2 -0
  12. package/apps/demo-frontend/README.md +73 -0
  13. package/apps/demo-frontend/eslint.config.js +23 -0
  14. package/apps/demo-frontend/index.html +13 -0
  15. package/apps/demo-frontend/package.json +24 -0
  16. package/apps/demo-frontend/public/favicon.svg +1 -0
  17. package/apps/demo-frontend/public/icons.svg +24 -0
  18. package/apps/demo-frontend/src/App.tsx +33 -0
  19. package/apps/demo-frontend/src/assets/hero.png +0 -0
  20. package/apps/demo-frontend/src/assets/vite.svg +1 -0
  21. package/apps/demo-frontend/src/components/AccountSwitcher.tsx +373 -0
  22. package/apps/demo-frontend/src/components/ChangePasswordModal.tsx +128 -0
  23. package/apps/demo-frontend/src/index.css +272 -0
  24. package/apps/demo-frontend/src/main.tsx +10 -0
  25. package/apps/demo-frontend/src/pages/DashboardPage.tsx +141 -0
  26. package/apps/demo-frontend/src/pages/ForgotPasswordPage.tsx +183 -0
  27. package/apps/demo-frontend/src/pages/LoginPage.tsx +158 -0
  28. package/apps/demo-frontend/src/pages/OtpLoginPage.tsx +114 -0
  29. package/apps/demo-frontend/src/pages/SignupPage.tsx +95 -0
  30. package/apps/demo-frontend/src/pages/VerifyEmailPage.tsx +84 -0
  31. package/apps/demo-frontend/tsconfig.app.json +28 -0
  32. package/apps/demo-frontend/tsconfig.json +7 -0
  33. package/apps/demo-frontend/tsconfig.node.json +26 -0
  34. package/apps/demo-frontend/vite.config.ts +15 -0
  35. package/docs/DATABASE_MONGODB.md +280 -0
  36. package/docs/DATABASE_SQL.md +472 -0
  37. package/package.json +21 -0
  38. package/packages/api/package.json +30 -0
  39. package/packages/api/src/createSecurePool.ts +113 -0
  40. package/packages/api/src/index.ts +8 -0
  41. package/packages/api/src/middleware/authMiddleware.ts +26 -0
  42. package/packages/api/src/middleware/authorize.ts +24 -0
  43. package/packages/api/src/middleware/rateLimiter.ts +25 -0
  44. package/packages/api/src/middleware/tenantMiddleware.ts +12 -0
  45. package/packages/api/src/routes/authRoutes.ts +229 -0
  46. package/packages/api/src/routes/sessionRoutes.ts +30 -0
  47. package/packages/api/src/swagger.ts +529 -0
  48. package/packages/api/tsconfig.json +8 -0
  49. package/packages/application/package.json +16 -0
  50. package/packages/application/src/index.ts +17 -0
  51. package/packages/application/src/interfaces/IAuditLogRepository.ts +6 -0
  52. package/packages/application/src/interfaces/IAuthPlugin.ts +4 -0
  53. package/packages/application/src/interfaces/IEmailService.ts +3 -0
  54. package/packages/application/src/interfaces/IGoogleAuthService.ts +3 -0
  55. package/packages/application/src/interfaces/IOtpRepository.ts +8 -0
  56. package/packages/application/src/interfaces/IOtpService.ts +4 -0
  57. package/packages/application/src/interfaces/IPasswordHasher.ts +4 -0
  58. package/packages/application/src/interfaces/IRoleRepository.ts +8 -0
  59. package/packages/application/src/interfaces/ISessionRepository.ts +8 -0
  60. package/packages/application/src/interfaces/ITokenRepository.ts +9 -0
  61. package/packages/application/src/interfaces/ITokenService.ts +5 -0
  62. package/packages/application/src/interfaces/IUserRepository.ts +8 -0
  63. package/packages/application/src/services/AuthService.ts +323 -0
  64. package/packages/application/src/services/RefreshTokenService.ts +53 -0
  65. package/packages/application/tsconfig.json +8 -0
  66. package/packages/core/package.json +13 -0
  67. package/packages/core/src/entities/AuditLog.ts +11 -0
  68. package/packages/core/src/entities/OtpCode.ts +10 -0
  69. package/packages/core/src/entities/RefreshToken.ts +9 -0
  70. package/packages/core/src/entities/Role.ts +6 -0
  71. package/packages/core/src/entities/Session.ts +10 -0
  72. package/packages/core/src/entities/Tenant.ts +7 -0
  73. package/packages/core/src/entities/User.ts +10 -0
  74. package/packages/core/src/entities/UserRole.ts +6 -0
  75. package/packages/core/src/enums/index.ts +22 -0
  76. package/packages/core/src/index.ts +10 -0
  77. package/packages/core/tsconfig.json +8 -0
  78. package/packages/infrastructure/package.json +24 -0
  79. package/packages/infrastructure/src/email/NodemailerEmailService.ts +55 -0
  80. package/packages/infrastructure/src/google/GoogleAuthServiceImpl.ts +28 -0
  81. package/packages/infrastructure/src/hashing/BcryptHasher.ts +18 -0
  82. package/packages/infrastructure/src/index.ts +6 -0
  83. package/packages/infrastructure/src/jwt/JwtTokenService.ts +32 -0
  84. package/packages/infrastructure/src/otp/OtpServiceImpl.ts +50 -0
  85. package/packages/infrastructure/tsconfig.json +8 -0
  86. package/packages/persistence/package.json +22 -0
  87. package/packages/persistence/prisma/schema.prisma +88 -0
  88. package/packages/persistence/src/factory.ts +48 -0
  89. package/packages/persistence/src/index.ts +30 -0
  90. package/packages/persistence/src/mongo/connection.ts +9 -0
  91. package/packages/persistence/src/mongo/models/AuditLogModel.ts +21 -0
  92. package/packages/persistence/src/mongo/models/OtpModel.ts +19 -0
  93. package/packages/persistence/src/mongo/models/RefreshTokenModel.ts +17 -0
  94. package/packages/persistence/src/mongo/models/RoleModel.ts +11 -0
  95. package/packages/persistence/src/mongo/models/SessionModel.ts +19 -0
  96. package/packages/persistence/src/mongo/models/UserModel.ts +21 -0
  97. package/packages/persistence/src/mongo/models/UserRoleModel.ts +15 -0
  98. package/packages/persistence/src/mongo/repositories/MongoAuditLogRepository.ts +29 -0
  99. package/packages/persistence/src/mongo/repositories/MongoOtpRepository.ts +34 -0
  100. package/packages/persistence/src/mongo/repositories/MongoRoleRepository.ts +32 -0
  101. package/packages/persistence/src/mongo/repositories/MongoSessionRepository.ts +29 -0
  102. package/packages/persistence/src/mongo/repositories/MongoTokenRepository.ts +34 -0
  103. package/packages/persistence/src/mongo/repositories/MongoUserRepository.ts +37 -0
  104. package/packages/persistence/src/prisma/repositories/PrismaAuditLogRepository.ts +37 -0
  105. package/packages/persistence/src/prisma/repositories/PrismaOtpRepository.ts +43 -0
  106. package/packages/persistence/src/prisma/repositories/PrismaRoleRepository.ts +36 -0
  107. package/packages/persistence/src/prisma/repositories/PrismaSessionRepository.ts +39 -0
  108. package/packages/persistence/src/prisma/repositories/PrismaTokenRepository.ts +50 -0
  109. package/packages/persistence/src/prisma/repositories/PrismaUserRepository.ts +45 -0
  110. package/packages/persistence/tsconfig.json +8 -0
  111. package/packages/react-sdk/package.json +23 -0
  112. package/packages/react-sdk/src/components/GoogleLoginButton.tsx +54 -0
  113. package/packages/react-sdk/src/components/LoginForm.tsx +67 -0
  114. package/packages/react-sdk/src/components/OTPVerification.tsx +104 -0
  115. package/packages/react-sdk/src/components/SessionList.tsx +64 -0
  116. package/packages/react-sdk/src/components/SignupForm.tsx +95 -0
  117. package/packages/react-sdk/src/context/AuthContext.ts +4 -0
  118. package/packages/react-sdk/src/context/SecurePoolProvider.tsx +492 -0
  119. package/packages/react-sdk/src/hooks/useAuth.ts +11 -0
  120. package/packages/react-sdk/src/index.ts +22 -0
  121. package/packages/react-sdk/src/types.ts +53 -0
  122. package/packages/react-sdk/tsconfig.json +12 -0
  123. package/scripts/setup.js +285 -0
  124. package/scripts/setup.sh +309 -0
  125. package/tsconfig.base.json +16 -0
  126. package/turbo.json +16 -0
@@ -0,0 +1,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,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist"
6
+ },
7
+ "include": ["src"]
8
+ }
@@ -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
+ };
@@ -0,0 +1,4 @@
1
+ import { createContext } from "react";
2
+ import { AuthContextValue } from "../types";
3
+
4
+ export const AuthContext = createContext<AuthContextValue | null>(null);