webspresso 0.0.35 → 0.0.37
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/bin/commands/admin-password.js +251 -0
- package/bin/webspresso.js +2 -0
- package/core/auth/hash.js +112 -0
- package/core/auth/index.js +202 -0
- package/core/auth/manager.js +404 -0
- package/core/auth/middleware.js +252 -0
- package/core/auth/policy.js +221 -0
- package/core/orm/events.js +278 -0
- package/core/orm/index.js +6 -0
- package/core/orm/model.js +29 -0
- package/core/orm/query-builder.js +40 -0
- package/core/orm/repository.js +169 -14
- package/package.json +8 -7
- package/plugins/admin-panel/app.js +10 -0
- package/plugins/admin-panel/components.js +170 -65
- package/plugins/admin-panel/core/api-extensions.js +295 -0
- package/plugins/admin-panel/core/registry.js +445 -0
- package/plugins/admin-panel/index.js +155 -33
- package/plugins/admin-panel/modules/bulk-actions.js +233 -0
- package/plugins/admin-panel/modules/custom-pages.js +304 -0
- package/plugins/admin-panel/modules/dashboard.js +333 -0
- package/plugins/admin-panel/modules/menu.js +277 -0
- package/plugins/admin-panel/modules/user-management.js +495 -0
- package/src/server.js +16 -3
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin Password Command
|
|
3
|
+
* Reset admin user password via CLI
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const readline = require('readline');
|
|
9
|
+
|
|
10
|
+
function registerCommand(program) {
|
|
11
|
+
program
|
|
12
|
+
.command('admin:password')
|
|
13
|
+
.description('Reset admin user password')
|
|
14
|
+
.option('-e, --email <email>', 'Admin user email')
|
|
15
|
+
.option('-p, --password <password>', 'New password (not recommended, use interactive mode)')
|
|
16
|
+
.option('-c, --config <path>', 'Path to database config file')
|
|
17
|
+
.action(async (options) => {
|
|
18
|
+
try {
|
|
19
|
+
// Find project root and load database
|
|
20
|
+
const cwd = process.cwd();
|
|
21
|
+
|
|
22
|
+
// Try to find and load the database config
|
|
23
|
+
let dbConfig = null;
|
|
24
|
+
const configPaths = [
|
|
25
|
+
options.config,
|
|
26
|
+
path.join(cwd, 'webspresso.config.js'),
|
|
27
|
+
path.join(cwd, 'database.config.js'),
|
|
28
|
+
path.join(cwd, 'db.config.js'),
|
|
29
|
+
].filter(Boolean);
|
|
30
|
+
|
|
31
|
+
for (const configPath of configPaths) {
|
|
32
|
+
if (fs.existsSync(configPath)) {
|
|
33
|
+
const config = require(configPath);
|
|
34
|
+
dbConfig = config.database || config;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!dbConfig) {
|
|
40
|
+
console.error('❌ Error: Could not find database configuration.');
|
|
41
|
+
console.error(' Please run this command from your project root.');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Lazy load dependencies
|
|
46
|
+
const bcrypt = require('bcrypt');
|
|
47
|
+
const knex = require('knex');
|
|
48
|
+
|
|
49
|
+
// Initialize database connection
|
|
50
|
+
const db = knex(dbConfig);
|
|
51
|
+
|
|
52
|
+
// Check if admin_users table exists
|
|
53
|
+
const hasTable = await db.schema.hasTable('admin_users');
|
|
54
|
+
if (!hasTable) {
|
|
55
|
+
console.error('❌ Error: admin_users table does not exist.');
|
|
56
|
+
console.error(' Run "webspresso admin:setup" and "webspresso db:migrate" first.');
|
|
57
|
+
await db.destroy();
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Get email (interactive if not provided)
|
|
62
|
+
let email = options.email;
|
|
63
|
+
if (!email) {
|
|
64
|
+
const rl = readline.createInterface({
|
|
65
|
+
input: process.stdin,
|
|
66
|
+
output: process.stdout,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
email = await new Promise((resolve) => {
|
|
70
|
+
rl.question('Enter admin email: ', (answer) => {
|
|
71
|
+
rl.close();
|
|
72
|
+
resolve(answer.trim());
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!email) {
|
|
78
|
+
console.error('❌ Error: Email is required.');
|
|
79
|
+
await db.destroy();
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check if user exists
|
|
84
|
+
const user = await db('admin_users').where({ email }).first();
|
|
85
|
+
if (!user) {
|
|
86
|
+
console.error(`❌ Error: Admin user with email "${email}" not found.`);
|
|
87
|
+
|
|
88
|
+
// Show available users
|
|
89
|
+
const users = await db('admin_users').select('id', 'email', 'name');
|
|
90
|
+
if (users.length > 0) {
|
|
91
|
+
console.log('\nAvailable admin users:');
|
|
92
|
+
users.forEach(u => console.log(` - ${u.email} (${u.name || 'No name'})`));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await db.destroy();
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Get new password (interactive if not provided)
|
|
100
|
+
let password = options.password;
|
|
101
|
+
if (!password) {
|
|
102
|
+
const rl = readline.createInterface({
|
|
103
|
+
input: process.stdin,
|
|
104
|
+
output: process.stdout,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Disable echo for password input
|
|
108
|
+
if (process.stdin.isTTY) {
|
|
109
|
+
process.stdout.write('Enter new password: ');
|
|
110
|
+
password = await new Promise((resolve) => {
|
|
111
|
+
let pwd = '';
|
|
112
|
+
process.stdin.setRawMode(true);
|
|
113
|
+
process.stdin.resume();
|
|
114
|
+
process.stdin.on('data', (char) => {
|
|
115
|
+
char = char.toString();
|
|
116
|
+
if (char === '\n' || char === '\r') {
|
|
117
|
+
process.stdin.setRawMode(false);
|
|
118
|
+
process.stdin.pause();
|
|
119
|
+
console.log(); // New line after password
|
|
120
|
+
resolve(pwd);
|
|
121
|
+
} else if (char === '\u0003') {
|
|
122
|
+
// Ctrl+C
|
|
123
|
+
process.exit();
|
|
124
|
+
} else if (char === '\u007F') {
|
|
125
|
+
// Backspace
|
|
126
|
+
if (pwd.length > 0) {
|
|
127
|
+
pwd = pwd.slice(0, -1);
|
|
128
|
+
process.stdout.write('\b \b');
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
pwd += char;
|
|
132
|
+
process.stdout.write('*');
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
rl.close();
|
|
137
|
+
} else {
|
|
138
|
+
password = await new Promise((resolve) => {
|
|
139
|
+
rl.question('Enter new password: ', (answer) => {
|
|
140
|
+
rl.close();
|
|
141
|
+
resolve(answer);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!password || password.length < 6) {
|
|
148
|
+
console.error('❌ Error: Password must be at least 6 characters.');
|
|
149
|
+
await db.destroy();
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Hash the password
|
|
154
|
+
const hashedPassword = await bcrypt.hash(password, 10);
|
|
155
|
+
|
|
156
|
+
// Update the password
|
|
157
|
+
await db('admin_users')
|
|
158
|
+
.where({ email })
|
|
159
|
+
.update({
|
|
160
|
+
password: hashedPassword,
|
|
161
|
+
updated_at: new Date(),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
console.log(`\n✅ Password updated successfully for: ${email}\n`);
|
|
165
|
+
|
|
166
|
+
await db.destroy();
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.error('❌ Error:', err.message);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Also add a command to list admin users
|
|
174
|
+
program
|
|
175
|
+
.command('admin:list')
|
|
176
|
+
.description('List all admin users')
|
|
177
|
+
.option('-c, --config <path>', 'Path to database config file')
|
|
178
|
+
.action(async (options) => {
|
|
179
|
+
try {
|
|
180
|
+
const cwd = process.cwd();
|
|
181
|
+
|
|
182
|
+
// Try to find and load the database config
|
|
183
|
+
let dbConfig = null;
|
|
184
|
+
const configPaths = [
|
|
185
|
+
options.config,
|
|
186
|
+
path.join(cwd, 'webspresso.config.js'),
|
|
187
|
+
path.join(cwd, 'database.config.js'),
|
|
188
|
+
path.join(cwd, 'db.config.js'),
|
|
189
|
+
].filter(Boolean);
|
|
190
|
+
|
|
191
|
+
for (const configPath of configPaths) {
|
|
192
|
+
if (fs.existsSync(configPath)) {
|
|
193
|
+
const config = require(configPath);
|
|
194
|
+
dbConfig = config.database || config;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!dbConfig) {
|
|
200
|
+
console.error('❌ Error: Could not find database configuration.');
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const knex = require('knex');
|
|
205
|
+
const db = knex(dbConfig);
|
|
206
|
+
|
|
207
|
+
// Check if admin_users table exists
|
|
208
|
+
const hasTable = await db.schema.hasTable('admin_users');
|
|
209
|
+
if (!hasTable) {
|
|
210
|
+
console.log('ℹ️ admin_users table does not exist yet.');
|
|
211
|
+
console.log(' Run "webspresso admin:setup" and "webspresso db:migrate" first.');
|
|
212
|
+
await db.destroy();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Get all admin users
|
|
217
|
+
const users = await db('admin_users')
|
|
218
|
+
.select('id', 'email', 'name', 'role', 'active', 'created_at')
|
|
219
|
+
.orderBy('id');
|
|
220
|
+
|
|
221
|
+
if (users.length === 0) {
|
|
222
|
+
console.log('\nNo admin users found.\n');
|
|
223
|
+
console.log('Create the first admin user via the admin panel setup page.');
|
|
224
|
+
} else {
|
|
225
|
+
console.log(`\n📋 Admin Users (${users.length}):\n`);
|
|
226
|
+
console.log(' ID | Email | Name | Role | Active | Created');
|
|
227
|
+
console.log(' ' + '-'.repeat(90));
|
|
228
|
+
|
|
229
|
+
users.forEach(user => {
|
|
230
|
+
const email = (user.email || '').padEnd(30).slice(0, 30);
|
|
231
|
+
const name = (user.name || '-').padEnd(14).slice(0, 14);
|
|
232
|
+
const role = (user.role || 'admin').padEnd(7).slice(0, 7);
|
|
233
|
+
const active = user.active ? ' ✓ ' : ' ✗ ';
|
|
234
|
+
const created = user.created_at
|
|
235
|
+
? new Date(user.created_at).toLocaleDateString()
|
|
236
|
+
: '-';
|
|
237
|
+
|
|
238
|
+
console.log(` ${String(user.id).padStart(3)} | ${email} | ${name} | ${role} | ${active} | ${created}`);
|
|
239
|
+
});
|
|
240
|
+
console.log();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
await db.destroy();
|
|
244
|
+
} catch (err) {
|
|
245
|
+
console.error('❌ Error:', err.message);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
module.exports = { registerCommand };
|
package/bin/webspresso.js
CHANGED
|
@@ -25,6 +25,7 @@ const { registerCommand: registerDbStatus } = require('./commands/db-status');
|
|
|
25
25
|
const { registerCommand: registerDbMake } = require('./commands/db-make');
|
|
26
26
|
const { registerCommand: registerSeed } = require('./commands/seed');
|
|
27
27
|
const { registerCommand: registerAdminSetup } = require('./commands/admin-setup');
|
|
28
|
+
const { registerCommand: registerAdminPassword } = require('./commands/admin-password');
|
|
28
29
|
|
|
29
30
|
registerNew(program);
|
|
30
31
|
registerPage(program);
|
|
@@ -38,6 +39,7 @@ registerDbStatus(program);
|
|
|
38
39
|
registerDbMake(program);
|
|
39
40
|
registerSeed(program);
|
|
40
41
|
registerAdminSetup(program);
|
|
42
|
+
registerAdminPassword(program);
|
|
41
43
|
|
|
42
44
|
// Parse arguments
|
|
43
45
|
program.parse();
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso Auth - Password Hashing
|
|
3
|
+
* Bcrypt wrapper for secure password hashing
|
|
4
|
+
* @module core/auth/hash
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
let bcrypt;
|
|
8
|
+
try {
|
|
9
|
+
bcrypt = require('bcrypt');
|
|
10
|
+
} catch {
|
|
11
|
+
// bcrypt is optional, will throw if used without installation
|
|
12
|
+
bcrypt = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Default bcrypt cost factor
|
|
17
|
+
* Higher = more secure but slower
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_ROUNDS = 12;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Hash a password using bcrypt
|
|
23
|
+
* @param {string} password - Plain text password
|
|
24
|
+
* @param {number} [rounds=12] - Cost factor (rounds)
|
|
25
|
+
* @returns {Promise<string>} Hashed password
|
|
26
|
+
*/
|
|
27
|
+
async function hash(password, rounds = DEFAULT_ROUNDS) {
|
|
28
|
+
if (!bcrypt) {
|
|
29
|
+
throw new Error('bcrypt is required for password hashing. Install it with: npm install bcrypt');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!password || typeof password !== 'string') {
|
|
33
|
+
throw new Error('Password must be a non-empty string');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return bcrypt.hash(password, rounds);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Verify a password against a hash
|
|
41
|
+
* @param {string} password - Plain text password to verify
|
|
42
|
+
* @param {string} hashedPassword - Hashed password to compare against
|
|
43
|
+
* @returns {Promise<boolean>} True if password matches
|
|
44
|
+
*/
|
|
45
|
+
async function verify(password, hashedPassword) {
|
|
46
|
+
if (!bcrypt) {
|
|
47
|
+
throw new Error('bcrypt is required for password verification. Install it with: npm install bcrypt');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!password || !hashedPassword) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
return await bcrypt.compare(password, hashedPassword);
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if a hash needs rehashing (e.g., cost factor changed)
|
|
63
|
+
* @param {string} hashedPassword - Hashed password to check
|
|
64
|
+
* @param {number} [rounds=12] - Desired cost factor
|
|
65
|
+
* @returns {boolean} True if rehash is needed
|
|
66
|
+
*/
|
|
67
|
+
function needsRehash(hashedPassword, rounds = DEFAULT_ROUNDS) {
|
|
68
|
+
if (!bcrypt) {
|
|
69
|
+
throw new Error('bcrypt is required. Install it with: npm install bcrypt');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!hashedPassword) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const hashRounds = bcrypt.getRounds(hashedPassword);
|
|
78
|
+
return hashRounds < rounds;
|
|
79
|
+
} catch {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Generate a secure random token
|
|
86
|
+
* @param {number} [length=32] - Token length in bytes
|
|
87
|
+
* @returns {string} Hex-encoded random token
|
|
88
|
+
*/
|
|
89
|
+
function generateToken(length = 32) {
|
|
90
|
+
const crypto = require('crypto');
|
|
91
|
+
return crypto.randomBytes(length).toString('hex');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Hash a token for storage (SHA-256)
|
|
96
|
+
* Used for remember me tokens - stored hashed, compared hashed
|
|
97
|
+
* @param {string} token - Plain token
|
|
98
|
+
* @returns {string} Hashed token
|
|
99
|
+
*/
|
|
100
|
+
function hashToken(token) {
|
|
101
|
+
const crypto = require('crypto');
|
|
102
|
+
return crypto.createHash('sha256').update(token).digest('hex');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
hash,
|
|
107
|
+
verify,
|
|
108
|
+
needsRehash,
|
|
109
|
+
generateToken,
|
|
110
|
+
hashToken,
|
|
111
|
+
DEFAULT_ROUNDS,
|
|
112
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso Auth
|
|
3
|
+
* Django/Rails-inspired authentication system with adapter pattern
|
|
4
|
+
* @module core/auth
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { AuthManager, AuthenticationError, DEFAULT_CONFIG } = require('./manager');
|
|
8
|
+
const { PolicyManager, AuthorizationError } = require('./policy');
|
|
9
|
+
const { createAuthMiddleware, setupAuthMiddleware } = require('./middleware');
|
|
10
|
+
const { hash, verify, needsRehash, generateToken, hashToken } = require('./hash');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create authentication instance
|
|
14
|
+
* @param {Object} config - Configuration
|
|
15
|
+
* @param {Function} config.findUserById - (id) => Promise<User|null>
|
|
16
|
+
* @param {Function} config.findUserByCredentials - (identifier, password) => Promise<User|null>
|
|
17
|
+
* @param {Object} [config.rememberTokens] - Remember token adapter
|
|
18
|
+
* @param {Object} [config.session] - Session configuration
|
|
19
|
+
* @param {Object} [config.rememberMe] - Remember me configuration
|
|
20
|
+
* @param {Object} [config.routes] - Route configuration
|
|
21
|
+
* @returns {AuthManager}
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* const auth = createAuth({
|
|
25
|
+
* findUserById: async (id) => {
|
|
26
|
+
* return await UserRepo.findById(id);
|
|
27
|
+
* },
|
|
28
|
+
*
|
|
29
|
+
* findUserByCredentials: async (email, password) => {
|
|
30
|
+
* const user = await UserRepo.findOne({ email });
|
|
31
|
+
* if (user && await verify(password, user.password)) {
|
|
32
|
+
* return user;
|
|
33
|
+
* }
|
|
34
|
+
* return null;
|
|
35
|
+
* },
|
|
36
|
+
*
|
|
37
|
+
* // Optional: Remember me tokens
|
|
38
|
+
* rememberTokens: {
|
|
39
|
+
* create: async (userId, token, expiresAt) => {
|
|
40
|
+
* await db.knex('remember_tokens').insert({
|
|
41
|
+
* user_id: userId,
|
|
42
|
+
* token,
|
|
43
|
+
* expires_at: expiresAt,
|
|
44
|
+
* });
|
|
45
|
+
* },
|
|
46
|
+
* find: async (token) => {
|
|
47
|
+
* return await db.knex('remember_tokens').where({ token }).first();
|
|
48
|
+
* },
|
|
49
|
+
* delete: async (token) => {
|
|
50
|
+
* await db.knex('remember_tokens').where({ token }).delete();
|
|
51
|
+
* },
|
|
52
|
+
* deleteAllForUser: async (userId) => {
|
|
53
|
+
* await db.knex('remember_tokens').where({ user_id: userId }).delete();
|
|
54
|
+
* },
|
|
55
|
+
* },
|
|
56
|
+
*
|
|
57
|
+
* session: {
|
|
58
|
+
* secret: process.env.SESSION_SECRET,
|
|
59
|
+
* cookie: {
|
|
60
|
+
* maxAge: 24 * 60 * 60 * 1000, // 1 day
|
|
61
|
+
* },
|
|
62
|
+
* },
|
|
63
|
+
* });
|
|
64
|
+
*/
|
|
65
|
+
function createAuth(config) {
|
|
66
|
+
return new AuthManager(config);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Quick auth setup helper for common patterns
|
|
71
|
+
* @param {Object} options - Options
|
|
72
|
+
* @param {Object} options.db - Database instance (with getRepository)
|
|
73
|
+
* @param {string} [options.userModel='User'] - User model name
|
|
74
|
+
* @param {string} [options.identifierField='email'] - Login identifier field
|
|
75
|
+
* @param {string} [options.passwordField='password'] - Password field
|
|
76
|
+
* @param {Object} [options.session] - Session config
|
|
77
|
+
* @param {boolean} [options.rememberMe=true] - Enable remember me
|
|
78
|
+
* @returns {AuthManager}
|
|
79
|
+
*/
|
|
80
|
+
function quickAuth(options) {
|
|
81
|
+
const {
|
|
82
|
+
db,
|
|
83
|
+
userModel = 'User',
|
|
84
|
+
identifierField = 'email',
|
|
85
|
+
passwordField = 'password',
|
|
86
|
+
session = {},
|
|
87
|
+
rememberMe = true,
|
|
88
|
+
} = options;
|
|
89
|
+
|
|
90
|
+
if (!db || typeof db.getRepository !== 'function') {
|
|
91
|
+
throw new Error('db with getRepository is required');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const UserRepo = db.getRepository(userModel);
|
|
95
|
+
|
|
96
|
+
const config = {
|
|
97
|
+
findUserById: async (id) => {
|
|
98
|
+
return await UserRepo.findById(id);
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
findUserByCredentials: async (identifier, password) => {
|
|
102
|
+
const user = await UserRepo.findOne({ [identifierField]: identifier });
|
|
103
|
+
if (user && await verify(password, user[passwordField])) {
|
|
104
|
+
return user;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
session: {
|
|
110
|
+
secret: process.env.SESSION_SECRET || session.secret,
|
|
111
|
+
...session,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Add remember tokens if enabled
|
|
116
|
+
if (rememberMe) {
|
|
117
|
+
config.rememberTokens = {
|
|
118
|
+
create: async (userId, token, expiresAt) => {
|
|
119
|
+
await db.knex('remember_tokens').insert({
|
|
120
|
+
user_id: userId,
|
|
121
|
+
token,
|
|
122
|
+
expires_at: expiresAt,
|
|
123
|
+
created_at: new Date(),
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
find: async (token) => {
|
|
127
|
+
return await db.knex('remember_tokens').where({ token }).first();
|
|
128
|
+
},
|
|
129
|
+
delete: async (token) => {
|
|
130
|
+
await db.knex('remember_tokens').where({ token }).delete();
|
|
131
|
+
},
|
|
132
|
+
deleteAllForUser: async (userId) => {
|
|
133
|
+
await db.knex('remember_tokens').where({ user_id: userId }).delete();
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return createAuth(config);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Migration helper for remember_tokens table
|
|
143
|
+
* @param {Object} knex - Knex instance
|
|
144
|
+
* @returns {Promise<void>}
|
|
145
|
+
*/
|
|
146
|
+
async function createRememberTokensTable(knex) {
|
|
147
|
+
const exists = await knex.schema.hasTable('remember_tokens');
|
|
148
|
+
|
|
149
|
+
if (!exists) {
|
|
150
|
+
await knex.schema.createTable('remember_tokens', (table) => {
|
|
151
|
+
table.bigIncrements('id').primary();
|
|
152
|
+
table.bigInteger('user_id').unsigned().notNullable();
|
|
153
|
+
table.string('token', 64).notNullable().unique();
|
|
154
|
+
table.timestamp('expires_at').notNullable();
|
|
155
|
+
table.timestamp('created_at').defaultTo(knex.fn.now());
|
|
156
|
+
|
|
157
|
+
table.index('user_id');
|
|
158
|
+
table.index('token');
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Drop remember_tokens table
|
|
165
|
+
* @param {Object} knex - Knex instance
|
|
166
|
+
* @returns {Promise<void>}
|
|
167
|
+
*/
|
|
168
|
+
async function dropRememberTokensTable(knex) {
|
|
169
|
+
await knex.schema.dropTableIfExists('remember_tokens');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
// Factory functions
|
|
174
|
+
createAuth,
|
|
175
|
+
quickAuth,
|
|
176
|
+
|
|
177
|
+
// Classes
|
|
178
|
+
AuthManager,
|
|
179
|
+
PolicyManager,
|
|
180
|
+
|
|
181
|
+
// Errors
|
|
182
|
+
AuthenticationError,
|
|
183
|
+
AuthorizationError,
|
|
184
|
+
|
|
185
|
+
// Middleware
|
|
186
|
+
createAuthMiddleware,
|
|
187
|
+
setupAuthMiddleware,
|
|
188
|
+
|
|
189
|
+
// Hash utilities
|
|
190
|
+
hash,
|
|
191
|
+
verify,
|
|
192
|
+
needsRehash,
|
|
193
|
+
generateToken,
|
|
194
|
+
hashToken,
|
|
195
|
+
|
|
196
|
+
// Migration helpers
|
|
197
|
+
createRememberTokensTable,
|
|
198
|
+
dropRememberTokensTable,
|
|
199
|
+
|
|
200
|
+
// Config
|
|
201
|
+
DEFAULT_CONFIG,
|
|
202
|
+
};
|