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
package/SETUP.md
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
# SecurePool - Local Setup Guide
|
|
2
|
+
|
|
3
|
+
## Quick Start (One Command)
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
cd securepool
|
|
7
|
+
npm run setup
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
This interactive script handles everything:
|
|
11
|
+
- Checks prerequisites (Node.js, MongoDB, OpenSSL)
|
|
12
|
+
- Installs dependencies
|
|
13
|
+
- Generates RSA keys for JWT
|
|
14
|
+
- Sets up MongoDB (with or without authentication)
|
|
15
|
+
- Configures email for OTP (optional)
|
|
16
|
+
- Creates `.env` file
|
|
17
|
+
- Builds all packages
|
|
18
|
+
- Verifies the setup
|
|
19
|
+
|
|
20
|
+
After setup, start the servers:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Terminal 1 - Backend
|
|
24
|
+
npm run start:backend
|
|
25
|
+
|
|
26
|
+
# Terminal 2 - Frontend
|
|
27
|
+
npm run start:frontend
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Open:
|
|
31
|
+
- Frontend: http://localhost:5173
|
|
32
|
+
- API Docs: http://localhost:5001/docs
|
|
33
|
+
- Health Check: http://localhost:5001/health
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Manual Setup (Step by Step)
|
|
38
|
+
|
|
39
|
+
If you prefer manual setup, follow the steps below.
|
|
40
|
+
|
|
41
|
+
### Prerequisites
|
|
42
|
+
|
|
43
|
+
| Tool | Version | Install |
|
|
44
|
+
|------|---------|---------|
|
|
45
|
+
| Node.js | 20+ | `brew install node` |
|
|
46
|
+
| MongoDB | 6+ | `brew install mongodb-community` |
|
|
47
|
+
| OpenSSL | any | Pre-installed on macOS |
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
### Step 1: Clone & Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
cd securepool
|
|
55
|
+
npm install
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Step 2: Start MongoDB
|
|
61
|
+
|
|
62
|
+
**Option A: Without authentication (simple)**
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
mongod --dbpath ~/mongodb-data
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Option B: With authentication (production-like)**
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Start MongoDB with auth
|
|
72
|
+
mongod --dbpath ~/mongodb-data --auth
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Then in a new terminal, create the database user:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
mongosh
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
use securepool
|
|
83
|
+
|
|
84
|
+
db.createUser({
|
|
85
|
+
user: "securepool-user",
|
|
86
|
+
pwd: "SecurePool@123",
|
|
87
|
+
roles: [{ role: "readWrite", db: "securepool" }]
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
exit
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Verify it works:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
mongosh "mongodb://securepool-user:SecurePool%40123@localhost:27017/securepool?authSource=securepool"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Step 3: Generate RSA Keys (for JWT)
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
cd securepool
|
|
105
|
+
openssl genrsa -out private.pem 2048
|
|
106
|
+
openssl rsa -in private.pem -pubout -out public.pem
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Step 4: Create `.env` File
|
|
112
|
+
|
|
113
|
+
Create a `.env` file in the `securepool/` root:
|
|
114
|
+
|
|
115
|
+
```env
|
|
116
|
+
# Database
|
|
117
|
+
DB_TYPE=mongo
|
|
118
|
+
DB_URL=mongodb://localhost:27017/securepool
|
|
119
|
+
|
|
120
|
+
# If using auth (Step 2 Option B):
|
|
121
|
+
# DB_URL=mongodb://securepool-user:SecurePool%40123@localhost:27017/securepool?authSource=securepool
|
|
122
|
+
|
|
123
|
+
# JWT (paths to RSA key files)
|
|
124
|
+
JWT_PRIVATE_KEY_PATH=./private.pem
|
|
125
|
+
JWT_PUBLIC_KEY_PATH=./public.pem
|
|
126
|
+
|
|
127
|
+
# Email (Gmail SMTP - optional, needed for OTP emails)
|
|
128
|
+
EMAIL_HOST=smtp.gmail.com
|
|
129
|
+
EMAIL_PORT=587
|
|
130
|
+
EMAIL_SECURE=false
|
|
131
|
+
EMAIL_USER=your-email@gmail.com
|
|
132
|
+
EMAIL_PASS=your-gmail-app-password
|
|
133
|
+
EMAIL_FROM=your-email@gmail.com
|
|
134
|
+
|
|
135
|
+
# Server
|
|
136
|
+
PORT=5001
|
|
137
|
+
|
|
138
|
+
# Security
|
|
139
|
+
RATE_LIMIT_ENABLED=true
|
|
140
|
+
CORS_ORIGINS=*
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Gmail App Password (required for email OTP)
|
|
144
|
+
|
|
145
|
+
1. Go to https://myaccount.google.com/apppasswords
|
|
146
|
+
2. Sign in (2FA must be enabled)
|
|
147
|
+
3. Create app password for "Mail"
|
|
148
|
+
4. Copy the 16-character password into `EMAIL_PASS`
|
|
149
|
+
|
|
150
|
+
If you skip email setup, OTP features won't send emails (but the rest works fine).
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Step 5: Build All Packages
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
npm run build
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
This builds all 6 packages using Turborepo:
|
|
161
|
+
- `@securepool/core`
|
|
162
|
+
- `@securepool/application`
|
|
163
|
+
- `@securepool/infrastructure`
|
|
164
|
+
- `@securepool/persistence`
|
|
165
|
+
- `@securepool/api`
|
|
166
|
+
- `@securepool/react-sdk`
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Step 6: Start Backend
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
cd apps/demo-backend
|
|
174
|
+
npx ts-node src/index.ts
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
You should see:
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
SecurePool API running on port 5001
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Verify:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
curl http://localhost:5001/health
|
|
187
|
+
# → {"status":"ok"}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### API Documentation (Swagger UI)
|
|
191
|
+
|
|
192
|
+
Open in your browser:
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
http://localhost:5001/docs
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
This gives you an interactive API explorer where you can:
|
|
199
|
+
|
|
200
|
+
- See all endpoints with request/response schemas
|
|
201
|
+
- Try any API live by clicking "Try it out"
|
|
202
|
+
- Authenticate by clicking the **Authorize** button (top right) and pasting your JWT access token
|
|
203
|
+
- View the raw OpenAPI spec at `http://localhost:5001/docs.json`
|
|
204
|
+
|
|
205
|
+
**How to use authenticated endpoints in Swagger:**
|
|
206
|
+
|
|
207
|
+
1. First call `POST /auth/login` with your email + password
|
|
208
|
+
2. Copy the `accessToken` from the response
|
|
209
|
+
3. Click the **Authorize** button (lock icon at top)
|
|
210
|
+
4. Paste the token and click "Authorize"
|
|
211
|
+
5. Now all authenticated endpoints (Sessions, Change Password) will work
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Step 7: Start Frontend
|
|
217
|
+
|
|
218
|
+
Open a **new terminal**:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
cd apps/demo-frontend
|
|
222
|
+
npx vite
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Opens at http://localhost:5173
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Step 8: Test the Full Flow
|
|
230
|
+
|
|
231
|
+
### Register a new user
|
|
232
|
+
|
|
233
|
+
1. Open http://localhost:5173/signup
|
|
234
|
+
2. Enter email and password (min 8 chars)
|
|
235
|
+
3. Check your email for the 6-digit OTP
|
|
236
|
+
4. Enter OTP on the verification page
|
|
237
|
+
5. You're logged in and redirected to the dashboard
|
|
238
|
+
|
|
239
|
+
### Login with password
|
|
240
|
+
|
|
241
|
+
1. Go to http://localhost:5173/login
|
|
242
|
+
2. Enter email and password
|
|
243
|
+
|
|
244
|
+
### Login with OTP
|
|
245
|
+
|
|
246
|
+
1. Go to http://localhost:5173/otp-login
|
|
247
|
+
2. Enter your email
|
|
248
|
+
3. Check email for OTP code
|
|
249
|
+
4. Enter the code
|
|
250
|
+
|
|
251
|
+
### Forgot password
|
|
252
|
+
|
|
253
|
+
1. Go to http://localhost:5173/forgot-password
|
|
254
|
+
2. Enter your email
|
|
255
|
+
3. Check email for OTP
|
|
256
|
+
4. Enter OTP, then set new password
|
|
257
|
+
|
|
258
|
+
### Change password
|
|
259
|
+
|
|
260
|
+
1. Login and go to dashboard
|
|
261
|
+
2. Click your avatar (top right)
|
|
262
|
+
3. Click "Change password"
|
|
263
|
+
4. Enter old password and new password
|
|
264
|
+
|
|
265
|
+
### Switch accounts
|
|
266
|
+
|
|
267
|
+
1. Login with one account
|
|
268
|
+
2. Click avatar → "Add another account"
|
|
269
|
+
3. Login with a different email
|
|
270
|
+
4. Click avatar → click any stored account to switch
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## API Endpoints
|
|
275
|
+
|
|
276
|
+
| Method | Endpoint | Auth | Description |
|
|
277
|
+
|--------|----------|------|-------------|
|
|
278
|
+
| GET | `/health` | No | Health check |
|
|
279
|
+
| POST | `/auth/register` | No | Register + send OTP |
|
|
280
|
+
| POST | `/auth/verify-email` | No | Verify OTP + create user |
|
|
281
|
+
| POST | `/auth/login` | No | Login with password |
|
|
282
|
+
| POST | `/auth/otp/request` | No | Request OTP for login |
|
|
283
|
+
| POST | `/auth/otp/verify` | No | Verify OTP + login |
|
|
284
|
+
| POST | `/auth/refresh` | No | Refresh token |
|
|
285
|
+
| POST | `/auth/google` | No | Google SSO login |
|
|
286
|
+
| POST | `/auth/forgot-password` | No | Send password reset OTP |
|
|
287
|
+
| POST | `/auth/reset-password` | No | Reset password with OTP |
|
|
288
|
+
| POST | `/auth/change-password` | Yes | Change password (old + new) |
|
|
289
|
+
| GET | `/sessions` | Yes | List active sessions |
|
|
290
|
+
| DELETE | `/sessions/:id` | Yes | Revoke a session |
|
|
291
|
+
| DELETE | `/sessions` | Yes | Revoke all sessions |
|
|
292
|
+
|
|
293
|
+
All `/auth/*` routes require the `x-tenant-id` header.
|
|
294
|
+
|
|
295
|
+
Authenticated routes require: `Authorization: Bearer <access_token>`
|
|
296
|
+
|
|
297
|
+
### Example: Register + Login via curl
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
# Register
|
|
301
|
+
curl -X POST http://localhost:5001/auth/register \
|
|
302
|
+
-H "Content-Type: application/json" \
|
|
303
|
+
-H "x-tenant-id: default" \
|
|
304
|
+
-d '{"email":"test@example.com","password":"MyPass@1234"}'
|
|
305
|
+
|
|
306
|
+
# Login
|
|
307
|
+
curl -X POST http://localhost:5001/auth/login \
|
|
308
|
+
-H "Content-Type: application/json" \
|
|
309
|
+
-H "x-tenant-id: default" \
|
|
310
|
+
-d '{"email":"test@example.com","password":"MyPass@1234"}'
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Project Structure
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
securepool/
|
|
319
|
+
├── packages/ # NPM library packages
|
|
320
|
+
│ ├── core/ # Entities, enums (zero dependencies)
|
|
321
|
+
│ ├── application/ # Interfaces, AuthService, business logic
|
|
322
|
+
│ ├── infrastructure/ # JWT, bcrypt, OTP, email (nodemailer)
|
|
323
|
+
│ ├── persistence/ # MongoDB (mongoose) + PostgreSQL (prisma)
|
|
324
|
+
│ ├── api/ # Express routes, middleware, createSecurePool()
|
|
325
|
+
│ └── react-sdk/ # SecurePoolProvider, useAuth, UI components
|
|
326
|
+
│
|
|
327
|
+
├── apps/ # Runnable applications
|
|
328
|
+
│ ├── demo-backend/ # Express server using @securepool/api
|
|
329
|
+
│ └── demo-frontend/ # React app using @securepool/react-sdk
|
|
330
|
+
│
|
|
331
|
+
├── package.json # Monorepo root (npm workspaces)
|
|
332
|
+
├── tsconfig.base.json # Shared TypeScript config
|
|
333
|
+
├── turbo.json # Turborepo build pipeline
|
|
334
|
+
├── .env # Environment variables (not in git)
|
|
335
|
+
├── private.pem # JWT private key (not in git)
|
|
336
|
+
└── public.pem # JWT public key (not in git)
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Common Issues
|
|
342
|
+
|
|
343
|
+
### Port 5001 already in use
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
lsof -ti:5001 | xargs kill -9
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Port 5000 used by AirPlay (macOS)
|
|
350
|
+
|
|
351
|
+
We use port 5001 to avoid this. Or disable AirPlay Receiver in System Settings.
|
|
352
|
+
|
|
353
|
+
### MongoDB connection failed
|
|
354
|
+
|
|
355
|
+
Make sure MongoDB is running:
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
mongosh --eval "db.runCommand({ ping: 1 })"
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### OTP email not received
|
|
362
|
+
|
|
363
|
+
- Check `EMAIL_USER` and `EMAIL_PASS` in `.env`
|
|
364
|
+
- Gmail requires an App Password (not your regular password)
|
|
365
|
+
- Enable 2FA on your Google account first
|
|
366
|
+
|
|
367
|
+
### Build errors after changing code
|
|
368
|
+
|
|
369
|
+
Rebuild all packages:
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
npm run build
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Then restart the backend.
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## Useful Commands
|
|
380
|
+
|
|
381
|
+
| Command | Description |
|
|
382
|
+
|---------|-------------|
|
|
383
|
+
| `npm run build` | Build all packages |
|
|
384
|
+
| `npm run clean` | Clean all dist folders |
|
|
385
|
+
| `npx turbo run build --filter=@securepool/api` | Build single package |
|
|
386
|
+
| `cd apps/demo-backend && npx ts-node src/index.ts` | Start backend |
|
|
387
|
+
| `cd apps/demo-frontend && npx vite` | Start frontend |
|
|
388
|
+
| `lsof -ti:5001 \| xargs kill -9` | Kill process on port |
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
FROM node:20-alpine AS builder
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
# Copy monorepo root files
|
|
6
|
+
COPY package.json package-lock.json tsconfig.base.json turbo.json ./
|
|
7
|
+
|
|
8
|
+
# Copy all packages and the backend app
|
|
9
|
+
COPY packages/ packages/
|
|
10
|
+
COPY apps/demo-backend/ apps/demo-backend/
|
|
11
|
+
|
|
12
|
+
# Install dependencies
|
|
13
|
+
RUN npm ci
|
|
14
|
+
|
|
15
|
+
# Build all packages
|
|
16
|
+
RUN npx turbo run build --filter=@securepool/*
|
|
17
|
+
|
|
18
|
+
# Build the backend app
|
|
19
|
+
RUN cd apps/demo-backend && npx tsc
|
|
20
|
+
|
|
21
|
+
# ---- Production stage ----
|
|
22
|
+
FROM node:20-alpine
|
|
23
|
+
|
|
24
|
+
WORKDIR /app
|
|
25
|
+
|
|
26
|
+
COPY --from=builder /app/package.json /app/package-lock.json ./
|
|
27
|
+
COPY --from=builder /app/packages/ packages/
|
|
28
|
+
COPY --from=builder /app/apps/demo-backend/ apps/demo-backend/
|
|
29
|
+
COPY --from=builder /app/node_modules/ node_modules/
|
|
30
|
+
|
|
31
|
+
EXPOSE 5001
|
|
32
|
+
|
|
33
|
+
CMD ["node", "apps/demo-backend/dist/index.js"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "demo-backend",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "ts-node-dev --respawn src/index.ts",
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"start": "node dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@securepool/api": "1.0.0",
|
|
12
|
+
"dotenv": "^16.4.5"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"typescript": "^5.5.0",
|
|
16
|
+
"ts-node-dev": "^2.0.0",
|
|
17
|
+
"@types/node": "^20.14.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import { createSecurePool } from "@securepool/api";
|
|
5
|
+
|
|
6
|
+
// Load .env (local dev: from monorepo root, production: from process.env)
|
|
7
|
+
dotenv.config({ path: path.resolve(__dirname, "../../../.env") });
|
|
8
|
+
dotenv.config(); // also check current dir
|
|
9
|
+
|
|
10
|
+
function getJwtKeys(): { privateKey: string; publicKey: string } {
|
|
11
|
+
// Option 1: Keys passed directly as env vars (production)
|
|
12
|
+
if (process.env.JWT_PRIVATE_KEY && process.env.JWT_PUBLIC_KEY) {
|
|
13
|
+
return {
|
|
14
|
+
privateKey: process.env.JWT_PRIVATE_KEY.replace(/\\n/g, "\n"),
|
|
15
|
+
publicKey: process.env.JWT_PUBLIC_KEY.replace(/\\n/g, "\n"),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Option 2: Keys from file paths (local dev)
|
|
20
|
+
const root = path.resolve(__dirname, "../../..");
|
|
21
|
+
const privateKeyPath = path.resolve(root, process.env.JWT_PRIVATE_KEY_PATH || "./private.pem");
|
|
22
|
+
const publicKeyPath = path.resolve(root, process.env.JWT_PUBLIC_KEY_PATH || "./public.pem");
|
|
23
|
+
|
|
24
|
+
if (fs.existsSync(privateKeyPath) && fs.existsSync(publicKeyPath)) {
|
|
25
|
+
return {
|
|
26
|
+
privateKey: fs.readFileSync(privateKeyPath, "utf-8"),
|
|
27
|
+
publicKey: fs.readFileSync(publicKeyPath, "utf-8"),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.error("JWT keys not found. Set JWT_PRIVATE_KEY/JWT_PUBLIC_KEY env vars or provide .pem files.");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function main() {
|
|
36
|
+
const { privateKey, publicKey } = getJwtKeys();
|
|
37
|
+
const port = parseInt(process.env.PORT || "5001", 10);
|
|
38
|
+
|
|
39
|
+
const { app } = await createSecurePool({
|
|
40
|
+
database: {
|
|
41
|
+
type: (process.env.DB_TYPE as "mongo" | "postgres") || "mongo",
|
|
42
|
+
url: process.env.DB_URL || "mongodb://localhost:27017/securepool",
|
|
43
|
+
},
|
|
44
|
+
jwt: {
|
|
45
|
+
privateKey,
|
|
46
|
+
publicKey,
|
|
47
|
+
},
|
|
48
|
+
email: process.env.EMAIL_USER ? {
|
|
49
|
+
host: process.env.EMAIL_HOST || "smtp.gmail.com",
|
|
50
|
+
port: parseInt(process.env.EMAIL_PORT || "587", 10),
|
|
51
|
+
secure: process.env.EMAIL_SECURE === "true",
|
|
52
|
+
user: process.env.EMAIL_USER,
|
|
53
|
+
pass: process.env.EMAIL_PASS || "",
|
|
54
|
+
from: process.env.EMAIL_FROM || process.env.EMAIL_USER,
|
|
55
|
+
} : undefined,
|
|
56
|
+
security: {
|
|
57
|
+
enableRateLimit: process.env.RATE_LIMIT_ENABLED !== "false",
|
|
58
|
+
corsOrigins: process.env.CORS_ORIGINS || "*",
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
app.listen(port, () => {
|
|
63
|
+
console.log(`SecurePool API running on port ${port}`);
|
|
64
|
+
console.log(`Api Docs: http://localhost:${port}/docs`);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
main().catch((err) => {
|
|
69
|
+
console.error("Failed to start SecurePool:", err);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# React + TypeScript + Vite
|
|
2
|
+
|
|
3
|
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
|
4
|
+
|
|
5
|
+
Currently, two official plugins are available:
|
|
6
|
+
|
|
7
|
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
|
8
|
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
|
9
|
+
|
|
10
|
+
## React Compiler
|
|
11
|
+
|
|
12
|
+
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
|
13
|
+
|
|
14
|
+
## Expanding the ESLint configuration
|
|
15
|
+
|
|
16
|
+
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
export default defineConfig([
|
|
20
|
+
globalIgnores(['dist']),
|
|
21
|
+
{
|
|
22
|
+
files: ['**/*.{ts,tsx}'],
|
|
23
|
+
extends: [
|
|
24
|
+
// Other configs...
|
|
25
|
+
|
|
26
|
+
// Remove tseslint.configs.recommended and replace with this
|
|
27
|
+
tseslint.configs.recommendedTypeChecked,
|
|
28
|
+
// Alternatively, use this for stricter rules
|
|
29
|
+
tseslint.configs.strictTypeChecked,
|
|
30
|
+
// Optionally, add this for stylistic rules
|
|
31
|
+
tseslint.configs.stylisticTypeChecked,
|
|
32
|
+
|
|
33
|
+
// Other configs...
|
|
34
|
+
],
|
|
35
|
+
languageOptions: {
|
|
36
|
+
parserOptions: {
|
|
37
|
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
38
|
+
tsconfigRootDir: import.meta.dirname,
|
|
39
|
+
},
|
|
40
|
+
// other options...
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
])
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
// eslint.config.js
|
|
50
|
+
import reactX from 'eslint-plugin-react-x'
|
|
51
|
+
import reactDom from 'eslint-plugin-react-dom'
|
|
52
|
+
|
|
53
|
+
export default defineConfig([
|
|
54
|
+
globalIgnores(['dist']),
|
|
55
|
+
{
|
|
56
|
+
files: ['**/*.{ts,tsx}'],
|
|
57
|
+
extends: [
|
|
58
|
+
// Other configs...
|
|
59
|
+
// Enable lint rules for React
|
|
60
|
+
reactX.configs['recommended-typescript'],
|
|
61
|
+
// Enable lint rules for React DOM
|
|
62
|
+
reactDom.configs.recommended,
|
|
63
|
+
],
|
|
64
|
+
languageOptions: {
|
|
65
|
+
parserOptions: {
|
|
66
|
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
67
|
+
tsconfigRootDir: import.meta.dirname,
|
|
68
|
+
},
|
|
69
|
+
// other options...
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
])
|
|
73
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import globals from 'globals'
|
|
3
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
4
|
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
5
|
+
import tseslint from 'typescript-eslint'
|
|
6
|
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
7
|
+
|
|
8
|
+
export default defineConfig([
|
|
9
|
+
globalIgnores(['dist']),
|
|
10
|
+
{
|
|
11
|
+
files: ['**/*.{ts,tsx}'],
|
|
12
|
+
extends: [
|
|
13
|
+
js.configs.recommended,
|
|
14
|
+
tseslint.configs.recommended,
|
|
15
|
+
reactHooks.configs.flat.recommended,
|
|
16
|
+
reactRefresh.configs.vite,
|
|
17
|
+
],
|
|
18
|
+
languageOptions: {
|
|
19
|
+
ecmaVersion: 2020,
|
|
20
|
+
globals: globals.browser,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
])
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>demo-frontend</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "demo-frontend",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"react": "^19.2.4",
|
|
13
|
+
"react-dom": "^19.2.4",
|
|
14
|
+
"react-router-dom": "^7.6.0",
|
|
15
|
+
"@securepool/react-sdk": "1.0.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/react": "^19.2.14",
|
|
19
|
+
"@types/react-dom": "^19.2.3",
|
|
20
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
21
|
+
"typescript": "~5.9.3",
|
|
22
|
+
"vite": "^8.0.1"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
|