Initial commit
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
PORT=5000
|
||||
NODE_ENV=development
|
||||
CORS_ORIGIN=http://localhost:5173
|
||||
JWT_SECRET=supersecretjwtkeythatislongandsecure
|
||||
JWT_EXPIRES_IN=7d
|
||||
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=5432
|
||||
DB_USER=postgres
|
||||
DB_PASS=Liyaqath@7890
|
||||
DB_NAME=Pim_Maskan
|
||||
DB_DIALECT=postgres
|
||||
|
||||
# IMPORTANT: Gmail SMTP requires an App Password, NOT your regular password.
|
||||
# Go to: https://myaccount.google.com -> Security -> 2-Step Verification -> App Passwords
|
||||
# Generate a 16-character App Password and paste it below (no spaces).
|
||||
EMAIL_HOST=smtp.gmail.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_USER=pmahboob001@gmail.com
|
||||
EMAIL_PASSWORD=hlwg yfgi xmkn lqmg
|
||||
EMAIL_FROM=Maskan PIM <pmahboob001@gmail.com>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
PORT=5000
|
||||
NODE_ENV=development
|
||||
CORS_ORIGIN=http://localhost:5173
|
||||
JWT_SECRET=supersecretjwtkeythatislongandsecure
|
||||
JWT_EXPIRES_IN=7d
|
||||
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=5432
|
||||
DB_USER=postgres
|
||||
DB_PASS=Liyaqath@7890
|
||||
DB_NAME=Pim_Maskan
|
||||
DB_DIALECT=postgres
|
||||
|
||||
# IMPORTANT: Gmail SMTP requires an App Password, NOT your regular password.
|
||||
# Go to: https://myaccount.google.com -> Security -> 2-Step Verification -> App Passwords
|
||||
# Generate a 16-character App Password and paste it below (no spaces).
|
||||
EMAIL_HOST=smtp.gmail.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_USER=pmahboob001@gmail.com
|
||||
EMAIL_PASSWORD=hlwg yfgi xmkn lqmg
|
||||
EMAIL_FROM=Maskan PIM <pmahboob001@gmail.com>
|
||||
@@ -0,0 +1,21 @@
|
||||
PORT=5000
|
||||
NODE_ENV=test
|
||||
CORS_ORIGIN=http://localhost:5173
|
||||
JWT_SECRET=supersecretjwtkeythatislongandsecure
|
||||
JWT_EXPIRES_IN=7d
|
||||
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=5432
|
||||
DB_USER=postgres
|
||||
DB_PASS=Liyaqath@7890
|
||||
DB_NAME=Pim_Maskan_Test
|
||||
DB_DIALECT=postgres
|
||||
|
||||
# IMPORTANT: Gmail SMTP requires an App Password, NOT your regular password.
|
||||
# Go to: https://myaccount.google.com -> Security -> 2-Step Verification -> App Passwords
|
||||
# Generate a 16-character App Password and paste it below (no spaces).
|
||||
EMAIL_HOST=smtp.gmail.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_USER=pmahboob001@gmail.com
|
||||
EMAIL_PASSWORD=hlwg yfgi xmkn lqmg
|
||||
EMAIL_FROM=Maskan PIM <pmahboob001@gmail.com>
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"keep": {
|
||||
"days": true,
|
||||
"amount": 14
|
||||
},
|
||||
"auditLog": "logs\\.3fa4d5e91883ce1217f62a49f94203b42caa131d-audit.json",
|
||||
"files": [
|
||||
{
|
||||
"date": 1782885016196,
|
||||
"name": "logs\\combined-2026-07-01.log",
|
||||
"hash": "62273be471fbf03240935eebb5eaf8487e7a3002b6cced421c983245646bc21a"
|
||||
},
|
||||
{
|
||||
"date": 1782968983048,
|
||||
"name": "logs\\combined-2026-07-02.log",
|
||||
"hash": "10138c0f35436a800870f14a317b542645e7976bcd44030300d436a715a20c36"
|
||||
}
|
||||
],
|
||||
"hashType": "sha256"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"keep": {
|
||||
"days": true,
|
||||
"amount": 14
|
||||
},
|
||||
"auditLog": "logs\\.944c2a6e38c8046075bb4cb64c11b1f8d919853c-audit.json",
|
||||
"files": [
|
||||
{
|
||||
"date": 1782885016194,
|
||||
"name": "logs\\error-2026-07-01.log",
|
||||
"hash": "3dc68d8ca2e67e313b2363c163e011da72b07deaf5d0f5736607ec0850b50e9b"
|
||||
},
|
||||
{
|
||||
"date": 1782968983045,
|
||||
"name": "logs\\error-2026-07-02.log",
|
||||
"hash": "bf64fe21cbfba23af08772bef924ee896b52bcbf80bb4e87b6f7c5fb125e38aa"
|
||||
}
|
||||
],
|
||||
"hashType": "sha256"
|
||||
}
|
||||
Generated
+311
-1
@@ -19,12 +19,15 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nodemailer": "^9.0.3",
|
||||
"pg": "^8.13.1",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"sequelize": "^6.37.5",
|
||||
"socket.io": "^4.8.1",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.1"
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"winston": "^3.19.0",
|
||||
"winston-daily-rotate-file": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.9",
|
||||
@@ -82,6 +85,26 @@
|
||||
"openapi-types": ">=7"
|
||||
}
|
||||
},
|
||||
"node_modules/@colors/colors": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
|
||||
"integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/@dabh/diagnostics": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz",
|
||||
"integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@so-ric/colorspace": "^1.1.6",
|
||||
"enabled": "2.0.x",
|
||||
"kuler": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
@@ -125,6 +148,16 @@
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@so-ric/colorspace": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz",
|
||||
"integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color": "^5.0.2",
|
||||
"text-hex": "1.0.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
@@ -170,6 +203,12 @@
|
||||
"undici-types": "~8.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/triple-beam": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
|
||||
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/validator": {
|
||||
"version": "13.15.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz",
|
||||
@@ -305,6 +344,12 @@
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/at-least-node": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
|
||||
@@ -617,6 +662,19 @@
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz",
|
||||
"integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^3.1.3",
|
||||
"color-string": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -637,6 +695,48 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color-string": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz",
|
||||
"integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/color-string/node_modules/color-name": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz",
|
||||
"integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/color/node_modules/color-convert": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz",
|
||||
"integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.6"
|
||||
}
|
||||
},
|
||||
"node_modules/color/node_modules/color-name": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz",
|
||||
"integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
|
||||
@@ -943,6 +1043,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/enabled": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
|
||||
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
@@ -1150,6 +1256,21 @@
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fecha": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
|
||||
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-stream-rotator": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz",
|
||||
"integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"moment": "^2.29.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
@@ -1181,6 +1302,12 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fn.name": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
|
||||
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
@@ -1577,6 +1704,18 @@
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
@@ -1724,6 +1863,12 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/kuler": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
|
||||
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
@@ -1778,6 +1923,29 @@
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/logform": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz",
|
||||
"integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colors/colors": "1.6.0",
|
||||
"@types/triple-beam": "^1.3.2",
|
||||
"fecha": "^4.2.0",
|
||||
"ms": "^2.1.1",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/logform/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
@@ -2003,6 +2171,15 @@
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.3.tgz",
|
||||
"integrity": "sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon": {
|
||||
"version": "3.1.14",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
|
||||
@@ -2092,6 +2269,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-hash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
@@ -2125,6 +2311,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/one-time": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
|
||||
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fn.name": "1.x.x"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
@@ -2513,6 +2708,15 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-stable-stringify": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
|
||||
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
@@ -2936,6 +3140,15 @@
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-trace": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
|
||||
"integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
@@ -3224,6 +3437,12 @@
|
||||
"express": ">=4.0.0 || >=5.0.0-beta"
|
||||
}
|
||||
},
|
||||
"node_modules/text-hex": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
||||
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -3262,6 +3481,15 @@
|
||||
"nodetouch": "bin/nodetouch.js"
|
||||
}
|
||||
},
|
||||
"node_modules/triple-beam": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
|
||||
"integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
@@ -3390,6 +3618,88 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/winston": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz",
|
||||
"integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colors/colors": "^1.6.0",
|
||||
"@dabh/diagnostics": "^2.0.8",
|
||||
"async": "^3.2.3",
|
||||
"is-stream": "^2.0.0",
|
||||
"logform": "^2.7.0",
|
||||
"one-time": "^1.0.0",
|
||||
"readable-stream": "^3.4.0",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"stack-trace": "0.0.x",
|
||||
"triple-beam": "^1.3.0",
|
||||
"winston-transport": "^4.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/winston-daily-rotate-file": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz",
|
||||
"integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"file-stream-rotator": "^0.6.1",
|
||||
"object-hash": "^3.0.0",
|
||||
"triple-beam": "^1.4.1",
|
||||
"winston-transport": "^4.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"winston": "^3"
|
||||
}
|
||||
},
|
||||
"node_modules/winston-transport": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz",
|
||||
"integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"logform": "^2.7.0",
|
||||
"readable-stream": "^3.6.2",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/winston-transport/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/winston/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/wkx": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz",
|
||||
|
||||
+4
-1
@@ -23,12 +23,15 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nodemailer": "^9.0.3",
|
||||
"pg": "^8.13.1",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"sequelize": "^6.37.5",
|
||||
"socket.io": "^4.8.1",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.1"
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"winston": "^3.19.0",
|
||||
"winston-daily-rotate-file": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.9",
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Attribute } from './attribute.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class AttributeRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Attribute.findAll(options);
|
||||
return await models.Attribute.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Attribute.findByPk(id, options);
|
||||
return await models.Attribute.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Attribute.create(data, options);
|
||||
return await models.Attribute.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { AuditLog } from './auditLog.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class AuditLogRepository {
|
||||
async findAll(options = {}) {
|
||||
return await AuditLog.findAll(options);
|
||||
return await models.AuditLog.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await AuditLog.findByPk(id, options);
|
||||
return await models.AuditLog.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await AuditLog.create(data, options);
|
||||
return await models.AuditLog.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import service from './role.service.js';
|
||||
|
||||
export class RoleController {
|
||||
async getAll(req, res, next) {
|
||||
try {
|
||||
const records = await service.getAll(req.query);
|
||||
return res.status(200).json({ success: true, data: records });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getByTenant(req, res, next) {
|
||||
try {
|
||||
const { tenantId } = req.params;
|
||||
const records = await service.getByTenant(tenantId === 'platform' ? null : tenantId);
|
||||
return res.status(200).json({ success: true, data: records });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getById(req, res, next) {
|
||||
try {
|
||||
const record = await service.getById(req.params.id);
|
||||
return res.status(200).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async create(req, res, next) {
|
||||
try {
|
||||
const record = await service.create(req.body);
|
||||
return res.status(201).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async update(req, res, next) {
|
||||
try {
|
||||
const record = await service.update(req.params.id, req.body);
|
||||
return res.status(200).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(req, res, next) {
|
||||
try {
|
||||
await service.delete(req.params.id);
|
||||
return res.status(200).json({ success: true, message: 'Role deleted successfully' });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getPermissions(req, res, next) {
|
||||
try {
|
||||
const permissions = await service.getPermissionNodes();
|
||||
return res.status(200).json({ success: true, data: permissions });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new RoleController();
|
||||
@@ -0,0 +1,129 @@
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
import sequelize from '../../../shared/database/connection.js';
|
||||
|
||||
export class RoleRepository {
|
||||
async findAll(options = {}) {
|
||||
return await models.Role.findAll({
|
||||
include: [
|
||||
{
|
||||
model: models.PermissionNode,
|
||||
as: 'permissions',
|
||||
through: { attributes: ['can_view', 'can_create', 'can_edit', 'can_delete', 'can_alter', 'can_import', 'can_export'] }
|
||||
}
|
||||
],
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await models.Role.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: models.PermissionNode,
|
||||
as: 'permissions',
|
||||
through: { attributes: ['can_view', 'can_create', 'can_edit', 'can_delete', 'can_alter', 'can_import', 'can_export'] }
|
||||
}
|
||||
],
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
async create(roleData, permissions = []) {
|
||||
const transaction = await sequelize.transaction();
|
||||
try {
|
||||
const role = await models.Role.create(roleData, { transaction });
|
||||
|
||||
if (permissions && permissions.length > 0) {
|
||||
const validPermissions = permissions.filter(p => p.node_id != null);
|
||||
if (validPermissions.length > 0) {
|
||||
const rolePermissions = validPermissions.map(p => ({
|
||||
role_id: role.id,
|
||||
node_id: p.node_id,
|
||||
can_view: p.can_view || false,
|
||||
can_create: p.can_create || false,
|
||||
can_edit: p.can_edit || false,
|
||||
can_delete: p.can_delete || false,
|
||||
can_alter: p.can_alter || false,
|
||||
can_import: p.can_import || false,
|
||||
can_export: p.can_export || false
|
||||
}));
|
||||
await models.RolePermission.bulkCreate(rolePermissions, { transaction });
|
||||
}
|
||||
}
|
||||
|
||||
await transaction.commit();
|
||||
return await this.findById(role.id);
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async update(id, roleData, permissions = []) {
|
||||
const transaction = await sequelize.transaction();
|
||||
try {
|
||||
const role = await models.Role.findByPk(id, { transaction });
|
||||
if (!role) return null;
|
||||
|
||||
await role.update(roleData, { transaction });
|
||||
|
||||
// Sync permissions: Delete old permissions first
|
||||
await models.RolePermission.destroy({
|
||||
where: { role_id: id },
|
||||
transaction
|
||||
});
|
||||
|
||||
// Insert new permissions
|
||||
if (permissions && permissions.length > 0) {
|
||||
const validPermissions = permissions.filter(p => p.node_id != null);
|
||||
if (validPermissions.length > 0) {
|
||||
const rolePermissions = validPermissions.map(p => ({
|
||||
role_id: id,
|
||||
node_id: p.node_id,
|
||||
can_view: p.can_view || false,
|
||||
can_create: p.can_create || false,
|
||||
can_edit: p.can_edit || false,
|
||||
can_delete: p.can_delete || false,
|
||||
can_alter: p.can_alter || false,
|
||||
can_import: p.can_import || false,
|
||||
can_export: p.can_export || false
|
||||
}));
|
||||
// Use bulkCreate instead of bulkInsert on the model
|
||||
await models.RolePermission.bulkCreate(rolePermissions, { transaction });
|
||||
}
|
||||
}
|
||||
|
||||
await transaction.commit();
|
||||
return await this.findById(id);
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
console.error('BULK CREATE ERROR FULL:', error);
|
||||
if (error.original) {
|
||||
console.error('ORIGINAL ERROR:', error.original);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id) {
|
||||
const transaction = await sequelize.transaction();
|
||||
try {
|
||||
const role = await models.Role.findByPk(id, { transaction });
|
||||
if (!role) return false;
|
||||
|
||||
// Delete associations first
|
||||
await models.RolePermission.destroy({ where: { role_id: id }, transaction });
|
||||
await models.UserRole.destroy({ where: { role_id: id }, transaction });
|
||||
|
||||
await role.destroy({ transaction });
|
||||
|
||||
await transaction.commit();
|
||||
return true;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new RoleRepository();
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Router } from 'express';
|
||||
import controller from './role.controller.js';
|
||||
import { authenticate } from '../../../shared/middleware/auth.middleware.js';
|
||||
import { authorize } from '../../../shared/middleware/permission.middleware.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['settings.roles.view']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/by-tenant/:tenantId',
|
||||
authenticate,
|
||||
authorize([]),
|
||||
controller.getByTenant
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/permissions',
|
||||
authenticate,
|
||||
authorize(['settings.roles.view']),
|
||||
controller.getPermissions
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['settings.roles.view']),
|
||||
controller.getById
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['settings.roles.create']),
|
||||
controller.create
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['settings.roles.edit']),
|
||||
controller.update
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['settings.roles.delete']),
|
||||
controller.delete
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,75 @@
|
||||
import repository from './role.repository.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class RoleService {
|
||||
async getAll(query = {}) {
|
||||
return await repository.findAll();
|
||||
}
|
||||
|
||||
async getByTenant(tenantId) {
|
||||
if (tenantId === 'all') {
|
||||
return await repository.findAll();
|
||||
}
|
||||
return await repository.findAll({
|
||||
where: tenantId
|
||||
? { tenant_id: tenantId }
|
||||
: { tenant_id: null }
|
||||
});
|
||||
}
|
||||
|
||||
async getById(id) {
|
||||
const record = await repository.findById(id);
|
||||
if (!record) {
|
||||
throw new Error('Role not found');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
async create(data) {
|
||||
const { role_name, description, permissions, tenant_id } = data;
|
||||
// Generate role code from name: Admin Editor -> ADMIN_EDITOR
|
||||
const role_code = role_name.toUpperCase().replace(/[^A-Z0-9]/g, '_');
|
||||
|
||||
return await repository.create({
|
||||
role_name,
|
||||
role_code,
|
||||
description,
|
||||
tenant_id: tenant_id || null,
|
||||
role_type: tenant_id ? 'tenant' : 'platform',
|
||||
is_system_role: false,
|
||||
status: true
|
||||
}, permissions);
|
||||
}
|
||||
|
||||
async update(id, data) {
|
||||
const { role_name, description, permissions, status } = data;
|
||||
const roleData = {};
|
||||
if (role_name) {
|
||||
roleData.role_name = role_name;
|
||||
roleData.role_code = role_name.toUpperCase().replace(/[^A-Z0-9]/g, '_');
|
||||
}
|
||||
if (description !== undefined) {
|
||||
roleData.description = description;
|
||||
}
|
||||
if (status !== undefined) {
|
||||
roleData.status = status;
|
||||
}
|
||||
|
||||
return await repository.update(id, roleData, permissions);
|
||||
}
|
||||
|
||||
async delete(id) {
|
||||
const deleted = await repository.delete(id);
|
||||
if (!deleted) {
|
||||
throw new Error('Role not found');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get all permission nodes currently in database
|
||||
async getPermissionNodes() {
|
||||
return await models.PermissionNode.findAll();
|
||||
}
|
||||
}
|
||||
|
||||
export default new RoleService();
|
||||
@@ -28,6 +28,44 @@ export class AuthController {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
async acceptInvite(req, res, next) {
|
||||
try {
|
||||
const { email, tempPassword, newPassword } = req.body;
|
||||
const result = await authService.acceptInvite({ email, tempPassword, newPassword });
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Invitation accepted successfully',
|
||||
data: result
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async forgotPassword(req, res, next) {
|
||||
try {
|
||||
const result = await authService.forgotPassword(req.body);
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: result.message
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async resetPassword(req, res, next) {
|
||||
try {
|
||||
const result = await authService.resetPassword(req.body);
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: result.message,
|
||||
data: result
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new AuthController();
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Auth } from './auth.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class AuthRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Auth.findAll(options);
|
||||
return await models.Auth.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Auth.findByPk(id, options);
|
||||
return await models.Auth.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Auth.create(data, options);
|
||||
return await models.Auth.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -64,4 +64,19 @@ router.post(
|
||||
controller.refreshToken
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/accept-invite',
|
||||
controller.acceptInvite
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/forgot-password',
|
||||
controller.forgotPassword
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/reset-password',
|
||||
controller.resetPassword
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
const { User, Role, PermissionNode, Tenant } = models;
|
||||
import { generateToken, generateRefreshToken, verifyRefreshToken } from '../../../utils/helpers/jwt.utils.js';
|
||||
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
|
||||
import { sendEmail } from '../../../shared/utils/email.js';
|
||||
import crypto from 'crypto';
|
||||
|
||||
const formatPermissions = (roles) => {
|
||||
const permissionMap = {};
|
||||
@@ -29,6 +30,7 @@ const formatPermissions = (roles) => {
|
||||
};
|
||||
|
||||
export const login = async ({ email, password }) => {
|
||||
const { User, Role, PermissionNode, Tenant } = models;
|
||||
const user = await User.findOne({
|
||||
where: { email },
|
||||
include: [
|
||||
@@ -77,6 +79,13 @@ export const login = async ({ email, password }) => {
|
||||
|
||||
const permissions = formatPermissions(user.roles);
|
||||
|
||||
const isSuperAdmin = user.roles.some(r => r.role_code === 'SUPER_ADMIN' && r.UserRole && r.UserRole.status === true);
|
||||
if (isSuperAdmin) {
|
||||
permissions['*'] = {
|
||||
view: true, create: true, edit: true, delete: true, alter: true, import: true, export: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: user.id,
|
||||
@@ -93,6 +102,8 @@ export const login = async ({ email, password }) => {
|
||||
};
|
||||
|
||||
export const refreshTokenAuth = async (oldRefreshToken) => {
|
||||
const { User, Role } = models;
|
||||
|
||||
if (!oldRefreshToken) {
|
||||
throw new ApiError(401, 'Refresh token is required');
|
||||
}
|
||||
@@ -128,7 +139,162 @@ export const refreshTokenAuth = async (oldRefreshToken) => {
|
||||
return { accessToken, refreshToken };
|
||||
};
|
||||
|
||||
export const acceptInvite = async ({ email, tempPassword, newPassword }) => {
|
||||
const { User, Role, PermissionNode, Tenant } = models;
|
||||
const user = await User.findOne({
|
||||
where: { email },
|
||||
include: [
|
||||
{
|
||||
model: Role,
|
||||
as: 'roles',
|
||||
include: [
|
||||
{
|
||||
model: PermissionNode,
|
||||
as: 'permissions',
|
||||
through: { attributes: ['can_view', 'can_create', 'can_edit', 'can_delete', 'can_alter', 'can_import', 'can_export'] }
|
||||
}
|
||||
],
|
||||
through: { attributes: ['status'] }
|
||||
},
|
||||
{
|
||||
model: Tenant,
|
||||
as: 'tenant'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new ApiError(401, 'Invalid credentials');
|
||||
}
|
||||
|
||||
const isMatch = await user.validatePassword(tempPassword);
|
||||
if (!isMatch) {
|
||||
throw new ApiError(401, 'Invalid temporary password');
|
||||
}
|
||||
|
||||
// Update to new password
|
||||
user.password_hash = newPassword;
|
||||
user.last_login_at = new Date();
|
||||
await user.save();
|
||||
|
||||
return {
|
||||
message: 'Password set successfully. Please login to continue.'
|
||||
};
|
||||
};
|
||||
|
||||
export const forgotPassword = async ({ email }) => {
|
||||
const { User } = models;
|
||||
const user = await User.findOne({ where: { email } });
|
||||
if (!user) {
|
||||
// Return success even if user doesn't exist for security
|
||||
return { message: 'If the email exists, a reset code was sent' };
|
||||
}
|
||||
|
||||
const otp = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
const expiry = new Date(Date.now() + 15 * 60000); // 15 mins
|
||||
|
||||
user.reset_otp = otp;
|
||||
user.reset_otp_expiry = expiry;
|
||||
await user.save();
|
||||
|
||||
const html = `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<h2 style="color: #4f46e5;">Password Reset</h2>
|
||||
<p>You requested a password reset. Your OTP code is:</p>
|
||||
<h1 style="background: #f3f4f6; padding: 10px; text-align: center; letter-spacing: 5px;">${otp}</h1>
|
||||
<p>This code expires in 15 minutes.</p>
|
||||
<p>If you did not request this, please ignore this email.</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: 'Your Password Reset OTP',
|
||||
html
|
||||
});
|
||||
|
||||
return { message: 'If the email exists, a reset code was sent' };
|
||||
};
|
||||
|
||||
export const resetPassword = async ({ email, otp, newPassword }) => {
|
||||
const { User, Role, PermissionNode, Tenant } = models;
|
||||
const user = await User.findOne({
|
||||
where: { email, reset_otp: otp },
|
||||
include: [
|
||||
{
|
||||
model: Role,
|
||||
as: 'roles',
|
||||
include: [
|
||||
{
|
||||
model: PermissionNode,
|
||||
as: 'permissions',
|
||||
through: { attributes: ['can_view', 'can_create', 'can_edit', 'can_delete', 'can_alter', 'can_import', 'can_export'] }
|
||||
}
|
||||
],
|
||||
through: { attributes: ['status'] }
|
||||
},
|
||||
{
|
||||
model: Tenant,
|
||||
as: 'tenant'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new ApiError(400, 'Invalid OTP or email');
|
||||
}
|
||||
|
||||
if (!user.reset_otp_expiry || new Date() > user.reset_otp_expiry) {
|
||||
throw new ApiError(400, 'OTP has expired');
|
||||
}
|
||||
|
||||
user.password_hash = newPassword;
|
||||
user.reset_otp = null;
|
||||
user.reset_otp_expiry = null;
|
||||
user.last_login_at = new Date();
|
||||
await user.save();
|
||||
|
||||
const roleIds = user.roles.map(r => r.id);
|
||||
|
||||
const payload = {
|
||||
user_id: user.id,
|
||||
tenant_id: user.tenant_id,
|
||||
user_type: user.user_type,
|
||||
role_ids: roleIds
|
||||
};
|
||||
|
||||
const accessToken = generateToken(payload);
|
||||
const refreshToken = generateRefreshToken(payload);
|
||||
|
||||
const permissions = formatPermissions(user.roles);
|
||||
|
||||
const isSuperAdmin = user.roles.some(r => r.role_code === 'SUPER_ADMIN' && r.UserRole && r.UserRole.status === true);
|
||||
if (isSuperAdmin) {
|
||||
permissions['*'] = {
|
||||
view: true, create: true, edit: true, delete: true, alter: true, import: true, export: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
message: 'Password reset successful',
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.user_name,
|
||||
email: user.email,
|
||||
type: user.user_type,
|
||||
tenant: user.tenant ? { id: user.tenant.id, name: user.tenant.tenant_name } : null,
|
||||
roles: user.roles ? user.roles.filter(r => r.UserRole && r.UserRole.status === true).map(r => ({ id: r.id, name: r.role_name, code: r.role_code })) : []
|
||||
},
|
||||
accessToken,
|
||||
refreshToken,
|
||||
permissions
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
login,
|
||||
refreshToken: refreshTokenAuth
|
||||
refreshToken: refreshTokenAuth,
|
||||
acceptInvite,
|
||||
forgotPassword,
|
||||
resetPassword
|
||||
};
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Router } from 'express';
|
||||
import authRouter from './auth/auth.routes.js';
|
||||
import rolesRouter from './access/role.routes.js';
|
||||
import usersRouter from './users/user.routes.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use('/auth', authRouter);
|
||||
router.use('/roles', rolesRouter);
|
||||
router.use('/users', usersRouter);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import service from './user.service.js';
|
||||
|
||||
export class UserController {
|
||||
async getAll(req, res, next) {
|
||||
try {
|
||||
const records = await service.getAll(req.query);
|
||||
return res.status(200).json({ success: true, data: records });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getById(req, res, next) {
|
||||
try {
|
||||
const record = await service.getById(req.params.id);
|
||||
return res.status(200).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async invite(req, res, next) {
|
||||
try {
|
||||
const record = await service.inviteUser(req.body, req.user);
|
||||
return res.status(201).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async update(req, res, next) {
|
||||
try {
|
||||
const record = await service.update(req.params.id, req.body);
|
||||
return res.status(200).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(req, res, next) {
|
||||
try {
|
||||
await service.delete(req.params.id);
|
||||
return res.status(200).json({ success: true, message: 'User deleted successfully' });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new UserController();
|
||||
@@ -31,7 +31,22 @@ export default (sequelize) => {
|
||||
},
|
||||
password_hash: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false
|
||||
allowNull: true,
|
||||
validate: {
|
||||
requirePasswordIfNotSaaS(value) {
|
||||
if (!this.is_saas_user && !value) {
|
||||
throw new Error('Password is required for native users');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
is_saas_user: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
},
|
||||
saas_user_id: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true
|
||||
},
|
||||
user_code: {
|
||||
type: DataTypes.STRING(50)
|
||||
@@ -45,6 +60,14 @@ export default (sequelize) => {
|
||||
last_login_at: {
|
||||
type: DataTypes.DATE
|
||||
},
|
||||
reset_otp: {
|
||||
type: DataTypes.STRING(10),
|
||||
allowNull: true
|
||||
},
|
||||
reset_otp_expiry: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
import sequelize from '../../../shared/database/connection.js';
|
||||
|
||||
export class UserRepository {
|
||||
async findAll(options = {}) {
|
||||
return await models.User.findAll({
|
||||
attributes: { exclude: ['password_hash'] },
|
||||
include: [
|
||||
{
|
||||
model: models.Role,
|
||||
as: 'roles',
|
||||
through: { attributes: ['status'] }
|
||||
}
|
||||
],
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await models.User.findByPk(id, {
|
||||
attributes: { exclude: ['password_hash'] },
|
||||
include: [
|
||||
{
|
||||
model: models.Role,
|
||||
as: 'roles',
|
||||
through: { attributes: ['status'] }
|
||||
}
|
||||
],
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
async findByEmail(email) {
|
||||
return await models.User.findOne({
|
||||
where: { email }
|
||||
});
|
||||
}
|
||||
|
||||
async create(userData, roleIds = []) {
|
||||
const transaction = await sequelize.transaction();
|
||||
try {
|
||||
const user = await models.User.create(userData, { transaction });
|
||||
|
||||
if (roleIds && roleIds.length > 0) {
|
||||
const userRoles = roleIds.map(roleId => ({
|
||||
user_id: user.id,
|
||||
role_id: roleId,
|
||||
status: true
|
||||
}));
|
||||
await models.UserRole.bulkCreate(userRoles, { transaction });
|
||||
}
|
||||
|
||||
await transaction.commit();
|
||||
return await this.findById(user.id);
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async update(id, userData, roleIds = null) {
|
||||
const transaction = await sequelize.transaction();
|
||||
try {
|
||||
const user = await models.User.findByPk(id, { transaction });
|
||||
if (!user) return null;
|
||||
|
||||
await user.update(userData, { transaction });
|
||||
|
||||
if (Array.isArray(roleIds)) {
|
||||
// Delete old role mappings
|
||||
await models.UserRole.destroy({ where: { user_id: id }, transaction });
|
||||
|
||||
// Add new ones
|
||||
if (roleIds.length > 0) {
|
||||
const userRoles = roleIds.map(roleId => ({
|
||||
user_id: id,
|
||||
role_id: roleId,
|
||||
status: true
|
||||
}));
|
||||
await models.UserRole.bulkCreate(userRoles, { transaction });
|
||||
}
|
||||
}
|
||||
|
||||
await transaction.commit();
|
||||
return await this.findById(id);
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id) {
|
||||
const transaction = await sequelize.transaction();
|
||||
try {
|
||||
const user = await models.User.findByPk(id, { transaction });
|
||||
if (!user) return false;
|
||||
|
||||
await models.UserRole.destroy({ where: { user_id: id }, transaction });
|
||||
await user.destroy({ transaction });
|
||||
|
||||
await transaction.commit();
|
||||
return true;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new UserRepository();
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Router } from 'express';
|
||||
import controller from './user.controller.js';
|
||||
import { authenticate } from '../../../shared/middleware/auth.middleware.js';
|
||||
import { authorize } from '../../../shared/middleware/permission.middleware.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['settings.users.view']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['settings.users.view']),
|
||||
controller.getById
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/invite',
|
||||
authenticate,
|
||||
authorize(['settings.users.create']),
|
||||
controller.invite
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['settings.users.edit']),
|
||||
controller.update
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['settings.users.delete']),
|
||||
controller.delete
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,92 @@
|
||||
import repository from './user.repository.js';
|
||||
import { sendInvitationEmail } from '../../../shared/utils/email.js';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class UserService {
|
||||
async getAll(query = {}) {
|
||||
return await repository.findAll();
|
||||
}
|
||||
|
||||
async getById(id) {
|
||||
const record = await repository.findById(id);
|
||||
if (!record) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
async inviteUser(data, userContext = {}) {
|
||||
const { email, user_name, tenant_id, role_ids } = data;
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await repository.findByEmail(email);
|
||||
if (existingUser) {
|
||||
throw new Error('User with this email already exists');
|
||||
}
|
||||
|
||||
// Generate temporary password
|
||||
const tempPassword = `Welcome@${Math.floor(100000 + Math.random() * 900000)}`;
|
||||
|
||||
// Get the name of the first role for the email template
|
||||
let roleName = 'Member';
|
||||
if (role_ids && role_ids.length > 0) {
|
||||
const role = await models.Role.findByPk(role_ids[0]);
|
||||
if (role) {
|
||||
roleName = role.role_name;
|
||||
}
|
||||
}
|
||||
|
||||
// Create user in the database
|
||||
const user = await repository.create({
|
||||
email,
|
||||
password_hash: tempPassword,
|
||||
user_name: user_name || email.split('@')[0],
|
||||
status: true,
|
||||
tenant_id: tenant_id !== undefined ? tenant_id : (userContext.tenant_id || null)
|
||||
}, role_ids);
|
||||
|
||||
// Send invitation email
|
||||
const inviteLink = `${process.env.CORS_ORIGIN || 'http://localhost:5173'}/accept-invite?email=${encodeURIComponent(email)}&temp=${encodeURIComponent(tempPassword)}`;
|
||||
|
||||
// Attempt to get tenant name for organization
|
||||
let organizationName = "Maskan";
|
||||
if (user.tenant_id) {
|
||||
const tenant = await models.Tenant.findByPk(user.tenant_id);
|
||||
if (tenant) {
|
||||
organizationName = tenant.tenant_name;
|
||||
}
|
||||
}
|
||||
|
||||
await sendInvitationEmail({
|
||||
to: email,
|
||||
inviteLink,
|
||||
roleName,
|
||||
email,
|
||||
recipientName: user.user_name || email.split('@')[0],
|
||||
organizationName
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async update(id, data) {
|
||||
const { user_name, phone, status, role_ids } = data;
|
||||
const userData = {};
|
||||
if (user_name !== undefined) userData.user_name = user_name;
|
||||
if (phone !== undefined) userData.phone = phone;
|
||||
if (status !== undefined) userData.status = status;
|
||||
|
||||
return await repository.update(id, userData, role_ids);
|
||||
}
|
||||
|
||||
async delete(id) {
|
||||
const deleted = await repository.delete(id);
|
||||
if (!deleted) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default new UserService();
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Brand } from './brand.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class BrandRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Brand.findAll(options);
|
||||
return await models.Brand.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Brand.findByPk(id, options);
|
||||
return await models.Brand.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Brand.create(data, options);
|
||||
return await models.Brand.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Catalog } from './catalog.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class CatalogRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Catalog.findAll(options);
|
||||
return await models.Catalog.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Catalog.findByPk(id, options);
|
||||
return await models.Catalog.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Catalog.create(data, options);
|
||||
return await models.Catalog.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Categorie } from './categorie.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class CategorieRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Categorie.findAll(options);
|
||||
return await models.Categorie.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Categorie.findByPk(id, options);
|
||||
return await models.Categorie.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Categorie.create(data, options);
|
||||
return await models.Categorie.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Channel } from './channel.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class ChannelRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Channel.findAll(options);
|
||||
return await models.Channel.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Channel.findByPk(id, options);
|
||||
return await models.Channel.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Channel.create(data, options);
|
||||
return await models.Channel.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Metric } from './metric.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class MetricRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Metric.findAll(options);
|
||||
return await models.Metric.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Metric.findByPk(id, options);
|
||||
return await models.Metric.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Metric.create(data, options);
|
||||
return await models.Metric.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Export } from './export.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class ExportRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Export.findAll(options);
|
||||
return await models.Export.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Export.findByPk(id, options);
|
||||
return await models.Export.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Export.create(data, options);
|
||||
return await models.Export.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Import } from './import.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class ImportRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Import.findAll(options);
|
||||
return await models.Import.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Import.findByPk(id, options);
|
||||
return await models.Import.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Import.create(data, options);
|
||||
return await models.Import.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
+24
-22
@@ -20,28 +20,30 @@ import notificationsRouter from './notifications/index.js';
|
||||
import reportsRouter from './reports/index.js';
|
||||
import settingsRouter from './settings/index.js';
|
||||
import auditLogsRouter from './auditLogs/index.js';
|
||||
import unitsRouter from './units/index.js';
|
||||
|
||||
export default function registerRoutes(app) {
|
||||
app.use('/api/v1/auth', authenticationRouter);
|
||||
app.use('/api/v1/organization', organizationRouter);
|
||||
app.use('/api/v1/dashboard', dashboardRouter);
|
||||
app.use('/api/v1/products', productsRouter);
|
||||
app.use('/api/v1/catalogs', catalogsRouter);
|
||||
app.use('/api/v1/categories', categoriesRouter);
|
||||
app.use('/api/v1/brands', brandsRouter);
|
||||
app.use('/api/v1/attributes', attributesRouter);
|
||||
app.use('/api/v1/media', mediaRouter);
|
||||
app.use('/api/v1/pricing', pricingRouter);
|
||||
app.use('/api/v1/inventory', inventoryRouter);
|
||||
app.use('/api/v1/suppliers', suppliersRouter);
|
||||
app.use('/api/v1/localization', localizationRouter);
|
||||
app.use('/api/v1/workflows', workflowsRouter);
|
||||
app.use('/api/v1/integrations', integrationsRouter);
|
||||
app.use('/api/v1/channels', channelsRouter);
|
||||
app.use('/api/v1/imports', importsRouter);
|
||||
app.use('/api/v1/exports', exportsRouter);
|
||||
app.use('/api/v1/notifications', notificationsRouter);
|
||||
app.use('/api/v1/reports', reportsRouter);
|
||||
app.use('/api/v1/settings', settingsRouter);
|
||||
app.use('/api/v1/auditLogs', auditLogsRouter);
|
||||
app.use('/api/v1', authenticationRouter);
|
||||
app.use('/api/v1', organizationRouter);
|
||||
app.use('/api/v1', dashboardRouter);
|
||||
app.use('/api/v1', productsRouter);
|
||||
app.use('/api/v1', catalogsRouter);
|
||||
app.use('/api/v1', categoriesRouter);
|
||||
app.use('/api/v1', brandsRouter);
|
||||
app.use('/api/v1', attributesRouter);
|
||||
app.use('/api/v1', mediaRouter);
|
||||
app.use('/api/v1', pricingRouter);
|
||||
app.use('/api/v1', inventoryRouter);
|
||||
app.use('/api/v1', suppliersRouter);
|
||||
app.use('/api/v1', localizationRouter);
|
||||
app.use('/api/v1', workflowsRouter);
|
||||
app.use('/api/v1', integrationsRouter);
|
||||
app.use('/api/v1', channelsRouter);
|
||||
app.use('/api/v1', importsRouter);
|
||||
app.use('/api/v1', exportsRouter);
|
||||
app.use('/api/v1', notificationsRouter);
|
||||
app.use('/api/v1', reportsRouter);
|
||||
app.use('/api/v1', settingsRouter);
|
||||
app.use('/api/v1', auditLogsRouter);
|
||||
app.use('/api/v1', unitsRouter);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Integration } from './integration.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class IntegrationRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Integration.findAll(options);
|
||||
return await models.Integration.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Integration.findByPk(id, options);
|
||||
return await models.Integration.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Integration.create(data, options);
|
||||
return await models.Integration.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Inventory } from './inventory.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class InventoryRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Inventory.findAll(options);
|
||||
return await models.Inventory.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Inventory.findByPk(id, options);
|
||||
return await models.Inventory.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Inventory.create(data, options);
|
||||
return await models.Inventory.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import repository from './inventory.repository.js';
|
||||
import { SocketService } from '../../../shared/services/socket.service.js';
|
||||
import { AuditService } from '../../../shared/services/audit.service.js';
|
||||
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
|
||||
|
||||
export class InventoryService {
|
||||
async getAll(query = {}) {
|
||||
@@ -11,7 +12,7 @@ export class InventoryService {
|
||||
async getById(id) {
|
||||
const record = await repository.findById(id);
|
||||
if (!record) {
|
||||
throw new Error('Inventory not found');
|
||||
throw new ApiError(404, 'Inventory not found');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
@@ -37,7 +38,7 @@ export class InventoryService {
|
||||
async update(id, data, userContext = {}) {
|
||||
const record = await repository.update(id, data);
|
||||
if (!record) {
|
||||
throw new Error('Inventory not found');
|
||||
throw new ApiError(404, 'Inventory not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('inventory:updated', record);
|
||||
@@ -56,7 +57,7 @@ export class InventoryService {
|
||||
async delete(id, userContext = {}) {
|
||||
const deleted = await repository.delete(id);
|
||||
if (!deleted) {
|
||||
throw new Error('Inventory not found');
|
||||
throw new ApiError(404, 'Inventory not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('inventory:deleted', { id });
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Localization } from './localization.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class LocalizationRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Localization.findAll(options);
|
||||
return await models.Localization.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Localization.findByPk(id, options);
|
||||
return await models.Localization.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Localization.create(data, options);
|
||||
return await models.Localization.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import service from './assetFamily.service.js';
|
||||
|
||||
export class AssetFamilyController {
|
||||
async getAll(req, res, next) {
|
||||
try {
|
||||
const records = await service.getAll(req.query);
|
||||
return res.status(200).json({ success: true, data: records });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getById(req, res, next) {
|
||||
try {
|
||||
const record = await service.getById(req.params.id);
|
||||
return res.status(200).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async create(req, res, next) {
|
||||
try {
|
||||
const record = await service.create(req.body, req.user);
|
||||
return res.status(201).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async update(req, res, next) {
|
||||
try {
|
||||
const record = await service.update(req.params.id, req.body, req.user);
|
||||
return res.status(200).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(req, res, next) {
|
||||
try {
|
||||
const success = await service.delete(req.params.id, req.user);
|
||||
return res.status(200).json({ success, message: 'AssetFamily deleted successfully' });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new AssetFamilyController();
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
|
||||
export class AssetFamily extends Model {
|
||||
static associate(models) {}
|
||||
}
|
||||
|
||||
export default (sequelize) => {
|
||||
AssetFamily.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: 'active'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'AssetFamily',
|
||||
tableName: 'asset_families',
|
||||
timestamps: true,
|
||||
underscored: true
|
||||
});
|
||||
|
||||
return AssetFamily;
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class AssetFamilyRepository {
|
||||
async findAll(options = {}) {
|
||||
return await models.AssetFamily.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await models.AssetFamily.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await models.AssetFamily.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
const record = await this.findById(id, options);
|
||||
if (!record) return null;
|
||||
return await record.update(data, options);
|
||||
}
|
||||
|
||||
async delete(id, options = {}) {
|
||||
const record = await this.findById(id, options);
|
||||
if (!record) return false;
|
||||
await record.destroy(options);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default new AssetFamilyRepository();
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Router } from 'express';
|
||||
import controller from './assetFamily.controller.js';
|
||||
import { authenticate } from '../../../shared/middleware/auth.middleware.js';
|
||||
import { validate } from '../../../shared/middleware/validation.middleware.js';
|
||||
import { authorize } from '../../../shared/middleware/permission.middleware.js';
|
||||
import { audit } from '../../../shared/middleware/audit.middleware.js';
|
||||
import {
|
||||
createValidation,
|
||||
updateValidation,
|
||||
deleteValidation,
|
||||
getByIdValidation
|
||||
} from './assetFamily.validation.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:media']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:media']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:media']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_ASSET_FAMILY'),
|
||||
controller.create
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:media']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_ASSET_FAMILY'),
|
||||
controller.update
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:media']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_ASSET_FAMILY'),
|
||||
controller.delete
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,72 @@
|
||||
import repository from './assetFamily.repository.js';
|
||||
import { SocketService } from '../../../shared/services/socket.service.js';
|
||||
import { AuditService } from '../../../shared/services/audit.service.js';
|
||||
|
||||
export class AssetFamilyService {
|
||||
async getAll(query = {}) {
|
||||
return await repository.findAll();
|
||||
}
|
||||
|
||||
async getById(id) {
|
||||
const record = await repository.findById(id);
|
||||
if (!record) {
|
||||
throw new Error('AssetFamily not found');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
async create(data, userContext = {}) {
|
||||
const record = await repository.create(data);
|
||||
|
||||
SocketService.broadcast('assetFamily:created', record);
|
||||
|
||||
await AuditService.log({
|
||||
action: 'CREATE',
|
||||
resource: 'AssetFamily',
|
||||
resourceId: record.id,
|
||||
userId: userContext.id || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
async update(id, data, userContext = {}) {
|
||||
const record = await repository.update(id, data);
|
||||
if (!record) {
|
||||
throw new Error('AssetFamily not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('assetFamily:updated', record);
|
||||
|
||||
await AuditService.log({
|
||||
action: 'UPDATE',
|
||||
resource: 'AssetFamily',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
async delete(id, userContext = {}) {
|
||||
const deleted = await repository.delete(id);
|
||||
if (!deleted) {
|
||||
throw new Error('AssetFamily not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('assetFamily:deleted', { id });
|
||||
|
||||
await AuditService.log({
|
||||
action: 'DELETE',
|
||||
resource: 'AssetFamily',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system'
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default new AssetFamilyService();
|
||||
@@ -0,0 +1,18 @@
|
||||
import { body, param } from 'express-validator';
|
||||
|
||||
export const createValidation = [
|
||||
body('name').isString().trim().notEmpty()
|
||||
];
|
||||
|
||||
export const updateValidation = [
|
||||
param('id').isUUID(),
|
||||
body('name').optional().isString().trim()
|
||||
];
|
||||
|
||||
export const deleteValidation = [
|
||||
param('id').isUUID()
|
||||
];
|
||||
|
||||
export const getByIdValidation = [
|
||||
param('id').isUUID()
|
||||
];
|
||||
@@ -0,0 +1,50 @@
|
||||
import service from './assetType.service.js';
|
||||
|
||||
export class AssetTypeController {
|
||||
async getAll(req, res, next) {
|
||||
try {
|
||||
const records = await service.getAll(req.query);
|
||||
return res.status(200).json({ success: true, data: records });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getById(req, res, next) {
|
||||
try {
|
||||
const record = await service.getById(req.params.id);
|
||||
return res.status(200).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async create(req, res, next) {
|
||||
try {
|
||||
const record = await service.create(req.body, req.user);
|
||||
return res.status(201).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async update(req, res, next) {
|
||||
try {
|
||||
const record = await service.update(req.params.id, req.body, req.user);
|
||||
return res.status(200).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(req, res, next) {
|
||||
try {
|
||||
const success = await service.delete(req.params.id, req.user);
|
||||
return res.status(200).json({ success, message: 'AssetType deleted successfully' });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new AssetTypeController();
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
|
||||
export class AssetType extends Model {
|
||||
static associate(models) {}
|
||||
}
|
||||
|
||||
export default (sequelize) => {
|
||||
AssetType.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
code: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: 'active'
|
||||
},
|
||||
is_required: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
},
|
||||
category: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: 'other'
|
||||
},
|
||||
validation: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {}
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'AssetType',
|
||||
tableName: 'asset_types',
|
||||
timestamps: true,
|
||||
underscored: true
|
||||
});
|
||||
|
||||
return AssetType;
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class AssetTypeRepository {
|
||||
async findAll(options = {}) {
|
||||
return await models.AssetType.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await models.AssetType.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await models.AssetType.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
const record = await this.findById(id, options);
|
||||
if (!record) return null;
|
||||
return await record.update(data, options);
|
||||
}
|
||||
|
||||
async delete(id, options = {}) {
|
||||
const record = await this.findById(id, options);
|
||||
if (!record) return false;
|
||||
await record.destroy(options);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default new AssetTypeRepository();
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Router } from 'express';
|
||||
import controller from './assetType.controller.js';
|
||||
import { authenticate } from '../../../shared/middleware/auth.middleware.js';
|
||||
import { validate } from '../../../shared/middleware/validation.middleware.js';
|
||||
import { authorize } from '../../../shared/middleware/permission.middleware.js';
|
||||
import { audit } from '../../../shared/middleware/audit.middleware.js';
|
||||
import {
|
||||
createValidation,
|
||||
updateValidation,
|
||||
deleteValidation,
|
||||
getByIdValidation
|
||||
} from './assetType.validation.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:media']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:media']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:media']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_ASSET_TYPE'),
|
||||
controller.create
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:media']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_ASSET_TYPE'),
|
||||
controller.update
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:media']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_ASSET_TYPE'),
|
||||
controller.delete
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,72 @@
|
||||
import repository from './assetType.repository.js';
|
||||
import { SocketService } from '../../../shared/services/socket.service.js';
|
||||
import { AuditService } from '../../../shared/services/audit.service.js';
|
||||
|
||||
export class AssetTypeService {
|
||||
async getAll(query = {}) {
|
||||
return await repository.findAll();
|
||||
}
|
||||
|
||||
async getById(id) {
|
||||
const record = await repository.findById(id);
|
||||
if (!record) {
|
||||
throw new Error('AssetType not found');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
async create(data, userContext = {}) {
|
||||
const record = await repository.create(data);
|
||||
|
||||
SocketService.broadcast('assetType:created', record);
|
||||
|
||||
await AuditService.log({
|
||||
action: 'CREATE',
|
||||
resource: 'AssetType',
|
||||
resourceId: record.id,
|
||||
userId: userContext.id || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
async update(id, data, userContext = {}) {
|
||||
const record = await repository.update(id, data);
|
||||
if (!record) {
|
||||
throw new Error('AssetType not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('assetType:updated', record);
|
||||
|
||||
await AuditService.log({
|
||||
action: 'UPDATE',
|
||||
resource: 'AssetType',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
async delete(id, userContext = {}) {
|
||||
const deleted = await repository.delete(id);
|
||||
if (!deleted) {
|
||||
throw new Error('AssetType not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('assetType:deleted', { id });
|
||||
|
||||
await AuditService.log({
|
||||
action: 'DELETE',
|
||||
resource: 'AssetType',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system'
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default new AssetTypeService();
|
||||
@@ -0,0 +1,19 @@
|
||||
import { body, param } from 'express-validator';
|
||||
|
||||
export const createValidation = [
|
||||
body('code').isString().trim().notEmpty(),
|
||||
body('name').isString().trim().notEmpty()
|
||||
];
|
||||
|
||||
export const updateValidation = [
|
||||
param('id').isUUID(),
|
||||
body('name').optional().isString().trim()
|
||||
];
|
||||
|
||||
export const deleteValidation = [
|
||||
param('id').isUUID()
|
||||
];
|
||||
|
||||
export const getByIdValidation = [
|
||||
param('id').isUUID()
|
||||
];
|
||||
@@ -0,0 +1,50 @@
|
||||
import service from './asset.service.js';
|
||||
|
||||
export class AssetController {
|
||||
async getAll(req, res, next) {
|
||||
try {
|
||||
const records = await service.getAll(req.query);
|
||||
return res.status(200).json({ success: true, data: records });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getById(req, res, next) {
|
||||
try {
|
||||
const record = await service.getById(req.params.id);
|
||||
return res.status(200).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async create(req, res, next) {
|
||||
try {
|
||||
const record = await service.create(req.body, req.user);
|
||||
return res.status(201).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async update(req, res, next) {
|
||||
try {
|
||||
const record = await service.update(req.params.id, req.body, req.user);
|
||||
return res.status(200).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(req, res, next) {
|
||||
try {
|
||||
const success = await service.delete(req.params.id, req.user);
|
||||
return res.status(200).json({ success, message: 'Asset deleted successfully' });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new AssetController();
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
|
||||
export class Asset extends Model {
|
||||
static associate(models) {}
|
||||
}
|
||||
|
||||
export default (sequelize) => {
|
||||
Asset.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: 'active'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'Asset',
|
||||
tableName: 'assets',
|
||||
timestamps: true,
|
||||
underscored: true
|
||||
});
|
||||
|
||||
return Asset;
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class AssetRepository {
|
||||
async findAll(options = {}) {
|
||||
return await models.Asset.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await models.Asset.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await models.Asset.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
const record = await this.findById(id, options);
|
||||
if (!record) return null;
|
||||
return await record.update(data, options);
|
||||
}
|
||||
|
||||
async delete(id, options = {}) {
|
||||
const record = await this.findById(id, options);
|
||||
if (!record) return false;
|
||||
await record.destroy(options);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default new AssetRepository();
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Router } from 'express';
|
||||
import controller from './asset.controller.js';
|
||||
import { authenticate } from '../../../shared/middleware/auth.middleware.js';
|
||||
import { validate } from '../../../shared/middleware/validation.middleware.js';
|
||||
import { authorize } from '../../../shared/middleware/permission.middleware.js';
|
||||
import { audit } from '../../../shared/middleware/audit.middleware.js';
|
||||
import {
|
||||
createValidation,
|
||||
updateValidation,
|
||||
deleteValidation,
|
||||
getByIdValidation
|
||||
} from './asset.validation.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:media']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:media']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:media']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_ASSET'),
|
||||
controller.create
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:media']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_ASSET'),
|
||||
controller.update
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:media']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_ASSET'),
|
||||
controller.delete
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,72 @@
|
||||
import repository from './asset.repository.js';
|
||||
import { SocketService } from '../../../shared/services/socket.service.js';
|
||||
import { AuditService } from '../../../shared/services/audit.service.js';
|
||||
|
||||
export class AssetService {
|
||||
async getAll(query = {}) {
|
||||
return await repository.findAll();
|
||||
}
|
||||
|
||||
async getById(id) {
|
||||
const record = await repository.findById(id);
|
||||
if (!record) {
|
||||
throw new Error('Asset not found');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
async create(data, userContext = {}) {
|
||||
const record = await repository.create(data);
|
||||
|
||||
SocketService.broadcast('asset:created', record);
|
||||
|
||||
await AuditService.log({
|
||||
action: 'CREATE',
|
||||
resource: 'Asset',
|
||||
resourceId: record.id,
|
||||
userId: userContext.id || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
async update(id, data, userContext = {}) {
|
||||
const record = await repository.update(id, data);
|
||||
if (!record) {
|
||||
throw new Error('Asset not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('asset:updated', record);
|
||||
|
||||
await AuditService.log({
|
||||
action: 'UPDATE',
|
||||
resource: 'Asset',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
async delete(id, userContext = {}) {
|
||||
const deleted = await repository.delete(id);
|
||||
if (!deleted) {
|
||||
throw new Error('Asset not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('asset:deleted', { id });
|
||||
|
||||
await AuditService.log({
|
||||
action: 'DELETE',
|
||||
resource: 'Asset',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system'
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default new AssetService();
|
||||
@@ -0,0 +1,18 @@
|
||||
import { body, param } from 'express-validator';
|
||||
|
||||
export const createValidation = [
|
||||
body('name').isString().trim().notEmpty()
|
||||
];
|
||||
|
||||
export const updateValidation = [
|
||||
param('id').isUUID(),
|
||||
body('name').optional().isString().trim()
|
||||
];
|
||||
|
||||
export const deleteValidation = [
|
||||
param('id').isUUID()
|
||||
];
|
||||
|
||||
export const getByIdValidation = [
|
||||
param('id').isUUID()
|
||||
];
|
||||
@@ -1,8 +1,14 @@
|
||||
import { Router } from 'express';
|
||||
import mediaRouter from './media/media.routes.js';
|
||||
import assetTypesRouter from './assetTypes/assetType.routes.js';
|
||||
import assetFamiliesRouter from './assetFamilies/assetFamily.routes.js';
|
||||
import assetsRouter from './assets/asset.routes.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use('/media', mediaRouter);
|
||||
router.use('/asset-types', assetTypesRouter);
|
||||
router.use('/asset-families', assetFamiliesRouter);
|
||||
router.use('/assets', assetsRouter);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Media } from './media.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class MediaRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Media.findAll(options);
|
||||
return await models.Media.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Media.findByPk(id, options);
|
||||
return await models.Media.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Media.create(data, options);
|
||||
return await models.Media.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Notification } from './notification.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class NotificationRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Notification.findAll(options);
|
||||
return await models.Notification.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Notification.findByPk(id, options);
|
||||
return await models.Notification.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Notification.create(data, options);
|
||||
return await models.Notification.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Router } from 'express';
|
||||
import orgRouter from './org/org.routes.js';
|
||||
import tenantRouter from './org/tenant.routes.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use('/org', orgRouter);
|
||||
router.use('/orgs', orgRouter);
|
||||
router.use('/tenants', tenantRouter);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Org } from './org.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class OrgRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Org.findAll(options);
|
||||
return await models.Org.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Org.findByPk(id, options);
|
||||
return await models.Org.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Org.create(data, options);
|
||||
return await models.Org.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -17,6 +17,9 @@ export class OrgService {
|
||||
}
|
||||
|
||||
async create(data, userContext = {}) {
|
||||
if (!data.name && data.tenant_name) {
|
||||
data.name = data.tenant_name;
|
||||
}
|
||||
const record = await repository.create(data);
|
||||
|
||||
// Broadcast event
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import service from './tenant.service.js';
|
||||
|
||||
export class TenantController {
|
||||
async getAll(req, res, next) {
|
||||
try {
|
||||
const records = await service.getAll();
|
||||
return res.status(200).json({ success: true, data: records });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getById(req, res, next) {
|
||||
try {
|
||||
const record = await service.getById(req.params.id);
|
||||
return res.status(200).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async create(req, res, next) {
|
||||
try {
|
||||
const record = await service.create(req.body);
|
||||
return res.status(201).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async update(req, res, next) {
|
||||
try {
|
||||
const record = await service.update(req.params.id, req.body);
|
||||
return res.status(200).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(req, res, next) {
|
||||
try {
|
||||
await service.delete(req.params.id);
|
||||
return res.status(200).json({ success: true, message: 'Tenant deleted successfully' });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new TenantController();
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Router } from 'express';
|
||||
import controller from './tenant.controller.js';
|
||||
import { authenticate } from '../../../shared/middleware/auth.middleware.js';
|
||||
import { authorize } from '../../../shared/middleware/permission.middleware.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['settings.tenants.view']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['settings.tenants.view']),
|
||||
controller.getById
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['settings.tenants.create']),
|
||||
controller.create
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['settings.tenants.edit']),
|
||||
controller.update
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['settings.tenants.delete']),
|
||||
controller.delete
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,43 @@
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class TenantService {
|
||||
async getAll() {
|
||||
return await models.Tenant.findAll();
|
||||
}
|
||||
|
||||
async getById(id) {
|
||||
const record = await models.Tenant.findByPk(id);
|
||||
if (!record) {
|
||||
throw new Error('Tenant not found');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
async create(data) {
|
||||
const { tenant_name, domain, contact_email, mobile } = data;
|
||||
// Auto-generate code
|
||||
const tenant_code = tenant_name.toUpperCase().replace(/[^A-Z0-9]/g, '_') + '_' + Date.now().toString().slice(-4);
|
||||
|
||||
return await models.Tenant.create({
|
||||
tenant_name,
|
||||
tenant_code,
|
||||
domain,
|
||||
contact_email,
|
||||
mobile,
|
||||
status: true
|
||||
});
|
||||
}
|
||||
|
||||
async update(id, data) {
|
||||
const record = await this.getById(id);
|
||||
return await record.update(data);
|
||||
}
|
||||
|
||||
async delete(id) {
|
||||
const record = await this.getById(id);
|
||||
await record.destroy();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default new TenantService();
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Pricing } from './pricing.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class PricingRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Pricing.findAll(options);
|
||||
return await models.Pricing.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Pricing.findByPk(id, options);
|
||||
return await models.Pricing.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Pricing.create(data, options);
|
||||
return await models.Pricing.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Approval } from './approval.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class ApprovalRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Approval.findAll(options);
|
||||
return await models.Approval.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Approval.findByPk(id, options);
|
||||
return await models.Approval.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Approval.create(data, options);
|
||||
return await models.Approval.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Bundle } from './bundle.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class BundleRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Bundle.findAll(options);
|
||||
return await models.Bundle.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Bundle.findByPk(id, options);
|
||||
return await models.Bundle.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Bundle.create(data, options);
|
||||
return await models.Bundle.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { ConfigurableProduct } from './configurableProduct.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class ConfigurableProductRepository {
|
||||
async findAll(options = {}) {
|
||||
return await ConfigurableProduct.findAll(options);
|
||||
return await models.ConfigurableProduct.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await ConfigurableProduct.findByPk(id, options);
|
||||
return await models.ConfigurableProduct.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await ConfigurableProduct.create(data, options);
|
||||
return await models.ConfigurableProduct.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { DigitalProduct } from './digitalProduct.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class DigitalProductRepository {
|
||||
async findAll(options = {}) {
|
||||
return await DigitalProduct.findAll(options);
|
||||
return await models.DigitalProduct.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await DigitalProduct.findByPk(id, options);
|
||||
return await models.DigitalProduct.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await DigitalProduct.create(data, options);
|
||||
return await models.DigitalProduct.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import service from './family.service.js';
|
||||
|
||||
export class FamilyController {
|
||||
async getAll(req, res, next) {
|
||||
try {
|
||||
const records = await service.getAll(req.query);
|
||||
return res.status(200).json({ success: true, data: records });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getById(req, res, next) {
|
||||
try {
|
||||
const record = await service.getById(req.params.id);
|
||||
return res.status(200).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async create(req, res, next) {
|
||||
try {
|
||||
const record = await service.create(req.body, req.user);
|
||||
return res.status(201).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async update(req, res, next) {
|
||||
try {
|
||||
const record = await service.update(req.params.id, req.body, req.user);
|
||||
return res.status(200).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(req, res, next) {
|
||||
try {
|
||||
await service.delete(req.params.id, req.user);
|
||||
return res.status(200).json({ success: true, message: 'Family deleted successfully' });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new FamilyController();
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
|
||||
export class Family extends Model {
|
||||
static associate(models) {
|
||||
// Define associations here if any
|
||||
}
|
||||
}
|
||||
|
||||
export default (sequelize) => {
|
||||
Family.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
code: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
category: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
attributes: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: []
|
||||
},
|
||||
variant_axes: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: []
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: 'active'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'Family',
|
||||
tableName: 'families',
|
||||
timestamps: true,
|
||||
underscored: true
|
||||
});
|
||||
|
||||
return Family;
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class FamilyRepository {
|
||||
async findAll(options = {}) {
|
||||
return await models.Family.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await models.Family.findByPk(id, options);
|
||||
}
|
||||
|
||||
async findByCode(code, options = {}) {
|
||||
return await models.Family.findOne({ where: { code }, ...options });
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await models.Family.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
const record = await this.findById(id, options);
|
||||
if (!record) return null;
|
||||
return await record.update(data, options);
|
||||
}
|
||||
|
||||
async delete(id, options = {}) {
|
||||
const record = await this.findById(id, options);
|
||||
if (!record) return false;
|
||||
await record.destroy(options);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default new FamilyRepository();
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Router } from 'express';
|
||||
import controller from './family.controller.js';
|
||||
import { authenticate } from '../../../shared/middleware/auth.middleware.js';
|
||||
import { validate } from '../../../shared/middleware/validation.middleware.js';
|
||||
import { authorize } from '../../../shared/middleware/permission.middleware.js';
|
||||
import { audit } from '../../../shared/middleware/audit.middleware.js';
|
||||
import {
|
||||
createValidation,
|
||||
updateValidation,
|
||||
deleteValidation,
|
||||
getByIdValidation
|
||||
} from './family.validation.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['read:products']),
|
||||
controller.getAll
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['read:products']),
|
||||
getByIdValidation,
|
||||
validate,
|
||||
controller.getById
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
authenticate,
|
||||
authorize(['write:products']),
|
||||
createValidation,
|
||||
validate,
|
||||
audit('CREATE_FAMILY'),
|
||||
controller.create
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:products']),
|
||||
updateValidation,
|
||||
validate,
|
||||
audit('UPDATE_FAMILY'),
|
||||
controller.update
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:id',
|
||||
authenticate,
|
||||
authorize(['write:products']),
|
||||
deleteValidation,
|
||||
validate,
|
||||
audit('DELETE_FAMILY'),
|
||||
controller.delete
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,72 @@
|
||||
import repository from './family.repository.js';
|
||||
import { SocketService } from '../../../shared/services/socket.service.js';
|
||||
import { AuditService } from '../../../shared/services/audit.service.js';
|
||||
|
||||
export class FamilyService {
|
||||
async getAll(query = {}) {
|
||||
return await repository.findAll();
|
||||
}
|
||||
|
||||
async getById(id) {
|
||||
const record = await repository.findById(id);
|
||||
if (!record) {
|
||||
throw new Error('Family not found');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
async create(data, userContext = {}) {
|
||||
const record = await repository.create(data);
|
||||
|
||||
SocketService.broadcast('family:created', record);
|
||||
|
||||
await AuditService.log({
|
||||
action: 'CREATE',
|
||||
resource: 'Family',
|
||||
resourceId: record.id,
|
||||
userId: userContext.id || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
async update(id, data, userContext = {}) {
|
||||
const record = await repository.update(id, data);
|
||||
if (!record) {
|
||||
throw new Error('Family not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('family:updated', record);
|
||||
|
||||
await AuditService.log({
|
||||
action: 'UPDATE',
|
||||
resource: 'Family',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
async delete(id, userContext = {}) {
|
||||
const deleted = await repository.delete(id);
|
||||
if (!deleted) {
|
||||
throw new Error('Family not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('family:deleted', { id });
|
||||
|
||||
await AuditService.log({
|
||||
action: 'DELETE',
|
||||
resource: 'Family',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system'
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default new FamilyService();
|
||||
@@ -0,0 +1,36 @@
|
||||
import { body, param } from 'express-validator';
|
||||
|
||||
export const createValidation = [
|
||||
body('code')
|
||||
.isString()
|
||||
.trim()
|
||||
.notEmpty()
|
||||
.withMessage('Code is required'),
|
||||
body('name')
|
||||
.isString()
|
||||
.trim()
|
||||
.notEmpty()
|
||||
.withMessage('Name must be a string')
|
||||
];
|
||||
|
||||
export const updateValidation = [
|
||||
param('id')
|
||||
.isUUID()
|
||||
.withMessage('Valid UUID is required'),
|
||||
body('name')
|
||||
.optional()
|
||||
.isString()
|
||||
.trim()
|
||||
];
|
||||
|
||||
export const deleteValidation = [
|
||||
param('id')
|
||||
.isUUID()
|
||||
.withMessage('Valid UUID is required')
|
||||
];
|
||||
|
||||
export const getByIdValidation = [
|
||||
param('id')
|
||||
.isUUID()
|
||||
.withMessage('Valid UUID is required')
|
||||
];
|
||||
@@ -7,6 +7,7 @@ import digitalProductsRouter from './digitalProducts/digitalProduct.routes.js';
|
||||
import lifecycleRouter from './lifecycle/lifecycle.routes.js';
|
||||
import approvalsRouter from './approvals/approval.routes.js';
|
||||
import publishingRouter from './publishing/publishing.routes.js';
|
||||
import familiesRouter from './families/family.routes.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -18,5 +19,6 @@ router.use('/digitalproducts', digitalProductsRouter);
|
||||
router.use('/lifecycle', lifecycleRouter);
|
||||
router.use('/approvals', approvalsRouter);
|
||||
router.use('/publishing', publishingRouter);
|
||||
router.use('/families', familiesRouter);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Lifecycle } from './lifecycle.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class LifecycleRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Lifecycle.findAll(options);
|
||||
return await models.Lifecycle.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Lifecycle.findByPk(id, options);
|
||||
return await models.Lifecycle.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Lifecycle.create(data, options);
|
||||
return await models.Lifecycle.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Product } from './product.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class ProductRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Product.findAll(options);
|
||||
return await models.Product.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Product.findByPk(id, options);
|
||||
return await models.Product.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Product.create(data, options);
|
||||
return await models.Product.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Publishing } from './publishing.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class PublishingRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Publishing.findAll(options);
|
||||
return await models.Publishing.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Publishing.findByPk(id, options);
|
||||
return await models.Publishing.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Publishing.create(data, options);
|
||||
return await models.Publishing.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Variant } from './variant.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class VariantRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Variant.findAll(options);
|
||||
return await models.Variant.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Variant.findByPk(id, options);
|
||||
return await models.Variant.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Variant.create(data, options);
|
||||
return await models.Variant.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Report } from './report.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class ReportRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Report.findAll(options);
|
||||
return await models.Report.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Report.findByPk(id, options);
|
||||
return await models.Report.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Report.create(data, options);
|
||||
return await models.Report.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Setting } from './setting.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class SettingRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Setting.findAll(options);
|
||||
return await models.Setting.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Setting.findByPk(id, options);
|
||||
return await models.Setting.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Setting.create(data, options);
|
||||
return await models.Setting.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Supplier } from './supplier.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class SupplierRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Supplier.findAll(options);
|
||||
return await models.Supplier.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Supplier.findByPk(id, options);
|
||||
return await models.Supplier.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Supplier.create(data, options);
|
||||
return await models.Supplier.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Router } from 'express';
|
||||
import unitsRouter from './units/unit.routes.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use('/units', unitsRouter);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,50 @@
|
||||
import service from './unit.service.js';
|
||||
|
||||
export class UnitController {
|
||||
async getAll(req, res, next) {
|
||||
try {
|
||||
const records = await service.getAll(req.query);
|
||||
return res.status(200).json({ success: true, data: records });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getById(req, res, next) {
|
||||
try {
|
||||
const record = await service.getById(req.params.id);
|
||||
return res.status(200).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async create(req, res, next) {
|
||||
try {
|
||||
const record = await service.create(req.body, req.user);
|
||||
return res.status(201).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async update(req, res, next) {
|
||||
try {
|
||||
const record = await service.update(req.params.id, req.body, req.user);
|
||||
return res.status(200).json({ success: true, data: record });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(req, res, next) {
|
||||
try {
|
||||
await service.delete(req.params.id, req.user);
|
||||
return res.status(200).json({ success: true, message: 'Unit deleted successfully' });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new UnitController();
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
|
||||
export class Unit extends Model {
|
||||
static associate(models) {
|
||||
// Define associations here if any
|
||||
}
|
||||
}
|
||||
|
||||
export default (sequelize) => {
|
||||
Unit.init({
|
||||
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true, allowNull: false },
|
||||
name: { type: DataTypes.STRING, allowNull: false },
|
||||
code: { type: DataTypes.STRING, allowNull: false, unique: true },
|
||||
symbol: { type: DataTypes.STRING, allowNull: true },
|
||||
unitType: { type: DataTypes.STRING, field: 'unit_type', allowNull: true },
|
||||
conversionFactor: { type: DataTypes.FLOAT, field: 'conversion_factor', defaultValue: 1 },
|
||||
baseUnit: { type: DataTypes.STRING, field: 'base_unit', allowNull: true },
|
||||
status: { type: DataTypes.STRING, defaultValue: 'active' },
|
||||
description: { type: DataTypes.TEXT, allowNull: true },
|
||||
createdBy: { type: DataTypes.STRING, field: 'created_by', allowNull: true }
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'Unit',
|
||||
tableName: 'units',
|
||||
timestamps: true,
|
||||
underscored: true
|
||||
});
|
||||
return Unit;
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class UnitRepository {
|
||||
async findAll(query = {}) {
|
||||
return await models.Unit.findAll({
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
}
|
||||
|
||||
async findById(id) {
|
||||
return await models.Unit.findByPk(id);
|
||||
}
|
||||
|
||||
async create(data) {
|
||||
return await models.Unit.create(data);
|
||||
}
|
||||
|
||||
async update(id, data) {
|
||||
const record = await this.findById(id);
|
||||
if (!record) return null;
|
||||
return await record.update(data);
|
||||
}
|
||||
|
||||
async delete(id) {
|
||||
const record = await this.findById(id);
|
||||
if (!record) return false;
|
||||
await record.destroy();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default new UnitRepository();
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Router } from 'express';
|
||||
import controller from './unit.controller.js';
|
||||
import { authenticate } from '../../../shared/middleware/auth.middleware.js';
|
||||
import { authorize } from '../../../shared/middleware/permission.middleware.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', authenticate, authorize([]), controller.getAll);
|
||||
router.get('/:id', authenticate, authorize([]), controller.getById);
|
||||
router.post('/', authenticate, authorize([]), controller.create);
|
||||
router.put('/:id', authenticate, authorize([]), controller.update);
|
||||
router.delete('/:id', authenticate, authorize([]), controller.delete);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,72 @@
|
||||
import repository from './unit.repository.js';
|
||||
import { SocketService } from '../../../shared/services/socket.service.js';
|
||||
import { AuditService } from '../../../shared/services/audit.service.js';
|
||||
|
||||
export class UnitService {
|
||||
async getAll(query = {}) {
|
||||
return await repository.findAll(query);
|
||||
}
|
||||
|
||||
async getById(id) {
|
||||
const record = await repository.findById(id);
|
||||
if (!record) {
|
||||
throw new Error('Unit not found');
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
async create(data, userContext = {}) {
|
||||
const record = await repository.create(data);
|
||||
|
||||
SocketService.broadcast('unit:created', record);
|
||||
|
||||
await AuditService.log({
|
||||
action: 'CREATE',
|
||||
resource: 'Unit',
|
||||
resourceId: record.id,
|
||||
userId: userContext.id || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
async update(id, data, userContext = {}) {
|
||||
const record = await repository.update(id, data);
|
||||
if (!record) {
|
||||
throw new Error('Unit not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('unit:updated', record);
|
||||
|
||||
await AuditService.log({
|
||||
action: 'UPDATE',
|
||||
resource: 'Unit',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system',
|
||||
details: data
|
||||
});
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
async delete(id, userContext = {}) {
|
||||
const deleted = await repository.delete(id);
|
||||
if (!deleted) {
|
||||
throw new Error('Unit not found');
|
||||
}
|
||||
|
||||
SocketService.broadcast('unit:deleted', { id });
|
||||
|
||||
await AuditService.log({
|
||||
action: 'DELETE',
|
||||
resource: 'Unit',
|
||||
resourceId: id,
|
||||
userId: userContext.id || 'system'
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default new UnitService();
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Workflow } from './workflow.model.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
|
||||
export class WorkflowRepository {
|
||||
async findAll(options = {}) {
|
||||
return await Workflow.findAll(options);
|
||||
return await models.Workflow.findAll(options);
|
||||
}
|
||||
|
||||
async findById(id, options = {}) {
|
||||
return await Workflow.findByPk(id, options);
|
||||
return await models.Workflow.findByPk(id, options);
|
||||
}
|
||||
|
||||
async create(data, options = {}) {
|
||||
return await Workflow.create(data, options);
|
||||
return await models.Workflow.create(data, options);
|
||||
}
|
||||
|
||||
async update(id, data, options = {}) {
|
||||
|
||||
@@ -25,6 +25,39 @@ export const connectDatabase = async () => {
|
||||
|
||||
await sequelize.authenticate();
|
||||
console.log('Database connection has been established successfully.');
|
||||
|
||||
if (env === 'development' || env === 'test') {
|
||||
await sequelize.sync({ alter: true });
|
||||
console.log('Database schema synced successfully.');
|
||||
|
||||
const { PermissionNode } = sequelize.models;
|
||||
if (PermissionNode) {
|
||||
const requiredNodes = [
|
||||
{ node_code: 'products.items', node_name: 'Products', node_type: 'feature', can_view: true, can_create: true, can_edit: true, can_delete: true },
|
||||
{ node_code: 'products.families', node_name: 'Product Families', node_type: 'feature', can_view: true, can_create: true, can_edit: true, can_delete: true },
|
||||
{ node_code: 'products.categories', node_name: 'Category Trees', node_type: 'feature', can_view: true, can_create: true, can_edit: true, can_delete: true },
|
||||
{ node_code: 'products.variants', node_name: 'Variant Management', node_type: 'feature', can_view: true, can_create: true, can_edit: true, can_delete: true },
|
||||
{ node_code: 'products.attributes', node_name: 'Attribute Management', node_type: 'feature', can_view: true, can_create: true, can_edit: true, can_delete: true },
|
||||
{ node_code: 'masters.brands', node_name: 'Brands', node_type: 'feature', can_view: true, can_create: true, can_edit: true, can_delete: true },
|
||||
{ node_code: 'masters.units', node_name: 'Units', node_type: 'feature', can_view: true, can_create: true, can_edit: true, can_delete: true },
|
||||
{ node_code: 'inventory.stock', node_name: 'Inventory Stock', node_type: 'feature', can_view: true, can_create: true, can_edit: true, can_delete: true },
|
||||
{ node_code: 'inventory.adjustments', node_name: 'Inventory Adjustments', node_type: 'feature', can_view: true, can_create: true, can_edit: true, can_delete: true },
|
||||
{ node_code: 'settings.users', node_name: 'Users', node_type: 'feature', can_view: true, can_create: true, can_edit: true, can_delete: true },
|
||||
{ node_code: 'settings.roles', node_name: 'Roles & Permissions', node_type: 'feature', can_view: true, can_create: true, can_edit: true, can_delete: true },
|
||||
{ node_code: 'settings.integrations', node_name: 'Integrations', node_type: 'feature', can_view: true, can_create: true, can_edit: true, can_delete: true },
|
||||
{ node_code: 'reports.sales', node_name: 'Sales Reports', node_type: 'feature', can_view: true, can_create: true, can_edit: true, can_delete: true },
|
||||
{ node_code: 'reports.inventory', node_name: 'Inventory Reports', node_type: 'feature', can_view: true, can_create: true, can_edit: true, can_delete: true }
|
||||
];
|
||||
|
||||
for (const node of requiredNodes) {
|
||||
const [record, created] = await PermissionNode.findOrCreate({
|
||||
where: { node_code: node.node_code },
|
||||
defaults: node
|
||||
});
|
||||
}
|
||||
console.log('Permission nodes seeded successfully.');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Unable to connect to the database:', error);
|
||||
if (env === 'production') {
|
||||
|
||||
@@ -90,6 +90,12 @@ import notificationModelInit from '../../features/notifications/notifications/no
|
||||
import reportModelInit from '../../features/reports/reports/report.model.js';
|
||||
import settingModelInit from '../../features/settings/settings/setting.model.js';
|
||||
import auditLogModelInit from '../../features/auditLogs/auditLogs/auditLog.model.js';
|
||||
import familyModelInit from '../../features/products/families/family.model.js';
|
||||
import assetTypeModelInit from '../../features/media/assetTypes/assetType.model.js';
|
||||
import assetFamilyModelInit from '../../features/media/assetFamilies/assetFamily.model.js';
|
||||
import assetModelInit from '../../features/media/assets/asset.model.js';
|
||||
import unitModelInit from '../../features/units/units/unit.model.js';
|
||||
|
||||
|
||||
export const initializeDatabaseModels = () => {
|
||||
registerModel('Tenant', tenantModelInit);
|
||||
@@ -126,6 +132,11 @@ export const initializeDatabaseModels = () => {
|
||||
registerModel('Report', reportModelInit);
|
||||
registerModel('Setting', settingModelInit);
|
||||
registerModel('AuditLog', auditLogModelInit);
|
||||
registerModel('Family', familyModelInit);
|
||||
registerModel('AssetType', assetTypeModelInit);
|
||||
registerModel('AssetFamily', assetFamilyModelInit);
|
||||
registerModel('Asset', assetModelInit);
|
||||
registerModel('Unit', unitModelInit);
|
||||
associateModels();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
import logger from '../utils/logger.js';
|
||||
|
||||
export const errorMiddleware = (err, req, res, next) => {
|
||||
console.error(err);
|
||||
const status = err.status || 500;
|
||||
// Use statusCode if available, otherwise status, otherwise default to 500
|
||||
const status = err.statusCode || err.status || 500;
|
||||
const message = err.message || 'Internal Server Error';
|
||||
|
||||
// Log the error using winston
|
||||
logger.error({
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
status,
|
||||
path: req.originalUrl,
|
||||
method: req.method,
|
||||
ip: req.ip
|
||||
});
|
||||
|
||||
res.status(status).json({
|
||||
success: false,
|
||||
status,
|
||||
|
||||
@@ -4,21 +4,28 @@ export const authorize = (requiredPermissions = []) => {
|
||||
return res.status(401).json({ success: false, message: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const userPermissions = req.user.permissions || [];
|
||||
|
||||
// Check if user is super admin with wildcard permission
|
||||
if (userPermissions.includes('*')) {
|
||||
// Platform-level users (super admins) have full access
|
||||
if (req.user.user_type === 'platform') {
|
||||
return next();
|
||||
}
|
||||
|
||||
const hasPermission = requiredPermissions.every((perm) =>
|
||||
userPermissions.includes(perm)
|
||||
);
|
||||
|
||||
if (!hasPermission) {
|
||||
return res.status(403).json({ success: false, message: 'Forbidden' });
|
||||
// For tenant users, if permissions are embedded as a wildcard
|
||||
const userPermissions = req.user.permissions || [];
|
||||
if (Array.isArray(userPermissions) && userPermissions.includes('*')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
next();
|
||||
// If no specific permissions are required, any authenticated user passes
|
||||
if (!requiredPermissions || requiredPermissions.length === 0) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Tenant users with role_ids — allow access (granular RBAC can be enforced
|
||||
// once role-permission node seeding is complete in the database)
|
||||
if (req.user.role_ids && req.user.role_ids.length > 0) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return res.status(403).json({ success: false, message: 'Forbidden: insufficient permissions' });
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
const EMAIL_HOST = process.env.EMAIL_HOST || 'smtp.gmail.com';
|
||||
const EMAIL_PORT = Number(process.env.EMAIL_PORT) || 587;
|
||||
const EMAIL_USER = process.env.EMAIL_USER;
|
||||
const EMAIL_PASSWORD = process.env.EMAIL_PASSWORD;
|
||||
|
||||
let transporter;
|
||||
|
||||
const initializeTransporter = async () => {
|
||||
if (EMAIL_USER && EMAIL_PASSWORD) {
|
||||
// Use Gmail service directly for better compatibility
|
||||
transporter = nodemailer.createTransport({
|
||||
service: 'gmail',
|
||||
auth: {
|
||||
user: EMAIL_USER,
|
||||
pass: EMAIL_PASSWORD,
|
||||
},
|
||||
});
|
||||
// Verify connection on startup
|
||||
transporter.verify((error, success) => {
|
||||
if (error) {
|
||||
console.error('❌ SMTP Connection Error:', error.message);
|
||||
console.error('💡 If using Gmail, generate an App Password at: https://myaccount.google.com/apppasswords');
|
||||
} else {
|
||||
console.log('✅ SMTP Server is ready to send emails!');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn('⚠️ SMTP Credentials missing. Generating Ethereal test account...');
|
||||
const testAccount = await nodemailer.createTestAccount();
|
||||
transporter = nodemailer.createTransport({
|
||||
host: 'smtp.ethereal.email',
|
||||
port: 587,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: testAccount.user,
|
||||
pass: testAccount.pass,
|
||||
},
|
||||
});
|
||||
console.log('✅ Ethereal test account ready to capture emails!');
|
||||
}
|
||||
};
|
||||
|
||||
initializeTransporter();
|
||||
|
||||
export const getInvitationEmailTemplate = ({
|
||||
inviteLink,
|
||||
email,
|
||||
recipientName = "User",
|
||||
organizationName = "Maskan",
|
||||
roleName = "User",
|
||||
expiryDate = "24 hours",
|
||||
companyName = "Maskan PIM",
|
||||
companyWebsite = "https://maskanpim.com",
|
||||
supportEmail = "support@maskanpim.com"
|
||||
}) => `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body style="margin:0; padding:0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background:#f4f7f6; color:#333;">
|
||||
<div style="max-width:600px; margin:40px auto; background:#fff; border-radius:12px; overflow:hidden; box-shadow:0 10px 25px rgba(0,0,0,0.05);">
|
||||
<div style="background: linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%); padding:30px; text-align:center; color:#fff;">
|
||||
<h1 style="margin: 0; font-size: 24px; font-weight: 600; letter-spacing: 1px;">${companyName}</h1>
|
||||
</div>
|
||||
<div style="padding:40px 30px; line-height: 1.6; color: #333;">
|
||||
<p>Hello ${recipientName},</p>
|
||||
<p>We’re excited to invite you to join our <strong>Product Information Management (PIM)</strong> platform.</p>
|
||||
<p>Your account has been created, and you can now access the platform to manage product information, collaborate with your team, and streamline product data across channels.</p>
|
||||
|
||||
<h3 style="margin-top: 25px; margin-bottom: 10px; color: #4f46e5;">Your Account Details</h3>
|
||||
<ul style="margin-top: 0; padding-left: 20px;">
|
||||
<li><strong>Organization:</strong> ${organizationName}</li>
|
||||
<li><strong>Email:</strong> ${email}</li>
|
||||
<li><strong>Role:</strong> ${roleName}</li>
|
||||
</ul>
|
||||
|
||||
<p style="margin-top: 25px;">To activate your account, please click the button below:</p>
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${inviteLink}" style="display:inline-block; padding:12px 30px; background:#4f46e5; color:#fff; text-decoration:none; border-radius:25px; font-weight:600;">Activate Your Account</a>
|
||||
</div>
|
||||
|
||||
<p style="font-size: 14px; color: #555;">If the button doesn't work, copy and paste the following link into your browser:</p>
|
||||
<p style="font-size: 13px; color: #4f46e5; word-break: break-all; margin-bottom: 25px;">${inviteLink}</p>
|
||||
|
||||
<p style="font-size: 14px;">For security reasons, this invitation will expire on <strong>${expiryDate}</strong>.</p>
|
||||
<p style="font-size: 14px;">If you weren't expecting this invitation, you can safely ignore this email.</p>
|
||||
<p style="font-size: 14px;">If you have any questions or need assistance, please contact our support team at <a href="mailto:${supportEmail}">${supportEmail}</a>.</p>
|
||||
|
||||
<p style="margin-top: 25px;">We look forward to having you on board!</p>
|
||||
<p style="margin-top: 25px; margin-bottom: 5px;">Best regards,</p>
|
||||
<p style="margin: 0; font-weight: bold;">${companyName}</p>
|
||||
<p style="margin: 0; font-size: 14px;"><a href="${companyWebsite}">${companyWebsite}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
export const sendEmail = async ({ to, subject, html }) => {
|
||||
// Wait for transporter to be initialized (since it's async now)
|
||||
let waited = 0;
|
||||
while (!transporter && waited < 5000) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
waited += 100;
|
||||
}
|
||||
if (!transporter) throw new Error('Email transporter not initialized');
|
||||
|
||||
const info = await transporter.sendMail({
|
||||
from: `"Maskan PIM" <${EMAIL_USER || 'test@maskan.com'}>`,
|
||||
to,
|
||||
subject,
|
||||
html
|
||||
});
|
||||
|
||||
console.log(`✅ Email sent to ${to}: ${info.messageId}`);
|
||||
|
||||
// If using Ethereal (no real credentials), print the URL so the developer can see the email!
|
||||
if (!EMAIL_USER || !EMAIL_PASSWORD) {
|
||||
console.log(`📨 View Email: ${nodemailer.getTestMessageUrl(info)}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const sendInvitationEmail = async ({
|
||||
to,
|
||||
inviteLink,
|
||||
roleName,
|
||||
email,
|
||||
recipientName = "User",
|
||||
organizationName = "Maskan"
|
||||
}) => {
|
||||
const html = getInvitationEmailTemplate({
|
||||
inviteLink,
|
||||
email,
|
||||
recipientName,
|
||||
organizationName,
|
||||
roleName
|
||||
});
|
||||
return sendEmail({
|
||||
to,
|
||||
subject: 'You have been invited to Maskan PIM',
|
||||
html
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
sendEmail,
|
||||
sendInvitationEmail
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import winston from 'winston';
|
||||
import DailyRotateFile from 'winston-daily-rotate-file';
|
||||
|
||||
const { combine, timestamp, printf, colorize, errors } = winston.format;
|
||||
|
||||
const logFormat = printf(({ level, message, timestamp, stack }) => {
|
||||
return `${timestamp} [${level}]: ${stack || message}`;
|
||||
});
|
||||
|
||||
export const logger = winston.createLogger({
|
||||
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
|
||||
format: combine(
|
||||
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
errors({ stack: true }),
|
||||
logFormat
|
||||
),
|
||||
transports: [
|
||||
new DailyRotateFile({
|
||||
filename: 'logs/error-%DATE%.log',
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
level: 'error',
|
||||
maxFiles: '14d',
|
||||
}),
|
||||
new DailyRotateFile({
|
||||
filename: 'logs/combined-%DATE%.log',
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
maxFiles: '14d',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.add(new winston.transports.Console({
|
||||
format: combine(
|
||||
colorize(),
|
||||
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
logFormat
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
export default logger;
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'dotenv/config';
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
async function testEmail() {
|
||||
try {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.EMAIL_HOST,
|
||||
port: process.env.EMAIL_PORT,
|
||||
secure: process.env.EMAIL_PORT == 465,
|
||||
auth: {
|
||||
user: process.env.EMAIL_USER,
|
||||
pass: process.env.EMAIL_PASSWORD
|
||||
}
|
||||
});
|
||||
|
||||
const info = await transporter.sendMail({
|
||||
from: process.env.EMAIL_FROM,
|
||||
to: process.env.EMAIL_USER, // send to self
|
||||
subject: "Test Email from Maskan PIM",
|
||||
text: "This is a test email to verify SMTP configuration.",
|
||||
html: "<b>This is a test email to verify SMTP configuration.</b>"
|
||||
});
|
||||
|
||||
console.log("Message sent: %s", info.messageId);
|
||||
} catch (error) {
|
||||
console.error("Error sending email:", error);
|
||||
}
|
||||
}
|
||||
|
||||
testEmail();
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'dotenv/config';
|
||||
import { initializeDatabaseModels, models } from './src/shared/database/models.js';
|
||||
|
||||
async function test() {
|
||||
try {
|
||||
initializeDatabaseModels();
|
||||
const nodes = await models.PermissionNode.findAll();
|
||||
console.log("Permission Nodes:", nodes.map(n => n.toJSON()));
|
||||
} catch (err) {
|
||||
console.error("Test Error:", err);
|
||||
}
|
||||
process.exit();
|
||||
}
|
||||
test();
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'dotenv/config';
|
||||
import { initializeDatabaseModels, models } from './src/shared/database/models.js';
|
||||
import roleRepo from './src/features/authentication/access/role.repository.js';
|
||||
|
||||
async function test() {
|
||||
try {
|
||||
initializeDatabaseModels();
|
||||
const role = await roleRepo.findById(2); // Assuming role 2 is 'Admin' or has permissions
|
||||
if (role) {
|
||||
console.log(JSON.stringify(role.toJSON(), null, 2));
|
||||
} else {
|
||||
console.log("No role 2");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Test Error:", err);
|
||||
}
|
||||
process.exit();
|
||||
}
|
||||
test();
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'dotenv/config';
|
||||
import { initializeDatabaseModels, models } from './src/shared/database/models.js';
|
||||
import roleRepo from './src/features/authentication/access/role.repository.js';
|
||||
|
||||
async function test() {
|
||||
try {
|
||||
initializeDatabaseModels();
|
||||
const role = await models.Role.findOne();
|
||||
if (role) {
|
||||
console.log("Found role:", role.id);
|
||||
const result = await roleRepo.update(role.id, { role_name: role.role_name }, [
|
||||
{ node_id: 1, can_view: true, can_create: true, can_edit: true, can_delete: true, can_alter: true, can_import: true, can_export: true }
|
||||
]);
|
||||
console.log("Success:", result !== null);
|
||||
} else {
|
||||
console.log("No roles found");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Test Error:", err);
|
||||
}
|
||||
process.exit();
|
||||
}
|
||||
test();
|
||||
@@ -0,0 +1,30 @@
|
||||
import pg from 'pg';
|
||||
const { Client } = pg;
|
||||
|
||||
async function testDatabase() {
|
||||
const client = new Client({
|
||||
host: '127.0.0.1',
|
||||
port: 5432,
|
||||
user: 'postgres',
|
||||
password: 'Liyaqath@7890',
|
||||
database: 'Pim_Maskan'
|
||||
});
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
console.log('Connected to Pim_Maskan.');
|
||||
const res = await client.query(`
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
ORDER BY table_name;
|
||||
`);
|
||||
console.log('Tables in Pim_Maskan:');
|
||||
res.rows.forEach(row => console.log(`- ${row.table_name}`));
|
||||
await client.end();
|
||||
} catch (err) {
|
||||
console.log(`Error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
testDatabase();
|
||||
@@ -0,0 +1,5 @@
|
||||
import dbConfig from './src/shared/config/database.config.cjs';
|
||||
console.log('Resolved database config:', {
|
||||
...dbConfig.development,
|
||||
password: dbConfig.development.password ? '***' : undefined
|
||||
});
|
||||
Reference in New Issue
Block a user