frontend-hamroun 1.1.90 → 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/dist/{src/backend → backend}/api-utils.d.ts +2 -2
- package/dist/backend/api-utils.js +135 -0
- package/dist/backend/auth.js +387 -0
- package/dist/{src/backend → backend}/database.d.ts +1 -1
- package/dist/backend/database.js +91 -0
- package/dist/{src/backend → backend}/model.d.ts +2 -2
- package/dist/backend/model.js +176 -0
- package/dist/{src/backend → backend}/router.d.ts +1 -1
- package/dist/backend/router.js +137 -0
- package/dist/backend/server.js +268 -0
- package/dist/batch.js +22 -0
- package/dist/cli/index.js +1 -0
- package/dist/{src/component.d.ts → component.d.ts} +1 -1
- package/dist/component.js +84 -0
- package/dist/components/Counter.js +2 -0
- package/dist/context.js +20 -0
- package/dist/frontend-hamroun.es.js +1680 -0
- package/dist/frontend-hamroun.es.js.map +1 -0
- package/dist/frontend-hamroun.umd.js +2 -0
- package/dist/frontend-hamroun.umd.js.map +1 -0
- package/dist/hooks.js +164 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +52 -355
- package/dist/jsx-runtime/index.d.ts +9 -0
- package/dist/jsx-runtime/index.js +16 -0
- package/dist/jsx-runtime/jsx-dev-runtime.js +1 -0
- package/dist/jsx-runtime/jsx-runtime.js +91 -0
- package/dist/{src/jsx-runtime.d.ts → jsx-runtime.d.ts} +1 -1
- package/dist/jsx-runtime.js +192 -0
- package/dist/renderer.js +51 -0
- package/dist/{src/server-renderer.d.ts → server-renderer.d.ts} +3 -0
- package/dist/server-renderer.js +102 -0
- package/dist/vdom.js +27 -0
- package/package.json +38 -52
- package/scripts/generate.js +134 -0
- package/src/backend/api-utils.ts +178 -0
- package/src/backend/auth.ts +543 -0
- package/src/backend/database.ts +104 -0
- package/src/backend/model.ts +196 -0
- package/src/backend/router.ts +176 -0
- package/src/backend/server.ts +330 -0
- package/src/backend/types.ts +257 -0
- package/src/batch.ts +24 -0
- package/src/cli/index.js +22 -40
- package/src/component.ts +98 -0
- package/src/components/Counter.tsx +4 -0
- package/src/context.ts +32 -0
- package/src/hooks.ts +211 -0
- package/src/index.ts +113 -0
- package/src/jsx-runtime/index.ts +24 -0
- package/src/jsx-runtime/jsx-dev-runtime.ts +0 -0
- package/src/jsx-runtime/jsx-runtime.ts +99 -0
- package/src/jsx-runtime.ts +226 -0
- package/src/renderer.ts +55 -0
- package/src/server-renderer.ts +114 -0
- package/src/types/bcrypt.d.ts +30 -0
- package/src/types/jsonwebtoken.d.ts +55 -0
- package/src/types.d.ts +26 -0
- package/src/types.ts +21 -0
- package/src/vdom.ts +34 -0
- package/templates/basic-app/package.json +17 -15
- package/templates/basic-app/postcss.config.js +1 -0
- package/templates/basic-app/src/App.tsx +65 -0
- package/templates/basic-app/src/api.ts +58 -0
- package/templates/basic-app/src/components/Counter.tsx +26 -0
- package/templates/basic-app/src/components/Header.tsx +9 -0
- package/templates/basic-app/src/components/TodoList.tsx +90 -0
- package/templates/basic-app/src/main.ts +20 -0
- package/templates/basic-app/src/server.ts +99 -0
- package/templates/basic-app/tailwind.config.js +23 -2
- package/bin/cli.js +0 -371
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -139269
- package/dist/index.mjs.map +0 -1
- package/dist/src/index.d.ts +0 -16
- package/dist/test/setupTests.d.ts +0 -4
- /package/dist/{src/backend → backend}/auth.d.ts +0 -0
- /package/dist/{src/backend → backend}/server.d.ts +0 -0
- /package/dist/{src/backend → backend}/types.d.ts +0 -0
- /package/dist/{test/backend.test.d.ts → backend/types.js} +0 -0
- /package/dist/{src/batch.d.ts → batch.d.ts} +0 -0
- /package/dist/{src/cli → cli}/index.d.ts +0 -0
- /package/dist/{src/components → components}/Counter.d.ts +0 -0
- /package/dist/{src/context.d.ts → context.d.ts} +0 -0
- /package/dist/{src/hooks.d.ts → hooks.d.ts} +0 -0
- /package/dist/{src/jsx-runtime → jsx-runtime}/jsx-dev-runtime.d.ts +0 -0
- /package/dist/{src/jsx-runtime → jsx-runtime}/jsx-runtime.d.ts +0 -0
- /package/dist/{src/renderer.d.ts → renderer.d.ts} +0 -0
- /package/dist/{src/types.d.ts → types.d.ts} +0 -0
- /package/dist/{test/mockTest.d.ts → types.js} +0 -0
- /package/dist/{src/vdom.d.ts → vdom.d.ts} +0 -0
- /package/{dist/test/mongooseSetup.d.ts → src/cli/index.ts} +0 -0
@@ -27,7 +27,7 @@ export declare function validateRequest(schema: any): (req: Request, res: Respon
|
|
27
27
|
/**
|
28
28
|
* Create a new router with common middleware and options
|
29
29
|
*/
|
30
|
-
export declare function createApiRouter(options?: RouterOptions): Router;
|
30
|
+
export declare function createApiRouter(options?: RouterOptions, auth?: any): Router;
|
31
31
|
/**
|
32
32
|
* Wrap an async route handler to catch errors
|
33
33
|
*/
|
@@ -35,4 +35,4 @@ export declare function asyncHandler(fn: (req: Request, res: Response, next: Nex
|
|
35
35
|
/**
|
36
36
|
* Create standard REST endpoints for a model
|
37
37
|
*/
|
38
|
-
export declare function createRestEndpoints<T>(model: any): import(
|
38
|
+
export declare function createRestEndpoints<T>(model: any): import("express-serve-static-core").Router;
|
@@ -0,0 +1,135 @@
|
|
1
|
+
import { Router } from 'express';
|
2
|
+
/**
|
3
|
+
* Creates a consistent API response structure
|
4
|
+
*/
|
5
|
+
export function apiResponse(success, data, message, error, meta) {
|
6
|
+
const response = { success };
|
7
|
+
if (data !== undefined)
|
8
|
+
response.data = data;
|
9
|
+
if (message)
|
10
|
+
response.message = message;
|
11
|
+
if (error)
|
12
|
+
response.error = error;
|
13
|
+
if (meta)
|
14
|
+
response.meta = meta;
|
15
|
+
return response;
|
16
|
+
}
|
17
|
+
/**
|
18
|
+
* Helper to send successful responses
|
19
|
+
*/
|
20
|
+
export function sendSuccess(res, data, message, statusCode = 200, meta) {
|
21
|
+
res.status(statusCode).json(apiResponse(true, data, message, undefined, meta));
|
22
|
+
}
|
23
|
+
/**
|
24
|
+
* Helper to send error responses
|
25
|
+
*/
|
26
|
+
export function sendError(res, error, statusCode = 400, meta) {
|
27
|
+
const errorMessage = error instanceof Error ? error.message : error;
|
28
|
+
res.status(statusCode).json(apiResponse(false, undefined, undefined, errorMessage, meta));
|
29
|
+
}
|
30
|
+
/**
|
31
|
+
* Helper to extract pagination parameters from request
|
32
|
+
*/
|
33
|
+
export function getPaginationParams(req) {
|
34
|
+
const page = parseInt(req.query.page) || 1;
|
35
|
+
const limit = parseInt(req.query.limit) || 10;
|
36
|
+
const sort = req.query.sort || 'createdAt';
|
37
|
+
const order = req.query.order === 'asc' ? 'asc' : 'desc';
|
38
|
+
return { page, limit, sort, order };
|
39
|
+
}
|
40
|
+
/**
|
41
|
+
* Middleware to extract and validate pagination parameters
|
42
|
+
*/
|
43
|
+
export function paginationMiddleware(req, res, next) {
|
44
|
+
req.pagination = getPaginationParams(req);
|
45
|
+
next();
|
46
|
+
}
|
47
|
+
/**
|
48
|
+
* Middleware for basic request validation
|
49
|
+
*/
|
50
|
+
export function validateRequest(schema) {
|
51
|
+
return (req, res, next) => {
|
52
|
+
try {
|
53
|
+
const { error, value } = schema.validate(req.body);
|
54
|
+
if (error) {
|
55
|
+
sendError(res, `Validation error: ${error.message}`, 400);
|
56
|
+
return;
|
57
|
+
}
|
58
|
+
// Replace request body with validated value
|
59
|
+
req.body = value;
|
60
|
+
next();
|
61
|
+
}
|
62
|
+
catch (err) {
|
63
|
+
sendError(res, 'Validation error', 400);
|
64
|
+
}
|
65
|
+
};
|
66
|
+
}
|
67
|
+
/**
|
68
|
+
* Create a new router with common middleware and options
|
69
|
+
*/
|
70
|
+
export function createApiRouter(options = {}, auth) {
|
71
|
+
const router = Router();
|
72
|
+
// Add authentication middleware if required
|
73
|
+
if (options.requireAuth && auth) {
|
74
|
+
router.use(auth.authenticate);
|
75
|
+
// Add role check if specified
|
76
|
+
if (options.requiredRole) {
|
77
|
+
router.use(auth.hasRole(options.requiredRole));
|
78
|
+
}
|
79
|
+
}
|
80
|
+
// Add rate limiting if specified
|
81
|
+
if (options.rateLimit) {
|
82
|
+
console.warn('Rate limiting is disabled: express-rate-limit dependency is not installed');
|
83
|
+
// Note: To use rate limiting, install express-rate-limit: npm install express-rate-limit
|
84
|
+
}
|
85
|
+
return router;
|
86
|
+
}
|
87
|
+
/**
|
88
|
+
* Wrap an async route handler to catch errors
|
89
|
+
*/
|
90
|
+
export function asyncHandler(fn) {
|
91
|
+
return (req, res, next) => {
|
92
|
+
fn(req, res, next).catch(next);
|
93
|
+
};
|
94
|
+
}
|
95
|
+
/**
|
96
|
+
* Create standard REST endpoints for a model
|
97
|
+
*/
|
98
|
+
export function createRestEndpoints(model) {
|
99
|
+
const router = Router();
|
100
|
+
// GET all with pagination
|
101
|
+
router.get('/', paginationMiddleware, asyncHandler(async (req, res) => {
|
102
|
+
const result = await model.getAll(req.pagination);
|
103
|
+
sendSuccess(res, result);
|
104
|
+
}));
|
105
|
+
// GET by ID
|
106
|
+
router.get('/:id', asyncHandler(async (req, res) => {
|
107
|
+
const item = await model.getById(req.params.id);
|
108
|
+
if (!item) {
|
109
|
+
return sendError(res, 'Item not found', 404);
|
110
|
+
}
|
111
|
+
sendSuccess(res, item);
|
112
|
+
}));
|
113
|
+
// POST new item
|
114
|
+
router.post('/', asyncHandler(async (req, res) => {
|
115
|
+
const newItem = await model.create(req.body);
|
116
|
+
sendSuccess(res, newItem, 'Item created successfully', 201);
|
117
|
+
}));
|
118
|
+
// PUT update item
|
119
|
+
router.put('/:id', asyncHandler(async (req, res) => {
|
120
|
+
const updatedItem = await model.update(req.params.id, req.body);
|
121
|
+
if (!updatedItem) {
|
122
|
+
return sendError(res, 'Item not found', 404);
|
123
|
+
}
|
124
|
+
sendSuccess(res, updatedItem, 'Item updated successfully');
|
125
|
+
}));
|
126
|
+
// DELETE item
|
127
|
+
router.delete('/:id', asyncHandler(async (req, res) => {
|
128
|
+
const result = await model.delete(req.params.id);
|
129
|
+
if (!result) {
|
130
|
+
return sendError(res, 'Item not found', 404);
|
131
|
+
}
|
132
|
+
sendSuccess(res, null, 'Item deleted successfully');
|
133
|
+
}));
|
134
|
+
return router;
|
135
|
+
}
|
@@ -0,0 +1,387 @@
|
|
1
|
+
import jwt from 'jsonwebtoken';
|
2
|
+
import bcrypt from 'bcrypt';
|
3
|
+
import crypto from 'crypto';
|
4
|
+
/**
|
5
|
+
* Authentication service
|
6
|
+
*/
|
7
|
+
export class Auth {
|
8
|
+
constructor(options) {
|
9
|
+
this.loginAttempts = new Map();
|
10
|
+
/**
|
11
|
+
* Login middleware to authenticate users
|
12
|
+
*/
|
13
|
+
this.login = async (req, res) => {
|
14
|
+
try {
|
15
|
+
const { username, password } = req.body;
|
16
|
+
const ip = req.ip || req.connection.remoteAddress || '';
|
17
|
+
// Check rate limiting
|
18
|
+
if (!this.checkRateLimit(ip)) {
|
19
|
+
res.status(429).json({
|
20
|
+
success: false,
|
21
|
+
message: 'Too many login attempts. Please try again later.'
|
22
|
+
});
|
23
|
+
return;
|
24
|
+
}
|
25
|
+
if (!username || !password) {
|
26
|
+
res.status(400).json({
|
27
|
+
success: false,
|
28
|
+
message: 'Username and password are required'
|
29
|
+
});
|
30
|
+
return;
|
31
|
+
}
|
32
|
+
// Use custom find user function if provided
|
33
|
+
if (!this.options.findUser) {
|
34
|
+
res.status(500).json({
|
35
|
+
success: false,
|
36
|
+
message: 'User finder function not configured'
|
37
|
+
});
|
38
|
+
return;
|
39
|
+
}
|
40
|
+
const user = await this.options.findUser(username);
|
41
|
+
if (!user) {
|
42
|
+
res.status(401).json({
|
43
|
+
success: false,
|
44
|
+
message: 'Invalid credentials'
|
45
|
+
});
|
46
|
+
return;
|
47
|
+
}
|
48
|
+
// Verify password
|
49
|
+
const isPasswordValid = await this.options.verifyPassword(password, user.password);
|
50
|
+
if (!isPasswordValid) {
|
51
|
+
res.status(401).json({
|
52
|
+
success: false,
|
53
|
+
message: 'Invalid credentials'
|
54
|
+
});
|
55
|
+
return;
|
56
|
+
}
|
57
|
+
// Create a sanitized user object (without password)
|
58
|
+
const userWithoutPassword = { ...user };
|
59
|
+
delete userWithoutPassword.password;
|
60
|
+
// Generate tokens
|
61
|
+
const tokenPair = this.generateTokenPair({
|
62
|
+
id: user.id || user._id,
|
63
|
+
username: user.username,
|
64
|
+
role: user.role || 'user'
|
65
|
+
});
|
66
|
+
// Save refresh token if the function is provided
|
67
|
+
if (this.options.saveRefreshToken) {
|
68
|
+
const refreshExpiry = new Date();
|
69
|
+
refreshExpiry.setSeconds(refreshExpiry.getSeconds() +
|
70
|
+
this.getExpirationSeconds(this.options.refreshExpiration || '7d'));
|
71
|
+
await this.options.saveRefreshToken(user.id || user._id, tokenPair.refreshToken, refreshExpiry);
|
72
|
+
}
|
73
|
+
// Set authentication cookies if using cookie-based auth
|
74
|
+
if (req.body.useCookies) {
|
75
|
+
this.setAuthCookies(res, tokenPair);
|
76
|
+
}
|
77
|
+
res.json({
|
78
|
+
success: true,
|
79
|
+
message: 'Authentication successful',
|
80
|
+
tokens: tokenPair,
|
81
|
+
user: userWithoutPassword
|
82
|
+
});
|
83
|
+
}
|
84
|
+
catch (error) {
|
85
|
+
console.error('Authentication error:', error);
|
86
|
+
res.status(500).json({
|
87
|
+
success: false,
|
88
|
+
message: 'Authentication failed',
|
89
|
+
error: process.env.NODE_ENV !== 'production' ? error.message : undefined
|
90
|
+
});
|
91
|
+
}
|
92
|
+
};
|
93
|
+
/**
|
94
|
+
* Refresh token handler
|
95
|
+
*/
|
96
|
+
this.refreshToken = async (req, res) => {
|
97
|
+
try {
|
98
|
+
// Get refresh token from cookies or request body
|
99
|
+
const refreshToken = req.cookies?.refreshToken || req.body.refreshToken;
|
100
|
+
if (!refreshToken) {
|
101
|
+
res.status(401).json({
|
102
|
+
success: false,
|
103
|
+
message: 'Refresh token required'
|
104
|
+
});
|
105
|
+
return;
|
106
|
+
}
|
107
|
+
// Verify the refresh token
|
108
|
+
const decoded = this.verifyToken(refreshToken, 'refresh');
|
109
|
+
// Check if token is still valid in database if verification function provided
|
110
|
+
if (this.options.verifyRefreshToken) {
|
111
|
+
const isValid = await this.options.verifyRefreshToken(decoded.id, refreshToken);
|
112
|
+
if (!isValid) {
|
113
|
+
this.clearAuthCookies(res);
|
114
|
+
res.status(401).json({
|
115
|
+
success: false,
|
116
|
+
message: 'Invalid refresh token'
|
117
|
+
});
|
118
|
+
return;
|
119
|
+
}
|
120
|
+
}
|
121
|
+
// Generate new token pair
|
122
|
+
const tokenPair = this.generateTokenPair({
|
123
|
+
id: decoded.id,
|
124
|
+
// We need to fetch the user data again for complete payload
|
125
|
+
...(this.options.findUser ? await this.options.findUser(decoded.id) : {})
|
126
|
+
});
|
127
|
+
// Update refresh token in database if save function provided
|
128
|
+
if (this.options.saveRefreshToken) {
|
129
|
+
const refreshExpiry = new Date();
|
130
|
+
refreshExpiry.setSeconds(refreshExpiry.getSeconds() +
|
131
|
+
this.getExpirationSeconds(this.options.refreshExpiration || '7d'));
|
132
|
+
await this.options.saveRefreshToken(decoded.id, tokenPair.refreshToken, refreshExpiry);
|
133
|
+
}
|
134
|
+
// Set cookies if cookie-based auth is used
|
135
|
+
const useCookies = req.cookies?.accessToken || req.body.useCookies;
|
136
|
+
if (useCookies) {
|
137
|
+
this.setAuthCookies(res, tokenPair);
|
138
|
+
}
|
139
|
+
res.json({
|
140
|
+
success: true,
|
141
|
+
message: 'Token refreshed successfully',
|
142
|
+
tokens: tokenPair
|
143
|
+
});
|
144
|
+
}
|
145
|
+
catch (error) {
|
146
|
+
this.clearAuthCookies(res);
|
147
|
+
res.status(401).json({
|
148
|
+
success: false,
|
149
|
+
message: 'Invalid or expired refresh token',
|
150
|
+
error: process.env.NODE_ENV !== 'production' ? error.message : undefined
|
151
|
+
});
|
152
|
+
}
|
153
|
+
};
|
154
|
+
/**
|
155
|
+
* Logout handler
|
156
|
+
*/
|
157
|
+
this.logout = async (req, res) => {
|
158
|
+
try {
|
159
|
+
// Clear cookies
|
160
|
+
this.clearAuthCookies(res);
|
161
|
+
// Invalidate refresh token if verification function provided
|
162
|
+
const refreshToken = req.cookies?.refreshToken || req.body.refreshToken;
|
163
|
+
if (refreshToken && this.options.saveRefreshToken) {
|
164
|
+
try {
|
165
|
+
const decoded = this.verifyToken(refreshToken, 'refresh');
|
166
|
+
// Check if invalidateToken function exists, otherwise we just remove it from storage
|
167
|
+
if (typeof this.options.saveRefreshToken === 'function') {
|
168
|
+
// We pass null as the token to indicate deletion/invalidation
|
169
|
+
await this.options.saveRefreshToken(decoded.id, '', new Date());
|
170
|
+
}
|
171
|
+
}
|
172
|
+
catch (e) {
|
173
|
+
// Token already invalid, nothing to do
|
174
|
+
}
|
175
|
+
}
|
176
|
+
res.json({ success: true, message: 'Logged out successfully' });
|
177
|
+
}
|
178
|
+
catch (error) {
|
179
|
+
console.error('Logout error:', error);
|
180
|
+
res.status(500).json({ success: false, message: 'Logout failed', error: error.message });
|
181
|
+
}
|
182
|
+
};
|
183
|
+
/**
|
184
|
+
* Middleware to verify user is authenticated
|
185
|
+
*/
|
186
|
+
this.authenticate = (req, res, next) => {
|
187
|
+
try {
|
188
|
+
// Get token from Authorization header or cookies
|
189
|
+
let token = req.cookies?.accessToken;
|
190
|
+
if (!token) {
|
191
|
+
const authHeader = req.headers.authorization;
|
192
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
193
|
+
token = authHeader.split(' ')[1];
|
194
|
+
}
|
195
|
+
}
|
196
|
+
if (!token) {
|
197
|
+
res.status(401).json({
|
198
|
+
success: false,
|
199
|
+
message: 'Authentication required'
|
200
|
+
});
|
201
|
+
return;
|
202
|
+
}
|
203
|
+
const decoded = this.verifyToken(token, 'access');
|
204
|
+
// Add user info to request
|
205
|
+
req.user = decoded;
|
206
|
+
next();
|
207
|
+
}
|
208
|
+
catch (error) {
|
209
|
+
res.status(401).json({
|
210
|
+
success: false,
|
211
|
+
message: 'Invalid or expired token',
|
212
|
+
error: process.env.NODE_ENV !== 'production' ? error.message : undefined
|
213
|
+
});
|
214
|
+
}
|
215
|
+
};
|
216
|
+
/**
|
217
|
+
* Middleware to check if user has required role
|
218
|
+
*/
|
219
|
+
this.hasRole = (role) => {
|
220
|
+
return (req, res, next) => {
|
221
|
+
const user = req.user;
|
222
|
+
if (!user) {
|
223
|
+
res.status(401).json({
|
224
|
+
success: false,
|
225
|
+
message: 'Authentication required'
|
226
|
+
});
|
227
|
+
return;
|
228
|
+
}
|
229
|
+
const roles = Array.isArray(role) ? role : [role];
|
230
|
+
if (roles.includes(user.role)) {
|
231
|
+
next();
|
232
|
+
}
|
233
|
+
else {
|
234
|
+
res.status(403).json({
|
235
|
+
success: false,
|
236
|
+
message: 'Insufficient permissions'
|
237
|
+
});
|
238
|
+
}
|
239
|
+
};
|
240
|
+
};
|
241
|
+
this.options = {
|
242
|
+
tokenExpiration: '15m',
|
243
|
+
refreshExpiration: '7d',
|
244
|
+
saltRounds: 10,
|
245
|
+
secureCookies: process.env.NODE_ENV === 'production',
|
246
|
+
httpOnlyCookies: true,
|
247
|
+
rateLimit: true,
|
248
|
+
...options,
|
249
|
+
refreshSecret: options.refreshSecret || options.jwtSecret
|
250
|
+
};
|
251
|
+
if (!options.jwtSecret) {
|
252
|
+
throw new Error('JWT secret is required for authentication');
|
253
|
+
}
|
254
|
+
// Set default password verification if not provided
|
255
|
+
if (!this.options.verifyPassword) {
|
256
|
+
this.options.verifyPassword = this.verifyPasswordWithBcrypt;
|
257
|
+
}
|
258
|
+
}
|
259
|
+
/**
|
260
|
+
* Hash a password using bcrypt
|
261
|
+
*/
|
262
|
+
async hashPassword(password) {
|
263
|
+
return await bcrypt.hash(password, this.options.saltRounds || 10);
|
264
|
+
}
|
265
|
+
/**
|
266
|
+
* Verify a password against a hash using bcrypt
|
267
|
+
*/
|
268
|
+
async verifyPasswordWithBcrypt(password, hashedPassword) {
|
269
|
+
return await bcrypt.compare(password, hashedPassword);
|
270
|
+
}
|
271
|
+
/**
|
272
|
+
* Generate a cryptographically secure random token
|
273
|
+
*/
|
274
|
+
generateSecureToken(length = 32) {
|
275
|
+
return crypto.randomBytes(length).toString('hex');
|
276
|
+
}
|
277
|
+
/**
|
278
|
+
* Generate token pair (access token + refresh token)
|
279
|
+
*/
|
280
|
+
generateTokenPair(payload) {
|
281
|
+
// Calculate expiration times
|
282
|
+
const expiresIn = this.getExpirationSeconds(this.options.tokenExpiration || '15m');
|
283
|
+
// Generate the access token
|
284
|
+
const accessToken = jwt.sign({ ...payload, type: 'access' }, this.options.jwtSecret, { expiresIn: this.options.tokenExpiration });
|
285
|
+
// Generate the refresh token
|
286
|
+
const refreshToken = jwt.sign({ id: payload.id, type: 'refresh' }, this.options.refreshSecret, { expiresIn: this.options.refreshExpiration });
|
287
|
+
return {
|
288
|
+
accessToken,
|
289
|
+
refreshToken,
|
290
|
+
expiresIn
|
291
|
+
};
|
292
|
+
}
|
293
|
+
/**
|
294
|
+
* Convert JWT expiration time to seconds
|
295
|
+
*/
|
296
|
+
getExpirationSeconds(expiration) {
|
297
|
+
const unit = expiration.charAt(expiration.length - 1);
|
298
|
+
const value = parseInt(expiration.slice(0, -1));
|
299
|
+
switch (unit) {
|
300
|
+
case 's': return value;
|
301
|
+
case 'm': return value * 60;
|
302
|
+
case 'h': return value * 60 * 60;
|
303
|
+
case 'd': return value * 60 * 60 * 24;
|
304
|
+
default: return 3600; // Default 1 hour
|
305
|
+
}
|
306
|
+
}
|
307
|
+
/**
|
308
|
+
* Verify a JWT token
|
309
|
+
*/
|
310
|
+
verifyToken(token, type = 'access') {
|
311
|
+
try {
|
312
|
+
const secret = type === 'access' ? this.options.jwtSecret : this.options.refreshSecret;
|
313
|
+
const decoded = jwt.verify(token, secret);
|
314
|
+
// Verify token type matches expected type
|
315
|
+
if (typeof decoded === 'object' && decoded.type !== type) {
|
316
|
+
throw new Error('Invalid token type');
|
317
|
+
}
|
318
|
+
return decoded;
|
319
|
+
}
|
320
|
+
catch (error) {
|
321
|
+
throw new Error('Invalid or expired token');
|
322
|
+
}
|
323
|
+
}
|
324
|
+
/**
|
325
|
+
* Set authentication cookies
|
326
|
+
*/
|
327
|
+
setAuthCookies(res, tokens) {
|
328
|
+
// Set access token cookie
|
329
|
+
res.cookie('accessToken', tokens.accessToken, {
|
330
|
+
httpOnly: this.options.httpOnlyCookies,
|
331
|
+
secure: this.options.secureCookies,
|
332
|
+
domain: this.options.cookieDomain,
|
333
|
+
sameSite: 'strict',
|
334
|
+
maxAge: tokens.expiresIn * 1000
|
335
|
+
});
|
336
|
+
// Set refresh token cookie with longer expiration
|
337
|
+
const refreshExpiresIn = this.getExpirationSeconds(this.options.refreshExpiration || '7d');
|
338
|
+
res.cookie('refreshToken', tokens.refreshToken, {
|
339
|
+
httpOnly: true, // Always HTTP only for refresh tokens
|
340
|
+
secure: this.options.secureCookies,
|
341
|
+
domain: this.options.cookieDomain,
|
342
|
+
sameSite: 'strict',
|
343
|
+
maxAge: refreshExpiresIn * 1000,
|
344
|
+
path: '/api/auth/refresh' // Restrict to refresh endpoint
|
345
|
+
});
|
346
|
+
}
|
347
|
+
/**
|
348
|
+
* Clear authentication cookies
|
349
|
+
*/
|
350
|
+
clearAuthCookies(res) {
|
351
|
+
res.clearCookie('accessToken');
|
352
|
+
res.clearCookie('refreshToken', { path: '/api/auth/refresh' });
|
353
|
+
}
|
354
|
+
/**
|
355
|
+
* Check and handle rate limiting
|
356
|
+
*/
|
357
|
+
checkRateLimit(ip) {
|
358
|
+
if (!this.options.rateLimit) {
|
359
|
+
return true;
|
360
|
+
}
|
361
|
+
const now = Date.now();
|
362
|
+
const attempt = this.loginAttempts.get(ip);
|
363
|
+
if (!attempt) {
|
364
|
+
this.loginAttempts.set(ip, { count: 1, resetTime: now + 3600000 });
|
365
|
+
return true;
|
366
|
+
}
|
367
|
+
// Reset if time expired
|
368
|
+
if (now > attempt.resetTime) {
|
369
|
+
this.loginAttempts.set(ip, { count: 1, resetTime: now + 3600000 });
|
370
|
+
return true;
|
371
|
+
}
|
372
|
+
// Check attempts
|
373
|
+
if (attempt.count >= 5) {
|
374
|
+
return false;
|
375
|
+
}
|
376
|
+
// Increment counter
|
377
|
+
attempt.count++;
|
378
|
+
this.loginAttempts.set(ip, attempt);
|
379
|
+
return true;
|
380
|
+
}
|
381
|
+
}
|
382
|
+
/**
|
383
|
+
* Create an authentication service
|
384
|
+
*/
|
385
|
+
export function createAuth(options) {
|
386
|
+
return new Auth(options);
|
387
|
+
}
|
@@ -0,0 +1,91 @@
|
|
1
|
+
import mongoose from 'mongoose';
|
2
|
+
/**
|
3
|
+
* Database connector for MongoDB using Mongoose
|
4
|
+
*/
|
5
|
+
export class DatabaseConnector {
|
6
|
+
constructor(options) {
|
7
|
+
this.connection = null;
|
8
|
+
this._connected = false; // Renamed from isConnected to _connected
|
9
|
+
this.options = {
|
10
|
+
retryAttempts: 3,
|
11
|
+
retryDelay: 1000,
|
12
|
+
connectionTimeout: 10000,
|
13
|
+
autoIndex: true,
|
14
|
+
...options
|
15
|
+
};
|
16
|
+
}
|
17
|
+
/**
|
18
|
+
* Connect to the database
|
19
|
+
*/
|
20
|
+
async connect() {
|
21
|
+
try {
|
22
|
+
if (this._connected && this.connection) {
|
23
|
+
return this.connection;
|
24
|
+
}
|
25
|
+
// Set Mongoose options
|
26
|
+
mongoose.set('strictQuery', true);
|
27
|
+
// Connect with retry logic
|
28
|
+
let attempts = 0;
|
29
|
+
while (attempts < (this.options.retryAttempts || 3)) {
|
30
|
+
try {
|
31
|
+
await mongoose.connect(this.options.uri, {
|
32
|
+
dbName: this.options.name,
|
33
|
+
connectTimeoutMS: this.options.connectionTimeout,
|
34
|
+
autoIndex: this.options.autoIndex,
|
35
|
+
...this.options.options
|
36
|
+
});
|
37
|
+
break;
|
38
|
+
}
|
39
|
+
catch (error) {
|
40
|
+
attempts++;
|
41
|
+
if (attempts >= (this.options.retryAttempts || 3)) {
|
42
|
+
throw error;
|
43
|
+
}
|
44
|
+
console.log(`Connection attempt ${attempts} failed. Retrying in ${this.options.retryDelay}ms...`);
|
45
|
+
await new Promise(resolve => setTimeout(resolve, this.options.retryDelay));
|
46
|
+
}
|
47
|
+
}
|
48
|
+
this.connection = mongoose.connection;
|
49
|
+
this._connected = true;
|
50
|
+
// Log successful connection
|
51
|
+
console.log(`Connected to MongoDB at ${this.options.uri}/${this.options.name}`);
|
52
|
+
// Handle connection events
|
53
|
+
this.connection.on('error', (err) => {
|
54
|
+
console.error('MongoDB connection error:', err);
|
55
|
+
this._connected = false;
|
56
|
+
});
|
57
|
+
this.connection.on('disconnected', () => {
|
58
|
+
console.log('MongoDB disconnected');
|
59
|
+
this._connected = false;
|
60
|
+
});
|
61
|
+
return this.connection;
|
62
|
+
}
|
63
|
+
catch (error) {
|
64
|
+
console.error('Failed to connect to MongoDB:', error);
|
65
|
+
throw error;
|
66
|
+
}
|
67
|
+
}
|
68
|
+
/**
|
69
|
+
* Disconnect from the database
|
70
|
+
*/
|
71
|
+
async disconnect() {
|
72
|
+
if (this.connection) {
|
73
|
+
await mongoose.disconnect();
|
74
|
+
this._connected = false;
|
75
|
+
this.connection = null;
|
76
|
+
console.log('Disconnected from MongoDB');
|
77
|
+
}
|
78
|
+
}
|
79
|
+
/**
|
80
|
+
* Check if connected to the database
|
81
|
+
*/
|
82
|
+
isConnected() {
|
83
|
+
return this._connected;
|
84
|
+
}
|
85
|
+
/**
|
86
|
+
* Get the mongoose connection
|
87
|
+
*/
|
88
|
+
getConnection() {
|
89
|
+
return this.connection;
|
90
|
+
}
|
91
|
+
}
|
@@ -24,12 +24,12 @@ export declare const FieldTypes: {
|
|
24
24
|
type: DateConstructor;
|
25
25
|
};
|
26
26
|
ObjectId: {
|
27
|
-
type:
|
27
|
+
type: StringConstructor;
|
28
28
|
};
|
29
29
|
Required: (fieldType: any) => any;
|
30
30
|
Unique: (fieldType: any) => any;
|
31
31
|
Ref: (model: string) => {
|
32
|
-
type:
|
32
|
+
type: StringConstructor;
|
33
33
|
ref: string;
|
34
34
|
};
|
35
35
|
Enum: (values: any[]) => {
|