@dax-crafta/auth 2.1.7 → 2.1.8
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/.mocharc.cjs +6 -0
- package/README.md +167 -274
- package/image.png +0 -0
- package/package.json +6 -2
- package/src/index.d.ts +8 -1
- package/src/index.js +75 -7
- package/src/middlewares/auth.middleware.js +30 -9
- package/src/models/user.js +2 -1
- package/src/services/audit.service.js +66 -0
- package/src/services/auth.service.js +36 -9
- package/src/services/role.service.js +5 -0
- package/src/services/session.service.js +46 -5
- package/src/services/webhook.service.js +42 -1
- package/src/utils/email.js +4 -3
- package/src/utils/password-policy.js +6 -1
- package/src/utils/password-strength.js +68 -0
- package/test/01-register-login.test.js +134 -0
- package/test/02-password-reset.test.js +50 -0
- package/test/03-2fa.test.js +54 -0
- package/test/04-rbac.test.js +51 -0
- package/test/05-audit-webhook.test.js +78 -0
- package/test/06-sessions.test.js +36 -0
- package/test/07-error-normalization.test.js +64 -0
- package/test/08-cors-health.test.js +45 -0
- package/test/09-passwordless.test.js +40 -0
- package/test/10-webhook-signing.test.js +37 -0
- package/test/helpers.js +130 -0
- package/test/run-tests.js +94 -0
- package/test/tiny-test.js +68 -0
package/.mocharc.cjs
ADDED
package/README.md
CHANGED
|
@@ -1,274 +1,167 @@
|
|
|
1
|
-
# @dax-crafta/auth
|
|
2
|
-
|
|
3
|
-
Production-ready
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/@dax-crafta/auth)
|
|
6
|
-
[](https://github.com/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
auditLogs: true,
|
|
169
|
-
twoFactor: true,
|
|
170
|
-
csrf: false
|
|
171
|
-
}
|
|
172
|
-
})(app);
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
Notes:
|
|
176
|
-
- If SMTP is missing, emailVerification/loginAlerts are auto-disabled with warning.
|
|
177
|
-
- If `securityAttempts: false`, lockout attempt handling is disabled.
|
|
178
|
-
|
|
179
|
-
## 6. Email setup (optional)
|
|
180
|
-
|
|
181
|
-
Enable this only when SMTP is ready:
|
|
182
|
-
|
|
183
|
-
```js
|
|
184
|
-
auth({
|
|
185
|
-
features: {
|
|
186
|
-
emailVerification: true,
|
|
187
|
-
loginAlerts: true
|
|
188
|
-
},
|
|
189
|
-
smtp: {
|
|
190
|
-
host: process.env.SMTP_HOST,
|
|
191
|
-
port: Number(process.env.SMTP_PORT || 587),
|
|
192
|
-
auth: {
|
|
193
|
-
user: process.env.SMTP_USER,
|
|
194
|
-
pass: process.env.SMTP_PASS
|
|
195
|
-
},
|
|
196
|
-
from: process.env.SMTP_FROM
|
|
197
|
-
},
|
|
198
|
-
baseUrl: process.env.BASE_URL || 'http://localhost:3000'
|
|
199
|
-
})(app);
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
## 7. Google OAuth setup (optional)
|
|
203
|
-
|
|
204
|
-
```js
|
|
205
|
-
auth({
|
|
206
|
-
social: {
|
|
207
|
-
google: {
|
|
208
|
-
clientID: process.env.GOOGLE_CLIENT_ID,
|
|
209
|
-
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
210
|
-
callbackURL: 'http://localhost:3000/auth/google/callback'
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
})(app);
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
Routes:
|
|
217
|
-
- `GET /auth/google`
|
|
218
|
-
- `GET /auth/google/callback`
|
|
219
|
-
|
|
220
|
-
## 8. RBAC quick use
|
|
221
|
-
|
|
222
|
-
```js
|
|
223
|
-
const role = await app.roleService.createRole({
|
|
224
|
-
name: 'admin',
|
|
225
|
-
permissions: [
|
|
226
|
-
{ resource: 'users', actions: ['create', 'read', 'update', 'delete'] }
|
|
227
|
-
]
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
const allowed = await app.roleService.checkPermission('admin', 'users', 'delete');
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
## 9. Environment variables
|
|
234
|
-
|
|
235
|
-
```bash
|
|
236
|
-
JWT_SECRET=my-super-secret
|
|
237
|
-
MONGO_URL=mongodb://127.0.0.1:27017/crafta-auth-demo
|
|
238
|
-
SMTP_HOST=smtp.example.com
|
|
239
|
-
SMTP_PORT=587
|
|
240
|
-
SMTP_USER=you@example.com
|
|
241
|
-
SMTP_PASS=your-password
|
|
242
|
-
SMTP_FROM=noreply@example.com
|
|
243
|
-
BASE_URL=http://localhost:3000
|
|
244
|
-
GOOGLE_CLIENT_ID=...
|
|
245
|
-
GOOGLE_CLIENT_SECRET=...
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
## 10. Troubleshooting
|
|
249
|
-
|
|
250
|
-
- 401 on protected route
|
|
251
|
-
- Token missing or expired.
|
|
252
|
-
- Send `Authorization: Bearer <accessToken>`.
|
|
253
|
-
|
|
254
|
-
- Register fails on password policy
|
|
255
|
-
- Use uppercase + lowercase + number + special char.
|
|
256
|
-
|
|
257
|
-
- Email not sending
|
|
258
|
-
- Check SMTP credentials and port.
|
|
259
|
-
- If SMTP absent, email features auto-disable by design.
|
|
260
|
-
|
|
261
|
-
- Refresh token invalid
|
|
262
|
-
- Refresh token is rotated; use latest token returned by API.
|
|
263
|
-
|
|
264
|
-
## 11. Release confidence
|
|
265
|
-
|
|
266
|
-
- Built-in smoke suite is available:
|
|
267
|
-
|
|
268
|
-
```bash
|
|
269
|
-
npm test
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
## License
|
|
273
|
-
|
|
274
|
-
MIT © Dax Crafta
|
|
1
|
+
# @dax-crafta/auth
|
|
2
|
+
|
|
3
|
+
Production-ready authentication library for Crafta apps, featuring plug-and-play defaults, robust security, and clean JSON-based customization.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@dax-crafta/auth)
|
|
6
|
+
[](https://github.com/NPM-crafta/crafta-auth/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
1. **JWT Authentication**: Secure sessions with automatic refresh token rotation.
|
|
10
|
+
2. **Standard Routes**: Built-in register, login, verify email, forgot/reset password, and refresh routes.
|
|
11
|
+
3. **Multi-Factor Authentication (MFA)**: Optional TOTP-based 2FA with backup codes support.
|
|
12
|
+
4. **RBAC**: Role-Based Access Control service to manage permissions and roles.
|
|
13
|
+
5. **Rate Limiting**: Route-level protection for sensitive endpoints (login, register, 2FA).
|
|
14
|
+
6. **Password Policy**: Strict policy enforcement with password history checks and strength validation.
|
|
15
|
+
7. **Google OAuth**: Optional social login integration.
|
|
16
|
+
8. **Audit Logs**: Comprehensive logging to MongoDB and local files.
|
|
17
|
+
9. **Session Management**: Full session control with per-user session limits and global logout.
|
|
18
|
+
10. **Webhooks**: Real-time event notifications for auth lifecycle events (registered, login, logout, password change).
|
|
19
|
+
11. **Passwordless Login**: Magic link authentication.
|
|
20
|
+
12. **Standardized Error Handling**: Unified error format with 19 distinct `AUTH_*` codes.
|
|
21
|
+
13. **CORS Middleware**: Explicit origin whitelisting for secure cross-origin requests.
|
|
22
|
+
14. **Customizable Fields**: Flexible user model customization.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
```bash
|
|
26
|
+
npm install @dax-crafta/auth
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Minimal Setup
|
|
30
|
+
```javascript
|
|
31
|
+
const { crafta } = require('crafta');
|
|
32
|
+
const { auth } = require('@dax-crafta/auth');
|
|
33
|
+
|
|
34
|
+
const app = crafta();
|
|
35
|
+
|
|
36
|
+
auth({
|
|
37
|
+
env: {
|
|
38
|
+
JWT_SECRET: process.env.JWT_SECRET || 'dev-secret-123'
|
|
39
|
+
},
|
|
40
|
+
mongoUrl: process.env.MONGO_URL || 'mongodb://127.0.0.1:27017/crafta-auth-demo'
|
|
41
|
+
})(app);
|
|
42
|
+
|
|
43
|
+
app.listen(3000, () => console.log('Server running on http://localhost:3000'));
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Configuration Options
|
|
47
|
+
The library is configured via a single object passed to the `auth()` function:
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
auth({
|
|
51
|
+
strategy: 'jwt', // Currently defaults to 'jwt'
|
|
52
|
+
fields: ['name', 'email', 'password'],
|
|
53
|
+
routes: { /* Custom route paths */ },
|
|
54
|
+
passwordPolicy: {
|
|
55
|
+
minLength: 8,
|
|
56
|
+
requireUppercase: true,
|
|
57
|
+
requireNumbers: true,
|
|
58
|
+
requireSpecialChars: true,
|
|
59
|
+
expiryDays: 90
|
|
60
|
+
},
|
|
61
|
+
limits: {
|
|
62
|
+
loginMax: 10,
|
|
63
|
+
registerMax: 20
|
|
64
|
+
}
|
|
65
|
+
})(app);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Feature Toggles
|
|
69
|
+
Enable or disable features via the `features` object:
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
auth({
|
|
73
|
+
features: {
|
|
74
|
+
emailVerification: true,
|
|
75
|
+
twoFactor: true,
|
|
76
|
+
passwordless: false,
|
|
77
|
+
auditLogs: true,
|
|
78
|
+
webhooks: false,
|
|
79
|
+
rateLimit: true,
|
|
80
|
+
csrf: false
|
|
81
|
+
}
|
|
82
|
+
})(app);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Email Setup
|
|
86
|
+
Configure SMTP to enable email-based features (Verification, Forgot Password, Magic Links, Login Alerts):
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
auth({
|
|
90
|
+
smtp: {
|
|
91
|
+
host: process.env.SMTP_HOST,
|
|
92
|
+
port: 587,
|
|
93
|
+
auth: {
|
|
94
|
+
user: process.env.SMTP_USER,
|
|
95
|
+
pass: process.env.SMTP_PASS
|
|
96
|
+
},
|
|
97
|
+
from: process.env.SMTP_FROM
|
|
98
|
+
},
|
|
99
|
+
baseUrl: 'http://localhost:3000'
|
|
100
|
+
})(app);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Google OAuth Setup
|
|
104
|
+
```javascript
|
|
105
|
+
auth({
|
|
106
|
+
social: {
|
|
107
|
+
google: {
|
|
108
|
+
clientID: process.env.GOOGLE_CLIENT_ID,
|
|
109
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
110
|
+
callbackURL: 'http://localhost:3000/auth/google/callback'
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
})(app);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## RBAC Quick Use
|
|
117
|
+
Access the role service directly from your app instance:
|
|
118
|
+
|
|
119
|
+
```javascript
|
|
120
|
+
// Check permission
|
|
121
|
+
const allowed = await app.roleService.checkPermission('admin', 'users', 'delete');
|
|
122
|
+
|
|
123
|
+
// Create role
|
|
124
|
+
await app.roleService.createRole({
|
|
125
|
+
name: 'editor',
|
|
126
|
+
permissions: [{ resource: 'posts', actions: ['create', 'update'] }]
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Environment Variables
|
|
131
|
+
- `JWT_SECRET`: Secret key for signing tokens.
|
|
132
|
+
- `MONGO_URL`: MongoDB connection string.
|
|
133
|
+
- `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`, `SMTP_FROM`: SMTP configuration.
|
|
134
|
+
- `BASE_URL`: Base URL for email links.
|
|
135
|
+
- `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`: Google OAuth credentials.
|
|
136
|
+
|
|
137
|
+
## Troubleshooting
|
|
138
|
+
- **401 Unauthorized**: Check if `Authorization: Bearer <accessToken>` header is present and valid.
|
|
139
|
+
- **Password policy errors**: Ensure passwords meet length, uppercase, number, and special character requirements.
|
|
140
|
+
- **Email issues**: Verify SMTP configuration; features relying on email will automatically disable if SMTP is misconfigured.
|
|
141
|
+
|
|
142
|
+
## Error Codes
|
|
143
|
+
| Code | Description | HTTP Status |
|
|
144
|
+
| :--- | :--- | :--- |
|
|
145
|
+
| `AUTH_BAD_REQUEST` | Generic bad request error | 400 |
|
|
146
|
+
| `AUTH_VALIDATION` | Invalid request data | 400 |
|
|
147
|
+
| `AUTH_PASSWORD_POLICY` | Password policy violation | 400 |
|
|
148
|
+
| `AUTH_PASSWORD_REUSE` | Cannot reuse previous passwords | 400 |
|
|
149
|
+
| `AUTH_SOCIAL_FAILED` | Social login failure | 400 |
|
|
150
|
+
| `AUTH_INVALID_ID` | Invalid resource identifier | 400 |
|
|
151
|
+
| `AUTH_UNAUTHORIZED` | Authentication required | 401 |
|
|
152
|
+
| `AUTH_INVALID_CREDENTIALS` | Invalid email or password | 401 |
|
|
153
|
+
| `AUTH_INVALID_TOKEN` | Token invalid or missing | 401 |
|
|
154
|
+
| `AUTH_EXPIRED_TOKEN` | Token expired | 401 |
|
|
155
|
+
| `AUTH_2FA_FAILED` | Invalid 2FA code | 401 |
|
|
156
|
+
| `AUTH_FORBIDDEN` | Insufficient permissions | 403 |
|
|
157
|
+
| `AUTH_ACCOUNT_LOCKED` | Account locked due to failed attempts | 403 |
|
|
158
|
+
| `AUTH_EMAIL_NOT_VERIFIED` | Email verification required | 403 |
|
|
159
|
+
| `AUTH_2FA_REQUIRED` | 2FA code input required | 403 |
|
|
160
|
+
| `AUTH_NOT_FOUND` | Resource not found | 404 |
|
|
161
|
+
| `AUTH_CONFLICT` | Resource already exists | 409 |
|
|
162
|
+
| `AUTH_RATE_LIMITED` | Too many requests | 429 |
|
|
163
|
+
| `AUTH_SMTP_DISABLED` | Email delivery service disabled | 503 |
|
|
164
|
+
| `AUTH_INTERNAL` | Unexpected internal error | 500 |
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
MIT © Dax Crafta
|
package/image.png
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dax-crafta/auth",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.8",
|
|
4
4
|
"description": "A powerful, flexible, and secure authentication plugin for the Crafta framework. Supports JWT, social login, 2FA, RBAC, audit logging, and enterprise-grade security features.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -61,5 +61,9 @@
|
|
|
61
61
|
},
|
|
62
62
|
"homepage": "https://github.com/daxp472/crafta#readme",
|
|
63
63
|
"license": "MIT",
|
|
64
|
-
"author": "Dax Patel"
|
|
64
|
+
"author": "Dax Patel",
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@types/mocha": "^10.0.10",
|
|
67
|
+
"mocha": "^10.8.2"
|
|
68
|
+
}
|
|
65
69
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -56,6 +56,8 @@ export interface RateLimitConfig {
|
|
|
56
56
|
forgotPasswordMax?: number;
|
|
57
57
|
refreshMax?: number;
|
|
58
58
|
registerMax?: number;
|
|
59
|
+
/** Per-route max overrides keyed by route name (e.g. "login", "register", "2fa") */
|
|
60
|
+
[routeName: string]: number | undefined;
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
export interface WebhookConfig {
|
|
@@ -67,6 +69,8 @@ export interface WebhookConfig {
|
|
|
67
69
|
export interface SessionConfig {
|
|
68
70
|
maxSessions?: number;
|
|
69
71
|
sessionExpiryMs?: number;
|
|
72
|
+
/** Milliseconds before expiry at which the X-Session-Expires-In warning header is emitted. Default: 300000 (5 min). */
|
|
73
|
+
warningThresholdMs?: number;
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
export interface LockoutConfig {
|
|
@@ -183,11 +187,14 @@ export interface MFAService {
|
|
|
183
187
|
}
|
|
184
188
|
|
|
185
189
|
export interface PasswordPolicyService {
|
|
186
|
-
validate(password: string, userInfo?: any): { isValid: boolean; errors: string[]; strength: number };
|
|
190
|
+
validate(password: string, userInfo?: any): { isValid: boolean; errors: string[]; strength: number; strengthScore: number };
|
|
187
191
|
allowsReuse(password: string, passwordHistory: any[], bcrypt: any): Promise<boolean>;
|
|
188
192
|
isExpired(lastChange: Date): boolean;
|
|
189
193
|
}
|
|
190
194
|
|
|
195
|
+
/** Returns a heuristic password strength score from 0 (very weak) to 4 (very strong). */
|
|
196
|
+
export function getPasswordStrength(password: string): number;
|
|
197
|
+
|
|
191
198
|
export function auth(config?: AuthConfig): (app: any) => void;
|
|
192
199
|
export function normalizeError(err: unknown): ApiError;
|
|
193
200
|
|
package/src/index.js
CHANGED
|
@@ -3,6 +3,7 @@ const cookieParser = require('cookie-parser');
|
|
|
3
3
|
const csrf = require('csurf');
|
|
4
4
|
const helmet = require("helmet");
|
|
5
5
|
const mongoose = require("mongoose");
|
|
6
|
+
const jwt = require('jsonwebtoken');
|
|
6
7
|
|
|
7
8
|
const GoogleStrategy = require('passport-google-oauth20').Strategy;
|
|
8
9
|
const AuthService = require('./services/auth.service');
|
|
@@ -18,6 +19,7 @@ const corsMiddleware = require('./middlewares/cors.middleware');
|
|
|
18
19
|
const validators = require('./middlewares/validation.middleware');
|
|
19
20
|
const { createLogger } = require('./utils/logger');
|
|
20
21
|
const ApiError = require('./utils/api-error');
|
|
22
|
+
const { getPasswordStrength } = require('./utils/password-strength');
|
|
21
23
|
const { runAdaptiveTests, inferFeatureFlags, testCatalog } = require('./testing/adaptive-runner');
|
|
22
24
|
|
|
23
25
|
|
|
@@ -78,7 +80,8 @@ const defaultConfig = {
|
|
|
78
80
|
webhooks: null,
|
|
79
81
|
session: {
|
|
80
82
|
maxSessions: 5,
|
|
81
|
-
sessionExpiryMs: 30 * 24 * 60 * 60 * 1000
|
|
83
|
+
sessionExpiryMs: 30 * 24 * 60 * 60 * 1000,
|
|
84
|
+
warningThresholdMs: 5 * 60 * 1000
|
|
82
85
|
}
|
|
83
86
|
};
|
|
84
87
|
|
|
@@ -154,13 +157,24 @@ function auth(config = {}) {
|
|
|
154
157
|
// ------------------------------------------
|
|
155
158
|
// MongoDB Connection — support external connections
|
|
156
159
|
// ------------------------------------------
|
|
160
|
+
const connectDB = () => {
|
|
161
|
+
mongoose.connect(finalConfig.mongoUrl)
|
|
162
|
+
.then(() => logger.logSuccess("MongoDB connected"))
|
|
163
|
+
.catch(err => {
|
|
164
|
+
logger.logError(`MongoDB connection error: ${err.message}`);
|
|
165
|
+
setTimeout(connectDB, 5000);
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
|
|
157
169
|
if (finalConfig.mongooseConnection) {
|
|
158
170
|
// Allow host app to pass an existing mongoose connection
|
|
159
171
|
logger.logInfo("Using provided mongoose connection");
|
|
160
172
|
} else if (mongoose.connection.readyState === 0) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
173
|
+
connectDB();
|
|
174
|
+
mongoose.connection.on('disconnected', () => {
|
|
175
|
+
logger.logWarn("MongoDB disconnected, attempting to reconnect...");
|
|
176
|
+
connectDB();
|
|
177
|
+
});
|
|
164
178
|
} else {
|
|
165
179
|
logger.logInfo("Using existing mongoose connection");
|
|
166
180
|
}
|
|
@@ -219,6 +233,29 @@ function auth(config = {}) {
|
|
|
219
233
|
}
|
|
220
234
|
};
|
|
221
235
|
|
|
236
|
+
// ------------------------------------------
|
|
237
|
+
// SESSION TIMEOUT WARNING MIDDLEWARE
|
|
238
|
+
// ------------------------------------------
|
|
239
|
+
const warningThresholdMs = finalConfig.session?.warningThresholdMs || 5 * 60 * 1000;
|
|
240
|
+
app.use((req, res, next) => {
|
|
241
|
+
const authHeader = req.headers.authorization;
|
|
242
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
243
|
+
const token = authHeader.split(' ')[1];
|
|
244
|
+
try {
|
|
245
|
+
const decoded = jwt.decode(token);
|
|
246
|
+
if (decoded && decoded.exp) {
|
|
247
|
+
const timeLeft = decoded.exp * 1000 - Date.now();
|
|
248
|
+
if (timeLeft > 0 && timeLeft < warningThresholdMs) {
|
|
249
|
+
res.setHeader('X-Session-Expires-In', Math.floor(timeLeft / 1000));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} catch (err) {
|
|
253
|
+
// Ignore decoding errors in middleware
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
next();
|
|
257
|
+
});
|
|
258
|
+
|
|
222
259
|
app.use(cookieParser());
|
|
223
260
|
|
|
224
261
|
// CORS — applied before routes
|
|
@@ -253,14 +290,45 @@ function auth(config = {}) {
|
|
|
253
290
|
// ------------------------------------------
|
|
254
291
|
// HEALTH CHECK
|
|
255
292
|
// ------------------------------------------
|
|
256
|
-
app.get('/auth/health', (req, res) => {
|
|
293
|
+
app.get('/auth/health', async (req, res) => {
|
|
257
294
|
const dbState = mongoose.connection.readyState;
|
|
258
295
|
const dbStatus = dbState === 1 ? 'connected' : dbState === 2 ? 'connecting' : 'disconnected';
|
|
296
|
+
|
|
297
|
+
// Get active session count
|
|
298
|
+
let activeSessions = 0;
|
|
299
|
+
if (mongoose.connection.readyState === 1) {
|
|
300
|
+
try {
|
|
301
|
+
activeSessions = await mongoose.connection.db.collection('users').aggregate([
|
|
302
|
+
{ $match: { sessions: { $exists: true, $not: { $size: 0 } } } },
|
|
303
|
+
{ $unwind: '$sessions' },
|
|
304
|
+
{ $match: { 'sessions.createdAt': { $gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) } } },
|
|
305
|
+
{ $group: { _id: '$_id', count: { $sum: 1 } } },
|
|
306
|
+
{ $group: { _id: null, total: { $sum: '$count' } } }
|
|
307
|
+
]).toArray();
|
|
308
|
+
activeSessions = activeSessions[0]?.total || 0;
|
|
309
|
+
} catch (err) {
|
|
310
|
+
logger.logWarn(`Failed to count active sessions: ${err.message}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Get memory usage
|
|
315
|
+
const memoryUsage = process.memoryUsage();
|
|
316
|
+
|
|
317
|
+
// Get uptime
|
|
318
|
+
const uptime = process.uptime();
|
|
319
|
+
|
|
259
320
|
res.json({
|
|
260
321
|
success: true,
|
|
261
322
|
status: 'ok',
|
|
262
323
|
db: dbStatus,
|
|
263
324
|
version: require('../package.json').version,
|
|
325
|
+
uptime: `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`,
|
|
326
|
+
activeSessions,
|
|
327
|
+
memoryUsage: {
|
|
328
|
+
rss: `${Math.round(memoryUsage.rss / 1024 / 1024)}MB`,
|
|
329
|
+
heapTotal: `${Math.round(memoryUsage.heapTotal / 1024 / 1024)}MB`,
|
|
330
|
+
heapUsed: `${Math.round(memoryUsage.heapUsed / 1024 / 1024)}MB`
|
|
331
|
+
},
|
|
264
332
|
features: {
|
|
265
333
|
emailVerification: finalConfig.features.emailVerification,
|
|
266
334
|
twoFactor: finalConfig.features.twoFactor,
|
|
@@ -482,7 +550,7 @@ function auth(config = {}) {
|
|
|
482
550
|
verifyToken,
|
|
483
551
|
async (req, res, next) => {
|
|
484
552
|
try {
|
|
485
|
-
await authService.logout(req.user.id, req.body.sessionId);
|
|
553
|
+
await authService.logout(req.user.id, req.body.sessionId, req.body.refreshToken);
|
|
486
554
|
await safeAudit({ userId: req.user.id, action: 'logout', status: 'success' });
|
|
487
555
|
await safeWebhook('user.logout', { userId: req.user.id });
|
|
488
556
|
res.json({ success: true, message: 'Logged out successfully' });
|
|
@@ -598,4 +666,4 @@ function auth(config = {}) {
|
|
|
598
666
|
};
|
|
599
667
|
}
|
|
600
668
|
|
|
601
|
-
module.exports = { auth, ApiError, runAdaptiveTests, inferFeatureFlags, testCatalog };
|
|
669
|
+
module.exports = { auth, ApiError, getPasswordStrength, runAdaptiveTests, inferFeatureFlags, testCatalog };
|