voltjs-framework 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/LICENSE +21 -0
- package/README.md +1265 -0
- package/bin/volt.js +139 -0
- package/package.json +56 -0
- package/src/api/graphql.js +399 -0
- package/src/api/rest.js +204 -0
- package/src/api/websocket.js +285 -0
- package/src/cli/build.js +111 -0
- package/src/cli/create.js +371 -0
- package/src/cli/db.js +106 -0
- package/src/cli/dev.js +114 -0
- package/src/cli/generate.js +278 -0
- package/src/cli/lint.js +172 -0
- package/src/cli/routes.js +118 -0
- package/src/cli/start.js +42 -0
- package/src/cli/test.js +138 -0
- package/src/core/app.js +701 -0
- package/src/core/config.js +232 -0
- package/src/core/middleware.js +133 -0
- package/src/core/plugins.js +88 -0
- package/src/core/react-renderer.js +244 -0
- package/src/core/renderer.js +337 -0
- package/src/core/router.js +183 -0
- package/src/database/index.js +461 -0
- package/src/database/migration.js +192 -0
- package/src/database/model.js +285 -0
- package/src/database/query.js +394 -0
- package/src/database/seeder.js +89 -0
- package/src/index.js +156 -0
- package/src/security/auth.js +425 -0
- package/src/security/cors.js +80 -0
- package/src/security/csrf.js +125 -0
- package/src/security/encryption.js +110 -0
- package/src/security/helmet.js +103 -0
- package/src/security/index.js +75 -0
- package/src/security/rateLimit.js +119 -0
- package/src/security/sanitizer.js +113 -0
- package/src/security/xss.js +110 -0
- package/src/ui/component.js +224 -0
- package/src/ui/reactive.js +503 -0
- package/src/ui/template.js +448 -0
- package/src/utils/cache.js +216 -0
- package/src/utils/collection.js +772 -0
- package/src/utils/cron.js +213 -0
- package/src/utils/date.js +223 -0
- package/src/utils/events.js +181 -0
- package/src/utils/excel.js +482 -0
- package/src/utils/form.js +547 -0
- package/src/utils/hash.js +121 -0
- package/src/utils/http.js +461 -0
- package/src/utils/logger.js +186 -0
- package/src/utils/mail.js +347 -0
- package/src/utils/paginator.js +179 -0
- package/src/utils/pdf.js +417 -0
- package/src/utils/queue.js +199 -0
- package/src/utils/schema.js +985 -0
- package/src/utils/sms.js +243 -0
- package/src/utils/storage.js +348 -0
- package/src/utils/string.js +236 -0
- package/src/utils/validation.js +318 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoltJS Database Seeder
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* // seeders/users.js
|
|
6
|
+
* module.exports = async (db) => {
|
|
7
|
+
* const User = db.model('users', { name: 'string', email: 'string' });
|
|
8
|
+
* await User.create({ name: 'Admin', email: 'admin@example.com' });
|
|
9
|
+
* await User.create({ name: 'John', email: 'john@example.com' });
|
|
10
|
+
* };
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
class Seeder {
|
|
19
|
+
constructor(db, seedersDir) {
|
|
20
|
+
this._db = db;
|
|
21
|
+
this._dir = seedersDir || path.join(process.cwd(), 'seeders');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Run all seeders */
|
|
25
|
+
async run() {
|
|
26
|
+
const files = this._getSeederFiles();
|
|
27
|
+
if (files.length === 0) {
|
|
28
|
+
console.log('\x1b[33m No seeders found.\x1b[0m');
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const results = [];
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
try {
|
|
35
|
+
const seeder = require(path.join(this._dir, file));
|
|
36
|
+
console.log(`\x1b[36m Seeding: ${file}\x1b[0m`);
|
|
37
|
+
if (typeof seeder === 'function') {
|
|
38
|
+
await seeder(this._db);
|
|
39
|
+
} else if (typeof seeder.run === 'function') {
|
|
40
|
+
await seeder.run(this._db);
|
|
41
|
+
}
|
|
42
|
+
console.log(`\x1b[32m ✓ Seeded: ${file}\x1b[0m`);
|
|
43
|
+
results.push({ file, status: 'success' });
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error(`\x1b[31m ✗ Seeder failed: ${file} — ${err.message}\x1b[0m`);
|
|
46
|
+
results.push({ file, status: 'error', error: err.message });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return results;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Get seeder files sorted by name */
|
|
54
|
+
_getSeederFiles() {
|
|
55
|
+
try {
|
|
56
|
+
if (!fs.existsSync(this._dir)) return [];
|
|
57
|
+
return fs.readdirSync(this._dir).filter(f => f.endsWith('.js')).sort();
|
|
58
|
+
} catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Generate a new seeder file */
|
|
64
|
+
static generate(name, seedersDir) {
|
|
65
|
+
const dir = seedersDir || path.join(process.cwd(), 'seeders');
|
|
66
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
67
|
+
|
|
68
|
+
const filename = `${name}.seed.js`;
|
|
69
|
+
const filepath = path.join(dir, filename);
|
|
70
|
+
|
|
71
|
+
const template = `/**
|
|
72
|
+
* Seeder: ${name}
|
|
73
|
+
* Created: ${new Date().toISOString()}
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
module.exports = async (db) => {
|
|
77
|
+
// Example: seed ${name} table
|
|
78
|
+
// const Model = db.model('${name}', { /* schema */ });
|
|
79
|
+
// await Model.create({ ... });
|
|
80
|
+
};
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
fs.writeFileSync(filepath, template);
|
|
84
|
+
console.log(`\x1b[32m ✓ Created seeder: ${filename}\x1b[0m`);
|
|
85
|
+
return filepath;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { Seeder };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoltJS - Lightning-fast, batteries-included, security-first JavaScript framework
|
|
3
|
+
*
|
|
4
|
+
* @description One framework to rule them all. Zero boilerplate. Maximum power.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - File-based routing + programmatic routing
|
|
8
|
+
* - Built-in security (CSRF, XSS, CORS, Rate Limiting, Helmet)
|
|
9
|
+
* - Built-in authentication (JWT, Sessions, OAuth)
|
|
10
|
+
* - Built-in database ORM with migrations
|
|
11
|
+
* - Built-in utilities (Excel, Mail, SMS, PDF, Validation, Cache, Queue, Cron)
|
|
12
|
+
* - Reactive UI components (compiled, no virtual DOM)
|
|
13
|
+
* - Full-stack: SSR + CSR + API in one
|
|
14
|
+
* - Plugin architecture
|
|
15
|
+
* - WebSocket support
|
|
16
|
+
* - GraphQL support
|
|
17
|
+
* - Zero config needed
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const { Volt } = require('voltjs');
|
|
21
|
+
*
|
|
22
|
+
* const app = new Volt();
|
|
23
|
+
*
|
|
24
|
+
* app.get('/', (req, res) => {
|
|
25
|
+
* res.render('Welcome to VoltJS!');
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* app.listen(3000);
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
'use strict';
|
|
32
|
+
|
|
33
|
+
// Core
|
|
34
|
+
const { Volt } = require('./core/app');
|
|
35
|
+
const { Router } = require('./core/router');
|
|
36
|
+
const { Middleware } = require('./core/middleware');
|
|
37
|
+
const { Config } = require('./core/config');
|
|
38
|
+
const { Renderer } = require('./core/renderer');
|
|
39
|
+
const { PluginManager } = require('./core/plugins');
|
|
40
|
+
const { ReactRenderer } = require('./core/react-renderer');
|
|
41
|
+
|
|
42
|
+
// Security
|
|
43
|
+
const { Security } = require('./security');
|
|
44
|
+
const { Auth } = require('./security/auth');
|
|
45
|
+
const { CSRF } = require('./security/csrf');
|
|
46
|
+
const { XSSProtection } = require('./security/xss');
|
|
47
|
+
const { CORSHandler } = require('./security/cors');
|
|
48
|
+
const { RateLimiter } = require('./security/rateLimit');
|
|
49
|
+
const { Encryption } = require('./security/encryption');
|
|
50
|
+
const { SecurityHeaders } = require('./security/helmet');
|
|
51
|
+
const { InputSanitizer } = require('./security/sanitizer');
|
|
52
|
+
|
|
53
|
+
// Database
|
|
54
|
+
const { Database } = require('./database');
|
|
55
|
+
const { Model } = require('./database/model');
|
|
56
|
+
const { QueryBuilder } = require('./database/query');
|
|
57
|
+
const { Migration } = require('./database/migration');
|
|
58
|
+
const { Seeder } = require('./database/seeder');
|
|
59
|
+
|
|
60
|
+
// Utilities
|
|
61
|
+
const { Excel } = require('./utils/excel');
|
|
62
|
+
const { Mail } = require('./utils/mail');
|
|
63
|
+
const { SMS } = require('./utils/sms');
|
|
64
|
+
const { PDF } = require('./utils/pdf');
|
|
65
|
+
const { Validator } = require('./utils/validation');
|
|
66
|
+
const { Cache } = require('./utils/cache');
|
|
67
|
+
const { Logger } = require('./utils/logger');
|
|
68
|
+
const { Storage } = require('./utils/storage');
|
|
69
|
+
const { Queue } = require('./utils/queue');
|
|
70
|
+
const { Cron } = require('./utils/cron');
|
|
71
|
+
const { Hash } = require('./utils/hash');
|
|
72
|
+
const { DateHelper } = require('./utils/date');
|
|
73
|
+
const { StringHelper } = require('./utils/string');
|
|
74
|
+
const { HttpClient } = require('./utils/http');
|
|
75
|
+
const { EventBus } = require('./utils/events');
|
|
76
|
+
const { Paginator } = require('./utils/paginator');
|
|
77
|
+
const { Form } = require('./utils/form');
|
|
78
|
+
const { _, Collection } = require('./utils/collection');
|
|
79
|
+
const { Schema } = require('./utils/schema');
|
|
80
|
+
|
|
81
|
+
// UI
|
|
82
|
+
const { Component } = require('./ui/component');
|
|
83
|
+
const { Reactive } = require('./ui/reactive');
|
|
84
|
+
const { TemplateEngine } = require('./ui/template');
|
|
85
|
+
|
|
86
|
+
// API
|
|
87
|
+
const { RestAPI } = require('./api/rest');
|
|
88
|
+
const { WebSocketServer } = require('./api/websocket');
|
|
89
|
+
const { GraphQLHandler } = require('./api/graphql');
|
|
90
|
+
|
|
91
|
+
// Export everything - one import, full power
|
|
92
|
+
module.exports = {
|
|
93
|
+
// Main framework
|
|
94
|
+
Volt,
|
|
95
|
+
Router,
|
|
96
|
+
Middleware,
|
|
97
|
+
Config,
|
|
98
|
+
Renderer,
|
|
99
|
+
ReactRenderer,
|
|
100
|
+
PluginManager,
|
|
101
|
+
|
|
102
|
+
// Security
|
|
103
|
+
Security,
|
|
104
|
+
Auth,
|
|
105
|
+
CSRF,
|
|
106
|
+
XSSProtection,
|
|
107
|
+
CORSHandler,
|
|
108
|
+
RateLimiter,
|
|
109
|
+
Encryption,
|
|
110
|
+
SecurityHeaders,
|
|
111
|
+
InputSanitizer,
|
|
112
|
+
|
|
113
|
+
// Database
|
|
114
|
+
Database,
|
|
115
|
+
Model,
|
|
116
|
+
QueryBuilder,
|
|
117
|
+
Migration,
|
|
118
|
+
Seeder,
|
|
119
|
+
|
|
120
|
+
// Utilities
|
|
121
|
+
Excel,
|
|
122
|
+
Mail,
|
|
123
|
+
SMS,
|
|
124
|
+
PDF,
|
|
125
|
+
Validator,
|
|
126
|
+
Cache,
|
|
127
|
+
Logger,
|
|
128
|
+
Storage,
|
|
129
|
+
Queue,
|
|
130
|
+
Cron,
|
|
131
|
+
Hash,
|
|
132
|
+
DateHelper,
|
|
133
|
+
StringHelper,
|
|
134
|
+
HttpClient,
|
|
135
|
+
EventBus,
|
|
136
|
+
Paginator,
|
|
137
|
+
Form,
|
|
138
|
+
_,
|
|
139
|
+
Collection,
|
|
140
|
+
Schema,
|
|
141
|
+
|
|
142
|
+
// UI
|
|
143
|
+
Component,
|
|
144
|
+
Reactive,
|
|
145
|
+
TemplateEngine,
|
|
146
|
+
|
|
147
|
+
// API
|
|
148
|
+
RestAPI,
|
|
149
|
+
WebSocketServer,
|
|
150
|
+
GraphQLHandler,
|
|
151
|
+
|
|
152
|
+
// Convenience: create a new app instance
|
|
153
|
+
create(config = {}) {
|
|
154
|
+
return new Volt(config);
|
|
155
|
+
},
|
|
156
|
+
};
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoltJS Authentication
|
|
3
|
+
*
|
|
4
|
+
* Built-in auth with support for:
|
|
5
|
+
* - JWT tokens (access + refresh)
|
|
6
|
+
* - Session-based auth
|
|
7
|
+
* - API key auth
|
|
8
|
+
* - OAuth2 helpers
|
|
9
|
+
* - Password hashing (bcrypt-compatible using native crypto)
|
|
10
|
+
* - Role-based access control (RBAC)
|
|
11
|
+
* - Two-factor authentication (TOTP)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
|
|
18
|
+
class Auth {
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
this._secret = options.secret || crypto.randomBytes(64).toString('hex');
|
|
21
|
+
this._algorithm = options.algorithm || 'HS256';
|
|
22
|
+
this._tokenExpiry = options.tokenExpiry || '24h';
|
|
23
|
+
this._refreshExpiry = options.refreshExpiry || '7d';
|
|
24
|
+
this._sessions = new Map();
|
|
25
|
+
this._apiKeys = new Map();
|
|
26
|
+
this._users = options.userProvider || null; // Function to look up users
|
|
27
|
+
this._roles = new Map();
|
|
28
|
+
this._permissions = new Map();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ===== JWT =====
|
|
32
|
+
|
|
33
|
+
/** Generate a JWT token */
|
|
34
|
+
generateToken(payload, expiresIn) {
|
|
35
|
+
const header = { alg: this._algorithm, typ: 'JWT' };
|
|
36
|
+
const exp = this._parseExpiry(expiresIn || this._tokenExpiry);
|
|
37
|
+
|
|
38
|
+
const tokenPayload = {
|
|
39
|
+
...payload,
|
|
40
|
+
iat: Math.floor(Date.now() / 1000),
|
|
41
|
+
exp: Math.floor(Date.now() / 1000) + exp,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const headerB64 = this._base64url(JSON.stringify(header));
|
|
45
|
+
const payloadB64 = this._base64url(JSON.stringify(tokenPayload));
|
|
46
|
+
const signature = this._sign(`${headerB64}.${payloadB64}`);
|
|
47
|
+
|
|
48
|
+
return `${headerB64}.${payloadB64}.${signature}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Generate access + refresh token pair */
|
|
52
|
+
generateTokenPair(payload) {
|
|
53
|
+
return {
|
|
54
|
+
accessToken: this.generateToken(payload, this._tokenExpiry),
|
|
55
|
+
refreshToken: this.generateToken({ ...payload, type: 'refresh' }, this._refreshExpiry),
|
|
56
|
+
expiresIn: this._parseExpiry(this._tokenExpiry),
|
|
57
|
+
tokenType: 'Bearer',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Verify and decode a JWT token */
|
|
62
|
+
verifyToken(token) {
|
|
63
|
+
try {
|
|
64
|
+
const parts = token.split('.');
|
|
65
|
+
if (parts.length !== 3) return { valid: false, error: 'Invalid token format' };
|
|
66
|
+
|
|
67
|
+
const [headerB64, payloadB64, signature] = parts;
|
|
68
|
+
|
|
69
|
+
// Verify signature
|
|
70
|
+
const expected = this._sign(`${headerB64}.${payloadB64}`);
|
|
71
|
+
if (signature !== expected) {
|
|
72
|
+
return { valid: false, error: 'Invalid signature' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Decode payload
|
|
76
|
+
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
|
|
77
|
+
|
|
78
|
+
// Check expiration
|
|
79
|
+
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
|
|
80
|
+
return { valid: false, error: 'Token expired' };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { valid: true, payload };
|
|
84
|
+
} catch (err) {
|
|
85
|
+
return { valid: false, error: err.message };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Middleware: Require valid JWT token */
|
|
90
|
+
requireAuth(options = {}) {
|
|
91
|
+
return (req, res) => {
|
|
92
|
+
const token = this._extractToken(req);
|
|
93
|
+
if (!token) {
|
|
94
|
+
res.json({ error: 'Authentication required' }, 401);
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const result = this.verifyToken(token);
|
|
99
|
+
if (!result.valid) {
|
|
100
|
+
res.json({ error: result.error }, 401);
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
req.user = result.payload;
|
|
105
|
+
req.token = token;
|
|
106
|
+
|
|
107
|
+
// Check roles if specified
|
|
108
|
+
if (options.roles) {
|
|
109
|
+
const userRoles = result.payload.roles || [];
|
|
110
|
+
const hasRole = options.roles.some(r => userRoles.includes(r));
|
|
111
|
+
if (!hasRole) {
|
|
112
|
+
res.json({ error: 'Insufficient permissions' }, 403);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check permissions if specified
|
|
118
|
+
if (options.permissions) {
|
|
119
|
+
const userPerms = result.payload.permissions || [];
|
|
120
|
+
const hasPermission = options.permissions.every(p => userPerms.includes(p));
|
|
121
|
+
if (!hasPermission) {
|
|
122
|
+
res.json({ error: 'Insufficient permissions' }, 403);
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Middleware: Optional auth (populates req.user if token exists) */
|
|
130
|
+
optionalAuth() {
|
|
131
|
+
return (req, res) => {
|
|
132
|
+
const token = this._extractToken(req);
|
|
133
|
+
if (token) {
|
|
134
|
+
const result = this.verifyToken(token);
|
|
135
|
+
if (result.valid) {
|
|
136
|
+
req.user = result.payload;
|
|
137
|
+
req.token = token;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ===== SESSIONS =====
|
|
144
|
+
|
|
145
|
+
/** Create a new session */
|
|
146
|
+
createSession(userId, data = {}) {
|
|
147
|
+
const sessionId = crypto.randomBytes(32).toString('hex');
|
|
148
|
+
const session = {
|
|
149
|
+
id: sessionId,
|
|
150
|
+
userId,
|
|
151
|
+
data,
|
|
152
|
+
createdAt: new Date(),
|
|
153
|
+
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
|
154
|
+
};
|
|
155
|
+
this._sessions.set(sessionId, session);
|
|
156
|
+
return session;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Get session by ID */
|
|
160
|
+
getSession(sessionId) {
|
|
161
|
+
const session = this._sessions.get(sessionId);
|
|
162
|
+
if (!session) return null;
|
|
163
|
+
if (session.expiresAt < new Date()) {
|
|
164
|
+
this._sessions.delete(sessionId);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
return session;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Destroy a session */
|
|
171
|
+
destroySession(sessionId) {
|
|
172
|
+
this._sessions.delete(sessionId);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Middleware: Session-based auth */
|
|
176
|
+
requireSession(options = {}) {
|
|
177
|
+
const cookieName = options.cookieName || 'volt.sid';
|
|
178
|
+
return (req, res) => {
|
|
179
|
+
const sessionId = req.cookies[cookieName];
|
|
180
|
+
if (!sessionId) {
|
|
181
|
+
res.json({ error: 'Session required' }, 401);
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const session = this.getSession(sessionId);
|
|
186
|
+
if (!session) {
|
|
187
|
+
res.json({ error: 'Invalid or expired session' }, 401);
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
req.session = session;
|
|
192
|
+
req.userId = session.userId;
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ===== API KEYS =====
|
|
197
|
+
|
|
198
|
+
/** Generate an API key */
|
|
199
|
+
generateApiKey(name, permissions = []) {
|
|
200
|
+
const key = `vk_${crypto.randomBytes(32).toString('hex')}`;
|
|
201
|
+
this._apiKeys.set(key, { name, permissions, createdAt: new Date() });
|
|
202
|
+
return key;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Middleware: Require API key */
|
|
206
|
+
requireApiKey(requiredPermissions = []) {
|
|
207
|
+
return (req, res) => {
|
|
208
|
+
const key = req.headers['x-api-key'] || req.query.api_key;
|
|
209
|
+
if (!key) {
|
|
210
|
+
res.json({ error: 'API key required' }, 401);
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const keyData = this._apiKeys.get(key);
|
|
215
|
+
if (!keyData) {
|
|
216
|
+
res.json({ error: 'Invalid API key' }, 401);
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check permissions
|
|
221
|
+
if (requiredPermissions.length > 0) {
|
|
222
|
+
const hasAll = requiredPermissions.every(p => keyData.permissions.includes(p));
|
|
223
|
+
if (!hasAll) {
|
|
224
|
+
res.json({ error: 'Insufficient API key permissions' }, 403);
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
req.apiKey = keyData;
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ===== PASSWORD =====
|
|
234
|
+
|
|
235
|
+
/** Hash a password using PBKDF2 (no external deps needed) */
|
|
236
|
+
async hashPassword(password, rounds = 100000) {
|
|
237
|
+
const salt = crypto.randomBytes(16).toString('hex');
|
|
238
|
+
return new Promise((resolve, reject) => {
|
|
239
|
+
crypto.pbkdf2(password, salt, rounds, 64, 'sha512', (err, derivedKey) => {
|
|
240
|
+
if (err) reject(err);
|
|
241
|
+
else resolve(`$volt$${rounds}$${salt}$${derivedKey.toString('hex')}`);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Verify a password against a hash */
|
|
247
|
+
async verifyPassword(password, hash) {
|
|
248
|
+
const parts = hash.split('$').filter(Boolean);
|
|
249
|
+
if (parts[0] !== 'volt') return false;
|
|
250
|
+
|
|
251
|
+
const rounds = parseInt(parts[1], 10);
|
|
252
|
+
const salt = parts[2];
|
|
253
|
+
const key = parts[3];
|
|
254
|
+
|
|
255
|
+
return new Promise((resolve, reject) => {
|
|
256
|
+
crypto.pbkdf2(password, salt, rounds, 64, 'sha512', (err, derivedKey) => {
|
|
257
|
+
if (err) reject(err);
|
|
258
|
+
else resolve(crypto.timingSafeEqual(Buffer.from(key, 'hex'), derivedKey));
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ===== RBAC =====
|
|
264
|
+
|
|
265
|
+
/** Define a role with permissions */
|
|
266
|
+
defineRole(roleName, permissions) {
|
|
267
|
+
this._roles.set(roleName, new Set(permissions));
|
|
268
|
+
return this;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Check if a role has a permission */
|
|
272
|
+
roleHasPermission(roleName, permission) {
|
|
273
|
+
const role = this._roles.get(roleName);
|
|
274
|
+
return role ? role.has(permission) : false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Middleware: Require specific roles */
|
|
278
|
+
requireRole(...roles) {
|
|
279
|
+
return (req, res) => {
|
|
280
|
+
if (!req.user || !req.user.roles) {
|
|
281
|
+
res.json({ error: 'Authentication required' }, 401);
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
const hasRole = roles.some(r => req.user.roles.includes(r));
|
|
285
|
+
if (!hasRole) {
|
|
286
|
+
res.json({ error: 'Insufficient role permissions' }, 403);
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Middleware: Require specific permissions */
|
|
293
|
+
requirePermission(...permissions) {
|
|
294
|
+
return (req, res) => {
|
|
295
|
+
if (!req.user) {
|
|
296
|
+
res.json({ error: 'Authentication required' }, 401);
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
const userPerms = req.user.permissions || [];
|
|
300
|
+
const userRoles = req.user.roles || [];
|
|
301
|
+
|
|
302
|
+
// Check direct permissions
|
|
303
|
+
const hasDirectPerm = permissions.every(p => userPerms.includes(p));
|
|
304
|
+
if (hasDirectPerm) return;
|
|
305
|
+
|
|
306
|
+
// Check role-based permissions
|
|
307
|
+
const hasRolePerm = permissions.every(p => {
|
|
308
|
+
return userRoles.some(role => this.roleHasPermission(role, p));
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
if (!hasRolePerm) {
|
|
312
|
+
res.json({ error: 'Insufficient permissions' }, 403);
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ===== TOTP (Two-Factor Authentication) =====
|
|
319
|
+
|
|
320
|
+
/** Generate a TOTP secret */
|
|
321
|
+
generateTOTPSecret(length = 20) {
|
|
322
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
323
|
+
let secret = '';
|
|
324
|
+
const bytes = crypto.randomBytes(length);
|
|
325
|
+
for (let i = 0; i < length; i++) {
|
|
326
|
+
secret += chars[bytes[i] % chars.length];
|
|
327
|
+
}
|
|
328
|
+
return secret;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Generate current TOTP code */
|
|
332
|
+
generateTOTP(secret, timeStep = 30) {
|
|
333
|
+
const epoch = Math.floor(Date.now() / 1000);
|
|
334
|
+
const counter = Math.floor(epoch / timeStep);
|
|
335
|
+
return this._hotpGenerate(secret, counter);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Verify a TOTP code (with window for clock drift) */
|
|
339
|
+
verifyTOTP(token, secret, window = 1) {
|
|
340
|
+
const epoch = Math.floor(Date.now() / 1000);
|
|
341
|
+
const timeStep = 30;
|
|
342
|
+
|
|
343
|
+
for (let i = -window; i <= window; i++) {
|
|
344
|
+
const counter = Math.floor(epoch / timeStep) + i;
|
|
345
|
+
const expected = this._hotpGenerate(secret, counter);
|
|
346
|
+
if (token === expected) return true;
|
|
347
|
+
}
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ===== INTERNAL =====
|
|
352
|
+
|
|
353
|
+
_sign(input) {
|
|
354
|
+
return crypto
|
|
355
|
+
.createHmac('sha256', this._secret)
|
|
356
|
+
.update(input)
|
|
357
|
+
.digest('base64url');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
_base64url(str) {
|
|
361
|
+
return Buffer.from(str).toString('base64url');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
_extractToken(req) {
|
|
365
|
+
// Check Authorization header
|
|
366
|
+
const authHeader = req.headers.authorization;
|
|
367
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
368
|
+
return authHeader.substring(7);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Check query parameter
|
|
372
|
+
if (req.query && req.query.token) {
|
|
373
|
+
return req.query.token;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Check cookie
|
|
377
|
+
if (req.cookies && req.cookies.token) {
|
|
378
|
+
return req.cookies.token;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
_parseExpiry(expiry) {
|
|
385
|
+
if (typeof expiry === 'number') return expiry;
|
|
386
|
+
const match = String(expiry).match(/^(\d+)(s|m|h|d)$/);
|
|
387
|
+
if (!match) return 86400; // Default: 24 hours
|
|
388
|
+
|
|
389
|
+
const value = parseInt(match[1], 10);
|
|
390
|
+
const unit = match[2];
|
|
391
|
+
const multipliers = { s: 1, m: 60, h: 3600, d: 86400 };
|
|
392
|
+
return value * (multipliers[unit] || 1);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
_hotpGenerate(secret, counter) {
|
|
396
|
+
const decodedSecret = Buffer.from(this._base32Decode(secret));
|
|
397
|
+
const buffer = Buffer.alloc(8);
|
|
398
|
+
for (let i = 7; i >= 0; i--) {
|
|
399
|
+
buffer[i] = counter & 0xff;
|
|
400
|
+
counter = counter >> 8;
|
|
401
|
+
}
|
|
402
|
+
const hmac = crypto.createHmac('sha1', decodedSecret).update(buffer).digest();
|
|
403
|
+
const offset = hmac[hmac.length - 1] & 0xf;
|
|
404
|
+
const code = ((hmac[offset] & 0x7f) << 24) | ((hmac[offset + 1] & 0xff) << 16) |
|
|
405
|
+
((hmac[offset + 2] & 0xff) << 8) | (hmac[offset + 3] & 0xff);
|
|
406
|
+
return String(code % 1000000).padStart(6, '0');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
_base32Decode(input) {
|
|
410
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
411
|
+
let bits = '';
|
|
412
|
+
for (const c of input.toUpperCase()) {
|
|
413
|
+
const val = chars.indexOf(c);
|
|
414
|
+
if (val === -1) continue;
|
|
415
|
+
bits += val.toString(2).padStart(5, '0');
|
|
416
|
+
}
|
|
417
|
+
const bytes = [];
|
|
418
|
+
for (let i = 0; i + 8 <= bits.length; i += 8) {
|
|
419
|
+
bytes.push(parseInt(bits.substr(i, 8), 2));
|
|
420
|
+
}
|
|
421
|
+
return bytes;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
module.exports = { Auth };
|