@ecopex/ecopex-framework 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/.env +73 -0
  2. package/README.md +248 -0
  3. package/bun.lockb +0 -0
  4. package/config/swagger/admin.js +44 -0
  5. package/config/swagger/api.js +19 -0
  6. package/database/migrations/20240000135243_timezones.js +22 -0
  7. package/database/migrations/20240000135244_countries.js +23 -0
  8. package/database/migrations/20240000135244_create_admins_table.js +66 -0
  9. package/database/migrations/20240000135244_currencies.js +21 -0
  10. package/database/migrations/20240000135244_languages.js +21 -0
  11. package/database/migrations/20240000135244_taxes.js +10 -0
  12. package/database/migrations/20240000135245_sites.js +37 -0
  13. package/database/migrations/20240000135246_payment_methods.js +33 -0
  14. package/database/migrations/20251016113547_devices.js +37 -0
  15. package/database/migrations/20251019192600_users.js +62 -0
  16. package/database/migrations/20251019213551_language_lines.js +35 -0
  17. package/database/migrations/20251222214131_category_groups.js +18 -0
  18. package/database/migrations/20251222214619_categories.js +27 -0
  19. package/database/migrations/20251222214848_brands.js +23 -0
  20. package/database/migrations/20251222214946_products.js +30 -0
  21. package/database/migrations/20251222215428_product_images.js +18 -0
  22. package/database/migrations/20251222215553_options.js +30 -0
  23. package/database/migrations/20251222215806_variants.js +23 -0
  24. package/database/migrations/20251222215940_attributes.js +25 -0
  25. package/database/migrations/20251222220135_discounts.js +15 -0
  26. package/database/migrations/20251222220253_reviews.js +22 -0
  27. package/database/migrations/20251222220341_favorites.js +10 -0
  28. package/database/migrations/20251222220422_search_logs.js +17 -0
  29. package/database/migrations/20251222220636_orders.js +16 -0
  30. package/database/migrations/20251222220806_order_items.js +19 -0
  31. package/database/migrations/20251222221317_order_statuses.js +10 -0
  32. package/database/migrations/20251222221446_order_payments.js +13 -0
  33. package/database/migrations/20251222221654_order_addresses.js +23 -0
  34. package/database/migrations/20251222221807_order_status_logs.js +13 -0
  35. package/database/seeds/admins.js +37 -0
  36. package/database/seeds/countries.js +203 -0
  37. package/database/seeds/currencies.js +165 -0
  38. package/database/seeds/languages.js +113 -0
  39. package/database/seeds/timezones.js +149 -0
  40. package/ecosystem.config.js +26 -0
  41. package/env.example +73 -0
  42. package/knexfile.js +3 -0
  43. package/libraries/2fa.js +22 -0
  44. package/libraries/aws.js +63 -0
  45. package/libraries/bcrypt.js +284 -0
  46. package/libraries/controls.js +113 -0
  47. package/libraries/date.js +14 -0
  48. package/libraries/general.js +8 -0
  49. package/libraries/image.js +57 -0
  50. package/libraries/jwt.js +178 -0
  51. package/libraries/knex.js +7 -0
  52. package/libraries/slug.js +14 -0
  53. package/libraries/stores.js +22 -0
  54. package/libraries/upload.js +194 -0
  55. package/locales/en/messages.json +4 -0
  56. package/locales/en/sql.json +3 -0
  57. package/locales/en/validation.json +52 -0
  58. package/locales/es/validation.json +52 -0
  59. package/locales/tr/validation.json +59 -0
  60. package/package.json +75 -0
  61. package/routes/admin/auto/admins.json +63 -0
  62. package/routes/admin/auto/devices.json +37 -0
  63. package/routes/admin/auto/migrations.json +21 -0
  64. package/routes/admin/auto/users.json +61 -0
  65. package/routes/admin/middlewares/index.js +87 -0
  66. package/routes/admin/spec/auth.js +626 -0
  67. package/routes/admin/spec/users.js +3 -0
  68. package/routes/auto/handler.js +635 -0
  69. package/routes/common/auto/countries.json +28 -0
  70. package/routes/common/auto/currencies.json +26 -0
  71. package/routes/common/auto/languages.json +26 -0
  72. package/routes/common/auto/taxes.json +46 -0
  73. package/routes/common/auto/timezones.json +29 -0
  74. package/stores/base.js +73 -0
  75. package/stores/index.js +195 -0
  76. package/utils/i18n.js +187 -0
  77. package/utils/jsonRouteLoader.js +587 -0
  78. package/utils/middleware.js +154 -0
  79. package/utils/routeLoader.js +227 -0
  80. package/workers/admin.js +124 -0
  81. package/workers/api.js +106 -0
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Seed the timezones table with unique offsets.
3
+ * @param { import("knex").Knex } knex
4
+ */
5
+ exports.seed = async function(knex) {
6
+ // Inserts seed entries with unique offsets
7
+ await knex('timezones').insert([
8
+ {
9
+ timezone_id: 1,
10
+ name: 'Greenwich Mean Time',
11
+ code: 'GMT',
12
+ offset: '+00:00',
13
+ gmt: 'GMT+0'
14
+ },
15
+ {
16
+ timezone_id: 2,
17
+ name: 'Central European Time',
18
+ code: 'CET',
19
+ offset: '+01:00',
20
+ gmt: 'GMT+1'
21
+ },
22
+ {
23
+ timezone_id: 3,
24
+ name: 'Eastern European Time',
25
+ code: 'EET',
26
+ offset: '+02:00',
27
+ gmt: 'GMT+2'
28
+ },
29
+ {
30
+ timezone_id: 4,
31
+ name: 'Moscow Standard Time',
32
+ code: 'MSK',
33
+ offset: '+03:00',
34
+ gmt: 'GMT+3'
35
+ },
36
+ {
37
+ timezone_id: 5,
38
+ name: 'Pakistan Standard Time',
39
+ code: 'PKT',
40
+ offset: '+05:00',
41
+ gmt: 'GMT+5'
42
+ },
43
+ {
44
+ timezone_id: 6,
45
+ name: 'India Standard Time',
46
+ code: 'IST',
47
+ offset: '+05:30',
48
+ gmt: 'GMT+5:30'
49
+ },
50
+ {
51
+ timezone_id: 7,
52
+ name: 'Bangladesh Standard Time',
53
+ code: 'BST',
54
+ offset: '+06:00',
55
+ gmt: 'GMT+6'
56
+ },
57
+ {
58
+ timezone_id: 8,
59
+ name: 'Indochina Time',
60
+ code: 'ICT',
61
+ offset: '+07:00',
62
+ gmt: 'GMT+7'
63
+ },
64
+ {
65
+ timezone_id: 9,
66
+ name: 'China Standard Time',
67
+ code: 'CST',
68
+ offset: '+08:00',
69
+ gmt: 'GMT+8'
70
+ },
71
+ {
72
+ timezone_id: 10,
73
+ name: 'Japan Standard Time',
74
+ code: 'JST',
75
+ offset: '+09:00',
76
+ gmt: 'GMT+9'
77
+ },
78
+ {
79
+ timezone_id: 11,
80
+ name: 'Australian Eastern Standard Time',
81
+ code: 'AEST',
82
+ offset: '+10:00',
83
+ gmt: 'GMT+10'
84
+ },
85
+ {
86
+ timezone_id: 12,
87
+ name: 'New Zealand Standard Time',
88
+ code: 'NZST',
89
+ offset: '+12:00',
90
+ gmt: 'GMT+12'
91
+ },
92
+ {
93
+ timezone_id: 13,
94
+ name: 'Atlantic Standard Time',
95
+ code: 'AST',
96
+ offset: '-04:00',
97
+ gmt: 'GMT-4'
98
+ },
99
+ {
100
+ timezone_id: 14,
101
+ name: 'Eastern Standard Time',
102
+ code: 'EST',
103
+ offset: '-05:00',
104
+ gmt: 'GMT-5'
105
+ },
106
+ {
107
+ timezone_id: 15,
108
+ name: 'Central Standard Time',
109
+ code: 'CST',
110
+ offset: '-06:00',
111
+ gmt: 'GMT-6'
112
+ },
113
+ {
114
+ timezone_id: 16,
115
+ name: 'Mountain Standard Time',
116
+ code: 'MST',
117
+ offset: '-07:00',
118
+ gmt: 'GMT-7'
119
+ },
120
+ {
121
+ timezone_id: 17,
122
+ name: 'Pacific Standard Time',
123
+ code: 'PST',
124
+ offset: '-08:00',
125
+ gmt: 'GMT-8'
126
+ },
127
+ {
128
+ timezone_id: 18,
129
+ name: 'Alaska Standard Time',
130
+ code: 'AKST',
131
+ offset: '-09:00',
132
+ gmt: 'GMT-9'
133
+ },
134
+ {
135
+ timezone_id: 19,
136
+ name: 'Hawaii-Aleutian Standard Time',
137
+ code: 'HST',
138
+ offset: '-10:00',
139
+ gmt: 'GMT-10'
140
+ },
141
+ {
142
+ timezone_id: 20,
143
+ name: 'Samoa Standard Time',
144
+ code: 'SST',
145
+ offset: '-11:00',
146
+ gmt: 'GMT-11'
147
+ }
148
+ ]).onConflict('timezone_id').ignore();
149
+ };
@@ -0,0 +1,26 @@
1
+ require('dotenv').config({
2
+ path: './.env'
3
+ });
4
+
5
+ module.exports = {
6
+ apps: [
7
+ // {
8
+ // name: 'sb-system-api',
9
+ // script: './backend/workers/api.js',
10
+ // watch: process.env.NODE_ENV === 'development',
11
+ // ignore_watch: ['node_modules', 'database', 'logs', '*.log', '*.json'],
12
+ // },
13
+ {
14
+ name: 'sb-system-admin',
15
+ script: './workers/admin.js',
16
+ watch: process.env.NODE_ENV === 'development',
17
+ ignore_watch: ['node_modules', 'database', 'logs', '*.log', '*.json']
18
+ },
19
+ // {
20
+ // name : "sb-system-admin-dev",
21
+ // cwd : "./admin",
22
+ // script : "bun",
23
+ // args : "run dev"
24
+ // },
25
+ ]
26
+ };
package/env.example ADDED
@@ -0,0 +1,73 @@
1
+ # ===========================================
2
+ # SB System Environment Configuration
3
+ # ===========================================
4
+
5
+ # Application Environment
6
+ NODE_ENV=development
7
+ LOG_LEVEL=info
8
+
9
+ # Server Configuration
10
+ HOST=0.0.0.0
11
+ API_PORT=3000
12
+ ADMIN_PORT=3001
13
+
14
+ # Database Configuration
15
+ DB_HOST=localhost
16
+ DB_PORT=3306
17
+ DB_USER=root
18
+ DB_PASSWORD=your_password_here
19
+ DB_NAME=sb_system
20
+
21
+ # Alternative: Use DATABASE_URL for production
22
+ # DATABASE_URL=mysql://username:password@host:port/database
23
+
24
+ # CORS Configuration
25
+ CORS_ORIGIN=*
26
+
27
+ # PM2 Process Management
28
+ API_INSTANCES=1
29
+ API_EXEC_MODE=fork
30
+ ADMIN_INSTANCES=1
31
+ ADMIN_EXEC_MODE=fork
32
+
33
+ # Internationalization
34
+ DEFAULT_LOCALE=en
35
+ SUPPORTED_LOCALES=en,tr,es
36
+
37
+ # Security (for production)
38
+ # JWT_SECRET=your_jwt_secret_here
39
+ # BCRYPT_ROUNDS=12
40
+ # SESSION_SECRET=your_session_secret_here
41
+
42
+ # Redis (for caching/sessions)
43
+ # REDIS_HOST=localhost
44
+ # REDIS_PORT=6379
45
+ # REDIS_PASSWORD=
46
+
47
+ # Email Configuration (for notifications)
48
+ # SMTP_HOST=smtp.gmail.com
49
+ # SMTP_PORT=587
50
+ # SMTP_USER=your_email@gmail.com
51
+ # SMTP_PASS=your_app_password
52
+ # FROM_EMAIL=noreply@yoursystem.com
53
+
54
+ # File Upload Configuration
55
+ # UPLOAD_MAX_SIZE=10485760
56
+ # UPLOAD_ALLOWED_TYPES=image/jpeg,image/png,image/gif,application/pdf
57
+
58
+ # Rate Limiting
59
+ # RATE_LIMIT_WINDOW_MS=900000
60
+ # RATE_LIMIT_MAX_REQUESTS=100
61
+
62
+ # Logging
63
+ # LOG_FILE_PATH=./logs/app.log
64
+ # LOG_MAX_SIZE=10m
65
+ # LOG_MAX_FILES=5
66
+
67
+ # Monitoring
68
+ # HEALTH_CHECK_INTERVAL=30000
69
+ # METRICS_ENABLED=true
70
+
71
+ # Development Tools
72
+ # DEBUG=sb_system:*
73
+ # NODE_OPTIONS=--max-old-space-size=4096
package/knexfile.js ADDED
@@ -0,0 +1,3 @@
1
+ require('dotenv').config();
2
+
3
+ module.exports = require('./config/database');
@@ -0,0 +1,22 @@
1
+ const twofactor = require("node-2fa");
2
+
3
+ const generate_2fa_secret = (account = '') => {
4
+ const newSecret = twofactor.generateSecret({ name: process.env.NAME || 'System', account });
5
+ return newSecret
6
+ }
7
+
8
+ const generate_2fa_token = (secret = '') => {
9
+ const token = twofactor.generateToken(secret);
10
+ return token
11
+ }
12
+
13
+ const verify_2fa_token = (secret = '', token = '') => {
14
+ const isValid = twofactor.verifyToken(secret, token);
15
+ return isValid
16
+ }
17
+
18
+ module.exports = {
19
+ generate_2fa_secret,
20
+ generate_2fa_token,
21
+ verify_2fa_token
22
+ }
@@ -0,0 +1,63 @@
1
+ const { S3Client } = require("@aws-sdk/client-s3");
2
+ const { Upload } = require("@aws-sdk/lib-storage");
3
+ const fs = require('fs');
4
+
5
+ // Config
6
+
7
+ // UPLOAD Base
8
+ const uploadBase = __dirname + '../uploads/'
9
+
10
+ // The name of the bucket that you have created
11
+ const BUCKET_NAME = process.env.AWS_BUCKET_NAME;
12
+
13
+
14
+ const uploadAws = async (filePath, contentType) => {
15
+
16
+ const client = new S3Client({
17
+ bucketEndpoint: BUCKET_NAME,
18
+ region: process.env.AWS_REGION,
19
+ credentials: {
20
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
21
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID
22
+ }
23
+ });
24
+
25
+ let uploadStatus = false
26
+
27
+ // Read content from the file
28
+ const fileContent = await fs.readFileSync(uploadBase + filePath);
29
+
30
+ // Setting up S3 upload parameters
31
+ const params = {
32
+ ACL: 'public-read',
33
+ Bucket: BUCKET_NAME,
34
+ Key: filePath, // File name you want to save as in S3
35
+ Body: fileContent,
36
+ ContentType: contentType
37
+ };
38
+
39
+ // Uploading files to the bucket
40
+ try {
41
+
42
+ const uploadAws = await Upload({
43
+ client,
44
+ params
45
+ });
46
+ fs.unlinkSync(uploadBase + filePath);
47
+
48
+ return {
49
+ status: true,
50
+ uploadAws
51
+ }
52
+
53
+ } catch (error) {
54
+ return {
55
+ status: false,
56
+ msg: error
57
+ };
58
+ }
59
+ }
60
+
61
+ module.exports = {
62
+ uploadAws
63
+ }
@@ -0,0 +1,284 @@
1
+ const bcrypt = require('bcryptjs');
2
+
3
+ /**
4
+ * Bcrypt Library Class
5
+ * Provides methods for password hashing, comparison, and salt generation
6
+ */
7
+ class Bcrypt {
8
+ constructor(saltRounds = 12) {
9
+ this.saltRounds = saltRounds;
10
+ }
11
+
12
+ /**
13
+ * Hash a password using bcrypt
14
+ * @param {string} password - The password to hash
15
+ * @param {number} saltRounds - Number of salt rounds (optional, uses instance default)
16
+ * @returns {Promise<string>} Hashed password
17
+ */
18
+ async hash(password, saltRounds = this.saltRounds) {
19
+ try {
20
+ if (!password || typeof password !== 'string') {
21
+ throw new Error('Password must be a non-empty string');
22
+ }
23
+
24
+ if (saltRounds < 4 || saltRounds > 31) {
25
+ throw new Error('Salt rounds must be between 4 and 31');
26
+ }
27
+
28
+ return await bcrypt.hash(password, saltRounds);
29
+ } catch (error) {
30
+ throw new Error(`Bcrypt Hash Error: ${error.message}`);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Compare a password with its hash
36
+ * @param {string} password - The plain text password
37
+ * @param {string} hash - The hashed password to compare against
38
+ * @returns {Promise<boolean>} True if password matches, false otherwise
39
+ */
40
+ async compare(password, hash) {
41
+ try {
42
+ if (!password || typeof password !== 'string') {
43
+ throw new Error('Password must be a non-empty string');
44
+ }
45
+
46
+ if (!hash || typeof hash !== 'string') {
47
+ throw new Error('Hash must be a non-empty string');
48
+ }
49
+
50
+ return await bcrypt.compare(password, hash);
51
+ } catch (error) {
52
+ throw new Error(`Bcrypt Compare Error: ${error.message}`);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Generate a salt
58
+ * @param {number} saltRounds - Number of salt rounds (optional, uses instance default)
59
+ * @returns {Promise<string>} Generated salt
60
+ */
61
+ async genSalt(saltRounds = this.saltRounds) {
62
+ try {
63
+ if (saltRounds < 4 || saltRounds > 31) {
64
+ throw new Error('Salt rounds must be between 4 and 31');
65
+ }
66
+
67
+ return await bcrypt.genSalt(saltRounds);
68
+ } catch (error) {
69
+ throw new Error(`Bcrypt GenSalt Error: ${error.message}`);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Hash a password with a specific salt
75
+ * @param {string} password - The password to hash
76
+ * @param {string} salt - The salt to use
77
+ * @returns {Promise<string>} Hashed password
78
+ */
79
+ async hashWithSalt(password, salt) {
80
+ try {
81
+ if (!password || typeof password !== 'string') {
82
+ throw new Error('Password must be a non-empty string');
83
+ }
84
+
85
+ if (!salt || typeof salt !== 'string') {
86
+ throw new Error('Salt must be a non-empty string');
87
+ }
88
+
89
+ return await bcrypt.hash(password, salt);
90
+ } catch (error) {
91
+ throw new Error(`Bcrypt HashWithSalt Error: ${error.message}`);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Get the number of rounds used in a hash
97
+ * @param {string} hash - The bcrypt hash
98
+ * @returns {number} Number of rounds used
99
+ */
100
+ getRounds(hash) {
101
+ try {
102
+ if (!hash || typeof hash !== 'string') {
103
+ throw new Error('Hash must be a non-empty string');
104
+ }
105
+
106
+ return bcrypt.getRounds(hash);
107
+ } catch (error) {
108
+ throw new Error(`Bcrypt GetRounds Error: ${error.message}`);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Check if a hash needs to be upgraded (rehashed with more rounds)
114
+ * @param {string} hash - The bcrypt hash to check
115
+ * @param {number} minRounds - Minimum rounds required (optional, uses instance default)
116
+ * @returns {boolean} True if hash needs upgrading, false otherwise
117
+ */
118
+ needsUpgrade(hash, minRounds = this.saltRounds) {
119
+ try {
120
+ if (!hash || typeof hash !== 'string') {
121
+ throw new Error('Hash must be a non-empty string');
122
+ }
123
+
124
+ const currentRounds = this.getRounds(hash);
125
+ return currentRounds < minRounds;
126
+ } catch (error) {
127
+ throw new Error(`Bcrypt NeedsUpgrade Error: ${error.message}`);
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Upgrade a hash to use more rounds
133
+ * @param {string} password - The original password
134
+ * @param {string} oldHash - The old hash
135
+ * @param {number} newRounds - New number of rounds (optional, uses instance default)
136
+ * @returns {Promise<string>} New upgraded hash
137
+ */
138
+ async upgradeHash(password, oldHash, newRounds = this.saltRounds) {
139
+ try {
140
+ if (!password || typeof password !== 'string') {
141
+ throw new Error('Password must be a non-empty string');
142
+ }
143
+
144
+ if (!oldHash || typeof oldHash !== 'string') {
145
+ throw new Error('Old hash must be a non-empty string');
146
+ }
147
+
148
+ // Verify the old password first
149
+ const isValid = await this.compare(password, oldHash);
150
+ if (!isValid) {
151
+ throw new Error('Invalid password for hash upgrade');
152
+ }
153
+
154
+ // Generate new hash with more rounds
155
+ return await this.hash(password, newRounds);
156
+ } catch (error) {
157
+ throw new Error(`Bcrypt UpgradeHash Error: ${error.message}`);
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Validate password strength
163
+ * @param {string} password - The password to validate
164
+ * @param {Object} options - Validation options
165
+ * @returns {Object} Validation result with isValid and errors
166
+ */
167
+ validatePassword(password, options = {}) {
168
+ const defaults = {
169
+ minLength: 8,
170
+ requireUppercase: true,
171
+ requireLowercase: true,
172
+ requireNumbers: true,
173
+ requireSpecialChars: true,
174
+ maxLength: 128
175
+ };
176
+
177
+ const config = { ...defaults, ...options };
178
+ const errors = [];
179
+
180
+ if (!password || typeof password !== 'string') {
181
+ return { isValid: false, errors: ['Password must be a string'] };
182
+ }
183
+
184
+ if (password.length < config.minLength) {
185
+ errors.push(`Password must be at least ${config.minLength} characters long`);
186
+ }
187
+
188
+ if (password.length > config.maxLength) {
189
+ errors.push(`Password must be no more than ${config.maxLength} characters long`);
190
+ }
191
+
192
+ if (config.requireUppercase && !/[A-Z]/.test(password)) {
193
+ errors.push('Password must contain at least one uppercase letter');
194
+ }
195
+
196
+ if (config.requireLowercase && !/[a-z]/.test(password)) {
197
+ errors.push('Password must contain at least one lowercase letter');
198
+ }
199
+
200
+ if (config.requireNumbers && !/\d/.test(password)) {
201
+ errors.push('Password must contain at least one number');
202
+ }
203
+
204
+ if (config.requireSpecialChars && !/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) {
205
+ errors.push('Password must contain at least one special character');
206
+ }
207
+
208
+ return {
209
+ isValid: errors.length === 0,
210
+ errors
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Set the default number of salt rounds
216
+ * @param {number} saltRounds - Number of salt rounds
217
+ */
218
+ setSaltRounds(saltRounds) {
219
+ if (saltRounds < 4 || saltRounds > 31) {
220
+ throw new Error('Salt rounds must be between 4 and 31');
221
+ }
222
+ this.saltRounds = saltRounds;
223
+ }
224
+
225
+ /**
226
+ * Get the current number of salt rounds
227
+ * @returns {number} Current salt rounds
228
+ */
229
+ getSaltRounds() {
230
+ return this.saltRounds;
231
+ }
232
+
233
+ /**
234
+ * Generate a random password
235
+ * @param {Object} options - Password generation options
236
+ * @returns {string} Generated password
237
+ */
238
+ generatePassword(options = {}) {
239
+ const defaults = {
240
+ length: 16,
241
+ includeUppercase: true,
242
+ includeLowercase: true,
243
+ includeNumbers: true,
244
+ includeSpecialChars: true,
245
+ excludeSimilar: true
246
+ };
247
+
248
+ const config = { ...defaults, ...options };
249
+ let charset = '';
250
+
251
+ if (config.includeLowercase) {
252
+ charset += 'abcdefghijklmnopqrstuvwxyz';
253
+ }
254
+
255
+ if (config.includeUppercase) {
256
+ charset += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
257
+ }
258
+
259
+ if (config.includeNumbers) {
260
+ charset += '0123456789';
261
+ }
262
+
263
+ if (config.includeSpecialChars) {
264
+ charset += '!@#$%^&*()_+-=[]{}|;:,.<>?';
265
+ }
266
+
267
+ if (config.excludeSimilar) {
268
+ charset = charset.replace(/[il1Lo0O]/g, '');
269
+ }
270
+
271
+ if (charset.length === 0) {
272
+ throw new Error('At least one character type must be included');
273
+ }
274
+
275
+ let password = '';
276
+ for (let i = 0; i < config.length; i++) {
277
+ password += charset.charAt(Math.floor(Math.random() * charset.length));
278
+ }
279
+
280
+ return password;
281
+ }
282
+ }
283
+
284
+ module.exports = Bcrypt;