@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.
- package/.env +73 -0
- package/README.md +248 -0
- package/bun.lockb +0 -0
- package/config/swagger/admin.js +44 -0
- package/config/swagger/api.js +19 -0
- package/database/migrations/20240000135243_timezones.js +22 -0
- package/database/migrations/20240000135244_countries.js +23 -0
- package/database/migrations/20240000135244_create_admins_table.js +66 -0
- package/database/migrations/20240000135244_currencies.js +21 -0
- package/database/migrations/20240000135244_languages.js +21 -0
- package/database/migrations/20240000135244_taxes.js +10 -0
- package/database/migrations/20240000135245_sites.js +37 -0
- package/database/migrations/20240000135246_payment_methods.js +33 -0
- package/database/migrations/20251016113547_devices.js +37 -0
- package/database/migrations/20251019192600_users.js +62 -0
- package/database/migrations/20251019213551_language_lines.js +35 -0
- package/database/migrations/20251222214131_category_groups.js +18 -0
- package/database/migrations/20251222214619_categories.js +27 -0
- package/database/migrations/20251222214848_brands.js +23 -0
- package/database/migrations/20251222214946_products.js +30 -0
- package/database/migrations/20251222215428_product_images.js +18 -0
- package/database/migrations/20251222215553_options.js +30 -0
- package/database/migrations/20251222215806_variants.js +23 -0
- package/database/migrations/20251222215940_attributes.js +25 -0
- package/database/migrations/20251222220135_discounts.js +15 -0
- package/database/migrations/20251222220253_reviews.js +22 -0
- package/database/migrations/20251222220341_favorites.js +10 -0
- package/database/migrations/20251222220422_search_logs.js +17 -0
- package/database/migrations/20251222220636_orders.js +16 -0
- package/database/migrations/20251222220806_order_items.js +19 -0
- package/database/migrations/20251222221317_order_statuses.js +10 -0
- package/database/migrations/20251222221446_order_payments.js +13 -0
- package/database/migrations/20251222221654_order_addresses.js +23 -0
- package/database/migrations/20251222221807_order_status_logs.js +13 -0
- package/database/seeds/admins.js +37 -0
- package/database/seeds/countries.js +203 -0
- package/database/seeds/currencies.js +165 -0
- package/database/seeds/languages.js +113 -0
- package/database/seeds/timezones.js +149 -0
- package/ecosystem.config.js +26 -0
- package/env.example +73 -0
- package/knexfile.js +3 -0
- package/libraries/2fa.js +22 -0
- package/libraries/aws.js +63 -0
- package/libraries/bcrypt.js +284 -0
- package/libraries/controls.js +113 -0
- package/libraries/date.js +14 -0
- package/libraries/general.js +8 -0
- package/libraries/image.js +57 -0
- package/libraries/jwt.js +178 -0
- package/libraries/knex.js +7 -0
- package/libraries/slug.js +14 -0
- package/libraries/stores.js +22 -0
- package/libraries/upload.js +194 -0
- package/locales/en/messages.json +4 -0
- package/locales/en/sql.json +3 -0
- package/locales/en/validation.json +52 -0
- package/locales/es/validation.json +52 -0
- package/locales/tr/validation.json +59 -0
- package/package.json +75 -0
- package/routes/admin/auto/admins.json +63 -0
- package/routes/admin/auto/devices.json +37 -0
- package/routes/admin/auto/migrations.json +21 -0
- package/routes/admin/auto/users.json +61 -0
- package/routes/admin/middlewares/index.js +87 -0
- package/routes/admin/spec/auth.js +626 -0
- package/routes/admin/spec/users.js +3 -0
- package/routes/auto/handler.js +635 -0
- package/routes/common/auto/countries.json +28 -0
- package/routes/common/auto/currencies.json +26 -0
- package/routes/common/auto/languages.json +26 -0
- package/routes/common/auto/taxes.json +46 -0
- package/routes/common/auto/timezones.json +29 -0
- package/stores/base.js +73 -0
- package/stores/index.js +195 -0
- package/utils/i18n.js +187 -0
- package/utils/jsonRouteLoader.js +587 -0
- package/utils/middleware.js +154 -0
- package/utils/routeLoader.js +227 -0
- package/workers/admin.js +124 -0
- 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
package/libraries/2fa.js
ADDED
|
@@ -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
|
+
}
|
package/libraries/aws.js
ADDED
|
@@ -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;
|