@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.
@@ -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 cdnUrls = Object.keys(this.domainCdnMapping).map(domain => this.domainCdnMapping[domain]);
287
- let missingCDNs = [];
288
- for(let cdnUrl of cdnUrls){
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
- if(!sc.hasOwn(this.developmentExternalDomains, cdnHostname)){
291
- missingCDNs.push(cdnHostname);
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 < missingCDNs.length){
295
- Logger.info('CDN domains not found in developmentExternalDomains: '+missingCDNs.join(', ')
296
- +'. Add them to avoid CORS issues in development mode.');
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;
@@ -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) && !sc.isArray(props.domainPublicUrlMapping) ? 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) && !sc.isArray(props.domainCdnMapping) ? 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.47.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.42.0",
37
- "@reldens/storage": "^0.84.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"
@@ -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
@@ -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
- <!-- CSS and JS -->
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="[url(/js/functions.js)]"></script>
45
- <script defer type="text/javascript" src="[url(/js/scripts.js)]"></script>
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>