implement authentication and authorization
This commit is contained in:
Generated
+234
@@ -9,7 +9,13 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"express-session": "^1.18.2",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
|
"passport-local": "^1.0.0",
|
||||||
"sax": "^1.4.3",
|
"sax": "^1.4.3",
|
||||||
"xml2js": "^0.6.2"
|
"xml2js": "^0.6.2"
|
||||||
},
|
},
|
||||||
@@ -100,6 +106,15 @@
|
|||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/bcryptjs": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"bin": {
|
||||||
|
"bcrypt": "bin/bcrypt"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.4",
|
"version": "1.20.4",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||||
@@ -124,6 +139,12 @@
|
|||||||
"npm": "1.2.8000 || >= 1.4.16"
|
"npm": "1.2.8000 || >= 1.4.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-equal-constant-time": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/buffer-from": {
|
"node_modules/buffer-from": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
@@ -270,6 +291,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ecdsa-sig-formatter": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ee-first": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@@ -386,6 +416,25 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express-session": {
|
||||||
|
"version": "1.18.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz",
|
||||||
|
"integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie": "0.7.2",
|
||||||
|
"cookie-signature": "1.0.7",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"depd": "~2.0.0",
|
||||||
|
"on-headers": "~1.1.0",
|
||||||
|
"parseurl": "~1.3.3",
|
||||||
|
"safe-buffer": "5.2.1",
|
||||||
|
"uid-safe": "~2.1.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ffmpeg-static": {
|
"node_modules/ffmpeg-static": {
|
||||||
"version": "5.3.0",
|
"version": "5.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.3.0.tgz",
|
||||||
@@ -617,6 +666,97 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jsonwebtoken": {
|
||||||
|
"version": "9.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||||
|
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jws": "^4.0.1",
|
||||||
|
"lodash.includes": "^4.3.0",
|
||||||
|
"lodash.isboolean": "^3.0.3",
|
||||||
|
"lodash.isinteger": "^4.0.4",
|
||||||
|
"lodash.isnumber": "^3.0.3",
|
||||||
|
"lodash.isplainobject": "^4.0.6",
|
||||||
|
"lodash.isstring": "^4.0.1",
|
||||||
|
"lodash.once": "^4.0.0",
|
||||||
|
"ms": "^2.1.1",
|
||||||
|
"semver": "^7.5.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12",
|
||||||
|
"npm": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsonwebtoken/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/jwa": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-equal-constant-time": "^1.0.1",
|
||||||
|
"ecdsa-sig-formatter": "1.0.11",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jws": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jwa": "^2.0.1",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lodash.includes": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isboolean": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isinteger": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isnumber": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isplainobject": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isstring": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.once": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -725,6 +865,15 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/on-headers": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/parse-cache-control": {
|
"node_modules/parse-cache-control": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
|
||||||
@@ -740,12 +889,64 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/passport": {
|
||||||
|
"version": "0.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
|
||||||
|
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"passport-strategy": "1.x.x",
|
||||||
|
"pause": "0.0.1",
|
||||||
|
"utils-merge": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/jaredhanson"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/passport-jwt": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jsonwebtoken": "^9.0.0",
|
||||||
|
"passport-strategy": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/passport-local": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==",
|
||||||
|
"dependencies": {
|
||||||
|
"passport-strategy": "1.x.x"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/passport-strategy": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "0.1.12",
|
"version": "0.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pause": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
||||||
|
},
|
||||||
"node_modules/progress": {
|
"node_modules/progress": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||||
@@ -784,6 +985,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/random-bytes": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/range-parser": {
|
"node_modules/range-parser": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
@@ -855,6 +1065,18 @@
|
|||||||
"integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==",
|
"integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==",
|
||||||
"license": "BlueOak-1.0.0"
|
"license": "BlueOak-1.0.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "7.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
|
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/send": {
|
"node_modules/send": {
|
||||||
"version": "0.19.2",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
||||||
@@ -1026,6 +1248,18 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/uid-safe": {
|
||||||
|
"version": "2.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||||
|
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"random-bytes": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/unpipe": {
|
"node_modules/unpipe": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
|
|||||||
@@ -9,7 +9,13 @@
|
|||||||
"dev": "node --watch server/index.js"
|
"dev": "node --watch server/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"express-session": "^1.18.2",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
|
"passport-local": "^1.0.0",
|
||||||
"sax": "^1.4.3",
|
"sax": "^1.4.3",
|
||||||
"xml2js": "^0.6.2"
|
"xml2js": "^0.6.2"
|
||||||
},
|
},
|
||||||
|
|||||||
+70
-1
@@ -3062,4 +3062,73 @@ kbd {
|
|||||||
|
|
||||||
.spin {
|
.spin {
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
/* User Management Styles */
|
||||||
|
.user-list-container {
|
||||||
|
margin: var(--space-lg) 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-table thead {
|
||||||
|
background: var(--color-bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-table th {
|
||||||
|
padding: var(--space-md);
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-table td {
|
||||||
|
padding: var(--space-md);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-table tbody tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-table tbody tr:hover {
|
||||||
|
background: var(--color-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-table .btn {
|
||||||
|
padding: var(--space-xs) var(--space-sm);
|
||||||
|
margin: 0 var(--space-xs);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-user-section {
|
||||||
|
margin-top: var(--space-2xl);
|
||||||
|
padding: var(--space-lg);
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-form {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-md);
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
margin-bottom: var(--space-xs);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|||||||
@@ -280,6 +280,7 @@
|
|||||||
<button class="tab active" data-tab="sources">Sources</button>
|
<button class="tab active" data-tab="sources">Sources</button>
|
||||||
<button class="tab" data-tab="player">Player</button>
|
<button class="tab" data-tab="player">Player</button>
|
||||||
<button class="tab" data-tab="content">Manage Content</button>
|
<button class="tab" data-tab="content">Manage Content</button>
|
||||||
|
<button class="tab" data-tab="users" id="users-tab" style="display:none;">Users</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sources Tab -->
|
<!-- Sources Tab -->
|
||||||
@@ -475,6 +476,54 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Users Tab (Admin Only) -->
|
||||||
|
<div id="tab-users" class="tab-content">
|
||||||
|
<div class="settings-section">
|
||||||
|
<h3>User Management</h3>
|
||||||
|
<p class="hint">Manage user accounts and permissions</p>
|
||||||
|
|
||||||
|
<div class="user-list-container">
|
||||||
|
<table class="user-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="user-list">
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="hint">Loading users...</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="add-user-section">
|
||||||
|
<h4>Add New User</h4>
|
||||||
|
<form id="add-user-form" class="user-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new-username">Username</label>
|
||||||
|
<input type="text" id="new-username" name="username" class="form-input" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new-password">Password</label>
|
||||||
|
<input type="password" id="new-password" name="password" class="form-input" required minlength="6">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new-role">Role</label>
|
||||||
|
<select id="new-role" name="role" class="form-input" required>
|
||||||
|
<option value="viewer">Viewer</option>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Add User</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ const API = {
|
|||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add authentication token if available
|
||||||
|
const token = localStorage.getItem('authToken');
|
||||||
|
if (token) {
|
||||||
|
options.headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
options.body = JSON.stringify(data);
|
options.body = JSON.stringify(data);
|
||||||
@@ -30,6 +36,12 @@ const API = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
// If unauthorized, redirect to login
|
||||||
|
if (response.status === 401) {
|
||||||
|
localStorage.removeItem('authToken');
|
||||||
|
window.location.href = '/login.html';
|
||||||
|
return;
|
||||||
|
}
|
||||||
throw new Error(result.error || `Server responded with ${response.status}`);
|
throw new Error(result.error || `Server responded with ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +132,14 @@ const API = {
|
|||||||
update: (data) => API.request('PUT', '/settings', data),
|
update: (data) => API.request('PUT', '/settings', data),
|
||||||
reset: () => API.request('DELETE', '/settings'),
|
reset: () => API.request('DELETE', '/settings'),
|
||||||
getDefaults: () => API.request('GET', '/settings/defaults')
|
getDefaults: () => API.request('GET', '/settings/defaults')
|
||||||
|
},
|
||||||
|
|
||||||
|
// Users (admin only)
|
||||||
|
users: {
|
||||||
|
getAll: () => API.request('GET', '/auth/users'),
|
||||||
|
create: (data) => API.request('POST', '/auth/users', data),
|
||||||
|
update: (id, data) => API.request('PUT', `/auth/users/${id}`, data),
|
||||||
|
delete: (id) => API.request('DELETE', `/auth/users/${id}`)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ class App {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.currentPage = 'home';
|
this.currentPage = 'home';
|
||||||
this.pages = {};
|
this.pages = {};
|
||||||
|
this.currentUser = null;
|
||||||
|
|
||||||
// Initialize components
|
// Initialize components
|
||||||
this.player = new VideoPlayer();
|
this.player = new VideoPlayer();
|
||||||
@@ -24,6 +25,9 @@ class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
// Check authentication first
|
||||||
|
await this.checkAuth();
|
||||||
|
|
||||||
// Mobile menu toggle
|
// Mobile menu toggle
|
||||||
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
|
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
|
||||||
const navbarMenu = document.getElementById('navbar-menu');
|
const navbarMenu = document.getElementById('navbar-menu');
|
||||||
@@ -110,6 +114,82 @@ class App {
|
|||||||
console.log('NodeCast TV initialized');
|
console.log('NodeCast TV initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkAuth() {
|
||||||
|
const token = localStorage.getItem('authToken');
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
// No token, redirect to login
|
||||||
|
window.location.href = '/login.html';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verify token with server
|
||||||
|
const response = await fetch('/api/auth/me', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Invalid token');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentUser = await response.json();
|
||||||
|
|
||||||
|
// Hide settings for viewers
|
||||||
|
if (this.currentUser.role === 'viewer') {
|
||||||
|
const settingsLink = document.querySelector('.nav-link[data-page="settings"]');
|
||||||
|
if (settingsLink) {
|
||||||
|
settingsLink.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add logout button to navbar
|
||||||
|
this.addLogoutButton();
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Authentication error:', err);
|
||||||
|
localStorage.removeItem('authToken');
|
||||||
|
window.location.href = '/login.html';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addLogoutButton() {
|
||||||
|
const navbar = document.querySelector('.navbar-menu');
|
||||||
|
if (!navbar || document.getElementById('logout-btn')) return;
|
||||||
|
|
||||||
|
const logoutLink = document.createElement('a');
|
||||||
|
logoutLink.href = '#';
|
||||||
|
logoutLink.className = 'nav-link';
|
||||||
|
logoutLink.id = 'logout-btn';
|
||||||
|
logoutLink.innerHTML = `
|
||||||
|
<span class="nav-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
|
||||||
|
<path d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"/>
|
||||||
|
</svg></span>
|
||||||
|
<span>Logout</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
logoutLink.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const token = localStorage.getItem('authToken');
|
||||||
|
if (token) {
|
||||||
|
await fetch('/api/auth/logout', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.removeItem('authToken');
|
||||||
|
window.location.href = '/login.html';
|
||||||
|
});
|
||||||
|
|
||||||
|
navbar.appendChild(logoutLink);
|
||||||
|
}
|
||||||
|
|
||||||
navigateTo(pageName) {
|
navigateTo(pageName) {
|
||||||
// Update nav
|
// Update nav
|
||||||
document.querySelectorAll('.nav-link').forEach(link => {
|
document.querySelectorAll('.nav-link').forEach(link => {
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
/**
|
||||||
|
* Auth Manager - Frontend authentication state management
|
||||||
|
*/
|
||||||
|
|
||||||
|
const Auth = {
|
||||||
|
currentUser: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize auth - check setup status and current user
|
||||||
|
*/
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
// Check if setup is required
|
||||||
|
const setupStatus = await API.auth.checkSetup();
|
||||||
|
if (setupStatus.setupRequired) {
|
||||||
|
this.showSetup();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is logged in
|
||||||
|
if (API.getToken()) {
|
||||||
|
try {
|
||||||
|
this.currentUser = await API.auth.me();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
// Token invalid, clear it
|
||||||
|
console.log('Token invalid, showing login');
|
||||||
|
API.setToken(null);
|
||||||
|
this.showLogin();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.showLogin();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Auth initialization failed:', error);
|
||||||
|
// On any error, show login (don't loop)
|
||||||
|
this.showLogin();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show setup screen
|
||||||
|
*/
|
||||||
|
showSetup() {
|
||||||
|
document.getElementById('setup-screen').classList.add('active');
|
||||||
|
document.getElementById('login-screen').classList.remove('active');
|
||||||
|
document.getElementById('app').classList.remove('active');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show login screen
|
||||||
|
*/
|
||||||
|
showLogin() {
|
||||||
|
document.getElementById('setup-screen').classList.remove('active');
|
||||||
|
document.getElementById('login-screen').classList.add('active');
|
||||||
|
document.getElementById('app').classList.remove('active');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show main app
|
||||||
|
*/
|
||||||
|
showApp() {
|
||||||
|
document.getElementById('setup-screen').classList.remove('active');
|
||||||
|
document.getElementById('login-screen').classList.remove('active');
|
||||||
|
document.getElementById('app').classList.add('active');
|
||||||
|
|
||||||
|
// Hide settings tab if viewer
|
||||||
|
if (!this.isAdmin()) {
|
||||||
|
const settingsLink = document.querySelector('[data-page="settings"]');
|
||||||
|
if (settingsLink) {
|
||||||
|
settingsLink.parentElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup initial admin user
|
||||||
|
*/
|
||||||
|
async setup(username, password) {
|
||||||
|
try {
|
||||||
|
const result = await API.auth.setup(username, password);
|
||||||
|
API.setToken(result.token);
|
||||||
|
this.currentUser = result.user;
|
||||||
|
this.showApp();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login user
|
||||||
|
*/
|
||||||
|
async login(username, password) {
|
||||||
|
try {
|
||||||
|
const result = await API.auth.login(username, password);
|
||||||
|
API.setToken(result.token);
|
||||||
|
this.currentUser = result.user;
|
||||||
|
this.showApp();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout user
|
||||||
|
*/
|
||||||
|
async logout() {
|
||||||
|
try {
|
||||||
|
await API.auth.logout();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error);
|
||||||
|
} finally {
|
||||||
|
API.setToken(null);
|
||||||
|
this.currentUser = null;
|
||||||
|
this.showLogin();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if current user is admin
|
||||||
|
*/
|
||||||
|
isAdmin() {
|
||||||
|
return this.currentUser && this.currentUser.role === 'admin';
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if current user is viewer
|
||||||
|
*/
|
||||||
|
isViewer() {
|
||||||
|
return this.currentUser && this.currentUser.role === 'viewer';
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current user
|
||||||
|
*/
|
||||||
|
getCurrentUser() {
|
||||||
|
return this.currentUser;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -3,6 +3,11 @@
|
|||||||
* Handles HLS video playback with custom controls
|
* Handles HLS video playback with custom controls
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Check if device is mobile
|
||||||
|
function isMobile() {
|
||||||
|
return /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
class VideoPlayer {
|
class VideoPlayer {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.video = document.getElementById('video-player');
|
this.video = document.getElementById('video-player');
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
document.getElementById('login-form').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const username = document.getElementById('username').value;
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await API.request('POST', '/auth/login', { username, password });
|
||||||
|
localStorage.setItem('sessionToken', response.token);
|
||||||
|
window.location.href = '/';
|
||||||
|
} catch (err) {
|
||||||
|
document.getElementById('login-error').textContent = 'Login failed: ' + err.message;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -19,6 +19,9 @@ class SettingsPage {
|
|||||||
|
|
||||||
// Player settings
|
// Player settings
|
||||||
this.initPlayerSettings();
|
this.initPlayerSettings();
|
||||||
|
|
||||||
|
// User management (admin only)
|
||||||
|
this.initUserManagement();
|
||||||
}
|
}
|
||||||
|
|
||||||
initPlayerSettings() {
|
initPlayerSettings() {
|
||||||
@@ -130,6 +133,99 @@ class SettingsPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initUserManagement() {
|
||||||
|
// User tab visibility is handled in show() method
|
||||||
|
// when currentUser is available
|
||||||
|
|
||||||
|
// Handle add user form
|
||||||
|
const addUserForm = document.getElementById('add-user-form');
|
||||||
|
if (addUserForm) {
|
||||||
|
addUserForm.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const username = document.getElementById('new-username').value;
|
||||||
|
const password = document.getElementById('new-password').value;
|
||||||
|
const role = document.getElementById('new-role').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await API.users.create({ username, password, role });
|
||||||
|
alert('User created successfully!');
|
||||||
|
addUserForm.reset();
|
||||||
|
this.loadUsers();
|
||||||
|
} catch (err) {
|
||||||
|
alert('Error creating user: ' + err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadUsers() {
|
||||||
|
const userList = document.getElementById('user-list');
|
||||||
|
if (!userList) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const users = await API.users.getAll();
|
||||||
|
|
||||||
|
if (users.length === 0) {
|
||||||
|
userList.innerHTML = '<tr><td colspan="4" class="hint">No users found</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
userList.innerHTML = users.map(user => `
|
||||||
|
<tr>
|
||||||
|
<td>${user.username}</td>
|
||||||
|
<td><span class="badge badge-${user.role === 'admin' ? 'primary' : 'secondary'}">${user.role}</span></td>
|
||||||
|
<td>${user.createdAt ? new Date(user.createdAt).toLocaleDateString() : 'N/A'}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-secondary" onclick="window.app.pages.settings.editUser(${user.id})">Edit</button>
|
||||||
|
<button class="btn btn-sm btn-error" onclick="window.app.pages.settings.deleteUser(${user.id}, '${user.username}')">Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading users:', err);
|
||||||
|
userList.innerHTML = '<tr><td colspan="4" class="hint">Error loading users</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async editUser(userId) {
|
||||||
|
const username = prompt('Enter new username (leave blank to keep current):');
|
||||||
|
const password = prompt('Enter new password (leave blank to keep current):');
|
||||||
|
const role = prompt('Enter role (admin or viewer, leave blank to keep current):');
|
||||||
|
|
||||||
|
const updates = {};
|
||||||
|
if (username) updates.username = username;
|
||||||
|
if (password) updates.password = password;
|
||||||
|
if (role) updates.role = role;
|
||||||
|
|
||||||
|
if (Object.keys(updates).length === 0) {
|
||||||
|
alert('No changes made');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await API.users.update(userId, updates);
|
||||||
|
alert('User updated successfully!');
|
||||||
|
this.loadUsers();
|
||||||
|
} catch (err) {
|
||||||
|
alert('Error updating user: ' + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteUser(userId, username) {
|
||||||
|
if (!confirm(`Are you sure you want to delete user "${username}"?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await API.users.delete(userId);
|
||||||
|
alert('User deleted successfully!');
|
||||||
|
this.loadUsers();
|
||||||
|
} catch (err) {
|
||||||
|
alert('Error deleting user: ' + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switchTab(tabName) {
|
switchTab(tabName) {
|
||||||
this.tabs.forEach(t => t.classList.toggle('active', t.dataset.tab === tabName));
|
this.tabs.forEach(t => t.classList.toggle('active', t.dataset.tab === tabName));
|
||||||
this.tabContents.forEach(c => c.classList.toggle('active', c.id === `tab-${tabName}`));
|
this.tabContents.forEach(c => c.classList.toggle('active', c.id === `tab-${tabName}`));
|
||||||
@@ -138,9 +234,22 @@ class SettingsPage {
|
|||||||
if (tabName === 'content') {
|
if (tabName === 'content') {
|
||||||
this.app.sourceManager.loadContentSources();
|
this.app.sourceManager.loadContentSources();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load users when switching to users tab
|
||||||
|
if (tabName === 'users') {
|
||||||
|
this.loadUsers();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async show() {
|
async show() {
|
||||||
|
// Show users tab for admin
|
||||||
|
if (this.app.currentUser && this.app.currentUser.role === 'admin') {
|
||||||
|
const usersTab = document.getElementById('users-tab');
|
||||||
|
if (usersTab) {
|
||||||
|
usersTab.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load sources when page is shown
|
// Load sources when page is shown
|
||||||
await this.app.sourceManager.loadSources();
|
await this.app.sourceManager.loadSources();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Login - NodeCast TV</title>
|
||||||
|
<link rel="stylesheet" href="css/main.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--color-bg-primary);
|
||||||
|
font-family: 'Outfit', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box {
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
padding: var(--space-2xl);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--space-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo .logo-icon {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
margin: 0 auto var(--space-md);
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo .brand-accent {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo p {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
margin: var(--space-sm) 0 0 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--color-bg-tertiary);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-size: 15px;
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
transition: all 0.2s;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
background: var(--color-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input::placeholder {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-login {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
background: var(--color-accent);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-login:hover {
|
||||||
|
background: var(--color-accent-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-login:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-login:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
color: var(--color-error);
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||||
|
font-size: 14px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-message {
|
||||||
|
background: rgba(99, 102, 241, 0.1);
|
||||||
|
color: var(--color-accent);
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
border: 1px solid var(--color-accent-dim);
|
||||||
|
font-size: 14px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-message.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-box">
|
||||||
|
<div class="login-logo">
|
||||||
|
<svg class="logo-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M21 6h-7.59l3.29-3.29L16 2l-4 4-4-4-.71.71L10.59 6H3a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V8a2 2 0 0 0-2-2zm0 14H3V8h18v12zM9 10v8l7-4z" />
|
||||||
|
</svg>
|
||||||
|
<h1>NodeCast <span class="brand-accent">TV</span></h1>
|
||||||
|
<p id="login-subtitle">Sign in to continue</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-message" id="error-message"></div>
|
||||||
|
<div class="setup-message" id="setup-message">
|
||||||
|
Welcome! Please create your admin account to get started.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="login-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" id="username" name="username" required autocomplete="username" placeholder="Enter your username">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" id="password" name="password" required autocomplete="current-password" minlength="6" placeholder="Enter your password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn-login" id="submit-btn">Sign In</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Check if setup is required
|
||||||
|
let isSetupMode = false;
|
||||||
|
|
||||||
|
async function checkSetupRequired() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/setup-required');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.setupRequired) {
|
||||||
|
isSetupMode = true;
|
||||||
|
document.getElementById('login-subtitle').textContent = 'Create Admin Account';
|
||||||
|
document.getElementById('submit-btn').textContent = 'Create Account';
|
||||||
|
document.getElementById('setup-message').classList.add('show');
|
||||||
|
document.getElementById('password').placeholder = 'Minimum 6 characters';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error checking setup status:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle form submission
|
||||||
|
document.getElementById('login-form').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const username = document.getElementById('username').value;
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
const submitBtn = document.getElementById('submit-btn');
|
||||||
|
const errorMessage = document.getElementById('error-message');
|
||||||
|
|
||||||
|
// Clear previous errors
|
||||||
|
errorMessage.classList.remove('show');
|
||||||
|
errorMessage.textContent = '';
|
||||||
|
|
||||||
|
// Disable button
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.textContent = 'Please wait...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const endpoint = isSetupMode ? '/api/auth/setup' : '/api/auth/login';
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ username, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || 'Authentication failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store token
|
||||||
|
localStorage.setItem('authToken', data.token);
|
||||||
|
|
||||||
|
// Redirect to main app
|
||||||
|
window.location.href = '/';
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
errorMessage.textContent = err.message;
|
||||||
|
errorMessage.classList.add('show');
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.textContent = isSetupMode ? 'Create Account' : 'Sign In';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check setup status on load
|
||||||
|
checkSetupRequired();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+151
@@ -0,0 +1,151 @@
|
|||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const passport = require('passport');
|
||||||
|
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
|
||||||
|
const { Strategy: LocalStrategy } = require('passport-local');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentication and Authorization Module
|
||||||
|
* Handles user authentication, session management, and role-based access control
|
||||||
|
* Using Passport.js with JWT tokens
|
||||||
|
*/
|
||||||
|
|
||||||
|
// JWT Secret - In production, use environment variable
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'nodecast-tv-secret-key-change-in-production';
|
||||||
|
const JWT_EXPIRY = '24h';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash password using bcrypt
|
||||||
|
*/
|
||||||
|
async function hashPassword(password) {
|
||||||
|
const salt = await bcrypt.genSalt(10);
|
||||||
|
return bcrypt.hash(password, salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify password against hash
|
||||||
|
*/
|
||||||
|
async function verifyPassword(password, hash) {
|
||||||
|
return bcrypt.compare(password, hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate JWT token
|
||||||
|
*/
|
||||||
|
function generateToken(user) {
|
||||||
|
return jwt.sign(
|
||||||
|
{
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
role: user.role
|
||||||
|
},
|
||||||
|
JWT_SECRET,
|
||||||
|
{ expiresIn: JWT_EXPIRY }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify JWT token
|
||||||
|
*/
|
||||||
|
function verifyToken(token) {
|
||||||
|
try {
|
||||||
|
return jwt.verify(token, JWT_SECRET);
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure Passport Local Strategy for username/password authentication
|
||||||
|
*/
|
||||||
|
function configureLocalStrategy(getUserByUsername, verifyUserPassword) {
|
||||||
|
passport.use(new LocalStrategy(
|
||||||
|
async (username, password, done) => {
|
||||||
|
try {
|
||||||
|
const user = await getUserByUsername(username);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return done(null, false, { message: 'Invalid credentials' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await verifyUserPassword(password, user.passwordHash);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
return done(null, false, { message: 'Invalid credentials' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return done(null, user);
|
||||||
|
} catch (err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure Passport JWT Strategy for token-based authentication
|
||||||
|
*/
|
||||||
|
function configureJwtStrategy(getUserById) {
|
||||||
|
const options = {
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
secretOrKey: JWT_SECRET
|
||||||
|
};
|
||||||
|
|
||||||
|
passport.use(new JwtStrategy(options, async (payload, done) => {
|
||||||
|
try {
|
||||||
|
const user = await getUserById(payload.id);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return done(null, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return done(null, {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
role: user.role
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return done(err, false);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware: Require authentication using Passport JWT
|
||||||
|
*/
|
||||||
|
const requireAuth = passport.authenticate('jwt', { session: false });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware: Require admin role
|
||||||
|
*/
|
||||||
|
function requireAdmin(req, res, next) {
|
||||||
|
if (!req.user || req.user.role !== 'admin') {
|
||||||
|
return res.status(403).json({ error: 'Forbidden - Admin access required' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware: Check for specific role
|
||||||
|
*/
|
||||||
|
function requireRole(role) {
|
||||||
|
return (req, res, next) => {
|
||||||
|
if (!req.user || req.user.role !== role) {
|
||||||
|
return res.status(403).json({ error: `Forbidden - ${role} access required` });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
passport,
|
||||||
|
hashPassword,
|
||||||
|
verifyPassword,
|
||||||
|
generateToken,
|
||||||
|
verifyToken,
|
||||||
|
configureLocalStrategy,
|
||||||
|
configureJwtStrategy,
|
||||||
|
requireAuth,
|
||||||
|
requireAdmin,
|
||||||
|
requireRole
|
||||||
|
};
|
||||||
+104
-1
@@ -22,6 +22,7 @@ async function loadDb() {
|
|||||||
hiddenItems: data.hiddenItems || [],
|
hiddenItems: data.hiddenItems || [],
|
||||||
favorites: data.favorites || [],
|
favorites: data.favorites || [],
|
||||||
settings: data.settings || getDefaultSettings(),
|
settings: data.settings || getDefaultSettings(),
|
||||||
|
users: data.users || [],
|
||||||
nextId: data.nextId || 1
|
nextId: data.nextId || 1
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -32,6 +33,7 @@ async function loadDb() {
|
|||||||
hiddenItems: [],
|
hiddenItems: [],
|
||||||
favorites: [],
|
favorites: [],
|
||||||
settings: getDefaultSettings(),
|
settings: getDefaultSettings(),
|
||||||
|
users: [],
|
||||||
nextId: 1
|
nextId: 1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -45,6 +47,7 @@ async function loadDb() {
|
|||||||
hiddenItems: [],
|
hiddenItems: [],
|
||||||
favorites: [],
|
favorites: [],
|
||||||
settings: getDefaultSettings(),
|
settings: getDefaultSettings(),
|
||||||
|
users: [],
|
||||||
nextId: 1
|
nextId: 1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -311,4 +314,104 @@ const settings = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { sources, hiddenItems, favorites, settings, getDefaultSettings };
|
// User operations
|
||||||
|
const users = {
|
||||||
|
async getAll() {
|
||||||
|
const db = await loadDb();
|
||||||
|
return db.users || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async getById(id) {
|
||||||
|
const db = await loadDb();
|
||||||
|
return db.users?.find(u => u.id === parseInt(id));
|
||||||
|
},
|
||||||
|
|
||||||
|
async getByUsername(username) {
|
||||||
|
const db = await loadDb();
|
||||||
|
return db.users?.find(u => u.username === username);
|
||||||
|
},
|
||||||
|
|
||||||
|
async create(userData) {
|
||||||
|
const db = await loadDb();
|
||||||
|
if (!db.users) {
|
||||||
|
db.users = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if username already exists
|
||||||
|
if (db.users.some(u => u.username === userData.username)) {
|
||||||
|
throw new Error('Username already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUser = {
|
||||||
|
id: db.nextId++,
|
||||||
|
username: userData.username,
|
||||||
|
passwordHash: userData.passwordHash,
|
||||||
|
role: userData.role || 'viewer',
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
db.users.push(newUser);
|
||||||
|
await saveDb(db);
|
||||||
|
|
||||||
|
// Return user without password hash
|
||||||
|
const { passwordHash, ...userWithoutPassword } = newUser;
|
||||||
|
return userWithoutPassword;
|
||||||
|
},
|
||||||
|
|
||||||
|
async update(id, updates) {
|
||||||
|
const db = await loadDb();
|
||||||
|
const userIndex = db.users?.findIndex(u => u.id === parseInt(id));
|
||||||
|
|
||||||
|
if (userIndex === -1 || userIndex === undefined) {
|
||||||
|
throw new Error('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if username is being changed and if it already exists
|
||||||
|
if (updates.username && updates.username !== db.users[userIndex].username) {
|
||||||
|
if (db.users.some(u => u.username === updates.username)) {
|
||||||
|
throw new Error('Username already exists');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
db.users[userIndex] = {
|
||||||
|
...db.users[userIndex],
|
||||||
|
...updates,
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
await saveDb(db);
|
||||||
|
|
||||||
|
// Return user without password hash
|
||||||
|
const { passwordHash, ...userWithoutPassword } = db.users[userIndex];
|
||||||
|
return userWithoutPassword;
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(id) {
|
||||||
|
const db = await loadDb();
|
||||||
|
const userIndex = db.users?.findIndex(u => u.id === parseInt(id));
|
||||||
|
|
||||||
|
if (userIndex === -1 || userIndex === undefined) {
|
||||||
|
throw new Error('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent deleting the last admin
|
||||||
|
const user = db.users[userIndex];
|
||||||
|
if (user.role === 'admin') {
|
||||||
|
const adminCount = db.users.filter(u => u.role === 'admin').length;
|
||||||
|
if (adminCount <= 1) {
|
||||||
|
throw new Error('Cannot delete the last admin user');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
db.users.splice(userIndex, 1);
|
||||||
|
await saveDb(db);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async count() {
|
||||||
|
const db = await loadDb();
|
||||||
|
return db.users?.length || 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { loadDb, saveDb, sources, hiddenItems, favorites, settings, users, getDefaultSettings };
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const passport = require('passport');
|
||||||
|
|
||||||
// Initialize database
|
// Initialize database
|
||||||
require('./db');
|
require('./db');
|
||||||
@@ -13,6 +14,10 @@ app.set('trust proxy', true);
|
|||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
app.use(express.json({ limit: '50mb' }));
|
app.use(express.json({ limit: '50mb' }));
|
||||||
|
|
||||||
|
// Initialize Passport
|
||||||
|
app.use(passport.initialize());
|
||||||
|
|
||||||
app.use(express.static(path.join(__dirname, '..', 'public')));
|
app.use(express.static(path.join(__dirname, '..', 'public')));
|
||||||
|
|
||||||
// FFMPEG Configuration (optional - for transcoding support)
|
// FFMPEG Configuration (optional - for transcoding support)
|
||||||
@@ -49,6 +54,7 @@ function findFFmpeg() {
|
|||||||
app.locals.ffmpegPath = findFFmpeg();
|
app.locals.ffmpegPath = findFFmpeg();
|
||||||
|
|
||||||
// API Routes
|
// API Routes
|
||||||
|
app.use('/api/auth', require('./routes/auth'));
|
||||||
app.use('/api/sources', require('./routes/sources'));
|
app.use('/api/sources', require('./routes/sources'));
|
||||||
app.use('/api/proxy', require('./routes/proxy'));
|
app.use('/api/proxy', require('./routes/proxy'));
|
||||||
app.use('/api/channels', require('./routes/channels'));
|
app.use('/api/channels', require('./routes/channels'));
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const db = require('../db');
|
||||||
|
const auth = require('../auth');
|
||||||
|
|
||||||
|
// Configure Passport strategies
|
||||||
|
auth.configureLocalStrategy(
|
||||||
|
async (username) => await db.users.getByUsername(username),
|
||||||
|
async (password, hash) => await auth.verifyPassword(password, hash)
|
||||||
|
);
|
||||||
|
|
||||||
|
auth.configureJwtStrategy(
|
||||||
|
async (id) => await db.users.getById(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if initial setup is required
|
||||||
|
* GET /api/auth/setup-required
|
||||||
|
*/
|
||||||
|
router.get('/setup-required', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userCount = await db.users.count();
|
||||||
|
res.json({ setupRequired: userCount === 0 });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in /setup-required:', err);
|
||||||
|
res.status(500).json({ error: 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial setup - Create admin user
|
||||||
|
* POST /api/auth/setup
|
||||||
|
*/
|
||||||
|
router.post('/setup', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userCount = await db.users.count();
|
||||||
|
|
||||||
|
// Check if setup already done
|
||||||
|
if (userCount > 0) {
|
||||||
|
return res.status(400).json({ error: 'Setup already completed' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { username, password } = req.body;
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
return res.status(400).json({ error: 'Username and password required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
return res.status(400).json({ error: 'Password must be at least 6 characters' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create admin user
|
||||||
|
const passwordHash = await auth.hashPassword(password);
|
||||||
|
const adminUser = await db.users.create({
|
||||||
|
username,
|
||||||
|
passwordHash,
|
||||||
|
role: 'admin'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate token for immediate login
|
||||||
|
const token = auth.generateToken(adminUser);
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
message: 'Admin user created successfully',
|
||||||
|
token,
|
||||||
|
user: adminUser
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in /setup:', err);
|
||||||
|
res.status(500).json({ error: err.message || 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login with Passport Local Strategy
|
||||||
|
* POST /api/auth/login
|
||||||
|
*/
|
||||||
|
router.post('/login', (req, res, next) => {
|
||||||
|
auth.passport.authenticate('local', { session: false }, (err, user, info) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Login error:', err);
|
||||||
|
return res.status(500).json({ error: 'Server error' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(401).json({ error: info?.message || 'Invalid credentials' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate JWT token
|
||||||
|
const token = auth.generateToken(user);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
token,
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
role: user.role
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})(req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout (client-side handles token removal)
|
||||||
|
* POST /api/auth/logout
|
||||||
|
*/
|
||||||
|
router.post('/logout', (req, res) => {
|
||||||
|
// With JWT, logout is handled client-side by removing the token
|
||||||
|
// This endpoint exists for consistency and future server-side token blacklisting
|
||||||
|
res.json({ success: true, message: 'Logged out successfully' });
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current user
|
||||||
|
* GET /api/auth/me
|
||||||
|
*/
|
||||||
|
router.get('/me', auth.requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = await db.users.getById(req.user.id);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
role: user.role
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in /me:', err);
|
||||||
|
res.status(500).json({ error: 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all users (admin only)
|
||||||
|
* GET /api/auth/users
|
||||||
|
*/
|
||||||
|
router.get('/users', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const allUsers = await db.users.getAll();
|
||||||
|
|
||||||
|
// Remove password hashes
|
||||||
|
const users = allUsers.map(u => {
|
||||||
|
const { passwordHash, ...userWithoutPassword } = u;
|
||||||
|
return userWithoutPassword;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(users);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching users:', err);
|
||||||
|
res.status(500).json({ error: 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new user (admin only)
|
||||||
|
* POST /api/auth/users
|
||||||
|
*/
|
||||||
|
router.post('/users', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { username, password, role } = req.body;
|
||||||
|
|
||||||
|
if (!username || !password || !role) {
|
||||||
|
return res.status(400).json({ error: 'Username, password, and role are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
return res.status(400).json({ error: 'Password must be at least 6 characters' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!['admin', 'viewer'].includes(role)) {
|
||||||
|
return res.status(400).json({ error: 'Role must be either "admin" or "viewer"' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordHash = await auth.hashPassword(password);
|
||||||
|
const newUser = await db.users.create({
|
||||||
|
username,
|
||||||
|
passwordHash,
|
||||||
|
role
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json(newUser);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error creating user:', err);
|
||||||
|
res.status(500).json({ error: err.message || 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a user (admin only)
|
||||||
|
* PUT /api/auth/users/:id
|
||||||
|
*/
|
||||||
|
router.put('/users/:id', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { username, password, role } = req.body;
|
||||||
|
|
||||||
|
const updates = {};
|
||||||
|
|
||||||
|
if (username) {
|
||||||
|
updates.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password) {
|
||||||
|
if (password.length < 6) {
|
||||||
|
return res.status(400).json({ error: 'Password must be at least 6 characters' });
|
||||||
|
}
|
||||||
|
updates.passwordHash = await auth.hashPassword(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role) {
|
||||||
|
if (!['admin', 'viewer'].includes(role)) {
|
||||||
|
return res.status(400).json({ error: 'Role must be either "admin" or "viewer"' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent removing admin role from the last admin
|
||||||
|
const user = await db.users.getById(id);
|
||||||
|
if (user && user.role === 'admin' && role !== 'admin') {
|
||||||
|
const allUsers = await db.users.getAll();
|
||||||
|
const adminCount = allUsers.filter(u => u.role === 'admin').length;
|
||||||
|
if (adminCount <= 1) {
|
||||||
|
return res.status(400).json({ error: 'Cannot remove admin role from the last admin user' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updates.role = role;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedUser = await db.users.update(id, updates);
|
||||||
|
res.json(updatedUser);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error updating user:', err);
|
||||||
|
res.status(500).json({ error: err.message || 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a user (admin only)
|
||||||
|
* DELETE /api/auth/users/:id
|
||||||
|
*/
|
||||||
|
router.delete('/users/:id', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
// Prevent deleting yourself
|
||||||
|
if (parseInt(id) === req.user.id) {
|
||||||
|
return res.status(400).json({ error: 'Cannot delete your own account' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.users.delete(id);
|
||||||
|
res.json({ success: true, message: 'User deleted successfully' });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error deleting user:', err);
|
||||||
|
res.status(500).json({ error: err.message || 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const db = require('../db');
|
||||||
|
const auth = require('../auth');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all users (admin only)
|
||||||
|
* GET /api/users
|
||||||
|
*/
|
||||||
|
router.get('/', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const data = await db.loadDb();
|
||||||
|
const users = (data.users || []).map(u => ({
|
||||||
|
id: u.id,
|
||||||
|
username: u.username,
|
||||||
|
role: u.role,
|
||||||
|
createdAt: u.createdAt
|
||||||
|
}));
|
||||||
|
res.json(users);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create user (admin only)
|
||||||
|
* POST /api/users
|
||||||
|
*/
|
||||||
|
router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { username, password, role } = req.body;
|
||||||
|
|
||||||
|
if (!username || !password || !role) {
|
||||||
|
return res.status(400).json({ error: 'Username, password, and role required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
return res.status(400).json({ error: 'Password must be at least 6 characters' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!['admin', 'viewer'].includes(role)) {
|
||||||
|
return res.status(400).json({ error: 'Role must be admin or viewer' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await db.loadDb();
|
||||||
|
|
||||||
|
// Check if username exists
|
||||||
|
if (data.users?.some(u => u.username === username)) {
|
||||||
|
return res.status(400).json({ error: 'Username already exists' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
const passwordHash = await auth.hashPassword(password);
|
||||||
|
const newUser = {
|
||||||
|
id: data.nextUserId || (data.users?.length || 0) + 1,
|
||||||
|
username,
|
||||||
|
passwordHash,
|
||||||
|
role,
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
data.users = data.users || [];
|
||||||
|
data.users.push(newUser);
|
||||||
|
data.nextUserId = newUser.id + 1;
|
||||||
|
|
||||||
|
await db.saveDb(data);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
id: newUser.id,
|
||||||
|
username: newUser.username,
|
||||||
|
role: newUser.role,
|
||||||
|
createdAt: newUser.createdAt
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Create user error:', err);
|
||||||
|
res.status(500).json({ error: 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update user (admin only)
|
||||||
|
* PUT /api/users/:id
|
||||||
|
*/
|
||||||
|
router.put('/:id', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = parseInt(req.params.id);
|
||||||
|
const { username, password, role } = req.body;
|
||||||
|
|
||||||
|
const data = await db.loadDb();
|
||||||
|
const userIndex = data.users?.findIndex(u => u.id === userId);
|
||||||
|
|
||||||
|
if (userIndex === -1 || userIndex === undefined) {
|
||||||
|
return res.status(404).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = data.users[userIndex];
|
||||||
|
|
||||||
|
// Update username if provided
|
||||||
|
if (username && username !== user.username) {
|
||||||
|
// Check if new username exists
|
||||||
|
if (data.users.some(u => u.username === username && u.id !== userId)) {
|
||||||
|
return res.status(400).json({ error: 'Username already exists' });
|
||||||
|
}
|
||||||
|
user.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update password if provided
|
||||||
|
if (password) {
|
||||||
|
if (password.length < 6) {
|
||||||
|
return res.status(400).json({ error: 'Password must be at least 6 characters' });
|
||||||
|
}
|
||||||
|
user.passwordHash = await auth.hashPassword(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update role if provided
|
||||||
|
if (role) {
|
||||||
|
if (!['admin', 'viewer'].includes(role)) {
|
||||||
|
return res.status(400).json({ error: 'Role must be admin or viewer' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent removing last admin
|
||||||
|
if (user.role === 'admin' && role !== 'admin') {
|
||||||
|
const adminCount = data.users.filter(u => u.role === 'admin').length;
|
||||||
|
if (adminCount <= 1) {
|
||||||
|
return res.status(400).json({ error: 'Cannot remove last admin user' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user.role = role;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.saveDb(data);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
role: user.role,
|
||||||
|
createdAt: user.createdAt
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Update user error:', err);
|
||||||
|
res.status(500).json({ error: 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete user (admin only)
|
||||||
|
* DELETE /api/users/:id
|
||||||
|
*/
|
||||||
|
router.delete('/:id', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = parseInt(req.params.id);
|
||||||
|
|
||||||
|
const data = await db.loadDb();
|
||||||
|
const userIndex = data.users?.findIndex(u => u.id === userId);
|
||||||
|
|
||||||
|
if (userIndex === -1 || userIndex === undefined) {
|
||||||
|
return res.status(404).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = data.users[userIndex];
|
||||||
|
|
||||||
|
// Prevent deleting yourself
|
||||||
|
if (user.id === req.session.userId) {
|
||||||
|
return res.status(400).json({ error: 'Cannot delete your own account' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent deleting last admin
|
||||||
|
if (user.role === 'admin') {
|
||||||
|
const adminCount = data.users.filter(u => u.role === 'admin').length;
|
||||||
|
if (adminCount <= 1) {
|
||||||
|
return res.status(400).json({ error: 'Cannot delete last admin user' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.users.splice(userIndex, 1);
|
||||||
|
await db.saveDb(data);
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Delete user error:', err);
|
||||||
|
res.status(500).json({ error: 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
Reference in New Issue
Block a user