@morojs/moro 1.1.0 → 1.2.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/README.md +56 -1
- package/dist/core/auth/morojs-adapter.d.ts +94 -0
- package/dist/core/auth/morojs-adapter.js +288 -0
- package/dist/core/auth/morojs-adapter.js.map +1 -0
- package/dist/core/http/http-server.d.ts +2 -0
- package/dist/core/http/http-server.js +52 -9
- package/dist/core/http/http-server.js.map +1 -1
- package/dist/core/middleware/built-in/auth-helpers.d.ts +124 -0
- package/dist/core/middleware/built-in/auth-helpers.js +338 -0
- package/dist/core/middleware/built-in/auth-helpers.js.map +1 -0
- package/dist/core/middleware/built-in/auth-providers.d.ts +125 -0
- package/dist/core/middleware/built-in/auth-providers.js +394 -0
- package/dist/core/middleware/built-in/auth-providers.js.map +1 -0
- package/dist/core/middleware/built-in/auth.d.ts +29 -1
- package/dist/core/middleware/built-in/auth.js +259 -16
- package/dist/core/middleware/built-in/auth.js.map +1 -1
- package/dist/core/middleware/built-in/index.d.ts +3 -1
- package/dist/core/middleware/built-in/index.js +19 -1
- package/dist/core/middleware/built-in/index.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/moro.d.ts +1 -0
- package/dist/moro.js +19 -1
- package/dist/moro.js.map +1 -1
- package/dist/types/auth.d.ts +367 -0
- package/dist/types/auth.js +28 -0
- package/dist/types/auth.js.map +1 -0
- package/package.json +6 -2
- package/src/core/auth/README.md +339 -0
- package/src/core/auth/morojs-adapter.ts +402 -0
- package/src/core/http/http-server.ts +61 -10
- package/src/core/middleware/built-in/auth-helpers.ts +401 -0
- package/src/core/middleware/built-in/auth-providers.ts +480 -0
- package/src/core/middleware/built-in/auth.ts +306 -16
- package/src/core/middleware/built-in/index.ts +22 -0
- package/src/index.ts +26 -0
- package/src/moro.ts +29 -1
- package/src/types/auth.ts +440 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
// Auth Helper Middleware and Utilities
|
|
2
|
+
import { AuthRequest } from '../../../types/auth';
|
|
3
|
+
|
|
4
|
+
export interface AuthGuardOptions {
|
|
5
|
+
redirectTo?: string;
|
|
6
|
+
redirectOnAuth?: string; // Redirect if already authenticated
|
|
7
|
+
authorize?: (user: any) => boolean | Promise<boolean>;
|
|
8
|
+
roles?: string[];
|
|
9
|
+
permissions?: string[];
|
|
10
|
+
allowUnauthenticated?: boolean;
|
|
11
|
+
onUnauthorized?: (req: any, res: any) => void;
|
|
12
|
+
onForbidden?: (req: any, res: any) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface AuthRouteOptions {
|
|
16
|
+
requireAuth?: boolean;
|
|
17
|
+
roles?: string[];
|
|
18
|
+
permissions?: string[];
|
|
19
|
+
redirectTo?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Auth Guard Middleware - Protects routes with authentication and authorization
|
|
24
|
+
*/
|
|
25
|
+
export function requireAuth(options: AuthGuardOptions = {}) {
|
|
26
|
+
return async (req: any, res: any, next: any) => {
|
|
27
|
+
const auth: AuthRequest = req.auth;
|
|
28
|
+
|
|
29
|
+
if (!auth) {
|
|
30
|
+
throw new Error('Auth middleware must be installed before using requireAuth');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check if already authenticated and should redirect
|
|
34
|
+
if (auth.isAuthenticated && options.redirectOnAuth) {
|
|
35
|
+
return res.redirect(options.redirectOnAuth);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check authentication requirement
|
|
39
|
+
if (!options.allowUnauthenticated && !auth.isAuthenticated) {
|
|
40
|
+
if (options.onUnauthorized) {
|
|
41
|
+
return options.onUnauthorized(req, res);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (options.redirectTo) {
|
|
45
|
+
return res.redirect(`${options.redirectTo}?callbackUrl=${encodeURIComponent(req.url)}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return res.status(401).json({
|
|
49
|
+
error: 'Authentication required',
|
|
50
|
+
message: 'You must be logged in to access this resource',
|
|
51
|
+
signInUrl: '/api/auth/signin',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Skip further checks if not authenticated but allowed
|
|
56
|
+
if (!auth.isAuthenticated && options.allowUnauthenticated) {
|
|
57
|
+
return next();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const user = auth.user;
|
|
61
|
+
|
|
62
|
+
// Check roles if specified
|
|
63
|
+
if (options.roles && options.roles.length > 0) {
|
|
64
|
+
const userRoles = user?.roles || [];
|
|
65
|
+
const hasRole = options.roles.some(role => userRoles.includes(role));
|
|
66
|
+
|
|
67
|
+
if (!hasRole) {
|
|
68
|
+
if (options.onForbidden) {
|
|
69
|
+
return options.onForbidden(req, res);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return res.status(403).json({
|
|
73
|
+
error: 'Insufficient permissions',
|
|
74
|
+
message: `Required roles: ${options.roles.join(', ')}`,
|
|
75
|
+
userRoles,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check permissions if specified
|
|
81
|
+
if (options.permissions && options.permissions.length > 0) {
|
|
82
|
+
const userPermissions = user?.permissions || [];
|
|
83
|
+
const hasPermission = options.permissions.every(permission =>
|
|
84
|
+
userPermissions.includes(permission)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (!hasPermission) {
|
|
88
|
+
if (options.onForbidden) {
|
|
89
|
+
return options.onForbidden(req, res);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return res.status(403).json({
|
|
93
|
+
error: 'Insufficient permissions',
|
|
94
|
+
message: `Required permissions: ${options.permissions.join(', ')}`,
|
|
95
|
+
userPermissions,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Custom authorization function
|
|
101
|
+
if (options.authorize) {
|
|
102
|
+
try {
|
|
103
|
+
const authorized = await options.authorize(user);
|
|
104
|
+
|
|
105
|
+
if (!authorized) {
|
|
106
|
+
if (options.onForbidden) {
|
|
107
|
+
return options.onForbidden(req, res);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return res.status(403).json({
|
|
111
|
+
error: 'Access denied',
|
|
112
|
+
message: 'Custom authorization check failed',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return res.status(500).json({
|
|
117
|
+
error: 'Authorization error',
|
|
118
|
+
message: 'Failed to verify authorization',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// All checks passed
|
|
124
|
+
next();
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Role-based access control middleware
|
|
130
|
+
*/
|
|
131
|
+
export function requireRole(
|
|
132
|
+
role: string | string[],
|
|
133
|
+
options: Omit<AuthGuardOptions, 'roles'> = {}
|
|
134
|
+
) {
|
|
135
|
+
const roles = Array.isArray(role) ? role : [role];
|
|
136
|
+
return requireAuth({ ...options, roles });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Permission-based access control middleware
|
|
141
|
+
*/
|
|
142
|
+
export function requirePermission(
|
|
143
|
+
permission: string | string[],
|
|
144
|
+
options: Omit<AuthGuardOptions, 'permissions'> = {}
|
|
145
|
+
) {
|
|
146
|
+
const permissions = Array.isArray(permission) ? permission : [permission];
|
|
147
|
+
return requireAuth({ ...options, permissions });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Admin-only access middleware
|
|
152
|
+
*/
|
|
153
|
+
export function requireAdmin(options: Omit<AuthGuardOptions, 'roles'> = {}) {
|
|
154
|
+
return requireRole('admin', options);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Guest-only middleware (redirect if authenticated)
|
|
159
|
+
*/
|
|
160
|
+
export function guestOnly(redirectTo = '/dashboard') {
|
|
161
|
+
return requireAuth({
|
|
162
|
+
allowUnauthenticated: true,
|
|
163
|
+
redirectOnAuth: redirectTo,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Optional auth middleware (allows both authenticated and unauthenticated)
|
|
169
|
+
*/
|
|
170
|
+
export function optionalAuth() {
|
|
171
|
+
return requireAuth({
|
|
172
|
+
allowUnauthenticated: true,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Route-level auth decorator
|
|
178
|
+
*/
|
|
179
|
+
export function withAuth(options: AuthRouteOptions = {}) {
|
|
180
|
+
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
181
|
+
const originalMethod = descriptor.value;
|
|
182
|
+
|
|
183
|
+
descriptor.value = async function (req: any, res: any, next: any) {
|
|
184
|
+
if (options.requireAuth !== false) {
|
|
185
|
+
const authMiddleware = requireAuth({
|
|
186
|
+
roles: options.roles,
|
|
187
|
+
redirectTo: options.redirectTo,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
await new Promise<void>((resolve, reject) => {
|
|
191
|
+
authMiddleware(req, res, (error: any) => {
|
|
192
|
+
if (error) reject(error);
|
|
193
|
+
else resolve();
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return originalMethod.call(this, req, res, next);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return descriptor;
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Auth utilities for manual checks in route handlers
|
|
207
|
+
*/
|
|
208
|
+
export const authUtils = {
|
|
209
|
+
/**
|
|
210
|
+
* Check if user is authenticated
|
|
211
|
+
*/
|
|
212
|
+
isAuthenticated(req: any): boolean {
|
|
213
|
+
return req.auth?.isAuthenticated || false;
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get current user
|
|
218
|
+
*/
|
|
219
|
+
getUser(req: any) {
|
|
220
|
+
return req.auth?.user || null;
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Check if user has role
|
|
225
|
+
*/
|
|
226
|
+
hasRole(req: any, role: string | string[]): boolean {
|
|
227
|
+
const user = this.getUser(req);
|
|
228
|
+
if (!user?.roles) return false;
|
|
229
|
+
|
|
230
|
+
const roles = Array.isArray(role) ? role : [role];
|
|
231
|
+
return roles.some(r => user.roles.includes(r));
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Check if user has permission
|
|
236
|
+
*/
|
|
237
|
+
hasPermission(req: any, permission: string | string[]): boolean {
|
|
238
|
+
const user = this.getUser(req);
|
|
239
|
+
if (!user?.permissions) return false;
|
|
240
|
+
|
|
241
|
+
const permissions = Array.isArray(permission) ? permission : [permission];
|
|
242
|
+
return permissions.every(p => user.permissions.includes(p));
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check if user is admin
|
|
247
|
+
*/
|
|
248
|
+
isAdmin(req: any): boolean {
|
|
249
|
+
return this.hasRole(req, 'admin');
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Get user ID
|
|
254
|
+
*/
|
|
255
|
+
getUserId(req: any): string | null {
|
|
256
|
+
return this.getUser(req)?.id || null;
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Force authentication check and redirect if needed
|
|
261
|
+
*/
|
|
262
|
+
ensureAuth(req: any, res: any, redirectTo = '/api/auth/signin'): boolean {
|
|
263
|
+
if (!this.isAuthenticated(req)) {
|
|
264
|
+
res.redirect(`${redirectTo}?callbackUrl=${encodeURIComponent(req.url)}`);
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
return true;
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Create auth response for API endpoints
|
|
272
|
+
*/
|
|
273
|
+
createAuthResponse(req: any) {
|
|
274
|
+
const auth = req.auth;
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
isAuthenticated: auth?.isAuthenticated || false,
|
|
278
|
+
user: auth?.user || null,
|
|
279
|
+
session: auth?.session || null,
|
|
280
|
+
timestamp: new Date().toISOString(),
|
|
281
|
+
};
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* API Response helpers for auth endpoints
|
|
287
|
+
*/
|
|
288
|
+
export const authResponses = {
|
|
289
|
+
unauthorized: (res: any, message = 'Authentication required') => {
|
|
290
|
+
return res.status(401).json({
|
|
291
|
+
error: 'Unauthorized',
|
|
292
|
+
message,
|
|
293
|
+
code: 'AUTH_REQUIRED',
|
|
294
|
+
signInUrl: '/api/auth/signin',
|
|
295
|
+
});
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
forbidden: (res: any, message = 'Insufficient permissions') => {
|
|
299
|
+
return res.status(403).json({
|
|
300
|
+
error: 'Forbidden',
|
|
301
|
+
message,
|
|
302
|
+
code: 'INSUFFICIENT_PERMISSIONS',
|
|
303
|
+
});
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
authSuccess: (res: any, user: any, message = 'Authentication successful') => {
|
|
307
|
+
return res.json({
|
|
308
|
+
success: true,
|
|
309
|
+
message,
|
|
310
|
+
user: {
|
|
311
|
+
id: user.id,
|
|
312
|
+
name: user.name,
|
|
313
|
+
email: user.email,
|
|
314
|
+
roles: user.roles || [],
|
|
315
|
+
permissions: user.permissions || [],
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
authError: (res: any, error: string, message = 'Authentication failed') => {
|
|
321
|
+
return res.status(400).json({
|
|
322
|
+
error,
|
|
323
|
+
message,
|
|
324
|
+
code: 'AUTH_ERROR',
|
|
325
|
+
});
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Higher-order function to create protected route handlers
|
|
331
|
+
*/
|
|
332
|
+
export function protectedRoute(
|
|
333
|
+
handler: (req: any, res: any, next?: any) => any,
|
|
334
|
+
options: AuthGuardOptions = {}
|
|
335
|
+
) {
|
|
336
|
+
return async (req: any, res: any, next: any) => {
|
|
337
|
+
const authMiddleware = requireAuth(options);
|
|
338
|
+
|
|
339
|
+
return new Promise<void>((resolve, reject) => {
|
|
340
|
+
authMiddleware(req, res, (error: any) => {
|
|
341
|
+
if (error) {
|
|
342
|
+
reject(error);
|
|
343
|
+
} else {
|
|
344
|
+
Promise.resolve(handler(req, res, next))
|
|
345
|
+
.then(() => resolve())
|
|
346
|
+
.catch(reject);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Session management helpers
|
|
355
|
+
*/
|
|
356
|
+
export const sessionHelpers = {
|
|
357
|
+
/**
|
|
358
|
+
* Store data in session
|
|
359
|
+
*/
|
|
360
|
+
async setSessionData(req: any, key: string, value: any) {
|
|
361
|
+
if (req.session) {
|
|
362
|
+
req.session[key] = value;
|
|
363
|
+
await req.session.save();
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Get data from session
|
|
369
|
+
*/
|
|
370
|
+
getSessionData(req: any, key: string) {
|
|
371
|
+
return req.session?.[key] || null;
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Remove data from session
|
|
376
|
+
*/
|
|
377
|
+
async removeSessionData(req: any, key: string) {
|
|
378
|
+
if (req.session && key in req.session.data) {
|
|
379
|
+
delete req.session.data[key];
|
|
380
|
+
await req.session.save();
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Clear entire session
|
|
386
|
+
*/
|
|
387
|
+
async clearSession(req: any) {
|
|
388
|
+
if (req.session) {
|
|
389
|
+
await req.session.destroy();
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Regenerate session ID
|
|
395
|
+
*/
|
|
396
|
+
async regenerateSession(req: any) {
|
|
397
|
+
if (req.session) {
|
|
398
|
+
return await req.session.regenerate();
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
};
|