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.
@@ -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
+ };