@reldens/cms 0.47.0 → 0.49.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/.claude/advanced-usage-guide.md +98 -0
- package/.claude/api-reference.md +146 -0
- package/.claude/configuration-guide.md +126 -0
- package/.claude/database-schema.md +32 -0
- package/.claude/forms-system-guide.md +193 -0
- package/.claude/installation-guide.md +223 -0
- package/.claude/multi-domain-i18n-guide.md +178 -0
- package/.claude/password-management-guide.md +426 -0
- package/.claude/search-guide.md +66 -0
- package/.claude/templating-system-guide.md +349 -0
- package/CLAUDE.md +92 -3
- package/README.md +71 -1447
- package/bin/reldens-cms-update-password.js +246 -0
- package/lib/admin-manager/router-contents.js +14 -2
- package/lib/admin-manager/router.js +1 -0
- package/lib/manager.js +70 -8
- package/lib/password-encryption-handler.js +94 -0
- package/lib/template-engine.js +8 -2
- package/package.json +5 -4
- package/templates/.env.dist +1 -1
- package/templates/page.html +15 -11
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
*
|
|
5
|
+
* Reldens - CMS - Update Password CLI
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { Manager } = require('../index');
|
|
10
|
+
const { EntitiesLoader } = require('../lib/entities-loader');
|
|
11
|
+
const { Logger, sc } = require('@reldens/utils');
|
|
12
|
+
const { FileHandler, Encryptor } = require('@reldens/server-utils');
|
|
13
|
+
const dotenv = require('dotenv');
|
|
14
|
+
const readline = require('readline');
|
|
15
|
+
|
|
16
|
+
class CmsPasswordUpdater
|
|
17
|
+
{
|
|
18
|
+
|
|
19
|
+
constructor()
|
|
20
|
+
{
|
|
21
|
+
this.args = process.argv.slice(2);
|
|
22
|
+
this.projectRoot = process.cwd();
|
|
23
|
+
this.config = {};
|
|
24
|
+
this.parseArguments();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
parseArguments()
|
|
28
|
+
{
|
|
29
|
+
for(let i = 0; i < this.args.length; i++){
|
|
30
|
+
let arg = this.args[i];
|
|
31
|
+
if(!arg.startsWith('--')){
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
let equalIndex = arg.indexOf('=');
|
|
35
|
+
if(-1 === equalIndex){
|
|
36
|
+
let flag = arg.substring(2);
|
|
37
|
+
if('help' === flag || 'h' === flag){
|
|
38
|
+
this.config[flag] = true;
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
let key = arg.substring(2, equalIndex);
|
|
43
|
+
let value = arg.substring(equalIndex + 1);
|
|
44
|
+
this.config[key] = value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
shouldShowHelp()
|
|
49
|
+
{
|
|
50
|
+
return 0 === this.args.length || sc.get(this.config, 'help', false) || sc.get(this.config, 'h', false);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
showHelp()
|
|
54
|
+
{
|
|
55
|
+
Logger.info('');
|
|
56
|
+
Logger.info('Reldens CMS Password Updater');
|
|
57
|
+
Logger.info('============================');
|
|
58
|
+
Logger.info('');
|
|
59
|
+
Logger.info('Usage: npx reldens-cms-update-password [options]');
|
|
60
|
+
Logger.info('');
|
|
61
|
+
Logger.info('Options:');
|
|
62
|
+
Logger.info(' --email=[email] User email address');
|
|
63
|
+
Logger.info(' --username=[username] User username');
|
|
64
|
+
Logger.info(' --password=[password] New password (prompted if not provided)');
|
|
65
|
+
Logger.info(' --help, -h Show this help message');
|
|
66
|
+
Logger.info('');
|
|
67
|
+
Logger.info('Examples:');
|
|
68
|
+
Logger.info(' npx reldens-cms-update-password --email=admin@example.com');
|
|
69
|
+
Logger.info(' npx reldens-cms-update-password --username=admin');
|
|
70
|
+
Logger.info(' npx reldens-cms-update-password --email=admin@example.com --password=newpass123');
|
|
71
|
+
Logger.info('');
|
|
72
|
+
Logger.info('Note: You must provide either --email or --username to identify the user.');
|
|
73
|
+
Logger.info('Note: The tool uses the storage driver configured in your .env file (RELDENS_STORAGE_DRIVER).');
|
|
74
|
+
Logger.info('');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get email()
|
|
78
|
+
{
|
|
79
|
+
return sc.get(this.config, 'email', null);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get username()
|
|
83
|
+
{
|
|
84
|
+
return sc.get(this.config, 'username', null);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get password()
|
|
88
|
+
{
|
|
89
|
+
return sc.get(this.config, 'password', null);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async run()
|
|
93
|
+
{
|
|
94
|
+
if(this.shouldShowHelp()){
|
|
95
|
+
this.showHelp();
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
if(!this.email && !this.username){
|
|
99
|
+
Logger.error('Error: You must provide either --email or --username to identify the user.');
|
|
100
|
+
Logger.info('Run with --help for usage information.');
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
let newPassword = this.password;
|
|
104
|
+
if(!newPassword){
|
|
105
|
+
newPassword = await this.promptPassword();
|
|
106
|
+
if(!newPassword){
|
|
107
|
+
Logger.error('Error: Password cannot be empty.');
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
let envFilePath = FileHandler.joinPaths(this.projectRoot, '.env');
|
|
112
|
+
dotenv.config({path: envFilePath});
|
|
113
|
+
let storageDriver = process.env.RELDENS_STORAGE_DRIVER || 'prisma';
|
|
114
|
+
Logger.debug('Using storage driver: '+storageDriver);
|
|
115
|
+
let entitiesLoader = new EntitiesLoader({projectRoot: this.projectRoot});
|
|
116
|
+
let loadedEntities = entitiesLoader.loadEntities(storageDriver);
|
|
117
|
+
if(!loadedEntities || !loadedEntities.rawRegisteredEntities){
|
|
118
|
+
Logger.error('Failed to load entities for driver: '+storageDriver);
|
|
119
|
+
Logger.error('Make sure you have run "npx reldens-cms-generate-entities" first.');
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
Logger.debug('Loaded entities for driver: '+storageDriver);
|
|
123
|
+
let managerConfig = {
|
|
124
|
+
projectRoot: this.projectRoot,
|
|
125
|
+
rawRegisteredEntities: loadedEntities.rawRegisteredEntities,
|
|
126
|
+
entitiesConfig: loadedEntities.entitiesConfig,
|
|
127
|
+
entitiesTranslations: loadedEntities.entitiesTranslations
|
|
128
|
+
};
|
|
129
|
+
if('prisma' === storageDriver){
|
|
130
|
+
let prismaClient = this.loadPrismaClient();
|
|
131
|
+
if(prismaClient){
|
|
132
|
+
managerConfig.prismaClient = prismaClient;
|
|
133
|
+
Logger.debug('Prisma client loaded and configured.');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
let manager = new Manager(managerConfig);
|
|
137
|
+
if(!manager.isInstalled()){
|
|
138
|
+
Logger.error('CMS is not installed. Please run installation first.');
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
Logger.debug('Reldens CMS Manager instance created for password update.');
|
|
142
|
+
let initResult = await manager.initializeDataServer();
|
|
143
|
+
if(!initResult){
|
|
144
|
+
Logger.error('Failed to initialize data server.');
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
Logger.debug('Data server initialized successfully.');
|
|
148
|
+
let success = await this.updateUserPassword(manager.dataServer, newPassword);
|
|
149
|
+
if(!success){
|
|
150
|
+
Logger.error('Password update failed.');
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
Logger.info('Password updated successfully!');
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
loadPrismaClient()
|
|
158
|
+
{
|
|
159
|
+
let clientPath = FileHandler.joinPaths(this.projectRoot, 'prisma', 'client');
|
|
160
|
+
if(!FileHandler.exists(clientPath)){
|
|
161
|
+
Logger.debug('Prisma client path not found: '+clientPath);
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
let { PrismaClient } = require(clientPath);
|
|
166
|
+
return new PrismaClient();
|
|
167
|
+
} catch(error) {
|
|
168
|
+
Logger.error('Failed to load Prisma client: '+error.message);
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async promptPassword()
|
|
174
|
+
{
|
|
175
|
+
let rl = readline.createInterface({
|
|
176
|
+
input: process.stdin,
|
|
177
|
+
output: process.stdout
|
|
178
|
+
});
|
|
179
|
+
return new Promise((resolve) => {
|
|
180
|
+
rl.question('Enter new password: ', (password) => {
|
|
181
|
+
rl.question('Confirm new password: ', (confirmPassword) => {
|
|
182
|
+
rl.close();
|
|
183
|
+
if(password !== confirmPassword){
|
|
184
|
+
Logger.error('Passwords do not match.');
|
|
185
|
+
resolve(null);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
resolve(password);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async updateUserPassword(dataServer, newPassword)
|
|
195
|
+
{
|
|
196
|
+
let usersEntity = dataServer.getEntity('users');
|
|
197
|
+
if(!usersEntity){
|
|
198
|
+
Logger.error('Users entity not found.');
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
let user = null;
|
|
202
|
+
if(this.email){
|
|
203
|
+
Logger.info('Looking up user by email: '+this.email);
|
|
204
|
+
user = await usersEntity.loadOneBy('email', this.email);
|
|
205
|
+
}
|
|
206
|
+
if(!user && this.username){
|
|
207
|
+
Logger.info('Looking up user by username: '+this.username);
|
|
208
|
+
user = await usersEntity.loadOneBy('username', this.username);
|
|
209
|
+
}
|
|
210
|
+
if(!user){
|
|
211
|
+
Logger.error('User not found with provided credentials.');
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
Logger.info('User found: '+user.email+' ('+user.username+')');
|
|
215
|
+
let encryptedPassword = Encryptor.encryptPassword(newPassword);
|
|
216
|
+
if(!encryptedPassword){
|
|
217
|
+
Logger.error('Failed to encrypt password.');
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
Logger.debug('Password encrypted successfully.');
|
|
221
|
+
try {
|
|
222
|
+
let updateResult = await usersEntity.updateById(user.id, {password: encryptedPassword});
|
|
223
|
+
if(!updateResult){
|
|
224
|
+
Logger.error('Failed to update password in database.');
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
Logger.info('Password updated in database for user: '+user.email);
|
|
228
|
+
return true;
|
|
229
|
+
} catch(error) {
|
|
230
|
+
Logger.error('Error updating password: '+error.message);
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let updater = new CmsPasswordUpdater();
|
|
238
|
+
updater.run().then((success) => {
|
|
239
|
+
if(!success){
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
process.exit(0);
|
|
243
|
+
}).catch((error) => {
|
|
244
|
+
Logger.critical('Error during password update: '+error.message);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
});
|
|
@@ -33,6 +33,7 @@ class RouterContents
|
|
|
33
33
|
this.fetchUploadProperties = props.fetchUploadProperties;
|
|
34
34
|
this.uploaderFactory = props.uploaderFactory;
|
|
35
35
|
this.filtersManager = new AdminFiltersManager();
|
|
36
|
+
this.passwordFieldNames = props.passwordFieldNames || ['password'];
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
async generateListRouteContent(req, driverResource, entityPath)
|
|
@@ -220,7 +221,7 @@ class RouterContents
|
|
|
220
221
|
fieldDisabled: fieldDisabled ? ' disabled="disabled"' : '',
|
|
221
222
|
required: (!property.isUpload || !loadedEntity) && property.isRequired ? ' required="required"' : '',
|
|
222
223
|
multiple: property.isArray ? ' multiple="multiple"' : '',
|
|
223
|
-
inputType: this.getInputType(property, fieldDisabled)
|
|
224
|
+
inputType: this.getInputType(property, propertyKey, fieldDisabled)
|
|
224
225
|
}
|
|
225
226
|
);
|
|
226
227
|
}
|
|
@@ -512,6 +513,11 @@ class RouterContents
|
|
|
512
513
|
);
|
|
513
514
|
return false;
|
|
514
515
|
}
|
|
516
|
+
if(-1 !== this.passwordFieldNames.indexOf(i)){
|
|
517
|
+
if(!propertyUpdateValue || '' === propertyUpdateValue){
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
515
521
|
if(!property.isUpload || (property.isUpload && !isNull)){
|
|
516
522
|
entityDataPatch[i] = propertyUpdateValue;
|
|
517
523
|
}
|
|
@@ -597,6 +603,9 @@ class RouterContents
|
|
|
597
603
|
|
|
598
604
|
async generatePropertyEditRenderedValue(entity, propertyKey, resourceProperty)
|
|
599
605
|
{
|
|
606
|
+
if(-1 !== this.passwordFieldNames.indexOf(propertyKey)){
|
|
607
|
+
return '';
|
|
608
|
+
}
|
|
600
609
|
let entityPropertyValue = sc.get(entity, propertyKey, null);
|
|
601
610
|
let fieldValue = (0 === entityPropertyValue ? '0' : entityPropertyValue || '');
|
|
602
611
|
if('null' === fieldValue){
|
|
@@ -693,8 +702,11 @@ class RouterContents
|
|
|
693
702
|
return propertyType;
|
|
694
703
|
}
|
|
695
704
|
|
|
696
|
-
getInputType(resourceProperty, fieldDisabled)
|
|
705
|
+
getInputType(resourceProperty, propertyKey, fieldDisabled)
|
|
697
706
|
{
|
|
707
|
+
if(-1 !== this.passwordFieldNames.indexOf(propertyKey) && !fieldDisabled){
|
|
708
|
+
return 'password';
|
|
709
|
+
}
|
|
698
710
|
if('datetime' === resourceProperty.type && !fieldDisabled){
|
|
699
711
|
return 'datetime-local';
|
|
700
712
|
}
|
|
@@ -84,6 +84,7 @@ class Router
|
|
|
84
84
|
req.session.user = loginResult;
|
|
85
85
|
return res.redirect(this.rootPath);
|
|
86
86
|
}
|
|
87
|
+
Logger.error('Admin authentication failed.', {email, adminRoleId: this.adminRoleId});
|
|
87
88
|
return res.redirect(this.rootPath+this.loginPath+'?login-error=true');
|
|
88
89
|
});
|
|
89
90
|
this.adminRouter.get('/', this.isAuthenticated.bind(this), async (req, res) => {
|
package/lib/manager.js
CHANGED
|
@@ -19,6 +19,7 @@ const { Installer } = require('./installer');
|
|
|
19
19
|
const { Frontend } = require('./frontend');
|
|
20
20
|
const { CacheManager } = require('./cache/cache-manager');
|
|
21
21
|
const { TemplateReloader } = require('./template-reloader');
|
|
22
|
+
const { PasswordEncryptionHandler } = require('./password-encryption-handler');
|
|
22
23
|
const { EventsManagerSingleton, Logger, sc } = require('@reldens/utils');
|
|
23
24
|
const { DriversMap } = require('@reldens/storage');
|
|
24
25
|
const { AppServerFactory, FileHandler, Encryptor } = require('@reldens/server-utils');
|
|
@@ -44,6 +45,7 @@ class Manager
|
|
|
44
45
|
this.entityAccess = sc.get(props, 'entityAccess', {});
|
|
45
46
|
this.authenticationMethod = sc.get(props, 'authenticationMethod', 'db-users');
|
|
46
47
|
this.authenticationCallback = sc.get(props, 'authenticationCallback', false);
|
|
48
|
+
this.enablePasswordEncryption = sc.get(props, 'enablePasswordEncryption', true);
|
|
47
49
|
this.events = sc.get(props, 'events', EventsManagerSingleton);
|
|
48
50
|
this.adminTemplatesList = sc.get(props, 'adminTemplatesList', TemplatesList);
|
|
49
51
|
this.projectAdminPath = FileHandler.joinPaths(this.projectRoot, 'admin');
|
|
@@ -86,6 +88,7 @@ class Manager
|
|
|
86
88
|
this.developmentPorts = sc.get(props, 'developmentPorts', []);
|
|
87
89
|
this.developmentMultiplier = sc.get(props, 'developmentMultiplier', 10);
|
|
88
90
|
this.appServerConfig = sc.get(props, 'appServerConfig', {});
|
|
91
|
+
this.useDefaultErrorCallback = sc.get(props, 'useDefaultErrorCallback', true);
|
|
89
92
|
this.developmentExternalDomains = sc.get(props, 'developmentExternalDomains', {});
|
|
90
93
|
this.appServerFactory = new AppServerFactory();
|
|
91
94
|
this.adminEntitiesGenerator = new AdminEntitiesGenerator();
|
|
@@ -250,6 +253,34 @@ class Manager
|
|
|
250
253
|
developmentMultiplier: this.developmentMultiplier,
|
|
251
254
|
developmentExternalDomains: this.developmentExternalDomains
|
|
252
255
|
};
|
|
256
|
+
if(this.useDefaultErrorCallback && !this.appServerConfig.onError){
|
|
257
|
+
baseConfig.onError = (event) => {
|
|
258
|
+
if(!event.error){
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
let errorKey = event.key || 'unknown';
|
|
262
|
+
let errorMessage = 'string' === typeof event.error ? event.error : event.error.message;
|
|
263
|
+
let errorCode = event.error.code || '';
|
|
264
|
+
let errorStack = event.error.stack || '';
|
|
265
|
+
let logParts = ['Server error - key: '+errorKey];
|
|
266
|
+
if(event.hostname){
|
|
267
|
+
logParts.push('hostname: '+event.hostname);
|
|
268
|
+
}
|
|
269
|
+
if(event.path){
|
|
270
|
+
logParts.push('path: '+event.path);
|
|
271
|
+
}
|
|
272
|
+
if(errorCode){
|
|
273
|
+
logParts.push('code: '+errorCode);
|
|
274
|
+
}
|
|
275
|
+
if(errorMessage){
|
|
276
|
+
logParts.push('message: '+errorMessage);
|
|
277
|
+
}
|
|
278
|
+
if(errorStack){
|
|
279
|
+
logParts.push('stack: '+errorStack);
|
|
280
|
+
}
|
|
281
|
+
Logger.error(logParts.join(', '));
|
|
282
|
+
};
|
|
283
|
+
}
|
|
253
284
|
let appServerConfig = Object.assign({}, baseConfig, this.appServerConfig);
|
|
254
285
|
if(this.domainMapping && 'object' === typeof this.domainMapping){
|
|
255
286
|
this.appServerFactory.setDomainMapping(this.domainMapping);
|
|
@@ -283,17 +314,32 @@ class Manager
|
|
|
283
314
|
+'Add CDN domains to developmentExternalDomains configuration.');
|
|
284
315
|
return;
|
|
285
316
|
}
|
|
286
|
-
let
|
|
287
|
-
let
|
|
288
|
-
for(let
|
|
317
|
+
let domainKeys = Object.keys(this.domainCdnMapping);
|
|
318
|
+
let domainsWithMissingCdn = [];
|
|
319
|
+
for(let domain of domainKeys){
|
|
320
|
+
let cdnUrl = this.domainCdnMapping[domain];
|
|
289
321
|
let cdnHostname = cdnUrl.replace(/^https?:\/\//, '').split('/')[0];
|
|
290
|
-
|
|
291
|
-
|
|
322
|
+
let cdnUrlWithProtocol = cdnUrl.split('/')[0];
|
|
323
|
+
let foundInDirective = false;
|
|
324
|
+
let directiveKeys = Object.keys(this.developmentExternalDomains);
|
|
325
|
+
for(let directiveKey of directiveKeys){
|
|
326
|
+
let domains = this.developmentExternalDomains[directiveKey];
|
|
327
|
+
if(!sc.isArray(domains)){
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if(domains.includes(cdnUrlWithProtocol) || domains.includes(cdnHostname)){
|
|
331
|
+
foundInDirective = true;
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if(!foundInDirective){
|
|
336
|
+
domainsWithMissingCdn.push(domain);
|
|
292
337
|
}
|
|
293
338
|
}
|
|
294
|
-
if(0 <
|
|
295
|
-
Logger.info('CDN
|
|
296
|
-
+'
|
|
339
|
+
if(0 < domainsWithMissingCdn.length){
|
|
340
|
+
Logger.info('CDN mapping for domains: '+domainsWithMissingCdn.join(', ')
|
|
341
|
+
+' not found in CSP directives (scriptSrc, styleSrc, fontSrc, imgSrc, connectSrc, manifestSrc) '
|
|
342
|
+
+'within developmentExternalDomains. Add CDN URLs to avoid CORS issues in development mode.');
|
|
297
343
|
}
|
|
298
344
|
}
|
|
299
345
|
|
|
@@ -499,6 +545,7 @@ class Manager
|
|
|
499
545
|
adminFilesContents,
|
|
500
546
|
translations
|
|
501
547
|
});
|
|
548
|
+
this.initializePasswordEncryptionHandler();
|
|
502
549
|
let adminConfig = {
|
|
503
550
|
events: this.events,
|
|
504
551
|
dataServer: this.dataServer,
|
|
@@ -535,6 +582,21 @@ class Manager
|
|
|
535
582
|
return true;
|
|
536
583
|
}
|
|
537
584
|
|
|
585
|
+
initializePasswordEncryptionHandler()
|
|
586
|
+
{
|
|
587
|
+
if(!this.enablePasswordEncryption){
|
|
588
|
+
Logger.debug('Password encryption handler is disabled.');
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
this.passwordEncryptionHandler = new PasswordEncryptionHandler({
|
|
592
|
+
events: this.events,
|
|
593
|
+
enabled: this.enablePasswordEncryption
|
|
594
|
+
});
|
|
595
|
+
this.passwordEncryptionHandler.registerEventListeners();
|
|
596
|
+
Logger.debug('Password encryption handler initialized and registered.');
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
|
|
538
600
|
async initializeCmsPagesRouteManager()
|
|
539
601
|
{
|
|
540
602
|
if(!this.dataServer){
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Reldens - CMS - Password Encryption Handler
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { Encryptor } = require('@reldens/server-utils');
|
|
8
|
+
const { Logger, sc } = require('@reldens/utils');
|
|
9
|
+
|
|
10
|
+
class PasswordEncryptionHandler
|
|
11
|
+
{
|
|
12
|
+
|
|
13
|
+
constructor(props)
|
|
14
|
+
{
|
|
15
|
+
this.events = sc.get(props, 'events', false);
|
|
16
|
+
this.entityKey = sc.get(props, 'entityKey', 'users');
|
|
17
|
+
this.passwordField = sc.get(props, 'passwordField', 'password');
|
|
18
|
+
this.enabled = sc.get(props, 'enabled', true);
|
|
19
|
+
if(!this.events){
|
|
20
|
+
Logger.warning('PasswordEncryptionHandler: No events manager provided, handler will not be active.');
|
|
21
|
+
this.enabled = false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
registerEventListeners()
|
|
26
|
+
{
|
|
27
|
+
if(!this.enabled){
|
|
28
|
+
Logger.debug('PasswordEncryptionHandler is disabled, skipping event registration.');
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
this.events.on('reldens.adminBeforeEntitySave', async (eventData) => {
|
|
32
|
+
await this.handleBeforeEntitySave(eventData);
|
|
33
|
+
});
|
|
34
|
+
Logger.debug('PasswordEncryptionHandler registered for event: reldens.adminBeforeEntitySave');
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async handleBeforeEntitySave(eventData)
|
|
39
|
+
{
|
|
40
|
+
let driverResource = sc.get(eventData, 'driverResource', false);
|
|
41
|
+
if(!driverResource){
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if(driverResource.entityKey !== this.entityKey){
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
let req = sc.get(eventData, 'req', false);
|
|
48
|
+
if(!req){
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
let passwordValue = sc.get(req.body, this.passwordField, null);
|
|
52
|
+
if(!passwordValue){
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if('' === passwordValue){
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if(this.isAlreadyEncrypted(passwordValue)){
|
|
59
|
+
Logger.debug('Password field appears to be already encrypted, skipping encryption.');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
Logger.debug('Encrypting password for user entity save.');
|
|
63
|
+
let encryptedPassword = Encryptor.encryptPassword(passwordValue);
|
|
64
|
+
if(!encryptedPassword){
|
|
65
|
+
Logger.error('Failed to encrypt password for user entity save.');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
req.body[this.passwordField] = encryptedPassword;
|
|
69
|
+
Logger.debug('Password encrypted successfully for user entity save.');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
isAlreadyEncrypted(value)
|
|
73
|
+
{
|
|
74
|
+
if(!sc.isString(value)){
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
let parts = value.split(':');
|
|
78
|
+
if(2 !== parts.length){
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
let saltLength = parts[0].length;
|
|
82
|
+
let hashLength = parts[1].length;
|
|
83
|
+
if(64 !== saltLength){
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
if(128 !== hashLength){
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports.PasswordEncryptionHandler = PasswordEncryptionHandler;
|
package/lib/template-engine.js
CHANGED
|
@@ -30,9 +30,15 @@ class TemplateEngine
|
|
|
30
30
|
this.defaultDomain = sc.get(props, 'defaultDomain', 'default');
|
|
31
31
|
this.projectRoot = sc.get(props, 'projectRoot', './');
|
|
32
32
|
this.publicPath = sc.get(props, 'publicPath', './public');
|
|
33
|
-
this.domainPublicUrlMapping = sc.isObject(props.domainPublicUrlMapping)
|
|
33
|
+
this.domainPublicUrlMapping = sc.isObject(props.domainPublicUrlMapping)
|
|
34
|
+
&& !sc.isArray(props.domainPublicUrlMapping)
|
|
35
|
+
? props.domainPublicUrlMapping
|
|
36
|
+
: {};
|
|
34
37
|
this.defaultPublicUrl = sc.get(props, 'defaultPublicUrl', '');
|
|
35
|
-
this.domainCdnMapping = sc.isObject(props.domainCdnMapping)
|
|
38
|
+
this.domainCdnMapping = sc.isObject(props.domainCdnMapping)
|
|
39
|
+
&& !sc.isArray(props.domainCdnMapping)
|
|
40
|
+
? props.domainCdnMapping
|
|
41
|
+
: {};
|
|
36
42
|
this.dynamicForm = sc.get(props, 'dynamicForm', false);
|
|
37
43
|
this.dynamicFormRenderer = sc.get(props, 'dynamicFormRenderer', false);
|
|
38
44
|
this.jsonFieldsParser = new JsonFieldsParser({entitiesConfig: sc.get(props, 'entitiesConfig', {})});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reldens/cms",
|
|
3
3
|
"scope": "@reldens",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.49.0",
|
|
5
5
|
"description": "Reldens - CMS",
|
|
6
6
|
"author": "Damian A. Pastorini",
|
|
7
7
|
"license": "MIT",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
},
|
|
13
13
|
"bin": {
|
|
14
14
|
"reldens-cms": "bin/reldens-cms.js",
|
|
15
|
-
"reldens-cms-generate-entities": "bin/reldens-cms-generate-entities.js"
|
|
15
|
+
"reldens-cms-generate-entities": "bin/reldens-cms-generate-entities.js",
|
|
16
|
+
"reldens-cms-update-password": "bin/reldens-cms-update-password.js"
|
|
16
17
|
},
|
|
17
18
|
"repository": {
|
|
18
19
|
"type": "git",
|
|
@@ -33,8 +34,8 @@
|
|
|
33
34
|
"url": "https://github.com/damian-pastorini/reldens-cms/issues"
|
|
34
35
|
},
|
|
35
36
|
"dependencies": {
|
|
36
|
-
"@reldens/server-utils": "^0.
|
|
37
|
-
"@reldens/storage": "^0.
|
|
37
|
+
"@reldens/server-utils": "^0.43.0",
|
|
38
|
+
"@reldens/storage": "^0.85.0",
|
|
38
39
|
"@reldens/utils": "^0.54.0",
|
|
39
40
|
"dotenv": "17.2.3",
|
|
40
41
|
"mustache": "4.2.0"
|
package/templates/.env.dist
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Database Configuration
|
|
2
|
+
RELDENS_STORAGE_DRIVER={{&dbDriver}}
|
|
2
3
|
RELDENS_DB_CLIENT={{&dbClient}}
|
|
3
4
|
RELDENS_DB_HOST={{&dbHost}}
|
|
4
5
|
RELDENS_DB_PORT={{&dbPort}}
|
|
5
6
|
RELDENS_DB_NAME={{&dbName}}
|
|
6
7
|
RELDENS_DB_USER={{&dbUser}}
|
|
7
8
|
RELDENS_DB_PASSWORD={{&dbPassword}}
|
|
8
|
-
RELDENS_DB_DRIVER={{&dbDriver}}
|
|
9
9
|
RELDENS_DB_URL={{&dbClient}}://{{&dbUser}}:{{&dbPassword}}@{{&dbHost}}:{{&dbPort}}/{{&dbName}}
|
|
10
10
|
|
|
11
11
|
# Admin Panel Configuration
|
package/templates/page.html
CHANGED
|
@@ -7,19 +7,24 @@
|
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes, viewport-fit=cover"/>
|
|
8
8
|
<meta name="robots" content="{{meta_robots}}">
|
|
9
9
|
<meta name="theme-color" content="{{meta_theme_color}}"/>
|
|
10
|
+
<meta property="og:title" content="{{meta_og_title}}"/>
|
|
11
|
+
<meta property="og:type" content="website"/>
|
|
12
|
+
<meta property="og:site_name" content="{{siteHandle}}"/>
|
|
13
|
+
<meta property="og:url" content="{{currentRequest.fullUrl}}"/>
|
|
14
|
+
<meta property="og:locale" content="{{locale}}"/>
|
|
10
15
|
{{#meta_description}}
|
|
11
16
|
<meta name="description" content="{{meta_description}}"/>
|
|
17
|
+
<meta property="og:description" content="{{meta_description}}"/>
|
|
12
18
|
{{/meta_description}}
|
|
13
|
-
<meta property="og:title" content="{{meta_og_title}}"/>
|
|
14
|
-
{{#meta_og_description}}
|
|
15
|
-
<meta property="og:description" content="{{meta_og_description}}"/>
|
|
16
|
-
{{/meta_og_description}}
|
|
17
19
|
{{#meta_og_image}}
|
|
18
20
|
<meta property="og:image" content="{{meta_og_image}}"/>
|
|
19
21
|
{{/meta_og_image}}
|
|
20
22
|
{{#meta_twitter_card_type}}
|
|
21
23
|
<meta name="twitter:card" content="{{meta_twitter_card_type}}"/>
|
|
22
24
|
{{/meta_twitter_card_type}}
|
|
25
|
+
{{#article_author}}
|
|
26
|
+
<meta property="article:author" content="{{article_author}}"/>
|
|
27
|
+
{{/article_author}}
|
|
23
28
|
{{#publish_date}}
|
|
24
29
|
<meta name="publish-date" content="{{publish_date}}"/>
|
|
25
30
|
{{/publish_date}}
|
|
@@ -34,14 +39,13 @@
|
|
|
34
39
|
<link rel="icon" href="[asset(/favicons/default/favicon.ico)]" type="image/x-icon"/>
|
|
35
40
|
<link rel="shortcut icon" href="[asset(/favicons/default/favicon.ico)]" type="image/x-icon">
|
|
36
41
|
<link rel="manifest" href="[asset(/favicons/default/site.webmanifest)]"/>
|
|
37
|
-
|
|
38
|
-
<link rel="stylesheet" href="[url(/css/styles.css)]"/>
|
|
39
|
-
<script defer src="[url(/js/cookie-consent.js)]"></script>
|
|
42
|
+
<link rel="stylesheet" href="[cdn(/css/styles.css)]"/>
|
|
40
43
|
</head>
|
|
41
44
|
<body class="{{siteHandle}}">
|
|
42
|
-
{{&content}}
|
|
43
|
-
{{>cookie-consent}}
|
|
44
|
-
<script defer type="text/javascript" src="[
|
|
45
|
-
<script defer type="text/javascript" src="[
|
|
45
|
+
{{&content}}
|
|
46
|
+
{{>cookie-consent}}
|
|
47
|
+
<script defer type="text/javascript" src="[cdn(/js/functions.js)]"></script>
|
|
48
|
+
<script defer type="text/javascript" src="[cdn(/js/scripts.js)]"></script>
|
|
49
|
+
<script defer src="[cdn(/js/cookie-consent.js)]"></script>
|
|
46
50
|
</body>
|
|
47
51
|
</html>
|