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,158 @@
|
|
|
1
|
+
import { useState, useEffect, type FormEvent } from "react";
|
|
2
|
+
import { useAuth } from "@securepool/react-sdk";
|
|
3
|
+
import { useNavigate, Link } from "react-router-dom";
|
|
4
|
+
|
|
5
|
+
function getInitials(email: string): string {
|
|
6
|
+
return email.split("@")[0].slice(0, 2).toUpperCase();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function getAvatarUrl(email: string): string {
|
|
10
|
+
const initials = getInitials(email);
|
|
11
|
+
return `https://ui-avatars.com/api/?name=${encodeURIComponent(initials)}&size=80&background=3b82f6&color=fff&bold=true&format=svg`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function LoginPage() {
|
|
15
|
+
const { login, accounts, switchAccount, isLoading, error, clearError } = useAuth();
|
|
16
|
+
const navigate = useNavigate();
|
|
17
|
+
const [email, setEmail] = useState("");
|
|
18
|
+
|
|
19
|
+
useEffect(() => { clearError(); }, [clearError]);
|
|
20
|
+
const [password, setPassword] = useState("");
|
|
21
|
+
|
|
22
|
+
const handleSubmit = async (e: FormEvent) => {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
try {
|
|
25
|
+
await login(email, password);
|
|
26
|
+
navigate("/dashboard");
|
|
27
|
+
} catch {
|
|
28
|
+
// error is set in context
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const handleSwitchTo = (accountId: string) => {
|
|
33
|
+
switchAccount(accountId);
|
|
34
|
+
navigate("/dashboard");
|
|
35
|
+
window.location.reload();
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="auth-layout">
|
|
40
|
+
<div className="auth-card">
|
|
41
|
+
<h1>Welcome back</h1>
|
|
42
|
+
<p className="subtitle">Sign in to your SecurePool account</p>
|
|
43
|
+
|
|
44
|
+
{/* Show existing logged-in accounts */}
|
|
45
|
+
{accounts.length > 0 && (
|
|
46
|
+
<>
|
|
47
|
+
<div style={{ marginBottom: 16 }}>
|
|
48
|
+
<div style={{ fontSize: 12, color: "#64748b", marginBottom: 8, textTransform: "uppercase", letterSpacing: 0.5 }}>
|
|
49
|
+
Switch to an existing account
|
|
50
|
+
</div>
|
|
51
|
+
{accounts.map((account) => (
|
|
52
|
+
<div
|
|
53
|
+
key={account.id}
|
|
54
|
+
onClick={() => handleSwitchTo(account.id)}
|
|
55
|
+
style={{
|
|
56
|
+
display: "flex",
|
|
57
|
+
alignItems: "center",
|
|
58
|
+
gap: 12,
|
|
59
|
+
padding: "10px 12px",
|
|
60
|
+
background: "rgba(59,130,246,0.06)",
|
|
61
|
+
border: "1px solid #334155",
|
|
62
|
+
borderRadius: 10,
|
|
63
|
+
cursor: "pointer",
|
|
64
|
+
marginBottom: 6,
|
|
65
|
+
transition: "all 0.15s",
|
|
66
|
+
}}
|
|
67
|
+
onMouseEnter={(e) => {
|
|
68
|
+
e.currentTarget.style.background = "rgba(59,130,246,0.15)";
|
|
69
|
+
e.currentTarget.style.borderColor = "#3b82f6";
|
|
70
|
+
}}
|
|
71
|
+
onMouseLeave={(e) => {
|
|
72
|
+
e.currentTarget.style.background = "rgba(59,130,246,0.06)";
|
|
73
|
+
e.currentTarget.style.borderColor = "#334155";
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
<img
|
|
77
|
+
src={getAvatarUrl(account.email)}
|
|
78
|
+
alt=""
|
|
79
|
+
style={{ width: 36, height: 36, borderRadius: "50%", flexShrink: 0 }}
|
|
80
|
+
/>
|
|
81
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
82
|
+
<div style={{
|
|
83
|
+
fontSize: 14,
|
|
84
|
+
color: "#e2e8f0",
|
|
85
|
+
fontWeight: 500,
|
|
86
|
+
overflow: "hidden",
|
|
87
|
+
textOverflow: "ellipsis",
|
|
88
|
+
whiteSpace: "nowrap",
|
|
89
|
+
}}>
|
|
90
|
+
{account.email.trim().toLowerCase()}
|
|
91
|
+
</div>
|
|
92
|
+
<div style={{ fontSize: 11, color: "#64748b" }}>Click to open dashboard</div>
|
|
93
|
+
</div>
|
|
94
|
+
<span style={{
|
|
95
|
+
fontSize: 18,
|
|
96
|
+
color: "#3b82f6",
|
|
97
|
+
}}>
|
|
98
|
+
→
|
|
99
|
+
</span>
|
|
100
|
+
</div>
|
|
101
|
+
))}
|
|
102
|
+
</div>
|
|
103
|
+
<div className="divider"><span>or sign in with another account</span></div>
|
|
104
|
+
</>
|
|
105
|
+
)}
|
|
106
|
+
|
|
107
|
+
{error && <div className="error-msg">{error}</div>}
|
|
108
|
+
|
|
109
|
+
<form onSubmit={handleSubmit}>
|
|
110
|
+
<div className="form-group">
|
|
111
|
+
<label htmlFor="email">Email</label>
|
|
112
|
+
<input
|
|
113
|
+
id="email"
|
|
114
|
+
type="email"
|
|
115
|
+
value={email}
|
|
116
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
117
|
+
placeholder="you@example.com"
|
|
118
|
+
required
|
|
119
|
+
disabled={isLoading}
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
<div className="form-group">
|
|
123
|
+
<label htmlFor="password">Password</label>
|
|
124
|
+
<input
|
|
125
|
+
id="password"
|
|
126
|
+
type="password"
|
|
127
|
+
value={password}
|
|
128
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
129
|
+
placeholder="Enter your password"
|
|
130
|
+
required
|
|
131
|
+
disabled={isLoading}
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
<div style={{ textAlign: "right", marginBottom: 8 }}>
|
|
135
|
+
<Link to="/forgot-password" style={{ fontSize: 13, color: "#60a5fa" }}>
|
|
136
|
+
Forgot password?
|
|
137
|
+
</Link>
|
|
138
|
+
</div>
|
|
139
|
+
<button type="submit" className="btn btn-primary" disabled={isLoading}>
|
|
140
|
+
{isLoading ? "Signing in..." : "Sign In"}
|
|
141
|
+
</button>
|
|
142
|
+
</form>
|
|
143
|
+
|
|
144
|
+
<div className="divider"><span>or</span></div>
|
|
145
|
+
|
|
146
|
+
<Link to="/otp-login">
|
|
147
|
+
<button type="button" className="btn btn-outline">
|
|
148
|
+
Sign in with OTP
|
|
149
|
+
</button>
|
|
150
|
+
</Link>
|
|
151
|
+
|
|
152
|
+
<div className="auth-links">
|
|
153
|
+
Don't have an account? <Link to="/signup">Sign up</Link>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useState, useEffect, type FormEvent } from "react";
|
|
2
|
+
import { useAuth } from "@securepool/react-sdk";
|
|
3
|
+
import { useNavigate, Link } from "react-router-dom";
|
|
4
|
+
|
|
5
|
+
export default function OtpLoginPage() {
|
|
6
|
+
const { requestOtp, verifyOtp, isLoading, error, clearError } = useAuth();
|
|
7
|
+
const navigate = useNavigate();
|
|
8
|
+
|
|
9
|
+
useEffect(() => { clearError(); }, [clearError]);
|
|
10
|
+
const [email, setEmail] = useState("");
|
|
11
|
+
const [code, setCode] = useState("");
|
|
12
|
+
const [otpSent, setOtpSent] = useState(false);
|
|
13
|
+
|
|
14
|
+
const handleRequestOtp = async (e: FormEvent) => {
|
|
15
|
+
e.preventDefault();
|
|
16
|
+
try {
|
|
17
|
+
await requestOtp(email);
|
|
18
|
+
setOtpSent(true);
|
|
19
|
+
} catch {
|
|
20
|
+
// error set in context
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const handleVerifyOtp = async (e: FormEvent) => {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
try {
|
|
27
|
+
await verifyOtp(email, code);
|
|
28
|
+
navigate("/dashboard");
|
|
29
|
+
} catch {
|
|
30
|
+
// error set in context
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (!otpSent) {
|
|
35
|
+
return (
|
|
36
|
+
<div className="auth-layout">
|
|
37
|
+
<div className="auth-card">
|
|
38
|
+
<h1>OTP Login</h1>
|
|
39
|
+
<p className="subtitle">We'll send a 6-digit code to your email</p>
|
|
40
|
+
|
|
41
|
+
{error && <div className="error-msg">{error}</div>}
|
|
42
|
+
|
|
43
|
+
<form onSubmit={handleRequestOtp}>
|
|
44
|
+
<div className="form-group">
|
|
45
|
+
<label htmlFor="email">Email</label>
|
|
46
|
+
<input
|
|
47
|
+
id="email"
|
|
48
|
+
type="email"
|
|
49
|
+
value={email}
|
|
50
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
51
|
+
placeholder="you@example.com"
|
|
52
|
+
required
|
|
53
|
+
disabled={isLoading}
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
<button type="submit" className="btn btn-primary" disabled={isLoading}>
|
|
57
|
+
{isLoading ? "Sending..." : "Send OTP"}
|
|
58
|
+
</button>
|
|
59
|
+
</form>
|
|
60
|
+
|
|
61
|
+
<div className="auth-links">
|
|
62
|
+
<Link to="/login">Back to password login</Link>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="auth-layout">
|
|
71
|
+
<div className="auth-card">
|
|
72
|
+
<h1>Enter OTP</h1>
|
|
73
|
+
<p className="subtitle">
|
|
74
|
+
Code sent to <strong>{email}</strong>. Check your inbox.
|
|
75
|
+
</p>
|
|
76
|
+
|
|
77
|
+
{error && <div className="error-msg">{error}</div>}
|
|
78
|
+
|
|
79
|
+
<form onSubmit={handleVerifyOtp}>
|
|
80
|
+
<div className="form-group">
|
|
81
|
+
<label htmlFor="otp">6-digit Code</label>
|
|
82
|
+
<input
|
|
83
|
+
id="otp"
|
|
84
|
+
type="text"
|
|
85
|
+
className="otp-input"
|
|
86
|
+
value={code}
|
|
87
|
+
onChange={(e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
|
|
88
|
+
placeholder="000000"
|
|
89
|
+
maxLength={6}
|
|
90
|
+
required
|
|
91
|
+
disabled={isLoading}
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
<button type="submit" className="btn btn-primary" disabled={isLoading || code.length !== 6}>
|
|
95
|
+
{isLoading ? "Verifying..." : "Verify & Sign In"}
|
|
96
|
+
</button>
|
|
97
|
+
</form>
|
|
98
|
+
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
className="btn btn-outline"
|
|
102
|
+
style={{ marginTop: 12 }}
|
|
103
|
+
onClick={() => {
|
|
104
|
+
setOtpSent(false);
|
|
105
|
+
setCode("");
|
|
106
|
+
}}
|
|
107
|
+
disabled={isLoading}
|
|
108
|
+
>
|
|
109
|
+
Use different email
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useState, useEffect, type FormEvent } from "react";
|
|
2
|
+
import { useAuth } from "@securepool/react-sdk";
|
|
3
|
+
import { useNavigate, Link } from "react-router-dom";
|
|
4
|
+
|
|
5
|
+
export default function SignupPage() {
|
|
6
|
+
const { register, isLoading, error, clearError } = useAuth();
|
|
7
|
+
const navigate = useNavigate();
|
|
8
|
+
|
|
9
|
+
useEffect(() => { clearError(); }, [clearError]);
|
|
10
|
+
const [email, setEmail] = useState("");
|
|
11
|
+
const [password, setPassword] = useState("");
|
|
12
|
+
const [confirmPassword, setConfirmPassword] = useState("");
|
|
13
|
+
const [localError, setLocalError] = useState<string | null>(null);
|
|
14
|
+
|
|
15
|
+
const handleSubmit = async (e: FormEvent) => {
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
setLocalError(null);
|
|
18
|
+
|
|
19
|
+
if (password !== confirmPassword) {
|
|
20
|
+
setLocalError("Passwords do not match");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (password.length < 8) {
|
|
24
|
+
setLocalError("Password must be at least 8 characters");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
await register(email, password);
|
|
30
|
+
// Redirect to verify email page with the email
|
|
31
|
+
navigate("/verify-email", { state: { email } });
|
|
32
|
+
} catch {
|
|
33
|
+
// error in context
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const displayError = localError || error;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="auth-layout">
|
|
41
|
+
<div className="auth-card">
|
|
42
|
+
<h1>Create account</h1>
|
|
43
|
+
<p className="subtitle">Get started with SecurePool</p>
|
|
44
|
+
|
|
45
|
+
{displayError && <div className="error-msg">{displayError}</div>}
|
|
46
|
+
|
|
47
|
+
<form onSubmit={handleSubmit}>
|
|
48
|
+
<div className="form-group">
|
|
49
|
+
<label htmlFor="email">Email</label>
|
|
50
|
+
<input
|
|
51
|
+
id="email"
|
|
52
|
+
type="email"
|
|
53
|
+
value={email}
|
|
54
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
55
|
+
placeholder="you@example.com"
|
|
56
|
+
required
|
|
57
|
+
disabled={isLoading}
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
<div className="form-group">
|
|
61
|
+
<label htmlFor="password">Password</label>
|
|
62
|
+
<input
|
|
63
|
+
id="password"
|
|
64
|
+
type="password"
|
|
65
|
+
value={password}
|
|
66
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
67
|
+
placeholder="Min 8 characters"
|
|
68
|
+
required
|
|
69
|
+
disabled={isLoading}
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
<div className="form-group">
|
|
73
|
+
<label htmlFor="confirm">Confirm Password</label>
|
|
74
|
+
<input
|
|
75
|
+
id="confirm"
|
|
76
|
+
type="password"
|
|
77
|
+
value={confirmPassword}
|
|
78
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
79
|
+
placeholder="Re-enter your password"
|
|
80
|
+
required
|
|
81
|
+
disabled={isLoading}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
<button type="submit" className="btn btn-primary" disabled={isLoading}>
|
|
85
|
+
{isLoading ? "Creating account..." : "Sign Up"}
|
|
86
|
+
</button>
|
|
87
|
+
</form>
|
|
88
|
+
|
|
89
|
+
<div className="auth-links">
|
|
90
|
+
Already have an account? <Link to="/login">Sign in</Link>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useState, useEffect, type FormEvent } from "react";
|
|
2
|
+
import { useAuth } from "@securepool/react-sdk";
|
|
3
|
+
import { useNavigate, useLocation, Link } from "react-router-dom";
|
|
4
|
+
|
|
5
|
+
export default function VerifyEmailPage() {
|
|
6
|
+
const { verifyEmail, requestOtp, isLoading, error, clearError } = useAuth();
|
|
7
|
+
const navigate = useNavigate();
|
|
8
|
+
|
|
9
|
+
useEffect(() => { clearError(); }, [clearError]);
|
|
10
|
+
const location = useLocation();
|
|
11
|
+
const passedEmail = (location.state as any)?.email || "";
|
|
12
|
+
|
|
13
|
+
const [email] = useState(passedEmail);
|
|
14
|
+
const [code, setCode] = useState("");
|
|
15
|
+
const [resent, setResent] = useState(false);
|
|
16
|
+
|
|
17
|
+
const handleVerify = async (e: FormEvent) => {
|
|
18
|
+
e.preventDefault();
|
|
19
|
+
try {
|
|
20
|
+
await verifyEmail(email, code);
|
|
21
|
+
navigate("/dashboard");
|
|
22
|
+
} catch {
|
|
23
|
+
// error in context
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const handleResend = async () => {
|
|
28
|
+
try {
|
|
29
|
+
await requestOtp(email);
|
|
30
|
+
setResent(true);
|
|
31
|
+
setTimeout(() => setResent(false), 3000);
|
|
32
|
+
} catch {
|
|
33
|
+
// error in context
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="auth-layout">
|
|
39
|
+
<div className="auth-card">
|
|
40
|
+
<h1>Verify your email</h1>
|
|
41
|
+
<p className="subtitle">
|
|
42
|
+
We sent a 6-digit code to <strong>{email}</strong>
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
{resent && <div className="success-msg">OTP resent to your email</div>}
|
|
46
|
+
{error && <div className="error-msg">{error}</div>}
|
|
47
|
+
|
|
48
|
+
<form onSubmit={handleVerify}>
|
|
49
|
+
<div className="form-group">
|
|
50
|
+
<label htmlFor="otp">Verification Code</label>
|
|
51
|
+
<input
|
|
52
|
+
id="otp"
|
|
53
|
+
type="text"
|
|
54
|
+
className="otp-input"
|
|
55
|
+
value={code}
|
|
56
|
+
onChange={(e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
|
|
57
|
+
placeholder="000000"
|
|
58
|
+
maxLength={6}
|
|
59
|
+
required
|
|
60
|
+
disabled={isLoading}
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
<button type="submit" className="btn btn-primary" disabled={isLoading || code.length !== 6}>
|
|
64
|
+
{isLoading ? "Verifying..." : "Verify & Continue"}
|
|
65
|
+
</button>
|
|
66
|
+
</form>
|
|
67
|
+
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
className="btn btn-outline"
|
|
71
|
+
style={{ marginTop: 12 }}
|
|
72
|
+
onClick={handleResend}
|
|
73
|
+
disabled={isLoading}
|
|
74
|
+
>
|
|
75
|
+
Resend OTP
|
|
76
|
+
</button>
|
|
77
|
+
|
|
78
|
+
<div className="auth-links">
|
|
79
|
+
<Link to="/login">Back to login</Link>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2023",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"types": ["vite/client"],
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
|
|
11
|
+
/* Bundler mode */
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"moduleDetection": "force",
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"jsx": "react-jsx",
|
|
18
|
+
|
|
19
|
+
/* Linting */
|
|
20
|
+
"strict": true,
|
|
21
|
+
"noUnusedLocals": true,
|
|
22
|
+
"noUnusedParameters": true,
|
|
23
|
+
"erasableSyntaxOnly": true,
|
|
24
|
+
"noFallthroughCasesInSwitch": true,
|
|
25
|
+
"noUncheckedSideEffectImports": true
|
|
26
|
+
},
|
|
27
|
+
"include": ["src"]
|
|
28
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "ES2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"erasableSyntaxOnly": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noUncheckedSideEffectImports": true
|
|
24
|
+
},
|
|
25
|
+
"include": ["vite.config.ts"]
|
|
26
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
// https://vite.dev/config/
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [react()],
|
|
8
|
+
resolve: {
|
|
9
|
+
dedupe: ['react', 'react-dom'],
|
|
10
|
+
alias: {
|
|
11
|
+
react: path.resolve(__dirname, 'node_modules/react'),
|
|
12
|
+
'react-dom': path.resolve(__dirname, 'node_modules/react-dom'),
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
})
|