Initial commit

This commit is contained in:
liyaqath
2026-07-02 16:04:26 +05:30
parent bcb937dd26
commit 3179e3d331
100 changed files with 3306 additions and 160 deletions
+21
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}
+311 -1
View File
@@ -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
View File
@@ -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
};
+4
View File
@@ -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
View File
@@ -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();
+32
View File
@@ -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();
+62
View File
@@ -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()
];
+6
View File
@@ -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;
+4 -4
View File
@@ -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 = {}) {
+3 -1
View File
@@ -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')
];
+2
View File
@@ -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 = {}) {
+8
View File
@@ -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();
+29
View File
@@ -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();
+14
View File
@@ -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;
+72
View File
@@ -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 = {}) {
+33
View File
@@ -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') {
+11
View File
@@ -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();
};
+14 -2
View File
@@ -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,
+18 -11
View File
@@ -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' });
};
};
+153
View File
@@ -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>Were 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
};
+42
View File
@@ -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;
+30
View File
@@ -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();
+14
View File
@@ -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();
+19
View File
@@ -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();
+23
View File
@@ -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();
+30
View File
@@ -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();
+5
View File
@@ -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
});