feat(auth): implement JWT-based authentication with admin access controls
This commit is contained in:
@ -37,6 +37,8 @@
|
|||||||
"@push.rocks/smartdata": "^5.15.1",
|
"@push.rocks/smartdata": "^5.15.1",
|
||||||
"@push.rocks/smartdns": "^7.5.0",
|
"@push.rocks/smartdns": "^7.5.0",
|
||||||
"@push.rocks/smartfile": "^11.2.5",
|
"@push.rocks/smartfile": "^11.2.5",
|
||||||
|
"@push.rocks/smartguard": "^3.1.0",
|
||||||
|
"@push.rocks/smartjwt": "^2.2.1",
|
||||||
"@push.rocks/smartlog": "^3.1.8",
|
"@push.rocks/smartlog": "^3.1.8",
|
||||||
"@push.rocks/smartmail": "^2.1.0",
|
"@push.rocks/smartmail": "^2.1.0",
|
||||||
"@push.rocks/smartnetwork": "^4.0.2",
|
"@push.rocks/smartnetwork": "^4.0.2",
|
||||||
@ -47,6 +49,7 @@
|
|||||||
"@push.rocks/smartrule": "^2.0.1",
|
"@push.rocks/smartrule": "^2.0.1",
|
||||||
"@push.rocks/smartrx": "^3.0.10",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
"@push.rocks/smartstate": "^2.0.0",
|
"@push.rocks/smartstate": "^2.0.0",
|
||||||
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
"@serve.zone/interfaces": "^5.0.4",
|
"@serve.zone/interfaces": "^5.0.4",
|
||||||
"@tsclass/tsclass": "^9.2.0",
|
"@tsclass/tsclass": "^9.2.0",
|
||||||
"@types/mailparser": "^3.4.6",
|
"@types/mailparser": "^3.4.6",
|
||||||
|
112
pnpm-lock.yaml
generated
112
pnpm-lock.yaml
generated
@ -47,6 +47,12 @@ importers:
|
|||||||
'@push.rocks/smartfile':
|
'@push.rocks/smartfile':
|
||||||
specifier: ^11.2.5
|
specifier: ^11.2.5
|
||||||
version: 11.2.5
|
version: 11.2.5
|
||||||
|
'@push.rocks/smartguard':
|
||||||
|
specifier: ^3.1.0
|
||||||
|
version: 3.1.0
|
||||||
|
'@push.rocks/smartjwt':
|
||||||
|
specifier: ^2.2.1
|
||||||
|
version: 2.2.1
|
||||||
'@push.rocks/smartlog':
|
'@push.rocks/smartlog':
|
||||||
specifier: ^3.1.8
|
specifier: ^3.1.8
|
||||||
version: 3.1.8
|
version: 3.1.8
|
||||||
@ -77,6 +83,9 @@ importers:
|
|||||||
'@push.rocks/smartstate':
|
'@push.rocks/smartstate':
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.0.19
|
version: 2.0.19
|
||||||
|
'@push.rocks/smartunique':
|
||||||
|
specifier: ^3.0.9
|
||||||
|
version: 3.0.9
|
||||||
'@serve.zone/interfaces':
|
'@serve.zone/interfaces':
|
||||||
specifier: ^5.0.4
|
specifier: ^5.0.4
|
||||||
version: 5.0.4
|
version: 5.0.4
|
||||||
@ -974,6 +983,9 @@ packages:
|
|||||||
'@push.rocks/smartjson@5.0.20':
|
'@push.rocks/smartjson@5.0.20':
|
||||||
resolution: {integrity: sha512-ogGBLyOTluphZVwBYNyjhm5sziPGuiAwWihW07OSRxD4HQUyqj9Ek6r1pqH07JUG5EbtRYivM1Yt1cCwnu3JVQ==}
|
resolution: {integrity: sha512-ogGBLyOTluphZVwBYNyjhm5sziPGuiAwWihW07OSRxD4HQUyqj9Ek6r1pqH07JUG5EbtRYivM1Yt1cCwnu3JVQ==}
|
||||||
|
|
||||||
|
'@push.rocks/smartjwt@2.2.1':
|
||||||
|
resolution: {integrity: sha512-Xwau9o8u7kLfSGi5v+kiyGB/hiDPclZjVEuj69J0LszO9nOh4OexYizKIOgOzKQMqnYQ03Dy35KqP9pdEjccbQ==}
|
||||||
|
|
||||||
'@push.rocks/smartlog-destination-devtools@1.0.12':
|
'@push.rocks/smartlog-destination-devtools@1.0.12':
|
||||||
resolution: {integrity: sha512-zvsIkrqByc0JRaBgIyhh+PSz2SY/e/bmhZdUcr/OW6pudgAcqe2sso68EzrKux0w9OMl1P9ZnzF3FpCZPFWD/A==}
|
resolution: {integrity: sha512-zvsIkrqByc0JRaBgIyhh+PSz2SY/e/bmhZdUcr/OW6pudgAcqe2sso68EzrKux0w9OMl1P9ZnzF3FpCZPFWD/A==}
|
||||||
|
|
||||||
@ -1606,6 +1618,9 @@ packages:
|
|||||||
'@types/jsonfile@6.1.4':
|
'@types/jsonfile@6.1.4':
|
||||||
resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==}
|
resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==}
|
||||||
|
|
||||||
|
'@types/jsonwebtoken@9.0.9':
|
||||||
|
resolution: {integrity: sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==}
|
||||||
|
|
||||||
'@types/mailparser@3.4.6':
|
'@types/mailparser@3.4.6':
|
||||||
resolution: {integrity: sha512-wVV3cnIKzxTffaPH8iRnddX1zahbYB1ZEoAxyhoBo3TBCBuK6nZ8M8JYO/RhsCuuBVOw/DEN/t/ENbruwlxn6Q==}
|
resolution: {integrity: sha512-wVV3cnIKzxTffaPH8iRnddX1zahbYB1ZEoAxyhoBo3TBCBuK6nZ8M8JYO/RhsCuuBVOw/DEN/t/ENbruwlxn6Q==}
|
||||||
|
|
||||||
@ -1886,6 +1901,9 @@ packages:
|
|||||||
buffer-crc32@0.2.13:
|
buffer-crc32@0.2.13:
|
||||||
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
||||||
|
|
||||||
|
buffer-equal-constant-time@1.0.1:
|
||||||
|
resolution: {integrity: sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=}
|
||||||
|
|
||||||
buffer-from@1.1.2:
|
buffer-from@1.1.2:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
|
||||||
@ -2238,6 +2256,9 @@ packages:
|
|||||||
eastasianwidth@0.2.0:
|
eastasianwidth@0.2.0:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
|
|
||||||
|
ecdsa-sig-formatter@1.0.11:
|
||||||
|
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||||
|
|
||||||
ee-first@1.1.1:
|
ee-first@1.1.1:
|
||||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
|
|
||||||
@ -2884,6 +2905,16 @@ packages:
|
|||||||
jsonfile@6.1.0:
|
jsonfile@6.1.0:
|
||||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||||
|
|
||||||
|
jsonwebtoken@9.0.2:
|
||||||
|
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
|
||||||
|
engines: {node: '>=12', npm: '>=6'}
|
||||||
|
|
||||||
|
jwa@1.4.2:
|
||||||
|
resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==}
|
||||||
|
|
||||||
|
jws@3.2.2:
|
||||||
|
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
||||||
|
|
||||||
keygrip@1.1.0:
|
keygrip@1.1.0:
|
||||||
resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==}
|
resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@ -2982,15 +3013,36 @@ packages:
|
|||||||
lodash.clonedeep@4.5.0:
|
lodash.clonedeep@4.5.0:
|
||||||
resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==}
|
resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==}
|
||||||
|
|
||||||
|
lodash.includes@4.3.0:
|
||||||
|
resolution: {integrity: sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=}
|
||||||
|
|
||||||
lodash.isarguments@3.1.0:
|
lodash.isarguments@3.1.0:
|
||||||
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
|
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
|
||||||
|
|
||||||
lodash.isarray@3.0.4:
|
lodash.isarray@3.0.4:
|
||||||
resolution: {integrity: sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==}
|
resolution: {integrity: sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==}
|
||||||
|
|
||||||
|
lodash.isboolean@3.0.3:
|
||||||
|
resolution: {integrity: sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=}
|
||||||
|
|
||||||
|
lodash.isinteger@4.0.4:
|
||||||
|
resolution: {integrity: sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=}
|
||||||
|
|
||||||
|
lodash.isnumber@3.0.3:
|
||||||
|
resolution: {integrity: sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=}
|
||||||
|
|
||||||
|
lodash.isplainobject@4.0.6:
|
||||||
|
resolution: {integrity: sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=}
|
||||||
|
|
||||||
|
lodash.isstring@4.0.1:
|
||||||
|
resolution: {integrity: sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=}
|
||||||
|
|
||||||
lodash.keys@3.1.2:
|
lodash.keys@3.1.2:
|
||||||
resolution: {integrity: sha512-CuBsapFjcubOGMn3VD+24HOAPxM79tH+V6ivJL3CHYjtrawauDJHUk//Yew9Hvc6e9rbCrURGk8z6PC+8WJBfQ==}
|
resolution: {integrity: sha512-CuBsapFjcubOGMn3VD+24HOAPxM79tH+V6ivJL3CHYjtrawauDJHUk//Yew9Hvc6e9rbCrURGk8z6PC+8WJBfQ==}
|
||||||
|
|
||||||
|
lodash.once@4.1.1:
|
||||||
|
resolution: {integrity: sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=}
|
||||||
|
|
||||||
lodash.restparam@3.6.1:
|
lodash.restparam@3.6.1:
|
||||||
resolution: {integrity: sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==}
|
resolution: {integrity: sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==}
|
||||||
|
|
||||||
@ -5356,10 +5408,8 @@ snapshots:
|
|||||||
'@push.rocks/taskbuffer': 3.1.7
|
'@push.rocks/taskbuffer': 3.1.7
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@nuxt/kit'
|
- '@nuxt/kit'
|
||||||
- bufferutil
|
|
||||||
- react
|
- react
|
||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@hapi/hoek@9.3.0': {}
|
'@hapi/hoek@9.3.0': {}
|
||||||
@ -5940,6 +5990,15 @@ snapshots:
|
|||||||
fast-json-stable-stringify: 2.1.0
|
fast-json-stable-stringify: 2.1.0
|
||||||
lodash.clonedeep: 4.5.0
|
lodash.clonedeep: 4.5.0
|
||||||
|
|
||||||
|
'@push.rocks/smartjwt@2.2.1':
|
||||||
|
dependencies:
|
||||||
|
'@push.rocks/smartcrypto': 2.0.4
|
||||||
|
'@push.rocks/smartguard': 3.1.0
|
||||||
|
'@push.rocks/smartjson': 5.0.20
|
||||||
|
'@tsclass/tsclass': 4.4.4
|
||||||
|
'@types/jsonwebtoken': 9.0.9
|
||||||
|
jsonwebtoken: 9.0.2
|
||||||
|
|
||||||
'@push.rocks/smartlog-destination-devtools@1.0.12':
|
'@push.rocks/smartlog-destination-devtools@1.0.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartlog-interfaces': 3.0.2
|
'@push.rocks/smartlog-interfaces': 3.0.2
|
||||||
@ -7045,6 +7104,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.30
|
'@types/node': 22.15.30
|
||||||
|
|
||||||
|
'@types/jsonwebtoken@9.0.9':
|
||||||
|
dependencies:
|
||||||
|
'@types/ms': 2.1.0
|
||||||
|
'@types/node': 22.15.30
|
||||||
|
|
||||||
'@types/mailparser@3.4.6':
|
'@types/mailparser@3.4.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.30
|
'@types/node': 22.15.30
|
||||||
@ -7337,6 +7401,8 @@ snapshots:
|
|||||||
|
|
||||||
buffer-crc32@0.2.13: {}
|
buffer-crc32@0.2.13: {}
|
||||||
|
|
||||||
|
buffer-equal-constant-time@1.0.1: {}
|
||||||
|
|
||||||
buffer-from@1.1.2: {}
|
buffer-from@1.1.2: {}
|
||||||
|
|
||||||
buffer-json@2.0.0: {}
|
buffer-json@2.0.0: {}
|
||||||
@ -7670,6 +7736,10 @@ snapshots:
|
|||||||
|
|
||||||
eastasianwidth@0.2.0: {}
|
eastasianwidth@0.2.0: {}
|
||||||
|
|
||||||
|
ecdsa-sig-formatter@1.0.11:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
ee-first@1.1.1: {}
|
ee-first@1.1.1: {}
|
||||||
|
|
||||||
elliptic@6.6.1:
|
elliptic@6.6.1:
|
||||||
@ -8463,6 +8533,30 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
|
|
||||||
|
jsonwebtoken@9.0.2:
|
||||||
|
dependencies:
|
||||||
|
jws: 3.2.2
|
||||||
|
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.1.1
|
||||||
|
ms: 2.1.3
|
||||||
|
semver: 7.7.2
|
||||||
|
|
||||||
|
jwa@1.4.2:
|
||||||
|
dependencies:
|
||||||
|
buffer-equal-constant-time: 1.0.1
|
||||||
|
ecdsa-sig-formatter: 1.0.11
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
|
jws@3.2.2:
|
||||||
|
dependencies:
|
||||||
|
jwa: 1.4.2
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
keygrip@1.1.0:
|
keygrip@1.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
tsscmp: 1.0.6
|
tsscmp: 1.0.6
|
||||||
@ -8585,16 +8679,30 @@ snapshots:
|
|||||||
|
|
||||||
lodash.clonedeep@4.5.0: {}
|
lodash.clonedeep@4.5.0: {}
|
||||||
|
|
||||||
|
lodash.includes@4.3.0: {}
|
||||||
|
|
||||||
lodash.isarguments@3.1.0: {}
|
lodash.isarguments@3.1.0: {}
|
||||||
|
|
||||||
lodash.isarray@3.0.4: {}
|
lodash.isarray@3.0.4: {}
|
||||||
|
|
||||||
|
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.keys@3.1.2:
|
lodash.keys@3.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
lodash._getnative: 3.9.1
|
lodash._getnative: 3.9.1
|
||||||
lodash.isarguments: 3.1.0
|
lodash.isarguments: 3.1.0
|
||||||
lodash.isarray: 3.0.4
|
lodash.isarray: 3.0.4
|
||||||
|
|
||||||
|
lodash.once@4.1.1: {}
|
||||||
|
|
||||||
lodash.restparam@3.6.1: {}
|
lodash.restparam@3.6.1: {}
|
||||||
|
|
||||||
lodash@4.17.21: {}
|
lodash@4.17.21: {}
|
||||||
|
@ -229,11 +229,13 @@ Create modular components in `ts_web/elements/components/`:
|
|||||||
|
|
||||||
### Phase 6: Optional Enhancements
|
### Phase 6: Optional Enhancements
|
||||||
|
|
||||||
#### 6.1 Authentication (if required)
|
#### 6.1 Authentication ✓ (Implemented)
|
||||||
- [ ] Simple token-based authentication
|
- [x] JWT-based authentication using `@push.rocks/smartjwt`
|
||||||
- [ ] Login component
|
- [x] Guards for identity validation and admin access
|
||||||
- [ ] Protected route handling
|
- [x] Login/logout endpoints following cloudly pattern
|
||||||
- [ ] Session management
|
- [ ] Login component (frontend)
|
||||||
|
- [ ] Protected route handling (frontend)
|
||||||
|
- [ ] Session persistence (frontend)
|
||||||
|
|
||||||
#### 6.2 Real-time Updates (future)
|
#### 6.2 Real-time Updates (future)
|
||||||
- [ ] WebSocket integration for live stats
|
- [ ] WebSocket integration for live stats
|
||||||
@ -304,6 +306,13 @@ Create modular components in `ts_web/elements/components/`:
|
|||||||
- Implemented mock data responses for all endpoints
|
- Implemented mock data responses for all endpoints
|
||||||
- Fixed all TypeScript compilation errors
|
- Fixed all TypeScript compilation errors
|
||||||
- VirtualStream used for log streaming with Uint8Array encoding
|
- VirtualStream used for log streaming with Uint8Array encoding
|
||||||
|
- **JWT Authentication** - Following cloudly pattern:
|
||||||
|
- Added `@push.rocks/smartjwt` and `@push.rocks/smartguard` dependencies
|
||||||
|
- Updated IIdentity interface to match cloudly structure
|
||||||
|
- Implemented JWT-based authentication with RSA keypairs
|
||||||
|
- Created validIdentityGuard and adminIdentityGuard
|
||||||
|
- Added guard helpers for protecting endpoints
|
||||||
|
- Full test coverage for JWT authentication flows
|
||||||
|
|
||||||
### Next Steps
|
### Next Steps
|
||||||
- Phase 3: Frontend State Management - Set up Smartstate
|
- Phase 3: Frontend State Management - Set up Smartstate
|
||||||
|
130
test/test.jwt-auth.ts
Normal file
130
test/test.jwt-auth.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
import { DcRouter } from '../ts/index.js';
|
||||||
|
import { TypedRequest } from '@api.global/typedrequest';
|
||||||
|
import * as interfaces from '../ts_interfaces/index.js';
|
||||||
|
|
||||||
|
let testDcRouter: DcRouter;
|
||||||
|
let identity: interfaces.data.IIdentity;
|
||||||
|
|
||||||
|
tap.test('should start DCRouter with OpsServer', async () => {
|
||||||
|
testDcRouter = new DcRouter({
|
||||||
|
// Minimal config for testing
|
||||||
|
});
|
||||||
|
|
||||||
|
await testDcRouter.start();
|
||||||
|
expect(testDcRouter.opsServer).toBeInstanceOf(Object);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should login with admin credentials and receive JWT', async () => {
|
||||||
|
const loginRequest = new TypedRequest<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
||||||
|
'http://localhost:3000/typedrequest',
|
||||||
|
'adminLoginWithUsernameAndPassword'
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await loginRequest.fire({
|
||||||
|
username: 'admin',
|
||||||
|
password: 'admin'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response).toHaveProperty('identity');
|
||||||
|
expect(response.identity).toHaveProperty('jwt');
|
||||||
|
expect(response.identity).toHaveProperty('userId');
|
||||||
|
expect(response.identity).toHaveProperty('name');
|
||||||
|
expect(response.identity).toHaveProperty('expiresAt');
|
||||||
|
expect(response.identity).toHaveProperty('role');
|
||||||
|
expect(response.identity.role).toEqual('admin');
|
||||||
|
|
||||||
|
identity = response.identity;
|
||||||
|
console.log('JWT:', identity.jwt);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should verify valid JWT identity', async () => {
|
||||||
|
const verifyRequest = new TypedRequest<interfaces.requests.IReq_VerifyIdentity>(
|
||||||
|
'http://localhost:3000/typedrequest',
|
||||||
|
'verifyIdentity'
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await verifyRequest.fire({
|
||||||
|
identity
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response).toHaveProperty('valid');
|
||||||
|
expect(response.valid).toBeTrue();
|
||||||
|
expect(response).toHaveProperty('identity');
|
||||||
|
expect(response.identity.userId).toEqual(identity.userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should reject invalid JWT', async () => {
|
||||||
|
const verifyRequest = new TypedRequest<interfaces.requests.IReq_VerifyIdentity>(
|
||||||
|
'http://localhost:3000/typedrequest',
|
||||||
|
'verifyIdentity'
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await verifyRequest.fire({
|
||||||
|
identity: {
|
||||||
|
...identity,
|
||||||
|
jwt: 'invalid.jwt.token'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response).toHaveProperty('valid');
|
||||||
|
expect(response.valid).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should verify JWT matches identity data', async () => {
|
||||||
|
const verifyRequest = new TypedRequest<interfaces.requests.IReq_VerifyIdentity>(
|
||||||
|
'http://localhost:3000/typedrequest',
|
||||||
|
'verifyIdentity'
|
||||||
|
);
|
||||||
|
|
||||||
|
// The response should contain the same identity data as the JWT
|
||||||
|
const response = await verifyRequest.fire({
|
||||||
|
identity
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response).toHaveProperty('valid');
|
||||||
|
expect(response.valid).toBeTrue();
|
||||||
|
expect(response.identity.expiresAt).toEqual(identity.expiresAt);
|
||||||
|
expect(response.identity.userId).toEqual(identity.userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should handle logout', async () => {
|
||||||
|
const logoutRequest = new TypedRequest<interfaces.requests.IReq_AdminLogout>(
|
||||||
|
'http://localhost:3000/typedrequest',
|
||||||
|
'adminLogout'
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await logoutRequest.fire({
|
||||||
|
identity
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response).toHaveProperty('success');
|
||||||
|
expect(response.success).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should reject wrong credentials', async () => {
|
||||||
|
const loginRequest = new TypedRequest<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
||||||
|
'http://localhost:3000/typedrequest',
|
||||||
|
'adminLoginWithUsernameAndPassword'
|
||||||
|
);
|
||||||
|
|
||||||
|
let errorOccurred = false;
|
||||||
|
try {
|
||||||
|
await loginRequest.fire({
|
||||||
|
username: 'admin',
|
||||||
|
password: 'wrongpassword'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
errorOccurred = true;
|
||||||
|
// TypedResponseError is thrown
|
||||||
|
expect(error).toBeTruthy();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(errorOccurred).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should stop DCRouter', async () => {
|
||||||
|
await testDcRouter.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
115
test/test.protected-endpoint.ts
Normal file
115
test/test.protected-endpoint.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
import { DcRouter } from '../ts/index.js';
|
||||||
|
import { TypedRequest } from '@api.global/typedrequest';
|
||||||
|
import * as interfaces from '../ts_interfaces/index.js';
|
||||||
|
|
||||||
|
let testDcRouter: DcRouter;
|
||||||
|
let adminIdentity: interfaces.data.IIdentity;
|
||||||
|
|
||||||
|
tap.test('should start DCRouter with OpsServer', async () => {
|
||||||
|
testDcRouter = new DcRouter({
|
||||||
|
// Minimal config for testing
|
||||||
|
});
|
||||||
|
|
||||||
|
await testDcRouter.start();
|
||||||
|
expect(testDcRouter.opsServer).toBeInstanceOf(Object);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should login as admin', async () => {
|
||||||
|
const loginRequest = new TypedRequest<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
||||||
|
'http://localhost:3000/typedrequest',
|
||||||
|
'adminLoginWithUsernameAndPassword'
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await loginRequest.fire({
|
||||||
|
username: 'admin',
|
||||||
|
password: 'admin'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response).toHaveProperty('identity');
|
||||||
|
adminIdentity = response.identity;
|
||||||
|
console.log('Admin logged in with JWT');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should allow admin to update configuration', async () => {
|
||||||
|
const updateRequest = new TypedRequest<interfaces.requests.IReq_UpdateConfiguration>(
|
||||||
|
'http://localhost:3000/typedrequest',
|
||||||
|
'updateConfiguration'
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await updateRequest.fire({
|
||||||
|
identity: adminIdentity,
|
||||||
|
section: 'security',
|
||||||
|
config: {
|
||||||
|
rateLimit: true,
|
||||||
|
spamDetection: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response).toHaveProperty('updated');
|
||||||
|
expect(response.updated).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should reject configuration update without identity', async () => {
|
||||||
|
const updateRequest = new TypedRequest<interfaces.requests.IReq_UpdateConfiguration>(
|
||||||
|
'http://localhost:3000/typedrequest',
|
||||||
|
'updateConfiguration'
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateRequest.fire({
|
||||||
|
section: 'security',
|
||||||
|
config: {
|
||||||
|
rateLimit: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(true).toBeFalse(); // Should not reach here
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toBeTruthy();
|
||||||
|
console.log('Successfully rejected request without identity');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should reject configuration update with invalid JWT', async () => {
|
||||||
|
const updateRequest = new TypedRequest<interfaces.requests.IReq_UpdateConfiguration>(
|
||||||
|
'http://localhost:3000/typedrequest',
|
||||||
|
'updateConfiguration'
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateRequest.fire({
|
||||||
|
identity: {
|
||||||
|
...adminIdentity,
|
||||||
|
jwt: 'invalid.jwt.token'
|
||||||
|
},
|
||||||
|
section: 'security',
|
||||||
|
config: {
|
||||||
|
rateLimit: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(true).toBeFalse(); // Should not reach here
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toBeTruthy();
|
||||||
|
console.log('Successfully rejected request with invalid JWT');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should allow access to public endpoints without auth', async () => {
|
||||||
|
const healthRequest = new TypedRequest<interfaces.requests.IReq_GetHealthStatus>(
|
||||||
|
'http://localhost:3000/typedrequest',
|
||||||
|
'getHealthStatus'
|
||||||
|
);
|
||||||
|
|
||||||
|
// No identity provided
|
||||||
|
const response = await healthRequest.fire({});
|
||||||
|
|
||||||
|
expect(response).toHaveProperty('health');
|
||||||
|
expect(response.health.healthy).toBeTrue();
|
||||||
|
console.log('Public endpoint accessible without auth');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should stop DCRouter', async () => {
|
||||||
|
await testDcRouter.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
@ -11,7 +11,7 @@ export class OpsServer {
|
|||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
// Handler instances
|
// Handler instances
|
||||||
private adminHandler: handlers.AdminHandler;
|
public adminHandler: handlers.AdminHandler;
|
||||||
private configHandler: handlers.ConfigHandler;
|
private configHandler: handlers.ConfigHandler;
|
||||||
private logsHandler: handlers.LogsHandler;
|
private logsHandler: handlers.LogsHandler;
|
||||||
private securityHandler: handlers.SecurityHandler;
|
private securityHandler: handlers.SecurityHandler;
|
||||||
@ -36,7 +36,7 @@ export class OpsServer {
|
|||||||
this.server.typedrouter.addTypedRouter(this.dcRouterRef.typedrouter);
|
this.server.typedrouter.addTypedRouter(this.dcRouterRef.typedrouter);
|
||||||
|
|
||||||
// Set up handlers
|
// Set up handlers
|
||||||
this.setupHandlers();
|
await this.setupHandlers();
|
||||||
|
|
||||||
await this.server.start(3000);
|
await this.server.start(3000);
|
||||||
}
|
}
|
||||||
@ -44,9 +44,11 @@ export class OpsServer {
|
|||||||
/**
|
/**
|
||||||
* Set up all TypedRequest handlers
|
* Set up all TypedRequest handlers
|
||||||
*/
|
*/
|
||||||
private setupHandlers(): void {
|
private async setupHandlers(): Promise<void> {
|
||||||
// Instantiate all handlers - they self-register with the typedrouter
|
// Instantiate all handlers - they self-register with the typedrouter
|
||||||
this.adminHandler = new handlers.AdminHandler(this);
|
this.adminHandler = new handlers.AdminHandler(this);
|
||||||
|
await this.adminHandler.initialize(); // JWT needs async initialization
|
||||||
|
|
||||||
this.configHandler = new handlers.ConfigHandler(this);
|
this.configHandler = new handlers.ConfigHandler(this);
|
||||||
this.logsHandler = new handlers.LogsHandler(this);
|
this.logsHandler = new handlers.LogsHandler(this);
|
||||||
this.securityHandler = new handlers.SecurityHandler(this);
|
this.securityHandler = new handlers.SecurityHandler(this);
|
||||||
|
@ -2,57 +2,100 @@ import * as plugins from '../../plugins.js';
|
|||||||
import type { OpsServer } from '../classes.opsserver.js';
|
import type { OpsServer } from '../classes.opsserver.js';
|
||||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||||
|
|
||||||
|
export interface IJwtData {
|
||||||
|
userId: string;
|
||||||
|
status: 'loggedIn' | 'loggedOut';
|
||||||
|
expiresAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class AdminHandler {
|
export class AdminHandler {
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
// Simple in-memory session storage (in production, use proper session management)
|
// JWT instance
|
||||||
private sessions = new Map<string, {
|
public smartjwtInstance: plugins.smartjwt.SmartJwt<IJwtData>;
|
||||||
identity: interfaces.data.IIdentity;
|
|
||||||
createdAt: number;
|
// Simple in-memory user storage (in production, use proper database)
|
||||||
lastAccess: number;
|
private users = new Map<string, {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
role: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
constructor(private opsServerRef: OpsServer) {
|
constructor(private opsServerRef: OpsServer) {
|
||||||
// Add this handler's router to the parent
|
// Add this handler's router to the parent
|
||||||
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async initialize(): Promise<void> {
|
||||||
|
await this.initializeJwt();
|
||||||
|
this.initializeDefaultUsers();
|
||||||
this.registerHandlers();
|
this.registerHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async initializeJwt(): Promise<void> {
|
||||||
|
this.smartjwtInstance = new plugins.smartjwt.SmartJwt();
|
||||||
|
await this.smartjwtInstance.init();
|
||||||
|
|
||||||
|
// For development, create new keypair each time
|
||||||
|
// In production, load from storage like cloudly does
|
||||||
|
await this.smartjwtInstance.createNewKeyPair();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeDefaultUsers(): void {
|
||||||
|
// Add default admin user
|
||||||
|
const adminId = plugins.uuid.v4();
|
||||||
|
this.users.set(adminId, {
|
||||||
|
id: adminId,
|
||||||
|
username: 'admin',
|
||||||
|
password: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private registerHandlers(): void {
|
private registerHandlers(): void {
|
||||||
// Admin Login Handler
|
// Admin Login Handler
|
||||||
this.typedrouter.addTypedHandler(
|
this.typedrouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
||||||
'adminLoginWithUsernameAndPassword',
|
'adminLoginWithUsernameAndPassword',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg) => {
|
||||||
try {
|
try {
|
||||||
// TODO: Implement proper authentication
|
// Find user by username and password
|
||||||
// For now, use a simple hardcoded check
|
let user: { id: string; username: string; password: string; role: string } | null = null;
|
||||||
if (dataArg.username === 'admin' && dataArg.password === 'admin') {
|
for (const [_, userData] of this.users) {
|
||||||
const token = plugins.uuid.v4();
|
if (userData.username === dataArg.username && userData.password === dataArg.password) {
|
||||||
const identity: interfaces.data.IIdentity = {
|
user = userData;
|
||||||
token,
|
break;
|
||||||
expiresAt: Date.now() + 24 * 60 * 60 * 1000, // 24 hours
|
}
|
||||||
permissions: ['admin'],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store session
|
|
||||||
this.sessions.set(token, {
|
|
||||||
identity,
|
|
||||||
createdAt: Date.now(),
|
|
||||||
lastAccess: Date.now(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clean up old sessions
|
|
||||||
this.cleanupSessions();
|
|
||||||
|
|
||||||
return {
|
|
||||||
identity,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('login failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const expiresAtTimestamp = Date.now() + 3600 * 1000 * 24 * 7; // 7 days
|
||||||
|
|
||||||
|
const jwt = await this.smartjwtInstance.createJWT({
|
||||||
|
userId: user.id,
|
||||||
|
status: 'loggedIn',
|
||||||
|
expiresAt: expiresAtTimestamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
identity: {
|
||||||
|
jwt,
|
||||||
|
userId: user.id,
|
||||||
|
name: user.username,
|
||||||
|
expiresAt: expiresAtTimestamp,
|
||||||
|
role: user.role,
|
||||||
|
type: 'user',
|
||||||
|
},
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {};
|
if (error instanceof plugins.typedrequest.TypedResponseError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('login failed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -62,17 +105,12 @@ export class AdminHandler {
|
|||||||
this.typedrouter.addTypedHandler(
|
this.typedrouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_AdminLogout>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_AdminLogout>(
|
||||||
'adminLogout',
|
'adminLogout',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg) => {
|
||||||
if (dataArg.identity?.token && this.sessions.has(dataArg.identity.token)) {
|
// In a real implementation, you might want to blacklist the JWT
|
||||||
this.sessions.delete(dataArg.identity.token);
|
// For now, just return success
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -81,27 +119,50 @@ export class AdminHandler {
|
|||||||
this.typedrouter.addTypedHandler(
|
this.typedrouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_VerifyIdentity>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_VerifyIdentity>(
|
||||||
'verifyIdentity',
|
'verifyIdentity',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg) => {
|
||||||
if (!dataArg.identity?.token) {
|
if (!dataArg.identity?.jwt) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = this.sessions.get(dataArg.identity.token);
|
try {
|
||||||
if (session && session.identity.expiresAt > Date.now()) {
|
const jwtData = await this.smartjwtInstance.verifyJWTAndGetData(dataArg.identity.jwt);
|
||||||
// Update last access
|
|
||||||
session.lastAccess = Date.now();
|
// Check if expired
|
||||||
|
if (jwtData.expiresAt < Date.now()) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if logged in
|
||||||
|
if (jwtData.status !== 'loggedIn') {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find user
|
||||||
|
const user = this.users.get(jwtData.userId);
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
valid: true,
|
valid: true,
|
||||||
identity: session.identity,
|
identity: {
|
||||||
|
jwt: dataArg.identity.jwt,
|
||||||
|
userId: user.id,
|
||||||
|
name: user.username,
|
||||||
|
expiresAt: jwtData.expiresAt,
|
||||||
|
role: user.role,
|
||||||
|
type: 'user',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
} else {
|
} catch (error) {
|
||||||
// Clean up expired session
|
|
||||||
if (session) {
|
|
||||||
this.sessions.delete(dataArg.identity.token);
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
};
|
};
|
||||||
@ -112,37 +173,68 @@ export class AdminHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up expired sessions (older than 24 hours)
|
* Create a guard for valid identity (matching cloudly pattern)
|
||||||
*/
|
*/
|
||||||
private cleanupSessions(): void {
|
public validIdentityGuard = new plugins.smartguard.Guard<{
|
||||||
const now = Date.now();
|
identity: interfaces.data.IIdentity;
|
||||||
const maxAge = 24 * 60 * 60 * 1000; // 24 hours
|
}>(
|
||||||
|
async (dataArg) => {
|
||||||
for (const [token, session] of this.sessions.entries()) {
|
if (!dataArg.identity?.jwt) {
|
||||||
if (now - session.lastAccess > maxAge) {
|
|
||||||
this.sessions.delete(token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a guard for authentication
|
|
||||||
* This can be used by other handlers to protect endpoints
|
|
||||||
*/
|
|
||||||
public createAuthGuard() {
|
|
||||||
return async (dataArg: { identity?: interfaces.data.IIdentity }) => {
|
|
||||||
if (!dataArg.identity?.token) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = this.sessions.get(dataArg.identity.token);
|
try {
|
||||||
if (session && session.identity.expiresAt > Date.now()) {
|
const jwtData = await this.smartjwtInstance.verifyJWTAndGetData(dataArg.identity.jwt);
|
||||||
// Update last access
|
|
||||||
session.lastAccess = Date.now();
|
// Check expiration
|
||||||
|
if (jwtData.expiresAt < Date.now()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check status
|
||||||
|
if (jwtData.status !== 'loggedIn') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify data hasn't been tampered with
|
||||||
|
if (dataArg.identity.expiresAt !== jwtData.expiresAt) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataArg.identity.userId !== jwtData.userId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
failedHint: 'identity is not valid',
|
||||||
|
name: 'validIdentityGuard',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a guard for admin identity (matching cloudly pattern)
|
||||||
|
*/
|
||||||
|
public adminIdentityGuard = new plugins.smartguard.Guard<{
|
||||||
|
identity: interfaces.data.IIdentity;
|
||||||
|
}>(
|
||||||
|
async (dataArg) => {
|
||||||
|
// First check if identity is valid
|
||||||
|
const isValid = await this.validIdentityGuard.exec(dataArg);
|
||||||
|
if (!isValid) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
// Check if user has admin role
|
||||||
};
|
return dataArg.identity.role === 'admin';
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
failedHint: 'user is not admin',
|
||||||
|
name: 'adminIdentityGuard',
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import type { OpsServer } from '../classes.opsserver.js';
|
import type { OpsServer } from '../classes.opsserver.js';
|
||||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||||
|
import { requireAdminIdentity } from '../helpers/guards.js';
|
||||||
|
|
||||||
export class ConfigHandler {
|
export class ConfigHandler {
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
@ -32,12 +33,18 @@ export class ConfigHandler {
|
|||||||
'updateConfiguration',
|
'updateConfiguration',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
try {
|
try {
|
||||||
|
// Require admin access to update configuration
|
||||||
|
await requireAdminIdentity(this.opsServerRef.adminHandler, dataArg);
|
||||||
|
|
||||||
const updatedConfig = await this.updateConfiguration(dataArg.section, dataArg.config);
|
const updatedConfig = await this.updateConfiguration(dataArg.section, dataArg.config);
|
||||||
return {
|
return {
|
||||||
updated: true,
|
updated: true,
|
||||||
config: updatedConfig,
|
config: updatedConfig,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof plugins.typedrequest.TypedResponseError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
updated: false,
|
updated: false,
|
||||||
config: null,
|
config: null,
|
||||||
|
56
ts/opsserver/helpers/guards.ts
Normal file
56
ts/opsserver/helpers/guards.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import * as plugins from '../../plugins.js';
|
||||||
|
import type { AdminHandler } from '../handlers/admin.handler.js';
|
||||||
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to use identity guards in handlers
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // In a handler:
|
||||||
|
* await passGuards(toolsArg, this.opsServerRef.adminHandler.validIdentityGuard, dataArg);
|
||||||
|
*/
|
||||||
|
export async function passGuards<T extends { identity?: any }>(
|
||||||
|
toolsArg: any,
|
||||||
|
guard: plugins.smartguard.Guard<T>,
|
||||||
|
dataArg: T
|
||||||
|
): Promise<void> {
|
||||||
|
const result = await guard.exec(dataArg);
|
||||||
|
if (!result) {
|
||||||
|
const failedHint = await guard.getFailedHint(dataArg);
|
||||||
|
throw new plugins.typedrequest.TypedResponseError(failedHint || 'Guard check failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to check admin identity in handlers
|
||||||
|
*/
|
||||||
|
export async function requireAdminIdentity<T extends { identity?: interfaces.data.IIdentity }>(
|
||||||
|
adminHandler: AdminHandler,
|
||||||
|
dataArg: T
|
||||||
|
): Promise<void> {
|
||||||
|
if (!dataArg.identity) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('No identity provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
const passed = await adminHandler.adminIdentityGuard.exec({ identity: dataArg.identity });
|
||||||
|
if (!passed) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('Admin access required');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to check valid identity in handlers
|
||||||
|
*/
|
||||||
|
export async function requireValidIdentity<T extends { identity?: interfaces.data.IIdentity }>(
|
||||||
|
adminHandler: AdminHandler,
|
||||||
|
dataArg: T
|
||||||
|
): Promise<void> {
|
||||||
|
if (!dataArg.identity) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('No identity provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
const passed = await adminHandler.validIdentityGuard.exec({ identity: dataArg.identity });
|
||||||
|
if (!passed) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('Valid identity required');
|
||||||
|
}
|
||||||
|
}
|
@ -46,6 +46,8 @@ import * as smartacme from '@push.rocks/smartacme';
|
|||||||
import * as smartdata from '@push.rocks/smartdata';
|
import * as smartdata from '@push.rocks/smartdata';
|
||||||
import * as smartdns from '@push.rocks/smartdns';
|
import * as smartdns from '@push.rocks/smartdns';
|
||||||
import * as smartfile from '@push.rocks/smartfile';
|
import * as smartfile from '@push.rocks/smartfile';
|
||||||
|
import * as smartguard from '@push.rocks/smartguard';
|
||||||
|
import * as smartjwt from '@push.rocks/smartjwt';
|
||||||
import * as smartlog from '@push.rocks/smartlog';
|
import * as smartlog from '@push.rocks/smartlog';
|
||||||
import * as smartmail from '@push.rocks/smartmail';
|
import * as smartmail from '@push.rocks/smartmail';
|
||||||
import * as smartnetwork from '@push.rocks/smartnetwork';
|
import * as smartnetwork from '@push.rocks/smartnetwork';
|
||||||
@ -55,8 +57,9 @@ import * as smartpromise from '@push.rocks/smartpromise';
|
|||||||
import * as smartrequest from '@push.rocks/smartrequest';
|
import * as smartrequest from '@push.rocks/smartrequest';
|
||||||
import * as smartrule from '@push.rocks/smartrule';
|
import * as smartrule from '@push.rocks/smartrule';
|
||||||
import * as smartrx from '@push.rocks/smartrx';
|
import * as smartrx from '@push.rocks/smartrx';
|
||||||
|
import * as smartunique from '@push.rocks/smartunique';
|
||||||
|
|
||||||
export { projectinfo, qenv, smartacme, smartdata, smartdns, smartfile, smartlog, smartmail, smartnetwork, smartpath, smartproxy, smartpromise, smartrequest, smartrule, smartrx };
|
export { projectinfo, qenv, smartacme, smartdata, smartdns, smartfile, smartguard, smartjwt, smartlog, smartmail, smartnetwork, smartpath, smartproxy, smartpromise, smartrequest, smartrule, smartrx, smartunique };
|
||||||
|
|
||||||
// Define SmartLog types for use in error handling
|
// Define SmartLog types for use in error handling
|
||||||
export type TLogLevel = 'error' | 'warn' | 'info' | 'success' | 'debug';
|
export type TLogLevel = 'error' | 'warn' | 'info' | 'success' | 'debug';
|
||||||
|
Reference in New Issue
Block a user